Compare commits
48 Commits
AGDNS-2184
...
infra-fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de86f57902 | ||
|
|
a74c32f742 | ||
|
|
cbae07e8e6 | ||
|
|
6fe4b9440d | ||
|
|
b443cf35c4 | ||
|
|
76344f9785 | ||
|
|
aab6769fa2 | ||
|
|
0b8bf13453 | ||
|
|
738958d90a | ||
|
|
30c0bbe5cc | ||
|
|
184f476bdc | ||
|
|
cdf970fcbf | ||
|
|
1a6ec30bd7 | ||
|
|
edfa8c147f | ||
|
|
b6ed769652 | ||
|
|
bc6d20ff10 | ||
|
|
9ea4838c07 | ||
|
|
0e459a7369 | ||
|
|
c48cc980fc | ||
|
|
2af8595363 | ||
|
|
af0f43c0f8 | ||
|
|
d860764498 | ||
|
|
bf1101b460 | ||
|
|
5c2ecfaa41 | ||
|
|
f29a1cf23a | ||
|
|
42c7cd6f8e | ||
|
|
c0a33ce708 | ||
|
|
130560b104 | ||
|
|
e269260fbe | ||
|
|
9a6dd0dc55 | ||
|
|
9a29aa9232 | ||
|
|
93e4005125 | ||
|
|
beeb8f0522 | ||
|
|
0e5e8e4dde | ||
|
|
fcdebfa4d4 | ||
|
|
9d1c45fd94 | ||
|
|
3993f4c476 | ||
|
|
a1a31cd916 | ||
|
|
65b7d232ab | ||
|
|
6472140920 | ||
|
|
08d863dd3a | ||
|
|
28a6c24db2 | ||
|
|
66877c92d9 | ||
|
|
1c82be2950 | ||
|
|
8432593be1 | ||
|
|
bed86d57f3 | ||
|
|
1afe226ce8 | ||
|
|
1085d59a65 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -1,7 +1,7 @@
|
||||
'name': 'build'
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.22.4'
|
||||
'GO_VERSION': '1.23.1'
|
||||
'NODE_VERSION': '16'
|
||||
|
||||
'on':
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -1,7 +1,7 @@
|
||||
'name': 'lint'
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.22.4'
|
||||
'GO_VERSION': '1.23.1'
|
||||
|
||||
'on':
|
||||
'push':
|
||||
|
||||
123
CHANGELOG.md
123
CHANGELOG.md
@@ -7,6 +7,10 @@ The format is based on
|
||||
and this project adheres to
|
||||
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
<!--
|
||||
TODO(a.garipov): Use the common markdown formatting tools.
|
||||
-->
|
||||
|
||||
|
||||
|
||||
## [Unreleased]
|
||||
@@ -14,38 +18,123 @@ and this project adheres to
|
||||
<!--
|
||||
## [v0.108.0] - TBA
|
||||
|
||||
## [v0.107.51] - 2024-06-22 (APPROX.)
|
||||
## [v0.107.53] - 2024-07-24 (APPROX.)
|
||||
|
||||
See also the [v0.107.51 GitHub milestone][ms-v0.107.51].
|
||||
See also the [v0.107.53 GitHub milestone][ms-v0.107.53].
|
||||
|
||||
[ms-v0.107.51]: https://github.com/AdguardTeam/AdGuardHome/milestone/86?closed=1
|
||||
[ms-v0.107.53]: https://github.com/AdguardTeam/AdGuardHome/milestone/88?closed=1
|
||||
|
||||
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
|
||||
|
||||
- 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
|
||||
[#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.
|
||||
-->
|
||||
|
||||
|
||||
|
||||
## [v0.107.52] - 2024-07-04
|
||||
|
||||
See also the [v0.107.52 GitHub milestone][ms-v0.107.52].
|
||||
|
||||
### Security
|
||||
|
||||
- Go version has been updated to prevent the possibility of exploiting the Go
|
||||
vulnerabilities fixed in [Go 1.22.5][go-1.22.5].
|
||||
|
||||
### Added
|
||||
|
||||
- The ability to disable logging using the new `log.enabled` configuration
|
||||
property ([#7079]).
|
||||
|
||||
### Changed
|
||||
|
||||
- Frontend rewritten in TypeScript.
|
||||
- The `systemd`-based service now uses `journal` for logging by default. It
|
||||
also doesn't create the `/var/log/` directory anymore ([#7053]).
|
||||
|
||||
**NOTE:** With an installed service for changes to take effect, you need to
|
||||
reinstall the service using `-r` flag of the [install script][install-script]
|
||||
or via the CLI (with root privileges):
|
||||
|
||||
```sh
|
||||
./AdGuardHome -s uninstall
|
||||
./AdGuardHome -s install
|
||||
```
|
||||
|
||||
Don't forget to backup your configuration file and other important data before
|
||||
reinstalling the service.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Node 18 support, Node 20 will be required in future releases.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Panic caused by missing user-specific blocked services object in configuration
|
||||
file ([#7069]).
|
||||
- Tracking `/etc/hosts` file changes causing panics within particular
|
||||
filesystems on start ([#7076]).
|
||||
|
||||
[#7053]: https://github.com/AdguardTeam/AdGuardHome/issues/7053
|
||||
[#7069]: https://github.com/AdguardTeam/AdGuardHome/issues/7069
|
||||
[#7076]: https://github.com/AdguardTeam/AdGuardHome/issues/7076
|
||||
[#7079]: https://github.com/AdguardTeam/AdGuardHome/issues/7079
|
||||
|
||||
[go-1.22.5]: https://groups.google.com/g/golang-announce/c/gyb7aM1C9H4
|
||||
[install-script]: https://github.com/AdguardTeam/AdGuardHome/?tab=readme-ov-file#automated-install-linux-and-mac
|
||||
|
||||
[ms-v0.107.52]: https://github.com/AdguardTeam/AdGuardHome/milestone/87?closed=1
|
||||
|
||||
|
||||
|
||||
## [v0.107.51] - 2024-06-06
|
||||
|
||||
See also the [v0.107.51 GitHub milestone][ms-v0.107.51].
|
||||
|
||||
### Security
|
||||
|
||||
- Go version has been updated to prevent the possibility of exploiting the Go
|
||||
vulnerabilities fixed in [Go 1.22.4][go-1.22.4].
|
||||
|
||||
### Changed
|
||||
|
||||
- Frontend rewritten in TypeScript.
|
||||
- The HTTP server's write timeout has been increased from 1 minute to 5 minutes
|
||||
to match the one used by AdGuard Home's HTTP client to fetch filtering-list
|
||||
data ([#7041]).
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Node 18 support, Node 20 will be required in future releases.
|
||||
|
||||
[#7041]: https://github.com/AdguardTeam/AdGuardHome/issues/7041
|
||||
|
||||
[go-1.22.4]: https://groups.google.com/g/golang-announce/c/XbxouI9gY7k/
|
||||
|
||||
<!--
|
||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
||||
-->
|
||||
[go-1.22.4]: https://groups.google.com/g/golang-announce/c/XbxouI9gY7k/
|
||||
[ms-v0.107.51]: https://github.com/AdguardTeam/AdGuardHome/milestone/86?closed=1
|
||||
|
||||
|
||||
|
||||
@@ -3006,11 +3095,13 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
|
||||
|
||||
|
||||
<!--
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.51...HEAD
|
||||
[v0.107.51]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.50...v0.107.51
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.53...HEAD
|
||||
[v0.107.53]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.52...v0.107.53
|
||||
-->
|
||||
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.50...HEAD
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.52...HEAD
|
||||
[v0.107.52]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.51...v0.107.52
|
||||
[v0.107.51]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.50...v0.107.51
|
||||
[v0.107.50]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.49...v0.107.50
|
||||
[v0.107.49]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.48...v0.107.49
|
||||
[v0.107.48]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.47...v0.107.48
|
||||
|
||||
48
Makefile
48
Makefile
@@ -8,7 +8,7 @@
|
||||
# Makefile. Bump this number every time a significant change is made to
|
||||
# this Makefile.
|
||||
#
|
||||
# AdGuard-Project-Version: 4
|
||||
# AdGuard-Project-Version: 6
|
||||
|
||||
# Don't name these macros "GO" etc., because GNU Make apparently makes
|
||||
# them exported environment variables with the literal value of
|
||||
@@ -23,11 +23,13 @@ VERBOSE.MACRO = $${VERBOSE:-0}
|
||||
CHANNEL = development
|
||||
CLIENT_DIR = client
|
||||
COMMIT = $$( git rev-parse --short HEAD )
|
||||
DEPLOY_SCRIPT_PATH = not/a/real/path
|
||||
DIST_DIR = dist
|
||||
GOAMD64 = v1
|
||||
GOPROXY = https://goproxy.cn|https://proxy.golang.org|direct
|
||||
GOPROXY = https://proxy.golang.org|direct
|
||||
GOSUMDB = sum.golang.google.cn
|
||||
GOTOOLCHAIN = go1.22.4
|
||||
GOTOOLCHAIN = go1.23.1
|
||||
GOTELEMETRY = off
|
||||
GPG_KEY = devteam@adguard.com
|
||||
GPG_KEY_PASSPHRASE = not-a-real-password
|
||||
NPM = npm
|
||||
@@ -36,6 +38,7 @@ NPM_INSTALL_FLAGS = $(NPM_FLAGS) --quiet --no-progress --ignore-engines\
|
||||
--ignore-optional --ignore-platform --ignore-scripts
|
||||
RACE = 0
|
||||
SIGN = 1
|
||||
SIGNER_API_KEY = not-a-real-key
|
||||
VERSION = v0.0.0
|
||||
YARN = yarn
|
||||
|
||||
@@ -59,20 +62,28 @@ BUILD_RELEASE_DEPS_1 = go-deps
|
||||
ENV = env\
|
||||
CHANNEL='$(CHANNEL)'\
|
||||
COMMIT='$(COMMIT)'\
|
||||
DEPLOY_SCRIPT_PATH='$(DEPLOY_SCRIPT_PATH)' \
|
||||
DIST_DIR='$(DIST_DIR)'\
|
||||
GO="$(GO.MACRO)"\
|
||||
GOAMD64="$(GOAMD64)"\
|
||||
GOAMD64='$(GOAMD64)'\
|
||||
GOPROXY='$(GOPROXY)'\
|
||||
GOSUMDB='$(GOSUMDB)'\
|
||||
GOTELEMETRY='$(GOTELEMETRY)'\
|
||||
GOTOOLCHAIN='$(GOTOOLCHAIN)'\
|
||||
GPG_KEY='$(GPG_KEY)'\
|
||||
GPG_KEY_PASSPHRASE='$(GPG_KEY_PASSPHRASE)'\
|
||||
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
|
||||
RACE='$(RACE)'\
|
||||
SIGN='$(SIGN)'\
|
||||
SIGNER_API_KEY='$(SIGNER_API_KEY)' \
|
||||
NEXTAPI='$(NEXTAPI)'\
|
||||
VERBOSE="$(VERBOSE.MACRO)"\
|
||||
VERSION='$(VERSION)'\
|
||||
VERSION="$(VERSION)"\
|
||||
|
||||
# Keep the line above blank.
|
||||
|
||||
ENV_MISC = env\
|
||||
VERBOSE="$(VERBOSE.MACRO)"\
|
||||
|
||||
# Keep the line above blank.
|
||||
|
||||
@@ -101,23 +112,22 @@ js-deps: ; $(NPM) $(NPM_INSTALL_FLAGS) ci
|
||||
js-lint: ; $(NPM) $(NPM_FLAGS) run lint
|
||||
js-test: ; $(NPM) $(NPM_FLAGS) run test
|
||||
|
||||
go-bench: ; $(ENV) "$(SHELL)" ./scripts/make/go-bench.sh
|
||||
go-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh
|
||||
go-deps: ; $(ENV) "$(SHELL)" ./scripts/make/go-deps.sh
|
||||
go-fuzz: ; $(ENV) "$(SHELL)" ./scripts/make/go-fuzz.sh
|
||||
go-lint: ; $(ENV) "$(SHELL)" ./scripts/make/go-lint.sh
|
||||
go-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-tools.sh
|
||||
|
||||
go-bench: ; $(ENV) "$(SHELL)" ./scripts/make/go-bench.sh
|
||||
go-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh
|
||||
go-deps: ; $(ENV) "$(SHELL)" ./scripts/make/go-deps.sh
|
||||
go-env: ; $(ENV) "$(GO.MACRO)" env
|
||||
go-fuzz: ; $(ENV) "$(SHELL)" ./scripts/make/go-fuzz.sh
|
||||
go-lint: ; $(ENV) "$(SHELL)" ./scripts/make/go-lint.sh
|
||||
# TODO(a.garipov): Think about making RACE='1' the default for all
|
||||
# targets.
|
||||
go-test: ; $(ENV) RACE='1' "$(SHELL)" ./scripts/make/go-test.sh
|
||||
|
||||
go-upd-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-upd-tools.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-check: go-tools go-lint go-test
|
||||
|
||||
# A quick check to make sure that all supported operating systems can be
|
||||
# typechecked and built successfully.
|
||||
# A quick check to make sure that all operating systems relevant to the
|
||||
# development of the project can be typechecked and built successfully.
|
||||
go-os-check:
|
||||
env GOOS='darwin' "$(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='windows' "$(GO.MACRO)" vet ./internal/...
|
||||
|
||||
|
||||
openapi-lint: ; cd ./openapi/ && $(YARN) test
|
||||
openapi-show: ; cd ./openapi/ && $(YARN) start
|
||||
|
||||
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
|
||||
|
||||
@@ -205,7 +205,7 @@ Run `make init` to prepare the development environment.
|
||||
|
||||
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;
|
||||
- [npm](https://www.npmjs.com/) v8 or later;
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
# Make sure to sync any changes with the branch overrides below.
|
||||
'variables':
|
||||
'channel': 'edge'
|
||||
'dockerFrontend': '${bamboo.adguardRegistryBasePath}/home-js-builder:2.0'
|
||||
'dockerGo': '${bamboo.adguardRegistryBasePath}/go-builder:1.22.4--1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||
'dockerGo': 'adguard/go-builder:1.23.1--1'
|
||||
|
||||
'stages':
|
||||
- 'Build frontend':
|
||||
@@ -91,6 +91,11 @@
|
||||
'tasks':
|
||||
- 'checkout':
|
||||
'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':
|
||||
'interpreter': 'SHELL'
|
||||
'scripts':
|
||||
@@ -99,6 +104,9 @@
|
||||
|
||||
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.
|
||||
echo "${bamboo.gpgSecretKeyPart1}${bamboo.gpgSecretKeyPart2}"\
|
||||
| awk '{ gsub(/\\n/, "\n"); print; }'\
|
||||
@@ -107,6 +115,8 @@
|
||||
make\
|
||||
CHANNEL=${bamboo.channel}\
|
||||
GPG_KEY_PASSPHRASE=${bamboo.gpgPassword}\
|
||||
DEPLOY_SCRIPT_PATH="./bamboo-deploy-publisher/deploy.sh"\
|
||||
SIGNER_API_KEY="${bamboo.adguardHomeWinSignerSecretApiKey}"\
|
||||
FRONTEND_PREBUILT=1\
|
||||
PARALLELISM=1\
|
||||
VERBOSE=2\
|
||||
@@ -265,8 +275,8 @@
|
||||
# need to build a few of these.
|
||||
'variables':
|
||||
'channel': 'beta'
|
||||
'dockerFrontend': '${bamboo.adguardRegistryBasePath}/home-js-builder:2.0'
|
||||
'dockerGo': '${bamboo.adguardRegistryBasePath}/go-builder:1.22.4--1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||
'dockerGo': 'adguard/go-builder:1.23.1--1'
|
||||
# release-vX.Y.Z branches are the branches from which the actual final
|
||||
# release is built.
|
||||
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
|
||||
@@ -281,5 +291,5 @@
|
||||
# are the ones that actually get released.
|
||||
'variables':
|
||||
'channel': 'release'
|
||||
'dockerFrontend': '${bamboo.adguardRegistryBasePath}/home-js-builder:2.0'
|
||||
'dockerGo': '${bamboo.adguardRegistryBasePath}/go-builder:1.22.4--1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||
'dockerGo': 'adguard/go-builder:1.23.1--1'
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
'key': 'AHBRTSPECS'
|
||||
'name': 'AdGuard Home - Build and run tests'
|
||||
'variables':
|
||||
'dockerFrontend': '${bamboo.adguardRegistryBasePath}/home-js-builder:2.0'
|
||||
'dockerGo': '${bamboo.adguardRegistryBasePath}/go-builder:1.22.4--1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||
'dockerGo': 'adguard/go-builder:1.23.1--1'
|
||||
'channel': 'development'
|
||||
|
||||
'stages':
|
||||
@@ -54,6 +54,7 @@
|
||||
'requirements':
|
||||
- 'adg-docker': 'true'
|
||||
|
||||
# TODO(e.burkov): Add the linting stage for markdown docs and shell scripts.
|
||||
'Test backend':
|
||||
'docker':
|
||||
'image': '${bamboo.dockerGo}'
|
||||
@@ -194,6 +195,6 @@
|
||||
# Set the default release channel on the release branch to beta, as we
|
||||
# may need to build a few of these.
|
||||
'variables':
|
||||
'dockerFrontend': '${bamboo.adguardRegistryBasePath}/home-js-builder:2.0'
|
||||
'dockerGo': '${bamboo.adguardRegistryBasePath}/go-builder:1.22.4--1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||
'dockerGo': 'adguard/go-builder:1.23.1--1'
|
||||
'channel': 'candidate'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"upstream_parallel": "Použijte paralelní požadavky na urychlení řešení simultánním dotazováním na všechny navazující servery.",
|
||||
"parallel_requests": "Paralelní požadavky",
|
||||
"load_balancing": "Optimalizace vytížení",
|
||||
"load_balancing_desc": "Optimalizovaný dotaz na odchozí server. AdGuard Home použije vážený náhodný algoritmus k výběru serveru, takže nejrychlejší server je používán častěji.",
|
||||
"load_balancing_desc": "Dotazy jednoho odchozího serveru ve stejný čas. AdGuard Home používá náhodný algoritmus pro výběr serverů s nejnižším počtem neúspěšných vyhledávání a nejnižší průměrnou dobou vyhledávání.",
|
||||
"bootstrap_dns": "Bootstrap DNS servery",
|
||||
"bootstrap_dns_desc": "IP adresy DNS serverů používaných k překladu IP adres řešitelů DoH/DoT, které zadáte jako odchozí servery. Komentáře nejsou povoleny.",
|
||||
"fallback_dns_title": "Záložní DNS servery",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"upstream_parallel": "Brug parallelforespørgsler til at accelerere fortolkningen ved at forespørge alle upstream-servere samtidigt.",
|
||||
"parallel_requests": "Parallelle forespørgsler",
|
||||
"load_balancing": "Belastningsfordeling",
|
||||
"load_balancing_desc": "Forespørg én server ad gangen. AdGuard Home vil bruge en vægtet randomiseringsalgoritme til valg af server, så den hurtigste server oftere anvendes.",
|
||||
"load_balancing_desc": "Forespørg én upstream-server ad gangen. AdGuard Home bruger en vægtet tilfældighedsalgoritme til vælg af servere med det laveste antal fejlslagne opslag og den laveste gennemsnitlige opslagstid.",
|
||||
"bootstrap_dns": "Bootstrap DNS-servere",
|
||||
"bootstrap_dns_desc": "IP-adresser på DNS-servere, som bruges til at opløse IP-adresser på de DoH/DoT-opløsere, som angives som upstreams. Kommentarer er ikke tilladt.",
|
||||
"fallback_dns_title": "Reserve DNS-servere",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"upstream_parallel": "Parallele Abfragen verwenden, um das Auflösen zu beschleunigen, indem alle Upstream-Server gleichzeitig abgefragt werden.",
|
||||
"parallel_requests": "Paralleles Abfragen",
|
||||
"load_balancing": "Lastverteilung",
|
||||
"load_balancing_desc": "Einen Server nach dem anderen abfragen. AdGuard Home verwendet den gewichteten Zufallsalgorithmus, um den Server so auszuwählen, dass der schnellste Server häufiger verwendet wird.",
|
||||
"load_balancing_desc": "Es wird jeweils ein Upstream-Server abgefragt. AdGuard Home verwendet einen gewichteten Zufallsalgorithmus, um die Server mit der geringsten Anzahl an fehlgeschlagenen Suchvorgängen und der niedrigsten durchschnittlichen Suchzeit auszuwählen.",
|
||||
"bootstrap_dns": "Bootstrap-DNS-Server",
|
||||
"bootstrap_dns_desc": "IP-Adressen der DNS-Server, die zum Auflösen der IP-Adressen von DoH/DoT Upstream-Servern verwendet werden, die Sie angegeben haben. Kommentare sind nicht erlaubt.",
|
||||
"fallback_dns_title": "Fallback-DNS-Server",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"upstream_parallel": "Use parallel queries to speed up resolving by querying all upstream servers simultaneously.",
|
||||
"parallel_requests": "Parallel requests",
|
||||
"load_balancing": "Load-balancing",
|
||||
"load_balancing_desc": "Query one upstream server at a time. AdGuard Home uses its weighted random algorithm to pick the server so that the fastest server is used more often.",
|
||||
"load_balancing_desc": "Query one upstream server at a time. AdGuard Home uses a weighted random algorithm to select servers with the lowest number of failed lookups and the lowest average lookup time.",
|
||||
"bootstrap_dns": "Bootstrap DNS servers",
|
||||
"bootstrap_dns_desc": "IP addresses of DNS servers used to resolve IP addresses of the DoH/DoT resolvers you specify as upstreams. Comments are not permitted.",
|
||||
"fallback_dns_title": "Fallback DNS servers",
|
||||
@@ -154,7 +154,7 @@
|
||||
"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.",
|
||||
"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",
|
||||
"general_settings": "General settings",
|
||||
"dns_settings": "DNS settings",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"upstream_parallel": "Usar consultas paralelas para acelerar la resolución al consultar simultáneamente a todos los servidores DNS de subida.",
|
||||
"parallel_requests": "Consultas paralelas",
|
||||
"load_balancing": "Balanceo de carga",
|
||||
"load_balancing_desc": "Consulta un servidor DNS de subida a la vez. AdGuard Home utiliza su algoritmo aleatorio ponderado para elegir el servidor más rápido y sea utilizado con más frecuencia.",
|
||||
"load_balancing_desc": "Consulta un servidor upstream a la vez. AdGuard Home utiliza un algoritmo aleatorio ponderado para seleccionar los servidores con el menor número de fallos y el menor tiempo medio de búsqueda.",
|
||||
"bootstrap_dns": "Servidores DNS de arranque",
|
||||
"bootstrap_dns_desc": "Direcciones IP de servidores DNS utilizadas para resolver direcciones IP de los solucionadores DoH/DoT que especifiques como ascendentes. No se permiten comentarios.",
|
||||
"fallback_dns_title": "Servidores DNS alternativos",
|
||||
|
||||
@@ -589,6 +589,7 @@
|
||||
"cache_optimistic_desc": "AdGuard Home را وادار می کند که از سمت حافظه پنهان پاسخ دهد حتی وقتی که موارد وارد شده منقضی شده باشد و همچنین سعی بر تازه کردن آنها می کند.",
|
||||
"filter_category_general": "General",
|
||||
"filter_category_security": "مسدودسازی بدافزار و فیشینگ",
|
||||
"filter_category_regional": "منطقهای",
|
||||
"filter_category_other": "ساير",
|
||||
"use_saved_key": "از کلید ذخیره شده قبلی استفاده کنید",
|
||||
"parental_control": "نظارت والدین",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"upstream_parallel": "Utilisez des requêtes parallèles pour accélérer la résolution en requêtant simultanément tous les serveurs en amont.",
|
||||
"parallel_requests": "Requêtes en parallèle",
|
||||
"load_balancing": "Équilibrage de charge",
|
||||
"load_balancing_desc": "Interroger un serveur en amont à la fois. AdGuard Home utilise son algorithme aléatoire pondéré pour choisir le serveur de sorte que le serveur le plus rapide soit utilisé plus souvent.",
|
||||
"load_balancing_desc": "Une requête par serveur en amont à la fois. AdGuard Home utilise un algorithme aléatoire pondéré pour sélectionner les serveurs avec le plus petit nombre d'échecs de recherche et le temps de recherche moyen le plus bas.",
|
||||
"bootstrap_dns": "Serveurs DNS d'amorçage",
|
||||
"bootstrap_dns_desc": "Les adresses IP des serveurs DNS utilisées pour résoudre les adresses IP des résolveurs DoH/DoT que vous spécifiez comme en amont. Les commentaires ne sont pas autorisés.",
|
||||
"fallback_dns_title": "Serveurs DNS de repli",
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
"fallback_dns_desc": "Popis rezervnih DNS poslužitelja koji se koriste kada uzvodni DNS poslužitelji ne odgovaraju. Sintaksa je ista kao u gornjem polju glavnog uzvodnog toka.",
|
||||
"fallback_dns_placeholder": "Unesite jedan rezervni DNS poslužitelj po retku",
|
||||
"local_ptr_title": "Privatni obrnuti DNS poslužitelji",
|
||||
"local_ptr_desc": "DNS poslužitelji koje AdGuard Home koristi za lokalne PTR upite. Ti se poslužitelji koriste za razrješavanje naziva glavnog računala klijenata s privatnim IP adresama, na primjer \"192.168.12.34\", koristeći obrnuti DNS. Ako nije postavljeno, AdGuard Home koristi adrese zadanih DNS razrješivača vašeg OS-a, osim za adrese samog AdGuard Homea.",
|
||||
"local_ptr_desc": "DNS poslužitelji koje koristi AdGuard Home za privatne PTR, SOA i NS zahtjeve. Zahtjev se smatra privatnim ako traži ARPA domenu koja sadrži podmrežu unutar privatnih IP raspona (kao što je \"192.168.12.34\") i dolazi od klijenta s privatnom IP adresom. Ako nije postavljeno, koristit će se zadani DNS rezolveri vašeg OS-a, osim za AdGuard Home IP adrese.",
|
||||
"local_ptr_default_resolver": "Prema zadanim postavkama AdGuard Home koristi sljedeće obrnute DNS razrješivače: {{ip}}.",
|
||||
"local_ptr_no_default_resolver": "AdGuard Home nije mogao odrediti prikladne privatne obrnute DNS razrješivače za ovaj sustav.",
|
||||
"local_ptr_placeholder": "Unesite jednu adresu poslužitelja po retku",
|
||||
"resolve_clients_title": "Omogući obrnuto rješavanje IP adresa klijenata",
|
||||
"resolve_clients_desc": "Obrnuto razriješite IP adrese klijenata u nazive glavnih računala slanjem PTR upita odgovarajućim razrješivačima (privatni DNS poslužitelji za lokalne klijente, uzvodni poslužitelji za klijente s javnim IP adresama).",
|
||||
"use_private_ptr_resolvers_title": "Koristi privatne reverzne DNS razrješivače",
|
||||
"use_private_ptr_resolvers_desc": "Izvršite obrnuta DNS traženja za lokalno poslužene adrese pomoću ovih uzlaznih poslužitelja. Ako je onemogućen, AdGuard Home odgovara S NXDOMAIN-om na sve takve PTR zahtjeve osim za klijente poznate iz DHCP-a, /etc/hosts i tako dalje.",
|
||||
"use_private_ptr_resolvers_desc": "Razriješi PTR, SOA i NS zahtjeve za ARPA domene koje sadrže privatne IP adrese putem privatnih uzvodnih poslužitelja, DHCP-a, /etc/hostova itd. Ako je onemogućeno, AdGuard Home će na sve takve zahtjeve odgovoriti s NXDOMAIN.",
|
||||
"check_dhcp_servers": "Provjera DHCP poslužitelja",
|
||||
"save_config": "Spremi konfiguraciju",
|
||||
"enabled_dhcp": "DHCP poslužitelj je omogućen",
|
||||
@@ -425,6 +425,9 @@
|
||||
"encryption_hostnames": "Nazivi računala",
|
||||
"encryption_reset": "Jeste li sigurni da želite poništiti postavke šifriranja?",
|
||||
"encryption_warning": "Upozorenje",
|
||||
"encryption_plain_dns_enable": "Omogući obični DNS",
|
||||
"encryption_plain_dns_desc": "Obični DNS je omogućen prema zadanim postavkama. Možete ga onemogućiti kako biste prisilili sve uređaje da koriste šifrirani DNS. Da biste to učinili, morate omogućiti barem jedan kriptirani DNS protokol",
|
||||
"encryption_plain_dns_error": "Da biste onemogućili obični DNS, omogućite barem jedan kriptirani DNS protokol",
|
||||
"topline_expiring_certificate": "Vaš SSL certifikat uskoro ističe. Ažurirajte <0>Postavke šifriranja</0>.",
|
||||
"topline_expired_certificate": "Vaš SSL certifikat je istekao. Ažurirajte <0>Postavke šifriranja</0>.",
|
||||
"form_error_port_range": "Unesite broj porta od 80 do 65536",
|
||||
@@ -675,7 +678,7 @@
|
||||
"use_saved_key": "Korištenje prethodno spremljenog ključa",
|
||||
"parental_control": "Roditeljska zaštita",
|
||||
"safe_browsing": "Sigurno surfanje",
|
||||
"served_from_cache": "{{value}} <i>(dohvaćeno iz predmemorije)</i>",
|
||||
"served_from_cache_label": "Posluženo iz predmemorije",
|
||||
"form_error_password_length": "Lozinka mora sadržavati od {{min}} do {{max}} znakova",
|
||||
"anonymizer_notification": "<0>Napomena:</0>IP anonimizacija je omogućena. Možete ju onemogućiti u <1>općim postavkama</1>.",
|
||||
"confirm_dns_cache_clear": "Jeste li sigurni da želite očistiti DNS predmemoriju?",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"upstream_parallel": "Utilizza richieste parallele per accelerare la risoluzione interrogando simultaneamente tutti i server upstream.",
|
||||
"parallel_requests": "Richieste parallele",
|
||||
"load_balancing": "Bilanciamento del carico",
|
||||
"load_balancing_desc": "Interroga un server upstream per volta. AdGuard Home utilizzerà un algoritmo casuale ponderato per la selezione del server, in maniera tale da scegliere spesso il più veloce.",
|
||||
"load_balancing_desc": "Esegui una query su un server upstream alla volta. AdGuard Home utilizza un algoritmo casuale ponderato per selezionare i server con il minor numero di ricerche fallite e il tempo medio di ricerca più basso.",
|
||||
"bootstrap_dns": "Server DNS bootstrap",
|
||||
"bootstrap_dns_desc": "Indirizzi IP dei server DNS utilizzati per risolvere gli indirizzi IP dei resolver DoH/DoT specificati come upstream. I commenti non sono ammessi.",
|
||||
"fallback_dns_title": "Server DNS di fallback",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"upstream_parallel": "並列リクエストを使用する(同時にすべてのアップストリームサーバーに処理要求することで解決スピードが向上)",
|
||||
"parallel_requests": "並列リクエスト",
|
||||
"load_balancing": "ロードバランシング",
|
||||
"load_balancing_desc": "一度に1つのアップストリームサーバに処理要求します。 AdGuard Homeは、重み付きランダムアルゴリズム(weighted random algorithm)を使用してサーバを選択するため、最速のサーバがより頻繁に使用されます。",
|
||||
"load_balancing_desc": "一度に1つのアップストリームサーバーをクエリします。AdGuard Home は、重み付き乱択アルゴリズムを使用して、ルックアップに失敗した回数が最も少なく、平均ルックアップ時間が最も短いサーバーを選択します。",
|
||||
"bootstrap_dns": "ブートストラップDNSサーバ",
|
||||
"bootstrap_dns_desc": "アップストリームとして指定したDoH/DoTリゾルバのIPアドレスを解決するために使用されるDNSサーバーのIPアドレスです。(コメントは許可されていません)",
|
||||
"fallback_dns_title": "フォールバックDNSサーバー",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"upstream_parallel": "쿼리 처리 속도를 높이려면 모든 업스트림 서버에서 동시에 병렬 쿼리를 사용해주세요.",
|
||||
"parallel_requests": "병렬 처리 요청",
|
||||
"load_balancing": "로드 밸런싱",
|
||||
"load_balancing_desc": "한 번에 하나의 서버씩 질의합니다. AdGuard Home은 가중 랜덤 알고리즘를 사용해서 가장 빠른 서버가 자주 사용되도록 서버를 선택합니다.",
|
||||
"load_balancing_desc": "한 번에 하나의 업스트림 서버를 쿼리합니다. AdGuard Home은 가중 무작위 알고리즘을 사용하여 조회 실패 횟수가 가장 적고 평균 조회 시간이 가장 짧은 서버를 선택합니다.",
|
||||
"bootstrap_dns": "부트스트랩 DNS 서버",
|
||||
"bootstrap_dns_desc": "업스트림으로 지정한 DoH/DoT 리졸버의 IP 주소를 확인하는 데 사용되는 DNS 서버의 IP 주소입니다. 주석은 허용되지 않습니다.",
|
||||
"fallback_dns_title": "폴백 DNS 서버",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"upstream_parallel": "Parallelle verzoeken gebruiken om te versnellen door gelijktijdig verzoeken te sturen naar alle upstream servers.",
|
||||
"parallel_requests": "Parallelle verzoeken",
|
||||
"load_balancing": "Volume balanceren",
|
||||
"load_balancing_desc": "Eén server per keer bevragen. AdGuard Home gebruikt hiervoor een gewogen willekeurig algoritme om de server te kiezen zodat de snelste server meer zal gebruikt worden.",
|
||||
"load_balancing_desc": "Voer zoekopdrachten uit op één upstream-server tegelijk. AdGuard Home gebruikt een gewogen willekeurig algoritme om servers te selecteren met het laagste aantal mislukte zoekopdrachten en de laagste gemiddelde opzoektijd.",
|
||||
"bootstrap_dns": "Bootstrap DNS-servers",
|
||||
"bootstrap_dns_desc": "IP-adressen van DNS-servers die worden gebruikt om IP-adressen om te zetten van de DoH/DoT-resolvers die je opgeeft als upstreams. Opmerkingen zijn niet toegestaan.",
|
||||
"fallback_dns_title": "Back-up DNS-servers",
|
||||
@@ -495,7 +495,7 @@
|
||||
"setup_dns_privacy_2": "<0>DNS-via-HTTPS:</0> Gebruik <1>{{address}}</1> string.",
|
||||
"setup_dns_privacy_3": "<0>Hou er rekening mee dat het beveiligde DNS protocol alleen beschikbaar is voor Android 9. U moet dus extra software installeren voor andere besturingssystemen.</0><0>Hier is een lijst van te gebruiken software.</0>",
|
||||
"setup_dns_privacy_4": "Op een iOS 14 of macOS Big Sur apparaat kan je een speciaal '.mobileconfig'-bestand downloaden dat <highlight>DNS-via-HTTPS</highlight> of <highlight>DNS-via-TLS</highlight> servers aan de DNS-instellingen toevoegt.",
|
||||
"setup_dns_privacy_android_1": "Android 9 ondersteunt native DNS-via-TLS. Om het te configureren, ga naar Instellingen → Netwerk & internet → Geavanceerd → Privé DNS en voer daar je domeinnaam in.",
|
||||
"setup_dns_privacy_android_1": "Android 9 ondersteunt native DNS-via-TLS. Om het te configureren, ga naar Instellingen → Netwerk & internet → Geavanceerd → Privé-DNS en voer daar je domeinnaam in.",
|
||||
"setup_dns_privacy_android_2": "<0>AdGuard voor Android</0>ondersteunt<1>DNS-via-HTTPS </1>en<1>DNS-via-TLS</1>.",
|
||||
"setup_dns_privacy_android_3": "<0> Intra </0> voegt <1> DNS-via-HTTPS</1> ondersteuning toe aan Android.",
|
||||
"setup_dns_privacy_ios_1": "<0>DNSCloak</0> ondersteunt <1> DNS-via-HTTPS </1>, maar om het te configureren op jouw eigen server moet er een <2> DNS-stempel </2> gegenereerd worden.",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"upstream_parallel": "Usar consultas paralelas para acelerar a resolução consultando simultaneamente todos os servidores DNS primário",
|
||||
"parallel_requests": "Solicitações paralelas",
|
||||
"load_balancing": "Balanceamento de carga",
|
||||
"load_balancing_desc": "Consulte um servidor DNS primário por vez. O AdGuard Home usa seu algoritmo aleatório ponderado para escolher o servidor para que o servidor mais rápido seja usado com mais frequência.",
|
||||
"load_balancing_desc": "Consulte um servidor upstream por vez. O AdGuard Home usa um algoritmo aleatório ponderado para selecionar servidores com o menor número de falhas e o menor tempo médio de consulta.",
|
||||
"bootstrap_dns": "Servidores DNS de inicialização",
|
||||
"bootstrap_dns_desc": "Endereços IP de servidores DNS usados para resolver endereços IP dos resolvedores DoH/DoT que você especifica como upstreams. Comentários não são permitidos.",
|
||||
"fallback_dns_title": "Servidores DNS Fallback",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"upstream_parallel": "Usar consultas paralelas para acelerar a resolução consultando simultaneamente todos os servidores DNS",
|
||||
"parallel_requests": "Solicitações paralelas",
|
||||
"load_balancing": "Balanceamento de carga",
|
||||
"load_balancing_desc": "Consulte um servidor DNS primário por vez. O AdGuard Home usa seu algoritmo aleatório ponderado para escolher o servidor para que o servidor mais rápido seja usado com mais frequência.",
|
||||
"load_balancing_desc": "Consulta um servidor a montante de cada vez. O AdGuard Home usa um algoritmo aleatório ponderado para selecionar servidores com o menor número de pesquisas com falha e o menor tempo médio de pesquisa.",
|
||||
"bootstrap_dns": "Servidores DNS de arranque",
|
||||
"bootstrap_dns_desc": "Endereços IP de servidores DNS usados para resolver endereços IP dos resolvedores DoH/DoT que você especifica como upstreams. Comentários não são permitidos.",
|
||||
"fallback_dns_title": "Servidores DNS de fallback",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"upstream_parallel": "Использовать параллельные запросы ко всем серверам одновременно для ускорения обработки запроса.",
|
||||
"parallel_requests": "Параллельные запросы",
|
||||
"load_balancing": "Распределение нагрузки\n",
|
||||
"load_balancing_desc": "Запрашивать по одному серверу за раз. AdGuard Home использует алгоритм взвешенного случайного выбора сервера, так что самый быстрый сервер используется чаще.",
|
||||
"load_balancing_desc": "Запрашивайте по одному серверу за раз. AdGuard Home использует алгоритм случайной выборки с учётом веса для выбора серверов с наименьшим количеством неудачных запросов и наименьшим средним временем выполнения запроса.",
|
||||
"bootstrap_dns": "Bootstrap DNS-серверы",
|
||||
"bootstrap_dns_desc": "IP-адреса DNS-серверов, используемых для поиска IP-адресов DoH/DoT upstream-серверов, которые вы указали. Комментарии не допускаются.",
|
||||
"fallback_dns_title": "Резервные DNS-серверы",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"upstream_parallel": "Používať paralelné dopyty na zrýchlenie súčasným dopytovaním všetkých upstream serverov súčasne.",
|
||||
"parallel_requests": "Paralelné dopyty",
|
||||
"load_balancing": "Vyrovnávanie záťaže",
|
||||
"load_balancing_desc": "Dopytovať len jeden server v danom čase. AdGuard Home použije na výber servera vážený náhodný algoritmus, aby sa najrýchlejší server používal častejšie.",
|
||||
"load_balancing_desc": "Dopytuje sa súčasne len jeden upstream server. AdGuard Home používa vážený náhodný algoritmus na výber serverov s najnižším počtom neúspešných vyhľadávaní a najnižším priemerným časom vyhľadávania.",
|
||||
"bootstrap_dns": "Bootstrap DNS servery",
|
||||
"bootstrap_dns_desc": "IP adresy serverov DNS používaných na rozlíšenie IP adries prekladačov DoH/DoT, ktoré zadáte ako upstream. Komentáre nie sú povolené.",
|
||||
"fallback_dns_title": "Záložné servery DNS",
|
||||
@@ -89,7 +89,7 @@
|
||||
"form_enter_hostname": "Zadajte meno hostiteľa",
|
||||
"error_details": "Podrobnosti chyby",
|
||||
"response_details": "Podrobnosti odpovede",
|
||||
"request_details": "Podrobnosti požiadavky",
|
||||
"request_details": "Podrobnosti dopytu",
|
||||
"client_details": "Podrobnosti klienta",
|
||||
"details": "Podrobnosti",
|
||||
"back": "Naspäť",
|
||||
@@ -308,7 +308,7 @@
|
||||
"form_enter_rate_limit": "Zadajte rýchlostný limit",
|
||||
"rate_limit": "Rýchlostný limit",
|
||||
"edns_enable": "Povoliť klientsku podsiete EDNS",
|
||||
"edns_cs_desc": "Pridáva možnosť EDNS Client Subnet (ECS) do upstream požiadaviek a zapíše hodnoty odoslané klientmi do denníka dopytov.",
|
||||
"edns_cs_desc": "Pridáva možnosť EDNS Client Subnet (ECS) do upstream dopytov a zapíše hodnoty odoslané klientami do denníka dopytov.",
|
||||
"edns_use_custom_ip": "Použiť vlastnú IP adresu pre EDNS",
|
||||
"edns_use_custom_ip_desc": "Povoliť používanie vlastnej IP adresy pre EDNS",
|
||||
"rate_limit_desc": "Počet požiadaviek za sekundu, ktoré môže jeden klient vykonať. Nastavenie na hodnotu 0 znamená neobmedzene.",
|
||||
@@ -480,9 +480,9 @@
|
||||
"access_title": "Nastavenia prístupu",
|
||||
"access_desc": "Tu môžete konfigurovať pravidlá prístupu pre server DNS AdGuard Home.",
|
||||
"access_allowed_title": "Povolení klienti",
|
||||
"access_allowed_desc": "Zoznam CIDR, IP adries alebo <a>ClientID</a>. Ak tento zoznam obsahuje položky, AdGuard Home bude akceptovať požiadavky iba od týchto klientov.",
|
||||
"access_allowed_desc": "Zoznam CIDR, IP adries alebo <a>ClientID</a>. Ak tento zoznam obsahuje položky, AdGuard Home bude akceptovať dopyty iba od týchto klientov.",
|
||||
"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ší požiadavky 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_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_settings_saved": "Nastavenia prístupu úspešne uložené",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"upstream_parallel": "Tüm üst sunucuları eş zamanlı sorgulayarak çözümlemeyi hızlandırmak için paralel sorgular kullanın.",
|
||||
"parallel_requests": "Paralel istekler",
|
||||
"load_balancing": "Yük dengeleme",
|
||||
"load_balancing_desc": "Her seferde bir üst sunucuyu sorgulayın. AdGuard Home, sunucuyu seçmek için ağırlıklı rastgele algoritmasını kullanır, böylece en hızlı sunucu daha sık kullanılır.",
|
||||
"load_balancing_desc": "Aynı anda bir üst kaynak sunucusunu sorgulayın. AdGuard Home, en düşük başarısız arama sayısına ve en düşük ortalama arama süresine sahip sunucuları seçmek için ağırlıklı rastgele bir algoritma kullanır.",
|
||||
"bootstrap_dns": "DNS Önyükleme sunucuları",
|
||||
"bootstrap_dns_desc": "Üst kaynak olarak belirttiğiniz DoH/DoT çözümleyicilerin IP adreslerini çözümlemek için kullanılan DNS sunucularının IP adresleri. Yorumlara izin verilmez.",
|
||||
"fallback_dns_title": "Yedek DNS sunucuları",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"upstream_parallel": "使用并行请求以同时查询所有上游服务器来加快解析速度。",
|
||||
"parallel_requests": "并行请求",
|
||||
"load_balancing": "负载均衡",
|
||||
"load_balancing_desc": "一次查询一台服务器。AdGuard Home 将使用加权随机算法来选择服务器,以便更常使用最快的服务器。",
|
||||
"load_balancing_desc": "一次查询一台服务器。AdGuard Home 使用加权随机算法来选择具有最少失败查找和最低平均查找时间的服务器。",
|
||||
"bootstrap_dns": "Bootstrap DNS 服务器",
|
||||
"bootstrap_dns_desc": "DNS 服务器的 IP 地址,用于解析指定为上游的 DoH/DoT 解析器的 IP 地址。不允许添加注释。",
|
||||
"fallback_dns_title": "后备 DNS 服务器",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"upstream_parallel": "透過同時地查詢所有上游的伺服器,使用並行的查詢以加速解析。",
|
||||
"parallel_requests": "並行的請求",
|
||||
"load_balancing": "負載平衡",
|
||||
"load_balancing_desc": "每次查詢一個上游伺服器。AdGuard Home 使用它的加權隨機的演算法來選擇伺服器,以便最快的伺服器被更常使用。",
|
||||
"load_balancing_desc": "一次查詢一台伺服器。AdGuard Home 使用加權隨機演算法來選擇具有最少失敗查詢和最低平均查詢時間的伺服器。",
|
||||
"bootstrap_dns": "自我啟動(Bootstrap)DNS 伺服器",
|
||||
"bootstrap_dns_desc": "DNS 伺服器的 IP 位址,用於解析您指定為上游伺服器的 DoH/DoT 解析器的 IP 位址。不允許註釋。",
|
||||
"fallback_dns_title": "應變 DNS 伺服器",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { Field, type InjectedFormProps, reduxForm } from 'redux-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
@@ -104,14 +104,13 @@ const FORM_NAMES = {
|
||||
response_status: 'response_status',
|
||||
};
|
||||
|
||||
interface FiltersFormProps {
|
||||
type FiltersFormProps = {
|
||||
className?: string;
|
||||
responseStatusClass?: string;
|
||||
change: (...args: unknown[]) => unknown;
|
||||
setIsLoading?: (...args: unknown[]) => unknown;
|
||||
}
|
||||
setIsLoading: (...args: unknown[]) => unknown;
|
||||
};
|
||||
|
||||
const Form = (props: FiltersFormProps) => {
|
||||
const Form = (props: FiltersFormProps & InjectedFormProps) => {
|
||||
const { className = '', responseStatusClass, setIsLoading, change } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
@@ -142,7 +141,6 @@ const Form = (props: FiltersFormProps) => {
|
||||
|
||||
const onInputClear = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
change(FORM_NAMES.search, DEFAULT_LOGS_FILTER[FORM_NAMES.search]);
|
||||
setIsLoading(false);
|
||||
};
|
||||
@@ -195,7 +193,7 @@ const Form = (props: FiltersFormProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default reduxForm({
|
||||
export const FiltersForm = reduxForm<Record<string, any>, FiltersFormProps>({
|
||||
form: FORM_NAME.LOGS_FILTER,
|
||||
enableReinitialize: true,
|
||||
})(Form);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import Form from './Form';
|
||||
import { FiltersForm } from './Form';
|
||||
import { refreshFilteredLogs } from '../../../actions/queryLogs';
|
||||
import { addSuccessToast } from '../../../actions/toasts';
|
||||
|
||||
@@ -38,12 +38,7 @@ const Filters = ({ filter, setIsLoading }: FiltersProps) => {
|
||||
</svg>
|
||||
</button>
|
||||
</h1>
|
||||
|
||||
<Form
|
||||
// responseStatusClass="d-sm-block"
|
||||
// setIsLoading={setIsLoading}
|
||||
initialValues={filter}
|
||||
/>
|
||||
<FiltersForm responseStatusClass="d-sm-block" setIsLoading={setIsLoading} initialValues={filter} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ class Encryption extends Component<EncryptionProps> {
|
||||
handleFormChange = debounce((values) => {
|
||||
const submitValues = this.getSubmitValues(values);
|
||||
|
||||
if (submitValues.enabled || submitValues.serve_plain_dns) {
|
||||
if (submitValues.enabled) {
|
||||
this.props.validateTlsConfig(submitValues);
|
||||
}
|
||||
}, DEBOUNCE_TIMEOUT);
|
||||
|
||||
@@ -12,12 +12,11 @@ const Version = () => {
|
||||
const dashboard = useSelector((state: RootState) => state.dashboard, shallowEqual);
|
||||
const install = useSelector((state: RootState) => state.install, shallowEqual);
|
||||
|
||||
if (!dashboard || !install) {
|
||||
if (!dashboard && !install) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { dnsVersion, processingVersion, checkUpdateFlag } = dashboard;
|
||||
const version = dnsVersion || install?.dnsVersion;
|
||||
const version = dashboard?.dnsVersion || install?.dnsVersion;
|
||||
|
||||
const onClick = () => {
|
||||
dispatch(getVersion(true));
|
||||
@@ -35,12 +34,12 @@ const Version = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{checkUpdateFlag && (
|
||||
{dashboard?.checkUpdateFlag && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-icon-sm btn-outline-primary btn-sm ml-2"
|
||||
onClick={onClick}
|
||||
disabled={processingVersion}
|
||||
disabled={dashboard?.processingVersion}
|
||||
title={t('check_updates_now')}>
|
||||
<svg className="icons icon12">
|
||||
<use xlinkHref="#refresh" />
|
||||
|
||||
@@ -295,7 +295,7 @@ export default {
|
||||
"phishing_army": {
|
||||
"name": "Phishing Army",
|
||||
"categoryId": "security",
|
||||
"homepage": "https://gitlab.com/malware-filter/phishing-filter",
|
||||
"homepage": "https://phishing.army/",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_18.txt"
|
||||
},
|
||||
"scam_blocklist_by_durablenapkin": {
|
||||
|
||||
@@ -66,7 +66,7 @@ export const renderFormattedClientCell = (value: any, info: any, isDetailed = fa
|
||||
}
|
||||
|
||||
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>
|
||||
{whoisContainer}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"timeUpdated": "2024-03-01T00:10:14.031Z",
|
||||
"timeUpdated": "2024-07-01T00:11:48.891Z",
|
||||
"categories": {
|
||||
"0": "audio_video_player",
|
||||
"1": "comments",
|
||||
@@ -1182,6 +1182,13 @@
|
||||
"url": "https://www.admitad.com/en/#",
|
||||
"companyId": "admitad"
|
||||
},
|
||||
"admixer": {
|
||||
"name": "Admixer",
|
||||
"categoryId": 4,
|
||||
"url": "https://admixer.com/",
|
||||
"companyId": "admixer",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"admixer.net": {
|
||||
"name": "Admixer",
|
||||
"categoryId": 4,
|
||||
@@ -1473,6 +1480,13 @@
|
||||
"url": "http://www.adplus.co.id/",
|
||||
"companyId": "adplus"
|
||||
},
|
||||
"adprofex": {
|
||||
"name": "AdProfex",
|
||||
"categoryId": 4,
|
||||
"url": "https://adprofex.com/",
|
||||
"companyId": "adprofex",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"adprofy": {
|
||||
"name": "AdProfy",
|
||||
"categoryId": 4,
|
||||
@@ -2019,6 +2033,13 @@
|
||||
"url": "https://hybridtheory.com/",
|
||||
"companyId": "affectv"
|
||||
},
|
||||
"affilbox": {
|
||||
"name": "Affilbox",
|
||||
"categoryId": 4,
|
||||
"url": "https://affilbox.com/",
|
||||
"companyId": "affilbox",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"affiliate-b": {
|
||||
"name": "Affiliate-B",
|
||||
"categoryId": 4,
|
||||
@@ -2373,6 +2394,13 @@
|
||||
"url": "http://www.amadesa.com/",
|
||||
"companyId": "amadesa"
|
||||
},
|
||||
"amap": {
|
||||
"name": "Amap",
|
||||
"categoryId": 2,
|
||||
"url": "https://www.amap.com/",
|
||||
"companyId": "softbank",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"amazon": {
|
||||
"name": "Amazon.com",
|
||||
"categoryId": 8,
|
||||
@@ -4591,6 +4619,13 @@
|
||||
"url": "http://clickaider.com/",
|
||||
"companyId": "clickaider"
|
||||
},
|
||||
"clickaine": {
|
||||
"name": "Clickaine",
|
||||
"categoryId": 4,
|
||||
"url": "https://clickaine.com/",
|
||||
"companyId": "clickaine",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"clickbank": {
|
||||
"name": "ClickBank",
|
||||
"categoryId": 4,
|
||||
@@ -12470,6 +12505,13 @@
|
||||
"url": "http://nolix.ru/",
|
||||
"companyId": "nolix"
|
||||
},
|
||||
"nonli": {
|
||||
"name": "Nonli",
|
||||
"categoryId": 4,
|
||||
"url": "https://www.nonli.com/",
|
||||
"companyId": "nonli",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"nonstop_consulting": {
|
||||
"name": "Resolution Media",
|
||||
"categoryId": 4,
|
||||
@@ -12869,6 +12911,13 @@
|
||||
"url": "http://opensharecount.com/",
|
||||
"companyId": "open_share_count"
|
||||
},
|
||||
"openai": {
|
||||
"name": "OpenAI",
|
||||
"categoryId": 8,
|
||||
"url": "https://openai.com/",
|
||||
"companyId": "openai",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"openload": {
|
||||
"name": "Openload",
|
||||
"categoryId": 9,
|
||||
@@ -20339,6 +20388,7 @@
|
||||
"adguard.org": "adguard",
|
||||
"adtidy.org": "adguard",
|
||||
"agrd.io": "adguard",
|
||||
"agrd.eu": "adguard",
|
||||
"adguard-dns.com": "adguard_dns",
|
||||
"adguard-dns.io": "adguard_dns",
|
||||
"adguard-vpn.com": "adguard_vpn",
|
||||
@@ -20413,7 +20463,8 @@
|
||||
"admicro.vn": "admicro",
|
||||
"vcmedia.vn": "admicro",
|
||||
"admitad.com": "admitad.com",
|
||||
"admixer.net": "admixer.net",
|
||||
"admixer.net": "admixer",
|
||||
"admixer.com": "admixer",
|
||||
"admized.com": "admized",
|
||||
"admo.tv": "admo.tv",
|
||||
"a.admob.com": "admob",
|
||||
@@ -20493,6 +20544,8 @@
|
||||
"advg.jp": "adplan",
|
||||
"c.p-advg.com": "adplan",
|
||||
"adplus.co.id": "adplus",
|
||||
"adprofex.com": "adprofex",
|
||||
"ads2.bid": "adprofex",
|
||||
"adframesrc.com": "adprofy",
|
||||
"adserve.adpulse.ir": "adpulse",
|
||||
"ads.adpv.com": "adpv",
|
||||
@@ -20616,6 +20669,8 @@
|
||||
"affectv.com": "affectv",
|
||||
"go.affec.tv": "affectv",
|
||||
"hybridtheory.com": "affectv",
|
||||
"affilbox.com": "affilbox",
|
||||
"affilbox.cz": "affilbox",
|
||||
"track.affiliate-b.com": "affiliate-b",
|
||||
"affiliate4you.nl": "affiliate4you",
|
||||
"ads.affbuzzads.com": "affiliatebuzz",
|
||||
@@ -20704,6 +20759,7 @@
|
||||
"inputs.alooma.com": "alooma",
|
||||
"arena.altitude-arena.com": "altitude_digital",
|
||||
"amadesa.com": "amadesa",
|
||||
"amap.com": "amap",
|
||||
"amazon.ca": "amazon",
|
||||
"amazon.co.jp": "amazon",
|
||||
"amazon.co.uk": "amazon",
|
||||
@@ -21231,6 +21287,7 @@
|
||||
"clickandchat.com": "click_and_chat",
|
||||
"software.clickback.com": "click_back",
|
||||
"hit.clickaider.com": "clickaider",
|
||||
"clickaine.com": "clickaine",
|
||||
"clickbank.net": "clickbank",
|
||||
"cbproads.com": "clickbank_proads",
|
||||
"adtoll.com": "clickbooth",
|
||||
@@ -22242,6 +22299,7 @@
|
||||
"withgoogle.com": "google",
|
||||
"googleadservices.com": "google_adservices",
|
||||
"google-analytics.com": "google_analytics",
|
||||
"app-analytics-services.com": "google_analytics",
|
||||
"ssl-google-analytics.l.google.com": "google_analytics",
|
||||
"www-googletagmanager.l.google.com": "google_analytics",
|
||||
"appspot.com": "google_appspot",
|
||||
@@ -23396,6 +23454,8 @@
|
||||
"noaa.gov": "noaa.gov",
|
||||
"track.noddus.com": "noddus",
|
||||
"contextbar.ru": "nolix",
|
||||
"nonli.com": "nonli",
|
||||
"non.li": "nonli",
|
||||
"trkme.net": "nonstop_consulting",
|
||||
"noop.style": "noop.style",
|
||||
"nosto.com": "nosto.com",
|
||||
@@ -23473,6 +23533,10 @@
|
||||
"realmedia.com": "open_adstream",
|
||||
"realmediadigital.com": "open_adstream",
|
||||
"opensharecount.com": "open_share_count",
|
||||
"chatgpt.com": "openai",
|
||||
"oaistatic.com": "openai",
|
||||
"oaiusercontent.com": "openai",
|
||||
"openai.com": "openai",
|
||||
"oloadcdn.net": "openload",
|
||||
"openload.co": "openload",
|
||||
"openstat.net": "openstat",
|
||||
@@ -23649,6 +23713,7 @@
|
||||
"popads.net": "popads",
|
||||
"popadscdn.net": "popads",
|
||||
"popcash.net": "popcash",
|
||||
"popcashjs.b-cdn.net": "popcash",
|
||||
"desv383oqqc0.cloudfront.net": "popcorn_metrics",
|
||||
"popin.cc": "popin.cc",
|
||||
"cdn.popmyads.com": "popmyads",
|
||||
@@ -24356,6 +24421,10 @@
|
||||
"spoteffects.net": "spoteffect",
|
||||
"scdn.co": "spotify",
|
||||
"spotify.com": "spotify",
|
||||
"pscdn.co": "spotify",
|
||||
"spotifycdn.com": "spotify",
|
||||
"spotifycdn.net": "spotify",
|
||||
"spotilocal.com": "spotify",
|
||||
"embed.spotify.com": "spotify_embed",
|
||||
"spotscenered.info": "spotscenered.info",
|
||||
"spotx.tv": "spotxchange",
|
||||
@@ -24675,6 +24744,8 @@
|
||||
"t.co": "twitter",
|
||||
"twimg.com": "twitter",
|
||||
"twitter.com": "twitter",
|
||||
"twttr.com": "twitter",
|
||||
"x.com": "twitter",
|
||||
"ads-twitter.com": "twitter_ads",
|
||||
"analytics.twitter.com": "twitter_analytics",
|
||||
"tellapart.com": "twitter_for_business",
|
||||
|
||||
26
go.mod
26
go.mod
@@ -1,11 +1,11 @@
|
||||
module github.com/AdguardTeam/AdGuardHome
|
||||
|
||||
go 1.22.4
|
||||
go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/AdguardTeam/dnsproxy v0.71.2
|
||||
github.com/AdguardTeam/golibs v0.23.2
|
||||
github.com/AdguardTeam/urlfilter v0.18.0
|
||||
github.com/AdguardTeam/dnsproxy v0.73.0
|
||||
github.com/AdguardTeam/golibs v0.26.0
|
||||
github.com/AdguardTeam/urlfilter v0.19.0
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
github.com/ameshkov/dnscrypt/v2 v2.3.0
|
||||
github.com/bluele/gcache v0.0.2
|
||||
@@ -27,15 +27,15 @@ require (
|
||||
// TODO(a.garipov): This package is deprecated; find a new one or use our
|
||||
// own code for that. Perhaps, use gopacket.
|
||||
github.com/mdlayher/raw v0.1.0
|
||||
github.com/miekg/dns v1.1.59
|
||||
github.com/miekg/dns v1.1.61
|
||||
github.com/quic-go/quic-go v0.44.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/ti-mo/netfilter v0.5.2
|
||||
go.etcd.io/bbolt v1.3.10
|
||||
golang.org/x/crypto v0.23.0
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
||||
golang.org/x/net v0.25.0
|
||||
golang.org/x/sys v0.20.0
|
||||
golang.org/x/crypto v0.26.0
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa
|
||||
golang.org/x/net v0.28.0
|
||||
golang.org/x/sys v0.24.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
howett.net/plist v1.0.1
|
||||
@@ -58,9 +58,9 @@ require (
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
golang.org/x/tools v0.21.0 // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
gonum.org/v1/gonum v0.15.0 // indirect
|
||||
)
|
||||
|
||||
64
go.sum
64
go.sum
@@ -1,9 +1,9 @@
|
||||
github.com/AdguardTeam/dnsproxy v0.71.2 h1:dFG2wga4GDdj1eI3rU2wqjQ6QGQm9MjLRb5ZzyH3Vgg=
|
||||
github.com/AdguardTeam/dnsproxy v0.71.2/go.mod h1:huI5zyWhlimHBhg0jt2CMinXzsEHymI+WlvxIfmfEGA=
|
||||
github.com/AdguardTeam/golibs v0.23.2 h1:rMjYantwtQ39e8G4zBQ6ZLlm4s3XH30Bc9VxhoOHwao=
|
||||
github.com/AdguardTeam/golibs v0.23.2/go.mod h1:o9i55Sx6v7qogRQeqaBfmLbC/pZqeMBWi015U5PTDY0=
|
||||
github.com/AdguardTeam/urlfilter v0.18.0 h1:ZZzwODC/ADpjJSODxySrrUnt/fvOCfGFaCW6j+wsGfQ=
|
||||
github.com/AdguardTeam/urlfilter v0.18.0/go.mod h1:IXxBwedLiZA2viyHkaFxY/8mjub0li2PXRg8a3d9Z1s=
|
||||
github.com/AdguardTeam/dnsproxy v0.73.0 h1:E1fxzosMqExZH8h7OJnKXLxyktcAFRJapLF4+nKULms=
|
||||
github.com/AdguardTeam/dnsproxy v0.73.0/go.mod h1:ZcvmyQY2EiX5B0yCTkiYTgtm+1lBWA0lajbEI9dOhW4=
|
||||
github.com/AdguardTeam/golibs v0.26.0 h1:uLL0XggEjB+87lL1tPpEAQNoKAlHDq5AyBUVWEgf63E=
|
||||
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/go.mod h1:+N54ZvxqXYLnXuvpaUhK2exDQW+djZBRSb6F6j0rkBY=
|
||||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
|
||||
@@ -78,8 +78,8 @@ github.com/mdlayher/raw v0.1.0/go.mod h1:yXnxvs6c0XoF/aK52/H5PjsVHmWBCFfZUfoh/Y5
|
||||
github.com/mdlayher/socket v0.2.1/go.mod h1:QLlNPkFR88mRUNQIzRBMfXxwKal8H7u1h3bL1CV+f0E=
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
|
||||
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
|
||||
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
|
||||
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/onsi/ginkgo/v2 v2.17.3 h1:oJcvKpIb7/8uLpDDtnQuf18xVnwKp8DTD7DQ6gTd/MU=
|
||||
@@ -101,8 +101,8 @@ github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/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.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4=
|
||||
github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -114,40 +114,40 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
|
||||
github.com/ti-mo/netfilter v0.2.0/go.mod h1:8GbBGsY/8fxtyIdfwy29JiluNcPK4K7wIT+x42ipqUU=
|
||||
github.com/ti-mo/netfilter v0.5.2 h1:CTjOwFuNNeZ9QPdRXt1MZFLFUf84cKtiQutNauHWd40=
|
||||
github.com/ti-mo/netfilter v0.5.2/go.mod h1:Btx3AtFiOVdHReTDmP9AE+hlkOcvIy403u7BXXbWZKo=
|
||||
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
|
||||
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
|
||||
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
|
||||
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
||||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
|
||||
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/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
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-20190312061237-fead79001313/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-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.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
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/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.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
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/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-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
|
||||
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,16 @@
|
||||
package aghhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||
"github.com/AdguardTeam/golibs/httphdr"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
)
|
||||
|
||||
// HTTP scheme constants.
|
||||
@@ -31,12 +34,39 @@ func OK(w http.ResponseWriter) {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
text := fmt.Sprintf(format, args...)
|
||||
log.Error("%s %s %s: %s", r.Method, r.Host, r.URL, text)
|
||||
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
|
||||
// be used as the value of the Server HTTP header.
|
||||
func UserAgent() (ua string) {
|
||||
|
||||
@@ -161,7 +161,8 @@ func (hc *HostsContainer) handleEvents() {
|
||||
|
||||
defer close(hc.updates)
|
||||
|
||||
ok, eventsCh := true, hc.watcher.Events()
|
||||
eventsCh := hc.watcher.Events()
|
||||
ok := eventsCh != nil
|
||||
for ok {
|
||||
select {
|
||||
case _, ok = <-eventsCh:
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
)
|
||||
|
||||
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
|
||||
// configurations from man rc.conf(5).
|
||||
fields := strings.Fields(line[cfgLeft:cfgRight])
|
||||
if len(fields) >= 2 &&
|
||||
strings.EqualFold(fields[0], "inet") &&
|
||||
net.ParseIP(fields[1]) != nil {
|
||||
switch {
|
||||
case
|
||||
len(fields) < 2,
|
||||
!strings.EqualFold(fields[0], "inet"),
|
||||
!netutil.IsValidIPString(fields[1]):
|
||||
continue
|
||||
default:
|
||||
return nil, false, s.Err()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
)
|
||||
|
||||
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() {
|
||||
line := strings.TrimSpace(s.Text())
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,3 +160,34 @@ func (w *osWatcher) handleErrors() {
|
||||
log.Error("%s: %s", osWatcherPref, err)
|
||||
}
|
||||
}
|
||||
|
||||
// EmptyFSWatcher is a no-op implementation of the [FSWatcher] interface. It
|
||||
// may be used on systems not supporting filesystem events.
|
||||
type EmptyFSWatcher struct{}
|
||||
|
||||
// type check
|
||||
var _ FSWatcher = EmptyFSWatcher{}
|
||||
|
||||
// Start implements the [FSWatcher] interface for EmptyFSWatcher. It always
|
||||
// returns nil error.
|
||||
func (EmptyFSWatcher) Start() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close implements the [FSWatcher] interface for EmptyFSWatcher. It always
|
||||
// returns nil error.
|
||||
func (EmptyFSWatcher) Close() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Events implements the [FSWatcher] interface for EmptyFSWatcher. It always
|
||||
// returns nil channel.
|
||||
func (EmptyFSWatcher) Events() (e <-chan event) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add implements the [FSWatcher] interface for EmptyFSWatcher. It always
|
||||
// returns nil error.
|
||||
func (EmptyFSWatcher) Add(_ string) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -19,25 +19,9 @@ import (
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// UnsupportedError is returned by functions and methods when a particular
|
||||
// operation Op cannot be performed on the current OS.
|
||||
type UnsupportedError struct {
|
||||
Op string
|
||||
OS string
|
||||
}
|
||||
|
||||
// Error implements the error interface for *UnsupportedError.
|
||||
func (err *UnsupportedError) Error() (msg string) {
|
||||
return fmt.Sprintf("%s is unsupported on %s", err.Op, err.OS)
|
||||
}
|
||||
|
||||
// Unsupported is a helper that returns an *UnsupportedError with the Op field
|
||||
// set to op and the OS field set to the current OS.
|
||||
// Unsupported is a helper that returns a wrapped [errors.ErrUnsupported].
|
||||
func Unsupported(op string) (err error) {
|
||||
return &UnsupportedError{
|
||||
Op: op,
|
||||
OS: runtime.GOOS,
|
||||
}
|
||||
return fmt.Errorf("%s: not supported on %s: %w", op, runtime.GOOS, errors.ErrUnsupported)
|
||||
}
|
||||
|
||||
// SetRlimit sets user-specified limit of how many fd's we can use.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package aghos
|
||||
|
||||
// ConfigureSyslog reroutes standard logger output to syslog.
|
||||
func ConfigureSyslog(serviceName string) error {
|
||||
func ConfigureSyslog(serviceName string) (err error) {
|
||||
return configureSyslog(serviceName)
|
||||
}
|
||||
|
||||
@@ -8,11 +8,15 @@ import (
|
||||
"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)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
log.SetOutput(w)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -19,23 +19,30 @@ func (w *eventLogWriter) Write(b []byte) (int, error) {
|
||||
return len(b), w.el.Info(1, string(b))
|
||||
}
|
||||
|
||||
func configureSyslog(serviceName string) error {
|
||||
// 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
|
||||
// configureSyslog sets standard log output to event log.
|
||||
func configureSyslog(serviceName string) (err error) {
|
||||
// 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
|
||||
// ERROR_ACCESS_DENIED so that we can log without administrative permissions
|
||||
// for pre-existing eventlog sources.
|
||||
if err := eventlog.InstallAsEventCreate(serviceName, eventlog.Info|eventlog.Warning|eventlog.Error); err != nil {
|
||||
if !strings.Contains(err.Error(), "registry key already exists") && err != windows.ERROR_ACCESS_DENIED {
|
||||
return err
|
||||
}
|
||||
err = eventlog.InstallAsEventCreate(serviceName, eventlog.Info|eventlog.Warning|eventlog.Error)
|
||||
if err != nil &&
|
||||
!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)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
log.SetOutput(&eventLogWriter{el: el})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
)
|
||||
|
||||
// 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
|
||||
// its subjectAltNames.
|
||||
func CertificateHasIP(cert *x509.Certificate) (ok bool) {
|
||||
if len(cert.IPAddresses) > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, name := range cert.DNSNames {
|
||||
if _, err := netip.ParseAddr(name); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return len(cert.IPAddresses) > 0 || slices.ContainsFunc(cert.DNSNames, netutil.IsValidIPString)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
@@ -12,7 +13,7 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"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/osutil"
|
||||
)
|
||||
@@ -38,8 +39,8 @@ type Interface interface {
|
||||
}
|
||||
|
||||
// New returns the [Interface] properly initialized for the OS.
|
||||
func New() (arp Interface) {
|
||||
return newARPDB()
|
||||
func New(logger *slog.Logger) (arp Interface) {
|
||||
return newARPDB(logger)
|
||||
}
|
||||
|
||||
// Empty is the [Interface] implementation that does nothing.
|
||||
@@ -69,6 +70,30 @@ type Neighbor struct {
|
||||
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.
|
||||
func (n Neighbor) Clone() (clone 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
|
||||
// otherwise, logging the validation error.
|
||||
func validatedHostname(h string) (host string) {
|
||||
func validatedHostname(logger *slog.Logger, h string) (host string) {
|
||||
err := netutil.ValidateHostname(h)
|
||||
if err != nil {
|
||||
log.Debug("arpdb: parsing arp output: host: %s", err)
|
||||
logger.Debug("parsing host of arp output", slogutil.KeyError, err)
|
||||
|
||||
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
|
||||
// ARP-related command. lenHint is a hint for the size of the allocated slice
|
||||
// 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
|
||||
// retrieve data.
|
||||
type cmdARPDB struct {
|
||||
parse parseNeighsFunc
|
||||
ns *neighs
|
||||
cmd string
|
||||
args []string
|
||||
logger *slog.Logger
|
||||
parse parseNeighsFunc
|
||||
ns *neighs
|
||||
cmd string
|
||||
args []string
|
||||
}
|
||||
|
||||
// type check
|
||||
@@ -158,7 +186,7 @@ func (arp *cmdARPDB) Refresh() (err error) {
|
||||
}
|
||||
|
||||
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 {
|
||||
// TODO(e.burkov): This error seems unreachable. Investigate.
|
||||
return fmt.Errorf("scanning the output: %w", err)
|
||||
|
||||
@@ -4,17 +4,17 @@ package arpdb
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/netip"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"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{
|
||||
parse: parseArpA,
|
||||
logger: logger,
|
||||
parse: parseArpA,
|
||||
ns: &neighs{
|
||||
mu: &sync.RWMutex{},
|
||||
ns: make([]Neighbor, 0),
|
||||
@@ -33,7 +33,7 @@ func newARPDB() (arp *cmdARPDB) {
|
||||
// The expected input format:
|
||||
//
|
||||
// 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)
|
||||
for sc.Scan() {
|
||||
ln := sc.Text()
|
||||
@@ -48,26 +48,15 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
||||
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 {
|
||||
log.Debug("arpdb: parsing arp output: ip: %s", err)
|
||||
logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
hwStr := fields[3]
|
||||
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]),
|
||||
})
|
||||
ns = append(ns, *n)
|
||||
}
|
||||
|
||||
return ns
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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) {
|
||||
var a Interface
|
||||
require.NotPanics(t, func() { a = New() })
|
||||
require.NotPanics(t, func() { a = New(slogutil.NewDiscardLogger()) })
|
||||
|
||||
assert.NotNil(t, a)
|
||||
}
|
||||
@@ -201,8 +202,9 @@ func Test_NewARPDBs(t *testing.T) {
|
||||
|
||||
func TestCmdARPDB_arpa(t *testing.T) {
|
||||
a := &cmdARPDB{
|
||||
cmd: "cmd",
|
||||
parse: parseArpA,
|
||||
logger: slogutil.NewDiscardLogger(),
|
||||
cmd: "cmd",
|
||||
parse: parseArpA,
|
||||
ns: &neighs{
|
||||
mu: &sync.RWMutex{},
|
||||
ns: make([]Neighbor, 0),
|
||||
|
||||
@@ -6,17 +6,18 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
)
|
||||
|
||||
func newARPDB() (arp *arpdbs) {
|
||||
func newARPDB(logger *slog.Logger) (arp *arpdbs) {
|
||||
// Use the common storage among the implementations.
|
||||
ns := &neighs{
|
||||
mu: &sync.RWMutex{},
|
||||
@@ -39,9 +40,10 @@ func newARPDB() (arp *arpdbs) {
|
||||
},
|
||||
// Then, try "arp -a -n".
|
||||
&cmdARPDB{
|
||||
parse: parseF,
|
||||
ns: ns,
|
||||
cmd: "arp",
|
||||
logger: logger,
|
||||
parse: parseF,
|
||||
ns: ns,
|
||||
cmd: "arp",
|
||||
// Use -n flag to avoid resolving the hostnames of the neighbors.
|
||||
// By default ARP attempts to resolve the hostnames via DNS. See
|
||||
// man 8 arp.
|
||||
@@ -51,10 +53,11 @@ func newARPDB() (arp *arpdbs) {
|
||||
},
|
||||
// Finally, try "ip neigh".
|
||||
&cmdARPDB{
|
||||
parse: parseIPNeigh,
|
||||
ns: ns,
|
||||
cmd: "ip",
|
||||
args: []string{"neigh"},
|
||||
logger: logger,
|
||||
parse: parseIPNeigh,
|
||||
ns: ns,
|
||||
cmd: "ip",
|
||||
args: []string{"neigh"},
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -131,7 +134,7 @@ func (arp *fsysARPDB) Neighbors() (ns []Neighbor) {
|
||||
//
|
||||
// IP address HW type Flags HW address Mask Device
|
||||
// 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() {
|
||||
// Skip the header.
|
||||
return
|
||||
@@ -146,25 +149,14 @@ func parseArpAWrt(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
||||
continue
|
||||
}
|
||||
|
||||
ip, err := netip.ParseAddr(fields[0])
|
||||
n, err := newNeighbor("", fields[0], fields[3])
|
||||
if err != nil {
|
||||
log.Debug("arpdb: parsing arp output: ip: %s", err)
|
||||
logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
hwStr := fields[3]
|
||||
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,
|
||||
})
|
||||
ns = append(ns, *n)
|
||||
}
|
||||
|
||||
return ns
|
||||
@@ -174,7 +166,7 @@ func parseArpAWrt(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
||||
// expected input format:
|
||||
//
|
||||
// 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)
|
||||
for sc.Scan() {
|
||||
ln := sc.Text()
|
||||
@@ -189,26 +181,15 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
||||
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 {
|
||||
log.Debug("arpdb: parsing arp output: ip: %s", err)
|
||||
logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
hwStr := fields[3]
|
||||
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]),
|
||||
})
|
||||
ns = append(ns, *n)
|
||||
}
|
||||
|
||||
return ns
|
||||
@@ -218,7 +199,7 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
||||
// expected input format:
|
||||
//
|
||||
// 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)
|
||||
for sc.Scan() {
|
||||
ln := sc.Text()
|
||||
@@ -228,27 +209,14 @@ func parseIPNeigh(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
||||
continue
|
||||
}
|
||||
|
||||
n := Neighbor{}
|
||||
|
||||
ip, err := netip.ParseAddr(fields[0])
|
||||
n, err := newNeighbor("", fields[0], fields[4])
|
||||
if err != nil {
|
||||
log.Debug("arpdb: parsing arp output: ip: %s", err)
|
||||
logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
|
||||
|
||||
continue
|
||||
} else {
|
||||
n.IP = ip
|
||||
}
|
||||
|
||||
mac, err := net.ParseMAC(fields[4])
|
||||
if err != nil {
|
||||
log.Debug("arpdb: parsing arp output: mac: %s", err)
|
||||
|
||||
continue
|
||||
} else {
|
||||
n.MAC = mac
|
||||
}
|
||||
|
||||
ns = append(ns, n)
|
||||
ns = append(ns, *n)
|
||||
}
|
||||
|
||||
return ns
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -69,9 +70,10 @@ func TestCmdARPDB_linux(t *testing.T) {
|
||||
|
||||
t.Run("wrt", func(t *testing.T) {
|
||||
a := &cmdARPDB{
|
||||
parse: parseArpAWrt,
|
||||
cmd: "arp",
|
||||
args: []string{"-a"},
|
||||
logger: slogutil.NewDiscardLogger(),
|
||||
parse: parseArpAWrt,
|
||||
cmd: "arp",
|
||||
args: []string{"-a"},
|
||||
ns: &neighs{
|
||||
mu: &sync.RWMutex{},
|
||||
ns: make([]Neighbor, 0),
|
||||
@@ -86,9 +88,10 @@ func TestCmdARPDB_linux(t *testing.T) {
|
||||
|
||||
t.Run("ip_neigh", func(t *testing.T) {
|
||||
a := &cmdARPDB{
|
||||
parse: parseIPNeigh,
|
||||
cmd: "ip",
|
||||
args: []string{"neigh"},
|
||||
logger: slogutil.NewDiscardLogger(),
|
||||
parse: parseIPNeigh,
|
||||
cmd: "ip",
|
||||
args: []string{"neigh"},
|
||||
ns: &neighs{
|
||||
mu: &sync.RWMutex{},
|
||||
ns: make([]Neighbor, 0),
|
||||
|
||||
@@ -4,17 +4,17 @@ package arpdb
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/netip"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"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{
|
||||
parse: parseArpA,
|
||||
logger: logger,
|
||||
parse: parseArpA,
|
||||
ns: &neighs{
|
||||
mu: &sync.RWMutex{},
|
||||
ns: make([]Neighbor, 0),
|
||||
@@ -34,7 +34,7 @@ func newARPDB() (arp *cmdARPDB) {
|
||||
//
|
||||
// Host Ethernet Address Netif Expire Flags
|
||||
// 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.
|
||||
if !sc.Scan() {
|
||||
return nil
|
||||
@@ -49,27 +49,14 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
||||
continue
|
||||
}
|
||||
|
||||
n := Neighbor{}
|
||||
|
||||
ip, err := netip.ParseAddr(fields[0])
|
||||
n, err := newNeighbor("", fields[0], fields[1])
|
||||
if err != nil {
|
||||
log.Debug("arpdb: parsing arp output: ip: %s", err)
|
||||
logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
|
||||
|
||||
continue
|
||||
} else {
|
||||
n.IP = ip
|
||||
}
|
||||
|
||||
mac, err := net.ParseMAC(fields[1])
|
||||
if err != nil {
|
||||
log.Debug("arpdb: parsing arp output: mac: %s", err)
|
||||
|
||||
continue
|
||||
} else {
|
||||
n.MAC = mac
|
||||
}
|
||||
|
||||
ns = append(ns, n)
|
||||
ns = append(ns, *n)
|
||||
}
|
||||
|
||||
return ns
|
||||
|
||||
@@ -4,17 +4,17 @@ package arpdb
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/netip"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"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{
|
||||
parse: parseArpA,
|
||||
logger: logger,
|
||||
parse: parseArpA,
|
||||
ns: &neighs{
|
||||
mu: &sync.RWMutex{},
|
||||
ns: make([]Neighbor, 0),
|
||||
@@ -31,7 +31,7 @@ func newARPDB() (arp *cmdARPDB) {
|
||||
// Internet Address Physical Address Type
|
||||
// 192.168.56.1 0a-00-27-00-00-00 dynamic
|
||||
// 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)
|
||||
for sc.Scan() {
|
||||
ln := sc.Text()
|
||||
@@ -44,24 +44,14 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
||||
continue
|
||||
}
|
||||
|
||||
ip, err := netip.ParseAddr(fields[0])
|
||||
n, err := newNeighbor("", fields[0], fields[1])
|
||||
if err != nil {
|
||||
log.Debug("arpdb: parsing arp output: ip: %s", err)
|
||||
logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
mac, err := net.ParseMAC(fields[1])
|
||||
if err != nil {
|
||||
log.Debug("arpdb: parsing arp output: mac: %s", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
ns = append(ns, Neighbor{
|
||||
IP: ip,
|
||||
MAC: mac,
|
||||
})
|
||||
ns = append(ns, *n)
|
||||
}
|
||||
|
||||
return ns
|
||||
|
||||
@@ -2,6 +2,7 @@ package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
)
|
||||
|
||||
@@ -38,6 +40,10 @@ func (EmptyAddrProc) Close() (_ error) { return nil }
|
||||
|
||||
// DefaultAddrProcConfig is the configuration structure for address processors.
|
||||
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 must not be nil if [DefaultAddrProcConfig.UseWHOIS] is true.
|
||||
DialContext aghnet.DialContextFunc
|
||||
@@ -147,6 +153,7 @@ func NewDefaultAddrProc(c *DefaultAddrProcConfig) (p *DefaultAddrProc) {
|
||||
|
||||
if c.UseRDNS {
|
||||
p.rdns = rdns.New(&rdns.Config{
|
||||
Logger: c.BaseLogger.With(slogutil.KeyPrefix, "rdns"),
|
||||
Exchanger: c.Exchanger,
|
||||
CacheSize: defaultCacheSize,
|
||||
CacheTTL: defaultIPTTL,
|
||||
@@ -154,7 +161,7 @@ func NewDefaultAddrProc(c *DefaultAddrProcConfig) (p *DefaultAddrProc) {
|
||||
}
|
||||
|
||||
if c.UseWHOIS {
|
||||
p.whois = newWHOIS(c.DialContext)
|
||||
p.whois = newWHOIS(c.BaseLogger.With(slogutil.KeyPrefix, "whois"), c.DialContext)
|
||||
}
|
||||
|
||||
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
|
||||
// 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.
|
||||
const (
|
||||
// defaultTimeout is the timeout for WHOIS requests.
|
||||
@@ -186,6 +193,7 @@ func newWHOIS(dialFunc aghnet.DialContextFunc) (w whois.Interface) {
|
||||
)
|
||||
|
||||
return whois.New(&whois.Config{
|
||||
Logger: logger,
|
||||
DialContext: dialFunc,
|
||||
ServerAddr: whois.DefaultServer,
|
||||
Port: whois.DefaultPort,
|
||||
@@ -227,9 +235,11 @@ func (p *DefaultAddrProc) process(catchPanics bool) {
|
||||
|
||||
log.Info("clients: processing addresses")
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
for ip := range p.clientIPs {
|
||||
host := p.processRDNS(ip)
|
||||
info := p.processWHOIS(ip)
|
||||
host := p.processRDNS(ctx, ip)
|
||||
info := p.processWHOIS(ctx, ip)
|
||||
|
||||
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
|
||||
// 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()
|
||||
log.Debug("clients: processing %s with rdns", ip)
|
||||
defer func() {
|
||||
@@ -251,7 +261,7 @@ func (p *DefaultAddrProc) processRDNS(ip netip.Addr) (host string) {
|
||||
return
|
||||
}
|
||||
|
||||
host, changed := p.rdns.Process(ip)
|
||||
host, changed := p.rdns.Process(ctx, ip)
|
||||
if !changed {
|
||||
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
|
||||
// WHOIS databases. info is nil if there were errors or if the information
|
||||
// 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()
|
||||
log.Debug("clients: processing %s with whois", ip)
|
||||
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
|
||||
// context.
|
||||
info, changed := p.whois.Process(context.Background(), ip)
|
||||
info, changed := p.whois.Process(ctx, ip)
|
||||
if !changed {
|
||||
info = nil
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/client"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/AdguardTeam/golibs/testutil/fakenet"
|
||||
@@ -99,6 +100,7 @@ func TestDefaultAddrProc_Process_rDNS(t *testing.T) {
|
||||
updInfoCh := make(chan *whois.Info, 1)
|
||||
|
||||
p := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{
|
||||
BaseLogger: slogutil.NewDiscardLogger(),
|
||||
DialContext: func(_ context.Context, _, _ string) (conn net.Conn, err error) {
|
||||
panic("not implemented")
|
||||
},
|
||||
@@ -208,6 +210,7 @@ func TestDefaultAddrProc_Process_WHOIS(t *testing.T) {
|
||||
updInfoCh := make(chan *whois.Info, 1)
|
||||
|
||||
p := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{
|
||||
BaseLogger: slogutil.NewDiscardLogger(),
|
||||
DialContext: func(_ context.Context, _, _ string) (conn net.Conn, err error) {
|
||||
return whoisConn, nil
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||
)
|
||||
@@ -120,6 +121,7 @@ func (r *Runtime) Info() (cs Source, host string) {
|
||||
|
||||
// SetInfo sets a host as a client information from the cs.
|
||||
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] == "" {
|
||||
hosts = []string{}
|
||||
}
|
||||
@@ -175,3 +177,15 @@ func (r *Runtime) isEmpty() (ok bool) {
|
||||
func (r *Runtime) Addr() (ip netip.Addr) {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ func macToKey(mac net.HardwareAddr) (key macKey) {
|
||||
}
|
||||
}
|
||||
|
||||
// Index stores all information about persistent clients.
|
||||
type Index struct {
|
||||
// index stores all information about persistent clients.
|
||||
type index struct {
|
||||
// nameToUID maps client name to UID.
|
||||
nameToUID map[string]UID
|
||||
|
||||
@@ -51,9 +51,9 @@ type Index struct {
|
||||
subnetToUID aghalg.SortedMap[netip.Prefix, UID]
|
||||
}
|
||||
|
||||
// NewIndex initializes the new instance of client index.
|
||||
func NewIndex() (ci *Index) {
|
||||
return &Index{
|
||||
// newIndex initializes the new instance of client index.
|
||||
func newIndex() (ci *index) {
|
||||
return &index{
|
||||
nameToUID: map[string]UID{},
|
||||
clientIDToUID: map[string]UID{},
|
||||
ipToUID: map[netip.Addr]UID{},
|
||||
@@ -63,9 +63,9 @@ func NewIndex() (ci *Index) {
|
||||
}
|
||||
}
|
||||
|
||||
// Add stores information about a persistent client in the index. c must be
|
||||
// non-nil and contain UID.
|
||||
func (ci *Index) Add(c *Persistent) {
|
||||
// add stores information about a persistent client in the index. c must be
|
||||
// non-nil, have a UID, and contain at least one identifier.
|
||||
func (ci *index) add(c *Persistent) {
|
||||
if (c.UID == UID{}) {
|
||||
panic("client must contain uid")
|
||||
}
|
||||
@@ -92,9 +92,9 @@ func (ci *Index) Add(c *Persistent) {
|
||||
ci.uidToClient[c.UID] = c
|
||||
}
|
||||
|
||||
// ClashesUID returns existing persistent client with the same UID as c. Note
|
||||
// clashesUID returns existing persistent client with the same UID as c. Note
|
||||
// that this is only possible when configuration contains duplicate fields.
|
||||
func (ci *Index) ClashesUID(c *Persistent) (err error) {
|
||||
func (ci *index) clashesUID(c *Persistent) (err error) {
|
||||
p, ok := ci.uidToClient[c.UID]
|
||||
if ok {
|
||||
return fmt.Errorf("another client %q uses the same uid", p.Name)
|
||||
@@ -103,9 +103,9 @@ func (ci *Index) ClashesUID(c *Persistent) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clashes returns an error if the index contains a different persistent client
|
||||
// clashes returns an error if the index contains a different persistent client
|
||||
// with at least a single identifier contained by c. c must be non-nil.
|
||||
func (ci *Index) Clashes(c *Persistent) (err error) {
|
||||
func (ci *index) clashes(c *Persistent) (err error) {
|
||||
if p := ci.clashesName(c); p != nil {
|
||||
return fmt.Errorf("another client uses the same name %q", p.Name)
|
||||
}
|
||||
@@ -139,8 +139,8 @@ func (ci *Index) Clashes(c *Persistent) (err error) {
|
||||
|
||||
// clashesName returns existing persistent client with the same name as c or
|
||||
// nil. c must be non-nil.
|
||||
func (ci *Index) clashesName(c *Persistent) (existing *Persistent) {
|
||||
existing, ok := ci.FindByName(c.Name)
|
||||
func (ci *index) clashesName(c *Persistent) (existing *Persistent) {
|
||||
existing, ok := ci.findByName(c.Name)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@@ -154,7 +154,7 @@ func (ci *Index) clashesName(c *Persistent) (existing *Persistent) {
|
||||
|
||||
// clashesIP returns a previous client with the same IP address as c. c must be
|
||||
// non-nil.
|
||||
func (ci *Index) clashesIP(c *Persistent) (p *Persistent, ip netip.Addr) {
|
||||
func (ci *index) clashesIP(c *Persistent) (p *Persistent, ip netip.Addr) {
|
||||
for _, ip := range c.IPs {
|
||||
existing, ok := ci.ipToUID[ip]
|
||||
if ok && existing != c.UID {
|
||||
@@ -167,7 +167,7 @@ func (ci *Index) clashesIP(c *Persistent) (p *Persistent, ip netip.Addr) {
|
||||
|
||||
// clashesSubnet returns a previous client with the same subnet as c. c must be
|
||||
// non-nil.
|
||||
func (ci *Index) clashesSubnet(c *Persistent) (p *Persistent, s netip.Prefix) {
|
||||
func (ci *index) clashesSubnet(c *Persistent) (p *Persistent, s netip.Prefix) {
|
||||
for _, s = range c.Subnets {
|
||||
var existing UID
|
||||
var ok bool
|
||||
@@ -193,7 +193,7 @@ func (ci *Index) clashesSubnet(c *Persistent) (p *Persistent, s netip.Prefix) {
|
||||
|
||||
// clashesMAC returns a previous client with the same MAC address as c. c must
|
||||
// be non-nil.
|
||||
func (ci *Index) clashesMAC(c *Persistent) (p *Persistent, mac net.HardwareAddr) {
|
||||
func (ci *index) clashesMAC(c *Persistent) (p *Persistent, mac net.HardwareAddr) {
|
||||
for _, mac = range c.MACs {
|
||||
k := macToKey(mac)
|
||||
existing, ok := ci.macToUID[k]
|
||||
@@ -205,9 +205,9 @@ func (ci *Index) clashesMAC(c *Persistent) (p *Persistent, mac net.HardwareAddr)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (ci *Index) Find(id string) (c *Persistent, ok bool) {
|
||||
func (ci *index) find(id string) (c *Persistent, ok bool) {
|
||||
uid, found := ci.clientIDToUID[id]
|
||||
if found {
|
||||
return ci.uidToClient[uid], true
|
||||
@@ -224,14 +224,14 @@ func (ci *Index) Find(id string) (c *Persistent, ok bool) {
|
||||
|
||||
mac, err := net.ParseMAC(id)
|
||||
if err == nil {
|
||||
return ci.FindByMAC(mac)
|
||||
return ci.findByMAC(mac)
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// FindByName finds persistent client by name.
|
||||
func (ci *Index) FindByName(name string) (c *Persistent, found bool) {
|
||||
// findByName finds persistent client by name.
|
||||
func (ci *index) findByName(name string) (c *Persistent, found bool) {
|
||||
uid, found := ci.nameToUID[name]
|
||||
if found {
|
||||
return ci.uidToClient[uid], true
|
||||
@@ -241,7 +241,7 @@ func (ci *Index) FindByName(name string) (c *Persistent, found bool) {
|
||||
}
|
||||
|
||||
// findByIP finds persistent client by IP address.
|
||||
func (ci *Index) findByIP(ip netip.Addr) (c *Persistent, found bool) {
|
||||
func (ci *index) findByIP(ip netip.Addr) (c *Persistent, found bool) {
|
||||
uid, found := ci.ipToUID[ip]
|
||||
if found {
|
||||
return ci.uidToClient[uid], true
|
||||
@@ -266,8 +266,8 @@ func (ci *Index) findByIP(ip netip.Addr) (c *Persistent, found bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// FindByMAC finds persistent client by MAC.
|
||||
func (ci *Index) FindByMAC(mac net.HardwareAddr) (c *Persistent, found bool) {
|
||||
// findByMAC finds persistent client by MAC.
|
||||
func (ci *index) findByMAC(mac net.HardwareAddr) (c *Persistent, found bool) {
|
||||
k := macToKey(mac)
|
||||
uid, found := ci.macToUID[k]
|
||||
if found {
|
||||
@@ -277,13 +277,13 @@ func (ci *Index) FindByMAC(mac net.HardwareAddr) (c *Persistent, found bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// FindByIPWithoutZone finds a persistent client by IP address without zone. It
|
||||
// findByIPWithoutZone finds a persistent client by IP address without zone. It
|
||||
// strips the IPv6 zone index from the stored IP addresses before comparing,
|
||||
// because querylog entries don't have it. See TODO on [querylog.logEntry.IP].
|
||||
//
|
||||
// Note that multiple clients can have the same IP address with different zones.
|
||||
// Therefore, the result of this method is indeterminate.
|
||||
func (ci *Index) FindByIPWithoutZone(ip netip.Addr) (c *Persistent) {
|
||||
func (ci *index) findByIPWithoutZone(ip netip.Addr) (c *Persistent) {
|
||||
if (ip == netip.Addr{}) {
|
||||
return nil
|
||||
}
|
||||
@@ -297,9 +297,9 @@ func (ci *Index) FindByIPWithoutZone(ip netip.Addr) (c *Persistent) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes information about persistent client from the index. c must be
|
||||
// remove removes information about persistent client from the index. c must be
|
||||
// non-nil.
|
||||
func (ci *Index) Delete(c *Persistent) {
|
||||
func (ci *index) remove(c *Persistent) {
|
||||
delete(ci.nameToUID, c.Name)
|
||||
|
||||
for _, id := range c.ClientIDs {
|
||||
@@ -322,24 +322,14 @@ func (ci *Index) Delete(c *Persistent) {
|
||||
delete(ci.uidToClient, c.UID)
|
||||
}
|
||||
|
||||
// Size returns the number of persistent clients.
|
||||
func (ci *Index) Size() (n int) {
|
||||
// size returns the number of persistent clients.
|
||||
func (ci *index) size() (n int) {
|
||||
return len(ci.uidToClient)
|
||||
}
|
||||
|
||||
// Range calls f for each persistent client, unless cont is false. The order is
|
||||
// undefined.
|
||||
func (ci *Index) Range(f func(c *Persistent) (cont bool)) {
|
||||
for _, c := range ci.uidToClient {
|
||||
if !f(c) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RangeByName is like [Index.Range] but sorts the persistent clients by name
|
||||
// rangeByName is like [Index.Range] but sorts the persistent clients by name
|
||||
// before iterating ensuring a predictable order.
|
||||
func (ci *Index) RangeByName(f func(c *Persistent) (cont bool)) {
|
||||
func (ci *index) rangeByName(f func(c *Persistent) (cont bool)) {
|
||||
cs := maps.Values(ci.uidToClient)
|
||||
slices.SortFunc(cs, func(a, b *Persistent) (n int) {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
@@ -352,10 +342,10 @@ func (ci *Index) RangeByName(f func(c *Persistent) (cont bool)) {
|
||||
}
|
||||
}
|
||||
|
||||
// CloseUpstreams closes upstream configurations of persistent clients.
|
||||
func (ci *Index) CloseUpstreams() (err error) {
|
||||
// closeUpstreams closes upstream configurations of persistent clients.
|
||||
func (ci *index) closeUpstreams() (err error) {
|
||||
var errs []error
|
||||
ci.RangeByName(func(c *Persistent) (cont bool) {
|
||||
ci.rangeByName(func(c *Persistent) (cont bool) {
|
||||
err = c.CloseUpstreams()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
|
||||
@@ -11,17 +11,18 @@ import (
|
||||
|
||||
// newIDIndex is a helper function that returns a client index filled with
|
||||
// persistent clients from the m. It also generates a UID for each client.
|
||||
func newIDIndex(m []*Persistent) (ci *Index) {
|
||||
ci = NewIndex()
|
||||
func newIDIndex(m []*Persistent) (ci *index) {
|
||||
ci = newIndex()
|
||||
|
||||
for _, c := range m {
|
||||
c.UID = MustNewUID()
|
||||
ci.Add(c)
|
||||
ci.add(c)
|
||||
}
|
||||
|
||||
return ci
|
||||
}
|
||||
|
||||
// TODO(s.chzhen): Remove.
|
||||
func TestClientIndex_Find(t *testing.T) {
|
||||
const (
|
||||
cliIPNone = "1.2.3.4"
|
||||
@@ -109,7 +110,7 @@ func TestClientIndex_Find(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, id := range tc.ids {
|
||||
c, ok := ci.Find(id)
|
||||
c, ok := ci.find(id)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, tc.want, c)
|
||||
@@ -118,7 +119,7 @@ func TestClientIndex_Find(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("not_found", func(t *testing.T) {
|
||||
_, ok := ci.Find(cliIPNone)
|
||||
_, ok := ci.find(cliIPNone)
|
||||
assert.False(t, ok)
|
||||
})
|
||||
}
|
||||
@@ -170,11 +171,11 @@ func TestClientIndex_Clashes(t *testing.T) {
|
||||
clone := tc.client.ShallowClone()
|
||||
clone.UID = MustNewUID()
|
||||
|
||||
err := ci.Clashes(clone)
|
||||
err := ci.clashes(clone)
|
||||
require.Error(t, err)
|
||||
|
||||
ci.Delete(tc.client)
|
||||
err = ci.Clashes(clone)
|
||||
ci.remove(tc.client)
|
||||
err = ci.clashes(clone)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
@@ -292,7 +293,7 @@ func TestIndex_FindByIPWithoutZone(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := ci.FindByIPWithoutZone(tc.ip.WithZone(""))
|
||||
c := ci.findByIPWithoutZone(tc.ip.WithZone(""))
|
||||
require.Equal(t, tc.want, c)
|
||||
})
|
||||
}
|
||||
@@ -338,7 +339,7 @@ func TestClientIndex_RangeByName(t *testing.T) {
|
||||
ci := newIDIndex(tc.want)
|
||||
|
||||
var got []*Persistent
|
||||
ci.RangeByName(func(c *Persistent) (cont bool) {
|
||||
ci.rangeByName(func(c *Persistent) (cont bool) {
|
||||
got = append(got, c)
|
||||
|
||||
return true
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/container"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
@@ -64,53 +65,107 @@ type Persistent struct {
|
||||
// upstream must be used.
|
||||
UpstreamConfig *proxy.CustomUpstreamConfig
|
||||
|
||||
// SafeSearch handles search engine hosts rewrites.
|
||||
SafeSearch filtering.SafeSearch
|
||||
|
||||
// BlockedServices is the configuration of blocked services of a client.
|
||||
// BlockedServices is the configuration of blocked services of a client. It
|
||||
// must not be nil after initialization.
|
||||
BlockedServices *filtering.BlockedServices
|
||||
|
||||
// Name of the persistent client. Must not be empty.
|
||||
Name string
|
||||
|
||||
Tags []string
|
||||
// Tags is a list of client tags that categorize the client.
|
||||
Tags []string
|
||||
|
||||
// Upstreams is a list of custom upstream DNS servers for the client.
|
||||
Upstreams []string
|
||||
|
||||
// IPs is a list of IP addresses that identify the client. The client must
|
||||
// have at least one ID (IP, subnet, MAC, or ClientID).
|
||||
IPs []netip.Addr
|
||||
|
||||
// Subnets identifying the client. The client must have at least one ID
|
||||
// (IP, subnet, MAC, or ClientID).
|
||||
//
|
||||
// TODO(s.chzhen): Use netutil.Prefix.
|
||||
Subnets []netip.Prefix
|
||||
MACs []net.HardwareAddr
|
||||
Subnets []netip.Prefix
|
||||
|
||||
// MACs identifying the client. The client must have at least one ID (IP,
|
||||
// subnet, MAC, or ClientID).
|
||||
MACs []net.HardwareAddr
|
||||
|
||||
// ClientIDs identifying the client. The client must have at least one ID
|
||||
// (IP, subnet, MAC, or ClientID).
|
||||
ClientIDs []string
|
||||
|
||||
// UID is the unique identifier of the persistent client.
|
||||
UID UID
|
||||
|
||||
UpstreamsCacheSize uint32
|
||||
// UpstreamsCacheSize is the cache size for custom upstreams.
|
||||
UpstreamsCacheSize uint32
|
||||
|
||||
// UpstreamsCacheEnabled specifies whether custom upstreams are used.
|
||||
UpstreamsCacheEnabled bool
|
||||
|
||||
UseOwnSettings bool
|
||||
FilteringEnabled bool
|
||||
SafeBrowsingEnabled bool
|
||||
ParentalEnabled bool
|
||||
UseOwnBlockedServices bool
|
||||
IgnoreQueryLog bool
|
||||
IgnoreStatistics bool
|
||||
// UseOwnSettings specifies whether custom filtering settings are used.
|
||||
UseOwnSettings bool
|
||||
|
||||
// FilteringEnabled specifies whether filtering is enabled.
|
||||
FilteringEnabled bool
|
||||
|
||||
// SafeBrowsingEnabled specifies whether safe browsing is enabled.
|
||||
SafeBrowsingEnabled bool
|
||||
|
||||
// ParentalEnabled specifies whether parental control is enabled.
|
||||
ParentalEnabled bool
|
||||
|
||||
// UseOwnBlockedServices specifies whether custom services are blocked.
|
||||
UseOwnBlockedServices bool
|
||||
|
||||
// IgnoreQueryLog specifies whether the client requests are logged.
|
||||
IgnoreQueryLog bool
|
||||
|
||||
// IgnoreStatistics specifies whether the client requests are counted.
|
||||
IgnoreStatistics bool
|
||||
|
||||
// SafeSearchConf is the safe search filtering configuration.
|
||||
//
|
||||
// TODO(d.kolyshev): Make SafeSearchConf a pointer.
|
||||
SafeSearchConf filtering.SafeSearchConfig
|
||||
}
|
||||
|
||||
// SetTags sets the tags if they are known, otherwise logs an unknown tag.
|
||||
func (c *Persistent) SetTags(tags []string, known *container.MapSet[string]) {
|
||||
for _, t := range tags {
|
||||
if !known.Has(t) {
|
||||
log.Info("skipping unknown tag %q", t)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
c.Tags = append(c.Tags, t)
|
||||
// validate returns an error if persistent client information contains errors.
|
||||
func (c *Persistent) validate(allTags *container.MapSet[string]) (err error) {
|
||||
switch {
|
||||
case c.Name == "":
|
||||
return errors.Error("empty name")
|
||||
case c.IDsLen() == 0:
|
||||
return errors.Error("id required")
|
||||
case c.UID == UID{}:
|
||||
return errors.Error("uid required")
|
||||
}
|
||||
|
||||
conf, err := proxy.ParseUpstreamsConfig(c.Upstreams, &upstream.Options{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid upstream servers: %w", err)
|
||||
}
|
||||
|
||||
err = conf.Close()
|
||||
if err != nil {
|
||||
log.Error("client: closing upstream config: %s", err)
|
||||
}
|
||||
|
||||
for _, t := range c.Tags {
|
||||
if !allTags.Has(t) {
|
||||
return fmt.Errorf("invalid tag: %q", t)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(s.chzhen): Move to the constructor.
|
||||
slices.Sort(c.Tags)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetIDs parses a list of strings into typed fields and returns an error if
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/golibs/container"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPersistentClient_EqualIDs(t *testing.T) {
|
||||
func TestPersistent_EqualIDs(t *testing.T) {
|
||||
const (
|
||||
ip = "0.0.0.0"
|
||||
ip1 = "1.1.1.1"
|
||||
@@ -122,3 +125,69 @@ func TestPersistentClient_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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
331
internal/client/storage.go
Normal file
331
internal/client/storage.go
Normal file
@@ -0,0 +1,331 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/AdguardTeam/golibs/container"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// Config is the client storage configuration structure.
|
||||
//
|
||||
// TODO(s.chzhen): Expand.
|
||||
type Config struct {
|
||||
// AllowedTags is a list of all allowed client tags.
|
||||
AllowedTags []string
|
||||
}
|
||||
|
||||
// Storage contains information about persistent and runtime clients.
|
||||
type Storage struct {
|
||||
// allowedTags is a set of all allowed tags.
|
||||
allowedTags *container.MapSet[string]
|
||||
|
||||
// mu protects indexes of persistent and runtime clients.
|
||||
mu *sync.Mutex
|
||||
|
||||
// index contains information about persistent clients.
|
||||
index *index
|
||||
|
||||
// runtimeIndex contains information about runtime clients.
|
||||
runtimeIndex *RuntimeIndex
|
||||
}
|
||||
|
||||
// NewStorage returns initialized client storage. conf must not be nil.
|
||||
func NewStorage(conf *Config) (s *Storage) {
|
||||
allowedTags := container.NewMapSet(conf.AllowedTags...)
|
||||
|
||||
return &Storage{
|
||||
allowedTags: allowedTags,
|
||||
mu: &sync.Mutex{},
|
||||
index: newIndex(),
|
||||
runtimeIndex: NewRuntimeIndex(),
|
||||
}
|
||||
}
|
||||
|
||||
// Add stores persistent client information or returns an error.
|
||||
func (s *Storage) Add(p *Persistent) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "adding client: %w") }()
|
||||
|
||||
err = p.validate(s.allowedTags)
|
||||
if err != nil {
|
||||
// Don't wrap the error since there is already an annotation deferred.
|
||||
return err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
err = s.index.clashesUID(p)
|
||||
if err != nil {
|
||||
// Don't wrap the error since there is already an annotation deferred.
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.index.clashes(p)
|
||||
if err != nil {
|
||||
// Don't wrap the error since there is already an annotation deferred.
|
||||
return err
|
||||
}
|
||||
|
||||
s.index.add(p)
|
||||
|
||||
log.Debug("client storage: added %q: IDs: %q [%d]", p.Name, p.IDs(), s.index.size())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindByName finds persistent client by name. And returns its shallow copy.
|
||||
func (s *Storage) FindByName(name string) (p *Persistent, ok bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
p, ok = s.index.findByName(name)
|
||||
if ok {
|
||||
return p.ShallowClone(), ok
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Find finds persistent client by string representation of the client ID, IP
|
||||
// address, or MAC. And returns its shallow copy.
|
||||
func (s *Storage) Find(id string) (p *Persistent, ok bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
p, ok = s.index.find(id)
|
||||
if ok {
|
||||
return p.ShallowClone(), ok
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// FindLoose is like [Storage.Find] but it also tries to find a persistent
|
||||
// client by IP address without zone. It strips the IPv6 zone index from the
|
||||
// stored IP addresses before comparing, because querylog entries don't have it.
|
||||
// See TODO on [querylog.logEntry.IP].
|
||||
//
|
||||
// Note that multiple clients can have the same IP address with different zones.
|
||||
// Therefore, the result of this method is indeterminate.
|
||||
func (s *Storage) FindLoose(ip netip.Addr, id string) (p *Persistent, ok bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
p, ok = s.index.find(id)
|
||||
if ok {
|
||||
return p.ShallowClone(), ok
|
||||
}
|
||||
|
||||
p = s.index.findByIPWithoutZone(ip)
|
||||
if p != nil {
|
||||
return p.ShallowClone(), true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// FindByMAC finds persistent client by MAC and returns its shallow copy.
|
||||
func (s *Storage) FindByMAC(mac net.HardwareAddr) (p *Persistent, ok bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
p, ok = s.index.findByMAC(mac)
|
||||
if ok {
|
||||
return p.ShallowClone(), ok
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// RemoveByName removes persistent client information. ok is false if no such
|
||||
// client exists by that name.
|
||||
func (s *Storage) RemoveByName(name string) (ok bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
p, ok := s.index.findByName(name)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if err := p.CloseUpstreams(); err != nil {
|
||||
log.Error("client storage: removing client %q: %s", p.Name, err)
|
||||
}
|
||||
|
||||
s.index.remove(p)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Update finds the stored persistent client by its name and updates its
|
||||
// information from p.
|
||||
func (s *Storage) Update(name string, p *Persistent) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "updating client: %w") }()
|
||||
|
||||
err = p.validate(s.allowedTags)
|
||||
if err != nil {
|
||||
// Don't wrap the error since there is already an annotation deferred.
|
||||
return err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
stored, ok := s.index.findByName(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("client %q is not found", name)
|
||||
}
|
||||
|
||||
// Client p has a newly generated UID, so replace it with the stored one.
|
||||
//
|
||||
// TODO(s.chzhen): Remove when frontend starts handling UIDs.
|
||||
p.UID = stored.UID
|
||||
|
||||
err = s.index.clashes(p)
|
||||
if err != nil {
|
||||
// Don't wrap the error since there is already an annotation deferred.
|
||||
return err
|
||||
}
|
||||
|
||||
s.index.remove(stored)
|
||||
s.index.add(p)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RangeByName calls f for each persistent client sorted by name, unless cont is
|
||||
// false.
|
||||
func (s *Storage) RangeByName(f func(c *Persistent) (cont bool)) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.index.rangeByName(f)
|
||||
}
|
||||
|
||||
// Size returns the number of persistent clients.
|
||||
func (s *Storage) Size() (n int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return s.index.size()
|
||||
}
|
||||
|
||||
// CloseUpstreams closes upstream configurations of persistent clients.
|
||||
func (s *Storage) CloseUpstreams() (err error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return s.index.closeUpstreams()
|
||||
}
|
||||
|
||||
// ClientRuntime returns a copy of the saved runtime client by ip. If no such
|
||||
// client exists, returns nil.
|
||||
//
|
||||
// TODO(s.chzhen): Use it.
|
||||
func (s *Storage) ClientRuntime(ip netip.Addr) (rc *Runtime) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return s.runtimeIndex.Client(ip)
|
||||
}
|
||||
|
||||
// UpdateRuntime updates the stored runtime client with information from rc. If
|
||||
// no such client exists, saves the copy of rc in storage. rc must not be nil.
|
||||
func (s *Storage) UpdateRuntime(rc *Runtime) (added bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return s.updateRuntimeLocked(rc)
|
||||
}
|
||||
|
||||
// updateRuntimeLocked updates the stored runtime client with information from
|
||||
// rc. rc must not be nil. Storage.mu is expected to be locked.
|
||||
func (s *Storage) updateRuntimeLocked(rc *Runtime) (added bool) {
|
||||
stored := s.runtimeIndex.Client(rc.ip)
|
||||
if stored == nil {
|
||||
s.runtimeIndex.Add(rc.Clone())
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if rc.whois != nil {
|
||||
stored.whois = rc.whois.Clone()
|
||||
}
|
||||
|
||||
if rc.arp != nil {
|
||||
stored.arp = slices.Clone(rc.arp)
|
||||
}
|
||||
|
||||
if rc.rdns != nil {
|
||||
stored.rdns = slices.Clone(rc.rdns)
|
||||
}
|
||||
|
||||
if rc.dhcp != nil {
|
||||
stored.dhcp = slices.Clone(rc.dhcp)
|
||||
}
|
||||
|
||||
if rc.hostsFile != nil {
|
||||
stored.hostsFile = slices.Clone(rc.hostsFile)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// BatchUpdateBySource updates the stored runtime clients information from the
|
||||
// specified source and returns the number of added and removed clients.
|
||||
func (s *Storage) BatchUpdateBySource(src Source, rcs []*Runtime) (added, removed int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for _, rc := range s.runtimeIndex.index {
|
||||
rc.unset(src)
|
||||
}
|
||||
|
||||
for _, rc := range rcs {
|
||||
if s.updateRuntimeLocked(rc) {
|
||||
added++
|
||||
}
|
||||
}
|
||||
|
||||
for ip, rc := range s.runtimeIndex.index {
|
||||
if rc.isEmpty() {
|
||||
delete(s.runtimeIndex.index, ip)
|
||||
removed++
|
||||
}
|
||||
}
|
||||
|
||||
return added, removed
|
||||
}
|
||||
|
||||
// SizeRuntime returns the number of the runtime clients.
|
||||
func (s *Storage) SizeRuntime() (n int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return s.runtimeIndex.Size()
|
||||
}
|
||||
|
||||
// RangeRuntime calls f for each runtime client in an undefined order.
|
||||
func (s *Storage) RangeRuntime(f func(rc *Runtime) (cont bool)) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.runtimeIndex.Range(f)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
779
internal/client/storage_test.go
Normal file
779
internal/client/storage_test.go
Normal file
@@ -0,0 +1,779 @@
|
||||
package client_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/client"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 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.
|
||||
func newStorage(tb testing.TB, m []*client.Persistent) (s *client.Storage) {
|
||||
tb.Helper()
|
||||
|
||||
s = client.NewStorage(&client.Config{
|
||||
AllowedTags: nil,
|
||||
})
|
||||
|
||||
for _, c := range m {
|
||||
c.UID = client.MustNewUID()
|
||||
require.NoError(tb, s.Add(c))
|
||||
}
|
||||
|
||||
require.Equal(tb, len(m), s.Size())
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// newRuntimeClient is a helper function that returns a new runtime client.
|
||||
func newRuntimeClient(ip netip.Addr, source client.Source, host string) (rc *client.Runtime) {
|
||||
rc = client.NewRuntime(ip)
|
||||
rc.SetInfo(source, []string{host})
|
||||
|
||||
return rc
|
||||
}
|
||||
|
||||
// mustParseMAC is wrapper around [net.ParseMAC] that panics if there is an
|
||||
// error.
|
||||
func mustParseMAC(s string) (mac net.HardwareAddr) {
|
||||
mac, err := net.ParseMAC(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return mac
|
||||
}
|
||||
|
||||
func TestStorage_Add(t *testing.T) {
|
||||
const (
|
||||
existingName = "existing_name"
|
||||
existingClientID = "existing_client_id"
|
||||
|
||||
allowedTag = "tag"
|
||||
notAllowedTag = "not_allowed_tag"
|
||||
)
|
||||
|
||||
var (
|
||||
existingClientUID = client.MustNewUID()
|
||||
existingIP = netip.MustParseAddr("1.2.3.4")
|
||||
existingSubnet = netip.MustParsePrefix("1.2.3.0/24")
|
||||
)
|
||||
|
||||
existingClient := &client.Persistent{
|
||||
Name: existingName,
|
||||
IPs: []netip.Addr{existingIP},
|
||||
Subnets: []netip.Prefix{existingSubnet},
|
||||
ClientIDs: []string{existingClientID},
|
||||
UID: existingClientUID,
|
||||
}
|
||||
|
||||
s := client.NewStorage(&client.Config{
|
||||
AllowedTags: []string{allowedTag},
|
||||
})
|
||||
err := s.Add(existingClient)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
cli *client.Persistent
|
||||
wantErrMsg string
|
||||
}{{
|
||||
name: "basic",
|
||||
cli: &client.Persistent{
|
||||
Name: "basic",
|
||||
IPs: []netip.Addr{netip.MustParseAddr("1.1.1.1")},
|
||||
UID: client.MustNewUID(),
|
||||
},
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "duplicate_uid",
|
||||
cli: &client.Persistent{
|
||||
Name: "no_uid",
|
||||
IPs: []netip.Addr{netip.MustParseAddr("2.2.2.2")},
|
||||
UID: existingClientUID,
|
||||
},
|
||||
wantErrMsg: `adding client: another client "existing_name" uses the same uid`,
|
||||
}, {
|
||||
name: "duplicate_name",
|
||||
cli: &client.Persistent{
|
||||
Name: existingName,
|
||||
IPs: []netip.Addr{netip.MustParseAddr("3.3.3.3")},
|
||||
UID: client.MustNewUID(),
|
||||
},
|
||||
wantErrMsg: `adding client: another client uses the same name "existing_name"`,
|
||||
}, {
|
||||
name: "duplicate_ip",
|
||||
cli: &client.Persistent{
|
||||
Name: "duplicate_ip",
|
||||
IPs: []netip.Addr{existingIP},
|
||||
UID: client.MustNewUID(),
|
||||
},
|
||||
wantErrMsg: `adding client: another client "existing_name" uses the same IP "1.2.3.4"`,
|
||||
}, {
|
||||
name: "duplicate_subnet",
|
||||
cli: &client.Persistent{
|
||||
Name: "duplicate_subnet",
|
||||
Subnets: []netip.Prefix{existingSubnet},
|
||||
UID: client.MustNewUID(),
|
||||
},
|
||||
wantErrMsg: `adding client: another client "existing_name" ` +
|
||||
`uses the same subnet "1.2.3.0/24"`,
|
||||
}, {
|
||||
name: "duplicate_client_id",
|
||||
cli: &client.Persistent{
|
||||
Name: "duplicate_client_id",
|
||||
ClientIDs: []string{existingClientID},
|
||||
UID: client.MustNewUID(),
|
||||
},
|
||||
wantErrMsg: `adding client: another client "existing_name" ` +
|
||||
`uses the same ClientID "existing_client_id"`,
|
||||
}, {
|
||||
name: "not_allowed_tag",
|
||||
cli: &client.Persistent{
|
||||
Name: "nont_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"`,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err = s.Add(tc.cli)
|
||||
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_RemoveByName(t *testing.T) {
|
||||
const (
|
||||
existingName = "existing_name"
|
||||
)
|
||||
|
||||
existingClient := &client.Persistent{
|
||||
Name: existingName,
|
||||
IPs: []netip.Addr{netip.MustParseAddr("1.2.3.4")},
|
||||
UID: client.MustNewUID(),
|
||||
}
|
||||
|
||||
s := client.NewStorage(&client.Config{
|
||||
AllowedTags: nil,
|
||||
})
|
||||
err := s.Add(existingClient)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
want assert.BoolAssertionFunc
|
||||
name string
|
||||
cliName string
|
||||
}{{
|
||||
name: "existing_client",
|
||||
cliName: existingName,
|
||||
want: assert.True,
|
||||
}, {
|
||||
name: "non_existing_client",
|
||||
cliName: "non_existing_client",
|
||||
want: assert.False,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tc.want(t, s.RemoveByName(tc.cliName))
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("duplicate_remove", func(t *testing.T) {
|
||||
s = client.NewStorage(&client.Config{
|
||||
AllowedTags: nil,
|
||||
})
|
||||
err = s.Add(existingClient)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, s.RemoveByName(existingName))
|
||||
assert.False(t, s.RemoveByName(existingName))
|
||||
})
|
||||
}
|
||||
|
||||
func TestStorage_Find(t *testing.T) {
|
||||
const (
|
||||
cliIPNone = "1.2.3.4"
|
||||
cliIP1 = "1.1.1.1"
|
||||
cliIP2 = "2.2.2.2"
|
||||
|
||||
cliIPv6 = "1:2:3::4"
|
||||
|
||||
cliSubnet = "2.2.2.0/24"
|
||||
cliSubnetIP = "2.2.2.222"
|
||||
|
||||
cliID = "client-id"
|
||||
cliMAC = "11:11:11:11:11:11"
|
||||
|
||||
linkLocalIP = "fe80::abcd:abcd:abcd:ab%eth0"
|
||||
linkLocalSubnet = "fe80::/16"
|
||||
)
|
||||
|
||||
var (
|
||||
clientWithBothFams = &client.Persistent{
|
||||
Name: "client1",
|
||||
IPs: []netip.Addr{
|
||||
netip.MustParseAddr(cliIP1),
|
||||
netip.MustParseAddr(cliIPv6),
|
||||
},
|
||||
}
|
||||
|
||||
clientWithSubnet = &client.Persistent{
|
||||
Name: "client2",
|
||||
IPs: []netip.Addr{netip.MustParseAddr(cliIP2)},
|
||||
Subnets: []netip.Prefix{netip.MustParsePrefix(cliSubnet)},
|
||||
}
|
||||
|
||||
clientWithMAC = &client.Persistent{
|
||||
Name: "client_with_mac",
|
||||
MACs: []net.HardwareAddr{mustParseMAC(cliMAC)},
|
||||
}
|
||||
|
||||
clientWithID = &client.Persistent{
|
||||
Name: "client_with_id",
|
||||
ClientIDs: []string{cliID},
|
||||
}
|
||||
|
||||
clientLinkLocal = &client.Persistent{
|
||||
Name: "client_link_local",
|
||||
Subnets: []netip.Prefix{netip.MustParsePrefix(linkLocalSubnet)},
|
||||
}
|
||||
)
|
||||
|
||||
clients := []*client.Persistent{
|
||||
clientWithBothFams,
|
||||
clientWithSubnet,
|
||||
clientWithMAC,
|
||||
clientWithID,
|
||||
clientLinkLocal,
|
||||
}
|
||||
s := newStorage(t, clients)
|
||||
|
||||
testCases := []struct {
|
||||
want *client.Persistent
|
||||
name string
|
||||
ids []string
|
||||
}{{
|
||||
name: "ipv4_ipv6",
|
||||
ids: []string{cliIP1, cliIPv6},
|
||||
want: clientWithBothFams,
|
||||
}, {
|
||||
name: "ipv4_subnet",
|
||||
ids: []string{cliIP2, cliSubnetIP},
|
||||
want: clientWithSubnet,
|
||||
}, {
|
||||
name: "mac",
|
||||
ids: []string{cliMAC},
|
||||
want: clientWithMAC,
|
||||
}, {
|
||||
name: "client_id",
|
||||
ids: []string{cliID},
|
||||
want: clientWithID,
|
||||
}, {
|
||||
name: "client_link_local_subnet",
|
||||
ids: []string{linkLocalIP},
|
||||
want: clientLinkLocal,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, id := range tc.ids {
|
||||
c, ok := s.Find(id)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, tc.want, c)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("not_found", func(t *testing.T) {
|
||||
_, ok := s.Find(cliIPNone)
|
||||
assert.False(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStorage_FindLoose(t *testing.T) {
|
||||
const (
|
||||
nonExistingClientID = "client_id"
|
||||
)
|
||||
|
||||
var (
|
||||
ip = netip.MustParseAddr("fe80::a098:7654:32ef:ff1")
|
||||
ipWithZone = netip.MustParseAddr("fe80::1ff:fe23:4567:890a%eth2")
|
||||
)
|
||||
|
||||
var (
|
||||
clientNoZone = &client.Persistent{
|
||||
Name: "client",
|
||||
IPs: []netip.Addr{ip},
|
||||
}
|
||||
|
||||
clientWithZone = &client.Persistent{
|
||||
Name: "client_with_zone",
|
||||
IPs: []netip.Addr{ipWithZone},
|
||||
}
|
||||
)
|
||||
|
||||
s := newStorage(
|
||||
t,
|
||||
[]*client.Persistent{
|
||||
clientNoZone,
|
||||
clientWithZone,
|
||||
},
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
ip netip.Addr
|
||||
want assert.BoolAssertionFunc
|
||||
wantCli *client.Persistent
|
||||
name string
|
||||
}{{
|
||||
name: "without_zone",
|
||||
ip: ip,
|
||||
wantCli: clientNoZone,
|
||||
want: assert.True,
|
||||
}, {
|
||||
name: "with_zone",
|
||||
ip: ipWithZone,
|
||||
wantCli: clientWithZone,
|
||||
want: assert.True,
|
||||
}, {
|
||||
name: "zero_address",
|
||||
ip: netip.Addr{},
|
||||
wantCli: nil,
|
||||
want: assert.False,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c, ok := s.FindLoose(tc.ip.WithZone(""), nonExistingClientID)
|
||||
assert.Equal(t, tc.wantCli, c)
|
||||
tc.want(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_FindByName(t *testing.T) {
|
||||
const (
|
||||
cliIP1 = "1.1.1.1"
|
||||
cliIP2 = "2.2.2.2"
|
||||
)
|
||||
|
||||
const (
|
||||
clientExistingName = "client_existing"
|
||||
clientAnotherExistingName = "client_another_existing"
|
||||
nonExistingClientName = "client_non_existing"
|
||||
)
|
||||
|
||||
var (
|
||||
clientExisting = &client.Persistent{
|
||||
Name: clientExistingName,
|
||||
IPs: []netip.Addr{netip.MustParseAddr(cliIP1)},
|
||||
}
|
||||
|
||||
clientAnotherExisting = &client.Persistent{
|
||||
Name: clientAnotherExistingName,
|
||||
IPs: []netip.Addr{netip.MustParseAddr(cliIP2)},
|
||||
}
|
||||
)
|
||||
|
||||
clients := []*client.Persistent{
|
||||
clientExisting,
|
||||
clientAnotherExisting,
|
||||
}
|
||||
s := newStorage(t, clients)
|
||||
|
||||
testCases := []struct {
|
||||
want *client.Persistent
|
||||
name string
|
||||
clientName string
|
||||
}{{
|
||||
name: "existing",
|
||||
clientName: clientExistingName,
|
||||
want: clientExisting,
|
||||
}, {
|
||||
name: "another_existing",
|
||||
clientName: clientAnotherExistingName,
|
||||
want: clientAnotherExisting,
|
||||
}, {
|
||||
name: "non_existing",
|
||||
clientName: nonExistingClientName,
|
||||
want: nil,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c, ok := s.FindByName(tc.clientName)
|
||||
if tc.want == nil {
|
||||
assert.False(t, ok)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, tc.want, c)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_FindByMAC(t *testing.T) {
|
||||
var (
|
||||
cliMAC = mustParseMAC("11:11:11:11:11:11")
|
||||
cliAnotherMAC = mustParseMAC("22:22:22:22:22:22")
|
||||
nonExistingClientMAC = mustParseMAC("33:33:33:33:33:33")
|
||||
)
|
||||
|
||||
var (
|
||||
clientExisting = &client.Persistent{
|
||||
Name: "client",
|
||||
MACs: []net.HardwareAddr{cliMAC},
|
||||
}
|
||||
|
||||
clientAnotherExisting = &client.Persistent{
|
||||
Name: "another_client",
|
||||
MACs: []net.HardwareAddr{cliAnotherMAC},
|
||||
}
|
||||
)
|
||||
|
||||
clients := []*client.Persistent{
|
||||
clientExisting,
|
||||
clientAnotherExisting,
|
||||
}
|
||||
s := newStorage(t, clients)
|
||||
|
||||
testCases := []struct {
|
||||
want *client.Persistent
|
||||
name string
|
||||
clientMAC net.HardwareAddr
|
||||
}{{
|
||||
name: "existing",
|
||||
clientMAC: cliMAC,
|
||||
want: clientExisting,
|
||||
}, {
|
||||
name: "another_existing",
|
||||
clientMAC: cliAnotherMAC,
|
||||
want: clientAnotherExisting,
|
||||
}, {
|
||||
name: "non_existing",
|
||||
clientMAC: nonExistingClientMAC,
|
||||
want: nil,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c, ok := s.FindByMAC(tc.clientMAC)
|
||||
if tc.want == nil {
|
||||
assert.False(t, ok)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, tc.want, c)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_Update(t *testing.T) {
|
||||
const (
|
||||
clientName = "client_name"
|
||||
obstructingName = "obstructing_name"
|
||||
obstructingClientID = "obstructing_client_id"
|
||||
)
|
||||
|
||||
var (
|
||||
obstructingIP = netip.MustParseAddr("1.2.3.4")
|
||||
obstructingSubnet = netip.MustParsePrefix("1.2.3.0/24")
|
||||
)
|
||||
|
||||
obstructingClient := &client.Persistent{
|
||||
Name: obstructingName,
|
||||
IPs: []netip.Addr{obstructingIP},
|
||||
Subnets: []netip.Prefix{obstructingSubnet},
|
||||
ClientIDs: []string{obstructingClientID},
|
||||
}
|
||||
|
||||
clientToUpdate := &client.Persistent{
|
||||
Name: clientName,
|
||||
IPs: []netip.Addr{netip.MustParseAddr("1.1.1.1")},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
cli *client.Persistent
|
||||
wantErrMsg string
|
||||
}{{
|
||||
name: "basic",
|
||||
cli: &client.Persistent{
|
||||
Name: "basic",
|
||||
IPs: []netip.Addr{netip.MustParseAddr("1.1.1.1")},
|
||||
UID: client.MustNewUID(),
|
||||
},
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "duplicate_name",
|
||||
cli: &client.Persistent{
|
||||
Name: obstructingName,
|
||||
IPs: []netip.Addr{netip.MustParseAddr("3.3.3.3")},
|
||||
UID: client.MustNewUID(),
|
||||
},
|
||||
wantErrMsg: `updating client: another client uses the same name "obstructing_name"`,
|
||||
}, {
|
||||
name: "duplicate_ip",
|
||||
cli: &client.Persistent{
|
||||
Name: "duplicate_ip",
|
||||
IPs: []netip.Addr{obstructingIP},
|
||||
UID: client.MustNewUID(),
|
||||
},
|
||||
wantErrMsg: `updating client: another client "obstructing_name" uses the same IP "1.2.3.4"`,
|
||||
}, {
|
||||
name: "duplicate_subnet",
|
||||
cli: &client.Persistent{
|
||||
Name: "duplicate_subnet",
|
||||
Subnets: []netip.Prefix{obstructingSubnet},
|
||||
UID: client.MustNewUID(),
|
||||
},
|
||||
wantErrMsg: `updating client: another client "obstructing_name" ` +
|
||||
`uses the same subnet "1.2.3.0/24"`,
|
||||
}, {
|
||||
name: "duplicate_client_id",
|
||||
cli: &client.Persistent{
|
||||
Name: "duplicate_client_id",
|
||||
ClientIDs: []string{obstructingClientID},
|
||||
UID: client.MustNewUID(),
|
||||
},
|
||||
wantErrMsg: `updating client: another client "obstructing_name" ` +
|
||||
`uses the same ClientID "obstructing_client_id"`,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := newStorage(
|
||||
t,
|
||||
[]*client.Persistent{
|
||||
clientToUpdate,
|
||||
obstructingClient,
|
||||
},
|
||||
)
|
||||
|
||||
err := s.Update(clientName, tc.cli)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_RangeByName(t *testing.T) {
|
||||
sortedClients := []*client.Persistent{{
|
||||
Name: "clientA",
|
||||
ClientIDs: []string{"A"},
|
||||
}, {
|
||||
Name: "clientB",
|
||||
ClientIDs: []string{"B"},
|
||||
}, {
|
||||
Name: "clientC",
|
||||
ClientIDs: []string{"C"},
|
||||
}, {
|
||||
Name: "clientD",
|
||||
ClientIDs: []string{"D"},
|
||||
}, {
|
||||
Name: "clientE",
|
||||
ClientIDs: []string{"E"},
|
||||
}}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
want []*client.Persistent
|
||||
}{{
|
||||
name: "basic",
|
||||
want: sortedClients,
|
||||
}, {
|
||||
name: "nil",
|
||||
want: nil,
|
||||
}, {
|
||||
name: "one_element",
|
||||
want: sortedClients[:1],
|
||||
}, {
|
||||
name: "two_elements",
|
||||
want: sortedClients[:2],
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := newStorage(t, tc.want)
|
||||
|
||||
var got []*client.Persistent
|
||||
s.RangeByName(func(c *client.Persistent) (cont bool) {
|
||||
got = append(got, c)
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorage_UpdateRuntime(t *testing.T) {
|
||||
const (
|
||||
addedARP = "added_arp"
|
||||
addedSecondARP = "added_arp"
|
||||
|
||||
updatedARP = "updated_arp"
|
||||
|
||||
cliCity = "City"
|
||||
cliCountry = "Country"
|
||||
cliOrgname = "Orgname"
|
||||
)
|
||||
|
||||
var (
|
||||
ip = netip.MustParseAddr("1.1.1.1")
|
||||
ip2 = netip.MustParseAddr("2.2.2.2")
|
||||
)
|
||||
|
||||
updated := client.NewRuntime(ip)
|
||||
updated.SetInfo(client.SourceARP, []string{updatedARP})
|
||||
|
||||
info := &whois.Info{
|
||||
City: cliCity,
|
||||
Country: cliCountry,
|
||||
Orgname: cliOrgname,
|
||||
}
|
||||
updated.SetWHOIS(info)
|
||||
|
||||
s := client.NewStorage(&client.Config{
|
||||
AllowedTags: nil,
|
||||
})
|
||||
|
||||
t.Run("add_arp_client", func(t *testing.T) {
|
||||
added := client.NewRuntime(ip)
|
||||
added.SetInfo(client.SourceARP, []string{addedARP})
|
||||
|
||||
require.True(t, s.UpdateRuntime(added))
|
||||
require.Equal(t, 1, s.SizeRuntime())
|
||||
|
||||
got := s.ClientRuntime(ip)
|
||||
source, host := got.Info()
|
||||
assert.Equal(t, client.SourceARP, source)
|
||||
assert.Equal(t, addedARP, host)
|
||||
})
|
||||
|
||||
t.Run("add_second_arp_client", func(t *testing.T) {
|
||||
added := client.NewRuntime(ip2)
|
||||
added.SetInfo(client.SourceARP, []string{addedSecondARP})
|
||||
|
||||
require.True(t, s.UpdateRuntime(added))
|
||||
require.Equal(t, 2, s.SizeRuntime())
|
||||
|
||||
got := s.ClientRuntime(ip2)
|
||||
source, host := got.Info()
|
||||
assert.Equal(t, client.SourceARP, source)
|
||||
assert.Equal(t, addedSecondARP, host)
|
||||
})
|
||||
|
||||
t.Run("update_first_client", func(t *testing.T) {
|
||||
require.False(t, s.UpdateRuntime(updated))
|
||||
got := s.ClientRuntime(ip)
|
||||
require.Equal(t, 2, s.SizeRuntime())
|
||||
|
||||
source, host := got.Info()
|
||||
assert.Equal(t, client.SourceARP, source)
|
||||
assert.Equal(t, updatedARP, host)
|
||||
})
|
||||
|
||||
t.Run("remove_arp_info", func(t *testing.T) {
|
||||
n := s.DeleteBySource(client.SourceARP)
|
||||
require.Equal(t, 1, n)
|
||||
require.Equal(t, 1, s.SizeRuntime())
|
||||
|
||||
got := s.ClientRuntime(ip)
|
||||
source, _ := got.Info()
|
||||
assert.Equal(t, client.SourceWHOIS, source)
|
||||
assert.Equal(t, info, got.WHOIS())
|
||||
})
|
||||
|
||||
t.Run("remove_whois_info", func(t *testing.T) {
|
||||
n := s.DeleteBySource(client.SourceWHOIS)
|
||||
require.Equal(t, 1, n)
|
||||
require.Equal(t, 0, s.SizeRuntime())
|
||||
})
|
||||
}
|
||||
|
||||
func TestStorage_BatchUpdateBySource(t *testing.T) {
|
||||
const (
|
||||
defSrc = client.SourceARP
|
||||
|
||||
cliFirstHost1 = "host1"
|
||||
cliFirstHost2 = "host2"
|
||||
cliUpdatedHost3 = "host3"
|
||||
cliUpdatedHost4 = "host4"
|
||||
cliUpdatedHost5 = "host5"
|
||||
)
|
||||
|
||||
var (
|
||||
cliFirstIP1 = netip.MustParseAddr("1.1.1.1")
|
||||
cliFirstIP2 = netip.MustParseAddr("2.2.2.2")
|
||||
cliUpdatedIP3 = netip.MustParseAddr("3.3.3.3")
|
||||
cliUpdatedIP4 = netip.MustParseAddr("4.4.4.4")
|
||||
cliUpdatedIP5 = netip.MustParseAddr("5.5.5.5")
|
||||
)
|
||||
|
||||
firstClients := []*client.Runtime{
|
||||
newRuntimeClient(cliFirstIP1, defSrc, cliFirstHost1),
|
||||
newRuntimeClient(cliFirstIP2, defSrc, cliFirstHost2),
|
||||
}
|
||||
|
||||
updatedClients := []*client.Runtime{
|
||||
newRuntimeClient(cliUpdatedIP3, defSrc, cliUpdatedHost3),
|
||||
newRuntimeClient(cliUpdatedIP4, defSrc, cliUpdatedHost4),
|
||||
newRuntimeClient(cliUpdatedIP5, defSrc, cliUpdatedHost5),
|
||||
}
|
||||
|
||||
s := client.NewStorage(&client.Config{
|
||||
AllowedTags: nil,
|
||||
})
|
||||
|
||||
t.Run("populate_storage_with_first_clients", func(t *testing.T) {
|
||||
added, removed := s.BatchUpdateBySource(defSrc, firstClients)
|
||||
require.Equal(t, len(firstClients), added)
|
||||
require.Equal(t, 0, removed)
|
||||
require.Equal(t, len(firstClients), s.SizeRuntime())
|
||||
|
||||
rc := s.ClientRuntime(cliFirstIP1)
|
||||
src, host := rc.Info()
|
||||
assert.Equal(t, defSrc, src)
|
||||
assert.Equal(t, cliFirstHost1, host)
|
||||
})
|
||||
|
||||
t.Run("update_storage", func(t *testing.T) {
|
||||
added, removed := s.BatchUpdateBySource(defSrc, updatedClients)
|
||||
require.Equal(t, len(updatedClients), added)
|
||||
require.Equal(t, len(firstClients), removed)
|
||||
require.Equal(t, len(updatedClients), s.SizeRuntime())
|
||||
|
||||
rc := s.ClientRuntime(cliUpdatedIP3)
|
||||
src, host := rc.Info()
|
||||
assert.Equal(t, defSrc, src)
|
||||
assert.Equal(t, cliUpdatedHost3, host)
|
||||
|
||||
rc = s.ClientRuntime(cliFirstIP1)
|
||||
assert.Nil(t, rc)
|
||||
})
|
||||
|
||||
t.Run("remove_all", func(t *testing.T) {
|
||||
added, removed := s.BatchUpdateBySource(defSrc, []*client.Runtime{})
|
||||
require.Equal(t, 0, added)
|
||||
require.Equal(t, len(updatedClients), removed)
|
||||
require.Equal(t, 0, s.SizeRuntime())
|
||||
})
|
||||
}
|
||||
@@ -2,11 +2,13 @@ package dhcpsvc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"log/slog"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/mapsutil"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
// Config is the configuration for the DHCP service.
|
||||
@@ -15,11 +17,15 @@ type Config struct {
|
||||
// interface identified by its name.
|
||||
Interfaces map[string]*InterfaceConfig
|
||||
|
||||
// Logger will be used to log the DHCP events.
|
||||
Logger *slog.Logger
|
||||
|
||||
// LocalDomainName is the top-level domain name to use for resolving DHCP
|
||||
// clients' hostnames.
|
||||
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 time.Duration
|
||||
@@ -38,36 +44,50 @@ type InterfaceConfig struct {
|
||||
}
|
||||
|
||||
// Validate returns an error in conf if any.
|
||||
//
|
||||
// TODO(e.burkov): Unexport and rewrite the test.
|
||||
func (conf *Config) Validate() (err error) {
|
||||
switch {
|
||||
case conf == nil:
|
||||
return errNilConfig
|
||||
case !conf.Enabled:
|
||||
return nil
|
||||
case conf.ICMPTimeout < 0:
|
||||
return newMustErr("icmp timeout", "be non-negative", conf.ICMPTimeout)
|
||||
}
|
||||
|
||||
var errs []error
|
||||
if conf.ICMPTimeout < 0 {
|
||||
err = newMustErr("icmp timeout", "be non-negative", conf.ICMPTimeout)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
err = netutil.ValidateDomainName(conf.LocalDomainName)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return 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 {
|
||||
return errNoInterfaces
|
||||
errs = append(errs, errNoInterfaces)
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
ifaces := maps.Keys(conf.Interfaces)
|
||||
slices.Sort(ifaces)
|
||||
|
||||
for _, iface := range ifaces {
|
||||
if err = conf.Interfaces[iface].validate(); err != nil {
|
||||
return fmt.Errorf("interface %q: %w", iface, err)
|
||||
mapsutil.SortedRange(conf.Interfaces, func(iface string, ic *InterfaceConfig) (ok bool) {
|
||||
err = ic.validate()
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("interface %q: %w", iface, err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return true
|
||||
})
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// validate returns an error in ic, if any.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dhcpsvc_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
|
||||
@@ -8,6 +9,8 @@ import (
|
||||
)
|
||||
|
||||
func TestConfig_Validate(t *testing.T) {
|
||||
leasesPath := filepath.Join(t.TempDir(), "leases.json")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
conf *dhcpsvc.Config
|
||||
@@ -23,7 +26,9 @@ func TestConfig_Validate(t *testing.T) {
|
||||
}, {
|
||||
name: "empty",
|
||||
conf: &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
Enabled: true,
|
||||
Interfaces: testInterfaceConf,
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
wantErrMsg: `bad domain name "": domain name is empty`,
|
||||
}, {
|
||||
@@ -31,6 +36,7 @@ func TestConfig_Validate(t *testing.T) {
|
||||
Enabled: true,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: nil,
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "no_interfaces",
|
||||
wantErrMsg: "no interfaces specified",
|
||||
@@ -39,6 +45,7 @@ func TestConfig_Validate(t *testing.T) {
|
||||
Enabled: true,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: nil,
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "no_interfaces",
|
||||
wantErrMsg: "no interfaces specified",
|
||||
@@ -49,6 +56,7 @@ func TestConfig_Validate(t *testing.T) {
|
||||
Interfaces: map[string]*dhcpsvc.InterfaceConfig{
|
||||
"eth0": nil,
|
||||
},
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "nil_interface",
|
||||
wantErrMsg: `interface "eth0": config is nil`,
|
||||
@@ -62,6 +70,7 @@ func TestConfig_Validate(t *testing.T) {
|
||||
IPv6: &dhcpsvc.IPv6Config{Enabled: false},
|
||||
},
|
||||
},
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "nil_ipv4",
|
||||
wantErrMsg: `interface "eth0": ipv4: config is nil`,
|
||||
@@ -75,6 +84,7 @@ func TestConfig_Validate(t *testing.T) {
|
||||
IPv6: nil,
|
||||
},
|
||||
},
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "nil_ipv6",
|
||||
wantErrMsg: `interface "eth0": ipv6: config is nil`,
|
||||
|
||||
195
internal/dhcpsvc/db.go
Normal file
195
internal/dhcpsvc/db.go
Normal 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
|
||||
}
|
||||
4
internal/dhcpsvc/db_internal_test.go
Normal file
4
internal/dhcpsvc/db_internal_test.go
Normal file
@@ -0,0 +1,4 @@
|
||||
package dhcpsvc
|
||||
|
||||
// DatabasePerm is the permissions for the test database file.
|
||||
const DatabasePerm = databasePerm
|
||||
@@ -11,6 +11,14 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
)
|
||||
|
||||
const (
|
||||
// keyInterface is the key for logging the network interface name.
|
||||
keyInterface = "iface"
|
||||
|
||||
// keyFamily is the key for logging the handled address family.
|
||||
keyFamily = "family"
|
||||
)
|
||||
|
||||
// Interface is a DHCP service.
|
||||
//
|
||||
// TODO(e.burkov): Separate HostByIP, MACByIP, IPByHost into a separate
|
||||
@@ -42,7 +50,7 @@ type Interface interface {
|
||||
IPByHost(host string) (ip netip.Addr)
|
||||
|
||||
// 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
|
||||
// signatures instead of cloning the whole list.
|
||||
@@ -50,21 +58,21 @@ type Interface interface {
|
||||
|
||||
// AddLease adds a new DHCP lease. l must be valid. It returns an error if
|
||||
// l already exists.
|
||||
AddLease(l *Lease) (err error)
|
||||
AddLease(ctx context.Context, l *Lease) (err error)
|
||||
|
||||
// UpdateStaticLease replaces an existing static DHCP lease. l must be
|
||||
// valid. It returns an error if the lease with the given hardware address
|
||||
// doesn't exist or if other values match another existing lease.
|
||||
UpdateStaticLease(l *Lease) (err error)
|
||||
UpdateStaticLease(ctx context.Context, l *Lease) (err error)
|
||||
|
||||
// RemoveLease removes an existing DHCP lease. l must be valid. It returns
|
||||
// an error if there is no lease equal to l.
|
||||
RemoveLease(l *Lease) (err error)
|
||||
RemoveLease(ctx context.Context, l *Lease) (err error)
|
||||
|
||||
// Reset removes all the DHCP leases.
|
||||
//
|
||||
// TODO(e.burkov): If it's really needed?
|
||||
Reset() (err error)
|
||||
Reset(ctx context.Context) (err error)
|
||||
}
|
||||
|
||||
// Empty is an [Interface] implementation that does nothing.
|
||||
@@ -101,13 +109,13 @@ func (Empty) IPByHost(_ string) (ip netip.Addr) { return netip.Addr{} }
|
||||
func (Empty) Leases() (leases []*Lease) { return nil }
|
||||
|
||||
// AddLease implements the [Interface] interface for Empty.
|
||||
func (Empty) AddLease(_ *Lease) (err error) { return nil }
|
||||
func (Empty) AddLease(_ context.Context, _ *Lease) (err error) { return nil }
|
||||
|
||||
// UpdateStaticLease implements the [Interface] interface for Empty.
|
||||
func (Empty) UpdateStaticLease(_ *Lease) (err error) { return nil }
|
||||
func (Empty) UpdateStaticLease(_ context.Context, _ *Lease) (err error) { return nil }
|
||||
|
||||
// RemoveLease implements the [Interface] interface for Empty.
|
||||
func (Empty) RemoveLease(_ *Lease) (err error) { return nil }
|
||||
func (Empty) RemoveLease(_ context.Context, _ *Lease) (err error) { return nil }
|
||||
|
||||
// Reset implements the [Interface] interface for Empty.
|
||||
func (Empty) Reset() (err error) { return nil }
|
||||
func (Empty) Reset(_ context.Context) (err error) { return nil }
|
||||
|
||||
66
internal/dhcpsvc/dhcpsvc_test.go
Normal file
66
internal/dhcpsvc/dhcpsvc_test.go
Normal 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
|
||||
}
|
||||
@@ -2,39 +2,75 @@ package dhcpsvc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"log/slog"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// netInterface is a common part of any network interface within the DHCP
|
||||
// server.
|
||||
// macKey contains hardware address as byte array of 6, 8, or 20 bytes.
|
||||
//
|
||||
// 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.
|
||||
type netInterface struct {
|
||||
// logger logs the events related to the network interface.
|
||||
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 string
|
||||
|
||||
// leases is a set of leases sorted by hardware address.
|
||||
leases []*Lease
|
||||
|
||||
// leaseTTL is the default Time-To-Live value for leases.
|
||||
leaseTTL time.Duration
|
||||
}
|
||||
|
||||
// reset clears all the slices in iface for reuse.
|
||||
func (iface *netInterface) reset() {
|
||||
iface.leases = iface.leases[:0]
|
||||
// newNetInterface creates a new netInterface with the given name, leaseTTL, and
|
||||
// logger.
|
||||
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.
|
||||
func (iface *netInterface) insertLease(l *Lease) (err error) {
|
||||
i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC)
|
||||
func (iface *netInterface) addLease(l *Lease) (err error) {
|
||||
mk := macToKey(l.HWAddr)
|
||||
_, found := iface.leases[mk]
|
||||
if found {
|
||||
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
|
||||
}
|
||||
@@ -42,12 +78,13 @@ func (iface *netInterface) insertLease(l *Lease) (err error) {
|
||||
// updateLease replaces an existing lease within iface with the given one. It
|
||||
// returns an error if there is no lease with such hardware address.
|
||||
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 {
|
||||
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
|
||||
}
|
||||
@@ -55,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
|
||||
// there is no lease equal to l.
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package dhcpsvc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
@@ -45,8 +44,3 @@ func (l *Lease) Clone() (clone *Lease) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
err = iface.insertLease(l)
|
||||
err = iface.addLease(l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -124,3 +124,18 @@ func (idx *leaseIndex) update(l *Lease, iface *netInterface) (err error) {
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
package dhcpsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"golang.org/x/exp/maps"
|
||||
"github.com/AdguardTeam/golibs/mapsutil"
|
||||
)
|
||||
|
||||
// DHCPServer is a DHCP server for both IPv4 and IPv6 address families.
|
||||
@@ -19,10 +20,20 @@ type DHCPServer struct {
|
||||
// information about its clients.
|
||||
enabled *atomic.Bool
|
||||
|
||||
// logger logs common DHCP events.
|
||||
logger *slog.Logger
|
||||
|
||||
// localTLD is the top-level domain name to use for resolving DHCP clients'
|
||||
// hostnames.
|
||||
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 *sync.RWMutex
|
||||
|
||||
@@ -30,10 +41,10 @@ type DHCPServer struct {
|
||||
leases *leaseIndex
|
||||
|
||||
// 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 netInterfacesV6
|
||||
interfaces6 dhcpInterfacesV6
|
||||
|
||||
// icmpTimeout is the timeout for checking another DHCP server's presence.
|
||||
icmpTimeout time.Duration
|
||||
@@ -43,36 +54,19 @@ type DHCPServer struct {
|
||||
// error if the given configuration can't be used.
|
||||
//
|
||||
// TODO(e.burkov): Use.
|
||||
func New(conf *Config) (srv *DHCPServer, err error) {
|
||||
func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) {
|
||||
l := conf.Logger
|
||||
if !conf.Enabled {
|
||||
l.DebugContext(ctx, "disabled")
|
||||
|
||||
// TODO(e.burkov): Perhaps return [Empty]?
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Add validations scoped to the network interfaces set.
|
||||
ifaces4 := make(netInterfacesV4, 0, len(conf.Interfaces))
|
||||
ifaces6 := make(netInterfacesV6, 0, len(conf.Interfaces))
|
||||
|
||||
ifaceNames := maps.Keys(conf.Interfaces)
|
||||
slices.Sort(ifaceNames)
|
||||
|
||||
var i4 *netInterfaceV4
|
||||
var i6 *netInterfaceV6
|
||||
|
||||
for _, ifaceName := range ifaceNames {
|
||||
iface := conf.Interfaces[ifaceName]
|
||||
|
||||
i4, err = newNetInterfaceV4(ifaceName, iface.IPv4)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("interface %q: ipv4: %w", ifaceName, err)
|
||||
} else if i4 != nil {
|
||||
ifaces4 = append(ifaces4, i4)
|
||||
}
|
||||
|
||||
i6 = newNetInterfaceV6(ifaceName, iface.IPv6)
|
||||
if i6 != nil {
|
||||
ifaces6 = append(ifaces6, i6)
|
||||
}
|
||||
ifaces4, ifaces6, err := newInterfaces(ctx, l, conf.Interfaces)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
enabled := &atomic.Bool{}
|
||||
@@ -80,19 +74,62 @@ func New(conf *Config) (srv *DHCPServer, err error) {
|
||||
|
||||
srv = &DHCPServer{
|
||||
enabled: enabled,
|
||||
logger: l,
|
||||
localTLD: conf.LocalDomainName,
|
||||
leasesMu: &sync.RWMutex{},
|
||||
leases: newLeaseIndex(),
|
||||
interfaces4: ifaces4,
|
||||
interfaces6: ifaces6,
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
//
|
||||
// TODO(e.burkov): Uncomment when the [Interface] interface is implemented.
|
||||
@@ -108,16 +145,11 @@ func (srv *DHCPServer) Leases() (leases []*Lease) {
|
||||
srv.leasesMu.RLock()
|
||||
defer srv.leasesMu.RUnlock()
|
||||
|
||||
for _, iface := range srv.interfaces4 {
|
||||
for _, lease := range iface.leases {
|
||||
leases = append(leases, lease.Clone())
|
||||
}
|
||||
}
|
||||
for _, iface := range srv.interfaces6 {
|
||||
for _, lease := range iface.leases {
|
||||
leases = append(leases, lease.Clone())
|
||||
}
|
||||
}
|
||||
srv.leases.rangeLeases(func(l *Lease) (cont bool) {
|
||||
leases = append(leases, l.Clone())
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return leases
|
||||
}
|
||||
@@ -159,72 +191,147 @@ func (srv *DHCPServer) IPByHost(host string) (ip netip.Addr) {
|
||||
}
|
||||
|
||||
// Reset implements the [Interface] interface for *DHCPServer.
|
||||
func (srv *DHCPServer) Reset() (err error) {
|
||||
func (srv *DHCPServer) Reset(ctx context.Context) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "resetting leases: %w") }()
|
||||
|
||||
srv.leasesMu.Lock()
|
||||
defer srv.leasesMu.Unlock()
|
||||
|
||||
for _, iface := range srv.interfaces4 {
|
||||
iface.reset()
|
||||
srv.resetLeases()
|
||||
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")
|
||||
|
||||
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.
|
||||
func (srv *DHCPServer) AddLease(l *Lease) (err error) {
|
||||
func (srv *DHCPServer) AddLease(ctx context.Context, l *Lease) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "adding lease: %w") }()
|
||||
|
||||
addr := l.IP
|
||||
iface, err := srv.ifaceForAddr(addr)
|
||||
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
|
||||
}
|
||||
|
||||
srv.leasesMu.Lock()
|
||||
defer srv.leasesMu.Unlock()
|
||||
|
||||
return srv.leases.add(l, iface)
|
||||
err = srv.leases.add(l, iface)
|
||||
if err != nil {
|
||||
// Don't wrap the error since there is already an annotation deferred.
|
||||
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(
|
||||
ctx, "added lease",
|
||||
"hostname", l.Hostname,
|
||||
"ip", l.IP,
|
||||
"mac", l.HWAddr,
|
||||
"static", l.IsStatic,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateStaticLease implements the [Interface] interface for *DHCPServer.
|
||||
//
|
||||
// TODO(e.burkov): Support moving leases between interfaces.
|
||||
func (srv *DHCPServer) UpdateStaticLease(l *Lease) (err error) {
|
||||
func (srv *DHCPServer) UpdateStaticLease(ctx context.Context, l *Lease) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "updating static lease: %w") }()
|
||||
|
||||
addr := l.IP
|
||||
iface, err := srv.ifaceForAddr(addr)
|
||||
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
|
||||
}
|
||||
|
||||
srv.leasesMu.Lock()
|
||||
defer srv.leasesMu.Unlock()
|
||||
|
||||
return srv.leases.update(l, iface)
|
||||
}
|
||||
|
||||
// RemoveLease implements the [Interface] interface for *DHCPServer.
|
||||
func (srv *DHCPServer) RemoveLease(l *Lease) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "removing lease: %w") }()
|
||||
|
||||
addr := l.IP
|
||||
iface, err := srv.ifaceForAddr(addr)
|
||||
err = srv.leases.update(l, iface)
|
||||
if err != nil {
|
||||
// Don't wrap the error since there is already an annotation deferred.
|
||||
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(
|
||||
ctx, "updated lease",
|
||||
"hostname", l.Hostname,
|
||||
"ip", l.IP,
|
||||
"mac", l.HWAddr,
|
||||
"static", l.IsStatic,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveLease implements the [Interface] interface for *DHCPServer.
|
||||
func (srv *DHCPServer) RemoveLease(ctx context.Context, l *Lease) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "removing lease: %w") }()
|
||||
|
||||
addr := l.IP
|
||||
iface, err := srv.ifaceForAddr(addr)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's already informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
srv.leasesMu.Lock()
|
||||
defer srv.leasesMu.Unlock()
|
||||
|
||||
return srv.leases.remove(l, iface)
|
||||
err = srv.leases.remove(l, iface)
|
||||
if err != nil {
|
||||
// Don't wrap the error since there is already an annotation deferred.
|
||||
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(
|
||||
ctx, "removed lease",
|
||||
"hostname", l.Hostname,
|
||||
"ip", l.IP,
|
||||
"mac", l.HWAddr,
|
||||
"static", l.IsStatic,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ifaceForAddr returns the handled network interface for the given IP address,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package dhcpsvc_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"io/fs"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -13,53 +16,26 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testLocalTLD is a common local TLD for tests.
|
||||
const testLocalTLD = "local"
|
||||
// testdata is a filesystem containing data for tests.
|
||||
var testdata = os.DirFS("testdata")
|
||||
|
||||
// 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
// newTempDB copies the leases database file located in the testdata FS, under
|
||||
// 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()
|
||||
|
||||
// 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)
|
||||
const filename = "leases.json"
|
||||
|
||||
return mac
|
||||
data, err := fs.ReadFile(testdata, path.Join(tb.Name(), filename))
|
||||
require.NoError(tb, err)
|
||||
|
||||
dst = filepath.Join(tb.TempDir(), filename)
|
||||
|
||||
err = os.WriteFile(dst, data, dhcpsvc.DatabasePerm)
|
||||
require.NoError(tb, err)
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
@@ -96,6 +72,8 @@ func TestNew(t *testing.T) {
|
||||
RASLAACOnly: true,
|
||||
}
|
||||
|
||||
leasesPath := filepath.Join(t.TempDir(), "leases.json")
|
||||
|
||||
testCases := []struct {
|
||||
conf *dhcpsvc.Config
|
||||
name string
|
||||
@@ -103,6 +81,7 @@ func TestNew(t *testing.T) {
|
||||
}{{
|
||||
conf: &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
Logger: discardLog,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: map[string]*dhcpsvc.InterfaceConfig{
|
||||
"eth0": {
|
||||
@@ -110,12 +89,14 @@ func TestNew(t *testing.T) {
|
||||
IPv6: validIPv6Conf,
|
||||
},
|
||||
},
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "valid",
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
conf: &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
Logger: discardLog,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: map[string]*dhcpsvc.InterfaceConfig{
|
||||
"eth0": {
|
||||
@@ -123,12 +104,14 @@ func TestNew(t *testing.T) {
|
||||
IPv6: &dhcpsvc.IPv6Config{Enabled: false},
|
||||
},
|
||||
},
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
name: "disabled_interfaces",
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
conf: &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
Logger: discardLog,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: map[string]*dhcpsvc.InterfaceConfig{
|
||||
"eth0": {
|
||||
@@ -136,13 +119,15 @@ func TestNew(t *testing.T) {
|
||||
IPv6: validIPv6Conf,
|
||||
},
|
||||
},
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
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`,
|
||||
}, {
|
||||
conf: &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
Logger: discardLog,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: map[string]*dhcpsvc.InterfaceConfig{
|
||||
"eth0": {
|
||||
@@ -150,46 +135,56 @@ func TestNew(t *testing.T) {
|
||||
IPv6: validIPv6Conf,
|
||||
},
|
||||
},
|
||||
DBFilePath: leasesPath,
|
||||
},
|
||||
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`,
|
||||
}}
|
||||
|
||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := dhcpsvc.New(tc.conf)
|
||||
_, err := dhcpsvc.New(ctx, tc.conf)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDHCPServer_AddLease(t *testing.T) {
|
||||
srv, err := dhcpsvc.New(&dhcpsvc.Config{
|
||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||
|
||||
leasesPath := filepath.Join(t.TempDir(), "leases.json")
|
||||
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
Logger: discardLog,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: testInterfaceConf,
|
||||
DBFilePath: leasesPath,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
const (
|
||||
host1 = "host1"
|
||||
host2 = "host2"
|
||||
host3 = "host3"
|
||||
existHost = "host1"
|
||||
newHost = "host2"
|
||||
ipv6Host = "host3"
|
||||
)
|
||||
|
||||
ip1 := netip.MustParseAddr("192.168.0.2")
|
||||
ip2 := netip.MustParseAddr("192.168.0.3")
|
||||
ip3 := netip.MustParseAddr("2001:db8::2")
|
||||
var (
|
||||
existIP = netip.MustParseAddr("192.168.0.2")
|
||||
newIP = netip.MustParseAddr("192.168.0.3")
|
||||
newIPv6 = netip.MustParseAddr("2001:db8::2")
|
||||
|
||||
mac1 := mustParseMAC(t, "01:02:03:04:05:06")
|
||||
mac2 := mustParseMAC(t, "06:05:04:03:02:01")
|
||||
mac3 := mustParseMAC(t, "02:03:04:05:06:07")
|
||||
existMAC = mustParseMAC(t, "01:02:03:04:05:06")
|
||||
newMAC = mustParseMAC(t, "06:05:04:03:02:01")
|
||||
ipv6MAC = mustParseMAC(t, "02:03:04:05:06:07")
|
||||
)
|
||||
|
||||
require.NoError(t, srv.AddLease(&dhcpsvc.Lease{
|
||||
Hostname: host1,
|
||||
IP: ip1,
|
||||
HWAddr: mac1,
|
||||
require.NoError(t, srv.AddLease(ctx, &dhcpsvc.Lease{
|
||||
Hostname: existHost,
|
||||
IP: existIP,
|
||||
HWAddr: existMAC,
|
||||
IsStatic: true,
|
||||
}))
|
||||
|
||||
@@ -200,77 +195,85 @@ func TestDHCPServer_AddLease(t *testing.T) {
|
||||
}{{
|
||||
name: "outside_range",
|
||||
lease: &dhcpsvc.Lease{
|
||||
Hostname: host2,
|
||||
Hostname: newHost,
|
||||
IP: netip.MustParseAddr("1.2.3.4"),
|
||||
HWAddr: mac2,
|
||||
HWAddr: newMAC,
|
||||
},
|
||||
wantErrMsg: "adding lease: no interface for ip 1.2.3.4",
|
||||
}, {
|
||||
name: "duplicate_ip",
|
||||
lease: &dhcpsvc.Lease{
|
||||
Hostname: host2,
|
||||
IP: ip1,
|
||||
HWAddr: mac2,
|
||||
Hostname: newHost,
|
||||
IP: existIP,
|
||||
HWAddr: newMAC,
|
||||
},
|
||||
wantErrMsg: "adding lease: lease for ip " + ip1.String() +
|
||||
wantErrMsg: "adding lease: lease for ip " + existIP.String() +
|
||||
" already exists",
|
||||
}, {
|
||||
name: "duplicate_hostname",
|
||||
lease: &dhcpsvc.Lease{
|
||||
Hostname: host1,
|
||||
IP: ip2,
|
||||
HWAddr: mac2,
|
||||
Hostname: existHost,
|
||||
IP: newIP,
|
||||
HWAddr: newMAC,
|
||||
},
|
||||
wantErrMsg: "adding lease: lease for hostname " + host1 +
|
||||
wantErrMsg: "adding lease: lease for hostname " + existHost +
|
||||
" already exists",
|
||||
}, {
|
||||
name: "duplicate_hostname_case",
|
||||
lease: &dhcpsvc.Lease{
|
||||
Hostname: strings.ToUpper(host1),
|
||||
IP: ip2,
|
||||
HWAddr: mac2,
|
||||
Hostname: strings.ToUpper(existHost),
|
||||
IP: newIP,
|
||||
HWAddr: newMAC,
|
||||
},
|
||||
wantErrMsg: "adding lease: lease for hostname " +
|
||||
strings.ToUpper(host1) + " already exists",
|
||||
strings.ToUpper(existHost) + " already exists",
|
||||
}, {
|
||||
name: "duplicate_mac",
|
||||
lease: &dhcpsvc.Lease{
|
||||
Hostname: host2,
|
||||
IP: ip2,
|
||||
HWAddr: mac1,
|
||||
Hostname: newHost,
|
||||
IP: newIP,
|
||||
HWAddr: existMAC,
|
||||
},
|
||||
wantErrMsg: "adding lease: lease for mac " + mac1.String() +
|
||||
wantErrMsg: "adding lease: lease for mac " + existMAC.String() +
|
||||
" already exists",
|
||||
}, {
|
||||
name: "valid",
|
||||
lease: &dhcpsvc.Lease{
|
||||
Hostname: host2,
|
||||
IP: ip2,
|
||||
HWAddr: mac2,
|
||||
Hostname: newHost,
|
||||
IP: newIP,
|
||||
HWAddr: newMAC,
|
||||
},
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "valid_v6",
|
||||
lease: &dhcpsvc.Lease{
|
||||
Hostname: host3,
|
||||
IP: ip3,
|
||||
HWAddr: mac3,
|
||||
Hostname: ipv6Host,
|
||||
IP: newIPv6,
|
||||
HWAddr: ipv6MAC,
|
||||
},
|
||||
wantErrMsg: "",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.AddLease(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) {
|
||||
srv, err := dhcpsvc.New(&dhcpsvc.Config{
|
||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||
|
||||
leasesPath := newTempDB(t)
|
||||
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
Logger: discardLog,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: testInterfaceConf,
|
||||
DBFilePath: leasesPath,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -282,46 +285,23 @@ func TestDHCPServer_index(t *testing.T) {
|
||||
host5 = "host5"
|
||||
)
|
||||
|
||||
ip1 := netip.MustParseAddr("192.168.0.2")
|
||||
ip2 := netip.MustParseAddr("192.168.0.3")
|
||||
ip3 := netip.MustParseAddr("172.16.0.3")
|
||||
ip4 := netip.MustParseAddr("172.16.0.4")
|
||||
var (
|
||||
ip1 = netip.MustParseAddr("192.168.0.2")
|
||||
ip2 = netip.MustParseAddr("192.168.0.3")
|
||||
ip3 = netip.MustParseAddr("172.16.0.3")
|
||||
ip4 = netip.MustParseAddr("172.16.0.4")
|
||||
|
||||
mac1 := mustParseMAC(t, "01:02:03:04:05:06")
|
||||
mac2 := mustParseMAC(t, "06:05:04:03:02:01")
|
||||
mac3 := mustParseMAC(t, "02:03:04:05:06:07")
|
||||
|
||||
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(l))
|
||||
}
|
||||
mac1 = mustParseMAC(t, "01:02:03:04:05:06")
|
||||
mac2 = mustParseMAC(t, "06:05:04:03:02:01")
|
||||
mac3 = mustParseMAC(t, "02:03:04:05:06:07")
|
||||
)
|
||||
|
||||
t.Run("ip_idx", func(t *testing.T) {
|
||||
assert.Equal(t, ip1, srv.IPByHost(host1))
|
||||
assert.Equal(t, ip2, srv.IPByHost(host2))
|
||||
assert.Equal(t, ip3, srv.IPByHost(host3))
|
||||
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) {
|
||||
@@ -329,7 +309,7 @@ func TestDHCPServer_index(t *testing.T) {
|
||||
assert.Equal(t, host2, srv.HostByIP(ip2))
|
||||
assert.Equal(t, host3, srv.HostByIP(ip3))
|
||||
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) {
|
||||
@@ -337,15 +317,20 @@ func TestDHCPServer_index(t *testing.T) {
|
||||
assert.Equal(t, mac2, srv.MACByIP(ip2))
|
||||
assert.Equal(t, mac3, srv.MACByIP(ip3))
|
||||
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) {
|
||||
srv, err := dhcpsvc.New(&dhcpsvc.Config{
|
||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||
|
||||
leasesPath := newTempDB(t)
|
||||
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
Logger: discardLog,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: testInterfaceConf,
|
||||
DBFilePath: leasesPath,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -358,36 +343,16 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
|
||||
host6 = "host6"
|
||||
)
|
||||
|
||||
ip1 := netip.MustParseAddr("192.168.0.2")
|
||||
ip2 := netip.MustParseAddr("192.168.0.3")
|
||||
ip3 := netip.MustParseAddr("192.168.0.4")
|
||||
ip4 := netip.MustParseAddr("2001:db8::2")
|
||||
ip5 := netip.MustParseAddr("2001:db8::3")
|
||||
var (
|
||||
ip1 = netip.MustParseAddr("192.168.0.2")
|
||||
ip2 = netip.MustParseAddr("192.168.0.3")
|
||||
ip3 = netip.MustParseAddr("192.168.0.4")
|
||||
ip4 = netip.MustParseAddr("2001:db8::3")
|
||||
|
||||
mac1 := mustParseMAC(t, "01:02:03:04:05:06")
|
||||
mac2 := mustParseMAC(t, "01:02:03:04:05:07")
|
||||
mac3 := mustParseMAC(t, "06:05:04:03:02:01")
|
||||
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(l))
|
||||
}
|
||||
mac1 = mustParseMAC(t, "01:02:03:04:05:06")
|
||||
mac2 = mustParseMAC(t, "06:05:04:03:02:01")
|
||||
mac3 = mustParseMAC(t, "06:05:04:03:02:02")
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -406,9 +371,9 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
|
||||
lease: &dhcpsvc.Lease{
|
||||
Hostname: host3,
|
||||
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",
|
||||
lease: &dhcpsvc.Lease{
|
||||
@@ -448,24 +413,31 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
|
||||
name: "valid_v6",
|
||||
lease: &dhcpsvc.Lease{
|
||||
Hostname: host6,
|
||||
IP: ip5,
|
||||
HWAddr: mac4,
|
||||
IP: ip4,
|
||||
HWAddr: mac3,
|
||||
},
|
||||
wantErrMsg: "",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.UpdateStaticLease(tc.lease))
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.UpdateStaticLease(ctx, tc.lease))
|
||||
})
|
||||
}
|
||||
|
||||
assert.FileExists(t, leasesPath)
|
||||
}
|
||||
|
||||
func TestDHCPServer_RemoveLease(t *testing.T) {
|
||||
srv, err := dhcpsvc.New(&dhcpsvc.Config{
|
||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||
|
||||
leasesPath := newTempDB(t)
|
||||
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
Logger: discardLog,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: testInterfaceConf,
|
||||
DBFilePath: leasesPath,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -475,28 +447,15 @@ func TestDHCPServer_RemoveLease(t *testing.T) {
|
||||
host3 = "host3"
|
||||
)
|
||||
|
||||
ip1 := netip.MustParseAddr("192.168.0.2")
|
||||
ip2 := netip.MustParseAddr("192.168.0.3")
|
||||
ip3 := netip.MustParseAddr("2001:db8::2")
|
||||
var (
|
||||
existIP = netip.MustParseAddr("192.168.0.2")
|
||||
newIP = netip.MustParseAddr("192.168.0.3")
|
||||
newIPv6 = netip.MustParseAddr("2001:db8::2")
|
||||
|
||||
mac1 := mustParseMAC(t, "01:02:03:04:05:06")
|
||||
mac2 := mustParseMAC(t, "02:03:04:05:06:07")
|
||||
mac3 := 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(l))
|
||||
}
|
||||
existMAC = mustParseMAC(t, "01:02:03:04:05:06")
|
||||
newMAC = mustParseMAC(t, "02:03:04:05:06:07")
|
||||
ipv6MAC = mustParseMAC(t, "06:05:04:03:02:01")
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -506,90 +465,108 @@ func TestDHCPServer_RemoveLease(t *testing.T) {
|
||||
name: "not_found_mac",
|
||||
lease: &dhcpsvc.Lease{
|
||||
Hostname: host1,
|
||||
IP: ip1,
|
||||
HWAddr: mac2,
|
||||
IP: existIP,
|
||||
HWAddr: newMAC,
|
||||
},
|
||||
wantErrMsg: "removing lease: no lease for mac " + mac2.String(),
|
||||
wantErrMsg: "removing lease: no lease for mac " + newMAC.String(),
|
||||
}, {
|
||||
name: "not_found_ip",
|
||||
lease: &dhcpsvc.Lease{
|
||||
Hostname: host1,
|
||||
IP: ip2,
|
||||
HWAddr: mac1,
|
||||
IP: newIP,
|
||||
HWAddr: existMAC,
|
||||
},
|
||||
wantErrMsg: "removing lease: no lease for ip " + ip2.String(),
|
||||
wantErrMsg: "removing lease: no lease for ip " + newIP.String(),
|
||||
}, {
|
||||
name: "not_found_host",
|
||||
lease: &dhcpsvc.Lease{
|
||||
Hostname: host2,
|
||||
IP: ip1,
|
||||
HWAddr: mac1,
|
||||
IP: existIP,
|
||||
HWAddr: existMAC,
|
||||
},
|
||||
wantErrMsg: "removing lease: no lease for hostname " + host2,
|
||||
}, {
|
||||
name: "valid",
|
||||
lease: &dhcpsvc.Lease{
|
||||
Hostname: host1,
|
||||
IP: ip1,
|
||||
HWAddr: mac1,
|
||||
IP: existIP,
|
||||
HWAddr: existMAC,
|
||||
},
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "valid_v6",
|
||||
lease: &dhcpsvc.Lease{
|
||||
Hostname: host3,
|
||||
IP: ip3,
|
||||
HWAddr: mac3,
|
||||
IP: newIPv6,
|
||||
HWAddr: ipv6MAC,
|
||||
},
|
||||
wantErrMsg: "",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.RemoveLease(tc.lease))
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.RemoveLease(ctx, tc.lease))
|
||||
})
|
||||
}
|
||||
|
||||
assert.FileExists(t, leasesPath)
|
||||
assert.Empty(t, srv.Leases())
|
||||
}
|
||||
|
||||
func TestDHCPServer_Reset(t *testing.T) {
|
||||
srv, err := dhcpsvc.New(&dhcpsvc.Config{
|
||||
leasesPath := newTempDB(t)
|
||||
conf := &dhcpsvc.Config{
|
||||
Enabled: true,
|
||||
Logger: discardLog,
|
||||
LocalDomainName: testLocalTLD,
|
||||
Interfaces: testInterfaceConf,
|
||||
})
|
||||
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(l))
|
||||
DBFilePath: leasesPath,
|
||||
}
|
||||
|
||||
require.Len(t, srv.Leases(), len(leases))
|
||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||
srv, err := dhcpsvc.New(ctx, conf)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, srv.Reset())
|
||||
const leasesNum = 4
|
||||
|
||||
require.Len(t, srv.Leases(), leasesNum)
|
||||
|
||||
require.NoError(t, srv.Reset(ctx))
|
||||
|
||||
assert.FileExists(t, leasesPath)
|
||||
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())
|
||||
}
|
||||
|
||||
19
internal/dhcpsvc/testdata/TestDHCPServer_RemoveLease/leases.json
vendored
Normal file
19
internal/dhcpsvc/testdata/TestDHCPServer_RemoveLease/leases.json
vendored
Normal 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
|
||||
}
|
||||
33
internal/dhcpsvc/testdata/TestDHCPServer_Reset/leases.json
vendored
Normal file
33
internal/dhcpsvc/testdata/TestDHCPServer_Reset/leases.json
vendored
Normal 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
|
||||
}
|
||||
26
internal/dhcpsvc/testdata/TestDHCPServer_UpdateStaticLease/leases.json
vendored
Normal file
26
internal/dhcpsvc/testdata/TestDHCPServer_UpdateStaticLease/leases.json
vendored
Normal 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
|
||||
}
|
||||
33
internal/dhcpsvc/testdata/TestDHCPServer_index/leases.json
vendored
Normal file
33
internal/dhcpsvc/testdata/TestDHCPServer_index/leases.json
vendored
Normal 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
|
||||
}
|
||||
15
internal/dhcpsvc/testdata/TestServer_Leases/leases.json
vendored
Normal file
15
internal/dhcpsvc/testdata/TestServer_Leases/leases.json
vendored
Normal 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
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
package dhcpsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
@@ -43,25 +45,130 @@ type IPv4Config struct {
|
||||
}
|
||||
|
||||
// validate returns an error in conf if any.
|
||||
func (conf *IPv4Config) validate() (err error) {
|
||||
switch {
|
||||
case conf == nil:
|
||||
func (c *IPv4Config) validate() (err error) {
|
||||
if c == nil {
|
||||
return errNilConfig
|
||||
case !conf.Enabled:
|
||||
return nil
|
||||
case !conf.GatewayIP.Is4():
|
||||
return newMustErr("gateway ip", "be a valid ipv4", conf.GatewayIP)
|
||||
case !conf.SubnetMask.Is4():
|
||||
return newMustErr("subnet mask", "be a valid ipv4 cidr mask", conf.SubnetMask)
|
||||
case !conf.RangeStart.Is4():
|
||||
return newMustErr("range start", "be a valid ipv4", conf.RangeStart)
|
||||
case !conf.RangeEnd.Is4():
|
||||
return newMustErr("range end", "be a valid ipv4", conf.RangeEnd)
|
||||
case conf.LeaseDuration <= 0:
|
||||
return newMustErr("lease duration", "be less than %d", conf.LeaseDuration)
|
||||
default:
|
||||
} else if !c.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errs []error
|
||||
|
||||
if !c.GatewayIP.Is4() {
|
||||
err = newMustErr("gateway ip", "be a valid ipv4", c.GatewayIP)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if !c.SubnetMask.Is4() {
|
||||
err = newMustErr("subnet mask", "be a valid ipv4 cidr mask", c.SubnetMask)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if !c.RangeStart.Is4() {
|
||||
err = newMustErr("range start", "be a valid ipv4", c.RangeStart)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if !c.RangeEnd.Is4() {
|
||||
err = newMustErr("range end", "be a valid ipv4", c.RangeEnd)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if c.LeaseDuration <= 0 {
|
||||
err = newMustErr("icmp timeout", "be positive", c.LeaseDuration)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// dhcpInterfaceV4 is a DHCP interface for IPv4 address family.
|
||||
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 netip.Addr
|
||||
|
||||
// subnet is the network subnet.
|
||||
subnet netip.Prefix
|
||||
|
||||
// addrSpace is the IPv4 address space allocated for leasing.
|
||||
addrSpace ipRange
|
||||
|
||||
// implicitOpts are the options listed in Appendix A of RFC 2131 and
|
||||
// initialized with default values. It must not have intersections with
|
||||
// explicitOpts.
|
||||
implicitOpts layers.DHCPOptions
|
||||
|
||||
// explicitOpts are the user-configured options. It must not have
|
||||
// intersections with implicitOpts.
|
||||
explicitOpts layers.DHCPOptions
|
||||
}
|
||||
|
||||
// newDHCPInterfaceV4 creates a new DHCP interface for IPv4 address family with
|
||||
// the given configuration. It returns an error if the given configuration
|
||||
// can't be used.
|
||||
func newDHCPInterfaceV4(
|
||||
ctx context.Context,
|
||||
l *slog.Logger,
|
||||
name string,
|
||||
conf *IPv4Config,
|
||||
) (i *dhcpInterfaceV4, err error) {
|
||||
l = l.With(
|
||||
keyInterface, name,
|
||||
keyFamily, netutil.AddrFamilyIPv4,
|
||||
)
|
||||
|
||||
if !conf.Enabled {
|
||||
l.DebugContext(ctx, "disabled")
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
maskLen, _ := net.IPMask(conf.SubnetMask.AsSlice()).Size()
|
||||
subnet := netip.PrefixFrom(conf.GatewayIP, maskLen)
|
||||
|
||||
switch {
|
||||
case !subnet.Contains(conf.RangeStart):
|
||||
return nil, fmt.Errorf("range start %s is not within %s", conf.RangeStart, subnet)
|
||||
case !subnet.Contains(conf.RangeEnd):
|
||||
return nil, fmt.Errorf("range end %s is not within %s", conf.RangeEnd, subnet)
|
||||
}
|
||||
|
||||
addrSpace, err := newIPRange(conf.RangeStart, conf.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if addrSpace.contains(conf.GatewayIP) {
|
||||
return nil, fmt.Errorf("gateway ip %s in the ip range %s", conf.GatewayIP, addrSpace)
|
||||
}
|
||||
|
||||
i = &dhcpInterfaceV4{
|
||||
gateway: conf.GatewayIP,
|
||||
subnet: subnet,
|
||||
addrSpace: addrSpace,
|
||||
common: newNetInterface(name, l, conf.LeaseDuration),
|
||||
}
|
||||
i.implicitOpts, i.explicitOpts = conf.options(ctx, l)
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// dhcpInterfacesV4 is a slice of network interfaces of IPv4 address family.
|
||||
type dhcpInterfacesV4 []*dhcpInterfaceV4
|
||||
|
||||
// find returns the first network interface within ifaces containing ip. It
|
||||
// returns false if there is no such interface.
|
||||
func (ifaces dhcpInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok bool) {
|
||||
i := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV4) (contains bool) {
|
||||
return iface.subnet.Contains(ip)
|
||||
})
|
||||
if i < 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return ifaces[i].common, true
|
||||
}
|
||||
|
||||
// options returns the implicit and explicit options for the interface. The two
|
||||
@@ -69,14 +176,14 @@ func (conf *IPv4Config) validate() (err error) {
|
||||
// values.
|
||||
//
|
||||
// TODO(e.burkov): DRY with the IPv6 version.
|
||||
func (conf *IPv4Config) options() (implicit, explicit layers.DHCPOptions) {
|
||||
func (c *IPv4Config) options(ctx context.Context, l *slog.Logger) (imp, exp layers.DHCPOptions) {
|
||||
// Set default values of host configuration parameters listed in Appendix A
|
||||
// of RFC-2131.
|
||||
implicit = layers.DHCPOptions{
|
||||
imp = layers.DHCPOptions{
|
||||
// Values From Configuration
|
||||
|
||||
layers.NewDHCPOption(layers.DHCPOptSubnetMask, conf.SubnetMask.AsSlice()),
|
||||
layers.NewDHCPOption(layers.DHCPOptRouter, conf.GatewayIP.AsSlice()),
|
||||
layers.NewDHCPOption(layers.DHCPOptSubnetMask, c.SubnetMask.AsSlice()),
|
||||
layers.NewDHCPOption(layers.DHCPOptRouter, c.GatewayIP.AsSlice()),
|
||||
|
||||
// IP-Layer Per Host
|
||||
|
||||
@@ -228,110 +335,29 @@ func (conf *IPv4Config) options() (implicit, explicit layers.DHCPOptions) {
|
||||
// See https://datatracker.ietf.org/doc/html/rfc1122#section-4.2.3.6.
|
||||
layers.NewDHCPOption(layers.DHCPOptTCPKeepAliveGarbage, []byte{0x1}),
|
||||
}
|
||||
slices.SortFunc(implicit, compareV4OptionCodes)
|
||||
slices.SortFunc(imp, compareV4OptionCodes)
|
||||
|
||||
// Set values for explicitly configured options.
|
||||
for _, exp := range conf.Options {
|
||||
i, found := slices.BinarySearchFunc(implicit, exp, compareV4OptionCodes)
|
||||
for _, o := range c.Options {
|
||||
i, found := slices.BinarySearchFunc(imp, o, compareV4OptionCodes)
|
||||
if found {
|
||||
implicit = slices.Delete(implicit, i, i+1)
|
||||
imp = slices.Delete(imp, i, i+1)
|
||||
}
|
||||
|
||||
i, found = slices.BinarySearchFunc(explicit, exp, compareV4OptionCodes)
|
||||
if exp.Length > 0 {
|
||||
explicit = slices.Insert(explicit, i, exp)
|
||||
i, found = slices.BinarySearchFunc(exp, o, compareV4OptionCodes)
|
||||
if o.Length > 0 {
|
||||
exp = slices.Insert(exp, i, o)
|
||||
} else if found {
|
||||
explicit = slices.Delete(explicit, i, i+1)
|
||||
exp = slices.Delete(exp, i, i+1)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("dhcpsvc: v4: implicit options: %s", implicit)
|
||||
log.Debug("dhcpsvc: v4: explicit options: %s", explicit)
|
||||
l.DebugContext(ctx, "options", "implicit", imp, "explicit", exp)
|
||||
|
||||
return implicit, explicit
|
||||
return imp, exp
|
||||
}
|
||||
|
||||
// compareV4OptionCodes compares option codes of a and b.
|
||||
func compareV4OptionCodes(a, b layers.DHCPOption) (res int) {
|
||||
return int(a.Type) - int(b.Type)
|
||||
}
|
||||
|
||||
// netInterfaceV4 is a DHCP interface for IPv4 address family.
|
||||
type netInterfaceV4 struct {
|
||||
// gateway is the IP address of the network gateway.
|
||||
gateway netip.Addr
|
||||
|
||||
// subnet is the network subnet.
|
||||
subnet netip.Prefix
|
||||
|
||||
// addrSpace is the IPv4 address space allocated for leasing.
|
||||
addrSpace ipRange
|
||||
|
||||
// implicitOpts are the options listed in Appendix A of RFC 2131 and
|
||||
// initialized with default values. It must not have intersections with
|
||||
// explicitOpts.
|
||||
implicitOpts layers.DHCPOptions
|
||||
|
||||
// explicitOpts are the user-configured options. It must not have
|
||||
// intersections with implicitOpts.
|
||||
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
|
||||
// the given configuration. It returns an error if the given configuration
|
||||
// can't be used.
|
||||
func newNetInterfaceV4(name string, conf *IPv4Config) (i *netInterfaceV4, err error) {
|
||||
if !conf.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
maskLen, _ := net.IPMask(conf.SubnetMask.AsSlice()).Size()
|
||||
subnet := netip.PrefixFrom(conf.GatewayIP, maskLen)
|
||||
|
||||
switch {
|
||||
case !subnet.Contains(conf.RangeStart):
|
||||
return nil, fmt.Errorf("range start %s is not within %s", conf.RangeStart, subnet)
|
||||
case !subnet.Contains(conf.RangeEnd):
|
||||
return nil, fmt.Errorf("range end %s is not within %s", conf.RangeEnd, subnet)
|
||||
}
|
||||
|
||||
addrSpace, err := newIPRange(conf.RangeStart, conf.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if addrSpace.contains(conf.GatewayIP) {
|
||||
return nil, fmt.Errorf("gateway ip %s in the ip range %s", conf.GatewayIP, addrSpace)
|
||||
}
|
||||
|
||||
i = &netInterfaceV4{
|
||||
gateway: conf.GatewayIP,
|
||||
subnet: subnet,
|
||||
addrSpace: addrSpace,
|
||||
netInterface: netInterface{
|
||||
name: name,
|
||||
leaseTTL: conf.LeaseDuration,
|
||||
},
|
||||
}
|
||||
i.implicitOpts, i.explicitOpts = conf.options()
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// netInterfacesV4 is a slice of network interfaces of IPv4 address family.
|
||||
type netInterfacesV4 []*netInterfaceV4
|
||||
|
||||
// find returns the first network interface within ifaces containing ip. It
|
||||
// returns false if there is no such interface.
|
||||
func (ifaces netInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok bool) {
|
||||
i := slices.IndexFunc(ifaces, func(iface *netInterfaceV4) (contains bool) {
|
||||
return iface.subnet.Contains(ip)
|
||||
})
|
||||
if i < 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return &ifaces[i].netInterface, true
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ package dhcpsvc
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -75,9 +78,12 @@ func TestIPv4Config_Options(t *testing.T) {
|
||||
wantExplicit: layers.DHCPOptions{opt1},
|
||||
}}
|
||||
|
||||
ctx := testutil.ContextWithTimeout(t, time.Second)
|
||||
l := slogutil.NewDiscardLogger()
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
imp, exp := tc.conf.options()
|
||||
imp, exp := tc.conf.options(ctx, l)
|
||||
assert.Equal(t, tc.wantExplicit, exp)
|
||||
|
||||
for c := range exp {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package dhcpsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
@@ -38,56 +40,34 @@ type IPv6Config struct {
|
||||
}
|
||||
|
||||
// validate returns an error in conf if any.
|
||||
func (conf *IPv6Config) validate() (err error) {
|
||||
switch {
|
||||
case conf == nil:
|
||||
func (c *IPv6Config) validate() (err error) {
|
||||
if c == nil {
|
||||
return errNilConfig
|
||||
case !conf.Enabled:
|
||||
return nil
|
||||
case !conf.RangeStart.Is6():
|
||||
return fmt.Errorf("range start %s should be a valid ipv6", conf.RangeStart)
|
||||
case conf.LeaseDuration <= 0:
|
||||
return fmt.Errorf("lease duration %s must be positive", conf.LeaseDuration)
|
||||
default:
|
||||
} else if !c.Enabled {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// options returns the implicit and explicit options for the interface. The two
|
||||
// lists are disjoint and the implicit options are initialized with default
|
||||
// values.
|
||||
//
|
||||
// TODO(e.burkov): Add implicit options according to RFC.
|
||||
func (conf *IPv6Config) options() (implicit, explicit layers.DHCPv6Options) {
|
||||
// Set default values of host configuration parameters listed in RFC 8415.
|
||||
implicit = layers.DHCPv6Options{}
|
||||
slices.SortFunc(implicit, compareV6OptionCodes)
|
||||
var errs []error
|
||||
|
||||
// Set values for explicitly configured options.
|
||||
for _, exp := range conf.Options {
|
||||
i, found := slices.BinarySearchFunc(implicit, exp, compareV6OptionCodes)
|
||||
if found {
|
||||
implicit = slices.Delete(implicit, i, i+1)
|
||||
}
|
||||
|
||||
explicit = append(explicit, exp)
|
||||
if !c.RangeStart.Is6() {
|
||||
err = fmt.Errorf("range start %s should be a valid ipv6", c.RangeStart)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
log.Debug("dhcpsvc: v6: implicit options: %s", implicit)
|
||||
log.Debug("dhcpsvc: v6: explicit options: %s", explicit)
|
||||
if c.LeaseDuration <= 0 {
|
||||
err = fmt.Errorf("lease duration %s must be positive", c.LeaseDuration)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
return implicit, explicit
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// compareV6OptionCodes compares option codes of a and b.
|
||||
func compareV6OptionCodes(a, b layers.DHCPv6Option) (res int) {
|
||||
return int(a.Code) - int(b.Code)
|
||||
}
|
||||
// dhcpInterfaceV6 is a DHCP interface for IPv6 address family.
|
||||
type dhcpInterfaceV6 struct {
|
||||
// common is the common part of any network interface within the DHCP
|
||||
// server.
|
||||
common *netInterface
|
||||
|
||||
// netInterfaceV6 is a DHCP interface for IPv6 address family.
|
||||
//
|
||||
// TODO(e.burkov): Add options.
|
||||
type netInterfaceV6 struct {
|
||||
// rangeStart is the first IP address in the range.
|
||||
rangeStart netip.Addr
|
||||
|
||||
@@ -100,10 +80,6 @@ type netInterfaceV6 struct {
|
||||
// intersections with implicitOpts.
|
||||
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
|
||||
// flags.
|
||||
raSLAACOnly bool
|
||||
@@ -112,35 +88,40 @@ type netInterfaceV6 struct {
|
||||
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.
|
||||
//
|
||||
// TODO(e.burkov): Validate properly.
|
||||
func newNetInterfaceV6(name string, conf *IPv6Config) (i *netInterfaceV6) {
|
||||
func newDHCPInterfaceV6(
|
||||
ctx context.Context,
|
||||
l *slog.Logger,
|
||||
name string,
|
||||
conf *IPv6Config,
|
||||
) (i *dhcpInterfaceV6) {
|
||||
l = l.With(keyInterface, name, keyFamily, netutil.AddrFamilyIPv6)
|
||||
if !conf.Enabled {
|
||||
l.DebugContext(ctx, "disabled")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
i = &netInterfaceV6{
|
||||
rangeStart: conf.RangeStart,
|
||||
netInterface: netInterface{
|
||||
name: name,
|
||||
leaseTTL: conf.LeaseDuration,
|
||||
},
|
||||
i = &dhcpInterfaceV6{
|
||||
rangeStart: conf.RangeStart,
|
||||
common: newNetInterface(name, l, conf.LeaseDuration),
|
||||
raSLAACOnly: conf.RASLAACOnly,
|
||||
raAllowSLAAC: conf.RAAllowSLAAC,
|
||||
}
|
||||
i.implicitOpts, i.explicitOpts = conf.options()
|
||||
i.implicitOpts, i.explicitOpts = conf.options(ctx, l)
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// netInterfacesV4 is a slice of network interfaces of IPv4 address family.
|
||||
type netInterfacesV6 []*netInterfaceV6
|
||||
// dhcpInterfacesV6 is a slice of network interfaces of IPv6 address family.
|
||||
type dhcpInterfacesV6 []*dhcpInterfaceV6
|
||||
|
||||
// find returns the first network interface within ifaces containing ip. It
|
||||
// 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.
|
||||
//
|
||||
// TODO(e.burkov): DHCPv6 inherits the weird behavior of legacy
|
||||
@@ -149,7 +130,7 @@ func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool
|
||||
// be used instead.
|
||||
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) &&
|
||||
netip.PrefixFrom(iface.rangeStart, prefLen).Contains(ip)
|
||||
})
|
||||
@@ -157,5 +138,35 @@ func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool
|
||||
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
|
||||
// lists are disjoint and the implicit options are initialized with default
|
||||
// values.
|
||||
//
|
||||
// TODO(e.burkov): Add implicit options according to RFC.
|
||||
func (c *IPv6Config) options(ctx context.Context, l *slog.Logger) (imp, exp layers.DHCPv6Options) {
|
||||
// Set default values of host configuration parameters listed in RFC 8415.
|
||||
imp = layers.DHCPv6Options{}
|
||||
slices.SortFunc(imp, compareV6OptionCodes)
|
||||
|
||||
// Set values for explicitly configured options.
|
||||
for _, e := range c.Options {
|
||||
i, found := slices.BinarySearchFunc(imp, e, compareV6OptionCodes)
|
||||
if found {
|
||||
imp = slices.Delete(imp, i, i+1)
|
||||
}
|
||||
|
||||
exp = append(exp, e)
|
||||
}
|
||||
|
||||
l.DebugContext(ctx, "options", "implicit", imp, "explicit", exp)
|
||||
|
||||
return imp, exp
|
||||
}
|
||||
|
||||
// compareV6OptionCodes compares option codes of a and b.
|
||||
func compareV6OptionCodes(a, b layers.DHCPv6Option) (res int) {
|
||||
return int(a.Code) - int(b.Code)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -217,7 +218,8 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
}
|
||||
|
||||
srv := &Server{
|
||||
conf: ServerConfig{TLSConfig: tlsConf},
|
||||
conf: ServerConfig{TLSConfig: tlsConf},
|
||||
baseLogger: slogutil.NewDiscardLogger(),
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/AdguardTeam/golibs/container"
|
||||
"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/stringutil"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
@@ -158,7 +159,7 @@ type Config struct {
|
||||
// IpsetList is the ipset configuration that allows AdGuard Home to add IP
|
||||
// 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.
|
||||
IpsetList []string `yaml:"ipset"`
|
||||
@@ -301,6 +302,8 @@ type ServerConfig struct {
|
||||
|
||||
// UpstreamMode is a enumeration of upstream mode representations. See
|
||||
// [proxy.UpstreamModeType].
|
||||
//
|
||||
// TODO(d.kolyshev): Consider using [proxy.UpstreamMode].
|
||||
type UpstreamMode string
|
||||
|
||||
const (
|
||||
@@ -315,6 +318,7 @@ func (s *Server) newProxyConfig() (conf *proxy.Config, err error) {
|
||||
trustedPrefixes := netutil.UnembedPrefixes(srvConf.TrustedProxies)
|
||||
|
||||
conf = &proxy.Config{
|
||||
Logger: s.baseLogger.With(slogutil.KeyPrefix, "dnsproxy"),
|
||||
HTTP3: srvConf.ServeHTTP3,
|
||||
Ratelimit: int(srvConf.Ratelimit),
|
||||
RatelimitSubnetLenIPv4: srvConf.RatelimitSubnetLenIPv4,
|
||||
@@ -420,8 +424,6 @@ func parseBogusNXDOMAIN(confBogusNXDOMAIN []string) (subnets []netip.Prefix, err
|
||||
return subnets, nil
|
||||
}
|
||||
|
||||
const defaultBlockedResponseTTL = 3600
|
||||
|
||||
// initDefaultSettings initializes default settings if nothing
|
||||
// is configured
|
||||
func (s *Server) initDefaultSettings() {
|
||||
@@ -452,24 +454,24 @@ func (s *Server) initDefaultSettings() {
|
||||
|
||||
// prepareIpsetListSettings reads and prepares the ipset configuration either
|
||||
// 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
|
||||
if fn == "" {
|
||||
return s.ipset.init(s.conf.IpsetList)
|
||||
return s.conf.IpsetList, nil
|
||||
}
|
||||
|
||||
// #nosec G304 -- Trust the path explicitly given by the user.
|
||||
data, err := os.ReadFile(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ipsets := stringutil.SplitTrimmed(string(data), "\n")
|
||||
ipsets = stringutil.FilterOut(ipsets, IsCommentOrEmpty)
|
||||
ipsets = stringutil.SplitTrimmed(string(data), "\n")
|
||||
ipsets = slices.DeleteFunc(ipsets, IsCommentOrEmpty)
|
||||
|
||||
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
|
||||
@@ -690,7 +692,7 @@ func matchesDomainWildcard(host, pat string) (ok bool) {
|
||||
// the DNS names and patterns from certificate. dnsNames must be sorted.
|
||||
func anyNameMatches(dnsNames []string, sni string) (ok bool) {
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
)
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
if net.ParseIP(host) != nil {
|
||||
if netutil.IsValidIPString(host) {
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
@@ -27,6 +28,7 @@ import (
|
||||
"github.com/AdguardTeam/golibs/cache"
|
||||
"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/sysresolv"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
@@ -121,12 +123,17 @@ type Server struct {
|
||||
// access drops disallowed clients.
|
||||
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
|
||||
// must be a valid domain name plus dots on each side.
|
||||
localDomainSuffix string
|
||||
|
||||
// ipset processes DNS requests using ipset data.
|
||||
ipset ipsetCtx
|
||||
// ipset processes DNS requests using ipset data. It must not be nil after
|
||||
// initialization. See [newIpsetHandler].
|
||||
ipset *ipsetHandler
|
||||
|
||||
// privateNets is the configured set of IP networks considered private.
|
||||
privateNets netutil.SubnetSet
|
||||
@@ -197,6 +204,10 @@ type DNSCreateParams struct {
|
||||
PrivateNets netutil.SubnetSet
|
||||
Anonymizer *aghnet.IPMut
|
||||
EtcHosts *aghnet.HostsContainer
|
||||
|
||||
// Logger is used as a base logger. It must not be nil.
|
||||
Logger *slog.Logger
|
||||
|
||||
LocalDomain string
|
||||
}
|
||||
|
||||
@@ -233,6 +244,7 @@ func NewServer(p DNSCreateParams) (s *Server, err error) {
|
||||
stats: p.Stats,
|
||||
queryLog: p.QueryLog,
|
||||
privateNets: p.PrivateNets,
|
||||
baseLogger: p.Logger,
|
||||
// TODO(e.burkov): Use some case-insensitive string comparison.
|
||||
localDomainSuffix: strings.ToLower(localDomainSuffix),
|
||||
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
|
||||
// Server not running.
|
||||
func (s *Server) prepareInternalDNS() (err error) {
|
||||
err = s.prepareIpsetListSettings()
|
||||
ipsetList, err := s.prepareIpsetListSettings()
|
||||
if err != nil {
|
||||
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{
|
||||
Timeout: DefaultTimeout,
|
||||
HTTPVersions: UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams),
|
||||
@@ -664,6 +683,7 @@ func (s *Server) setupAddrProc() {
|
||||
s.addrProc = client.EmptyAddrProc{}
|
||||
} else {
|
||||
c := s.conf.AddrProcConf
|
||||
c.BaseLogger = s.baseLogger
|
||||
c.DialContext = s.DialContext
|
||||
c.PrivateSubnets = s.privateNets
|
||||
c.UsePrivateRDNS = s.conf.UsePrivateRDNS
|
||||
@@ -707,6 +727,7 @@ func validateBlockingMode(
|
||||
func (s *Server) prepareInternalProxy() (err error) {
|
||||
srvConf := s.conf
|
||||
conf := &proxy.Config{
|
||||
Logger: s.baseLogger.With(slogutil.KeyPrefix, "dnsproxy"),
|
||||
CacheEnabled: true,
|
||||
CacheSizeBytes: 4096,
|
||||
PrivateRDNSUpstreamConfig: srvConf.PrivateRDNSUpstreamConfig,
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
@@ -99,6 +100,7 @@ func createTestServer(
|
||||
DHCPServer: dhcp,
|
||||
DNSFilter: f,
|
||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||
Logger: slogutil.NewDiscardLogger(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -339,7 +341,10 @@ func TestServer_timeout(t *testing.T) {
|
||||
ServePlainDNS: true,
|
||||
}
|
||||
|
||||
s, err := NewServer(DNSCreateParams{DNSFilter: createTestDNSFilter(t)})
|
||||
s, err := NewServer(DNSCreateParams{
|
||||
DNSFilter: createTestDNSFilter(t),
|
||||
Logger: slogutil.NewDiscardLogger(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Prepare(srvConf)
|
||||
@@ -349,7 +354,10 @@ func TestServer_timeout(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)
|
||||
|
||||
s.conf.Config.UpstreamMode = UpstreamModeLoadBalance
|
||||
@@ -376,7 +384,9 @@ func TestServer_Prepare_fallbacks(t *testing.T) {
|
||||
ServePlainDNS: true,
|
||||
}
|
||||
|
||||
s, err := NewServer(DNSCreateParams{})
|
||||
s, err := NewServer(DNSCreateParams{
|
||||
Logger: slogutil.NewDiscardLogger(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Prepare(srvConf)
|
||||
@@ -962,6 +972,7 @@ func TestBlockedCustomIP(t *testing.T) {
|
||||
DHCPServer: dhcp,
|
||||
DNSFilter: f,
|
||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||
Logger: slogutil.NewDiscardLogger(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -1127,6 +1138,7 @@ func TestRewrite(t *testing.T) {
|
||||
DHCPServer: dhcp,
|
||||
DNSFilter: f,
|
||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||
Logger: slogutil.NewDiscardLogger(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -1256,6 +1268,7 @@ func TestPTRResponseFromDHCPLeases(t *testing.T) {
|
||||
},
|
||||
},
|
||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||
Logger: slogutil.NewDiscardLogger(),
|
||||
LocalDomain: localDomain,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -1341,6 +1354,7 @@ func TestPTRResponseFromHosts(t *testing.T) {
|
||||
DHCPServer: dhcp,
|
||||
DNSFilter: flt,
|
||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||
Logger: slogutil.NewDiscardLogger(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -1392,24 +1406,29 @@ func TestNewServer(t *testing.T) {
|
||||
in DNSCreateParams
|
||||
wantErrMsg string
|
||||
}{{
|
||||
name: "success",
|
||||
in: DNSCreateParams{},
|
||||
name: "success",
|
||||
in: DNSCreateParams{
|
||||
Logger: slogutil.NewDiscardLogger(),
|
||||
},
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "success_local_tld",
|
||||
in: DNSCreateParams{
|
||||
Logger: slogutil.NewDiscardLogger(),
|
||||
LocalDomain: "mynet",
|
||||
},
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "success_local_domain",
|
||||
in: DNSCreateParams{
|
||||
Logger: slogutil.NewDiscardLogger(),
|
||||
LocalDomain: "my.local.net",
|
||||
},
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "bad_local_domain",
|
||||
in: DNSCreateParams{
|
||||
Logger: slogutil.NewDiscardLogger(),
|
||||
LocalDomain: "!!!",
|
||||
},
|
||||
wantErrMsg: `local domain: bad domain name "!!!": ` +
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -57,6 +58,7 @@ func TestHandleDNSRequest_handleDNSRequest(t *testing.T) {
|
||||
},
|
||||
DNSFilter: f,
|
||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||
Logger: slogutil.NewDiscardLogger(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -229,6 +231,7 @@ func TestHandleDNSRequest_filterDNSResponse(t *testing.T) {
|
||||
DHCPServer: &testDHCP{},
|
||||
DNSFilter: f,
|
||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||
Logger: slogutil.NewDiscardLogger(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
||||
}, {
|
||||
name: "upstream_dns_bad",
|
||||
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 '!'`,
|
||||
}, {
|
||||
name: "bootstraps_bad",
|
||||
|
||||
@@ -1,29 +1,43 @@
|
||||
package dnsforward
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/ipset"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// ipsetCtx is the ipset context. ipsetMgr can be nil.
|
||||
type ipsetCtx struct {
|
||||
// ipsetHandler is the ipset context. ipsetMgr can be nil.
|
||||
type ipsetHandler struct {
|
||||
ipsetMgr ipset.Manager
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// init initializes the ipset context. It is not safe for concurrent use.
|
||||
//
|
||||
// TODO(a.garipov): Rewrite into a simple constructor?
|
||||
func (c *ipsetCtx) init(ipsetConf []string) (err error) {
|
||||
c.ipsetMgr, err = ipset.NewManager(ipsetConf)
|
||||
if errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrPermission) {
|
||||
// newIpsetHandler returns a new initialized [ipsetHandler]. It is not safe for
|
||||
// concurrent use.
|
||||
func newIpsetHandler(
|
||||
ctx context.Context,
|
||||
logger *slog.Logger,
|
||||
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
|
||||
// from Snap or when the user or the binary doesn't have the required
|
||||
// permissions, or when the kernel doesn't support netfilter.
|
||||
@@ -32,30 +46,28 @@ func (c *ipsetCtx) init(ipsetConf []string) (err error) {
|
||||
//
|
||||
// TODO(a.garipov): The Snap problem can probably be solved if we add
|
||||
// the netlink-connector interface plug.
|
||||
log.Info("ipset: warning: cannot initialize: %s", err)
|
||||
logger.WarnContext(ctx, "cannot initialize", slogutil.KeyError, err)
|
||||
|
||||
return nil
|
||||
} else if unsupErr := (&aghos.UnsupportedError{}); errors.As(err, &unsupErr) {
|
||||
log.Info("ipset: warning: %s", err)
|
||||
|
||||
return nil
|
||||
return h, 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
|
||||
}
|
||||
|
||||
// close closes the Linux Netfilter connections.
|
||||
func (c *ipsetCtx) close() (err error) {
|
||||
if c.ipsetMgr != nil {
|
||||
return c.ipsetMgr.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ipsetCtx) dctxIsfilled(dctx *dnsContext) (ok bool) {
|
||||
// dctxIsFilled returns true if dctx has enough information to process.
|
||||
func dctxIsFilled(dctx *dnsContext) (ok bool) {
|
||||
return dctx != nil &&
|
||||
dctx.responseFromUpstream &&
|
||||
dctx.proxyCtx != nil &&
|
||||
@@ -66,8 +78,8 @@ func (c *ipsetCtx) dctxIsfilled(dctx *dnsContext) (ok bool) {
|
||||
|
||||
// skipIpsetProcessing returns true when the ipset processing can be skipped for
|
||||
// this request.
|
||||
func (c *ipsetCtx) skipIpsetProcessing(dctx *dnsContext) (ok bool) {
|
||||
if c == nil || c.ipsetMgr == nil || !c.dctxIsfilled(dctx) {
|
||||
func (h *ipsetHandler) skipIpsetProcessing(dctx *dnsContext) (ok bool) {
|
||||
if h == nil || h.ipsetMgr == nil || !dctxIsFilled(dctx) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -109,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.
|
||||
func (c *ipsetCtx) process(dctx *dnsContext) (rc resultCode) {
|
||||
log.Debug("dnsforward: ipset: started processing")
|
||||
defer log.Debug("dnsforward: ipset: finished processing")
|
||||
func (h *ipsetHandler) process(dctx *dnsContext) (rc resultCode) {
|
||||
// TODO(s.chzhen): Use passed context.
|
||||
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
|
||||
}
|
||||
|
||||
log.Debug("ipset: starting processing")
|
||||
|
||||
req := dctx.proxyCtx.Req
|
||||
host := req.Question[0].Name
|
||||
host = strings.TrimSuffix(host, ".")
|
||||
host = strings.ToLower(host)
|
||||
|
||||
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 {
|
||||
// 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
|
||||
}
|
||||
|
||||
log.Debug("dnsforward: ipset: added %d new ipset entries", n)
|
||||
h.logger.DebugContext(ctx, "added new ipset entries", "num", n)
|
||||
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package dnsforward
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -16,7 +18,7 @@ type fakeIpsetMgr struct {
|
||||
}
|
||||
|
||||
// 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.ip6s = append(m.ip6s, ip6s...)
|
||||
|
||||
@@ -58,7 +60,9 @@ func TestIpsetCtx_process(t *testing.T) {
|
||||
responseFromUpstream: true,
|
||||
}
|
||||
|
||||
ictx := &ipsetCtx{}
|
||||
ictx := &ipsetHandler{
|
||||
logger: slogutil.NewDiscardLogger(),
|
||||
}
|
||||
rc := ictx.process(dctx)
|
||||
assert.Equal(t, resultCodeSuccess, rc)
|
||||
|
||||
@@ -77,8 +81,9 @@ func TestIpsetCtx_process(t *testing.T) {
|
||||
}
|
||||
|
||||
m := &fakeIpsetMgr{}
|
||||
ictx := &ipsetCtx{
|
||||
ictx := &ipsetHandler{
|
||||
ipsetMgr: m,
|
||||
logger: slogutil.NewDiscardLogger(),
|
||||
}
|
||||
|
||||
rc := ictx.process(dctx)
|
||||
@@ -101,8 +106,9 @@ func TestIpsetCtx_process(t *testing.T) {
|
||||
}
|
||||
|
||||
m := &fakeIpsetMgr{}
|
||||
ictx := &ipsetCtx{
|
||||
ictx := &ipsetHandler{
|
||||
ipsetMgr: m,
|
||||
logger: slogutil.NewDiscardLogger(),
|
||||
}
|
||||
|
||||
rc := ictx.process(dctx)
|
||||
@@ -124,8 +130,9 @@ func TestIpsetCtx_SkipIpsetProcessing(t *testing.T) {
|
||||
}
|
||||
|
||||
m := &fakeIpsetMgr{}
|
||||
ictx := &ipsetCtx{
|
||||
ictx := &ipsetHandler{
|
||||
ipsetMgr: m,
|
||||
logger: slogutil.NewDiscardLogger(),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
|
||||
@@ -58,7 +58,7 @@ func (s *Server) genDNSFilterMessage(
|
||||
return s.replyCompressed(req)
|
||||
}
|
||||
|
||||
return s.newMsgNODATA(req)
|
||||
return s.NewMsgNODATA(req)
|
||||
}
|
||||
|
||||
switch res.Reason {
|
||||
@@ -344,51 +344,6 @@ func (s *Server) makeResponseREFUSED(req *dns.Msg) *dns.Msg {
|
||||
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
|
||||
var _ proxy.MessageConstructor = (*Server)(nil)
|
||||
|
||||
@@ -425,3 +380,52 @@ func (s *Server) NewMsgNOTIMPLEMENTED(req *dns.Msg) (resp *dns.Msg) {
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ func (s *Server) processInitial(dctx *dnsContext) (rc resultCode) {
|
||||
q := pctx.Req.Question[0]
|
||||
qt := q.Qtype
|
||||
if s.conf.AAAADisabled && qt == dns.TypeAAAA {
|
||||
_ = proxy.CheckDisabledAAAARequest(pctx, true)
|
||||
pctx.Res = s.NewMsgNODATA(pctx.Req)
|
||||
|
||||
return resultCodeFinish
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/AdguardTeam/urlfilter/rules"
|
||||
@@ -430,6 +431,7 @@ func TestServer_ProcessDHCPHosts_localRestriction(t *testing.T) {
|
||||
dnsFilter: createTestDNSFilter(t),
|
||||
dhcpServer: dhcp,
|
||||
localDomainSuffix: localDomainSuffix,
|
||||
baseLogger: slogutil.NewDiscardLogger(),
|
||||
}
|
||||
|
||||
req := &dns.Msg{
|
||||
@@ -565,6 +567,7 @@ func TestServer_ProcessDHCPHosts(t *testing.T) {
|
||||
dnsFilter: createTestDNSFilter(t),
|
||||
dhcpServer: testDHCP,
|
||||
localDomainSuffix: tc.suffix,
|
||||
baseLogger: slogutil.NewDiscardLogger(),
|
||||
}
|
||||
|
||||
req := &dns.Msg{
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/stats"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -202,6 +203,7 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) {
|
||||
ql := &testQueryLog{}
|
||||
st := &testStats{}
|
||||
srv := &Server{
|
||||
baseLogger: slogutil.NewDiscardLogger(),
|
||||
queryLog: ql,
|
||||
stats: st,
|
||||
anonymizer: aghnet.NewIPMut(nil),
|
||||
|
||||
@@ -150,12 +150,12 @@ func setProxyUpstreamMode(
|
||||
) (err error) {
|
||||
switch upstreamMode {
|
||||
case UpstreamModeParallel:
|
||||
conf.UpstreamMode = proxy.UModeParallel
|
||||
conf.UpstreamMode = proxy.UpstreamModeParallel
|
||||
case UpstreamModeFastestAddr:
|
||||
conf.UpstreamMode = proxy.UModeFastestAddr
|
||||
conf.UpstreamMode = proxy.UpstreamModeFastestAddr
|
||||
conf.FastestPingTimeout = fastestTimeout
|
||||
case UpstreamModeLoadBalance:
|
||||
conf.UpstreamMode = proxy.UModeLoadBalance
|
||||
conf.UpstreamMode = proxy.UpstreamModeLoadBalance
|
||||
default:
|
||||
return fmt.Errorf("unexpected value %q", upstreamMode)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ func fromCacheItem(item *cacheItem) (data []byte) {
|
||||
data = binary.BigEndian.AppendUint64(data, uint64(expiry))
|
||||
|
||||
for _, v := range item.hashes {
|
||||
// nolint:looppointer // The subslice of v is used for a copy.
|
||||
data = append(data, v[:]...)
|
||||
}
|
||||
|
||||
@@ -63,7 +62,6 @@ func (c *Checker) findInCache(
|
||||
|
||||
i := 0
|
||||
for _, hash := range hashes {
|
||||
// nolint:looppointer // The has subslice is used for a cache lookup.
|
||||
data := c.cache.Get(hash[:prefixLen])
|
||||
if data == nil {
|
||||
hashes[i] = hash
|
||||
@@ -98,7 +96,6 @@ func (c *Checker) storeInCache(hashesToRequest, respHashes []hostnameHash) {
|
||||
|
||||
for _, hash := range respHashes {
|
||||
var pref prefix
|
||||
// nolint:looppointer // The hash subslice is used for a copy.
|
||||
copy(pref[:], hash[:])
|
||||
|
||||
hashToStore[pref] = append(hashToStore[pref], hash)
|
||||
@@ -109,11 +106,9 @@ func (c *Checker) storeInCache(hashesToRequest, respHashes []hostnameHash) {
|
||||
}
|
||||
|
||||
for _, hash := range hashesToRequest {
|
||||
// nolint:looppointer // The hash subslice is used for a cache lookup.
|
||||
val := c.cache.Get(hash[:prefixLen])
|
||||
if val == nil {
|
||||
var pref prefix
|
||||
// nolint:looppointer // The hash subslice is used for a copy.
|
||||
copy(pref[:], hash[:])
|
||||
|
||||
c.setCache(pref, nil)
|
||||
|
||||
@@ -173,7 +173,6 @@ func (c *Checker) getQuestion(hashes []hostnameHash) (q string) {
|
||||
b := &strings.Builder{}
|
||||
|
||||
for _, hash := range hashes {
|
||||
// nolint:looppointer // The hash subslice is used for hex encoding.
|
||||
stringutil.WriteToBuilder(b, hex.EncodeToString(hash[:prefixLen]), ".")
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ type SafeSearchConfig struct {
|
||||
|
||||
Bing bool `yaml:"bing" json:"bing"`
|
||||
DuckDuckGo bool `yaml:"duckduckgo" json:"duckduckgo"`
|
||||
Ecosia bool `yaml:"ecosia" json:"ecosia"`
|
||||
Google bool `yaml:"google" json:"google"`
|
||||
Pixabay bool `yaml:"pixabay" json:"pixabay"`
|
||||
Yandex bool `yaml:"yandex" json:"yandex"`
|
||||
|
||||
@@ -14,6 +14,9 @@ var pixabay string
|
||||
//go:embed rules/duckduckgo.txt
|
||||
var duckduckgo string
|
||||
|
||||
//go:embed rules/ecosia.txt
|
||||
var ecosia string
|
||||
|
||||
//go:embed rules/yandex.txt
|
||||
var yandex string
|
||||
|
||||
@@ -27,6 +30,7 @@ var youtube string
|
||||
var safeSearchRules = map[Service]string{
|
||||
Bing: bing,
|
||||
DuckDuckGo: duckduckgo,
|
||||
Ecosia: ecosia,
|
||||
Google: google,
|
||||
Pixabay: pixabay,
|
||||
Yandex: yandex,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user