all: resync with master

This commit is contained in:
Ainar Garipov 2024-09-30 20:17:20 +03:00
parent c7d8b9ede1
commit 8cb5781770
153 changed files with 28633 additions and 27594 deletions

View file

@ -1,7 +1,7 @@
'name': 'build' 'name': 'build'
'env': 'env':
'GO_VERSION': '1.22.5' 'GO_VERSION': '1.23.1'
'NODE_VERSION': '16' 'NODE_VERSION': '16'
'on': 'on':

View file

@ -1,7 +1,7 @@
'name': 'lint' 'name': 'lint'
'env': 'env':
'GO_VERSION': '1.22.5' 'GO_VERSION': '1.23.1'
'on': 'on':
'push': 'push':

View file

@ -27,6 +27,37 @@ See also the [v0.107.53 GitHub milestone][ms-v0.107.53].
NOTE: Add new changes BELOW THIS COMMENT. NOTE: Add new changes BELOW THIS COMMENT.
--> -->
### Security
- Go version has been updated to prevent the possibility of exploiting the Go
vulnerabilities fixed in [1.23.1][go-1.23.1].
### Added
- Support for 64-bit RISC-V architecture ([#5704]).
- Ecosia search engine is now supported in safe search ([#5009]).
### Changed
- Upstream server URL domain names requirements has been relaxed and now follow
the same rules as their domain specifications.
### Fixed
- Property `clients.runtime_sources.dhcp` in the configuration file not taking
effect.
- Update Google safe search domains list ([#7155]).
- Enforce Bing safe search from Edge sidebar ([#7154]).
- Text overflow on the query log page ([#7119]).
[#5009]: https://github.com/AdguardTeam/AdGuardHome/issues/5009
[#5704]: https://github.com/AdguardTeam/AdGuardHome/issues/5704
[#7119]: https://github.com/AdguardTeam/AdGuardHome/issues/7119
[#7154]: https://github.com/AdguardTeam/AdGuardHome/pull/7154
[#7155]: https://github.com/AdguardTeam/AdGuardHome/pull/7155
[go-1.23.1]: https://groups.google.com/g/golang-announce/c/K-cEzDeCtpc
<!-- <!--
NOTE: Add new changes ABOVE THIS COMMENT. NOTE: Add new changes ABOVE THIS COMMENT.
--> -->

View file

@ -8,7 +8,7 @@
# Makefile. Bump this number every time a significant change is made to # Makefile. Bump this number every time a significant change is made to
# this Makefile. # this Makefile.
# #
# AdGuard-Project-Version: 4 # AdGuard-Project-Version: 6
# Don't name these macros "GO" etc., because GNU Make apparently makes # Don't name these macros "GO" etc., because GNU Make apparently makes
# them exported environment variables with the literal value of # them exported environment variables with the literal value of
@ -23,11 +23,13 @@ VERBOSE.MACRO = $${VERBOSE:-0}
CHANNEL = development CHANNEL = development
CLIENT_DIR = client CLIENT_DIR = client
COMMIT = $$( git rev-parse --short HEAD ) COMMIT = $$( git rev-parse --short HEAD )
DEPLOY_SCRIPT_PATH = not/a/real/path
DIST_DIR = dist DIST_DIR = dist
GOAMD64 = v1 GOAMD64 = v1
GOPROXY = https://goproxy.cn|https://proxy.golang.org|direct GOPROXY = https://proxy.golang.org|direct
GOSUMDB = sum.golang.google.cn GOSUMDB = sum.golang.google.cn
GOTOOLCHAIN = go1.22.5 GOTOOLCHAIN = go1.23.1
GOTELEMETRY = off
GPG_KEY = devteam@adguard.com GPG_KEY = devteam@adguard.com
GPG_KEY_PASSPHRASE = not-a-real-password GPG_KEY_PASSPHRASE = not-a-real-password
NPM = npm NPM = npm
@ -36,6 +38,7 @@ NPM_INSTALL_FLAGS = $(NPM_FLAGS) --quiet --no-progress --ignore-engines\
--ignore-optional --ignore-platform --ignore-scripts --ignore-optional --ignore-platform --ignore-scripts
RACE = 0 RACE = 0
SIGN = 1 SIGN = 1
SIGNER_API_KEY = not-a-real-key
VERSION = v0.0.0 VERSION = v0.0.0
YARN = yarn YARN = yarn
@ -59,20 +62,28 @@ BUILD_RELEASE_DEPS_1 = go-deps
ENV = env\ ENV = env\
CHANNEL='$(CHANNEL)'\ CHANNEL='$(CHANNEL)'\
COMMIT='$(COMMIT)'\ COMMIT='$(COMMIT)'\
DEPLOY_SCRIPT_PATH='$(DEPLOY_SCRIPT_PATH)' \
DIST_DIR='$(DIST_DIR)'\ DIST_DIR='$(DIST_DIR)'\
GO="$(GO.MACRO)"\ GO="$(GO.MACRO)"\
GOAMD64="$(GOAMD64)"\ GOAMD64='$(GOAMD64)'\
GOPROXY='$(GOPROXY)'\ GOPROXY='$(GOPROXY)'\
GOSUMDB='$(GOSUMDB)'\ GOSUMDB='$(GOSUMDB)'\
GOTELEMETRY='$(GOTELEMETRY)'\
GOTOOLCHAIN='$(GOTOOLCHAIN)'\ GOTOOLCHAIN='$(GOTOOLCHAIN)'\
GPG_KEY='$(GPG_KEY)'\ GPG_KEY='$(GPG_KEY)'\
GPG_KEY_PASSPHRASE='$(GPG_KEY_PASSPHRASE)'\ GPG_KEY_PASSPHRASE='$(GPG_KEY_PASSPHRASE)'\
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\ PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
RACE='$(RACE)'\ RACE='$(RACE)'\
SIGN='$(SIGN)'\ SIGN='$(SIGN)'\
SIGNER_API_KEY='$(SIGNER_API_KEY)' \
NEXTAPI='$(NEXTAPI)'\ NEXTAPI='$(NEXTAPI)'\
VERBOSE="$(VERBOSE.MACRO)"\ VERBOSE="$(VERBOSE.MACRO)"\
VERSION='$(VERSION)'\ VERSION="$(VERSION)"\
# Keep the line above blank.
ENV_MISC = env\
VERBOSE="$(VERBOSE.MACRO)"\
# Keep the line above blank. # Keep the line above blank.
@ -101,23 +112,22 @@ js-deps: ; $(NPM) $(NPM_INSTALL_FLAGS) ci
js-lint: ; $(NPM) $(NPM_FLAGS) run lint js-lint: ; $(NPM) $(NPM_FLAGS) run lint
js-test: ; $(NPM) $(NPM_FLAGS) run test js-test: ; $(NPM) $(NPM_FLAGS) run test
go-bench: ; $(ENV) "$(SHELL)" ./scripts/make/go-bench.sh go-bench: ; $(ENV) "$(SHELL)" ./scripts/make/go-bench.sh
go-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh go-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh
go-deps: ; $(ENV) "$(SHELL)" ./scripts/make/go-deps.sh go-deps: ; $(ENV) "$(SHELL)" ./scripts/make/go-deps.sh
go-fuzz: ; $(ENV) "$(SHELL)" ./scripts/make/go-fuzz.sh go-env: ; $(ENV) "$(GO.MACRO)" env
go-lint: ; $(ENV) "$(SHELL)" ./scripts/make/go-lint.sh go-fuzz: ; $(ENV) "$(SHELL)" ./scripts/make/go-fuzz.sh
go-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-tools.sh go-lint: ; $(ENV) "$(SHELL)" ./scripts/make/go-lint.sh
# TODO(a.garipov): Think about making RACE='1' the default for all # TODO(a.garipov): Think about making RACE='1' the default for all
# targets. # targets.
go-test: ; $(ENV) RACE='1' "$(SHELL)" ./scripts/make/go-test.sh go-test: ; $(ENV) RACE='1' "$(SHELL)" ./scripts/make/go-test.sh
go-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-tools.sh
go-upd-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-upd-tools.sh go-upd-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-upd-tools.sh
go-check: go-tools go-lint go-test go-check: go-tools go-lint go-test
# A quick check to make sure that all supported operating systems can be # A quick check to make sure that all operating systems relevant to the
# typechecked and built successfully. # development of the project can be typechecked and built successfully.
go-os-check: go-os-check:
env GOOS='darwin' "$(GO.MACRO)" vet ./internal/... env GOOS='darwin' "$(GO.MACRO)" vet ./internal/...
env GOOS='freebsd' "$(GO.MACRO)" vet ./internal/... env GOOS='freebsd' "$(GO.MACRO)" vet ./internal/...
@ -125,7 +135,11 @@ go-os-check:
env GOOS='linux' "$(GO.MACRO)" vet ./internal/... env GOOS='linux' "$(GO.MACRO)" vet ./internal/...
env GOOS='windows' "$(GO.MACRO)" vet ./internal/... env GOOS='windows' "$(GO.MACRO)" vet ./internal/...
openapi-lint: ; cd ./openapi/ && $(YARN) test openapi-lint: ; cd ./openapi/ && $(YARN) test
openapi-show: ; cd ./openapi/ && $(YARN) start openapi-show: ; cd ./openapi/ && $(YARN) start
txt-lint: ; $(ENV) "$(SHELL)" ./scripts/make/txt-lint.sh 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

View file

@ -205,7 +205,7 @@ Run `make init` to prepare the development environment.
You will need this to build AdGuard Home: You will need this to build AdGuard Home:
- [Go](https://golang.org/dl/) v1.22 or later; - [Go](https://golang.org/dl/) v1.23 or later;
- [Node.js](https://nodejs.org/en/download/) v18.18 or later; - [Node.js](https://nodejs.org/en/download/) v18.18 or later;
- [npm](https://www.npmjs.com/) v8 or later; - [npm](https://www.npmjs.com/) v8 or later;

View file

@ -8,7 +8,7 @@
'variables': 'variables':
'channel': 'edge' 'channel': 'edge'
'dockerFrontend': 'adguard/home-js-builder:2.0' 'dockerFrontend': 'adguard/home-js-builder:2.0'
'dockerGo': 'adguard/go-builder:1.22.5--1' 'dockerGo': 'adguard/go-builder:1.23.1--1'
'stages': 'stages':
- 'Build frontend': - 'Build frontend':
@ -91,6 +91,11 @@
'tasks': 'tasks':
- 'checkout': - 'checkout':
'force-clean-build': true 'force-clean-build': true
- 'checkout':
'repository': 'bamboo-deploy-publisher'
# The paths are always relative to the working directory.
'path': 'bamboo-deploy-publisher'
'force-clean-build': true
- 'script': - 'script':
'interpreter': 'SHELL' 'interpreter': 'SHELL'
'scripts': 'scripts':
@ -99,6 +104,9 @@
set -e -f -u -x set -e -f -u -x
# Explicitly checkout the revision that we need.
git checkout "${bamboo.repository.revision.number}"
# Run the build with the specified channel. # Run the build with the specified channel.
echo "${bamboo.gpgSecretKeyPart1}${bamboo.gpgSecretKeyPart2}"\ echo "${bamboo.gpgSecretKeyPart1}${bamboo.gpgSecretKeyPart2}"\
| awk '{ gsub(/\\n/, "\n"); print; }'\ | awk '{ gsub(/\\n/, "\n"); print; }'\
@ -107,6 +115,8 @@
make\ make\
CHANNEL=${bamboo.channel}\ CHANNEL=${bamboo.channel}\
GPG_KEY_PASSPHRASE=${bamboo.gpgPassword}\ GPG_KEY_PASSPHRASE=${bamboo.gpgPassword}\
DEPLOY_SCRIPT_PATH="./bamboo-deploy-publisher/deploy.sh"\
SIGNER_API_KEY="${bamboo.adguardHomeWinSignerSecretApiKey}"\
FRONTEND_PREBUILT=1\ FRONTEND_PREBUILT=1\
PARALLELISM=1\ PARALLELISM=1\
VERBOSE=2\ VERBOSE=2\
@ -266,7 +276,7 @@
'variables': 'variables':
'channel': 'beta' 'channel': 'beta'
'dockerFrontend': 'adguard/home-js-builder:2.0' 'dockerFrontend': 'adguard/home-js-builder:2.0'
'dockerGo': 'adguard/go-builder:1.22.5--1' 'dockerGo': 'adguard/go-builder:1.23.1--1'
# release-vX.Y.Z branches are the branches from which the actual final # release-vX.Y.Z branches are the branches from which the actual final
# release is built. # release is built.
- '^release-v[0-9]+\.[0-9]+\.[0-9]+': - '^release-v[0-9]+\.[0-9]+\.[0-9]+':
@ -282,4 +292,4 @@
'variables': 'variables':
'channel': 'release' 'channel': 'release'
'dockerFrontend': 'adguard/home-js-builder:2.0' 'dockerFrontend': 'adguard/home-js-builder:2.0'
'dockerGo': 'adguard/go-builder:1.22.5--1' 'dockerGo': 'adguard/go-builder:1.23.1--1'

View file

@ -6,7 +6,7 @@
'name': 'AdGuard Home - Build and run tests' 'name': 'AdGuard Home - Build and run tests'
'variables': 'variables':
'dockerFrontend': 'adguard/home-js-builder:2.0' 'dockerFrontend': 'adguard/home-js-builder:2.0'
'dockerGo': 'adguard/go-builder:1.22.5--1' 'dockerGo': 'adguard/go-builder:1.23.1--1'
'channel': 'development' 'channel': 'development'
'stages': 'stages':
@ -54,6 +54,7 @@
'requirements': 'requirements':
- 'adg-docker': 'true' - 'adg-docker': 'true'
# TODO(e.burkov): Add the linting stage for markdown docs and shell scripts.
'Test backend': 'Test backend':
'docker': 'docker':
'image': '${bamboo.dockerGo}' 'image': '${bamboo.dockerGo}'
@ -195,5 +196,5 @@
# may need to build a few of these. # may need to build a few of these.
'variables': 'variables':
'dockerFrontend': 'adguard/home-js-builder:2.0' 'dockerFrontend': 'adguard/home-js-builder:2.0'
'dockerGo': 'adguard/go-builder:1.22.5--1' 'dockerGo': 'adguard/go-builder:1.23.1--1'
'channel': 'candidate' 'channel': 'candidate'

View file

@ -291,7 +291,7 @@
"custom_ip": "عنوان IP مخصص", "custom_ip": "عنوان IP مخصص",
"blocking_ipv4": "حجب عنوان IPv4", "blocking_ipv4": "حجب عنوان IPv4",
"blocking_ipv6": "حجب عنوان IPv6", "blocking_ipv6": "حجب عنوان IPv6",
"blocked_response_ttl": "زمن حظر الاستجابة", "blocked_response_ttl": "حظر استجابة TTL",
"blocked_response_ttl_desc": "تحديد عدد الثواني التي يجب على العملاء تخزين الاستجابة التي تمت تصفيتها مؤقتًا", "blocked_response_ttl_desc": "تحديد عدد الثواني التي يجب على العملاء تخزين الاستجابة التي تمت تصفيتها مؤقتًا",
"form_enter_blocked_response_ttl": "أدخل وقت الاستجابة المحظورة TTL (بالثواني)", "form_enter_blocked_response_ttl": "أدخل وقت الاستجابة المحظورة TTL (بالثواني)",
"dnscrypt": "DNSCrypt", "dnscrypt": "DNSCrypt",
@ -734,10 +734,10 @@
"thursday": "الخميس", "thursday": "الخميس",
"friday": "الجمعة", "friday": "الجمعة",
"saturday": "السبت", "saturday": "السبت",
"sunday_short": "الاحد", "sunday_short": "الأحد",
"monday_short": "الإثنين", "monday_short": "الإثنين",
"tuesday_short": "الثلاثاء", "tuesday_short": "الثلاثاء",
"wednesday_short": "الاربعاء", "wednesday_short": "الأربعاء",
"thursday_short": "الخميس", "thursday_short": "الخميس",
"friday_short": "الجمعة", "friday_short": "الجمعة",
"saturday_short": "السبت", "saturday_short": "السبت",

View file

@ -159,6 +159,8 @@
"dns_over_https": "DNS-пред-HTTPS", "dns_over_https": "DNS-пред-HTTPS",
"dns_over_quic": "DNS-over-QUIC", "dns_over_quic": "DNS-over-QUIC",
"plain_dns": "Обикновен DNS", "plain_dns": "Обикновен DNS",
"theme_light": "Светла тема",
"theme_dark": "Тъмна тема",
"source_label": "Източник", "source_label": "Източник",
"found_in_known_domain_db": "Намерен в списъците с домейни.", "found_in_known_domain_db": "Намерен в списъците с домейни.",
"category_label": "Категория", "category_label": "Категория",
@ -283,5 +285,12 @@
"filter_category_general": "General", "filter_category_general": "General",
"filter_category_security": "Сигурност", "filter_category_security": "Сигурност",
"port_53_faq_link": "Порт 53 често е зает от \"DNSStubListener\" или \"systemd-resolved\" услуги. Моля, прочетете <0>тази инструкция</0> как да решите това.", "port_53_faq_link": "Порт 53 често е зает от \"DNSStubListener\" или \"systemd-resolved\" услуги. Моля, прочетете <0>тази инструкция</0> как да решите това.",
"parental_control": "Родителски контрол" "parental_control": "Родителски контрол",
"sunday_short": "Нд",
"monday_short": "Пон",
"tuesday_short": "Вт",
"wednesday_short": "Ср",
"thursday_short": "Чт",
"friday_short": "Пт",
"saturday_short": "Съб"
} }

View file

@ -154,7 +154,7 @@
"use_adguard_parental": "Použít službu AdGuard Rodičovská kontrola", "use_adguard_parental": "Použít službu AdGuard Rodičovská kontrola",
"use_adguard_parental_hint": "AdGuard Home zkontroluje, zda doména obsahuje materiály pro dospělé. Používá stejné API přátelské k ochraně osobních údajů jako služba Bezpečnost prohlížení.", "use_adguard_parental_hint": "AdGuard Home zkontroluje, zda doména obsahuje materiály pro dospělé. Používá stejné API přátelské k ochraně osobních údajů jako služba Bezpečnost prohlížení.",
"enforce_safe_search": "Použít bezpečné vyhledávání", "enforce_safe_search": "Použít bezpečné vyhledávání",
"enforce_save_search_hint": "AdGuard Home vynutí bezpečné vyhledávání v následujících vyhledávačích: Google, YouTube, Bing, DuckDuckGo, Yandex, Pixabay.", "enforce_save_search_hint": "AdGuard Home vynutí bezpečné vyhledávání v následujících vyhledávačích: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.",
"no_servers_specified": "Nebyly specifikovány žádné servery", "no_servers_specified": "Nebyly specifikovány žádné servery",
"general_settings": "Obecná nastavení", "general_settings": "Obecná nastavení",
"dns_settings": "Nastavení DNS", "dns_settings": "Nastavení DNS",

View file

@ -154,7 +154,7 @@
"use_adguard_parental": "Brug AdGuards forældrekontrolwebtjeneste", "use_adguard_parental": "Brug AdGuards forældrekontrolwebtjeneste",
"use_adguard_parental_hint": "AdGuard Home vil tjekke, om domænet indeholder voksenindhold vha. den samme fortrolighedsvenlige API som browsingsikkerhedswebtjenesten.", "use_adguard_parental_hint": "AdGuard Home vil tjekke, om domænet indeholder voksenindhold vha. den samme fortrolighedsvenlige API som browsingsikkerhedswebtjenesten.",
"enforce_safe_search": "Brug sikker søgning", "enforce_safe_search": "Brug sikker søgning",
"enforce_save_search_hint": "AdGuard Home vil håndhæve sikker søgning i flg. søgemaskiner: Google, YouTube, Bing, DuckDuckGo, Yandex og Pixabay.", "enforce_save_search_hint": "AdGuard Home vil håndhæve sikker søgning i flg. søgemaskiner: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.",
"no_servers_specified": "Ingen servere angivet", "no_servers_specified": "Ingen servere angivet",
"general_settings": "Generelle indstillinger", "general_settings": "Generelle indstillinger",
"dns_settings": "DNS-indstillinger", "dns_settings": "DNS-indstillinger",

View file

@ -154,7 +154,7 @@
"use_adguard_parental": "AdGuard Webservice für Kindersicherung verwenden", "use_adguard_parental": "AdGuard Webservice für Kindersicherung verwenden",
"use_adguard_parental_hint": "AdGuard Home wird prüfen, ob die Domain jugendgefährdende Inhalte enthält. Zum Schutz Ihrer Privatsphäre wird die selbe API wie für den Webservice für Internetsicherheit verwendet.", "use_adguard_parental_hint": "AdGuard Home wird prüfen, ob die Domain jugendgefährdende Inhalte enthält. Zum Schutz Ihrer Privatsphäre wird die selbe API wie für den Webservice für Internetsicherheit verwendet.",
"enforce_safe_search": "Sichere Suche verwenden", "enforce_safe_search": "Sichere Suche verwenden",
"enforce_save_search_hint": "AdGuard kann Sichere Suche für folgende Suchmaschinen erzwingen: Google, YouTube, Bing, DuckDuckGo, Yandex und Pixabay.", "enforce_save_search_hint": "AdGuard kann Sichere Suche für folgende Suchmaschinen erzwingen: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex und Pixabay.",
"no_servers_specified": "Keine Server festgelegt", "no_servers_specified": "Keine Server festgelegt",
"general_settings": "Allgemeine Einstellungen", "general_settings": "Allgemeine Einstellungen",
"dns_settings": "DNS-Einstellungen", "dns_settings": "DNS-Einstellungen",
@ -641,7 +641,7 @@
"show_processed_responses": "Verarbeitet", "show_processed_responses": "Verarbeitet",
"blocked_safebrowsing": "Gesperrt durch Internetsicherheit", "blocked_safebrowsing": "Gesperrt durch Internetsicherheit",
"blocked_adult_websites": "Gesperrt durch Kindersicherung", "blocked_adult_websites": "Gesperrt durch Kindersicherung",
"blocked_threats": "Gesperrte Bedrohungen", "blocked_threats": "Bedrohungen blockiert",
"allowed": "Zugelassen", "allowed": "Zugelassen",
"filtered": "Gefiltert", "filtered": "Gefiltert",
"rewritten": "Umgeschrieben", "rewritten": "Umgeschrieben",

View file

@ -154,7 +154,7 @@
"use_adguard_parental": "Use AdGuard parental control web service", "use_adguard_parental": "Use AdGuard parental control web service",
"use_adguard_parental_hint": "AdGuard Home will check if domain contains adult materials. It uses the same privacy-friendly API as the browsing security web service.", "use_adguard_parental_hint": "AdGuard Home will check if domain contains adult materials. It uses the same privacy-friendly API as the browsing security web service.",
"enforce_safe_search": "Use Safe Search", "enforce_safe_search": "Use Safe Search",
"enforce_save_search_hint": "AdGuard Home will enforce safe search in the following search engines: Google, YouTube, Bing, DuckDuckGo, Yandex, Pixabay.", "enforce_save_search_hint": "AdGuard Home will enforce safe search in the following search engines: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.",
"no_servers_specified": "No servers specified", "no_servers_specified": "No servers specified",
"general_settings": "General settings", "general_settings": "General settings",
"dns_settings": "DNS settings", "dns_settings": "DNS settings",

View file

@ -154,7 +154,7 @@
"use_adguard_parental": "Usar el control parental de AdGuard", "use_adguard_parental": "Usar el control parental de AdGuard",
"use_adguard_parental_hint": "AdGuard Home comprobará si el dominio contiene materiales para adultos. Utiliza la misma API amigable con la privacidad del servicio web de seguridad de navegación.", "use_adguard_parental_hint": "AdGuard Home comprobará si el dominio contiene materiales para adultos. Utiliza la misma API amigable con la privacidad del servicio web de seguridad de navegación.",
"enforce_safe_search": "Usar búsqueda segura", "enforce_safe_search": "Usar búsqueda segura",
"enforce_save_search_hint": "AdGuard Home reforzará la búsqueda segura en los siguientes motores de búsqueda: Google, YouTube, Bing, DuckDuckGo, Yandex y Pixabay.", "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", "no_servers_specified": "No hay servidores especificados",
"general_settings": "Configuración general", "general_settings": "Configuración general",
"dns_settings": "Configuración del DNS", "dns_settings": "Configuración del DNS",

View file

@ -6,7 +6,7 @@
"upstream_parallel": "Käytä rinnakkaisia pyyntöjä ja nopeuta selvitystä käyttämällä kaikkia ylävirtapalvelimia samanaikaisesti.", "upstream_parallel": "Käytä rinnakkaisia pyyntöjä ja nopeuta selvitystä käyttämällä kaikkia ylävirtapalvelimia samanaikaisesti.",
"parallel_requests": "Rinnakkaiset pyynnöt", "parallel_requests": "Rinnakkaiset pyynnöt",
"load_balancing": "Kuormantasaus", "load_balancing": "Kuormantasaus",
"load_balancing_desc": "Lähetä pyyntö yhdelle ylävirtapalvelimelle kerrallaan. AdGuard Home pyrkii valitsemaan nopeimman palvelimen painotetun satunnaisalgoritminsa avulla.", "load_balancing_desc": "Lähetä kysely kerrallaan yhdelle ylävirtapalvelimelle. AdGuard Home valitsee painotetun satunnaisalgoritmin avulla palvelimet, joilla on vähiten epäonnistuneita hakuja ja keskimääräisesti lyhin hakuaika.",
"bootstrap_dns": "Bootstrap DNS-palvelimet", "bootstrap_dns": "Bootstrap DNS-palvelimet",
"bootstrap_dns_desc": "Ylävirroiksi määrittämiesi DoH/DoT-resolverien IP-osoitteiden selvitykseen käytettävien DNS-palvelimien IP-osoitteet. Kommentteja ei sallita.", "bootstrap_dns_desc": "Ylävirroiksi määrittämiesi DoH/DoT-resolverien IP-osoitteiden selvitykseen käytettävien DNS-palvelimien IP-osoitteet. Kommentteja ei sallita.",
"fallback_dns_title": "DNS-varapalvelimet", "fallback_dns_title": "DNS-varapalvelimet",
@ -154,7 +154,7 @@
"use_adguard_parental": "Käytä AdGuardin lapsilukko-palvelua", "use_adguard_parental": "Käytä AdGuardin lapsilukko-palvelua",
"use_adguard_parental_hint": "AdGuard Home tarkistaa, sisältääkö verkkotunnus aikuisille tarkoitettua sisältöä. Se käyttää samaa tietosuojapainotteista rajapintaa, kuin turvallisen selauksen palvelu.", "use_adguard_parental_hint": "AdGuard Home tarkistaa, sisältääkö verkkotunnus aikuisille tarkoitettua sisältöä. Se käyttää samaa tietosuojapainotteista rajapintaa, kuin turvallisen selauksen palvelu.",
"enforce_safe_search": "Käytä turvallista hakua", "enforce_safe_search": "Käytä turvallista hakua",
"enforce_save_search_hint": "AdGuard Home voi pakottaa turvallisen haun käyttöön seuraavissa hakukoneissa: Google, YouTube, Bing, DuckDuckGo, Yandex, Pixabay.", "enforce_save_search_hint": "AdGuard Home pakottaa turvallisen haun käyttöön seuraavissa hakukoneissa: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex ja Pixabay.",
"no_servers_specified": "Palvelimia ei ole määritetty", "no_servers_specified": "Palvelimia ei ole määritetty",
"general_settings": "Yleiset asetukset", "general_settings": "Yleiset asetukset",
"dns_settings": "DNS-asetukset", "dns_settings": "DNS-asetukset",
@ -709,9 +709,9 @@
"log_and_stats_section_label": "Pyyntöhistoria ja tilastot", "log_and_stats_section_label": "Pyyntöhistoria ja tilastot",
"ignore_query_log": "Älä huomioi tätä päätelaitetta pyyntöhistoriassa", "ignore_query_log": "Älä huomioi tätä päätelaitetta pyyntöhistoriassa",
"ignore_statistics": "Älä huomioi tätä päätettä tilastoissa", "ignore_statistics": "Älä huomioi tätä päätettä tilastoissa",
"schedule_services": "Keskeytä palveluesto", "schedule_services": "Pysäytä palveluesto",
"schedule_services_desc": "Määritä palvelunestosuodattimen keskeytysajoitus.", "schedule_services_desc": "Määritä palvelunestosuodattimen pysäytysajoitus.",
"schedule_services_desc_client": "Määritä palvelunestosuodattimen keskeytysajoitus tälle päätteelle.", "schedule_services_desc_client": "Määritä palvelunestosuodattimen pysäytysajoitus tälle päätteelle.",
"schedule_desc": "Aseta estettujen palveluiden käyttämättömyysjaksot", "schedule_desc": "Aseta estettujen palveluiden käyttämättömyysjaksot",
"schedule_invalid_select": "Aloitusaika on oltava ennen lopetusaikaa", "schedule_invalid_select": "Aloitusaika on oltava ennen lopetusaikaa",
"schedule_select_days": "Valitse päivät", "schedule_select_days": "Valitse päivät",

View file

@ -154,7 +154,7 @@
"use_adguard_parental": "Utiliser le contrôle parental d'AdGuard", "use_adguard_parental": "Utiliser le contrôle parental d'AdGuard",
"use_adguard_parental_hint": "AdGuard Home va vérifier s'il y a du contenu pour adultes sur le domaine. Ce sera fait par aide du même API discret que celui utilisé par le service de Sécurité de navigation.", "use_adguard_parental_hint": "AdGuard Home va vérifier s'il y a du contenu pour adultes sur le domaine. Ce sera fait par aide du même API discret que celui utilisé par le service de Sécurité de navigation.",
"enforce_safe_search": "Utiliser la Recherche Sécurisée", "enforce_safe_search": "Utiliser la Recherche Sécurisée",
"enforce_save_search_hint": "AdGuard Home appliquera la recherche sécurisée dans les moteurs de recherche suivants : Google, YouTube, Bing, DuckDuckGo, Yandex, Pixabay.", "enforce_save_search_hint": "AdGuard Home appliquera la recherche sécurisée dans les moteurs de recherche suivants : Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.",
"no_servers_specified": "Pas de serveurs spécifiés", "no_servers_specified": "Pas de serveurs spécifiés",
"general_settings": "Paramètres généraux", "general_settings": "Paramètres généraux",
"dns_settings": "Paramètres DNS", "dns_settings": "Paramètres DNS",
@ -676,7 +676,7 @@
"filter_allowlist": "ATTENTION : Cette action exclura également la règle « {{disallowed_rule}} » de la liste des clients autorisés.", "filter_allowlist": "ATTENTION : Cette action exclura également la règle « {{disallowed_rule}} » de la liste des clients autorisés.",
"last_rule_in_allowlist": "Impossible dinterdire ce client, car lexclusion de la règle « {{disallowed_rule}} » DÉSACTIVERA la liste des « clients autorisés ».", "last_rule_in_allowlist": "Impossible dinterdire ce client, car lexclusion de la règle « {{disallowed_rule}} » DÉSACTIVERA la liste des « clients autorisés ».",
"use_saved_key": "Utiliser la clef précédemment enregistrée", "use_saved_key": "Utiliser la clef précédemment enregistrée",
"parental_control": "Contrôle parental", "parental_control": "Contrôle Parental",
"safe_browsing": "Navigation sécurisée", "safe_browsing": "Navigation sécurisée",
"served_from_cache_label": "Servi depuis le cache", "served_from_cache_label": "Servi depuis le cache",
"form_error_password_length": "Le mot de passe doit comporter entre {{min}} et {{max}}  caractères", "form_error_password_length": "Le mot de passe doit comporter entre {{min}} et {{max}}  caractères",

View file

@ -147,7 +147,7 @@
"average_upstream_response_time": "Rata-rata waktu respons hulu", "average_upstream_response_time": "Rata-rata waktu respons hulu",
"response_time": "Waktu respons", "response_time": "Waktu respons",
"average_processing_time_hint": "Rata-rata waktu dalam milidetik untuk pemrosesan sebuah permintaan DNS", "average_processing_time_hint": "Rata-rata waktu dalam milidetik untuk pemrosesan sebuah permintaan DNS",
"block_domain_use_filters_and_hosts": "Blokir domain menggunakan filter dan file hosts", "block_domain_use_filters_and_hosts": "Blokir domain menggunakan filter dan berkas host",
"filters_block_toggle_hint": "Anda dapat menyiapkan aturan pemblokiran dalam pengaturan <a>Filter</a>.", "filters_block_toggle_hint": "Anda dapat menyiapkan aturan pemblokiran dalam pengaturan <a>Filter</a>.",
"use_adguard_browsing_sec": "Gunakan layanan web Keamanan Penjelajahan AdGuard", "use_adguard_browsing_sec": "Gunakan layanan web Keamanan Penjelajahan AdGuard",
"use_adguard_browsing_sec_hint": "AdGuard Home akan memeriksa apakah domain diblokir oleh layanan web keamanan penjelajahan. Ini akan menggunakan API pencarian yang ramah privasi untuk melakukan pemeriksaan: hanya awalan singkat dari hash nama domain SHA256 yang dikirim ke server.", "use_adguard_browsing_sec_hint": "AdGuard Home akan memeriksa apakah domain diblokir oleh layanan web keamanan penjelajahan. Ini akan menggunakan API pencarian yang ramah privasi untuk melakukan pemeriksaan: hanya awalan singkat dari hash nama domain SHA256 yang dikirim ke server.",
@ -191,7 +191,7 @@
"edit_table_action": "Ubah", "edit_table_action": "Ubah",
"delete_table_action": "Hapus", "delete_table_action": "Hapus",
"elapsed": "Berlalu", "elapsed": "Berlalu",
"filters_and_hosts_hint": "AdGuard Home memahami aturan dasar adblock dan sintak file hosts.", "filters_and_hosts_hint": "AdGuard Home memahami aturan dasar adblock dan sintak berkas host.",
"no_blocklist_added": "Tidak ada daftar hitam yang ditambahkan", "no_blocklist_added": "Tidak ada daftar hitam yang ditambahkan",
"no_whitelist_added": "Tidak ada daftar putih yang ditambahkan", "no_whitelist_added": "Tidak ada daftar putih yang ditambahkan",
"add_blocklist": "Tambahkan daftar hitam", "add_blocklist": "Tambahkan daftar hitam",
@ -211,8 +211,8 @@
"form_error_url_format": "Format URL tidak valid", "form_error_url_format": "Format URL tidak valid",
"form_error_url_or_path_format": "URL atau jalur absolut dari daftar tidak valid", "form_error_url_or_path_format": "URL atau jalur absolut dari daftar tidak valid",
"custom_filter_rules": "Aturan penyaringan khusus", "custom_filter_rules": "Aturan penyaringan khusus",
"custom_filter_rules_hint": "Masukkan satu aturan dalam sebuah baris. Anda dapat menggunakan baik aturan adblock maupun sintaks file hosts.", "custom_filter_rules_hint": "Masukkan satu aturan pada satu baris. Anda dapat menggunakan aturan adblock atau sintaks berkas host.",
"system_host_files": "File host sistem", "system_host_files": "Berkas host sistem",
"examples_title": "Contoh", "examples_title": "Contoh",
"example_meaning_filter_block": "blokir akses ke example.org dan seluruh subdomainnya;", "example_meaning_filter_block": "blokir akses ke example.org dan seluruh subdomainnya;",
"example_meaning_filter_whitelist": "buka blokir akses ke domain example.orf dan seluruh subdomainnya;", "example_meaning_filter_whitelist": "buka blokir akses ke domain example.orf dan seluruh subdomainnya;",
@ -476,7 +476,7 @@
"client_confirm_delete": "Apakah anda yakin ingin menghapus klien \"{{key}}\"?", "client_confirm_delete": "Apakah anda yakin ingin menghapus klien \"{{key}}\"?",
"list_confirm_delete": "Anda yakin ingin menghapus daftar ini?", "list_confirm_delete": "Anda yakin ingin menghapus daftar ini?",
"auto_clients_title": "Klien (waktu berjalan)", "auto_clients_title": "Klien (waktu berjalan)",
"auto_clients_desc": "Informasi tentang alamat IP perangkat yang menggunakan atau mungkin menggunakan AdGuard Home. Informasi ini dikumpulkan dari beberapa sumber, termasuk file host, reverse DNS, dll.", "auto_clients_desc": "Informasi tentang alamat IP perangkat yang menggunakan atau mungkin menggunakan AdGuard Home. Informasi ini dikumpulkan dari beberapa sumber, termasuk berkas host, DNS terbalik, dll.",
"access_title": "Pengaturan akses", "access_title": "Pengaturan akses",
"access_desc": "Disini anda dapat mengatur aturan akses untuk server AdGuard Home DNS", "access_desc": "Disini anda dapat mengatur aturan akses untuk server AdGuard Home DNS",
"access_allowed_title": "Klien yang diizinkan", "access_allowed_title": "Klien yang diizinkan",
@ -494,7 +494,7 @@
"setup_dns_privacy_1": "<0>DNS melalui TLS:</0> Gunakan <1>{{address}}</1> string.", "setup_dns_privacy_1": "<0>DNS melalui TLS:</0> Gunakan <1>{{address}}</1> string.",
"setup_dns_privacy_2": "<0>DNS-over-TLS:</0> Memakai <1>{{address}}</1> string.", "setup_dns_privacy_2": "<0>DNS-over-TLS:</0> Memakai <1>{{address}}</1> string.",
"setup_dns_privacy_3": "<0>Berikut daftar perangkat lunak yang dapat Anda gunakan.</0>", "setup_dns_privacy_3": "<0>Berikut daftar perangkat lunak yang dapat Anda gunakan.</0>",
"setup_dns_privacy_4": "Di perangkat iOS 14 atau macOS Big Sur, Anda dapat mengunduh file '.mobileconfig' khusus yang menambahkan server <highlight>DNS-over-HTTPS</highlight> atau <highlight>DNS-over-TLS</highlight> ke pengaturan DNS.", "setup_dns_privacy_4": "Pada perangkat iOS 14 atau macOS Big Sur, Anda dapat mengunduh berkas khusus '.mobileconfig' yang menambahkan server <highlight>DNS melalui HTTPS</highlight> atau <highlight>DNS melalui TLS</highlight> ke pengaturan DNS.",
"setup_dns_privacy_android_1": "Android 9 mendukung DNS-over-TLS secara asli. Untuk mengkonfigurasinya, buka Pengaturan → Jaringan & internet → Tingkat Lanjut → DNS Pribadi dan masukkan nama domain Anda di sana.", "setup_dns_privacy_android_1": "Android 9 mendukung DNS-over-TLS secara asli. Untuk mengkonfigurasinya, buka Pengaturan → Jaringan & internet → Tingkat Lanjut → DNS Pribadi dan masukkan nama domain Anda di sana.",
"setup_dns_privacy_android_2": "<0>AdGuard untuk Android</0> mendukung <1>DNS-over-HTTPS</1> dan <1>DNS-over-TLS</1>.", "setup_dns_privacy_android_2": "<0>AdGuard untuk Android</0> mendukung <1>DNS-over-HTTPS</1> dan <1>DNS-over-TLS</1>.",
"setup_dns_privacy_android_3": "<0>Intra</0> menambahkan dukungan <1>DNS-over-HTTPS</1> untuk Android.", "setup_dns_privacy_android_3": "<0>Intra</0> menambahkan dukungan <1>DNS-over-HTTPS</1> untuk Android.",
@ -517,7 +517,7 @@
"rewrite_confirm_delete": "Apakah anda yakin ingin menghapus DNS rewrite untuk \"{{key}}\"?", "rewrite_confirm_delete": "Apakah anda yakin ingin menghapus DNS rewrite untuk \"{{key}}\"?",
"rewrite_desc": "Memungkinkan untuk dengan mudah mengkonfigurasi respons DNS kustom untuk nama domain tertentu.", "rewrite_desc": "Memungkinkan untuk dengan mudah mengkonfigurasi respons DNS kustom untuk nama domain tertentu.",
"rewrite_applied": "Aturan Rewrite yang diterapkan", "rewrite_applied": "Aturan Rewrite yang diterapkan",
"rewrite_hosts_applied": "Ditulis ulang oleh aturan file hosts", "rewrite_hosts_applied": "Ditulis ulang oleh aturan berkas host",
"dns_rewrites": "DNS rewrite", "dns_rewrites": "DNS rewrite",
"form_domain": "Masukkan nama domain", "form_domain": "Masukkan nama domain",
"form_answer": "Masaukan alamat IP atau nama domain", "form_answer": "Masaukan alamat IP atau nama domain",
@ -676,7 +676,7 @@
"filter_allowlist": "PERINGATAN: Tindakan ini juga akan mengecualikan aturan \"{{disallowed_rule}}\" dari daftar klien yang diizinkan.", "filter_allowlist": "PERINGATAN: Tindakan ini juga akan mengecualikan aturan \"{{disallowed_rule}}\" dari daftar klien yang diizinkan.",
"last_rule_in_allowlist": "Tidak dapat melarang klien ini karena mengecualikan aturan \"{{disallowed_rule}}\" akan MENONAKTIFKAN daftar \"Klien yang diizinkan\".", "last_rule_in_allowlist": "Tidak dapat melarang klien ini karena mengecualikan aturan \"{{disallowed_rule}}\" akan MENONAKTIFKAN daftar \"Klien yang diizinkan\".",
"use_saved_key": "Gunakan kunci yang disimpan sebelumnya", "use_saved_key": "Gunakan kunci yang disimpan sebelumnya",
"parental_control": "Kontrol Orang Tua", "parental_control": "Pengawasan Orang Tua",
"safe_browsing": "Penjelajahan Aman", "safe_browsing": "Penjelajahan Aman",
"served_from_cache": "{{value}} <i>(disajikan dari cache)</i>", "served_from_cache": "{{value}} <i>(disajikan dari cache)</i>",
"form_error_password_length": "Kata sandi harus terdiri dari {{min}} hingga {{max}}", "form_error_password_length": "Kata sandi harus terdiri dari {{min}} hingga {{max}}",

View file

@ -154,7 +154,7 @@
"use_adguard_parental": "Utilizza il Controllo Parentale di AdGuard", "use_adguard_parental": "Utilizza il Controllo Parentale di AdGuard",
"use_adguard_parental_hint": "AdGuard Home verificherà se il dominio contiene materiale per adulti. Utilizza le stesse API privacy-friendly del servizio web 'sicurezza di navigazione'.", "use_adguard_parental_hint": "AdGuard Home verificherà se il dominio contiene materiale per adulti. Utilizza le stesse API privacy-friendly del servizio web 'sicurezza di navigazione'.",
"enforce_safe_search": "Utilizza Ricerca Sicura", "enforce_safe_search": "Utilizza Ricerca Sicura",
"enforce_save_search_hint": "AdGuard Home forzerà la ricerca sicura sui seguenti motori di ricerca: Google, YouTube, Bing, DuckDuckGo, Yandex, Pixabay.", "enforce_save_search_hint": "AdGuard Home applicherà la ricerca sicura nei seguenti motori di ricerca: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.",
"no_servers_specified": "Nessun server specificato", "no_servers_specified": "Nessun server specificato",
"general_settings": "Impostazioni generali", "general_settings": "Impostazioni generali",
"dns_settings": "Impostazioni DNS", "dns_settings": "Impostazioni DNS",

View file

@ -154,7 +154,7 @@
"use_adguard_parental": "AdGuardペアレンタルコントロール・ウェブサービスを使用する", "use_adguard_parental": "AdGuardペアレンタルコントロール・ウェブサービスを使用する",
"use_adguard_parental_hint": "AdGuard Homeは、ドメインにアダルトコンテンツが含まれているかどうかを確認します。 ブラウジングセキュリティ・ウェブサービスと同じプライバシーに優しいAPIを使用します。", "use_adguard_parental_hint": "AdGuard Homeは、ドメインにアダルトコンテンツが含まれているかどうかを確認します。 ブラウジングセキュリティ・ウェブサービスと同じプライバシーに優しいAPIを使用します。",
"enforce_safe_search": "セーフサーチを使用する", "enforce_safe_search": "セーフサーチを使用する",
"enforce_save_search_hint": "AdGuard Homeは、次の検索エンジンでセーフサーチを強制適用します: Google, YouTube, Bing, DuckDuckGo, Yandex, Pixabay", "enforce_save_search_hint": "AdGuard Homeは、次の検索エンジンでセーフサーチを強制適用します: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay",
"no_servers_specified": "サーバが指定されていません", "no_servers_specified": "サーバが指定されていません",
"general_settings": "一般設定", "general_settings": "一般設定",
"dns_settings": "DNS設定", "dns_settings": "DNS設定",

View file

@ -108,7 +108,7 @@
"off": "OFF", "off": "OFF",
"copyright": "Copyright", "copyright": "Copyright",
"homepage": "홈페이지", "homepage": "홈페이지",
"report_an_issue": "문제를 보고합니다", "report_an_issue": "문제 신고",
"privacy_policy": "개인정보취급방침", "privacy_policy": "개인정보취급방침",
"enable_protection": "보호 활성화", "enable_protection": "보호 활성화",
"enabled_protection": "보호 활성화됨", "enabled_protection": "보호 활성화됨",
@ -154,7 +154,7 @@
"use_adguard_parental": "AdGuard 자녀 보호 웹 서비스 사용", "use_adguard_parental": "AdGuard 자녀 보호 웹 서비스 사용",
"use_adguard_parental_hint": "AdGuard Home은 도메인에 성인 자료가 포함되어 있는지 확인합니다. 브라우징 보안 웹 서비스와 동일한 개인정보 보호 API를 사용함.", "use_adguard_parental_hint": "AdGuard Home은 도메인에 성인 자료가 포함되어 있는지 확인합니다. 브라우징 보안 웹 서비스와 동일한 개인정보 보호 API를 사용함.",
"enforce_safe_search": "세이프서치 사용", "enforce_safe_search": "세이프서치 사용",
"enforce_save_search_hint": "AdGuard Home은 Google, YouTube, Bing, DuckDuckGo, Yandex, Pixabay와 같은 검색 엔진에서 세이프서치를 시행합니다.", "enforce_save_search_hint": "AdGuard Home은 Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay와 같은 검색 엔진에서 세이프서치를 시행합니다.",
"no_servers_specified": "지정된 서버 없음", "no_servers_specified": "지정된 서버 없음",
"general_settings": "일반 설정", "general_settings": "일반 설정",
"dns_settings": "DNS 설정", "dns_settings": "DNS 설정",

View file

@ -154,7 +154,7 @@
"use_adguard_parental": "Gebruik AdGuard Ouderlijk toezicht web service", "use_adguard_parental": "Gebruik AdGuard Ouderlijk toezicht web service",
"use_adguard_parental_hint": "AdGuard Home controleert of het domein 18+ content bevat. Dit gebeurt dmv dezelfde privacy vriendelijke API als de Browsing Security web service.", "use_adguard_parental_hint": "AdGuard Home controleert of het domein 18+ content bevat. Dit gebeurt dmv dezelfde privacy vriendelijke API als de Browsing Security web service.",
"enforce_safe_search": "Veilig zoeken gebruiken", "enforce_safe_search": "Veilig zoeken gebruiken",
"enforce_save_search_hint": "AdGuard Home kan veilig zoeken forceren voor de volgende zoekmachines: Google, Youtube, Bing, en DuckDuckGo, Yandex, Pixabay.", "enforce_save_search_hint": "AdGuard Home dwingt veilig zoeken af in de volgende zoekmachines: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.",
"no_servers_specified": "Geen servers gespecificeerd", "no_servers_specified": "Geen servers gespecificeerd",
"general_settings": "Algemene instellingen", "general_settings": "Algemene instellingen",
"dns_settings": "DNS instellingen", "dns_settings": "DNS instellingen",

View file

@ -128,6 +128,7 @@
"enforced_save_search": "Påtvungede barnevennlige søk", "enforced_save_search": "Påtvungede barnevennlige søk",
"number_of_dns_query_to_safe_search": "Antall DNS-forespørsler til søkemotorer der \"Safe Search\" ble fremtvunget", "number_of_dns_query_to_safe_search": "Antall DNS-forespørsler til søkemotorer der \"Safe Search\" ble fremtvunget",
"average_processing_time": "Gjennomsnittlig behandlingstid", "average_processing_time": "Gjennomsnittlig behandlingstid",
"response_time": "Responstid",
"average_processing_time_hint": "Gjennomsnittstid for behandling av DNS-forespørsler i millisekunder", "average_processing_time_hint": "Gjennomsnittstid for behandling av DNS-forespørsler i millisekunder",
"block_domain_use_filters_and_hosts": "Blokker domener ved hjelp av filtre, «hosts»-filer, og rå domener", "block_domain_use_filters_and_hosts": "Blokker domener ved hjelp av filtre, «hosts»-filer, og rå domener",
"filters_block_toggle_hint": "Du kan sette opp blokkeringsoppføringer i <a>Filtre</a>-innstillingene.", "filters_block_toggle_hint": "Du kan sette opp blokkeringsoppføringer i <a>Filtre</a>-innstillingene.",

View file

@ -6,7 +6,7 @@
"upstream_parallel": "Użyj zapytań równoległych, aby przyspieszyć rozwiązywanie przez jednoczesne wysyłanie zapytań do wszystkich serwerów nadrzędnych.", "upstream_parallel": "Użyj zapytań równoległych, aby przyspieszyć rozwiązywanie przez jednoczesne wysyłanie zapytań do wszystkich serwerów nadrzędnych.",
"parallel_requests": "Równoległe żądania", "parallel_requests": "Równoległe żądania",
"load_balancing": "Równoważenie obciążenia", "load_balancing": "Równoważenie obciążenia",
"load_balancing_desc": "Wysyłaj zapytania do jednego serwera nadrzędnego na raz. AdGuard Home używa swojego losowego algorytmu ważonego, aby wybrać serwer, tak aby najszybszy serwer był używany częściej.", "load_balancing_desc": "Zapytaj jeden serwer nadrzędny na raz. AdGuard Home używa ważonego, losowego algorytmu do wybierania serwerów z najmniejszą liczbą nieudanych wyszukiwań i najniższym uśrednionym czasem wyszukiwania.",
"bootstrap_dns": "Serwery DNS Bootstrap", "bootstrap_dns": "Serwery DNS Bootstrap",
"bootstrap_dns_desc": "Adresy IP serwerów DNS używanych do rozpoznawania adresów IP programów rozpoznawania nazw DoH/DoT określonych jako nadrzędne. Komentarze są niedozwolone.", "bootstrap_dns_desc": "Adresy IP serwerów DNS używanych do rozpoznawania adresów IP programów rozpoznawania nazw DoH/DoT określonych jako nadrzędne. Komentarze są niedozwolone.",
"fallback_dns_title": "Rezerwowe serwery DNS", "fallback_dns_title": "Rezerwowe serwery DNS",
@ -154,7 +154,7 @@
"use_adguard_parental": "Użyj usługi Kontrola Rodzicielska AdGuard", "use_adguard_parental": "Użyj usługi Kontrola Rodzicielska AdGuard",
"use_adguard_parental_hint": "AdGuard Home sprawdzi, czy domena zawiera materiały dla dorosłych. Używa tego samego interfejsu API przyjaznego prywatności, co usługa sieciowa Bezpieczne Przeglądanie. ", "use_adguard_parental_hint": "AdGuard Home sprawdzi, czy domena zawiera materiały dla dorosłych. Używa tego samego interfejsu API przyjaznego prywatności, co usługa sieciowa Bezpieczne Przeglądanie. ",
"enforce_safe_search": "Użyj bezpiecznego wyszukiwania", "enforce_safe_search": "Użyj bezpiecznego wyszukiwania",
"enforce_save_search_hint": "AdGuard Home wymusza bezpieczne wyszukiwanie w następujących wyszukiwarkach: Google, YouTube, Bing, DuckDuckGo, Yandex, Pixabay.", "enforce_save_search_hint": "AdGuard Home wymusza bezpieczne wyszukiwanie w następujących wyszukiwarkach: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.",
"no_servers_specified": "Nie określono serwerów", "no_servers_specified": "Nie określono serwerów",
"general_settings": "Ustawienia główne", "general_settings": "Ustawienia główne",
"dns_settings": "Ustawienia DNS", "dns_settings": "Ustawienia DNS",

View file

@ -154,7 +154,7 @@
"use_adguard_parental": "Usar o serviço de controle parental do AdGuard", "use_adguard_parental": "Usar o serviço de controle parental do AdGuard",
"use_adguard_parental_hint": "O AdGuard Home irá verificar se o domínio contém conteúdo adulto. Ele usa a mesma API amigável de privacidade que o serviço de segurança da navegação.", "use_adguard_parental_hint": "O AdGuard Home irá verificar se o domínio contém conteúdo adulto. Ele usa a mesma API amigável de privacidade que o serviço de segurança da navegação.",
"enforce_safe_search": "Usar pesquisa segura", "enforce_safe_search": "Usar pesquisa segura",
"enforce_save_search_hint": "O AdGuard Home forcará a pesquisa segura nos seguintes motores de busca: Google, YouTube, Bing, DuckDuckGo, Yandex, Pixabay.", "enforce_save_search_hint": "O AdGuard Home forcará a pesquisa segura nos seguintes motores de busca: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.",
"no_servers_specified": "Nenhum servidor especificado", "no_servers_specified": "Nenhum servidor especificado",
"general_settings": "Configurações gerais", "general_settings": "Configurações gerais",
"dns_settings": "Configurações de DNS", "dns_settings": "Configurações de DNS",

View file

@ -154,7 +154,7 @@
"use_adguard_parental": "Usar o serviço de controlo parental do AdGuard", "use_adguard_parental": "Usar o serviço de controlo parental do AdGuard",
"use_adguard_parental_hint": "O AdGuard Home irá verificar se o domínio contém conteúdo adulto. Usa a mesma API amigável de privacidade que o serviço de segurança da navegação.", "use_adguard_parental_hint": "O AdGuard Home irá verificar se o domínio contém conteúdo adulto. Usa a mesma API amigável de privacidade que o serviço de segurança da navegação.",
"enforce_safe_search": "Usar pesquisa segura", "enforce_safe_search": "Usar pesquisa segura",
"enforce_save_search_hint": "O AdGuard Home forçará a pesquisa segura nos seguintes motores de busca: Google, Youtube, Bing, DuckDuckGo, Yandex, Pixabay.", "enforce_save_search_hint": "O AdGuard Home aplicará pesquisa segura nos seguintes motores de busca: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.",
"no_servers_specified": "Nenhum servidor especificado", "no_servers_specified": "Nenhum servidor especificado",
"general_settings": "Definições gerais", "general_settings": "Definições gerais",
"dns_settings": "Definições de DNS", "dns_settings": "Definições de DNS",

View file

@ -154,7 +154,7 @@
"use_adguard_parental": "Включить модуль Родительского контроля AdGuard ", "use_adguard_parental": "Включить модуль Родительского контроля AdGuard ",
"use_adguard_parental_hint": "AdGuard Home проверит, содержит ли домен материалы 18+. Он использует тот же API для обеспечения конфиденциальности, что и веб-служба безопасности браузера.", "use_adguard_parental_hint": "AdGuard Home проверит, содержит ли домен материалы 18+. Он использует тот же API для обеспечения конфиденциальности, что и веб-служба безопасности браузера.",
"enforce_safe_search": "Включить безопасный поиск", "enforce_safe_search": "Включить безопасный поиск",
"enforce_save_search_hint": "AdGuard Home может обеспечить безопасный поиск в следующих поисковых системах: Google, YouTube, Bing, DuckDuckGo, Yandex и Pixabay.", "enforce_save_search_hint": "AdGuard Home будет обеспечивать безопасный поиск в следующих поисковых системах: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.",
"no_servers_specified": "Нет указанных серверов", "no_servers_specified": "Нет указанных серверов",
"general_settings": "Основные настройки", "general_settings": "Основные настройки",
"dns_settings": "Настройки DNS", "dns_settings": "Настройки DNS",

View file

@ -154,7 +154,7 @@
"use_adguard_parental": "Použiť AdGuard službu Rodičovská kontrola", "use_adguard_parental": "Použiť AdGuard službu Rodičovská kontrola",
"use_adguard_parental_hint": "AdGuard Home skontroluje, či doména obsahuje materiály pre dospelých. Používa rovnaké API priateľské k ochrane osobných údajov ako služba Bezpečného prehliadania.", "use_adguard_parental_hint": "AdGuard Home skontroluje, či doména obsahuje materiály pre dospelých. Používa rovnaké API priateľské k ochrane osobných údajov ako služba Bezpečného prehliadania.",
"enforce_safe_search": "Používať bezpečné vyhľadávanie", "enforce_safe_search": "Používať bezpečné vyhľadávanie",
"enforce_save_search_hint": "AdGuard Home vynucuje bezpečné vyhľadávanie v nasledujúcich vyhľadávačoch: Google, YouTube, Bing, DuckDuckGo, Yandex a Pixabay.", "enforce_save_search_hint": "AdGuard Home vynúti bezpečné vyhľadávanie v nasledujúcich vyhľadávačoch: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.",
"no_servers_specified": "Neboli špecifikované žiadne servery", "no_servers_specified": "Neboli špecifikované žiadne servery",
"general_settings": "Všeobecné nastavenia", "general_settings": "Všeobecné nastavenia",
"dns_settings": "Nastavenia DNS", "dns_settings": "Nastavenia DNS",
@ -484,7 +484,7 @@
"access_disallowed_title": "Nepovolení klienti", "access_disallowed_title": "Nepovolení klienti",
"access_disallowed_desc": "Zoznam CIDR, IP adries alebo <a>ClientID</a>. Ak tento zoznam obsahuje položky, AdGuard Home zruší dopyty od týchto klientov. Toto pole sa ignoruje, ak sú v poli Povolení klienti položky.", "access_disallowed_desc": "Zoznam CIDR, IP adries alebo <a>ClientID</a>. Ak tento zoznam obsahuje položky, AdGuard Home zruší dopyty od týchto klientov. Toto pole sa ignoruje, ak sú v poli Povolení klienti položky.",
"access_blocked_title": "Nepovolené domény", "access_blocked_title": "Nepovolené domény",
"access_blocked_desc": "Nesmie byť zamieňaná s filtrami. AdGuard Home zruší DNS dopyty, ktoré sa zhodujú s týmito doménami, a tieto dopyty sa nezobrazia ani v denníku dopytov. Môžete určiť presné názvy domén, zástupné znaky alebo pravidlá filtrovania URL adries, napr. \"example.org\", \"*.example.org\" alebo ||example.org^\" zodpovedajúcim spôsobom.", "access_blocked_desc": "Nesmie byť zamieňaná s filtrami. AdGuard Home zruší DNS dopyty, ktoré sa zhodujú s týmito doménami, a tieto dopyty sa nezobrazia ani v denníku dopytov. Môžete určiť presné názvy domén, zástupné znaky alebo pravidlá filtrácie URL adries, napr. \"example.org\", \"*.example.org\" alebo ||example.org^\" zodpovedajúcim spôsobom.",
"access_settings_saved": "Nastavenia prístupu úspešne uložené", "access_settings_saved": "Nastavenia prístupu úspešne uložené",
"updates_checked": "K dispozícii je nová verzia aplikácie AdGuard Home\n", "updates_checked": "K dispozícii je nová verzia aplikácie AdGuard Home\n",
"updates_version_equal": "AdGuard Home je aktuálny", "updates_version_equal": "AdGuard Home je aktuálny",

View file

@ -46,6 +46,7 @@
"settings": "การตั้งค่า", "settings": "การตั้งค่า",
"filters": "ตัวกรอง", "filters": "ตัวกรอง",
"query_log": "บันทึกการสืบค้น", "query_log": "บันทึกการสืบค้น",
"nothing_found": "ไม่พบอะไร",
"faq": "คำถามที่พบบ่อย", "faq": "คำถามที่พบบ่อย",
"version": "รุ่น", "version": "รุ่น",
"address": "ที่อยู่", "address": "ที่อยู่",
@ -349,7 +350,7 @@
"statistics_configuration": "การกำหนดค่าสถิติ", "statistics_configuration": "การกำหนดค่าสถิติ",
"statistics_retention": "การเก็บรักษาสถิติ", "statistics_retention": "การเก็บรักษาสถิติ",
"statistics_retention_desc": "หากคุณลดค่าช่วงเวลาข้อมูลบางอย่างจะหายไป", "statistics_retention_desc": "หากคุณลดค่าช่วงเวลาข้อมูลบางอย่างจะหายไป",
"statistics_clear": " ล้างค่าสถิติ", "statistics_clear": "ล้างสถิติ",
"statistics_clear_confirm": "คุณแน่ใจหรือไม่ว่าต้องการล้างสถิติ?", "statistics_clear_confirm": "คุณแน่ใจหรือไม่ว่าต้องการล้างสถิติ?",
"statistics_retention_confirm": "คุณแน่ใจหรือไม่ว่าต้องการเปลี่ยนการเก็บรักษาสถิติ? หากคุณลดค่าช่วงเวลา ข้อมูลบางอย่างจะหายไป", "statistics_retention_confirm": "คุณแน่ใจหรือไม่ว่าต้องการเปลี่ยนการเก็บรักษาสถิติ? หากคุณลดค่าช่วงเวลา ข้อมูลบางอย่างจะหายไป",
"statistics_cleared": "สถิติได้ถูกล้างเรียบร้อยแล้ว", "statistics_cleared": "สถิติได้ถูกล้างเรียบร้อยแล้ว",
@ -390,10 +391,13 @@
"check_title": "ตรวจสอบการกรอง", "check_title": "ตรวจสอบการกรอง",
"check_desc": "ตรวจสอบว่าชื่อโฮสต์ถูกกรอง", "check_desc": "ตรวจสอบว่าชื่อโฮสต์ถูกกรอง",
"form_enter_host": "ป้อนชื่อโฮสต์", "form_enter_host": "ป้อนชื่อโฮสต์",
"show_processed_responses": "การประมวลผล", "show_blocked_responses": "ปิดกั้นแล้ว",
"show_whitelisted_responses": "รายการที่อนุญาต",
"show_processed_responses": "ประมวลผลแล้ว",
"blocked_adult_websites": "ถูกปิดกั้นโดยการควบคุมของผู้ปกครอง", "blocked_adult_websites": "ถูกปิดกั้นโดยการควบคุมของผู้ปกครอง",
"safe_search": "ค้นหาอย่างปลอดภัย", "safe_search": "ค้นหาอย่างปลอดภัย",
"blocklist": "บัญชีดำ", "blocklist": "บัญชีดำ",
"allowed": "รายการที่อนุญาต",
"filter_category_other": "อื่น ๆ", "filter_category_other": "อื่น ๆ",
"parental_control": "ควบคุมโดยผู้ปกครอง", "parental_control": "ควบคุมโดยผู้ปกครอง",
"sunday_short": "อาทิตย์", "sunday_short": "อาทิตย์",

View file

@ -68,7 +68,7 @@
"ip": "IP", "ip": "IP",
"dhcp_table_hostname": "Ana makine Adı", "dhcp_table_hostname": "Ana makine Adı",
"dhcp_table_expires": "Bitiş tarihi", "dhcp_table_expires": "Bitiş tarihi",
"dhcp_warning": "DHCP sunucusunu yine de etkinleştirmek istiyorsanız, ağınızda başka aktif DHCP sunucusu olmadığından emin olun, aksi takdirde ağa bağlı cihazların İnternet bağlantısı kesilebilir!", "dhcp_warning": "DHCP sunucusunu yine de etkinleştirmek istiyorsanız, ağınızda başka aktif DHCP sunucusu olmadığından emin olun, aksi takdirde ağa bağlı cihazların internet bağlantısı kesilebilir!",
"dhcp_error": "AdGuard Home, ağda başka bir etkin DHCP sunucusu olup olmadığını belirleyemedi", "dhcp_error": "AdGuard Home, ağda başka bir etkin DHCP sunucusu olup olmadığını belirleyemedi",
"dhcp_static_ip_error": "DHCP sunucusunu kullanmak için sabit bir IP adresi ayarlanmalıdır. AdGuard Home, bu ağ arayüzünün sabit bir IP adresi kullanılarak yapılandırılıp yapılandırılmadığını belirleyemedi. Lütfen sabit IP adresini elle ayarlayın.", "dhcp_static_ip_error": "DHCP sunucusunu kullanmak için sabit bir IP adresi ayarlanmalıdır. AdGuard Home, bu ağ arayüzünün sabit bir IP adresi kullanılarak yapılandırılıp yapılandırılmadığını belirleyemedi. Lütfen sabit IP adresini elle ayarlayın.",
"dhcp_dynamic_ip_found": "Sisteminiz, <0>{{interfaceName}}</0> arayüzü için dinamik IP adresi yapılandırması kullanıyor. DHCP sunucusunu kullanmak için sabit bir IP adresi ayarlanmalıdır. Geçerli olan IP adresiniz <0>{{ipAddress}}</0>. \"DHCP sunucusunu etkinleştir\" düğmesine basarsanız, AdGuard Home bu IP adresini otomatik bir şekilde sabit olarak ayarlayacaktır.", "dhcp_dynamic_ip_found": "Sisteminiz, <0>{{interfaceName}}</0> arayüzü için dinamik IP adresi yapılandırması kullanıyor. DHCP sunucusunu kullanmak için sabit bir IP adresi ayarlanmalıdır. Geçerli olan IP adresiniz <0>{{ipAddress}}</0>. \"DHCP sunucusunu etkinleştir\" düğmesine basarsanız, AdGuard Home bu IP adresini otomatik bir şekilde sabit olarak ayarlayacaktır.",
@ -154,7 +154,7 @@
"use_adguard_parental": "AdGuard ebeveyn denetimi web hizmetini kullan", "use_adguard_parental": "AdGuard ebeveyn denetimi web hizmetini kullan",
"use_adguard_parental_hint": "AdGuard Home, alan adının yetişkin içerik bulundurup bulundurmadığını kontrol eder. Gezinti koruması web hizmeti ile kullandığımız aynı gizlilik dostu API'yi kullanır.", "use_adguard_parental_hint": "AdGuard Home, alan adının yetişkin içerik bulundurup bulundurmadığını kontrol eder. Gezinti koruması web hizmeti ile kullandığımız aynı gizlilik dostu API'yi kullanır.",
"enforce_safe_search": "Güvenli Aramayı kullan", "enforce_safe_search": "Güvenli Aramayı kullan",
"enforce_save_search_hint": "AdGuard Home, şu arama motorlarında güvenli aramayı uygular: Google, YouTube, Bing, DuckDuckGo, Yandex ve Pixabay.", "enforce_save_search_hint": "AdGuard Home, şu arama motorlarında güvenli aramayı uygular: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.",
"no_servers_specified": "Sunucu belirtilmedi", "no_servers_specified": "Sunucu belirtilmedi",
"general_settings": "Genel ayarlar", "general_settings": "Genel ayarlar",
"dns_settings": "DNS ayarları", "dns_settings": "DNS ayarları",
@ -476,7 +476,7 @@
"client_confirm_delete": "\"{{key}}\" istemcisini silmek istediğinizden emin misiniz?", "client_confirm_delete": "\"{{key}}\" istemcisini silmek istediğinizden emin misiniz?",
"list_confirm_delete": "Bu listeyi silmek istediğinizden emin misiniz?", "list_confirm_delete": "Bu listeyi silmek istediğinizden emin misiniz?",
"auto_clients_title": "Çalışma zamanı istemcileri", "auto_clients_title": "Çalışma zamanı istemcileri",
"auto_clients_desc": "AdGuard Home'u kullanan veya kullanabilecek cihazların IP adresleri hakkında bilgiler. Bu bilgiler, hosts dosyaları, ters DNS, vb. dahil olmak üzere çeşitli kaynaklardan toplanır.", "auto_clients_desc": "AdGuard Home'u kullanan veya kullanabilecek cihazların IP adresleri hakkında bilgiler. Bu bilgiler, hosts dosyaları, ters DNS, vb. dâhil olmak üzere çeşitli kaynaklardan toplanır.",
"access_title": "Erişim ayarları", "access_title": "Erişim ayarları",
"access_desc": "AdGuard Home DNS sunucusu için erişim kurallarını buradan yapılandırabilirsiniz", "access_desc": "AdGuard Home DNS sunucusu için erişim kurallarını buradan yapılandırabilirsiniz",
"access_allowed_title": "İzin verilen istemciler", "access_allowed_title": "İzin verilen istemciler",
@ -603,7 +603,7 @@
"autofix_warning_list": "Bu görevleri gerçekleştirir: <0>Sistem DNSStubListener'ı devre dışı bırakın</0> <0>DNS sunucusu adresini 127.0.0.1 olarak ayarlayın</0> <0>/etc/resolv.conf'un sembolik bağlantı hedefini /run/systemd/resolve/resolv.conf ile değiştirin<0> <0>DNSStubListener'ı durdurun (systemd çözümlenmiş hizmeti yeniden yükleyin)</0>", "autofix_warning_list": "Bu görevleri gerçekleştirir: <0>Sistem DNSStubListener'ı devre dışı bırakın</0> <0>DNS sunucusu adresini 127.0.0.1 olarak ayarlayın</0> <0>/etc/resolv.conf'un sembolik bağlantı hedefini /run/systemd/resolve/resolv.conf ile değiştirin<0> <0>DNSStubListener'ı durdurun (systemd çözümlenmiş hizmeti yeniden yükleyin)</0>",
"autofix_warning_result": "Sonuç olarak, sisteminizden gelen tüm DNS istekleri varsayılan olarak AdGuard Home tarafından işlenecektir.", "autofix_warning_result": "Sonuç olarak, sisteminizden gelen tüm DNS istekleri varsayılan olarak AdGuard Home tarafından işlenecektir.",
"tags_title": "Etiketler", "tags_title": "Etiketler",
"tags_desc": "İstemciye karşılık gelen etiketleri seçebilirsiniz. Etiketleri daha kesin olarak uygulamak için filtreleme kurallarına dahil edin. <0>Daha fazla bilgi edinin</0>.", "tags_desc": "İstemciye karşılık gelen etiketleri seçebilirsiniz. Etiketleri daha kesin olarak uygulamak için filtreleme kurallarına dâhil edin. <0>Daha fazla bilgi edinin</0>.",
"form_select_tags": "İstemci etiketlerini seçin", "form_select_tags": "İstemci etiketlerini seçin",
"check_title": "Filtrelemeyi denetleyin", "check_title": "Filtrelemeyi denetleyin",
"check_desc": "Ana makine adının filtreleme durumunu kontrol edin.", "check_desc": "Ana makine adının filtreleme durumunu kontrol edin.",

View file

@ -6,21 +6,21 @@
"upstream_parallel": "Sử dụng truy vấn song song để tăng tốc độ giải quyết bằng cách truy vấn đồng thời tất cả các máy chủ ngược tuyến", "upstream_parallel": "Sử dụng truy vấn song song để tăng tốc độ giải quyết bằng cách truy vấn đồng thời tất cả các máy chủ ngược tuyến",
"parallel_requests": "Yêu cầu song song", "parallel_requests": "Yêu cầu song song",
"load_balancing": "Cân bằng tải", "load_balancing": "Cân bằng tải",
"load_balancing_desc": "Chỉ truy xuất một máy chủ trong cùng thời điểm. AdGuard Home sẽ sử dụng thuật toán trọng số ngẫu nhiên để chọn một máy chủ nhanh nhất và sử dụng máy chủ đó thường xuyên hơn.", "load_balancing_desc": "Truy vấn một máy chủ thượng nguồn tại một thời điểm. AdGuard Home sử dụng thuật toán ngẫu nhiên có trọng số để chọn máy chủ có số lần tìm kiếm không thành công thấp nhất và thời gian tìm kiếm trung bình thấp nhất.",
"bootstrap_dns": "Máy chủ DNS Bootstrap", "bootstrap_dns": "Máy chủ DNS Bootstrap",
"bootstrap_dns_desc": "Địa chỉ IP của máy chủ DNS được sử dụng để phân giải địa chỉ IP của trình phân giải DoH/DoT mà bạn chỉ định làm thượng nguồn. Bình luận không được phép.", "bootstrap_dns_desc": "Địa chỉ IP của máy chủ DNS được sử dụng để phân giải địa chỉ IP của trình phân giải DoH/DoT mà bạn chỉ định làm thượng nguồn. Bình luận không được phép.",
"fallback_dns_title": "Máy chủ DNS dự phòng", "fallback_dns_title": "Máy chủ DNS dự phòng",
"fallback_dns_desc": "Danh sách máy chủ DNS dự phòng được sử dụng khi máy chủ DNS ngược tuyến không phản hồi. Cú pháp tương tự như trong trường ngược dòng chính ở trên.", "fallback_dns_desc": "Danh sách máy chủ DNS dự phòng được sử dụng khi máy chủ DNS ngược tuyến không phản hồi. Cú pháp tương tự như trong trường ngược dòng chính ở trên.",
"fallback_dns_placeholder": "Nhập một máy chủ DNS dự phòng trên mỗi dòng", "fallback_dns_placeholder": "Nhập một máy chủ DNS dự phòng trên mỗi dòng",
"local_ptr_title": "Máy chủ DNS riêng tư", "local_ptr_title": "Máy chủ DNS riêng tư",
"local_ptr_desc": "Máy chủ DNS hoặc các máy chủ mà AdGuard Home sẽ sử dụng cho các truy vấn về tài nguyên được phân phối cục bộ. Ví dụ: máy chủ này sẽ được sử dụng để phân giải tên máy khách của máy khách cho các máy khách có địa chỉ IP riêng. Nếu không được cài đặt, AdGuard Home sẽ tự động sử dụng trình phân giải DNS mặc định của bạn.", "local_ptr_desc": "Máy chủ DNS được AdGuard Home sử dụng cho các yêu cầu PTR, SOA và NS riêng tư. Một yêu cầu được coi là riêng tư nếu nó yêu cầu một miền ARPA chứa một mạng con trong phạm vi IP riêng tư (chẳng hạn như \"192.168.12.34\") và đến từ một máy khách có địa chỉ IP riêng tư. Nếu không được thiết lập, các trình phân giải DNS mặc định của hệ điều hành của bạn sẽ được sử dụng, ngoại trừ các địa chỉ IP của AdGuard Home.",
"local_ptr_default_resolver": "Theo mặc định, AdGuard Home sử dụng các hệ thống phân giải tên miền ngược sau: {{ip}}.", "local_ptr_default_resolver": "Theo mặc định, AdGuard Home sử dụng các hệ thống phân giải tên miền ngược sau: {{ip}}.",
"local_ptr_no_default_resolver": "AdGuard Home không thể xác định hệ thống phân giải tên miền ngược riêng phù hợp cho hệ thống này.", "local_ptr_no_default_resolver": "AdGuard Home không thể xác định hệ thống phân giải tên miền ngược riêng phù hợp cho hệ thống này.",
"local_ptr_placeholder": "Nhập một địa chỉ IP trên mỗi dòng", "local_ptr_placeholder": "Nhập một địa chỉ IP trên mỗi dòng",
"resolve_clients_title": "Kích hoạt cho phép phân giải ngược về địa chỉ IP của máy khách", "resolve_clients_title": "Kích hoạt cho phép phân giải ngược về địa chỉ IP của máy khách",
"resolve_clients_desc": "Nếu được bật, AdGuard Home sẽ cố gắng phân giải ngược lại địa chỉ IP của khách hàng thành tên máy chủ của họ bằng cách gửi các truy vấn PTR tới trình phân giải tương ứng (máy chủ DNS riêng cho máy khách cục bộ, máy chủ ngược dòng cho máy khách có địa chỉ IP công cộng).", "resolve_clients_desc": "Nếu được bật, AdGuard Home sẽ cố gắng phân giải ngược lại địa chỉ IP của khách hàng thành tên máy chủ của họ bằng cách gửi các truy vấn PTR tới trình phân giải tương ứng (máy chủ DNS riêng cho máy khách cục bộ, máy chủ ngược dòng cho máy khách có địa chỉ IP công cộng).",
"use_private_ptr_resolvers_title": "Sử dụng trình rDNS riêng tư", "use_private_ptr_resolvers_title": "Sử dụng trình rDNS riêng tư",
"use_private_ptr_resolvers_desc": "Thực hiện tra cứu ngược DNS cho các địa chỉ được phân phối cục bộ bằng cách sử dụng các máy chủ nguồn. Nếu bị vô hiệu hóa, AdGuard Home sẽ phản hồi với NXDOMAIN cho tất cả các yêu cầu PTR ngoại trừ các ứng dụng khách được biết đến bởi DHCP, / etc / hosts, v. v.", "use_private_ptr_resolvers_desc": "Giải quyết các yêu cầu PTR, SOA và NS cho các miền ARPA chứa địa chỉ IP riêng thông qua máy chủ thượng nguồn riêng, DHCP, /etc/hosts, v. v. Nếu bị vô hiệu hóa, AdGuard Home sẽ phản hồi tất cả các yêu cầu đó bằng NXDOMAIN.",
"check_dhcp_servers": "Kiểm tra máy chủ DHCP", "check_dhcp_servers": "Kiểm tra máy chủ DHCP",
"save_config": "Lưu thiết lập", "save_config": "Lưu thiết lập",
"enabled_dhcp": "Máy chủ DHCP đã kích hoạt", "enabled_dhcp": "Máy chủ DHCP đã kích hoạt",
@ -154,7 +154,7 @@
"use_adguard_parental": "Sử dụng dịch vụ quản lý của phụ huynh AdGuard", "use_adguard_parental": "Sử dụng dịch vụ quản lý của phụ huynh AdGuard",
"use_adguard_parental_hint": "AdGuard Home sẽ kiểm tra nếu tên miền chứa từ khoá người lớn. Tính năng sử dụng API thân thiện với quyền riêng tư tương tự với dịch vụ bảo vệ duyệt web", "use_adguard_parental_hint": "AdGuard Home sẽ kiểm tra nếu tên miền chứa từ khoá người lớn. Tính năng sử dụng API thân thiện với quyền riêng tư tương tự với dịch vụ bảo vệ duyệt web",
"enforce_safe_search": "Bắt buộc tìm kiếm an toàn", "enforce_safe_search": "Bắt buộc tìm kiếm an toàn",
"enforce_save_search_hint": "AdGuard Home có thể bắt buộc tìm kiếm an toàn với các dịch vụ tìm kiếm: Google, Youtube, Bing, Yandex.", "enforce_save_search_hint": "AdGuard Home sẽ thực thi tìm kiếm an toàn trong các công cụ tìm kiếm sau: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.",
"no_servers_specified": "Không có máy chủ nào được liệt kê", "no_servers_specified": "Không có máy chủ nào được liệt kê",
"general_settings": "Cài đặt chung", "general_settings": "Cài đặt chung",
"dns_settings": "Cài đặt DNS", "dns_settings": "Cài đặt DNS",
@ -425,6 +425,9 @@
"encryption_hostnames": "Tên máy chủ", "encryption_hostnames": "Tên máy chủ",
"encryption_reset": "Bạn có chắc chắn muốn đặt lại cài đặt mã hóa?", "encryption_reset": "Bạn có chắc chắn muốn đặt lại cài đặt mã hóa?",
"encryption_warning": "Cảnh báo", "encryption_warning": "Cảnh báo",
"encryption_plain_dns_enable": "Kích hoạt DNS đơn giản",
"encryption_plain_dns_desc": "DNS đơn giản được bật theo mặc định. Bạn có thể vô hiệu hóa nó để buộc tất cả các thiết bị sử dụng DNS được mã hóa. Để thực hiện việc này, bạn phải kích hoạt ít nhất một giao thức DNS được mã hóa",
"encryption_plain_dns_error": "Để tắt DNS đơn giản, hãy bật ít nhất một giao thức DNS được mã hóa",
"topline_expiring_certificate": "Chứng chỉ SSL của bạn sắp hết hạn. Cập nhật <0>Cài đặt mã hóa</0>.", "topline_expiring_certificate": "Chứng chỉ SSL của bạn sắp hết hạn. Cập nhật <0>Cài đặt mã hóa</0>.",
"topline_expired_certificate": "Chứng chỉ SSL của bạn đã hết hạn. Cập nhật <0>Cài đặt mã hóa</0>.", "topline_expired_certificate": "Chứng chỉ SSL của bạn đã hết hạn. Cập nhật <0>Cài đặt mã hóa</0>.",
"form_error_port_range": "Nhập giá trị cổng trong phạm vi 80-65535", "form_error_port_range": "Nhập giá trị cổng trong phạm vi 80-65535",
@ -675,7 +678,7 @@
"use_saved_key": "Sử dụng khóa đã lưu trước đó", "use_saved_key": "Sử dụng khóa đã lưu trước đó",
"parental_control": "Quản lý của phụ huynh", "parental_control": "Quản lý của phụ huynh",
"safe_browsing": "Duyệt web an toàn", "safe_browsing": "Duyệt web an toàn",
"served_from_cache": "{{value}} <i>(được phục vụ từ bộ nhớ cache)</i>", "served_from_cache_label": "Được phục vụ từ bộ nhớ đệm",
"form_error_password_length": "Mật khẩu phải dài từ {{min}} đến {{max}} ký tự", "form_error_password_length": "Mật khẩu phải dài từ {{min}} đến {{max}} ký tự",
"anonymizer_notification": "<0> Lưu ý:</0> Tính năng ẩn danh IP được bật. Bạn có thể tắt nó trong <1> Cài đặt chung</1>.", "anonymizer_notification": "<0> Lưu ý:</0> Tính năng ẩn danh IP được bật. Bạn có thể tắt nó trong <1> Cài đặt chung</1>.",
"confirm_dns_cache_clear": "Bạn có chắc chắn muốn xóa bộ đệm ẩn DNS không?", "confirm_dns_cache_clear": "Bạn có chắc chắn muốn xóa bộ đệm ẩn DNS không?",

View file

@ -154,7 +154,7 @@
"use_adguard_parental": "使用 AdGuard 【家长控制】服务", "use_adguard_parental": "使用 AdGuard 【家长控制】服务",
"use_adguard_parental_hint": "AdGuard Home 将使用与浏览安全服务相同的隐私性强的 API 来检查域名指向的网站是否包含成人内容。", "use_adguard_parental_hint": "AdGuard Home 将使用与浏览安全服务相同的隐私性强的 API 来检查域名指向的网站是否包含成人内容。",
"enforce_safe_search": "使用安全搜索", "enforce_safe_search": "使用安全搜索",
"enforce_save_search_hint": "AdGuard Home 对以下搜索引擎可强制启用安全搜索Google、YouTube、Bing、DuckDuckGo、Yandex、Pixabay。", "enforce_save_search_hint": "AdGuard Home 将会在下列搜索引擎中强制启用安全搜索Google、YouTube、Bing、DuckDuckGo、Ecosia、Yandex、Pixabay。",
"no_servers_specified": "未找到指定的服务器", "no_servers_specified": "未找到指定的服务器",
"general_settings": "常规设置", "general_settings": "常规设置",
"dns_settings": "DNS 设置", "dns_settings": "DNS 设置",

View file

@ -154,7 +154,7 @@
"use_adguard_parental": "使用 AdGuard 家長控制之網路服務", "use_adguard_parental": "使用 AdGuard 家長控制之網路服務",
"use_adguard_parental_hint": "AdGuard Home 將檢查網域是否包含成人資料。它使用如同瀏覽安全網路服務一樣之對隱私友好的應用程式介面API。", "use_adguard_parental_hint": "AdGuard Home 將檢查網域是否包含成人資料。它使用如同瀏覽安全網路服務一樣之對隱私友好的應用程式介面API。",
"enforce_safe_search": "使用安全搜尋", "enforce_safe_search": "使用安全搜尋",
"enforce_save_search_hint": "AdGuard Home 將在下列的搜尋引擎Google、YouTube、Bing、DuckDuckGo、Yandex 和 Pixabay 中強制執行安全搜尋。", "enforce_save_search_hint": "AdGuard Home 將在下列的搜尋引擎Google、YouTube、Bing、DuckDuckGo、Ecosia、Yandex 和 Pixabay 中強制執行安全搜尋。",
"no_servers_specified": "無已明確指定的伺服器", "no_servers_specified": "無已明確指定的伺服器",
"general_settings": "一般設定", "general_settings": "一般設定",
"dns_settings": "DNS 設定", "dns_settings": "DNS 設定",

View file

@ -256,6 +256,12 @@ export default {
"homepage": "https://github.com/hagezi/dns-blocklists", "homepage": "https://github.com/hagezi/dns-blocklists",
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_49.txt" "source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_49.txt"
}, },
"hagezi_xiaomi_tracking_blocklist": {
"name": "HaGeZi's Xiaomi Tracker Blocklist",
"categoryId": "other",
"homepage": "https://github.com/hagezi/dns-blocklists",
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_60.txt"
},
"no_google": { "no_google": {
"name": "No Google", "name": "No Google",
"categoryId": "other", "categoryId": "other",

View file

@ -66,7 +66,7 @@ export const renderFormattedClientCell = (value: any, info: any, isDetailed = fa
} }
return ( return (
<div className="logs__text mw-100" title={value}> <div className="logs__text logs__text--client mw-100" title={value}>
<Link to={`logs?search="${encodeURIComponent(value)}"`}>{nameContainer}</Link> <Link to={`logs?search="${encodeURIComponent(value)}"`}>{nameContainer}</Link>
{whoisContainer} {whoisContainer}
</div> </div>

File diff suppressed because it is too large Load diff

29
go.mod
View file

@ -1,10 +1,10 @@
module github.com/AdguardTeam/AdGuardHome module github.com/AdguardTeam/AdGuardHome
go 1.22.5 go 1.23.1
require ( require (
github.com/AdguardTeam/dnsproxy v0.71.2 github.com/AdguardTeam/dnsproxy v0.73.0
github.com/AdguardTeam/golibs v0.24.0 github.com/AdguardTeam/golibs v0.26.0
github.com/AdguardTeam/urlfilter v0.19.0 github.com/AdguardTeam/urlfilter v0.19.0
github.com/NYTimes/gziphandler v1.1.1 github.com/NYTimes/gziphandler v1.1.1
github.com/ameshkov/dnscrypt/v2 v2.3.0 github.com/ameshkov/dnscrypt/v2 v2.3.0
@ -28,14 +28,14 @@ require (
// own code for that. Perhaps, use gopacket. // own code for that. Perhaps, use gopacket.
github.com/mdlayher/raw v0.1.0 github.com/mdlayher/raw v0.1.0
github.com/miekg/dns v1.1.61 github.com/miekg/dns v1.1.61
github.com/quic-go/quic-go v0.44.0 github.com/quic-go/quic-go v0.47.0
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/ti-mo/netfilter v0.5.2 github.com/ti-mo/netfilter v0.5.2
go.etcd.io/bbolt v1.3.10 go.etcd.io/bbolt v1.3.10
golang.org/x/crypto v0.24.0 golang.org/x/crypto v0.26.0
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa
golang.org/x/net v0.26.0 golang.org/x/net v0.28.0
golang.org/x/sys v0.21.0 golang.org/x/sys v0.24.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
howett.net/plist v1.0.1 howett.net/plist v1.0.1
@ -55,12 +55,15 @@ require (
github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
go.uber.org/mock v0.4.0 // indirect go.uber.org/mock v0.4.0 // indirect
golang.org/x/mod v0.18.0 // indirect golang.org/x/mod v0.20.0 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.16.0 // indirect golang.org/x/text v0.17.0 // indirect
golang.org/x/tools v0.22.0 // indirect golang.org/x/tools v0.24.0 // indirect
gonum.org/v1/gonum v0.15.0 // indirect gonum.org/v1/gonum v0.15.0 // indirect
) )
// TODO(a.garipov): Remove once https://github.com/quic-go/quic-go/pull/4685 is merged.
replace github.com/quic-go/quic-go => github.com/ainar-g/quic-go v0.0.0-20240930125330-446bd86056fd

48
go.sum
View file

@ -1,7 +1,7 @@
github.com/AdguardTeam/dnsproxy v0.71.2 h1:dFG2wga4GDdj1eI3rU2wqjQ6QGQm9MjLRb5ZzyH3Vgg= github.com/AdguardTeam/dnsproxy v0.73.0 h1:E1fxzosMqExZH8h7OJnKXLxyktcAFRJapLF4+nKULms=
github.com/AdguardTeam/dnsproxy v0.71.2/go.mod h1:huI5zyWhlimHBhg0jt2CMinXzsEHymI+WlvxIfmfEGA= github.com/AdguardTeam/dnsproxy v0.73.0/go.mod h1:ZcvmyQY2EiX5B0yCTkiYTgtm+1lBWA0lajbEI9dOhW4=
github.com/AdguardTeam/golibs v0.24.0 h1:qAnOq7BQtwSVo7Co9q703/n+nZ2Ap6smkugU9G9MomY= github.com/AdguardTeam/golibs v0.26.0 h1:uLL0XggEjB+87lL1tPpEAQNoKAlHDq5AyBUVWEgf63E=
github.com/AdguardTeam/golibs v0.24.0/go.mod h1:9/vJcYznW7RlmCT/Qzi8XNZGj+ZbWfHZJmEXKnRpCAU= github.com/AdguardTeam/golibs v0.26.0/go.mod h1:iWdjXPCwmK2g2FKIb/OwEPnovSXeMqRhI8FWLxF5oxE=
github.com/AdguardTeam/urlfilter v0.19.0 h1:q7eH13+yNETlpD/VD3u5rLQOripcUdEktqZFy+KiQLk= github.com/AdguardTeam/urlfilter v0.19.0 h1:q7eH13+yNETlpD/VD3u5rLQOripcUdEktqZFy+KiQLk=
github.com/AdguardTeam/urlfilter v0.19.0/go.mod h1:+N54ZvxqXYLnXuvpaUhK2exDQW+djZBRSb6F6j0rkBY= github.com/AdguardTeam/urlfilter v0.19.0/go.mod h1:+N54ZvxqXYLnXuvpaUhK2exDQW+djZBRSb6F6j0rkBY=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
@ -10,6 +10,8 @@ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmH
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= 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 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
github.com/ainar-g/quic-go v0.0.0-20240930125330-446bd86056fd h1:mw4LqrCiv3vcKuCxBRg7kA17xfHKM+9hZgFWmyhe/AY=
github.com/ainar-g/quic-go v0.0.0-20240930125330-446bd86056fd/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/ameshkov/dnscrypt/v2 v2.3.0 h1:pDXDF7eFa6Lw+04C0hoMh8kCAQM8NwUdFEllSP2zNLs= 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.3.0/go.mod h1:N5hDwgx2cNb4Ay7AhvOSKst+eUiOZ/vbKRO9qMpQttE=
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo= github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
@ -97,10 +99,8 @@ 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/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 h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.44.0 h1:So5wOr7jyO4vzL2sd8/pD9Kesciv91zSk8BoFngItQ0=
github.com/quic-go/quic-go v0.44.0/go.mod h1:z4cx/9Ny9UtGITIPzmPTXh1ULfOyWh4qGQlpnPcWmek=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= 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/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
@ -128,26 +128,26 @@ go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-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-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -158,19 +158,19 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ= gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ=

View file

@ -1,94 +0,0 @@
package aghalg
// RingBuffer is the implementation of ring buffer data structure.
type RingBuffer[T any] struct {
buf []T
cur uint
full bool
}
// NewRingBuffer initializes the new instance of ring buffer. size must be
// greater or equal to zero.
func NewRingBuffer[T any](size uint) (rb *RingBuffer[T]) {
return &RingBuffer[T]{
buf: make([]T, size),
}
}
// Append appends an element to the buffer.
func (rb *RingBuffer[T]) Append(e T) {
if len(rb.buf) == 0 {
return
}
rb.buf[rb.cur] = e
rb.cur = (rb.cur + 1) % uint(cap(rb.buf))
if rb.cur == 0 {
rb.full = true
}
}
// Range calls cb for each element of the buffer. If cb returns false it stops.
func (rb *RingBuffer[T]) Range(cb func(T) (cont bool)) {
before, after := rb.splitCur()
for _, e := range before {
if !cb(e) {
return
}
}
for _, e := range after {
if !cb(e) {
return
}
}
}
// ReverseRange calls cb for each element of the buffer in reverse order. If
// cb returns false it stops.
func (rb *RingBuffer[T]) ReverseRange(cb func(T) (cont bool)) {
before, after := rb.splitCur()
for i := len(after) - 1; i >= 0; i-- {
if !cb(after[i]) {
return
}
}
for i := len(before) - 1; i >= 0; i-- {
if !cb(before[i]) {
return
}
}
}
// splitCur splits the buffer in two, before and after current position in
// chronological order. If buffer is not full, after is nil.
func (rb *RingBuffer[T]) splitCur() (before, after []T) {
if len(rb.buf) == 0 {
return nil, nil
}
cur := rb.cur
if !rb.full {
return rb.buf[:cur], nil
}
return rb.buf[cur:], rb.buf[:cur]
}
// Len returns a length of the buffer.
func (rb *RingBuffer[T]) Len() (l uint) {
if !rb.full {
return rb.cur
}
return uint(cap(rb.buf))
}
// Clear clears the buffer.
func (rb *RingBuffer[T]) Clear() {
rb.full = false
rb.cur = 0
}

View file

@ -1,169 +0,0 @@
package aghalg_test
import (
"slices"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/stretchr/testify/assert"
)
// elements is a helper function that returns n elements of the buffer.
func elements(b *aghalg.RingBuffer[int], n uint, reverse bool) (es []int) {
fn := b.Range
if reverse {
fn = b.ReverseRange
}
var i uint
fn(func(e int) (cont bool) {
if i >= n {
return false
}
es = append(es, e)
i++
return true
})
return es
}
func TestNewRingBuffer(t *testing.T) {
t.Run("success_and_clear", func(t *testing.T) {
b := aghalg.NewRingBuffer[int](5)
for i := range 10 {
b.Append(i)
}
assert.Equal(t, []int{5, 6, 7, 8, 9}, elements(b, b.Len(), false))
b.Clear()
assert.Zero(t, b.Len())
})
t.Run("zero", func(t *testing.T) {
b := aghalg.NewRingBuffer[int](0)
for i := range 10 {
b.Append(i)
bufLen := b.Len()
assert.EqualValues(t, 0, bufLen)
assert.Empty(t, elements(b, bufLen, false))
assert.Empty(t, elements(b, bufLen, true))
}
})
t.Run("single", func(t *testing.T) {
b := aghalg.NewRingBuffer[int](1)
for i := range 10 {
b.Append(i)
bufLen := b.Len()
assert.EqualValues(t, 1, bufLen)
assert.Equal(t, []int{i}, elements(b, bufLen, false))
assert.Equal(t, []int{i}, elements(b, bufLen, true))
}
})
}
func TestRingBuffer_Range(t *testing.T) {
const size = 5
b := aghalg.NewRingBuffer[int](size)
testCases := []struct {
name string
want []int
count int
length uint
}{{
name: "three",
count: 3,
length: 3,
want: []int{0, 1, 2},
}, {
name: "ten",
count: 10,
length: size,
want: []int{5, 6, 7, 8, 9},
}, {
name: "hundred",
count: 100,
length: size,
want: []int{95, 96, 97, 98, 99},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
for i := range tc.count {
b.Append(i)
}
bufLen := b.Len()
assert.Equal(t, tc.length, bufLen)
want := tc.want
assert.Equal(t, want, elements(b, bufLen, false))
assert.Equal(t, want[:len(want)-1], elements(b, bufLen-1, false))
assert.Equal(t, want[:len(want)/2], elements(b, bufLen/2, false))
want = want[:cap(want)]
slices.Reverse(want)
assert.Equal(t, want, elements(b, bufLen, true))
assert.Equal(t, want[:len(want)-1], elements(b, bufLen-1, true))
assert.Equal(t, want[:len(want)/2], elements(b, bufLen/2, true))
})
}
}
func TestRingBuffer_Range_increment(t *testing.T) {
const size = 5
b := aghalg.NewRingBuffer[int](size)
testCases := []struct {
name string
want []int
}{{
name: "one",
want: []int{0},
}, {
name: "two",
want: []int{0, 1},
}, {
name: "three",
want: []int{0, 1, 2},
}, {
name: "four",
want: []int{0, 1, 2, 3},
}, {
name: "five",
want: []int{0, 1, 2, 3, 4},
}, {
name: "six",
want: []int{1, 2, 3, 4, 5},
}, {
name: "seven",
want: []int{2, 3, 4, 5, 6},
}, {
name: "eight",
want: []int{3, 4, 5, 6, 7},
}, {
name: "nine",
want: []int{4, 5, 6, 7, 8},
}, {
name: "ten",
want: []int{5, 6, 7, 8, 9},
}}
for i, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
b.Append(i)
bufLen := b.Len()
assert.Equal(t, tc.want, elements(b, bufLen, false))
slices.Reverse(tc.want)
assert.Equal(t, tc.want, elements(b, bufLen, true))
})
}
}

View file

@ -2,13 +2,16 @@
package aghhttp package aghhttp
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"log/slog"
"net/http" "net/http"
"github.com/AdguardTeam/AdGuardHome/internal/version" "github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/httphdr" "github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
) )
// HTTP scheme constants. // HTTP scheme constants.
@ -31,12 +34,39 @@ func OK(w http.ResponseWriter) {
} }
// Error writes formatted message to w and also logs it. // Error writes formatted message to w and also logs it.
//
// TODO(s.chzhen): Remove it.
func Error(r *http.Request, w http.ResponseWriter, code int, format string, args ...any) { func Error(r *http.Request, w http.ResponseWriter, code int, format string, args ...any) {
text := fmt.Sprintf(format, args...) text := fmt.Sprintf(format, args...)
log.Error("%s %s %s: %s", r.Method, r.Host, r.URL, text) log.Error("%s %s %s: %s", r.Method, r.Host, r.URL, text)
http.Error(w, text, code) http.Error(w, text, code)
} }
// ErrorAndLog writes formatted message to w and also logs it with the specified
// logging level.
func ErrorAndLog(
ctx context.Context,
l *slog.Logger,
r *http.Request,
w http.ResponseWriter,
code int,
format string,
args ...any,
) {
text := fmt.Sprintf(format, args...)
l.ErrorContext(
ctx,
"http error",
"host", r.Host,
"method", r.Method,
"raddr", r.RemoteAddr,
"request_uri", r.RequestURI,
slogutil.KeyError, text,
)
http.Error(w, text, code)
}
// UserAgent returns the ID of the service as a User-Agent string. It can also // UserAgent returns the ID of the service as a User-Agent string. It can also
// be used as the value of the Server HTTP header. // be used as the value of the Server HTTP header.
func UserAgent() (ua string) { func UserAgent() (ua string) {

View file

@ -6,10 +6,10 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"io" "io"
"net"
"strings" "strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghos" "github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/netutil"
) )
func ifaceHasStaticIP(ifaceName string) (ok bool, err error) { func ifaceHasStaticIP(ifaceName string) (ok bool, err error) {
@ -38,9 +38,13 @@ func (n interfaceName) rcConfStaticConfig(r io.Reader) (_ []string, cont bool, e
// TODO(e.burkov): Expand the check to cover possible // TODO(e.burkov): Expand the check to cover possible
// configurations from man rc.conf(5). // configurations from man rc.conf(5).
fields := strings.Fields(line[cfgLeft:cfgRight]) fields := strings.Fields(line[cfgLeft:cfgRight])
if len(fields) >= 2 && switch {
strings.EqualFold(fields[0], "inet") && case
net.ParseIP(fields[1]) != nil { len(fields) < 2,
!strings.EqualFold(fields[0], "inet"),
!netutil.IsValidIPString(fields[1]):
continue
default:
return nil, false, s.Err() return nil, false, s.Err()
} }
} }

View file

@ -6,10 +6,10 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"io" "io"
"net"
"strings" "strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghos" "github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/netutil"
) )
func ifaceHasStaticIP(ifaceName string) (ok bool, err error) { func ifaceHasStaticIP(ifaceName string) (ok bool, err error) {
@ -25,7 +25,13 @@ func hostnameIfStaticConfig(r io.Reader) (_ []string, ok bool, err error) {
for s.Scan() { for s.Scan() {
line := strings.TrimSpace(s.Text()) line := strings.TrimSpace(s.Text())
fields := strings.Fields(line) fields := strings.Fields(line)
if len(fields) >= 2 && fields[0] == "inet" && net.ParseIP(fields[1]) != nil { switch {
case
len(fields) < 2,
fields[0] != "inet",
!netutil.IsValidIPString(fields[1]):
continue
default:
return nil, false, s.Err() return nil, false, s.Err()
} }
} }

View file

@ -1,6 +1,6 @@
package aghos package aghos
// ConfigureSyslog reroutes standard logger output to syslog. // ConfigureSyslog reroutes standard logger output to syslog.
func ConfigureSyslog(serviceName string) error { func ConfigureSyslog(serviceName string) (err error) {
return configureSyslog(serviceName) return configureSyslog(serviceName)
} }

View file

@ -8,11 +8,15 @@ import (
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
func configureSyslog(serviceName string) error { // configureSyslog sets standard log output to syslog.
func configureSyslog(serviceName string) (err error) {
w, err := syslog.New(syslog.LOG_NOTICE|syslog.LOG_USER, serviceName) w, err := syslog.New(syslog.LOG_NOTICE|syslog.LOG_USER, serviceName)
if err != nil { if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err return err
} }
log.SetOutput(w) log.SetOutput(w)
return nil return nil
} }

View file

@ -19,23 +19,30 @@ func (w *eventLogWriter) Write(b []byte) (int, error) {
return len(b), w.el.Info(1, string(b)) return len(b), w.el.Info(1, string(b))
} }
func configureSyslog(serviceName string) error { // configureSyslog sets standard log output to event log.
// Note that the eventlog src is the same as the service name func configureSyslog(serviceName string) (err error) {
// Otherwise, we will get "the description for event id cannot be found" warning in every log record // Note that the eventlog src is the same as the service name, otherwise we
// will get "the description for event id cannot be found" warning in every
// log record.
// Continue if we receive "registry key already exists" or if we get // Continue if we receive "registry key already exists" or if we get
// ERROR_ACCESS_DENIED so that we can log without administrative permissions // ERROR_ACCESS_DENIED so that we can log without administrative permissions
// for pre-existing eventlog sources. // for pre-existing eventlog sources.
if err := eventlog.InstallAsEventCreate(serviceName, eventlog.Info|eventlog.Warning|eventlog.Error); err != nil { err = eventlog.InstallAsEventCreate(serviceName, eventlog.Info|eventlog.Warning|eventlog.Error)
if !strings.Contains(err.Error(), "registry key already exists") && err != windows.ERROR_ACCESS_DENIED { if err != nil &&
return err !strings.Contains(err.Error(), "registry key already exists") &&
} err != windows.ERROR_ACCESS_DENIED {
// Don't wrap the error, because it's informative enough as is.
return err
} }
el, err := eventlog.Open(serviceName) el, err := eventlog.Open(serviceName)
if err != nil { if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err return err
} }
log.SetOutput(&eventLogWriter{el: el}) log.SetOutput(&eventLogWriter{el: el})
return nil return nil
} }

View file

@ -5,9 +5,10 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"net/netip" "slices"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
) )
// init makes sure that the cipher name map is filled. // init makes sure that the cipher name map is filled.
@ -75,15 +76,5 @@ func SaferCipherSuites() (safe []uint16) {
// CertificateHasIP returns true if cert has at least a single IP address among // CertificateHasIP returns true if cert has at least a single IP address among
// its subjectAltNames. // its subjectAltNames.
func CertificateHasIP(cert *x509.Certificate) (ok bool) { func CertificateHasIP(cert *x509.Certificate) (ok bool) {
if len(cert.IPAddresses) > 0 { return len(cert.IPAddresses) > 0 || slices.ContainsFunc(cert.DNSNames, netutil.IsValidIPString)
return true
}
for _, name := range cert.DNSNames {
if _, err := netip.ParseAddr(name); err == nil {
return true
}
}
return false
} }

View file

@ -5,6 +5,7 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"fmt" "fmt"
"log/slog"
"net" "net"
"net/netip" "net/netip"
"slices" "slices"
@ -12,7 +13,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghos" "github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/osutil" "github.com/AdguardTeam/golibs/osutil"
) )
@ -38,8 +39,8 @@ type Interface interface {
} }
// New returns the [Interface] properly initialized for the OS. // New returns the [Interface] properly initialized for the OS.
func New() (arp Interface) { func New(logger *slog.Logger) (arp Interface) {
return newARPDB() return newARPDB(logger)
} }
// Empty is the [Interface] implementation that does nothing. // Empty is the [Interface] implementation that does nothing.
@ -69,6 +70,30 @@ type Neighbor struct {
MAC net.HardwareAddr MAC net.HardwareAddr
} }
// newNeighbor returns the new initialized [Neighbor] by parsing string
// representations of IP and MAC addresses.
func newNeighbor(host, ipStr, macStr string) (n *Neighbor, err error) {
defer func() { err = errors.Annotate(err, "getting arp neighbor: %w") }()
ip, err := netip.ParseAddr(ipStr)
if err != nil {
// Don't wrap the error, as it will get annotated.
return nil, err
}
mac, err := net.ParseMAC(macStr)
if err != nil {
// Don't wrap the error, as it will get annotated.
return nil, err
}
return &Neighbor{
Name: host,
IP: ip,
MAC: mac,
}, nil
}
// Clone returns the deep copy of n. // Clone returns the deep copy of n.
func (n Neighbor) Clone() (clone Neighbor) { func (n Neighbor) Clone() (clone Neighbor) {
return Neighbor{ return Neighbor{
@ -80,10 +105,10 @@ func (n Neighbor) Clone() (clone Neighbor) {
// validatedHostname returns h if it's a valid hostname, or an empty string // validatedHostname returns h if it's a valid hostname, or an empty string
// otherwise, logging the validation error. // otherwise, logging the validation error.
func validatedHostname(h string) (host string) { func validatedHostname(logger *slog.Logger, h string) (host string) {
err := netutil.ValidateHostname(h) err := netutil.ValidateHostname(h)
if err != nil { if err != nil {
log.Debug("arpdb: parsing arp output: host: %s", err) logger.Debug("parsing host of arp output", slogutil.KeyError, err)
return "" return ""
} }
@ -132,15 +157,18 @@ func (ns *neighs) reset(with []Neighbor) {
// parseNeighsFunc parses the text from sc as if it'd be an output of some // parseNeighsFunc parses the text from sc as if it'd be an output of some
// ARP-related command. lenHint is a hint for the size of the allocated slice // ARP-related command. lenHint is a hint for the size of the allocated slice
// of Neighbors. // of Neighbors.
type parseNeighsFunc func(sc *bufio.Scanner, lenHint int) (ns []Neighbor) //
// TODO(s.chzhen): Return []*Neighbor instead.
type parseNeighsFunc func(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor)
// cmdARPDB is the implementation of the [Interface] that uses command line to // cmdARPDB is the implementation of the [Interface] that uses command line to
// retrieve data. // retrieve data.
type cmdARPDB struct { type cmdARPDB struct {
parse parseNeighsFunc logger *slog.Logger
ns *neighs parse parseNeighsFunc
cmd string ns *neighs
args []string cmd string
args []string
} }
// type check // type check
@ -158,7 +186,7 @@ func (arp *cmdARPDB) Refresh() (err error) {
} }
sc := bufio.NewScanner(bytes.NewReader(out)) sc := bufio.NewScanner(bytes.NewReader(out))
ns := arp.parse(sc, arp.ns.len()) ns := arp.parse(arp.logger, sc, arp.ns.len())
if err = sc.Err(); err != nil { if err = sc.Err(); err != nil {
// TODO(e.burkov): This error seems unreachable. Investigate. // TODO(e.burkov): This error seems unreachable. Investigate.
return fmt.Errorf("scanning the output: %w", err) return fmt.Errorf("scanning the output: %w", err)

View file

@ -4,17 +4,17 @@ package arpdb
import ( import (
"bufio" "bufio"
"net" "log/slog"
"net/netip"
"strings" "strings"
"sync" "sync"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/logutil/slogutil"
) )
func newARPDB() (arp *cmdARPDB) { func newARPDB(logger *slog.Logger) (arp *cmdARPDB) {
return &cmdARPDB{ return &cmdARPDB{
parse: parseArpA, logger: logger,
parse: parseArpA,
ns: &neighs{ ns: &neighs{
mu: &sync.RWMutex{}, mu: &sync.RWMutex{},
ns: make([]Neighbor, 0), ns: make([]Neighbor, 0),
@ -33,7 +33,7 @@ func newARPDB() (arp *cmdARPDB) {
// The expected input format: // The expected input format:
// //
// host.name (192.168.0.1) at ff:ff:ff:ff:ff:ff on en0 ifscope [ethernet] // host.name (192.168.0.1) at ff:ff:ff:ff:ff:ff on en0 ifscope [ethernet]
func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) { func parseArpA(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
ns = make([]Neighbor, 0, lenHint) ns = make([]Neighbor, 0, lenHint)
for sc.Scan() { for sc.Scan() {
ln := sc.Text() ln := sc.Text()
@ -48,26 +48,15 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
continue continue
} }
ip, err := netip.ParseAddr(ipStr[1 : len(ipStr)-1]) host := validatedHostname(logger, fields[0])
n, err := newNeighbor(host, ipStr[1:len(ipStr)-1], fields[3])
if err != nil { if err != nil {
log.Debug("arpdb: parsing arp output: ip: %s", err) logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
continue continue
} }
hwStr := fields[3] ns = append(ns, *n)
mac, err := net.ParseMAC(hwStr)
if err != nil {
log.Debug("arpdb: parsing arp output: mac: %s", err)
continue
}
ns = append(ns, Neighbor{
IP: ip,
MAC: mac,
Name: validatedHostname(fields[0]),
})
} }
return ns return ns

View file

@ -11,6 +11,7 @@ import (
"testing" "testing"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -61,7 +62,7 @@ func (s mapShell) RunCmd(cmd string, args ...string) (code int, out []byte, err
func Test_New(t *testing.T) { func Test_New(t *testing.T) {
var a Interface var a Interface
require.NotPanics(t, func() { a = New() }) require.NotPanics(t, func() { a = New(slogutil.NewDiscardLogger()) })
assert.NotNil(t, a) assert.NotNil(t, a)
} }
@ -201,8 +202,9 @@ func Test_NewARPDBs(t *testing.T) {
func TestCmdARPDB_arpa(t *testing.T) { func TestCmdARPDB_arpa(t *testing.T) {
a := &cmdARPDB{ a := &cmdARPDB{
cmd: "cmd", logger: slogutil.NewDiscardLogger(),
parse: parseArpA, cmd: "cmd",
parse: parseArpA,
ns: &neighs{ ns: &neighs{
mu: &sync.RWMutex{}, mu: &sync.RWMutex{},
ns: make([]Neighbor, 0), ns: make([]Neighbor, 0),

View file

@ -6,17 +6,18 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"io/fs" "io/fs"
"log/slog"
"net" "net"
"net/netip" "net/netip"
"strings" "strings"
"sync" "sync"
"github.com/AdguardTeam/AdGuardHome/internal/aghos" "github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/stringutil"
) )
func newARPDB() (arp *arpdbs) { func newARPDB(logger *slog.Logger) (arp *arpdbs) {
// Use the common storage among the implementations. // Use the common storage among the implementations.
ns := &neighs{ ns := &neighs{
mu: &sync.RWMutex{}, mu: &sync.RWMutex{},
@ -39,9 +40,10 @@ func newARPDB() (arp *arpdbs) {
}, },
// Then, try "arp -a -n". // Then, try "arp -a -n".
&cmdARPDB{ &cmdARPDB{
parse: parseF, logger: logger,
ns: ns, parse: parseF,
cmd: "arp", ns: ns,
cmd: "arp",
// Use -n flag to avoid resolving the hostnames of the neighbors. // Use -n flag to avoid resolving the hostnames of the neighbors.
// By default ARP attempts to resolve the hostnames via DNS. See // By default ARP attempts to resolve the hostnames via DNS. See
// man 8 arp. // man 8 arp.
@ -51,10 +53,11 @@ func newARPDB() (arp *arpdbs) {
}, },
// Finally, try "ip neigh". // Finally, try "ip neigh".
&cmdARPDB{ &cmdARPDB{
parse: parseIPNeigh, logger: logger,
ns: ns, parse: parseIPNeigh,
cmd: "ip", ns: ns,
args: []string{"neigh"}, cmd: "ip",
args: []string{"neigh"},
}, },
) )
} }
@ -131,7 +134,7 @@ func (arp *fsysARPDB) Neighbors() (ns []Neighbor) {
// //
// IP address HW type Flags HW address Mask Device // IP address HW type Flags HW address Mask Device
// 192.168.11.98 0x1 0x2 5a:92:df:a9:7e:28 * wan // 192.168.11.98 0x1 0x2 5a:92:df:a9:7e:28 * wan
func parseArpAWrt(sc *bufio.Scanner, lenHint int) (ns []Neighbor) { func parseArpAWrt(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
if !sc.Scan() { if !sc.Scan() {
// Skip the header. // Skip the header.
return return
@ -146,25 +149,14 @@ func parseArpAWrt(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
continue continue
} }
ip, err := netip.ParseAddr(fields[0]) n, err := newNeighbor("", fields[0], fields[3])
if err != nil { if err != nil {
log.Debug("arpdb: parsing arp output: ip: %s", err) logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
continue continue
} }
hwStr := fields[3] ns = append(ns, *n)
mac, err := net.ParseMAC(hwStr)
if err != nil {
log.Debug("arpdb: parsing arp output: mac: %s", err)
continue
}
ns = append(ns, Neighbor{
IP: ip,
MAC: mac,
})
} }
return ns return ns
@ -174,7 +166,7 @@ func parseArpAWrt(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
// expected input format: // expected input format:
// //
// hostname (192.168.1.1) at ab:cd:ef:ab:cd:ef [ether] on enp0s3 // hostname (192.168.1.1) at ab:cd:ef:ab:cd:ef [ether] on enp0s3
func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) { func parseArpA(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
ns = make([]Neighbor, 0, lenHint) ns = make([]Neighbor, 0, lenHint)
for sc.Scan() { for sc.Scan() {
ln := sc.Text() ln := sc.Text()
@ -189,26 +181,15 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
continue continue
} }
ip, err := netip.ParseAddr(ipStr[1 : len(ipStr)-1]) host := validatedHostname(logger, fields[0])
n, err := newNeighbor(host, ipStr[1:len(ipStr)-1], fields[3])
if err != nil { if err != nil {
log.Debug("arpdb: parsing arp output: ip: %s", err) logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
continue continue
} }
hwStr := fields[3] ns = append(ns, *n)
mac, err := net.ParseMAC(hwStr)
if err != nil {
log.Debug("arpdb: parsing arp output: mac: %s", err)
continue
}
ns = append(ns, Neighbor{
IP: ip,
MAC: mac,
Name: validatedHostname(fields[0]),
})
} }
return ns return ns
@ -218,7 +199,7 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
// expected input format: // expected input format:
// //
// 192.168.1.1 dev enp0s3 lladdr ab:cd:ef:ab:cd:ef REACHABLE // 192.168.1.1 dev enp0s3 lladdr ab:cd:ef:ab:cd:ef REACHABLE
func parseIPNeigh(sc *bufio.Scanner, lenHint int) (ns []Neighbor) { func parseIPNeigh(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
ns = make([]Neighbor, 0, lenHint) ns = make([]Neighbor, 0, lenHint)
for sc.Scan() { for sc.Scan() {
ln := sc.Text() ln := sc.Text()
@ -228,27 +209,14 @@ func parseIPNeigh(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
continue continue
} }
n := Neighbor{} n, err := newNeighbor("", fields[0], fields[4])
ip, err := netip.ParseAddr(fields[0])
if err != nil { if err != nil {
log.Debug("arpdb: parsing arp output: ip: %s", err) logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
continue continue
} else {
n.IP = ip
} }
mac, err := net.ParseMAC(fields[4]) ns = append(ns, *n)
if err != nil {
log.Debug("arpdb: parsing arp output: mac: %s", err)
continue
} else {
n.MAC = mac
}
ns = append(ns, n)
} }
return ns return ns

View file

@ -9,6 +9,7 @@ import (
"testing" "testing"
"testing/fstest" "testing/fstest"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -69,9 +70,10 @@ func TestCmdARPDB_linux(t *testing.T) {
t.Run("wrt", func(t *testing.T) { t.Run("wrt", func(t *testing.T) {
a := &cmdARPDB{ a := &cmdARPDB{
parse: parseArpAWrt, logger: slogutil.NewDiscardLogger(),
cmd: "arp", parse: parseArpAWrt,
args: []string{"-a"}, cmd: "arp",
args: []string{"-a"},
ns: &neighs{ ns: &neighs{
mu: &sync.RWMutex{}, mu: &sync.RWMutex{},
ns: make([]Neighbor, 0), ns: make([]Neighbor, 0),
@ -86,9 +88,10 @@ func TestCmdARPDB_linux(t *testing.T) {
t.Run("ip_neigh", func(t *testing.T) { t.Run("ip_neigh", func(t *testing.T) {
a := &cmdARPDB{ a := &cmdARPDB{
parse: parseIPNeigh, logger: slogutil.NewDiscardLogger(),
cmd: "ip", parse: parseIPNeigh,
args: []string{"neigh"}, cmd: "ip",
args: []string{"neigh"},
ns: &neighs{ ns: &neighs{
mu: &sync.RWMutex{}, mu: &sync.RWMutex{},
ns: make([]Neighbor, 0), ns: make([]Neighbor, 0),

View file

@ -4,17 +4,17 @@ package arpdb
import ( import (
"bufio" "bufio"
"net" "log/slog"
"net/netip"
"strings" "strings"
"sync" "sync"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/logutil/slogutil"
) )
func newARPDB() (arp *cmdARPDB) { func newARPDB(logger *slog.Logger) (arp *cmdARPDB) {
return &cmdARPDB{ return &cmdARPDB{
parse: parseArpA, logger: logger,
parse: parseArpA,
ns: &neighs{ ns: &neighs{
mu: &sync.RWMutex{}, mu: &sync.RWMutex{},
ns: make([]Neighbor, 0), ns: make([]Neighbor, 0),
@ -34,7 +34,7 @@ func newARPDB() (arp *cmdARPDB) {
// //
// Host Ethernet Address Netif Expire Flags // Host Ethernet Address Netif Expire Flags
// 192.168.1.1 ab:cd:ef:ab:cd:ef em0 19m59s // 192.168.1.1 ab:cd:ef:ab:cd:ef em0 19m59s
func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) { func parseArpA(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
// Skip the header. // Skip the header.
if !sc.Scan() { if !sc.Scan() {
return nil return nil
@ -49,27 +49,14 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
continue continue
} }
n := Neighbor{} n, err := newNeighbor("", fields[0], fields[1])
ip, err := netip.ParseAddr(fields[0])
if err != nil { if err != nil {
log.Debug("arpdb: parsing arp output: ip: %s", err) logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
continue continue
} else {
n.IP = ip
} }
mac, err := net.ParseMAC(fields[1]) ns = append(ns, *n)
if err != nil {
log.Debug("arpdb: parsing arp output: mac: %s", err)
continue
} else {
n.MAC = mac
}
ns = append(ns, n)
} }
return ns return ns

View file

@ -4,17 +4,17 @@ package arpdb
import ( import (
"bufio" "bufio"
"net" "log/slog"
"net/netip"
"strings" "strings"
"sync" "sync"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/logutil/slogutil"
) )
func newARPDB() (arp *cmdARPDB) { func newARPDB(logger *slog.Logger) (arp *cmdARPDB) {
return &cmdARPDB{ return &cmdARPDB{
parse: parseArpA, logger: logger,
parse: parseArpA,
ns: &neighs{ ns: &neighs{
mu: &sync.RWMutex{}, mu: &sync.RWMutex{},
ns: make([]Neighbor, 0), ns: make([]Neighbor, 0),
@ -31,7 +31,7 @@ func newARPDB() (arp *cmdARPDB) {
// Internet Address Physical Address Type // Internet Address Physical Address Type
// 192.168.56.1 0a-00-27-00-00-00 dynamic // 192.168.56.1 0a-00-27-00-00-00 dynamic
// 192.168.56.255 ff-ff-ff-ff-ff-ff static // 192.168.56.255 ff-ff-ff-ff-ff-ff static
func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) { func parseArpA(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
ns = make([]Neighbor, 0, lenHint) ns = make([]Neighbor, 0, lenHint)
for sc.Scan() { for sc.Scan() {
ln := sc.Text() ln := sc.Text()
@ -44,24 +44,14 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
continue continue
} }
ip, err := netip.ParseAddr(fields[0]) n, err := newNeighbor("", fields[0], fields[1])
if err != nil { if err != nil {
log.Debug("arpdb: parsing arp output: ip: %s", err) logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
continue continue
} }
mac, err := net.ParseMAC(fields[1]) ns = append(ns, *n)
if err != nil {
log.Debug("arpdb: parsing arp output: mac: %s", err)
continue
}
ns = append(ns, Neighbor{
IP: ip,
MAC: mac,
})
} }
return ns return ns

View file

@ -2,6 +2,7 @@ package client
import ( import (
"context" "context"
"log/slog"
"net/netip" "net/netip"
"sync" "sync"
"time" "time"
@ -11,6 +12,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/whois" "github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
) )
@ -38,6 +40,10 @@ func (EmptyAddrProc) Close() (_ error) { return nil }
// DefaultAddrProcConfig is the configuration structure for address processors. // DefaultAddrProcConfig is the configuration structure for address processors.
type DefaultAddrProcConfig struct { type DefaultAddrProcConfig struct {
// BaseLogger is used to create loggers with custom prefixes for sources of
// information about runtime clients. It must not be nil.
BaseLogger *slog.Logger
// DialContext is used to create TCP connections to WHOIS servers. // DialContext is used to create TCP connections to WHOIS servers.
// DialContext must not be nil if [DefaultAddrProcConfig.UseWHOIS] is true. // DialContext must not be nil if [DefaultAddrProcConfig.UseWHOIS] is true.
DialContext aghnet.DialContextFunc DialContext aghnet.DialContextFunc
@ -147,6 +153,7 @@ func NewDefaultAddrProc(c *DefaultAddrProcConfig) (p *DefaultAddrProc) {
if c.UseRDNS { if c.UseRDNS {
p.rdns = rdns.New(&rdns.Config{ p.rdns = rdns.New(&rdns.Config{
Logger: c.BaseLogger.With(slogutil.KeyPrefix, "rdns"),
Exchanger: c.Exchanger, Exchanger: c.Exchanger,
CacheSize: defaultCacheSize, CacheSize: defaultCacheSize,
CacheTTL: defaultIPTTL, CacheTTL: defaultIPTTL,
@ -154,7 +161,7 @@ func NewDefaultAddrProc(c *DefaultAddrProcConfig) (p *DefaultAddrProc) {
} }
if c.UseWHOIS { if c.UseWHOIS {
p.whois = newWHOIS(c.DialContext) p.whois = newWHOIS(c.BaseLogger.With(slogutil.KeyPrefix, "whois"), c.DialContext)
} }
go p.process(c.CatchPanics) go p.process(c.CatchPanics)
@ -168,7 +175,7 @@ func NewDefaultAddrProc(c *DefaultAddrProcConfig) (p *DefaultAddrProc) {
// newWHOIS returns a whois.Interface instance using the given function for // newWHOIS returns a whois.Interface instance using the given function for
// dialing. // dialing.
func newWHOIS(dialFunc aghnet.DialContextFunc) (w whois.Interface) { func newWHOIS(logger *slog.Logger, dialFunc aghnet.DialContextFunc) (w whois.Interface) {
// TODO(s.chzhen): Consider making configurable. // TODO(s.chzhen): Consider making configurable.
const ( const (
// defaultTimeout is the timeout for WHOIS requests. // defaultTimeout is the timeout for WHOIS requests.
@ -186,6 +193,7 @@ func newWHOIS(dialFunc aghnet.DialContextFunc) (w whois.Interface) {
) )
return whois.New(&whois.Config{ return whois.New(&whois.Config{
Logger: logger,
DialContext: dialFunc, DialContext: dialFunc,
ServerAddr: whois.DefaultServer, ServerAddr: whois.DefaultServer,
Port: whois.DefaultPort, Port: whois.DefaultPort,
@ -227,9 +235,11 @@ func (p *DefaultAddrProc) process(catchPanics bool) {
log.Info("clients: processing addresses") log.Info("clients: processing addresses")
ctx := context.TODO()
for ip := range p.clientIPs { for ip := range p.clientIPs {
host := p.processRDNS(ip) host := p.processRDNS(ctx, ip)
info := p.processWHOIS(ip) info := p.processWHOIS(ctx, ip)
p.addrUpdater.UpdateAddress(ip, host, info) p.addrUpdater.UpdateAddress(ip, host, info)
} }
@ -239,7 +249,7 @@ func (p *DefaultAddrProc) process(catchPanics bool) {
// processRDNS resolves the clients' IP addresses using reverse DNS. host is // processRDNS resolves the clients' IP addresses using reverse DNS. host is
// empty if there were errors or if the information hasn't changed. // empty if there were errors or if the information hasn't changed.
func (p *DefaultAddrProc) processRDNS(ip netip.Addr) (host string) { func (p *DefaultAddrProc) processRDNS(ctx context.Context, ip netip.Addr) (host string) {
start := time.Now() start := time.Now()
log.Debug("clients: processing %s with rdns", ip) log.Debug("clients: processing %s with rdns", ip)
defer func() { defer func() {
@ -251,7 +261,7 @@ func (p *DefaultAddrProc) processRDNS(ip netip.Addr) (host string) {
return return
} }
host, changed := p.rdns.Process(ip) host, changed := p.rdns.Process(ctx, ip)
if !changed { if !changed {
host = "" host = ""
} }
@ -268,7 +278,7 @@ func (p *DefaultAddrProc) shouldResolve(ip netip.Addr) (ok bool) {
// processWHOIS looks up the information about clients' IP addresses in the // processWHOIS looks up the information about clients' IP addresses in the
// WHOIS databases. info is nil if there were errors or if the information // WHOIS databases. info is nil if there were errors or if the information
// hasn't changed. // hasn't changed.
func (p *DefaultAddrProc) processWHOIS(ip netip.Addr) (info *whois.Info) { func (p *DefaultAddrProc) processWHOIS(ctx context.Context, ip netip.Addr) (info *whois.Info) {
start := time.Now() start := time.Now()
log.Debug("clients: processing %s with whois", ip) log.Debug("clients: processing %s with whois", ip)
defer func() { defer func() {
@ -277,7 +287,7 @@ func (p *DefaultAddrProc) processWHOIS(ip netip.Addr) (info *whois.Info) {
// TODO(s.chzhen): Move the timeout logic from WHOIS configuration to the // TODO(s.chzhen): Move the timeout logic from WHOIS configuration to the
// context. // context.
info, changed := p.whois.Process(context.Background(), ip) info, changed := p.whois.Process(ctx, ip)
if !changed { if !changed {
info = nil info = nil
} }

View file

@ -13,6 +13,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/client" "github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/whois" "github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/testutil/fakenet" "github.com/AdguardTeam/golibs/testutil/fakenet"
@ -99,6 +100,7 @@ func TestDefaultAddrProc_Process_rDNS(t *testing.T) {
updInfoCh := make(chan *whois.Info, 1) updInfoCh := make(chan *whois.Info, 1)
p := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{ p := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{
BaseLogger: slogutil.NewDiscardLogger(),
DialContext: func(_ context.Context, _, _ string) (conn net.Conn, err error) { DialContext: func(_ context.Context, _, _ string) (conn net.Conn, err error) {
panic("not implemented") panic("not implemented")
}, },
@ -208,6 +210,7 @@ func TestDefaultAddrProc_Process_WHOIS(t *testing.T) {
updInfoCh := make(chan *whois.Info, 1) updInfoCh := make(chan *whois.Info, 1)
p := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{ p := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{
BaseLogger: slogutil.NewDiscardLogger(),
DialContext: func(_ context.Context, _, _ string) (conn net.Conn, err error) { DialContext: func(_ context.Context, _, _ string) (conn net.Conn, err error) {
return whoisConn, nil return whoisConn, nil
}, },

View file

@ -8,6 +8,7 @@ import (
"encoding" "encoding"
"fmt" "fmt"
"net/netip" "net/netip"
"slices"
"github.com/AdguardTeam/AdGuardHome/internal/whois" "github.com/AdguardTeam/AdGuardHome/internal/whois"
) )
@ -118,8 +119,9 @@ func (r *Runtime) Info() (cs Source, host string) {
return cs, info[0] return cs, info[0]
} }
// SetInfo sets a host as a client information from the cs. // setInfo sets a host as a client information from the cs.
func (r *Runtime) SetInfo(cs Source, hosts []string) { func (r *Runtime) setInfo(cs Source, hosts []string) {
// TODO(s.chzhen): Use contract where hosts must contain non-empty host.
if len(hosts) == 1 && hosts[0] == "" { if len(hosts) == 1 && hosts[0] == "" {
hosts = []string{} hosts = []string{}
} }
@ -136,13 +138,13 @@ func (r *Runtime) SetInfo(cs Source, hosts []string) {
} }
} }
// WHOIS returns a WHOIS client information. // WHOIS returns a copy of WHOIS client information.
func (r *Runtime) WHOIS() (info *whois.Info) { func (r *Runtime) WHOIS() (info *whois.Info) {
return r.whois return r.whois.Clone()
} }
// SetWHOIS sets a WHOIS client information. info must be non-nil. // setWHOIS sets a WHOIS client information. info must be non-nil.
func (r *Runtime) SetWHOIS(info *whois.Info) { func (r *Runtime) setWHOIS(info *whois.Info) {
r.whois = info r.whois = info
} }
@ -175,3 +177,15 @@ func (r *Runtime) isEmpty() (ok bool) {
func (r *Runtime) Addr() (ip netip.Addr) { func (r *Runtime) Addr() (ip netip.Addr) {
return r.ip return r.ip
} }
// clone returns a deep copy of the runtime client.
func (r *Runtime) clone() (c *Runtime) {
return &Runtime{
ip: r.ip,
whois: r.whois.Clone(),
arp: slices.Clone(r.arp),
rdns: slices.Clone(r.rdns),
dhcp: slices.Clone(r.dhcp),
hostsFile: slices.Clone(r.hostsFile),
}
}

View file

@ -13,7 +13,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch" "github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
@ -136,7 +135,8 @@ type Persistent struct {
} }
// validate returns an error if persistent client information contains errors. // validate returns an error if persistent client information contains errors.
func (c *Persistent) validate(allTags *container.MapSet[string]) (err error) { // allTags must be sorted.
func (c *Persistent) validate(allTags []string) (err error) {
switch { switch {
case c.Name == "": case c.Name == "":
return errors.Error("empty name") return errors.Error("empty name")
@ -157,7 +157,8 @@ func (c *Persistent) validate(allTags *container.MapSet[string]) (err error) {
} }
for _, t := range c.Tags { for _, t := range c.Tags {
if !allTags.Has(t) { _, ok := slices.BinarySearch(allTags, t)
if !ok {
return fmt.Errorf("invalid tag: %q", t) return fmt.Errorf("invalid tag: %q", t)
} }
} }

View file

@ -1,11 +1,8 @@
package client package client
import ( import (
"net/netip"
"testing" "testing"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -125,69 +122,3 @@ func TestPersistent_EqualIDs(t *testing.T) {
}) })
} }
} }
func TestPersistent_Validate(t *testing.T) {
const (
allowedTag = "allowed_tag"
notAllowedTag = "not_allowed_tag"
)
allowedTags := container.NewMapSet(allowedTag)
testCases := []struct {
name string
cli *Persistent
wantErrMsg string
}{{
name: "success",
cli: &Persistent{
Name: "basic",
IPs: []netip.Addr{
netip.MustParseAddr("1.2.3.4"),
},
UID: MustNewUID(),
},
wantErrMsg: "",
}, {
name: "empty_name",
cli: &Persistent{
Name: "",
},
wantErrMsg: "empty name",
}, {
name: "no_id",
cli: &Persistent{
Name: "no_id",
},
wantErrMsg: "id required",
}, {
name: "no_uid",
cli: &Persistent{
Name: "no_uid",
IPs: []netip.Addr{
netip.MustParseAddr("1.2.3.4"),
},
},
wantErrMsg: "uid required",
}, {
name: "not_allowed_tag",
cli: &Persistent{
Name: "basic",
IPs: []netip.Addr{
netip.MustParseAddr("1.2.3.4"),
},
UID: MustNewUID(),
Tags: []string{
notAllowedTag,
},
},
wantErrMsg: `invalid tag: "` + notAllowedTag + `"`,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.cli.validate(allowedTags)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
})
}
}

View file

@ -2,39 +2,34 @@ package client
import "net/netip" import "net/netip"
// RuntimeIndex stores information about runtime clients. // runtimeIndex stores information about runtime clients.
type RuntimeIndex struct { type runtimeIndex struct {
// index maps IP address to runtime client. // index maps IP address to runtime client.
index map[netip.Addr]*Runtime index map[netip.Addr]*Runtime
} }
// NewRuntimeIndex returns initialized runtime index. // newRuntimeIndex returns initialized runtime index.
func NewRuntimeIndex() (ri *RuntimeIndex) { func newRuntimeIndex() (ri *runtimeIndex) {
return &RuntimeIndex{ return &runtimeIndex{
index: map[netip.Addr]*Runtime{}, index: map[netip.Addr]*Runtime{},
} }
} }
// Client returns the saved runtime client by ip. If no such client exists, // client returns the saved runtime client by ip. If no such client exists,
// returns nil. // returns nil.
func (ri *RuntimeIndex) Client(ip netip.Addr) (rc *Runtime) { func (ri *runtimeIndex) client(ip netip.Addr) (rc *Runtime) {
return ri.index[ip] return ri.index[ip]
} }
// Add saves the runtime client in the index. IP address of a client must be // add saves the runtime client in the index. IP address of a client must be
// unique. See [Runtime.Client]. rc must not be nil. // unique. See [Runtime.Client]. rc must not be nil.
func (ri *RuntimeIndex) Add(rc *Runtime) { func (ri *runtimeIndex) add(rc *Runtime) {
ip := rc.Addr() ip := rc.Addr()
ri.index[ip] = rc ri.index[ip] = rc
} }
// Size returns the number of the runtime clients. // rangeClients calls f for each runtime client in an undefined order.
func (ri *RuntimeIndex) Size() (n int) { func (ri *runtimeIndex) rangeClients(f func(rc *Runtime) (cont bool)) {
return len(ri.index)
}
// Range calls f for each runtime client in an undefined order.
func (ri *RuntimeIndex) Range(f func(rc *Runtime) (cont bool)) {
for _, rc := range ri.index { for _, rc := range ri.index {
if !f(rc) { if !f(rc) {
return return
@ -42,17 +37,31 @@ func (ri *RuntimeIndex) Range(f func(rc *Runtime) (cont bool)) {
} }
} }
// Delete removes the runtime client by ip. // setInfo sets the client information from cs for runtime client stored by ip.
func (ri *RuntimeIndex) Delete(ip netip.Addr) { // If no such client exists, it creates one.
delete(ri.index, ip) func (ri *runtimeIndex) setInfo(ip netip.Addr, cs Source, hosts []string) (rc *Runtime) {
rc = ri.index[ip]
if rc == nil {
rc = NewRuntime(ip)
ri.add(rc)
}
rc.setInfo(cs, hosts)
return rc
} }
// DeleteBySource removes all runtime clients that have information only from // clearSource removes information from the specified source from all clients.
// the specified source and returns the number of removed clients. func (ri *runtimeIndex) clearSource(src Source) {
func (ri *RuntimeIndex) DeleteBySource(src Source) (n int) { for _, rc := range ri.index {
for ip, rc := range ri.index {
rc.unset(src) rc.unset(src)
}
}
// removeEmpty removes empty runtime clients and returns the number of removed
// clients.
func (ri *runtimeIndex) removeEmpty() (n int) {
for ip, rc := range ri.index {
if rc.isEmpty() { if rc.isEmpty() {
delete(ri.index, ip) delete(ri.index, ip)
n++ n++

View file

@ -1,85 +0,0 @@
package client_test
import (
"net/netip"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/stretchr/testify/assert"
)
func TestRuntimeIndex(t *testing.T) {
const cliSrc = client.SourceARP
var (
ip1 = netip.MustParseAddr("1.1.1.1")
ip2 = netip.MustParseAddr("2.2.2.2")
ip3 = netip.MustParseAddr("3.3.3.3")
)
ri := client.NewRuntimeIndex()
currentSize := 0
testCases := []struct {
ip netip.Addr
name string
hosts []string
src client.Source
}{{
src: cliSrc,
ip: ip1,
name: "1",
hosts: []string{"host1"},
}, {
src: cliSrc,
ip: ip2,
name: "2",
hosts: []string{"host2"},
}, {
src: cliSrc,
ip: ip3,
name: "3",
hosts: []string{"host3"},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
rc := client.NewRuntime(tc.ip)
rc.SetInfo(tc.src, tc.hosts)
ri.Add(rc)
currentSize++
got := ri.Client(tc.ip)
assert.Equal(t, rc, got)
})
}
t.Run("size", func(t *testing.T) {
assert.Equal(t, currentSize, ri.Size())
})
t.Run("range", func(t *testing.T) {
s := 0
ri.Range(func(rc *client.Runtime) (cont bool) {
s++
return true
})
assert.Equal(t, currentSize, s)
})
t.Run("delete", func(t *testing.T) {
ri.Delete(ip1)
currentSize--
assert.Equal(t, currentSize, ri.Size())
})
t.Run("delete_by_src", func(t *testing.T) {
assert.Equal(t, currentSize, ri.DeleteBySource(cliSrc))
assert.Equal(t, 0, ri.Size())
})
}

View file

@ -1,29 +1,113 @@
package client package client
import ( import (
"context"
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"slices"
"sync" "sync"
"time"
"github.com/AdguardTeam/golibs/container" "github.com/AdguardTeam/AdGuardHome/internal/arpdb"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
// Config is the client storage configuration structure. // allowedTags is the list of available client tags.
// var allowedTags = []string{
// TODO(s.chzhen): Expand. "device_audio",
type Config struct { "device_camera",
// AllowedTags is a list of all allowed client tags. "device_gameconsole",
AllowedTags []string "device_laptop",
"device_nas", // Network-attached Storage
"device_other",
"device_pc",
"device_phone",
"device_printer",
"device_securityalarm",
"device_tablet",
"device_tv",
"os_android",
"os_ios",
"os_linux",
"os_macos",
"os_other",
"os_windows",
"user_admin",
"user_child",
"user_regular",
}
// DHCP is an interface for accessing DHCP lease data the [Storage] needs.
type DHCP interface {
// Leases returns all the DHCP leases.
Leases() (leases []*dhcpsvc.Lease)
// HostByIP returns the hostname of the DHCP client with the given IP
// address. host will be empty if there is no such client, due to an
// assumption that a DHCP client must always have a hostname.
HostByIP(ip netip.Addr) (host string)
// MACByIP returns the MAC address for the given IP address leased. It
// returns nil if there is no such client, due to an assumption that a DHCP
// client must always have a MAC address.
MACByIP(ip netip.Addr) (mac net.HardwareAddr)
}
// EmptyDHCP is the empty [DHCP] implementation that does nothing.
type EmptyDHCP struct{}
// type check
var _ DHCP = EmptyDHCP{}
// Leases implements the [DHCP] interface for emptyDHCP.
func (EmptyDHCP) Leases() (leases []*dhcpsvc.Lease) { return nil }
// HostByIP implements the [DHCP] interface for emptyDHCP.
func (EmptyDHCP) HostByIP(_ netip.Addr) (host string) { return "" }
// MACByIP implements the [DHCP] interface for emptyDHCP.
func (EmptyDHCP) MACByIP(_ netip.Addr) (mac net.HardwareAddr) { return nil }
// HostsContainer is an interface for receiving updates to the system hosts
// file.
type HostsContainer interface {
Upd() (updates <-chan *hostsfile.DefaultStorage)
}
// StorageConfig is the client storage configuration structure.
type StorageConfig struct {
// DHCP is used to match IPs against MACs of persistent clients and update
// [SourceDHCP] runtime client information. It must not be nil.
DHCP DHCP
// EtcHosts is used to update [SourceHostsFile] runtime client information.
EtcHosts HostsContainer
// ARPDB is used to update [SourceARP] runtime client information.
ARPDB arpdb.Interface
// InitialClients is a list of persistent clients parsed from the
// configuration file. Each client must not be nil.
InitialClients []*Persistent
// ARPClientsUpdatePeriod defines how often [SourceARP] runtime client
// information is updated.
ARPClientsUpdatePeriod time.Duration
// RuntimeSourceDHCP specifies whether to update [SourceDHCP] information
// of runtime clients.
RuntimeSourceDHCP bool
} }
// Storage contains information about persistent and runtime clients. // Storage contains information about persistent and runtime clients.
type Storage struct { type Storage struct {
// allowedTags is a set of all allowed tags.
allowedTags *container.MapSet[string]
// mu protects indexes of persistent and runtime clients. // mu protects indexes of persistent and runtime clients.
mu *sync.Mutex mu *sync.Mutex
@ -31,21 +115,250 @@ type Storage struct {
index *index index *index
// runtimeIndex contains information about runtime clients. // runtimeIndex contains information about runtime clients.
runtimeIndex *runtimeIndex
// dhcp is used to update [SourceDHCP] runtime client information.
dhcp DHCP
// etcHosts is used to update [SourceHostsFile] runtime client information.
etcHosts HostsContainer
// arpDB is used to update [SourceARP] runtime client information.
arpDB arpdb.Interface
// done is the shutdown signaling channel.
done chan struct{}
// allowedTags is a sorted list of all allowed tags. It must not be
// modified after initialization.
// //
// TODO(s.chzhen): Use it. // TODO(s.chzhen): Use custom type.
runtimeIndex *RuntimeIndex allowedTags []string
// arpClientsUpdatePeriod defines how often [SourceARP] runtime client
// information is updated. It must be greater than zero.
arpClientsUpdatePeriod time.Duration
// runtimeSourceDHCP specifies whether to update [SourceDHCP] information
// of runtime clients.
runtimeSourceDHCP bool
} }
// NewStorage returns initialized client storage. conf must not be nil. // NewStorage returns initialized client storage. conf must not be nil.
func NewStorage(conf *Config) (s *Storage) { func NewStorage(conf *StorageConfig) (s *Storage, err error) {
allowedTags := container.NewMapSet(conf.AllowedTags...) tags := slices.Clone(allowedTags)
slices.Sort(tags)
return &Storage{ s = &Storage{
allowedTags: allowedTags, allowedTags: tags,
mu: &sync.Mutex{}, mu: &sync.Mutex{},
index: newIndex(), index: newIndex(),
runtimeIndex: NewRuntimeIndex(), runtimeIndex: newRuntimeIndex(),
dhcp: conf.DHCP,
etcHosts: conf.EtcHosts,
arpDB: conf.ARPDB,
done: make(chan struct{}),
arpClientsUpdatePeriod: conf.ARPClientsUpdatePeriod,
runtimeSourceDHCP: conf.RuntimeSourceDHCP,
} }
for i, p := range conf.InitialClients {
err = s.Add(p)
if err != nil {
return nil, fmt.Errorf("adding client %q at index %d: %w", p.Name, i, err)
}
}
s.ReloadARP()
return s, nil
}
// Start starts the goroutines for updating the runtime client information.
//
// TODO(s.chzhen): Pass context.
func (s *Storage) Start(_ context.Context) (err error) {
go s.periodicARPUpdate()
go s.handleHostsUpdates()
return nil
}
// Shutdown gracefully stops the client storage.
//
// TODO(s.chzhen): Pass context.
func (s *Storage) Shutdown(_ context.Context) (err error) {
close(s.done)
return s.closeUpstreams()
}
// periodicARPUpdate periodically reloads runtime clients from ARP. It is
// intended to be used as a goroutine.
func (s *Storage) periodicARPUpdate() {
defer log.OnPanic("storage")
t := time.NewTicker(s.arpClientsUpdatePeriod)
for {
select {
case <-t.C:
s.ReloadARP()
case <-s.done:
return
}
}
}
// ReloadARP reloads runtime clients from ARP, if configured.
func (s *Storage) ReloadARP() {
if s.arpDB != nil {
s.addFromSystemARP()
}
}
// addFromSystemARP adds the IP-hostname pairings from the output of the arp -a
// command.
func (s *Storage) addFromSystemARP() {
s.mu.Lock()
defer s.mu.Unlock()
if err := s.arpDB.Refresh(); err != nil {
s.arpDB = arpdb.Empty{}
log.Error("refreshing arp container: %s", err)
return
}
ns := s.arpDB.Neighbors()
if len(ns) == 0 {
log.Debug("refreshing arp container: the update is empty")
return
}
src := SourceARP
s.runtimeIndex.clearSource(src)
for _, n := range ns {
s.runtimeIndex.setInfo(n.IP, src, []string{n.Name})
}
removed := s.runtimeIndex.removeEmpty()
log.Debug("storage: added %d, removed %d client aliases from arp neighborhood", len(ns), removed)
}
// handleHostsUpdates receives the updates from the hosts container and adds
// them to the clients storage. It is intended to be used as a goroutine.
func (s *Storage) handleHostsUpdates() {
if s.etcHosts == nil {
return
}
defer log.OnPanic("storage")
for {
select {
case upd, ok := <-s.etcHosts.Upd():
if !ok {
return
}
s.addFromHostsFile(upd)
case <-s.done:
return
}
}
}
// addFromHostsFile fills the client-hostname pairing index from the system's
// hosts files.
func (s *Storage) addFromHostsFile(hosts *hostsfile.DefaultStorage) {
s.mu.Lock()
defer s.mu.Unlock()
src := SourceHostsFile
s.runtimeIndex.clearSource(src)
added := 0
hosts.RangeNames(func(addr netip.Addr, names []string) (cont bool) {
// Only the first name of the first record is considered a canonical
// hostname for the IP address.
//
// TODO(e.burkov): Consider using all the names from all the records.
s.runtimeIndex.setInfo(addr, src, []string{names[0]})
added++
return true
})
removed := s.runtimeIndex.removeEmpty()
log.Debug("storage: added %d, removed %d client aliases from system hosts file", added, removed)
}
// type check
var _ AddressUpdater = (*Storage)(nil)
// UpdateAddress implements the [AddressUpdater] interface for *Storage
func (s *Storage) UpdateAddress(ip netip.Addr, host string, info *whois.Info) {
// Common fast path optimization.
if host == "" && info == nil {
return
}
s.mu.Lock()
defer s.mu.Unlock()
if host != "" {
s.runtimeIndex.setInfo(ip, SourceRDNS, []string{host})
}
if info != nil {
s.setWHOISInfo(ip, info)
}
}
// UpdateDHCP updates [SourceDHCP] runtime client information.
func (s *Storage) UpdateDHCP() {
if s.dhcp == nil || !s.runtimeSourceDHCP {
return
}
s.mu.Lock()
defer s.mu.Unlock()
src := SourceDHCP
s.runtimeIndex.clearSource(src)
added := 0
for _, l := range s.dhcp.Leases() {
s.runtimeIndex.setInfo(l.IP, src, []string{l.Hostname})
added++
}
removed := s.runtimeIndex.removeEmpty()
log.Debug("storage: added %d, removed %d client aliases from dhcp", added, removed)
}
// setWHOISInfo sets the WHOIS information for a runtime client.
func (s *Storage) setWHOISInfo(ip netip.Addr, wi *whois.Info) {
_, ok := s.index.findByIP(ip)
if ok {
log.Debug("storage: client for %s is already created, ignore whois info", ip)
return
}
rc := s.runtimeIndex.client(ip)
if rc == nil {
rc = NewRuntime(ip)
s.runtimeIndex.add(rc)
}
rc.setWHOIS(wi)
log.Debug("storage: set whois info for runtime client with ip %s: %+v", ip, wi)
} }
// Add stores persistent client information or returns an error. // Add stores persistent client information or returns an error.
@ -95,6 +408,9 @@ func (s *Storage) FindByName(name string) (p *Persistent, ok bool) {
// Find finds persistent client by string representation of the client ID, IP // Find finds persistent client by string representation of the client ID, IP
// address, or MAC. And returns its shallow copy. // address, or MAC. And returns its shallow copy.
//
// TODO(s.chzhen): Accept ClientIDData structure instead, which will contain
// the parsed IP address, if any.
func (s *Storage) Find(id string) (p *Persistent, ok bool) { func (s *Storage) Find(id string) (p *Persistent, ok bool) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -104,6 +420,16 @@ func (s *Storage) Find(id string) (p *Persistent, ok bool) {
return p.ShallowClone(), ok return p.ShallowClone(), ok
} }
ip, err := netip.ParseAddr(id)
if err != nil {
return nil, false
}
foundMAC := s.dhcp.MACByIP(ip)
if foundMAC != nil {
return s.FindByMAC(foundMAC)
}
return nil, false return nil, false
} }
@ -131,11 +457,9 @@ func (s *Storage) FindLoose(ip netip.Addr, id string) (p *Persistent, ok bool) {
return nil, false return nil, false
} }
// FindByMAC finds persistent client by MAC and returns its shallow copy. // FindByMAC finds persistent client by MAC and returns its shallow copy. s.mu
// is expected to be locked.
func (s *Storage) FindByMAC(mac net.HardwareAddr) (p *Persistent, ok bool) { func (s *Storage) FindByMAC(mac net.HardwareAddr) (p *Persistent, ok bool) {
s.mu.Lock()
defer s.mu.Unlock()
p, ok = s.index.findByMAC(mac) p, ok = s.index.findByMAC(mac)
if ok { if ok {
return p.ShallowClone(), ok return p.ShallowClone(), ok
@ -217,8 +541,8 @@ func (s *Storage) Size() (n int) {
return s.index.size() return s.index.size()
} }
// CloseUpstreams closes upstream configurations of persistent clients. // closeUpstreams closes upstream configurations of persistent clients.
func (s *Storage) CloseUpstreams() (err error) { func (s *Storage) closeUpstreams() (err error) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -227,63 +551,39 @@ func (s *Storage) CloseUpstreams() (err error) {
// ClientRuntime returns a copy of the saved runtime client by ip. If no such // ClientRuntime returns a copy of the saved runtime client by ip. If no such
// client exists, returns nil. // client exists, returns nil.
//
// TODO(s.chzhen): Use it.
func (s *Storage) ClientRuntime(ip netip.Addr) (rc *Runtime) { func (s *Storage) ClientRuntime(ip netip.Addr) (rc *Runtime) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
return s.runtimeIndex.Client(ip) rc = s.runtimeIndex.client(ip)
} if rc != nil {
return rc.clone()
}
// AddRuntime saves the runtime client information in the storage. IP address if !s.runtimeSourceDHCP {
// of a client must be unique. rc must not be nil. return nil
// }
// TODO(s.chzhen): Use it.
func (s *Storage) AddRuntime(rc *Runtime) {
s.mu.Lock()
defer s.mu.Unlock()
s.runtimeIndex.Add(rc) host := s.dhcp.HostByIP(ip)
} if host == "" {
return nil
}
// SizeRuntime returns the number of the runtime clients. rc = s.runtimeIndex.setInfo(ip, SourceDHCP, []string{host})
//
// TODO(s.chzhen): Use it.
func (s *Storage) SizeRuntime() (n int) {
s.mu.Lock()
defer s.mu.Unlock()
return s.runtimeIndex.Size() return rc.clone()
} }
// RangeRuntime calls f for each runtime client in an undefined order. // RangeRuntime calls f for each runtime client in an undefined order.
//
// TODO(s.chzhen): Use it.
func (s *Storage) RangeRuntime(f func(rc *Runtime) (cont bool)) { func (s *Storage) RangeRuntime(f func(rc *Runtime) (cont bool)) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
s.runtimeIndex.Range(f) s.runtimeIndex.rangeClients(f)
} }
// DeleteRuntime removes the runtime client by ip. // AllowedTags returns the list of available client tags. tags must not be
// // modified.
// TODO(s.chzhen): Use it. func (s *Storage) AllowedTags() (tags []string) {
func (s *Storage) DeleteRuntime(ip netip.Addr) { return s.allowedTags
s.mu.Lock()
defer s.mu.Unlock()
s.runtimeIndex.Delete(ip)
}
// DeleteBySource removes all runtime clients that have information only from
// the specified source and returns the number of removed clients.
//
// TODO(s.chzhen): Use it.
func (s *Storage) DeleteBySource(src Source) (n int) {
s.mu.Lock()
defer s.mu.Unlock()
return s.runtimeIndex.DeleteBySource(src)
} }

View file

@ -3,28 +3,521 @@ package client_test
import ( import (
"net" "net"
"net/netip" "net/netip"
"runtime"
"slices"
"sync"
"testing" "testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/arpdb"
"github.com/AdguardTeam/AdGuardHome/internal/client" "github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// testHostsContainer is a mock implementation of the [client.HostsContainer]
// interface.
type testHostsContainer struct {
onUpd func() (updates <-chan *hostsfile.DefaultStorage)
}
// type check
var _ client.HostsContainer = (*testHostsContainer)(nil)
// Upd implements the [client.HostsContainer] interface for *testHostsContainer.
func (c *testHostsContainer) Upd() (updates <-chan *hostsfile.DefaultStorage) {
return c.onUpd()
}
// Interface stores and refreshes the network neighborhood reported by ARP
// (Address Resolution Protocol).
type Interface interface {
// Refresh updates the stored data. It must be safe for concurrent use.
Refresh() (err error)
// Neighbors returnes the last set of data reported by ARP. Both the method
// and it's result must be safe for concurrent use.
Neighbors() (ns []arpdb.Neighbor)
}
// testARPDB is a mock implementation of the [arpdb.Interface].
type testARPDB struct {
onRefresh func() (err error)
onNeighbors func() (ns []arpdb.Neighbor)
}
// type check
var _ arpdb.Interface = (*testARPDB)(nil)
// Refresh implements the [arpdb.Interface] interface for *testARP.
func (c *testARPDB) Refresh() (err error) {
return c.onRefresh()
}
// Neighbors implements the [arpdb.Interface] interface for *testARP.
func (c *testARPDB) Neighbors() (ns []arpdb.Neighbor) {
return c.onNeighbors()
}
// testDHCP is a mock implementation of the [client.DHCP].
type testDHCP struct {
OnLeases func() (leases []*dhcpsvc.Lease)
OnHostBy func(ip netip.Addr) (host string)
OnMACBy func(ip netip.Addr) (mac net.HardwareAddr)
}
// type check
var _ client.DHCP = (*testDHCP)(nil)
// Lease implements the [client.DHCP] interface for *testDHCP.
func (t *testDHCP) Leases() (leases []*dhcpsvc.Lease) { return t.OnLeases() }
// HostByIP implements the [client.DHCP] interface for *testDHCP.
func (t *testDHCP) HostByIP(ip netip.Addr) (host string) { return t.OnHostBy(ip) }
// MACByIP implements the [client.DHCP] interface for *testDHCP.
func (t *testDHCP) MACByIP(ip netip.Addr) (mac net.HardwareAddr) { return t.OnMACBy(ip) }
// compareRuntimeInfo is a helper function that returns true if the runtime
// client has provided info.
func compareRuntimeInfo(rc *client.Runtime, src client.Source, host string) (ok bool) {
s, h := rc.Info()
if s != src {
return false
} else if h != host {
return false
}
return true
}
func TestStorage_Add_hostsfile(t *testing.T) {
var (
cliIP1 = netip.MustParseAddr("1.1.1.1")
cliName1 = "client_one"
cliIP2 = netip.MustParseAddr("2.2.2.2")
cliName2 = "client_two"
)
hostCh := make(chan *hostsfile.DefaultStorage)
h := &testHostsContainer{
onUpd: func() (updates <-chan *hostsfile.DefaultStorage) { return hostCh },
}
storage, err := client.NewStorage(&client.StorageConfig{
DHCP: client.EmptyDHCP{},
EtcHosts: h,
ARPClientsUpdatePeriod: testTimeout / 10,
})
require.NoError(t, err)
err = storage.Start(testutil.ContextWithTimeout(t, testTimeout))
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, func() (err error) {
return storage.Shutdown(testutil.ContextWithTimeout(t, testTimeout))
})
t.Run("add_hosts", func(t *testing.T) {
var s *hostsfile.DefaultStorage
s, err = hostsfile.NewDefaultStorage()
require.NoError(t, err)
s.Add(&hostsfile.Record{
Addr: cliIP1,
Names: []string{cliName1},
})
testutil.RequireSend(t, hostCh, s, testTimeout)
require.Eventually(t, func() (ok bool) {
cli1 := storage.ClientRuntime(cliIP1)
if cli1 == nil {
return false
}
assert.True(t, compareRuntimeInfo(cli1, client.SourceHostsFile, cliName1))
return true
}, testTimeout, testTimeout/10)
})
t.Run("update_hosts", func(t *testing.T) {
var s *hostsfile.DefaultStorage
s, err = hostsfile.NewDefaultStorage()
require.NoError(t, err)
s.Add(&hostsfile.Record{
Addr: cliIP2,
Names: []string{cliName2},
})
testutil.RequireSend(t, hostCh, s, testTimeout)
require.Eventually(t, func() (ok bool) {
cli2 := storage.ClientRuntime(cliIP2)
if cli2 == nil {
return false
}
assert.True(t, compareRuntimeInfo(cli2, client.SourceHostsFile, cliName2))
cli1 := storage.ClientRuntime(cliIP1)
require.Nil(t, cli1)
return true
}, testTimeout, testTimeout/10)
})
}
func TestStorage_Add_arp(t *testing.T) {
var (
mu sync.Mutex
neighbors []arpdb.Neighbor
cliIP1 = netip.MustParseAddr("1.1.1.1")
cliName1 = "client_one"
cliIP2 = netip.MustParseAddr("2.2.2.2")
cliName2 = "client_two"
)
a := &testARPDB{
onRefresh: func() (err error) { return nil },
onNeighbors: func() (ns []arpdb.Neighbor) {
mu.Lock()
defer mu.Unlock()
return neighbors
},
}
storage, err := client.NewStorage(&client.StorageConfig{
DHCP: client.EmptyDHCP{},
ARPDB: a,
ARPClientsUpdatePeriod: testTimeout / 10,
})
require.NoError(t, err)
err = storage.Start(testutil.ContextWithTimeout(t, testTimeout))
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, func() (err error) {
return storage.Shutdown(testutil.ContextWithTimeout(t, testTimeout))
})
t.Run("add_hosts", func(t *testing.T) {
func() {
mu.Lock()
defer mu.Unlock()
neighbors = []arpdb.Neighbor{{
Name: cliName1,
IP: cliIP1,
}}
}()
require.Eventually(t, func() (ok bool) {
cli1 := storage.ClientRuntime(cliIP1)
if cli1 == nil {
return false
}
assert.True(t, compareRuntimeInfo(cli1, client.SourceARP, cliName1))
return true
}, testTimeout, testTimeout/10)
})
t.Run("update_hosts", func(t *testing.T) {
func() {
mu.Lock()
defer mu.Unlock()
neighbors = []arpdb.Neighbor{{
Name: cliName2,
IP: cliIP2,
}}
}()
require.Eventually(t, func() (ok bool) {
cli2 := storage.ClientRuntime(cliIP2)
if cli2 == nil {
return false
}
assert.True(t, compareRuntimeInfo(cli2, client.SourceARP, cliName2))
cli1 := storage.ClientRuntime(cliIP1)
require.Nil(t, cli1)
return true
}, testTimeout, testTimeout/10)
})
}
func TestStorage_Add_whois(t *testing.T) {
var (
cliIP1 = netip.MustParseAddr("1.1.1.1")
cliIP2 = netip.MustParseAddr("2.2.2.2")
cliName2 = "client_two"
cliIP3 = netip.MustParseAddr("3.3.3.3")
cliName3 = "client_three"
)
storage, err := client.NewStorage(&client.StorageConfig{
DHCP: client.EmptyDHCP{},
})
require.NoError(t, err)
whois := &whois.Info{
Country: "AU",
Orgname: "Example Org",
}
t.Run("new_client", func(t *testing.T) {
storage.UpdateAddress(cliIP1, "", whois)
cli1 := storage.ClientRuntime(cliIP1)
require.NotNil(t, cli1)
assert.Equal(t, whois, cli1.WHOIS())
})
t.Run("existing_runtime_client", func(t *testing.T) {
storage.UpdateAddress(cliIP2, cliName2, nil)
storage.UpdateAddress(cliIP2, "", whois)
cli2 := storage.ClientRuntime(cliIP2)
require.NotNil(t, cli2)
assert.True(t, compareRuntimeInfo(cli2, client.SourceRDNS, cliName2))
assert.Equal(t, whois, cli2.WHOIS())
})
t.Run("can't_set_persistent_client", func(t *testing.T) {
err = storage.Add(&client.Persistent{
Name: cliName3,
UID: client.MustNewUID(),
IPs: []netip.Addr{cliIP3},
})
require.NoError(t, err)
storage.UpdateAddress(cliIP3, "", whois)
rc := storage.ClientRuntime(cliIP3)
require.Nil(t, rc)
})
}
func TestClientsDHCP(t *testing.T) {
var (
cliIP1 = netip.MustParseAddr("1.1.1.1")
cliName1 = "one.dhcp"
cliIP2 = netip.MustParseAddr("2.2.2.2")
cliMAC2 = mustParseMAC("22:22:22:22:22:22")
cliName2 = "two.dhcp"
cliIP3 = netip.MustParseAddr("3.3.3.3")
cliMAC3 = mustParseMAC("33:33:33:33:33:33")
cliName3 = "three.dhcp"
prsCliIP = netip.MustParseAddr("4.3.2.1")
prsCliMAC = mustParseMAC("AA:AA:AA:AA:AA:AA")
prsCliName = "persistent.dhcp"
)
ipToHost := map[netip.Addr]string{
cliIP1: cliName1,
}
ipToMAC := map[netip.Addr]net.HardwareAddr{
prsCliIP: prsCliMAC,
}
leases := []*dhcpsvc.Lease{{
IP: cliIP2,
Hostname: cliName2,
HWAddr: cliMAC2,
}, {
IP: cliIP3,
Hostname: cliName3,
HWAddr: cliMAC3,
}}
d := &testDHCP{
OnLeases: func() (ls []*dhcpsvc.Lease) {
return leases
},
OnHostBy: func(ip netip.Addr) (host string) {
return ipToHost[ip]
},
OnMACBy: func(ip netip.Addr) (mac net.HardwareAddr) {
return ipToMAC[ip]
},
}
storage, err := client.NewStorage(&client.StorageConfig{
DHCP: d,
RuntimeSourceDHCP: true,
})
require.NoError(t, err)
t.Run("find_runtime", func(t *testing.T) {
cli1 := storage.ClientRuntime(cliIP1)
require.NotNil(t, cli1)
assert.True(t, compareRuntimeInfo(cli1, client.SourceDHCP, cliName1))
})
t.Run("find_persistent", func(t *testing.T) {
err = storage.Add(&client.Persistent{
Name: prsCliName,
UID: client.MustNewUID(),
MACs: []net.HardwareAddr{prsCliMAC},
})
require.NoError(t, err)
prsCli, ok := storage.Find(prsCliIP.String())
require.True(t, ok)
assert.Equal(t, prsCliName, prsCli.Name)
})
t.Run("leases", func(t *testing.T) {
delete(ipToHost, cliIP1)
storage.UpdateDHCP()
cli1 := storage.ClientRuntime(cliIP1)
require.Nil(t, cli1)
for i, l := range leases {
cli := storage.ClientRuntime(l.IP)
require.NotNil(t, cli)
src, host := cli.Info()
assert.Equal(t, client.SourceDHCP, src)
assert.Equal(t, leases[i].Hostname, host)
}
})
t.Run("range", func(t *testing.T) {
s := 0
storage.RangeRuntime(func(rc *client.Runtime) (cont bool) {
s++
return true
})
assert.Equal(t, len(leases), s)
})
}
func TestClientsAddExisting(t *testing.T) {
t.Run("simple", func(t *testing.T) {
storage, err := client.NewStorage(&client.StorageConfig{
DHCP: client.EmptyDHCP{},
})
require.NoError(t, err)
ip := netip.MustParseAddr("1.1.1.1")
// Add a client.
err = storage.Add(&client.Persistent{
Name: "client1",
UID: client.MustNewUID(),
IPs: []netip.Addr{ip, netip.MustParseAddr("1:2:3::4")},
Subnets: []netip.Prefix{netip.MustParsePrefix("2.2.2.0/24")},
MACs: []net.HardwareAddr{{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}},
})
require.NoError(t, err)
// Now add an auto-client with the same IP.
storage.UpdateAddress(ip, "test", nil)
rc := storage.ClientRuntime(ip)
assert.True(t, compareRuntimeInfo(rc, client.SourceRDNS, "test"))
})
t.Run("complicated", func(t *testing.T) {
// TODO(a.garipov): Properly decouple the DHCP server from the client
// storage.
if runtime.GOOS == "windows" {
t.Skip("skipping dhcp test on windows")
}
// First, init a DHCP server with a single static lease.
config := &dhcpd.ServerConfig{
Enabled: true,
DataDir: t.TempDir(),
Conf4: dhcpd.V4ServerConf{
Enabled: true,
GatewayIP: netip.MustParseAddr("1.2.3.1"),
SubnetMask: netip.MustParseAddr("255.255.255.0"),
RangeStart: netip.MustParseAddr("1.2.3.2"),
RangeEnd: netip.MustParseAddr("1.2.3.10"),
},
}
dhcpServer, err := dhcpd.Create(config)
require.NoError(t, err)
storage, err := client.NewStorage(&client.StorageConfig{
DHCP: dhcpServer,
})
require.NoError(t, err)
ip := netip.MustParseAddr("1.2.3.4")
err = dhcpServer.AddStaticLease(&dhcpsvc.Lease{
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
IP: ip,
Hostname: "testhost",
Expiry: time.Now().Add(time.Hour),
})
require.NoError(t, err)
// Add a new client with the same IP as for a client with MAC.
err = storage.Add(&client.Persistent{
Name: "client2",
UID: client.MustNewUID(),
IPs: []netip.Addr{ip},
})
require.NoError(t, err)
// Add a new client with the IP from the first client's IP range.
err = storage.Add(&client.Persistent{
Name: "client3",
UID: client.MustNewUID(),
IPs: []netip.Addr{netip.MustParseAddr("2.2.2.2")},
})
require.NoError(t, err)
})
}
// newStorage is a helper function that returns a client storage filled with // newStorage is a helper function that returns a client storage filled with
// persistent clients from the m. It also generates a UID for each client. // persistent clients from the m. It also generates a UID for each client.
func newStorage(tb testing.TB, m []*client.Persistent) (s *client.Storage) { func newStorage(tb testing.TB, m []*client.Persistent) (s *client.Storage) {
tb.Helper() tb.Helper()
s = client.NewStorage(&client.Config{ s, err := client.NewStorage(&client.StorageConfig{
AllowedTags: nil, DHCP: client.EmptyDHCP{},
}) })
require.NoError(tb, err)
for _, c := range m { for _, c := range m {
c.UID = client.MustNewUID() c.UID = client.MustNewUID()
require.NoError(tb, s.Add(c)) require.NoError(tb, s.Add(c))
} }
require.Equal(tb, len(m), s.Size())
return s return s
} }
@ -43,6 +536,9 @@ func TestStorage_Add(t *testing.T) {
const ( const (
existingName = "existing_name" existingName = "existing_name"
existingClientID = "existing_client_id" existingClientID = "existing_client_id"
allowedTag = "user_admin"
notAllowedTag = "not_allowed_tag"
) )
var ( var (
@ -59,10 +555,20 @@ func TestStorage_Add(t *testing.T) {
UID: existingClientUID, UID: existingClientUID,
} }
s := client.NewStorage(&client.Config{ s, err := client.NewStorage(&client.StorageConfig{})
AllowedTags: nil, require.NoError(t, err)
})
err := s.Add(existingClient) tags := s.AllowedTags()
require.NotZero(t, len(tags))
require.True(t, slices.IsSorted(tags))
_, ok := slices.BinarySearch(tags, allowedTag)
require.True(t, ok)
_, ok = slices.BinarySearch(tags, notAllowedTag)
require.False(t, ok)
err = s.Add(existingClient)
require.NoError(t, err) require.NoError(t, err)
testCases := []struct { testCases := []struct {
@ -119,6 +625,46 @@ func TestStorage_Add(t *testing.T) {
}, },
wantErrMsg: `adding client: another client "existing_name" ` + wantErrMsg: `adding client: another client "existing_name" ` +
`uses the same ClientID "existing_client_id"`, `uses the same ClientID "existing_client_id"`,
}, {
name: "not_allowed_tag",
cli: &client.Persistent{
Name: "not_allowed_tag",
Tags: []string{notAllowedTag},
IPs: []netip.Addr{netip.MustParseAddr("4.4.4.4")},
UID: client.MustNewUID(),
},
wantErrMsg: `adding client: invalid tag: "not_allowed_tag"`,
}, {
name: "allowed_tag",
cli: &client.Persistent{
Name: "allowed_tag",
Tags: []string{allowedTag},
IPs: []netip.Addr{netip.MustParseAddr("5.5.5.5")},
UID: client.MustNewUID(),
},
wantErrMsg: "",
}, {
name: "",
cli: &client.Persistent{
Name: "",
IPs: []netip.Addr{netip.MustParseAddr("6.6.6.6")},
UID: client.MustNewUID(),
},
wantErrMsg: "adding client: empty name",
}, {
name: "no_id",
cli: &client.Persistent{
Name: "no_id",
UID: client.MustNewUID(),
},
wantErrMsg: "adding client: id required",
}, {
name: "no_uid",
cli: &client.Persistent{
Name: "no_uid",
IPs: []netip.Addr{netip.MustParseAddr("7.7.7.7")},
},
wantErrMsg: "adding client: uid required",
}} }}
for _, tc := range testCases { for _, tc := range testCases {
@ -141,10 +687,10 @@ func TestStorage_RemoveByName(t *testing.T) {
UID: client.MustNewUID(), UID: client.MustNewUID(),
} }
s := client.NewStorage(&client.Config{ s, err := client.NewStorage(&client.StorageConfig{})
AllowedTags: nil, require.NoError(t, err)
})
err := s.Add(existingClient) err = s.Add(existingClient)
require.NoError(t, err) require.NoError(t, err)
testCases := []struct { testCases := []struct {
@ -168,9 +714,9 @@ func TestStorage_RemoveByName(t *testing.T) {
} }
t.Run("duplicate_remove", func(t *testing.T) { t.Run("duplicate_remove", func(t *testing.T) {
s = client.NewStorage(&client.Config{ s, err = client.NewStorage(&client.StorageConfig{})
AllowedTags: nil, require.NoError(t, err)
})
err = s.Add(existingClient) err = s.Add(existingClient)
require.NoError(t, err) require.NoError(t, err)
@ -341,6 +887,127 @@ func TestStorage_FindLoose(t *testing.T) {
} }
} }
func TestStorage_FindByName(t *testing.T) {
const (
cliIP1 = "1.1.1.1"
cliIP2 = "2.2.2.2"
)
const (
clientExistingName = "client_existing"
clientAnotherExistingName = "client_another_existing"
nonExistingClientName = "client_non_existing"
)
var (
clientExisting = &client.Persistent{
Name: clientExistingName,
IPs: []netip.Addr{netip.MustParseAddr(cliIP1)},
}
clientAnotherExisting = &client.Persistent{
Name: clientAnotherExistingName,
IPs: []netip.Addr{netip.MustParseAddr(cliIP2)},
}
)
clients := []*client.Persistent{
clientExisting,
clientAnotherExisting,
}
s := newStorage(t, clients)
testCases := []struct {
want *client.Persistent
name string
clientName string
}{{
name: "existing",
clientName: clientExistingName,
want: clientExisting,
}, {
name: "another_existing",
clientName: clientAnotherExistingName,
want: clientAnotherExisting,
}, {
name: "non_existing",
clientName: nonExistingClientName,
want: nil,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c, ok := s.FindByName(tc.clientName)
if tc.want == nil {
assert.False(t, ok)
return
}
assert.True(t, ok)
assert.Equal(t, tc.want, c)
})
}
}
func TestStorage_FindByMAC(t *testing.T) {
var (
cliMAC = mustParseMAC("11:11:11:11:11:11")
cliAnotherMAC = mustParseMAC("22:22:22:22:22:22")
nonExistingClientMAC = mustParseMAC("33:33:33:33:33:33")
)
var (
clientExisting = &client.Persistent{
Name: "client",
MACs: []net.HardwareAddr{cliMAC},
}
clientAnotherExisting = &client.Persistent{
Name: "another_client",
MACs: []net.HardwareAddr{cliAnotherMAC},
}
)
clients := []*client.Persistent{
clientExisting,
clientAnotherExisting,
}
s := newStorage(t, clients)
testCases := []struct {
want *client.Persistent
name string
clientMAC net.HardwareAddr
}{{
name: "existing",
clientMAC: cliMAC,
want: clientExisting,
}, {
name: "another_existing",
clientMAC: cliAnotherMAC,
want: clientAnotherExisting,
}, {
name: "non_existing",
clientMAC: nonExistingClientMAC,
want: nil,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c, ok := s.FindByMAC(tc.clientMAC)
if tc.want == nil {
assert.False(t, ok)
return
}
assert.True(t, ok)
assert.Equal(t, tc.want, c)
})
}
}
func TestStorage_Update(t *testing.T) { func TestStorage_Update(t *testing.T) {
const ( const (
clientName = "client_name" clientName = "client_name"

View file

@ -3,6 +3,7 @@ package dhcpsvc
import ( import (
"fmt" "fmt"
"log/slog" "log/slog"
"os"
"time" "time"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
@ -23,7 +24,8 @@ type Config struct {
// clients' hostnames. // clients' hostnames.
LocalDomainName string LocalDomainName string
// TODO(e.burkov): Add DB path. // DBFilePath is the path to the database file containing the DHCP leases.
DBFilePath string
// ICMPTimeout is the timeout for checking another DHCP server's presence. // ICMPTimeout is the timeout for checking another DHCP server's presence.
ICMPTimeout time.Duration ICMPTimeout time.Duration
@ -64,6 +66,12 @@ func (conf *Config) Validate() (err error) {
errs = append(errs, err) errs = append(errs, err)
} }
// This is a best-effort check for the file accessibility. The file will be
// checked again when it is opened later.
if _, err = os.Stat(conf.DBFilePath); err != nil && !errors.Is(err, os.ErrNotExist) {
errs = append(errs, fmt.Errorf("db file path %q: %w", conf.DBFilePath, err))
}
if len(conf.Interfaces) == 0 { if len(conf.Interfaces) == 0 {
errs = append(errs, errNoInterfaces) errs = append(errs, errNoInterfaces)

View file

@ -1,6 +1,7 @@
package dhcpsvc_test package dhcpsvc_test
import ( import (
"path/filepath"
"testing" "testing"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
@ -8,6 +9,8 @@ import (
) )
func TestConfig_Validate(t *testing.T) { func TestConfig_Validate(t *testing.T) {
leasesPath := filepath.Join(t.TempDir(), "leases.json")
testCases := []struct { testCases := []struct {
name string name string
conf *dhcpsvc.Config conf *dhcpsvc.Config
@ -25,6 +28,7 @@ func TestConfig_Validate(t *testing.T) {
conf: &dhcpsvc.Config{ conf: &dhcpsvc.Config{
Enabled: true, Enabled: true,
Interfaces: testInterfaceConf, Interfaces: testInterfaceConf,
DBFilePath: leasesPath,
}, },
wantErrMsg: `bad domain name "": domain name is empty`, wantErrMsg: `bad domain name "": domain name is empty`,
}, { }, {
@ -32,6 +36,7 @@ func TestConfig_Validate(t *testing.T) {
Enabled: true, Enabled: true,
LocalDomainName: testLocalTLD, LocalDomainName: testLocalTLD,
Interfaces: nil, Interfaces: nil,
DBFilePath: leasesPath,
}, },
name: "no_interfaces", name: "no_interfaces",
wantErrMsg: "no interfaces specified", wantErrMsg: "no interfaces specified",
@ -40,6 +45,7 @@ func TestConfig_Validate(t *testing.T) {
Enabled: true, Enabled: true,
LocalDomainName: testLocalTLD, LocalDomainName: testLocalTLD,
Interfaces: nil, Interfaces: nil,
DBFilePath: leasesPath,
}, },
name: "no_interfaces", name: "no_interfaces",
wantErrMsg: "no interfaces specified", wantErrMsg: "no interfaces specified",
@ -50,6 +56,7 @@ func TestConfig_Validate(t *testing.T) {
Interfaces: map[string]*dhcpsvc.InterfaceConfig{ Interfaces: map[string]*dhcpsvc.InterfaceConfig{
"eth0": nil, "eth0": nil,
}, },
DBFilePath: leasesPath,
}, },
name: "nil_interface", name: "nil_interface",
wantErrMsg: `interface "eth0": config is nil`, wantErrMsg: `interface "eth0": config is nil`,
@ -63,6 +70,7 @@ func TestConfig_Validate(t *testing.T) {
IPv6: &dhcpsvc.IPv6Config{Enabled: false}, IPv6: &dhcpsvc.IPv6Config{Enabled: false},
}, },
}, },
DBFilePath: leasesPath,
}, },
name: "nil_ipv4", name: "nil_ipv4",
wantErrMsg: `interface "eth0": ipv4: config is nil`, wantErrMsg: `interface "eth0": ipv4: config is nil`,
@ -76,6 +84,7 @@ func TestConfig_Validate(t *testing.T) {
IPv6: nil, IPv6: nil,
}, },
}, },
DBFilePath: leasesPath,
}, },
name: "nil_ipv6", name: "nil_ipv6",
wantErrMsg: `interface "eth0": ipv6: config is nil`, wantErrMsg: `interface "eth0": ipv6: config is nil`,

195
internal/dhcpsvc/db.go Normal file
View file

@ -0,0 +1,195 @@
package dhcpsvc
import (
"context"
"encoding/json"
"fmt"
"io/fs"
"net"
"net/netip"
"os"
"slices"
"strings"
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/google/renameio/v2/maybe"
)
// dataVersion is the current version of the stored DHCP leases structure.
const dataVersion = 1
// databasePerm is the permissions for the database file.
const databasePerm fs.FileMode = 0o640
// dataLeases is the structure of the stored DHCP leases.
type dataLeases struct {
// Leases is the list containing stored DHCP leases.
Leases []*dbLease `json:"leases"`
// Version is the current version of the structure.
Version int `json:"version"`
}
// dbLease is the structure of stored lease.
type dbLease struct {
Expiry string `json:"expires"`
IP netip.Addr `json:"ip"`
Hostname string `json:"hostname"`
HWAddr string `json:"mac"`
IsStatic bool `json:"static"`
}
// compareNames returns the result of comparing the hostnames of dl and other
// lexicographically.
func (dl *dbLease) compareNames(other *dbLease) (res int) {
return strings.Compare(dl.Hostname, other.Hostname)
}
// toDBLease converts *Lease to *dbLease.
func toDBLease(l *Lease) (dl *dbLease) {
var expiryStr string
if !l.IsStatic {
// The front-end is waiting for RFC 3999 format of the time value. It
// also shouldn't got an Expiry field for static leases.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2692.
expiryStr = l.Expiry.Format(time.RFC3339)
}
return &dbLease{
Expiry: expiryStr,
Hostname: l.Hostname,
HWAddr: l.HWAddr.String(),
IP: l.IP,
IsStatic: l.IsStatic,
}
}
// toInternal converts dl to *Lease.
func (dl *dbLease) toInternal() (l *Lease, err error) {
mac, err := net.ParseMAC(dl.HWAddr)
if err != nil {
return nil, fmt.Errorf("parsing hardware address: %w", err)
}
expiry := time.Time{}
if !dl.IsStatic {
expiry, err = time.Parse(time.RFC3339, dl.Expiry)
if err != nil {
return nil, fmt.Errorf("parsing expiry time: %w", err)
}
}
return &Lease{
Expiry: expiry,
IP: dl.IP,
Hostname: dl.Hostname,
HWAddr: mac,
IsStatic: dl.IsStatic,
}, nil
}
// dbLoad loads stored leases. It must only be called before the service has
// been started.
func (srv *DHCPServer) dbLoad(ctx context.Context) (err error) {
defer func() { err = errors.Annotate(err, "loading db: %w") }()
file, err := os.Open(srv.dbFilePath)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("reading db: %w", err)
}
srv.logger.DebugContext(ctx, "no db file found")
return nil
}
defer func() {
err = errors.WithDeferred(err, file.Close())
}()
dl := &dataLeases{}
err = json.NewDecoder(file).Decode(dl)
if err != nil {
return fmt.Errorf("decoding db: %w", err)
}
srv.resetLeases()
srv.addDBLeases(ctx, dl.Leases)
return nil
}
// addDBLeases adds leases to the server.
func (srv *DHCPServer) addDBLeases(ctx context.Context, leases []*dbLease) {
var v4, v6 uint
for i, l := range leases {
lease, err := l.toInternal()
if err != nil {
srv.logger.WarnContext(ctx, "converting lease", "idx", i, slogutil.KeyError, err)
continue
}
iface, err := srv.ifaceForAddr(l.IP)
if err != nil {
srv.logger.WarnContext(ctx, "searching lease iface", "idx", i, slogutil.KeyError, err)
continue
}
err = srv.leases.add(lease, iface)
if err != nil {
srv.logger.WarnContext(ctx, "adding lease", "idx", i, slogutil.KeyError, err)
continue
}
if l.IP.Is4() {
v4++
} else {
v6++
}
}
// TODO(e.burkov): Group by interface.
srv.logger.InfoContext(ctx, "loaded leases", "v4", v4, "v6", v6, "total", len(leases))
}
// writeDB writes leases to the database file. It expects the
// [DHCPServer.leasesMu] to be locked.
func (srv *DHCPServer) dbStore(ctx context.Context) (err error) {
defer func() { err = errors.Annotate(err, "writing db: %w") }()
dl := &dataLeases{
// Avoid writing null into the database file if there are no leases.
Leases: make([]*dbLease, 0, srv.leases.len()),
Version: dataVersion,
}
srv.leases.rangeLeases(func(l *Lease) (cont bool) {
lease := toDBLease(l)
i, _ := slices.BinarySearchFunc(dl.Leases, lease, (*dbLease).compareNames)
dl.Leases = slices.Insert(dl.Leases, i, lease)
return true
})
buf, err := json.Marshal(dl)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
err = maybe.WriteFile(srv.dbFilePath, buf, databasePerm)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
srv.logger.InfoContext(ctx, "stored leases", "num", len(dl.Leases), "file", srv.dbFilePath)
return nil
}

View file

@ -0,0 +1,4 @@
package dhcpsvc
// DatabasePerm is the permissions for the test database file.
const DatabasePerm = databasePerm

View file

@ -50,7 +50,7 @@ type Interface interface {
IPByHost(host string) (ip netip.Addr) IPByHost(host string) (ip netip.Addr)
// Leases returns all the active DHCP leases. The returned slice should be // Leases returns all the active DHCP leases. The returned slice should be
// a clone. // a clone. The order of leases is undefined.
// //
// TODO(e.burkov): Consider implementing iterating methods with appropriate // TODO(e.burkov): Consider implementing iterating methods with appropriate
// signatures instead of cloning the whole list. // signatures instead of cloning the whole list.

View file

@ -0,0 +1,66 @@
package dhcpsvc_test
import (
"net"
"net/netip"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/stretchr/testify/require"
)
// testLocalTLD is a common local TLD for tests.
const testLocalTLD = "local"
// testTimeout is a common timeout for tests and contexts.
const testTimeout time.Duration = 10 * time.Second
// discardLog is a logger to discard test output.
var discardLog = slogutil.NewDiscardLogger()
// testInterfaceConf is a common set of interface configurations for tests.
var testInterfaceConf = map[string]*dhcpsvc.InterfaceConfig{
"eth0": {
IPv4: &dhcpsvc.IPv4Config{
Enabled: true,
GatewayIP: netip.MustParseAddr("192.168.0.1"),
SubnetMask: netip.MustParseAddr("255.255.255.0"),
RangeStart: netip.MustParseAddr("192.168.0.2"),
RangeEnd: netip.MustParseAddr("192.168.0.254"),
LeaseDuration: 1 * time.Hour,
},
IPv6: &dhcpsvc.IPv6Config{
Enabled: true,
RangeStart: netip.MustParseAddr("2001:db8::1"),
LeaseDuration: 1 * time.Hour,
RAAllowSLAAC: true,
RASLAACOnly: true,
},
},
"eth1": {
IPv4: &dhcpsvc.IPv4Config{
Enabled: true,
GatewayIP: netip.MustParseAddr("172.16.0.1"),
SubnetMask: netip.MustParseAddr("255.255.255.0"),
RangeStart: netip.MustParseAddr("172.16.0.2"),
RangeEnd: netip.MustParseAddr("172.16.0.255"),
LeaseDuration: 1 * time.Hour,
},
IPv6: &dhcpsvc.IPv6Config{
Enabled: true,
RangeStart: netip.MustParseAddr("2001:db9::1"),
LeaseDuration: 1 * time.Hour,
RAAllowSLAAC: true,
RASLAACOnly: true,
},
},
}
// mustParseMAC parses a hardware address from s and requires no errors.
func mustParseMAC(t require.TestingT, s string) (mac net.HardwareAddr) {
mac, err := net.ParseMAC(s)
require.NoError(t, err)
return mac
}

View file

@ -3,42 +3,74 @@ package dhcpsvc
import ( import (
"fmt" "fmt"
"log/slog" "log/slog"
"slices" "net"
"time" "time"
) )
// netInterface is a common part of any network interface within the DHCP // macKey contains hardware address as byte array of 6, 8, or 20 bytes.
// server. //
// TODO(e.burkov): Move to aghnet or even to netutil.
type macKey any
// macToKey converts mac into macKey, which is used as the key for the lease
// maps. mac must be a valid hardware address of length 6, 8, or 20 bytes, see
// [netutil.ValidateMAC].
func macToKey(mac net.HardwareAddr) (key macKey) {
switch len(mac) {
case 6:
return [6]byte(mac)
case 8:
return [8]byte(mac)
case 20:
return [20]byte(mac)
default:
panic(fmt.Errorf("invalid mac address %#v", mac))
}
}
// netInterface is a common part of any interface within the DHCP server.
// //
// TODO(e.burkov): Add other methods as [DHCPServer] evolves. // TODO(e.burkov): Add other methods as [DHCPServer] evolves.
type netInterface struct { type netInterface struct {
// logger logs the events related to the network interface. // logger logs the events related to the network interface.
logger *slog.Logger logger *slog.Logger
// leases is the set of DHCP leases assigned to this interface.
leases map[macKey]*Lease
// name is the name of the network interface. // name is the name of the network interface.
name string name string
// leases is a set of leases sorted by hardware address.
leases []*Lease
// leaseTTL is the default Time-To-Live value for leases. // leaseTTL is the default Time-To-Live value for leases.
leaseTTL time.Duration leaseTTL time.Duration
} }
// reset clears all the slices in iface for reuse. // newNetInterface creates a new netInterface with the given name, leaseTTL, and
func (iface *netInterface) reset() { // logger.
iface.leases = iface.leases[:0] func newNetInterface(name string, l *slog.Logger, leaseTTL time.Duration) (iface *netInterface) {
return &netInterface{
logger: l,
leases: map[macKey]*Lease{},
name: name,
leaseTTL: leaseTTL,
}
} }
// insertLease inserts the given lease into iface. It returns an error if the // reset clears all the slices in iface for reuse.
func (iface *netInterface) reset() {
clear(iface.leases)
}
// addLease inserts the given lease into iface. It returns an error if the
// lease can't be inserted. // lease can't be inserted.
func (iface *netInterface) insertLease(l *Lease) (err error) { func (iface *netInterface) addLease(l *Lease) (err error) {
i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC) mk := macToKey(l.HWAddr)
_, found := iface.leases[mk]
if found { if found {
return fmt.Errorf("lease for mac %s already exists", l.HWAddr) return fmt.Errorf("lease for mac %s already exists", l.HWAddr)
} }
iface.leases = slices.Insert(iface.leases, i, l) iface.leases[mk] = l
return nil return nil
} }
@ -46,12 +78,13 @@ func (iface *netInterface) insertLease(l *Lease) (err error) {
// updateLease replaces an existing lease within iface with the given one. It // updateLease replaces an existing lease within iface with the given one. It
// returns an error if there is no lease with such hardware address. // returns an error if there is no lease with such hardware address.
func (iface *netInterface) updateLease(l *Lease) (prev *Lease, err error) { func (iface *netInterface) updateLease(l *Lease) (prev *Lease, err error) {
i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC) mk := macToKey(l.HWAddr)
prev, found := iface.leases[mk]
if !found { if !found {
return nil, fmt.Errorf("no lease for mac %s", l.HWAddr) return nil, fmt.Errorf("no lease for mac %s", l.HWAddr)
} }
prev, iface.leases[i] = iface.leases[i], l iface.leases[mk] = l
return prev, nil return prev, nil
} }
@ -59,12 +92,13 @@ func (iface *netInterface) updateLease(l *Lease) (prev *Lease, err error) {
// removeLease removes an existing lease from iface. It returns an error if // removeLease removes an existing lease from iface. It returns an error if
// there is no lease equal to l. // there is no lease equal to l.
func (iface *netInterface) removeLease(l *Lease) (err error) { func (iface *netInterface) removeLease(l *Lease) (err error) {
i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC) mk := macToKey(l.HWAddr)
_, found := iface.leases[mk]
if !found { if !found {
return fmt.Errorf("no lease for mac %s", l.HWAddr) return fmt.Errorf("no lease for mac %s", l.HWAddr)
} }
iface.leases = slices.Delete(iface.leases, i, i+1) delete(iface.leases, mk)
return nil return nil
} }

View file

@ -1,7 +1,6 @@
package dhcpsvc package dhcpsvc
import ( import (
"bytes"
"net" "net"
"net/netip" "net/netip"
"slices" "slices"
@ -45,8 +44,3 @@ func (l *Lease) Clone() (clone *Lease) {
IsStatic: l.IsStatic, IsStatic: l.IsStatic,
} }
} }
// compareLeaseMAC compares two [Lease]s by hardware address.
func compareLeaseMAC(a, b *Lease) (res int) {
return bytes.Compare(a.HWAddr, b.HWAddr)
}

View file

@ -61,7 +61,7 @@ func (idx *leaseIndex) add(l *Lease, iface *netInterface) (err error) {
return fmt.Errorf("lease for hostname %s already exists", l.Hostname) return fmt.Errorf("lease for hostname %s already exists", l.Hostname)
} }
err = iface.insertLease(l) err = iface.addLease(l)
if err != nil { if err != nil {
return err return err
} }
@ -124,3 +124,18 @@ func (idx *leaseIndex) update(l *Lease, iface *netInterface) (err error) {
return nil return nil
} }
// rangeLeases calls f for each lease in idx in an unspecified order until f
// returns false.
func (idx *leaseIndex) rangeLeases(f func(l *Lease) (cont bool)) {
for _, l := range idx.byName {
if !f(l) {
break
}
}
}
// len returns the number of leases in idx.
func (idx *leaseIndex) len() (l uint) {
return uint(len(idx.byAddr))
}

View file

@ -27,6 +27,13 @@ type DHCPServer struct {
// hostnames. // hostnames.
localTLD string localTLD string
// dbFilePath is the path to the database file containing the DHCP leases.
//
// TODO(e.burkov): Consider extracting the database logic into a separate
// interface to prevent packages that only need lease data from depending on
// the entire server and to simplify testing.
dbFilePath string
// leasesMu protects the leases index as well as leases in the interfaces. // leasesMu protects the leases index as well as leases in the interfaces.
leasesMu *sync.RWMutex leasesMu *sync.RWMutex
@ -34,10 +41,10 @@ type DHCPServer struct {
leases *leaseIndex leases *leaseIndex
// interfaces4 is the set of IPv4 interfaces sorted by interface name. // interfaces4 is the set of IPv4 interfaces sorted by interface name.
interfaces4 netInterfacesV4 interfaces4 dhcpInterfacesV4
// interfaces6 is the set of IPv6 interfaces sorted by interface name. // interfaces6 is the set of IPv6 interfaces sorted by interface name.
interfaces6 netInterfacesV6 interfaces6 dhcpInterfacesV6
// icmpTimeout is the timeout for checking another DHCP server's presence. // icmpTimeout is the timeout for checking another DHCP server's presence.
icmpTimeout time.Duration icmpTimeout time.Duration
@ -56,28 +63,9 @@ func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) {
return nil, nil return nil, nil
} }
// TODO(e.burkov): Add validations scoped to the network interfaces set. ifaces4, ifaces6, err := newInterfaces(ctx, l, conf.Interfaces)
ifaces4 := make(netInterfacesV4, 0, len(conf.Interfaces)) if err != nil {
ifaces6 := make(netInterfacesV6, 0, len(conf.Interfaces)) // Don't wrap the error since it's informative enough as is.
var errs []error
mapsutil.SortedRange(conf.Interfaces, func(name string, iface *InterfaceConfig) (cont bool) {
var i4 *netInterfaceV4
i4, err = newNetInterfaceV4(ctx, l, name, iface.IPv4)
if err != nil {
errs = append(errs, fmt.Errorf("interface %q: ipv4: %w", name, err))
} else if i4 != nil {
ifaces4 = append(ifaces4, i4)
}
i6 := newNetInterfaceV6(ctx, l, name, iface.IPv6)
if i6 != nil {
ifaces6 = append(ifaces6, i6)
}
return true
})
if err = errors.Join(errs...); err != nil {
return nil, err return nil, err
} }
@ -93,13 +81,55 @@ func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) {
interfaces4: ifaces4, interfaces4: ifaces4,
interfaces6: ifaces6, interfaces6: ifaces6,
icmpTimeout: conf.ICMPTimeout, icmpTimeout: conf.ICMPTimeout,
dbFilePath: conf.DBFilePath,
} }
// TODO(e.burkov): Load leases. err = srv.dbLoad(ctx)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
return srv, nil return srv, nil
} }
// newInterfaces creates interfaces for the given map of interface names to
// their configurations.
func newInterfaces(
ctx context.Context,
l *slog.Logger,
ifaces map[string]*InterfaceConfig,
) (v4 dhcpInterfacesV4, v6 dhcpInterfacesV6, err error) {
defer func() { err = errors.Annotate(err, "creating interfaces: %w") }()
// TODO(e.burkov): Add validations scoped to the network interfaces set.
v4 = make(dhcpInterfacesV4, 0, len(ifaces))
v6 = make(dhcpInterfacesV6, 0, len(ifaces))
var errs []error
mapsutil.SortedRange(ifaces, func(name string, iface *InterfaceConfig) (cont bool) {
var i4 *dhcpInterfaceV4
i4, err = newDHCPInterfaceV4(ctx, l, name, iface.IPv4)
if err != nil {
errs = append(errs, fmt.Errorf("interface %q: ipv4: %w", name, err))
} else if i4 != nil {
v4 = append(v4, i4)
}
i6 := newDHCPInterfaceV6(ctx, l, name, iface.IPv6)
if i6 != nil {
v6 = append(v6, i6)
}
return true
})
if err = errors.Join(errs...); err != nil {
return nil, nil, err
}
return v4, v6, nil
}
// type check // type check
// //
// TODO(e.burkov): Uncomment when the [Interface] interface is implemented. // TODO(e.burkov): Uncomment when the [Interface] interface is implemented.
@ -115,16 +145,11 @@ func (srv *DHCPServer) Leases() (leases []*Lease) {
srv.leasesMu.RLock() srv.leasesMu.RLock()
defer srv.leasesMu.RUnlock() defer srv.leasesMu.RUnlock()
for _, iface := range srv.interfaces4 { srv.leases.rangeLeases(func(l *Lease) (cont bool) {
for _, lease := range iface.leases { leases = append(leases, l.Clone())
leases = append(leases, lease.Clone())
} return true
} })
for _, iface := range srv.interfaces6 {
for _, lease := range iface.leases {
leases = append(leases, lease.Clone())
}
}
return leases return leases
} }
@ -167,22 +192,35 @@ func (srv *DHCPServer) IPByHost(host string) (ip netip.Addr) {
// Reset implements the [Interface] interface for *DHCPServer. // Reset implements the [Interface] interface for *DHCPServer.
func (srv *DHCPServer) Reset(ctx context.Context) (err error) { func (srv *DHCPServer) Reset(ctx context.Context) (err error) {
defer func() { err = errors.Annotate(err, "resetting leases: %w") }()
srv.leasesMu.Lock() srv.leasesMu.Lock()
defer srv.leasesMu.Unlock() defer srv.leasesMu.Unlock()
for _, iface := range srv.interfaces4 { srv.resetLeases()
iface.reset() err = srv.dbStore(ctx)
if err != nil {
// Don't wrap the error since there is already an annotation deferred.
return err
} }
for _, iface := range srv.interfaces6 {
iface.reset()
}
srv.leases.clear()
srv.logger.DebugContext(ctx, "reset leases") srv.logger.DebugContext(ctx, "reset leases")
return nil return nil
} }
// resetLeases resets the leases for all network interfaces of the server. It
// expects the DHCPServer.leasesMu to be locked.
func (srv *DHCPServer) resetLeases() {
for _, iface := range srv.interfaces4 {
iface.common.reset()
}
for _, iface := range srv.interfaces6 {
iface.common.reset()
}
srv.leases.clear()
}
// AddLease implements the [Interface] interface for *DHCPServer. // AddLease implements the [Interface] interface for *DHCPServer.
func (srv *DHCPServer) AddLease(ctx context.Context, l *Lease) (err error) { func (srv *DHCPServer) AddLease(ctx context.Context, l *Lease) (err error) {
defer func() { err = errors.Annotate(err, "adding lease: %w") }() defer func() { err = errors.Annotate(err, "adding lease: %w") }()
@ -190,7 +228,7 @@ func (srv *DHCPServer) AddLease(ctx context.Context, l *Lease) (err error) {
addr := l.IP addr := l.IP
iface, err := srv.ifaceForAddr(addr) iface, err := srv.ifaceForAddr(addr)
if err != nil { if err != nil {
// Don't wrap the error since there is already an annotation deferred. // Don't wrap the error since it's already informative enough as is.
return err return err
} }
@ -203,6 +241,12 @@ func (srv *DHCPServer) AddLease(ctx context.Context, l *Lease) (err error) {
return err return err
} }
err = srv.dbStore(ctx)
if err != nil {
// Don't wrap the error since it's already informative enough as is.
return err
}
iface.logger.DebugContext( iface.logger.DebugContext(
ctx, "added lease", ctx, "added lease",
"hostname", l.Hostname, "hostname", l.Hostname,
@ -223,7 +267,7 @@ func (srv *DHCPServer) UpdateStaticLease(ctx context.Context, l *Lease) (err err
addr := l.IP addr := l.IP
iface, err := srv.ifaceForAddr(addr) iface, err := srv.ifaceForAddr(addr)
if err != nil { if err != nil {
// Don't wrap the error since there is already an annotation deferred. // Don't wrap the error since it's already informative enough as is.
return err return err
} }
@ -236,6 +280,12 @@ func (srv *DHCPServer) UpdateStaticLease(ctx context.Context, l *Lease) (err err
return err return err
} }
err = srv.dbStore(ctx)
if err != nil {
// Don't wrap the error since it's already informative enough as is.
return err
}
iface.logger.DebugContext( iface.logger.DebugContext(
ctx, "updated lease", ctx, "updated lease",
"hostname", l.Hostname, "hostname", l.Hostname,
@ -254,7 +304,7 @@ func (srv *DHCPServer) RemoveLease(ctx context.Context, l *Lease) (err error) {
addr := l.IP addr := l.IP
iface, err := srv.ifaceForAddr(addr) iface, err := srv.ifaceForAddr(addr)
if err != nil { if err != nil {
// Don't wrap the error since there is already an annotation deferred. // Don't wrap the error since it's already informative enough as is.
return err return err
} }
@ -267,6 +317,12 @@ func (srv *DHCPServer) RemoveLease(ctx context.Context, l *Lease) (err error) {
return err return err
} }
err = srv.dbStore(ctx)
if err != nil {
// Don't wrap the error since it's already informative enough as is.
return err
}
iface.logger.DebugContext( iface.logger.DebugContext(
ctx, "removed lease", ctx, "removed lease",
"hostname", l.Hostname, "hostname", l.Hostname,

View file

@ -1,72 +1,41 @@
package dhcpsvc_test package dhcpsvc_test
import ( import (
"net" "io/fs"
"net/netip" "net/netip"
"os"
"path"
"path/filepath"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc" "github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// testLocalTLD is a common local TLD for tests. // testdata is a filesystem containing data for tests.
const testLocalTLD = "local" var testdata = os.DirFS("testdata")
// testTimeout is a common timeout for tests and contexts. // newTempDB copies the leases database file located in the testdata FS, under
const testTimeout time.Duration = 10 * time.Second // tb.Name()/leases.json, to a temporary directory and returns the path to the
// copied file.
func newTempDB(tb testing.TB) (dst string) {
tb.Helper()
// discardLog is a logger to discard test output. const filename = "leases.json"
var discardLog = slogutil.NewDiscardLogger()
// testInterfaceConf is a common set of interface configurations for tests. data, err := fs.ReadFile(testdata, path.Join(tb.Name(), filename))
var testInterfaceConf = map[string]*dhcpsvc.InterfaceConfig{ require.NoError(tb, err)
"eth0": {
IPv4: &dhcpsvc.IPv4Config{
Enabled: true,
GatewayIP: netip.MustParseAddr("192.168.0.1"),
SubnetMask: netip.MustParseAddr("255.255.255.0"),
RangeStart: netip.MustParseAddr("192.168.0.2"),
RangeEnd: netip.MustParseAddr("192.168.0.254"),
LeaseDuration: 1 * time.Hour,
},
IPv6: &dhcpsvc.IPv6Config{
Enabled: true,
RangeStart: netip.MustParseAddr("2001:db8::1"),
LeaseDuration: 1 * time.Hour,
RAAllowSLAAC: true,
RASLAACOnly: true,
},
},
"eth1": {
IPv4: &dhcpsvc.IPv4Config{
Enabled: true,
GatewayIP: netip.MustParseAddr("172.16.0.1"),
SubnetMask: netip.MustParseAddr("255.255.255.0"),
RangeStart: netip.MustParseAddr("172.16.0.2"),
RangeEnd: netip.MustParseAddr("172.16.0.255"),
LeaseDuration: 1 * time.Hour,
},
IPv6: &dhcpsvc.IPv6Config{
Enabled: true,
RangeStart: netip.MustParseAddr("2001:db9::1"),
LeaseDuration: 1 * time.Hour,
RAAllowSLAAC: true,
RASLAACOnly: true,
},
},
}
// mustParseMAC parses a hardware address from s and requires no errors. dst = filepath.Join(tb.TempDir(), filename)
func mustParseMAC(t require.TestingT, s string) (mac net.HardwareAddr) {
mac, err := net.ParseMAC(s)
require.NoError(t, err)
return mac err = os.WriteFile(dst, data, dhcpsvc.DatabasePerm)
require.NoError(tb, err)
return dst
} }
func TestNew(t *testing.T) { func TestNew(t *testing.T) {
@ -103,6 +72,8 @@ func TestNew(t *testing.T) {
RASLAACOnly: true, RASLAACOnly: true,
} }
leasesPath := filepath.Join(t.TempDir(), "leases.json")
testCases := []struct { testCases := []struct {
conf *dhcpsvc.Config conf *dhcpsvc.Config
name string name string
@ -118,6 +89,7 @@ func TestNew(t *testing.T) {
IPv6: validIPv6Conf, IPv6: validIPv6Conf,
}, },
}, },
DBFilePath: leasesPath,
}, },
name: "valid", name: "valid",
wantErrMsg: "", wantErrMsg: "",
@ -132,6 +104,7 @@ func TestNew(t *testing.T) {
IPv6: &dhcpsvc.IPv6Config{Enabled: false}, IPv6: &dhcpsvc.IPv6Config{Enabled: false},
}, },
}, },
DBFilePath: leasesPath,
}, },
name: "disabled_interfaces", name: "disabled_interfaces",
wantErrMsg: "", wantErrMsg: "",
@ -146,9 +119,10 @@ func TestNew(t *testing.T) {
IPv6: validIPv6Conf, IPv6: validIPv6Conf,
}, },
}, },
DBFilePath: leasesPath,
}, },
name: "gateway_within_range", name: "gateway_within_range",
wantErrMsg: `interface "eth0": ipv4: ` + wantErrMsg: `creating interfaces: interface "eth0": ipv4: ` +
`gateway ip 192.168.0.100 in the ip range 192.168.0.1-192.168.0.254`, `gateway ip 192.168.0.100 in the ip range 192.168.0.1-192.168.0.254`,
}, { }, {
conf: &dhcpsvc.Config{ conf: &dhcpsvc.Config{
@ -161,9 +135,10 @@ func TestNew(t *testing.T) {
IPv6: validIPv6Conf, IPv6: validIPv6Conf,
}, },
}, },
DBFilePath: leasesPath,
}, },
name: "bad_start", name: "bad_start",
wantErrMsg: `interface "eth0": ipv4: ` + wantErrMsg: `creating interfaces: interface "eth0": ipv4: ` +
`range start 127.0.0.1 is not within 192.168.0.1/24`, `range start 127.0.0.1 is not within 192.168.0.1/24`,
}} }}
@ -180,32 +155,36 @@ func TestNew(t *testing.T) {
func TestDHCPServer_AddLease(t *testing.T) { func TestDHCPServer_AddLease(t *testing.T) {
ctx := testutil.ContextWithTimeout(t, testTimeout) ctx := testutil.ContextWithTimeout(t, testTimeout)
leasesPath := filepath.Join(t.TempDir(), "leases.json")
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{ srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
Enabled: true, Enabled: true,
Logger: discardLog, Logger: discardLog,
LocalDomainName: testLocalTLD, LocalDomainName: testLocalTLD,
Interfaces: testInterfaceConf, Interfaces: testInterfaceConf,
DBFilePath: leasesPath,
}) })
require.NoError(t, err) require.NoError(t, err)
const ( const (
host1 = "host1" existHost = "host1"
host2 = "host2" newHost = "host2"
host3 = "host3" ipv6Host = "host3"
) )
ip1 := netip.MustParseAddr("192.168.0.2") var (
ip2 := netip.MustParseAddr("192.168.0.3") existIP = netip.MustParseAddr("192.168.0.2")
ip3 := netip.MustParseAddr("2001:db8::2") newIP = netip.MustParseAddr("192.168.0.3")
newIPv6 = netip.MustParseAddr("2001:db8::2")
mac1 := mustParseMAC(t, "01:02:03:04:05:06") existMAC = mustParseMAC(t, "01:02:03:04:05:06")
mac2 := mustParseMAC(t, "06:05:04:03:02:01") newMAC = mustParseMAC(t, "06:05:04:03:02:01")
mac3 := mustParseMAC(t, "02:03:04:05:06:07") ipv6MAC = mustParseMAC(t, "02:03:04:05:06:07")
)
require.NoError(t, srv.AddLease(ctx, &dhcpsvc.Lease{ require.NoError(t, srv.AddLease(ctx, &dhcpsvc.Lease{
Hostname: host1, Hostname: existHost,
IP: ip1, IP: existIP,
HWAddr: mac1, HWAddr: existMAC,
IsStatic: true, IsStatic: true,
})) }))
@ -216,61 +195,61 @@ func TestDHCPServer_AddLease(t *testing.T) {
}{{ }{{
name: "outside_range", name: "outside_range",
lease: &dhcpsvc.Lease{ lease: &dhcpsvc.Lease{
Hostname: host2, Hostname: newHost,
IP: netip.MustParseAddr("1.2.3.4"), IP: netip.MustParseAddr("1.2.3.4"),
HWAddr: mac2, HWAddr: newMAC,
}, },
wantErrMsg: "adding lease: no interface for ip 1.2.3.4", wantErrMsg: "adding lease: no interface for ip 1.2.3.4",
}, { }, {
name: "duplicate_ip", name: "duplicate_ip",
lease: &dhcpsvc.Lease{ lease: &dhcpsvc.Lease{
Hostname: host2, Hostname: newHost,
IP: ip1, IP: existIP,
HWAddr: mac2, HWAddr: newMAC,
}, },
wantErrMsg: "adding lease: lease for ip " + ip1.String() + wantErrMsg: "adding lease: lease for ip " + existIP.String() +
" already exists", " already exists",
}, { }, {
name: "duplicate_hostname", name: "duplicate_hostname",
lease: &dhcpsvc.Lease{ lease: &dhcpsvc.Lease{
Hostname: host1, Hostname: existHost,
IP: ip2, IP: newIP,
HWAddr: mac2, HWAddr: newMAC,
}, },
wantErrMsg: "adding lease: lease for hostname " + host1 + wantErrMsg: "adding lease: lease for hostname " + existHost +
" already exists", " already exists",
}, { }, {
name: "duplicate_hostname_case", name: "duplicate_hostname_case",
lease: &dhcpsvc.Lease{ lease: &dhcpsvc.Lease{
Hostname: strings.ToUpper(host1), Hostname: strings.ToUpper(existHost),
IP: ip2, IP: newIP,
HWAddr: mac2, HWAddr: newMAC,
}, },
wantErrMsg: "adding lease: lease for hostname " + wantErrMsg: "adding lease: lease for hostname " +
strings.ToUpper(host1) + " already exists", strings.ToUpper(existHost) + " already exists",
}, { }, {
name: "duplicate_mac", name: "duplicate_mac",
lease: &dhcpsvc.Lease{ lease: &dhcpsvc.Lease{
Hostname: host2, Hostname: newHost,
IP: ip2, IP: newIP,
HWAddr: mac1, HWAddr: existMAC,
}, },
wantErrMsg: "adding lease: lease for mac " + mac1.String() + wantErrMsg: "adding lease: lease for mac " + existMAC.String() +
" already exists", " already exists",
}, { }, {
name: "valid", name: "valid",
lease: &dhcpsvc.Lease{ lease: &dhcpsvc.Lease{
Hostname: host2, Hostname: newHost,
IP: ip2, IP: newIP,
HWAddr: mac2, HWAddr: newMAC,
}, },
wantErrMsg: "", wantErrMsg: "",
}, { }, {
name: "valid_v6", name: "valid_v6",
lease: &dhcpsvc.Lease{ lease: &dhcpsvc.Lease{
Hostname: host3, Hostname: ipv6Host,
IP: ip3, IP: newIPv6,
HWAddr: mac3, HWAddr: ipv6MAC,
}, },
wantErrMsg: "", wantErrMsg: "",
}} }}
@ -280,16 +259,21 @@ func TestDHCPServer_AddLease(t *testing.T) {
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.AddLease(ctx, tc.lease)) testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.AddLease(ctx, tc.lease))
}) })
} }
assert.NotEmpty(t, srv.Leases())
assert.FileExists(t, leasesPath)
} }
func TestDHCPServer_index(t *testing.T) { func TestDHCPServer_index(t *testing.T) {
ctx := testutil.ContextWithTimeout(t, testTimeout) ctx := testutil.ContextWithTimeout(t, testTimeout)
leasesPath := newTempDB(t)
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{ srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
Enabled: true, Enabled: true,
Logger: discardLog, Logger: discardLog,
LocalDomainName: testLocalTLD, LocalDomainName: testLocalTLD,
Interfaces: testInterfaceConf, Interfaces: testInterfaceConf,
DBFilePath: leasesPath,
}) })
require.NoError(t, err) require.NoError(t, err)
@ -301,46 +285,23 @@ func TestDHCPServer_index(t *testing.T) {
host5 = "host5" host5 = "host5"
) )
ip1 := netip.MustParseAddr("192.168.0.2") var (
ip2 := netip.MustParseAddr("192.168.0.3") ip1 = netip.MustParseAddr("192.168.0.2")
ip3 := netip.MustParseAddr("172.16.0.3") ip2 = netip.MustParseAddr("192.168.0.3")
ip4 := netip.MustParseAddr("172.16.0.4") ip3 = netip.MustParseAddr("172.16.0.3")
ip4 = netip.MustParseAddr("172.16.0.4")
mac1 := mustParseMAC(t, "01:02:03:04:05:06") mac1 = mustParseMAC(t, "01:02:03:04:05:06")
mac2 := mustParseMAC(t, "06:05:04:03:02:01") mac2 = mustParseMAC(t, "06:05:04:03:02:01")
mac3 := mustParseMAC(t, "02:03:04:05:06:07") mac3 = mustParseMAC(t, "02:03:04:05:06:07")
)
leases := []*dhcpsvc.Lease{{
Hostname: host1,
IP: ip1,
HWAddr: mac1,
IsStatic: true,
}, {
Hostname: host2,
IP: ip2,
HWAddr: mac2,
IsStatic: true,
}, {
Hostname: host3,
IP: ip3,
HWAddr: mac3,
IsStatic: true,
}, {
Hostname: host4,
IP: ip4,
HWAddr: mac1,
IsStatic: true,
}}
for _, l := range leases {
require.NoError(t, srv.AddLease(ctx, l))
}
t.Run("ip_idx", func(t *testing.T) { t.Run("ip_idx", func(t *testing.T) {
assert.Equal(t, ip1, srv.IPByHost(host1)) assert.Equal(t, ip1, srv.IPByHost(host1))
assert.Equal(t, ip2, srv.IPByHost(host2)) assert.Equal(t, ip2, srv.IPByHost(host2))
assert.Equal(t, ip3, srv.IPByHost(host3)) assert.Equal(t, ip3, srv.IPByHost(host3))
assert.Equal(t, ip4, srv.IPByHost(host4)) assert.Equal(t, ip4, srv.IPByHost(host4))
assert.Equal(t, netip.Addr{}, srv.IPByHost(host5)) assert.Zero(t, srv.IPByHost(host5))
}) })
t.Run("name_idx", func(t *testing.T) { t.Run("name_idx", func(t *testing.T) {
@ -348,7 +309,7 @@ func TestDHCPServer_index(t *testing.T) {
assert.Equal(t, host2, srv.HostByIP(ip2)) assert.Equal(t, host2, srv.HostByIP(ip2))
assert.Equal(t, host3, srv.HostByIP(ip3)) assert.Equal(t, host3, srv.HostByIP(ip3))
assert.Equal(t, host4, srv.HostByIP(ip4)) assert.Equal(t, host4, srv.HostByIP(ip4))
assert.Equal(t, "", srv.HostByIP(netip.Addr{})) assert.Zero(t, srv.HostByIP(netip.Addr{}))
}) })
t.Run("mac_idx", func(t *testing.T) { t.Run("mac_idx", func(t *testing.T) {
@ -356,18 +317,20 @@ func TestDHCPServer_index(t *testing.T) {
assert.Equal(t, mac2, srv.MACByIP(ip2)) assert.Equal(t, mac2, srv.MACByIP(ip2))
assert.Equal(t, mac3, srv.MACByIP(ip3)) assert.Equal(t, mac3, srv.MACByIP(ip3))
assert.Equal(t, mac1, srv.MACByIP(ip4)) assert.Equal(t, mac1, srv.MACByIP(ip4))
assert.Nil(t, srv.MACByIP(netip.Addr{})) assert.Zero(t, srv.MACByIP(netip.Addr{}))
}) })
} }
func TestDHCPServer_UpdateStaticLease(t *testing.T) { func TestDHCPServer_UpdateStaticLease(t *testing.T) {
ctx := testutil.ContextWithTimeout(t, testTimeout) ctx := testutil.ContextWithTimeout(t, testTimeout)
leasesPath := newTempDB(t)
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{ srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
Enabled: true, Enabled: true,
Logger: discardLog, Logger: discardLog,
LocalDomainName: testLocalTLD, LocalDomainName: testLocalTLD,
Interfaces: testInterfaceConf, Interfaces: testInterfaceConf,
DBFilePath: leasesPath,
}) })
require.NoError(t, err) require.NoError(t, err)
@ -380,36 +343,16 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
host6 = "host6" host6 = "host6"
) )
ip1 := netip.MustParseAddr("192.168.0.2") var (
ip2 := netip.MustParseAddr("192.168.0.3") ip1 = netip.MustParseAddr("192.168.0.2")
ip3 := netip.MustParseAddr("192.168.0.4") ip2 = netip.MustParseAddr("192.168.0.3")
ip4 := netip.MustParseAddr("2001:db8::2") ip3 = netip.MustParseAddr("192.168.0.4")
ip5 := netip.MustParseAddr("2001:db8::3") ip4 = netip.MustParseAddr("2001:db8::3")
mac1 := mustParseMAC(t, "01:02:03:04:05:06") mac1 = mustParseMAC(t, "01:02:03:04:05:06")
mac2 := mustParseMAC(t, "01:02:03:04:05:07") mac2 = mustParseMAC(t, "06:05:04:03:02:01")
mac3 := mustParseMAC(t, "06:05:04:03:02:01") mac3 = mustParseMAC(t, "06:05:04:03:02:02")
mac4 := mustParseMAC(t, "06:05:04:03:02:02") )
leases := []*dhcpsvc.Lease{{
Hostname: host1,
IP: ip1,
HWAddr: mac1,
IsStatic: true,
}, {
Hostname: host2,
IP: ip2,
HWAddr: mac2,
IsStatic: true,
}, {
Hostname: host4,
IP: ip4,
HWAddr: mac4,
IsStatic: true,
}}
for _, l := range leases {
require.NoError(t, srv.AddLease(ctx, l))
}
testCases := []struct { testCases := []struct {
name string name string
@ -428,9 +371,9 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
lease: &dhcpsvc.Lease{ lease: &dhcpsvc.Lease{
Hostname: host3, Hostname: host3,
IP: ip3, IP: ip3,
HWAddr: mac3, HWAddr: mac2,
}, },
wantErrMsg: "updating static lease: no lease for mac " + mac3.String(), wantErrMsg: "updating static lease: no lease for mac " + mac2.String(),
}, { }, {
name: "duplicate_ip", name: "duplicate_ip",
lease: &dhcpsvc.Lease{ lease: &dhcpsvc.Lease{
@ -470,8 +413,8 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
name: "valid_v6", name: "valid_v6",
lease: &dhcpsvc.Lease{ lease: &dhcpsvc.Lease{
Hostname: host6, Hostname: host6,
IP: ip5, IP: ip4,
HWAddr: mac4, HWAddr: mac3,
}, },
wantErrMsg: "", wantErrMsg: "",
}} }}
@ -481,16 +424,20 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.UpdateStaticLease(ctx, tc.lease)) testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.UpdateStaticLease(ctx, tc.lease))
}) })
} }
assert.FileExists(t, leasesPath)
} }
func TestDHCPServer_RemoveLease(t *testing.T) { func TestDHCPServer_RemoveLease(t *testing.T) {
ctx := testutil.ContextWithTimeout(t, testTimeout) ctx := testutil.ContextWithTimeout(t, testTimeout)
leasesPath := newTempDB(t)
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{ srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
Enabled: true, Enabled: true,
Logger: discardLog, Logger: discardLog,
LocalDomainName: testLocalTLD, LocalDomainName: testLocalTLD,
Interfaces: testInterfaceConf, Interfaces: testInterfaceConf,
DBFilePath: leasesPath,
}) })
require.NoError(t, err) require.NoError(t, err)
@ -500,28 +447,15 @@ func TestDHCPServer_RemoveLease(t *testing.T) {
host3 = "host3" host3 = "host3"
) )
ip1 := netip.MustParseAddr("192.168.0.2") var (
ip2 := netip.MustParseAddr("192.168.0.3") existIP = netip.MustParseAddr("192.168.0.2")
ip3 := netip.MustParseAddr("2001:db8::2") newIP = netip.MustParseAddr("192.168.0.3")
newIPv6 = netip.MustParseAddr("2001:db8::2")
mac1 := mustParseMAC(t, "01:02:03:04:05:06") existMAC = mustParseMAC(t, "01:02:03:04:05:06")
mac2 := mustParseMAC(t, "02:03:04:05:06:07") newMAC = mustParseMAC(t, "02:03:04:05:06:07")
mac3 := mustParseMAC(t, "06:05:04:03:02:01") ipv6MAC = mustParseMAC(t, "06:05:04:03:02:01")
)
leases := []*dhcpsvc.Lease{{
Hostname: host1,
IP: ip1,
HWAddr: mac1,
IsStatic: true,
}, {
Hostname: host3,
IP: ip3,
HWAddr: mac3,
IsStatic: true,
}}
for _, l := range leases {
require.NoError(t, srv.AddLease(ctx, l))
}
testCases := []struct { testCases := []struct {
name string name string
@ -531,40 +465,40 @@ func TestDHCPServer_RemoveLease(t *testing.T) {
name: "not_found_mac", name: "not_found_mac",
lease: &dhcpsvc.Lease{ lease: &dhcpsvc.Lease{
Hostname: host1, Hostname: host1,
IP: ip1, IP: existIP,
HWAddr: mac2, HWAddr: newMAC,
}, },
wantErrMsg: "removing lease: no lease for mac " + mac2.String(), wantErrMsg: "removing lease: no lease for mac " + newMAC.String(),
}, { }, {
name: "not_found_ip", name: "not_found_ip",
lease: &dhcpsvc.Lease{ lease: &dhcpsvc.Lease{
Hostname: host1, Hostname: host1,
IP: ip2, IP: newIP,
HWAddr: mac1, HWAddr: existMAC,
}, },
wantErrMsg: "removing lease: no lease for ip " + ip2.String(), wantErrMsg: "removing lease: no lease for ip " + newIP.String(),
}, { }, {
name: "not_found_host", name: "not_found_host",
lease: &dhcpsvc.Lease{ lease: &dhcpsvc.Lease{
Hostname: host2, Hostname: host2,
IP: ip1, IP: existIP,
HWAddr: mac1, HWAddr: existMAC,
}, },
wantErrMsg: "removing lease: no lease for hostname " + host2, wantErrMsg: "removing lease: no lease for hostname " + host2,
}, { }, {
name: "valid", name: "valid",
lease: &dhcpsvc.Lease{ lease: &dhcpsvc.Lease{
Hostname: host1, Hostname: host1,
IP: ip1, IP: existIP,
HWAddr: mac1, HWAddr: existMAC,
}, },
wantErrMsg: "", wantErrMsg: "",
}, { }, {
name: "valid_v6", name: "valid_v6",
lease: &dhcpsvc.Lease{ lease: &dhcpsvc.Lease{
Hostname: host3, Hostname: host3,
IP: ip3, IP: newIPv6,
HWAddr: mac3, HWAddr: ipv6MAC,
}, },
wantErrMsg: "", wantErrMsg: "",
}} }}
@ -575,49 +509,64 @@ func TestDHCPServer_RemoveLease(t *testing.T) {
}) })
} }
assert.FileExists(t, leasesPath)
assert.Empty(t, srv.Leases()) assert.Empty(t, srv.Leases())
} }
func TestDHCPServer_Reset(t *testing.T) { func TestDHCPServer_Reset(t *testing.T) {
ctx := testutil.ContextWithTimeout(t, testTimeout) leasesPath := newTempDB(t)
conf := &dhcpsvc.Config{
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
Enabled: true, Enabled: true,
Logger: discardLog, Logger: discardLog,
LocalDomainName: testLocalTLD, LocalDomainName: testLocalTLD,
Interfaces: testInterfaceConf, Interfaces: testInterfaceConf,
}) DBFilePath: leasesPath,
require.NoError(t, err)
leases := []*dhcpsvc.Lease{{
Hostname: "host1",
IP: netip.MustParseAddr("192.168.0.2"),
HWAddr: mustParseMAC(t, "01:02:03:04:05:06"),
IsStatic: true,
}, {
Hostname: "host2",
IP: netip.MustParseAddr("192.168.0.3"),
HWAddr: mustParseMAC(t, "06:05:04:03:02:01"),
IsStatic: true,
}, {
Hostname: "host3",
IP: netip.MustParseAddr("2001:db8::2"),
HWAddr: mustParseMAC(t, "02:03:04:05:06:07"),
IsStatic: true,
}, {
Hostname: "host4",
IP: netip.MustParseAddr("2001:db8::3"),
HWAddr: mustParseMAC(t, "06:05:04:03:02:02"),
IsStatic: true,
}}
for _, l := range leases {
require.NoError(t, srv.AddLease(ctx, l))
} }
require.Len(t, srv.Leases(), len(leases)) ctx := testutil.ContextWithTimeout(t, testTimeout)
srv, err := dhcpsvc.New(ctx, conf)
require.NoError(t, err)
const leasesNum = 4
require.Len(t, srv.Leases(), leasesNum)
require.NoError(t, srv.Reset(ctx)) require.NoError(t, srv.Reset(ctx))
assert.FileExists(t, leasesPath)
assert.Empty(t, srv.Leases()) assert.Empty(t, srv.Leases())
} }
func TestServer_Leases(t *testing.T) {
leasesPath := newTempDB(t)
conf := &dhcpsvc.Config{
Enabled: true,
Logger: discardLog,
LocalDomainName: testLocalTLD,
Interfaces: testInterfaceConf,
DBFilePath: leasesPath,
}
ctx := testutil.ContextWithTimeout(t, testTimeout)
srv, err := dhcpsvc.New(ctx, conf)
require.NoError(t, err)
expiry, err := time.Parse(time.RFC3339, "2042-01-02T03:04:05Z")
require.NoError(t, err)
wantLeases := []*dhcpsvc.Lease{{
Expiry: expiry,
IP: netip.MustParseAddr("192.168.0.3"),
Hostname: "example.host",
HWAddr: mustParseMAC(t, "AA:AA:AA:AA:AA:AA"),
IsStatic: false,
}, {
Expiry: time.Time{},
IP: netip.MustParseAddr("192.168.0.4"),
Hostname: "example.static.host",
HWAddr: mustParseMAC(t, "BB:BB:BB:BB:BB:BB"),
IsStatic: true,
}}
assert.ElementsMatch(t, wantLeases, srv.Leases())
}

View file

@ -0,0 +1,19 @@
{
"leases": [
{
"expires": "",
"ip": "192.168.0.2",
"hostname": "host1",
"mac": "01:02:03:04:05:06",
"static": true
},
{
"expires": "",
"ip": "2001:db8::2",
"hostname": "host3",
"mac": "06:05:04:03:02:01",
"static": true
}
],
"version": 1
}

View file

@ -0,0 +1,33 @@
{
"leases": [
{
"expires": "",
"ip": "192.168.0.2",
"hostname": "host1",
"mac": "01:02:03:04:05:06",
"static": true
},
{
"expires": "",
"ip": "192.168.0.3",
"hostname": "host2",
"mac": "06:05:04:03:02:01",
"static": true
},
{
"expires": "",
"ip": "2001:db8::2",
"hostname": "host3",
"mac": "02:03:04:05:06:07",
"static": true
},
{
"expires": "",
"ip": "2001:db8::3",
"hostname": "host4",
"mac": "06:05:04:03:02:02",
"static": true
}
],
"version": 1
}

View file

@ -0,0 +1,26 @@
{
"leases": [
{
"expires": "",
"ip": "192.168.0.2",
"hostname": "host1",
"mac": "01:02:03:04:05:06",
"static": true
},
{
"expires": "",
"ip": "192.168.0.3",
"hostname": "host2",
"mac": "01:02:03:04:05:07",
"static": true
},
{
"expires": "",
"ip": "2001:db8::2",
"hostname": "host4",
"mac": "06:05:04:03:02:02",
"static": true
}
],
"version": 1
}

View file

@ -0,0 +1,33 @@
{
"leases": [
{
"expires": "",
"ip": "192.168.0.2",
"hostname": "host1",
"mac": "01:02:03:04:05:06",
"static": true
},
{
"expires": "",
"ip": "192.168.0.3",
"hostname": "host2",
"mac": "06:05:04:03:02:01",
"static": true
},
{
"expires": "",
"ip": "172.16.0.3",
"hostname": "host3",
"mac": "02:03:04:05:06:07",
"static": true
},
{
"expires": "",
"ip": "172.16.0.4",
"hostname": "host4",
"mac": "01:02:03:04:05:06",
"static": true
}
],
"version": 1
}

View file

@ -0,0 +1,15 @@
{
"leases": [{
"expires": "2042-01-02T03:04:05Z",
"ip": "192.168.0.3",
"hostname": "example.host",
"mac": "AA:AA:AA:AA:AA:AA",
"static": false
}, {
"ip": "192.168.0.4",
"hostname": "example.static.host",
"mac": "BB:BB:BB:BB:BB:BB",
"static": true
}],
"version": 1
}

View file

@ -82,8 +82,12 @@ func (c *IPv4Config) validate() (err error) {
return errors.Join(errs...) return errors.Join(errs...)
} }
// netInterfaceV4 is a DHCP interface for IPv4 address family. // dhcpInterfaceV4 is a DHCP interface for IPv4 address family.
type netInterfaceV4 struct { type dhcpInterfaceV4 struct {
// common is the common part of any network interface within the DHCP
// server.
common *netInterface
// gateway is the IP address of the network gateway. // gateway is the IP address of the network gateway.
gateway netip.Addr gateway netip.Addr
@ -101,25 +105,22 @@ type netInterfaceV4 struct {
// explicitOpts are the user-configured options. It must not have // explicitOpts are the user-configured options. It must not have
// intersections with implicitOpts. // intersections with implicitOpts.
explicitOpts layers.DHCPOptions explicitOpts layers.DHCPOptions
// netInterface is embedded here to provide some common network interface
// logic.
netInterface
} }
// newNetInterfaceV4 creates a new DHCP interface for IPv4 address family with // newDHCPInterfaceV4 creates a new DHCP interface for IPv4 address family with
// the given configuration. It returns an error if the given configuration // the given configuration. It returns an error if the given configuration
// can't be used. // can't be used.
func newNetInterfaceV4( func newDHCPInterfaceV4(
ctx context.Context, ctx context.Context,
l *slog.Logger, l *slog.Logger,
name string, name string,
conf *IPv4Config, conf *IPv4Config,
) (i *netInterfaceV4, err error) { ) (i *dhcpInterfaceV4, err error) {
l = l.With( l = l.With(
keyInterface, name, keyInterface, name,
keyFamily, netutil.AddrFamilyIPv4, keyFamily, netutil.AddrFamilyIPv4,
) )
if !conf.Enabled { if !conf.Enabled {
l.DebugContext(ctx, "disabled") l.DebugContext(ctx, "disabled")
@ -143,35 +144,31 @@ func newNetInterfaceV4(
return nil, fmt.Errorf("gateway ip %s in the ip range %s", conf.GatewayIP, addrSpace) return nil, fmt.Errorf("gateway ip %s in the ip range %s", conf.GatewayIP, addrSpace)
} }
i = &netInterfaceV4{ i = &dhcpInterfaceV4{
gateway: conf.GatewayIP, gateway: conf.GatewayIP,
subnet: subnet, subnet: subnet,
addrSpace: addrSpace, addrSpace: addrSpace,
netInterface: netInterface{ common: newNetInterface(name, l, conf.LeaseDuration),
name: name,
leaseTTL: conf.LeaseDuration,
logger: l,
},
} }
i.implicitOpts, i.explicitOpts = conf.options(ctx, l) i.implicitOpts, i.explicitOpts = conf.options(ctx, l)
return i, nil return i, nil
} }
// netInterfacesV4 is a slice of network interfaces of IPv4 address family. // dhcpInterfacesV4 is a slice of network interfaces of IPv4 address family.
type netInterfacesV4 []*netInterfaceV4 type dhcpInterfacesV4 []*dhcpInterfaceV4
// find returns the first network interface within ifaces containing ip. It // find returns the first network interface within ifaces containing ip. It
// returns false if there is no such interface. // returns false if there is no such interface.
func (ifaces netInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok bool) { func (ifaces dhcpInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok bool) {
i := slices.IndexFunc(ifaces, func(iface *netInterfaceV4) (contains bool) { i := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV4) (contains bool) {
return iface.subnet.Contains(ip) return iface.subnet.Contains(ip)
}) })
if i < 0 { if i < 0 {
return nil, false return nil, false
} }
return &ifaces[i].netInterface, true return ifaces[i].common, true
} }
// options returns the implicit and explicit options for the interface. The two // options returns the implicit and explicit options for the interface. The two

View file

@ -62,10 +62,12 @@ func (c *IPv6Config) validate() (err error) {
return errors.Join(errs...) return errors.Join(errs...)
} }
// netInterfaceV6 is a DHCP interface for IPv6 address family. // dhcpInterfaceV6 is a DHCP interface for IPv6 address family.
// type dhcpInterfaceV6 struct {
// TODO(e.burkov): Add options. // common is the common part of any network interface within the DHCP
type netInterfaceV6 struct { // server.
common *netInterface
// rangeStart is the first IP address in the range. // rangeStart is the first IP address in the range.
rangeStart netip.Addr rangeStart netip.Addr
@ -78,10 +80,6 @@ type netInterfaceV6 struct {
// intersections with implicitOpts. // intersections with implicitOpts.
explicitOpts layers.DHCPv6Options explicitOpts layers.DHCPv6Options
// netInterface is embedded here to provide some common network interface
// logic.
netInterface
// raSLAACOnly defines if DHCP should send ICMPv6.RA packets without MO // raSLAACOnly defines if DHCP should send ICMPv6.RA packets without MO
// flags. // flags.
raSLAACOnly bool raSLAACOnly bool
@ -90,16 +88,16 @@ type netInterfaceV6 struct {
raAllowSLAAC bool raAllowSLAAC bool
} }
// newNetInterfaceV6 creates a new DHCP interface for IPv6 address family with // newDHCPInterfaceV6 creates a new DHCP interface for IPv6 address family with
// the given configuration. // the given configuration.
// //
// TODO(e.burkov): Validate properly. // TODO(e.burkov): Validate properly.
func newNetInterfaceV6( func newDHCPInterfaceV6(
ctx context.Context, ctx context.Context,
l *slog.Logger, l *slog.Logger,
name string, name string,
conf *IPv6Config, conf *IPv6Config,
) (i *netInterfaceV6) { ) (i *dhcpInterfaceV6) {
l = l.With(keyInterface, name, keyFamily, netutil.AddrFamilyIPv6) l = l.With(keyInterface, name, keyFamily, netutil.AddrFamilyIPv6)
if !conf.Enabled { if !conf.Enabled {
l.DebugContext(ctx, "disabled") l.DebugContext(ctx, "disabled")
@ -107,13 +105,9 @@ func newNetInterfaceV6(
return nil return nil
} }
i = &netInterfaceV6{ i = &dhcpInterfaceV6{
rangeStart: conf.RangeStart, rangeStart: conf.RangeStart,
netInterface: netInterface{ common: newNetInterface(name, l, conf.LeaseDuration),
name: name,
leaseTTL: conf.LeaseDuration,
logger: l,
},
raSLAACOnly: conf.RASLAACOnly, raSLAACOnly: conf.RASLAACOnly,
raAllowSLAAC: conf.RAAllowSLAAC, raAllowSLAAC: conf.RAAllowSLAAC,
} }
@ -122,12 +116,12 @@ func newNetInterfaceV6(
return i return i
} }
// netInterfacesV4 is a slice of network interfaces of IPv4 address family. // dhcpInterfacesV6 is a slice of network interfaces of IPv6 address family.
type netInterfacesV6 []*netInterfaceV6 type dhcpInterfacesV6 []*dhcpInterfaceV6
// find returns the first network interface within ifaces containing ip. It // find returns the first network interface within ifaces containing ip. It
// returns false if there is no such interface. // returns false if there is no such interface.
func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool) { func (ifaces dhcpInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool) {
// prefLen is the length of prefix to match ip against. // prefLen is the length of prefix to match ip against.
// //
// TODO(e.burkov): DHCPv6 inherits the weird behavior of legacy // TODO(e.burkov): DHCPv6 inherits the weird behavior of legacy
@ -136,7 +130,7 @@ func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool
// be used instead. // be used instead.
const prefLen = netutil.IPv6BitLen - 8 const prefLen = netutil.IPv6BitLen - 8
i := slices.IndexFunc(ifaces, func(iface *netInterfaceV6) (contains bool) { i := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV6) (contains bool) {
return !ip.Less(iface.rangeStart) && return !ip.Less(iface.rangeStart) &&
netip.PrefixFrom(iface.rangeStart, prefLen).Contains(ip) netip.PrefixFrom(iface.rangeStart, prefLen).Contains(ip)
}) })
@ -144,7 +138,7 @@ func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool
return nil, false return nil, false
} }
return &ifaces[i].netInterface, true return ifaces[i].common, true
} }
// options returns the implicit and explicit options for the interface. The two // options returns the implicit and explicit options for the interface. The two

View file

@ -8,6 +8,7 @@ import (
"testing" "testing"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/quic-go/quic-go" "github.com/quic-go/quic-go"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -217,7 +218,8 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
} }
srv := &Server{ srv := &Server{
conf: ServerConfig{TLSConfig: tlsConf}, conf: ServerConfig{TLSConfig: tlsConf},
baseLogger: slogutil.NewDiscardLogger(),
} }
var ( var (

View file

@ -22,6 +22,7 @@ import (
"github.com/AdguardTeam/golibs/container" "github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
@ -158,7 +159,7 @@ type Config struct {
// IpsetList is the ipset configuration that allows AdGuard Home to add IP // IpsetList is the ipset configuration that allows AdGuard Home to add IP
// addresses of the specified domain names to an ipset list. Syntax: // addresses of the specified domain names to an ipset list. Syntax:
// //
// DOMAIN[,DOMAIN].../IPSET_NAME // DOMAIN[,DOMAIN].../IPSET_NAME[,IPSET_NAME]...
// //
// This field is ignored if [IpsetListFileName] is set. // This field is ignored if [IpsetListFileName] is set.
IpsetList []string `yaml:"ipset"` IpsetList []string `yaml:"ipset"`
@ -301,6 +302,8 @@ type ServerConfig struct {
// UpstreamMode is a enumeration of upstream mode representations. See // UpstreamMode is a enumeration of upstream mode representations. See
// [proxy.UpstreamModeType]. // [proxy.UpstreamModeType].
//
// TODO(d.kolyshev): Consider using [proxy.UpstreamMode].
type UpstreamMode string type UpstreamMode string
const ( const (
@ -315,6 +318,7 @@ func (s *Server) newProxyConfig() (conf *proxy.Config, err error) {
trustedPrefixes := netutil.UnembedPrefixes(srvConf.TrustedProxies) trustedPrefixes := netutil.UnembedPrefixes(srvConf.TrustedProxies)
conf = &proxy.Config{ conf = &proxy.Config{
Logger: s.baseLogger.With(slogutil.KeyPrefix, "dnsproxy"),
HTTP3: srvConf.ServeHTTP3, HTTP3: srvConf.ServeHTTP3,
Ratelimit: int(srvConf.Ratelimit), Ratelimit: int(srvConf.Ratelimit),
RatelimitSubnetLenIPv4: srvConf.RatelimitSubnetLenIPv4, RatelimitSubnetLenIPv4: srvConf.RatelimitSubnetLenIPv4,
@ -420,8 +424,6 @@ func parseBogusNXDOMAIN(confBogusNXDOMAIN []string) (subnets []netip.Prefix, err
return subnets, nil return subnets, nil
} }
const defaultBlockedResponseTTL = 3600
// initDefaultSettings initializes default settings if nothing // initDefaultSettings initializes default settings if nothing
// is configured // is configured
func (s *Server) initDefaultSettings() { func (s *Server) initDefaultSettings() {
@ -452,24 +454,24 @@ func (s *Server) initDefaultSettings() {
// prepareIpsetListSettings reads and prepares the ipset configuration either // prepareIpsetListSettings reads and prepares the ipset configuration either
// from a file or from the data in the configuration file. // from a file or from the data in the configuration file.
func (s *Server) prepareIpsetListSettings() (err error) { func (s *Server) prepareIpsetListSettings() (ipsets []string, err error) {
fn := s.conf.IpsetListFileName fn := s.conf.IpsetListFileName
if fn == "" { if fn == "" {
return s.ipset.init(s.conf.IpsetList) return s.conf.IpsetList, nil
} }
// #nosec G304 -- Trust the path explicitly given by the user. // #nosec G304 -- Trust the path explicitly given by the user.
data, err := os.ReadFile(fn) data, err := os.ReadFile(fn)
if err != nil { if err != nil {
return err return nil, err
} }
ipsets := stringutil.SplitTrimmed(string(data), "\n") ipsets = stringutil.SplitTrimmed(string(data), "\n")
ipsets = stringutil.FilterOut(ipsets, IsCommentOrEmpty) ipsets = slices.DeleteFunc(ipsets, IsCommentOrEmpty)
log.Debug("dns: using %d ipset rules from file %q", len(ipsets), fn) log.Debug("dns: using %d ipset rules from file %q", len(ipsets), fn)
return s.ipset.init(ipsets) return ipsets, nil
} }
// loadUpstreams parses upstream DNS servers from the configured file or from // loadUpstreams parses upstream DNS servers from the configured file or from
@ -690,7 +692,7 @@ func matchesDomainWildcard(host, pat string) (ok bool) {
// the DNS names and patterns from certificate. dnsNames must be sorted. // the DNS names and patterns from certificate. dnsNames must be sorted.
func anyNameMatches(dnsNames []string, sni string) (ok bool) { func anyNameMatches(dnsNames []string, sni string) (ok bool) {
// Check sni is either a valid hostname or a valid IP address. // Check sni is either a valid hostname or a valid IP address.
if netutil.ValidateHostname(sni) != nil && net.ParseIP(sni) == nil { if !netutil.IsValidHostname(sni) && !netutil.IsValidIPString(sni) {
return false return false
} }

View file

@ -10,6 +10,7 @@ import (
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
) )
// DialContext is an [aghnet.DialContextFunc] that uses s to resolve hostnames. // DialContext is an [aghnet.DialContextFunc] that uses s to resolve hostnames.
@ -28,7 +29,7 @@ func (s *Server) DialContext(ctx context.Context, network, addr string) (conn ne
Timeout: time.Minute * 5, Timeout: time.Minute * 5,
} }
if net.ParseIP(host) != nil { if netutil.IsValidIPString(host) {
return dialer.DialContext(ctx, network, addr) return dialer.DialContext(ctx, network, addr)
} }

View file

@ -6,6 +6,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"log/slog"
"net" "net"
"net/http" "net/http"
"net/netip" "net/netip"
@ -27,6 +28,7 @@ import (
"github.com/AdguardTeam/golibs/cache" "github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/netutil/sysresolv" "github.com/AdguardTeam/golibs/netutil/sysresolv"
"github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/stringutil"
@ -121,12 +123,17 @@ type Server struct {
// access drops disallowed clients. // access drops disallowed clients.
access *accessManager access *accessManager
// baseLogger is used to create loggers for other entities. It should not
// have a prefix and must not be nil.
baseLogger *slog.Logger
// localDomainSuffix is the suffix used to detect internal hosts. It // localDomainSuffix is the suffix used to detect internal hosts. It
// must be a valid domain name plus dots on each side. // must be a valid domain name plus dots on each side.
localDomainSuffix string localDomainSuffix string
// ipset processes DNS requests using ipset data. // ipset processes DNS requests using ipset data. It must not be nil after
ipset ipsetCtx // initialization. See [newIpsetHandler].
ipset *ipsetHandler
// privateNets is the configured set of IP networks considered private. // privateNets is the configured set of IP networks considered private.
privateNets netutil.SubnetSet privateNets netutil.SubnetSet
@ -197,6 +204,10 @@ type DNSCreateParams struct {
PrivateNets netutil.SubnetSet PrivateNets netutil.SubnetSet
Anonymizer *aghnet.IPMut Anonymizer *aghnet.IPMut
EtcHosts *aghnet.HostsContainer EtcHosts *aghnet.HostsContainer
// Logger is used as a base logger. It must not be nil.
Logger *slog.Logger
LocalDomain string LocalDomain string
} }
@ -233,6 +244,7 @@ func NewServer(p DNSCreateParams) (s *Server, err error) {
stats: p.Stats, stats: p.Stats,
queryLog: p.QueryLog, queryLog: p.QueryLog,
privateNets: p.PrivateNets, privateNets: p.PrivateNets,
baseLogger: p.Logger,
// TODO(e.burkov): Use some case-insensitive string comparison. // TODO(e.burkov): Use some case-insensitive string comparison.
localDomainSuffix: strings.ToLower(localDomainSuffix), localDomainSuffix: strings.ToLower(localDomainSuffix),
etcHosts: etcHosts, etcHosts: etcHosts,
@ -596,11 +608,18 @@ func (s *Server) prepareLocalResolvers() (uc *proxy.UpstreamConfig, err error) {
// the primary DNS proxy instance. It assumes s.serverLock is locked or the // the primary DNS proxy instance. It assumes s.serverLock is locked or the
// Server not running. // Server not running.
func (s *Server) prepareInternalDNS() (err error) { func (s *Server) prepareInternalDNS() (err error) {
err = s.prepareIpsetListSettings() ipsetList, err := s.prepareIpsetListSettings()
if err != nil { if err != nil {
return fmt.Errorf("preparing ipset settings: %w", err) return fmt.Errorf("preparing ipset settings: %w", err)
} }
ipsetLogger := s.baseLogger.With(slogutil.KeyPrefix, "ipset")
s.ipset, err = newIpsetHandler(context.TODO(), ipsetLogger, ipsetList)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
bootOpts := &upstream.Options{ bootOpts := &upstream.Options{
Timeout: DefaultTimeout, Timeout: DefaultTimeout,
HTTPVersions: UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams), HTTPVersions: UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams),
@ -664,6 +683,7 @@ func (s *Server) setupAddrProc() {
s.addrProc = client.EmptyAddrProc{} s.addrProc = client.EmptyAddrProc{}
} else { } else {
c := s.conf.AddrProcConf c := s.conf.AddrProcConf
c.BaseLogger = s.baseLogger
c.DialContext = s.DialContext c.DialContext = s.DialContext
c.PrivateSubnets = s.privateNets c.PrivateSubnets = s.privateNets
c.UsePrivateRDNS = s.conf.UsePrivateRDNS c.UsePrivateRDNS = s.conf.UsePrivateRDNS
@ -707,6 +727,7 @@ func validateBlockingMode(
func (s *Server) prepareInternalProxy() (err error) { func (s *Server) prepareInternalProxy() (err error) {
srvConf := s.conf srvConf := s.conf
conf := &proxy.Config{ conf := &proxy.Config{
Logger: s.baseLogger.With(slogutil.KeyPrefix, "dnsproxy"),
CacheEnabled: true, CacheEnabled: true,
CacheSizeBytes: 4096, CacheSizeBytes: 4096,
PrivateRDNSUpstreamConfig: srvConf.PrivateRDNSUpstreamConfig, PrivateRDNSUpstreamConfig: srvConf.PrivateRDNSUpstreamConfig,

View file

@ -28,6 +28,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch" "github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
@ -99,6 +100,7 @@ func createTestServer(
DHCPServer: dhcp, DHCPServer: dhcp,
DNSFilter: f, DNSFilter: f,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed), PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
Logger: slogutil.NewDiscardLogger(),
}) })
require.NoError(t, err) require.NoError(t, err)
@ -339,7 +341,10 @@ func TestServer_timeout(t *testing.T) {
ServePlainDNS: true, ServePlainDNS: true,
} }
s, err := NewServer(DNSCreateParams{DNSFilter: createTestDNSFilter(t)}) s, err := NewServer(DNSCreateParams{
DNSFilter: createTestDNSFilter(t),
Logger: slogutil.NewDiscardLogger(),
})
require.NoError(t, err) require.NoError(t, err)
err = s.Prepare(srvConf) err = s.Prepare(srvConf)
@ -349,7 +354,10 @@ func TestServer_timeout(t *testing.T) {
}) })
t.Run("default", func(t *testing.T) { t.Run("default", func(t *testing.T) {
s, err := NewServer(DNSCreateParams{DNSFilter: createTestDNSFilter(t)}) s, err := NewServer(DNSCreateParams{
DNSFilter: createTestDNSFilter(t),
Logger: slogutil.NewDiscardLogger(),
})
require.NoError(t, err) require.NoError(t, err)
s.conf.Config.UpstreamMode = UpstreamModeLoadBalance s.conf.Config.UpstreamMode = UpstreamModeLoadBalance
@ -376,7 +384,9 @@ func TestServer_Prepare_fallbacks(t *testing.T) {
ServePlainDNS: true, ServePlainDNS: true,
} }
s, err := NewServer(DNSCreateParams{}) s, err := NewServer(DNSCreateParams{
Logger: slogutil.NewDiscardLogger(),
})
require.NoError(t, err) require.NoError(t, err)
err = s.Prepare(srvConf) err = s.Prepare(srvConf)
@ -962,6 +972,7 @@ func TestBlockedCustomIP(t *testing.T) {
DHCPServer: dhcp, DHCPServer: dhcp,
DNSFilter: f, DNSFilter: f,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed), PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
Logger: slogutil.NewDiscardLogger(),
}) })
require.NoError(t, err) require.NoError(t, err)
@ -1127,6 +1138,7 @@ func TestRewrite(t *testing.T) {
DHCPServer: dhcp, DHCPServer: dhcp,
DNSFilter: f, DNSFilter: f,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed), PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
Logger: slogutil.NewDiscardLogger(),
}) })
require.NoError(t, err) require.NoError(t, err)
@ -1256,6 +1268,7 @@ func TestPTRResponseFromDHCPLeases(t *testing.T) {
}, },
}, },
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed), PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
Logger: slogutil.NewDiscardLogger(),
LocalDomain: localDomain, LocalDomain: localDomain,
}) })
require.NoError(t, err) require.NoError(t, err)
@ -1341,6 +1354,7 @@ func TestPTRResponseFromHosts(t *testing.T) {
DHCPServer: dhcp, DHCPServer: dhcp,
DNSFilter: flt, DNSFilter: flt,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed), PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
Logger: slogutil.NewDiscardLogger(),
}) })
require.NoError(t, err) require.NoError(t, err)
@ -1392,24 +1406,29 @@ func TestNewServer(t *testing.T) {
in DNSCreateParams in DNSCreateParams
wantErrMsg string wantErrMsg string
}{{ }{{
name: "success", name: "success",
in: DNSCreateParams{}, in: DNSCreateParams{
Logger: slogutil.NewDiscardLogger(),
},
wantErrMsg: "", wantErrMsg: "",
}, { }, {
name: "success_local_tld", name: "success_local_tld",
in: DNSCreateParams{ in: DNSCreateParams{
Logger: slogutil.NewDiscardLogger(),
LocalDomain: "mynet", LocalDomain: "mynet",
}, },
wantErrMsg: "", wantErrMsg: "",
}, { }, {
name: "success_local_domain", name: "success_local_domain",
in: DNSCreateParams{ in: DNSCreateParams{
Logger: slogutil.NewDiscardLogger(),
LocalDomain: "my.local.net", LocalDomain: "my.local.net",
}, },
wantErrMsg: "", wantErrMsg: "",
}, { }, {
name: "bad_local_domain", name: "bad_local_domain",
in: DNSCreateParams{ in: DNSCreateParams{
Logger: slogutil.NewDiscardLogger(),
LocalDomain: "!!!", LocalDomain: "!!!",
}, },
wantErrMsg: `local domain: bad domain name "!!!": ` + wantErrMsg: `local domain: bad domain name "!!!": ` +

View file

@ -9,6 +9,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -57,6 +58,7 @@ func TestHandleDNSRequest_handleDNSRequest(t *testing.T) {
}, },
DNSFilter: f, DNSFilter: f,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed), PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
Logger: slogutil.NewDiscardLogger(),
}) })
require.NoError(t, err) require.NoError(t, err)
@ -229,6 +231,7 @@ func TestHandleDNSRequest_filterDNSResponse(t *testing.T) {
DHCPServer: &testDHCP{}, DHCPServer: &testDHCP{},
DNSFilter: f, DNSFilter: f,
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed), PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
Logger: slogutil.NewDiscardLogger(),
}) })
require.NoError(t, err) require.NoError(t, err)

View file

@ -228,7 +228,7 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
}, { }, {
name: "upstream_dns_bad", name: "upstream_dns_bad",
wantSet: `validating dns config: upstream servers: parsing error at index 0: ` + wantSet: `validating dns config: upstream servers: parsing error at index 0: ` +
`cannot prepare the upstream: invalid address !!!: bad hostname "!!!": ` + `cannot prepare the upstream: invalid address !!!: bad domain name "!!!": ` +
`bad top-level domain name label "!!!": bad top-level domain name label rune '!'`, `bad top-level domain name label "!!!": bad top-level domain name label rune '!'`,
}, { }, {
name: "bootstraps_bad", name: "bootstraps_bad",

View file

@ -1,28 +1,43 @@
package dnsforward package dnsforward
import ( import (
"context"
"fmt" "fmt"
"log/slog"
"net" "net"
"os" "os"
"strings" "strings"
"github.com/AdguardTeam/AdGuardHome/internal/ipset" "github.com/AdguardTeam/AdGuardHome/internal/ipset"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
// ipsetCtx is the ipset context. ipsetMgr can be nil. // ipsetHandler is the ipset context. ipsetMgr can be nil.
type ipsetCtx struct { type ipsetHandler struct {
ipsetMgr ipset.Manager ipsetMgr ipset.Manager
logger *slog.Logger
} }
// init initializes the ipset context. It is not safe for concurrent use. // newIpsetHandler returns a new initialized [ipsetHandler]. It is not safe for
// // concurrent use.
// TODO(a.garipov): Rewrite into a simple constructor? func newIpsetHandler(
func (c *ipsetCtx) init(ipsetConf []string) (err error) { ctx context.Context,
c.ipsetMgr, err = ipset.NewManager(ipsetConf) logger *slog.Logger,
if errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrPermission) { ipsetList []string,
) (h *ipsetHandler, err error) {
h = &ipsetHandler{
logger: logger,
}
conf := &ipset.Config{
Logger: logger,
Lines: ipsetList,
}
h.ipsetMgr, err = ipset.NewManager(ctx, conf)
if errors.Is(err, os.ErrInvalid) ||
errors.Is(err, os.ErrPermission) ||
errors.Is(err, errors.ErrUnsupported) {
// ipset cannot currently be initialized if the server was installed // ipset cannot currently be initialized if the server was installed
// from Snap or when the user or the binary doesn't have the required // from Snap or when the user or the binary doesn't have the required
// permissions, or when the kernel doesn't support netfilter. // permissions, or when the kernel doesn't support netfilter.
@ -31,30 +46,28 @@ func (c *ipsetCtx) init(ipsetConf []string) (err error) {
// //
// TODO(a.garipov): The Snap problem can probably be solved if we add // TODO(a.garipov): The Snap problem can probably be solved if we add
// the netlink-connector interface plug. // the netlink-connector interface plug.
log.Info("ipset: warning: cannot initialize: %s", err) logger.WarnContext(ctx, "cannot initialize", slogutil.KeyError, err)
return nil return h, nil
} else if errors.Is(err, errors.ErrUnsupported) {
log.Info("ipset: warning: %s", err)
return nil
} else if err != nil { } else if err != nil {
return fmt.Errorf("initializing ipset: %w", err) return nil, fmt.Errorf("initializing ipset: %w", err)
}
return h, nil
}
// close closes the Linux Netfilter connections. close can be called on a nil
// handler.
func (h *ipsetHandler) close() (err error) {
if h != nil && h.ipsetMgr != nil {
return h.ipsetMgr.Close()
} }
return nil return nil
} }
// close closes the Linux Netfilter connections. // dctxIsFilled returns true if dctx has enough information to process.
func (c *ipsetCtx) close() (err error) { func dctxIsFilled(dctx *dnsContext) (ok bool) {
if c.ipsetMgr != nil {
return c.ipsetMgr.Close()
}
return nil
}
func (c *ipsetCtx) dctxIsfilled(dctx *dnsContext) (ok bool) {
return dctx != nil && return dctx != nil &&
dctx.responseFromUpstream && dctx.responseFromUpstream &&
dctx.proxyCtx != nil && dctx.proxyCtx != nil &&
@ -65,8 +78,8 @@ func (c *ipsetCtx) dctxIsfilled(dctx *dnsContext) (ok bool) {
// skipIpsetProcessing returns true when the ipset processing can be skipped for // skipIpsetProcessing returns true when the ipset processing can be skipped for
// this request. // this request.
func (c *ipsetCtx) skipIpsetProcessing(dctx *dnsContext) (ok bool) { func (h *ipsetHandler) skipIpsetProcessing(dctx *dnsContext) (ok bool) {
if c == nil || c.ipsetMgr == nil || !c.dctxIsfilled(dctx) { if h == nil || h.ipsetMgr == nil || !dctxIsFilled(dctx) {
return true return true
} }
@ -108,31 +121,31 @@ func ipsFromAnswer(ans []dns.RR) (ip4s, ip6s []net.IP) {
} }
// process adds the resolved IP addresses to the domain's ipsets, if any. // process adds the resolved IP addresses to the domain's ipsets, if any.
func (c *ipsetCtx) process(dctx *dnsContext) (rc resultCode) { func (h *ipsetHandler) process(dctx *dnsContext) (rc resultCode) {
log.Debug("dnsforward: ipset: started processing") // TODO(s.chzhen): Use passed context.
defer log.Debug("dnsforward: ipset: finished processing") ctx := context.TODO()
h.logger.DebugContext(ctx, "started processing")
defer h.logger.DebugContext(ctx, "finished processing")
if c.skipIpsetProcessing(dctx) { if h.skipIpsetProcessing(dctx) {
return resultCodeSuccess return resultCodeSuccess
} }
log.Debug("ipset: starting processing")
req := dctx.proxyCtx.Req req := dctx.proxyCtx.Req
host := req.Question[0].Name host := req.Question[0].Name
host = strings.TrimSuffix(host, ".") host = strings.TrimSuffix(host, ".")
host = strings.ToLower(host) host = strings.ToLower(host)
ip4s, ip6s := ipsFromAnswer(dctx.proxyCtx.Res.Answer) ip4s, ip6s := ipsFromAnswer(dctx.proxyCtx.Res.Answer)
n, err := c.ipsetMgr.Add(host, ip4s, ip6s) n, err := h.ipsetMgr.Add(ctx, host, ip4s, ip6s)
if err != nil { if err != nil {
// Consider ipset errors non-critical to the request. // Consider ipset errors non-critical to the request.
log.Error("dnsforward: ipset: adding host ips: %s", err) h.logger.ErrorContext(ctx, "adding host ips", slogutil.KeyError, err)
return resultCodeSuccess return resultCodeSuccess
} }
log.Debug("dnsforward: ipset: added %d new ipset entries", n) h.logger.DebugContext(ctx, "added new ipset entries", "num", n)
return resultCodeSuccess return resultCodeSuccess
} }

View file

@ -1,10 +1,12 @@
package dnsforward package dnsforward
import ( import (
"context"
"net" "net"
"testing" "testing"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -16,7 +18,7 @@ type fakeIpsetMgr struct {
} }
// Add implements the aghnet.IpsetManager interface for *fakeIpsetMgr. // Add implements the aghnet.IpsetManager interface for *fakeIpsetMgr.
func (m *fakeIpsetMgr) Add(host string, ip4s, ip6s []net.IP) (n int, err error) { func (m *fakeIpsetMgr) Add(_ context.Context, host string, ip4s, ip6s []net.IP) (n int, err error) {
m.ip4s = append(m.ip4s, ip4s...) m.ip4s = append(m.ip4s, ip4s...)
m.ip6s = append(m.ip6s, ip6s...) m.ip6s = append(m.ip6s, ip6s...)
@ -58,7 +60,9 @@ func TestIpsetCtx_process(t *testing.T) {
responseFromUpstream: true, responseFromUpstream: true,
} }
ictx := &ipsetCtx{} ictx := &ipsetHandler{
logger: slogutil.NewDiscardLogger(),
}
rc := ictx.process(dctx) rc := ictx.process(dctx)
assert.Equal(t, resultCodeSuccess, rc) assert.Equal(t, resultCodeSuccess, rc)
@ -77,8 +81,9 @@ func TestIpsetCtx_process(t *testing.T) {
} }
m := &fakeIpsetMgr{} m := &fakeIpsetMgr{}
ictx := &ipsetCtx{ ictx := &ipsetHandler{
ipsetMgr: m, ipsetMgr: m,
logger: slogutil.NewDiscardLogger(),
} }
rc := ictx.process(dctx) rc := ictx.process(dctx)
@ -101,8 +106,9 @@ func TestIpsetCtx_process(t *testing.T) {
} }
m := &fakeIpsetMgr{} m := &fakeIpsetMgr{}
ictx := &ipsetCtx{ ictx := &ipsetHandler{
ipsetMgr: m, ipsetMgr: m,
logger: slogutil.NewDiscardLogger(),
} }
rc := ictx.process(dctx) rc := ictx.process(dctx)
@ -124,8 +130,9 @@ func TestIpsetCtx_SkipIpsetProcessing(t *testing.T) {
} }
m := &fakeIpsetMgr{} m := &fakeIpsetMgr{}
ictx := &ipsetCtx{ ictx := &ipsetHandler{
ipsetMgr: m, ipsetMgr: m,
logger: slogutil.NewDiscardLogger(),
} }
testCases := []struct { testCases := []struct {

View file

@ -58,7 +58,7 @@ func (s *Server) genDNSFilterMessage(
return s.replyCompressed(req) return s.replyCompressed(req)
} }
return s.newMsgNODATA(req) return s.NewMsgNODATA(req)
} }
switch res.Reason { switch res.Reason {
@ -344,51 +344,6 @@ func (s *Server) makeResponseREFUSED(req *dns.Msg) *dns.Msg {
return s.reply(req, dns.RcodeRefused) return s.reply(req, dns.RcodeRefused)
} }
// newMsgNODATA returns a properly initialized NODATA response.
//
// See https://www.rfc-editor.org/rfc/rfc2308#section-2.2.
func (s *Server) newMsgNODATA(req *dns.Msg) (resp *dns.Msg) {
resp = s.reply(req, dns.RcodeSuccess)
resp.Ns = s.genSOA(req)
return resp
}
func (s *Server) genSOA(request *dns.Msg) []dns.RR {
zone := ""
if len(request.Question) > 0 {
zone = request.Question[0].Name
}
soa := dns.SOA{
// values copied from verisign's nonexistent .com domain
// their exact values are not important in our use case because they are used for domain transfers between primary/secondary DNS servers
Refresh: 1800,
Retry: 900,
Expire: 604800,
Minttl: 86400,
// copied from AdGuard DNS
Ns: "fake-for-negative-caching.adguard.com.",
Serial: 100500,
// rest is request-specific
Hdr: dns.RR_Header{
Name: zone,
Rrtype: dns.TypeSOA,
Ttl: s.dnsFilter.BlockedResponseTTL(),
Class: dns.ClassINET,
},
Mbox: "hostmaster.", // zone will be appended later if it's not empty or "."
}
if soa.Hdr.Ttl == 0 {
soa.Hdr.Ttl = defaultBlockedResponseTTL
}
if len(zone) > 0 && zone[0] != '.' {
soa.Mbox += zone
}
return []dns.RR{&soa}
}
// type check // type check
var _ proxy.MessageConstructor = (*Server)(nil) var _ proxy.MessageConstructor = (*Server)(nil)
@ -425,3 +380,52 @@ func (s *Server) NewMsgNOTIMPLEMENTED(req *dns.Msg) (resp *dns.Msg) {
return resp return resp
} }
// NewMsgNODATA implements the [proxy.MessageConstructor] interface for *Server.
func (s *Server) NewMsgNODATA(req *dns.Msg) (resp *dns.Msg) {
resp = s.reply(req, dns.RcodeSuccess)
resp.Ns = s.genSOA(req)
return resp
}
func (s *Server) genSOA(req *dns.Msg) []dns.RR {
zone := ""
if len(req.Question) > 0 {
zone = req.Question[0].Name
}
const defaultBlockedResponseTTL = 3600
soa := dns.SOA{
// Values copied from verisign's nonexistent.com domain.
//
// Their exact values are not important in our use case because they are
// used for domain transfers between primary/secondary DNS servers.
Refresh: 1800,
Retry: 900,
Expire: 604800,
Minttl: 86400,
// copied from AdGuard DNS
Ns: "fake-for-negative-caching.adguard.com.",
Serial: 100500,
// rest is request-specific
Hdr: dns.RR_Header{
Name: zone,
Rrtype: dns.TypeSOA,
Ttl: s.dnsFilter.BlockedResponseTTL(),
Class: dns.ClassINET,
},
// zone will be appended later if it's not ".".
Mbox: "hostmaster.",
}
if soa.Hdr.Ttl == 0 {
soa.Hdr.Ttl = defaultBlockedResponseTTL
}
if zone != "." {
soa.Mbox += zone
}
return []dns.RR{&soa}
}

View file

@ -159,7 +159,7 @@ func (s *Server) processInitial(dctx *dnsContext) (rc resultCode) {
q := pctx.Req.Question[0] q := pctx.Req.Question[0]
qt := q.Qtype qt := q.Qtype
if s.conf.AAAADisabled && qt == dns.TypeAAAA { if s.conf.AAAADisabled && qt == dns.TypeAAAA {
_ = proxy.CheckDisabledAAAARequest(pctx, true) pctx.Res = s.NewMsgNODATA(pctx.Req)
return resultCodeFinish return resultCodeFinish
} }

View file

@ -10,6 +10,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/urlfilter/rules" "github.com/AdguardTeam/urlfilter/rules"
@ -430,6 +431,7 @@ func TestServer_ProcessDHCPHosts_localRestriction(t *testing.T) {
dnsFilter: createTestDNSFilter(t), dnsFilter: createTestDNSFilter(t),
dhcpServer: dhcp, dhcpServer: dhcp,
localDomainSuffix: localDomainSuffix, localDomainSuffix: localDomainSuffix,
baseLogger: slogutil.NewDiscardLogger(),
} }
req := &dns.Msg{ req := &dns.Msg{
@ -565,6 +567,7 @@ func TestServer_ProcessDHCPHosts(t *testing.T) {
dnsFilter: createTestDNSFilter(t), dnsFilter: createTestDNSFilter(t),
dhcpServer: testDHCP, dhcpServer: testDHCP,
localDomainSuffix: tc.suffix, localDomainSuffix: tc.suffix,
baseLogger: slogutil.NewDiscardLogger(),
} }
req := &dns.Msg{ req := &dns.Msg{

View file

@ -11,6 +11,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/stats" "github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -202,6 +203,7 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) {
ql := &testQueryLog{} ql := &testQueryLog{}
st := &testStats{} st := &testStats{}
srv := &Server{ srv := &Server{
baseLogger: slogutil.NewDiscardLogger(),
queryLog: ql, queryLog: ql,
stats: st, stats: st,
anonymizer: aghnet.NewIPMut(nil), anonymizer: aghnet.NewIPMut(nil),

View file

@ -150,12 +150,12 @@ func setProxyUpstreamMode(
) (err error) { ) (err error) {
switch upstreamMode { switch upstreamMode {
case UpstreamModeParallel: case UpstreamModeParallel:
conf.UpstreamMode = proxy.UModeParallel conf.UpstreamMode = proxy.UpstreamModeParallel
case UpstreamModeFastestAddr: case UpstreamModeFastestAddr:
conf.UpstreamMode = proxy.UModeFastestAddr conf.UpstreamMode = proxy.UpstreamModeFastestAddr
conf.FastestPingTimeout = fastestTimeout conf.FastestPingTimeout = fastestTimeout
case UpstreamModeLoadBalance: case UpstreamModeLoadBalance:
conf.UpstreamMode = proxy.UModeLoadBalance conf.UpstreamMode = proxy.UpstreamModeLoadBalance
default: default:
return fmt.Errorf("unexpected value %q", upstreamMode) return fmt.Errorf("unexpected value %q", upstreamMode)
} }

View file

@ -47,7 +47,6 @@ func fromCacheItem(item *cacheItem) (data []byte) {
data = binary.BigEndian.AppendUint64(data, uint64(expiry)) data = binary.BigEndian.AppendUint64(data, uint64(expiry))
for _, v := range item.hashes { for _, v := range item.hashes {
// nolint:looppointer // The subslice of v is used for a copy.
data = append(data, v[:]...) data = append(data, v[:]...)
} }
@ -63,7 +62,6 @@ func (c *Checker) findInCache(
i := 0 i := 0
for _, hash := range hashes { for _, hash := range hashes {
// nolint:looppointer // The has subslice is used for a cache lookup.
data := c.cache.Get(hash[:prefixLen]) data := c.cache.Get(hash[:prefixLen])
if data == nil { if data == nil {
hashes[i] = hash hashes[i] = hash
@ -98,7 +96,6 @@ func (c *Checker) storeInCache(hashesToRequest, respHashes []hostnameHash) {
for _, hash := range respHashes { for _, hash := range respHashes {
var pref prefix var pref prefix
// nolint:looppointer // The hash subslice is used for a copy.
copy(pref[:], hash[:]) copy(pref[:], hash[:])
hashToStore[pref] = append(hashToStore[pref], hash) hashToStore[pref] = append(hashToStore[pref], hash)
@ -109,11 +106,9 @@ func (c *Checker) storeInCache(hashesToRequest, respHashes []hostnameHash) {
} }
for _, hash := range hashesToRequest { for _, hash := range hashesToRequest {
// nolint:looppointer // The hash subslice is used for a cache lookup.
val := c.cache.Get(hash[:prefixLen]) val := c.cache.Get(hash[:prefixLen])
if val == nil { if val == nil {
var pref prefix var pref prefix
// nolint:looppointer // The hash subslice is used for a copy.
copy(pref[:], hash[:]) copy(pref[:], hash[:])
c.setCache(pref, nil) c.setCache(pref, nil)

View file

@ -173,7 +173,6 @@ func (c *Checker) getQuestion(hashes []hostnameHash) (q string) {
b := &strings.Builder{} b := &strings.Builder{}
for _, hash := range hashes { for _, hash := range hashes {
// nolint:looppointer // The hash subslice is used for hex encoding.
stringutil.WriteToBuilder(b, hex.EncodeToString(hash[:prefixLen]), ".") stringutil.WriteToBuilder(b, hex.EncodeToString(hash[:prefixLen]), ".")
} }

View file

@ -22,6 +22,7 @@ type SafeSearchConfig struct {
Bing bool `yaml:"bing" json:"bing"` Bing bool `yaml:"bing" json:"bing"`
DuckDuckGo bool `yaml:"duckduckgo" json:"duckduckgo"` DuckDuckGo bool `yaml:"duckduckgo" json:"duckduckgo"`
Ecosia bool `yaml:"ecosia" json:"ecosia"`
Google bool `yaml:"google" json:"google"` Google bool `yaml:"google" json:"google"`
Pixabay bool `yaml:"pixabay" json:"pixabay"` Pixabay bool `yaml:"pixabay" json:"pixabay"`
Yandex bool `yaml:"yandex" json:"yandex"` Yandex bool `yaml:"yandex" json:"yandex"`

View file

@ -14,6 +14,9 @@ var pixabay string
//go:embed rules/duckduckgo.txt //go:embed rules/duckduckgo.txt
var duckduckgo string var duckduckgo string
//go:embed rules/ecosia.txt
var ecosia string
//go:embed rules/yandex.txt //go:embed rules/yandex.txt
var yandex string var yandex string
@ -27,6 +30,7 @@ var youtube string
var safeSearchRules = map[Service]string{ var safeSearchRules = map[Service]string{
Bing: bing, Bing: bing,
DuckDuckGo: duckduckgo, DuckDuckGo: duckduckgo,
Ecosia: ecosia,
Google: google, Google: google,
Pixabay: pixabay, Pixabay: pixabay,
Yandex: yandex, Yandex: yandex,

View file

@ -1 +1,2 @@
|www.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com |www.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com
|edgeservices.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com

View file

@ -0,0 +1 @@
|www.ecosia.org^$dnsrewrite=NOERROR;CNAME;strict-safe-search.ecosia.org

Some files were not shown because too many files have changed in this diff Show more