Compare commits
31 Commits
v0.108.0-b
...
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 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
'name': 'build'
|
'name': 'build'
|
||||||
|
|
||||||
'env':
|
'env':
|
||||||
'GO_VERSION': '1.22.5'
|
'GO_VERSION': '1.23.1'
|
||||||
'NODE_VERSION': '16'
|
'NODE_VERSION': '16'
|
||||||
|
|
||||||
'on':
|
'on':
|
||||||
|
|||||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
'name': 'lint'
|
'name': 'lint'
|
||||||
|
|
||||||
'env':
|
'env':
|
||||||
'GO_VERSION': '1.22.5'
|
'GO_VERSION': '1.23.1'
|
||||||
|
|
||||||
'on':
|
'on':
|
||||||
'push':
|
'push':
|
||||||
|
|||||||
57
CHANGELOG.md
57
CHANGELOG.md
@@ -18,17 +18,55 @@ TODO(a.garipov): Use the common markdown formatting tools.
|
|||||||
<!--
|
<!--
|
||||||
## [v0.108.0] - TBA
|
## [v0.108.0] - TBA
|
||||||
|
|
||||||
## [v0.107.52] - 2024-06-29 (APPROX.)
|
## [v0.107.53] - 2024-07-24 (APPROX.)
|
||||||
|
|
||||||
See also the [v0.107.52 GitHub milestone][ms-v0.107.52].
|
See also the [v0.107.53 GitHub milestone][ms-v0.107.53].
|
||||||
|
|
||||||
[ms-v0.107.52]: https://github.com/AdguardTeam/AdGuardHome/milestone/87?closed=1
|
[ms-v0.107.53]: https://github.com/AdguardTeam/AdGuardHome/milestone/88?closed=1
|
||||||
|
|
||||||
NOTE: Add new changes BELOW THIS COMMENT.
|
NOTE: Add new changes BELOW THIS COMMENT.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Security
|
### 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
|
- Go version has been updated to prevent the possibility of exploiting the Go
|
||||||
vulnerabilities fixed in [Go 1.22.5][go-1.22.5].
|
vulnerabilities fixed in [Go 1.22.5][go-1.22.5].
|
||||||
|
|
||||||
@@ -61,8 +99,6 @@ NOTE: Add new changes BELOW THIS COMMENT.
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Unnecessary validation call on the encryption page.
|
|
||||||
- Missing version in the footer.
|
|
||||||
- Panic caused by missing user-specific blocked services object in configuration
|
- Panic caused by missing user-specific blocked services object in configuration
|
||||||
file ([#7069]).
|
file ([#7069]).
|
||||||
- Tracking `/etc/hosts` file changes causing panics within particular
|
- Tracking `/etc/hosts` file changes causing panics within particular
|
||||||
@@ -76,9 +112,7 @@ NOTE: Add new changes BELOW THIS COMMENT.
|
|||||||
[go-1.22.5]: https://groups.google.com/g/golang-announce/c/gyb7aM1C9H4
|
[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
|
[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
|
||||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -3061,11 +3095,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
|
|||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.52...HEAD
|
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.53...HEAD
|
||||||
[v0.107.52]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.51...v0.107.52
|
[v0.107.53]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.52...v0.107.53
|
||||||
-->
|
-->
|
||||||
|
|
||||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.51...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.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.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.49]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.48...v0.107.49
|
||||||
|
|||||||
48
Makefile
48
Makefile
@@ -8,7 +8,7 @@
|
|||||||
# Makefile. Bump this number every time a significant change is made to
|
# Makefile. Bump this number every time a significant change is made to
|
||||||
# this Makefile.
|
# this Makefile.
|
||||||
#
|
#
|
||||||
# AdGuard-Project-Version: 4
|
# AdGuard-Project-Version: 6
|
||||||
|
|
||||||
# Don't name these macros "GO" etc., because GNU Make apparently makes
|
# Don't name these macros "GO" etc., because GNU Make apparently makes
|
||||||
# them exported environment variables with the literal value of
|
# them exported environment variables with the literal value of
|
||||||
@@ -23,11 +23,13 @@ VERBOSE.MACRO = $${VERBOSE:-0}
|
|||||||
CHANNEL = development
|
CHANNEL = development
|
||||||
CLIENT_DIR = client
|
CLIENT_DIR = client
|
||||||
COMMIT = $$( git rev-parse --short HEAD )
|
COMMIT = $$( git rev-parse --short HEAD )
|
||||||
|
DEPLOY_SCRIPT_PATH = not/a/real/path
|
||||||
DIST_DIR = dist
|
DIST_DIR = dist
|
||||||
GOAMD64 = v1
|
GOAMD64 = v1
|
||||||
GOPROXY = https://goproxy.cn|https://proxy.golang.org|direct
|
GOPROXY = https://proxy.golang.org|direct
|
||||||
GOSUMDB = sum.golang.google.cn
|
GOSUMDB = sum.golang.google.cn
|
||||||
GOTOOLCHAIN = go1.22.5
|
GOTOOLCHAIN = go1.23.1
|
||||||
|
GOTELEMETRY = off
|
||||||
GPG_KEY = devteam@adguard.com
|
GPG_KEY = devteam@adguard.com
|
||||||
GPG_KEY_PASSPHRASE = not-a-real-password
|
GPG_KEY_PASSPHRASE = not-a-real-password
|
||||||
NPM = npm
|
NPM = npm
|
||||||
@@ -36,6 +38,7 @@ NPM_INSTALL_FLAGS = $(NPM_FLAGS) --quiet --no-progress --ignore-engines\
|
|||||||
--ignore-optional --ignore-platform --ignore-scripts
|
--ignore-optional --ignore-platform --ignore-scripts
|
||||||
RACE = 0
|
RACE = 0
|
||||||
SIGN = 1
|
SIGN = 1
|
||||||
|
SIGNER_API_KEY = not-a-real-key
|
||||||
VERSION = v0.0.0
|
VERSION = v0.0.0
|
||||||
YARN = yarn
|
YARN = yarn
|
||||||
|
|
||||||
@@ -59,20 +62,28 @@ BUILD_RELEASE_DEPS_1 = go-deps
|
|||||||
ENV = env\
|
ENV = env\
|
||||||
CHANNEL='$(CHANNEL)'\
|
CHANNEL='$(CHANNEL)'\
|
||||||
COMMIT='$(COMMIT)'\
|
COMMIT='$(COMMIT)'\
|
||||||
|
DEPLOY_SCRIPT_PATH='$(DEPLOY_SCRIPT_PATH)' \
|
||||||
DIST_DIR='$(DIST_DIR)'\
|
DIST_DIR='$(DIST_DIR)'\
|
||||||
GO="$(GO.MACRO)"\
|
GO="$(GO.MACRO)"\
|
||||||
GOAMD64="$(GOAMD64)"\
|
GOAMD64='$(GOAMD64)'\
|
||||||
GOPROXY='$(GOPROXY)'\
|
GOPROXY='$(GOPROXY)'\
|
||||||
GOSUMDB='$(GOSUMDB)'\
|
GOSUMDB='$(GOSUMDB)'\
|
||||||
|
GOTELEMETRY='$(GOTELEMETRY)'\
|
||||||
GOTOOLCHAIN='$(GOTOOLCHAIN)'\
|
GOTOOLCHAIN='$(GOTOOLCHAIN)'\
|
||||||
GPG_KEY='$(GPG_KEY)'\
|
GPG_KEY='$(GPG_KEY)'\
|
||||||
GPG_KEY_PASSPHRASE='$(GPG_KEY_PASSPHRASE)'\
|
GPG_KEY_PASSPHRASE='$(GPG_KEY_PASSPHRASE)'\
|
||||||
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
|
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
|
||||||
RACE='$(RACE)'\
|
RACE='$(RACE)'\
|
||||||
SIGN='$(SIGN)'\
|
SIGN='$(SIGN)'\
|
||||||
|
SIGNER_API_KEY='$(SIGNER_API_KEY)' \
|
||||||
NEXTAPI='$(NEXTAPI)'\
|
NEXTAPI='$(NEXTAPI)'\
|
||||||
VERBOSE="$(VERBOSE.MACRO)"\
|
VERBOSE="$(VERBOSE.MACRO)"\
|
||||||
VERSION='$(VERSION)'\
|
VERSION="$(VERSION)"\
|
||||||
|
|
||||||
|
# Keep the line above blank.
|
||||||
|
|
||||||
|
ENV_MISC = env\
|
||||||
|
VERBOSE="$(VERBOSE.MACRO)"\
|
||||||
|
|
||||||
# Keep the line above blank.
|
# Keep the line above blank.
|
||||||
|
|
||||||
@@ -101,23 +112,22 @@ js-deps: ; $(NPM) $(NPM_INSTALL_FLAGS) ci
|
|||||||
js-lint: ; $(NPM) $(NPM_FLAGS) run lint
|
js-lint: ; $(NPM) $(NPM_FLAGS) run lint
|
||||||
js-test: ; $(NPM) $(NPM_FLAGS) run test
|
js-test: ; $(NPM) $(NPM_FLAGS) run test
|
||||||
|
|
||||||
go-bench: ; $(ENV) "$(SHELL)" ./scripts/make/go-bench.sh
|
go-bench: ; $(ENV) "$(SHELL)" ./scripts/make/go-bench.sh
|
||||||
go-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh
|
go-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh
|
||||||
go-deps: ; $(ENV) "$(SHELL)" ./scripts/make/go-deps.sh
|
go-deps: ; $(ENV) "$(SHELL)" ./scripts/make/go-deps.sh
|
||||||
go-fuzz: ; $(ENV) "$(SHELL)" ./scripts/make/go-fuzz.sh
|
go-env: ; $(ENV) "$(GO.MACRO)" env
|
||||||
go-lint: ; $(ENV) "$(SHELL)" ./scripts/make/go-lint.sh
|
go-fuzz: ; $(ENV) "$(SHELL)" ./scripts/make/go-fuzz.sh
|
||||||
go-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-tools.sh
|
go-lint: ; $(ENV) "$(SHELL)" ./scripts/make/go-lint.sh
|
||||||
|
|
||||||
# TODO(a.garipov): Think about making RACE='1' the default for all
|
# TODO(a.garipov): Think about making RACE='1' the default for all
|
||||||
# targets.
|
# targets.
|
||||||
go-test: ; $(ENV) RACE='1' "$(SHELL)" ./scripts/make/go-test.sh
|
go-test: ; $(ENV) RACE='1' "$(SHELL)" ./scripts/make/go-test.sh
|
||||||
|
go-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-tools.sh
|
||||||
go-upd-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-upd-tools.sh
|
go-upd-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-upd-tools.sh
|
||||||
|
|
||||||
go-check: go-tools go-lint go-test
|
go-check: go-tools go-lint go-test
|
||||||
|
|
||||||
# A quick check to make sure that all supported operating systems can be
|
# A quick check to make sure that all operating systems relevant to the
|
||||||
# typechecked and built successfully.
|
# development of the project can be typechecked and built successfully.
|
||||||
go-os-check:
|
go-os-check:
|
||||||
env GOOS='darwin' "$(GO.MACRO)" vet ./internal/...
|
env GOOS='darwin' "$(GO.MACRO)" vet ./internal/...
|
||||||
env GOOS='freebsd' "$(GO.MACRO)" vet ./internal/...
|
env GOOS='freebsd' "$(GO.MACRO)" vet ./internal/...
|
||||||
@@ -125,7 +135,11 @@ go-os-check:
|
|||||||
env GOOS='linux' "$(GO.MACRO)" vet ./internal/...
|
env GOOS='linux' "$(GO.MACRO)" vet ./internal/...
|
||||||
env GOOS='windows' "$(GO.MACRO)" vet ./internal/...
|
env GOOS='windows' "$(GO.MACRO)" vet ./internal/...
|
||||||
|
|
||||||
|
|
||||||
openapi-lint: ; cd ./openapi/ && $(YARN) test
|
openapi-lint: ; cd ./openapi/ && $(YARN) test
|
||||||
openapi-show: ; cd ./openapi/ && $(YARN) start
|
openapi-show: ; cd ./openapi/ && $(YARN) start
|
||||||
|
|
||||||
txt-lint: ; $(ENV) "$(SHELL)" ./scripts/make/txt-lint.sh
|
txt-lint: ; $(ENV) "$(SHELL)" ./scripts/make/txt-lint.sh
|
||||||
|
|
||||||
|
md-lint: ; $(ENV_MISC) "$(SHELL)" ./scripts/make/md-lint.sh
|
||||||
|
sh-lint: ; $(ENV_MISC) "$(SHELL)" ./scripts/make/sh-lint.sh
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ Run `make init` to prepare the development environment.
|
|||||||
|
|
||||||
You will need this to build AdGuard Home:
|
You will need this to build AdGuard Home:
|
||||||
|
|
||||||
- [Go](https://golang.org/dl/) v1.22 or later;
|
- [Go](https://golang.org/dl/) v1.23 or later;
|
||||||
- [Node.js](https://nodejs.org/en/download/) v18.18 or later;
|
- [Node.js](https://nodejs.org/en/download/) v18.18 or later;
|
||||||
- [npm](https://www.npmjs.com/) v8 or later;
|
- [npm](https://www.npmjs.com/) v8 or later;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
'variables':
|
'variables':
|
||||||
'channel': 'edge'
|
'channel': 'edge'
|
||||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||||
'dockerGo': 'adguard/go-builder:1.22.5--1'
|
'dockerGo': 'adguard/go-builder:1.23.1--1'
|
||||||
|
|
||||||
'stages':
|
'stages':
|
||||||
- 'Build frontend':
|
- 'Build frontend':
|
||||||
@@ -91,6 +91,11 @@
|
|||||||
'tasks':
|
'tasks':
|
||||||
- 'checkout':
|
- 'checkout':
|
||||||
'force-clean-build': true
|
'force-clean-build': true
|
||||||
|
- 'checkout':
|
||||||
|
'repository': 'bamboo-deploy-publisher'
|
||||||
|
# The paths are always relative to the working directory.
|
||||||
|
'path': 'bamboo-deploy-publisher'
|
||||||
|
'force-clean-build': true
|
||||||
- 'script':
|
- 'script':
|
||||||
'interpreter': 'SHELL'
|
'interpreter': 'SHELL'
|
||||||
'scripts':
|
'scripts':
|
||||||
@@ -99,6 +104,9 @@
|
|||||||
|
|
||||||
set -e -f -u -x
|
set -e -f -u -x
|
||||||
|
|
||||||
|
# Explicitly checkout the revision that we need.
|
||||||
|
git checkout "${bamboo.repository.revision.number}"
|
||||||
|
|
||||||
# Run the build with the specified channel.
|
# Run the build with the specified channel.
|
||||||
echo "${bamboo.gpgSecretKeyPart1}${bamboo.gpgSecretKeyPart2}"\
|
echo "${bamboo.gpgSecretKeyPart1}${bamboo.gpgSecretKeyPart2}"\
|
||||||
| awk '{ gsub(/\\n/, "\n"); print; }'\
|
| awk '{ gsub(/\\n/, "\n"); print; }'\
|
||||||
@@ -107,6 +115,8 @@
|
|||||||
make\
|
make\
|
||||||
CHANNEL=${bamboo.channel}\
|
CHANNEL=${bamboo.channel}\
|
||||||
GPG_KEY_PASSPHRASE=${bamboo.gpgPassword}\
|
GPG_KEY_PASSPHRASE=${bamboo.gpgPassword}\
|
||||||
|
DEPLOY_SCRIPT_PATH="./bamboo-deploy-publisher/deploy.sh"\
|
||||||
|
SIGNER_API_KEY="${bamboo.adguardHomeWinSignerSecretApiKey}"\
|
||||||
FRONTEND_PREBUILT=1\
|
FRONTEND_PREBUILT=1\
|
||||||
PARALLELISM=1\
|
PARALLELISM=1\
|
||||||
VERBOSE=2\
|
VERBOSE=2\
|
||||||
@@ -266,7 +276,7 @@
|
|||||||
'variables':
|
'variables':
|
||||||
'channel': 'beta'
|
'channel': 'beta'
|
||||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||||
'dockerGo': 'adguard/go-builder:1.22.5--1'
|
'dockerGo': 'adguard/go-builder:1.23.1--1'
|
||||||
# release-vX.Y.Z branches are the branches from which the actual final
|
# release-vX.Y.Z branches are the branches from which the actual final
|
||||||
# release is built.
|
# release is built.
|
||||||
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
|
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
|
||||||
@@ -282,4 +292,4 @@
|
|||||||
'variables':
|
'variables':
|
||||||
'channel': 'release'
|
'channel': 'release'
|
||||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||||
'dockerGo': 'adguard/go-builder:1.22.5--1'
|
'dockerGo': 'adguard/go-builder:1.23.1--1'
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
'name': 'AdGuard Home - Build and run tests'
|
'name': 'AdGuard Home - Build and run tests'
|
||||||
'variables':
|
'variables':
|
||||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||||
'dockerGo': 'adguard/go-builder:1.22.5--1'
|
'dockerGo': 'adguard/go-builder:1.23.1--1'
|
||||||
'channel': 'development'
|
'channel': 'development'
|
||||||
|
|
||||||
'stages':
|
'stages':
|
||||||
@@ -54,6 +54,7 @@
|
|||||||
'requirements':
|
'requirements':
|
||||||
- 'adg-docker': 'true'
|
- 'adg-docker': 'true'
|
||||||
|
|
||||||
|
# TODO(e.burkov): Add the linting stage for markdown docs and shell scripts.
|
||||||
'Test backend':
|
'Test backend':
|
||||||
'docker':
|
'docker':
|
||||||
'image': '${bamboo.dockerGo}'
|
'image': '${bamboo.dockerGo}'
|
||||||
@@ -195,5 +196,5 @@
|
|||||||
# may need to build a few of these.
|
# may need to build a few of these.
|
||||||
'variables':
|
'variables':
|
||||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||||
'dockerGo': 'adguard/go-builder:1.22.5--1'
|
'dockerGo': 'adguard/go-builder:1.23.1--1'
|
||||||
'channel': 'candidate'
|
'channel': 'candidate'
|
||||||
|
|||||||
@@ -154,7 +154,7 @@
|
|||||||
"use_adguard_parental": "Use AdGuard parental control web service",
|
"use_adguard_parental": "Use AdGuard parental control web service",
|
||||||
"use_adguard_parental_hint": "AdGuard Home will check if domain contains adult materials. It uses the same privacy-friendly API as the browsing security web service.",
|
"use_adguard_parental_hint": "AdGuard Home will check if domain contains adult materials. It uses the same privacy-friendly API as the browsing security web service.",
|
||||||
"enforce_safe_search": "Use Safe Search",
|
"enforce_safe_search": "Use Safe Search",
|
||||||
"enforce_save_search_hint": "AdGuard Home will enforce safe search in the following search engines: Google, YouTube, Bing, DuckDuckGo, Yandex, Pixabay.",
|
"enforce_save_search_hint": "AdGuard Home will enforce safe search in the following search engines: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.",
|
||||||
"no_servers_specified": "No servers specified",
|
"no_servers_specified": "No servers specified",
|
||||||
"general_settings": "General settings",
|
"general_settings": "General settings",
|
||||||
"dns_settings": "DNS settings",
|
"dns_settings": "DNS settings",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect } from 'react';
|
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 { useTranslation } from 'react-i18next';
|
||||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
@@ -104,14 +104,13 @@ const FORM_NAMES = {
|
|||||||
response_status: 'response_status',
|
response_status: 'response_status',
|
||||||
};
|
};
|
||||||
|
|
||||||
interface FiltersFormProps {
|
type FiltersFormProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
responseStatusClass?: 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 { className = '', responseStatusClass, setIsLoading, change } = props;
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -142,7 +141,6 @@ const Form = (props: FiltersFormProps) => {
|
|||||||
|
|
||||||
const onInputClear = async () => {
|
const onInputClear = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
change(FORM_NAMES.search, DEFAULT_LOGS_FILTER[FORM_NAMES.search]);
|
change(FORM_NAMES.search, DEFAULT_LOGS_FILTER[FORM_NAMES.search]);
|
||||||
setIsLoading(false);
|
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,
|
form: FORM_NAME.LOGS_FILTER,
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
})(Form);
|
})(Form);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import Form from './Form';
|
import { FiltersForm } from './Form';
|
||||||
import { refreshFilteredLogs } from '../../../actions/queryLogs';
|
import { refreshFilteredLogs } from '../../../actions/queryLogs';
|
||||||
import { addSuccessToast } from '../../../actions/toasts';
|
import { addSuccessToast } from '../../../actions/toasts';
|
||||||
|
|
||||||
@@ -38,12 +38,7 @@ const Filters = ({ filter, setIsLoading }: FiltersProps) => {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</h1>
|
</h1>
|
||||||
|
<FiltersForm responseStatusClass="d-sm-block" setIsLoading={setIsLoading} initialValues={filter} />
|
||||||
<Form
|
|
||||||
// responseStatusClass="d-sm-block"
|
|
||||||
// setIsLoading={setIsLoading}
|
|
||||||
initialValues={filter}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const renderFormattedClientCell = (value: any, info: any, isDetailed = fa
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="logs__text mw-100" title={value}>
|
<div className="logs__text logs__text--client mw-100" title={value}>
|
||||||
<Link to={`logs?search="${encodeURIComponent(value)}"`}>{nameContainer}</Link>
|
<Link to={`logs?search="${encodeURIComponent(value)}"`}>{nameContainer}</Link>
|
||||||
{whoisContainer}
|
{whoisContainer}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
22
go.mod
22
go.mod
@@ -1,10 +1,10 @@
|
|||||||
module github.com/AdguardTeam/AdGuardHome
|
module github.com/AdguardTeam/AdGuardHome
|
||||||
|
|
||||||
go 1.22.5
|
go 1.23.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AdguardTeam/dnsproxy v0.71.2
|
github.com/AdguardTeam/dnsproxy v0.73.0
|
||||||
github.com/AdguardTeam/golibs v0.24.0
|
github.com/AdguardTeam/golibs v0.26.0
|
||||||
github.com/AdguardTeam/urlfilter v0.19.0
|
github.com/AdguardTeam/urlfilter v0.19.0
|
||||||
github.com/NYTimes/gziphandler v1.1.1
|
github.com/NYTimes/gziphandler v1.1.1
|
||||||
github.com/ameshkov/dnscrypt/v2 v2.3.0
|
github.com/ameshkov/dnscrypt/v2 v2.3.0
|
||||||
@@ -32,10 +32,10 @@ require (
|
|||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/ti-mo/netfilter v0.5.2
|
github.com/ti-mo/netfilter v0.5.2
|
||||||
go.etcd.io/bbolt v1.3.10
|
go.etcd.io/bbolt v1.3.10
|
||||||
golang.org/x/crypto v0.24.0
|
golang.org/x/crypto v0.26.0
|
||||||
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8
|
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa
|
||||||
golang.org/x/net v0.26.0
|
golang.org/x/net v0.28.0
|
||||||
golang.org/x/sys v0.21.0
|
golang.org/x/sys v0.24.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
howett.net/plist v1.0.1
|
howett.net/plist v1.0.1
|
||||||
@@ -58,9 +58,9 @@ require (
|
|||||||
github.com/quic-go/qpack v0.4.0 // indirect
|
github.com/quic-go/qpack v0.4.0 // indirect
|
||||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
||||||
go.uber.org/mock v0.4.0 // indirect
|
go.uber.org/mock v0.4.0 // indirect
|
||||||
golang.org/x/mod v0.18.0 // indirect
|
golang.org/x/mod v0.20.0 // indirect
|
||||||
golang.org/x/sync v0.7.0 // indirect
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
golang.org/x/text v0.16.0 // indirect
|
golang.org/x/text v0.17.0 // indirect
|
||||||
golang.org/x/tools v0.22.0 // indirect
|
golang.org/x/tools v0.24.0 // indirect
|
||||||
gonum.org/v1/gonum v0.15.0 // indirect
|
gonum.org/v1/gonum v0.15.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
40
go.sum
40
go.sum
@@ -1,7 +1,7 @@
|
|||||||
github.com/AdguardTeam/dnsproxy v0.71.2 h1:dFG2wga4GDdj1eI3rU2wqjQ6QGQm9MjLRb5ZzyH3Vgg=
|
github.com/AdguardTeam/dnsproxy v0.73.0 h1:E1fxzosMqExZH8h7OJnKXLxyktcAFRJapLF4+nKULms=
|
||||||
github.com/AdguardTeam/dnsproxy v0.71.2/go.mod h1:huI5zyWhlimHBhg0jt2CMinXzsEHymI+WlvxIfmfEGA=
|
github.com/AdguardTeam/dnsproxy v0.73.0/go.mod h1:ZcvmyQY2EiX5B0yCTkiYTgtm+1lBWA0lajbEI9dOhW4=
|
||||||
github.com/AdguardTeam/golibs v0.24.0 h1:qAnOq7BQtwSVo7Co9q703/n+nZ2Ap6smkugU9G9MomY=
|
github.com/AdguardTeam/golibs v0.26.0 h1:uLL0XggEjB+87lL1tPpEAQNoKAlHDq5AyBUVWEgf63E=
|
||||||
github.com/AdguardTeam/golibs v0.24.0/go.mod h1:9/vJcYznW7RlmCT/Qzi8XNZGj+ZbWfHZJmEXKnRpCAU=
|
github.com/AdguardTeam/golibs v0.26.0/go.mod h1:iWdjXPCwmK2g2FKIb/OwEPnovSXeMqRhI8FWLxF5oxE=
|
||||||
github.com/AdguardTeam/urlfilter v0.19.0 h1:q7eH13+yNETlpD/VD3u5rLQOripcUdEktqZFy+KiQLk=
|
github.com/AdguardTeam/urlfilter v0.19.0 h1:q7eH13+yNETlpD/VD3u5rLQOripcUdEktqZFy+KiQLk=
|
||||||
github.com/AdguardTeam/urlfilter v0.19.0/go.mod h1:+N54ZvxqXYLnXuvpaUhK2exDQW+djZBRSb6F6j0rkBY=
|
github.com/AdguardTeam/urlfilter v0.19.0/go.mod h1:+N54ZvxqXYLnXuvpaUhK2exDQW+djZBRSb6F6j0rkBY=
|
||||||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||||
@@ -128,26 +128,26 @@ go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
|||||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||||
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM=
|
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
|
||||||
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -158,19 +158,19 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ=
|
gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ=
|
||||||
|
|||||||
@@ -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
|
package aghhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||||
"github.com/AdguardTeam/golibs/httphdr"
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTP scheme constants.
|
// HTTP scheme constants.
|
||||||
@@ -31,12 +34,39 @@ func OK(w http.ResponseWriter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Error writes formatted message to w and also logs it.
|
// Error writes formatted message to w and also logs it.
|
||||||
|
//
|
||||||
|
// TODO(s.chzhen): Remove it.
|
||||||
func Error(r *http.Request, w http.ResponseWriter, code int, format string, args ...any) {
|
func Error(r *http.Request, w http.ResponseWriter, code int, format string, args ...any) {
|
||||||
text := fmt.Sprintf(format, args...)
|
text := fmt.Sprintf(format, args...)
|
||||||
log.Error("%s %s %s: %s", r.Method, r.Host, r.URL, text)
|
log.Error("%s %s %s: %s", r.Method, r.Host, r.URL, text)
|
||||||
http.Error(w, text, code)
|
http.Error(w, text, code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrorAndLog writes formatted message to w and also logs it with the specified
|
||||||
|
// logging level.
|
||||||
|
func ErrorAndLog(
|
||||||
|
ctx context.Context,
|
||||||
|
l *slog.Logger,
|
||||||
|
r *http.Request,
|
||||||
|
w http.ResponseWriter,
|
||||||
|
code int,
|
||||||
|
format string,
|
||||||
|
args ...any,
|
||||||
|
) {
|
||||||
|
text := fmt.Sprintf(format, args...)
|
||||||
|
l.ErrorContext(
|
||||||
|
ctx,
|
||||||
|
"http error",
|
||||||
|
"host", r.Host,
|
||||||
|
"method", r.Method,
|
||||||
|
"raddr", r.RemoteAddr,
|
||||||
|
"request_uri", r.RequestURI,
|
||||||
|
slogutil.KeyError, text,
|
||||||
|
)
|
||||||
|
|
||||||
|
http.Error(w, text, code)
|
||||||
|
}
|
||||||
|
|
||||||
// UserAgent returns the ID of the service as a User-Agent string. It can also
|
// UserAgent returns the ID of the service as a User-Agent string. It can also
|
||||||
// be used as the value of the Server HTTP header.
|
// be used as the value of the Server HTTP header.
|
||||||
func UserAgent() (ua string) {
|
func UserAgent() (ua string) {
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ifaceHasStaticIP(ifaceName string) (ok bool, err error) {
|
func ifaceHasStaticIP(ifaceName string) (ok bool, err error) {
|
||||||
@@ -38,9 +38,13 @@ func (n interfaceName) rcConfStaticConfig(r io.Reader) (_ []string, cont bool, e
|
|||||||
// TODO(e.burkov): Expand the check to cover possible
|
// TODO(e.burkov): Expand the check to cover possible
|
||||||
// configurations from man rc.conf(5).
|
// configurations from man rc.conf(5).
|
||||||
fields := strings.Fields(line[cfgLeft:cfgRight])
|
fields := strings.Fields(line[cfgLeft:cfgRight])
|
||||||
if len(fields) >= 2 &&
|
switch {
|
||||||
strings.EqualFold(fields[0], "inet") &&
|
case
|
||||||
net.ParseIP(fields[1]) != nil {
|
len(fields) < 2,
|
||||||
|
!strings.EqualFold(fields[0], "inet"),
|
||||||
|
!netutil.IsValidIPString(fields[1]):
|
||||||
|
continue
|
||||||
|
default:
|
||||||
return nil, false, s.Err()
|
return nil, false, s.Err()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ifaceHasStaticIP(ifaceName string) (ok bool, err error) {
|
func ifaceHasStaticIP(ifaceName string) (ok bool, err error) {
|
||||||
@@ -25,7 +25,13 @@ func hostnameIfStaticConfig(r io.Reader) (_ []string, ok bool, err error) {
|
|||||||
for s.Scan() {
|
for s.Scan() {
|
||||||
line := strings.TrimSpace(s.Text())
|
line := strings.TrimSpace(s.Text())
|
||||||
fields := strings.Fields(line)
|
fields := strings.Fields(line)
|
||||||
if len(fields) >= 2 && fields[0] == "inet" && net.ParseIP(fields[1]) != nil {
|
switch {
|
||||||
|
case
|
||||||
|
len(fields) < 2,
|
||||||
|
fields[0] != "inet",
|
||||||
|
!netutil.IsValidIPString(fields[1]):
|
||||||
|
continue
|
||||||
|
default:
|
||||||
return nil, false, s.Err()
|
return nil, false, s.Err()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package aghos
|
package aghos
|
||||||
|
|
||||||
// ConfigureSyslog reroutes standard logger output to syslog.
|
// ConfigureSyslog reroutes standard logger output to syslog.
|
||||||
func ConfigureSyslog(serviceName string) error {
|
func ConfigureSyslog(serviceName string) (err error) {
|
||||||
return configureSyslog(serviceName)
|
return configureSyslog(serviceName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,15 @@ import (
|
|||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func configureSyslog(serviceName string) error {
|
// configureSyslog sets standard log output to syslog.
|
||||||
|
func configureSyslog(serviceName string) (err error) {
|
||||||
w, err := syslog.New(syslog.LOG_NOTICE|syslog.LOG_USER, serviceName)
|
w, err := syslog.New(syslog.LOG_NOTICE|syslog.LOG_USER, serviceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.SetOutput(w)
|
log.SetOutput(w)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,23 +19,30 @@ func (w *eventLogWriter) Write(b []byte) (int, error) {
|
|||||||
return len(b), w.el.Info(1, string(b))
|
return len(b), w.el.Info(1, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureSyslog(serviceName string) error {
|
// configureSyslog sets standard log output to event log.
|
||||||
// Note that the eventlog src is the same as the service name
|
func configureSyslog(serviceName string) (err error) {
|
||||||
// Otherwise, we will get "the description for event id cannot be found" warning in every log record
|
// Note that the eventlog src is the same as the service name, otherwise we
|
||||||
|
// will get "the description for event id cannot be found" warning in every
|
||||||
|
// log record.
|
||||||
|
|
||||||
// Continue if we receive "registry key already exists" or if we get
|
// Continue if we receive "registry key already exists" or if we get
|
||||||
// ERROR_ACCESS_DENIED so that we can log without administrative permissions
|
// ERROR_ACCESS_DENIED so that we can log without administrative permissions
|
||||||
// for pre-existing eventlog sources.
|
// for pre-existing eventlog sources.
|
||||||
if err := eventlog.InstallAsEventCreate(serviceName, eventlog.Info|eventlog.Warning|eventlog.Error); err != nil {
|
err = eventlog.InstallAsEventCreate(serviceName, eventlog.Info|eventlog.Warning|eventlog.Error)
|
||||||
if !strings.Contains(err.Error(), "registry key already exists") && err != windows.ERROR_ACCESS_DENIED {
|
if err != nil &&
|
||||||
return err
|
!strings.Contains(err.Error(), "registry key already exists") &&
|
||||||
}
|
err != windows.ERROR_ACCESS_DENIED {
|
||||||
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
el, err := eventlog.Open(serviceName)
|
el, err := eventlog.Open(serviceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.SetOutput(&eventLogWriter{el: el})
|
log.SetOutput(&eventLogWriter{el: el})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"slices"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// init makes sure that the cipher name map is filled.
|
// init makes sure that the cipher name map is filled.
|
||||||
@@ -75,15 +76,5 @@ func SaferCipherSuites() (safe []uint16) {
|
|||||||
// CertificateHasIP returns true if cert has at least a single IP address among
|
// CertificateHasIP returns true if cert has at least a single IP address among
|
||||||
// its subjectAltNames.
|
// its subjectAltNames.
|
||||||
func CertificateHasIP(cert *x509.Certificate) (ok bool) {
|
func CertificateHasIP(cert *x509.Certificate) (ok bool) {
|
||||||
if len(cert.IPAddresses) > 0 {
|
return len(cert.IPAddresses) > 0 || slices.ContainsFunc(cert.DNSNames, netutil.IsValidIPString)
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, name := range cert.DNSNames {
|
|
||||||
if _, err := netip.ParseAddr(name); err == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -12,7 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/osutil"
|
"github.com/AdguardTeam/golibs/osutil"
|
||||||
)
|
)
|
||||||
@@ -38,8 +39,8 @@ type Interface interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New returns the [Interface] properly initialized for the OS.
|
// New returns the [Interface] properly initialized for the OS.
|
||||||
func New() (arp Interface) {
|
func New(logger *slog.Logger) (arp Interface) {
|
||||||
return newARPDB()
|
return newARPDB(logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty is the [Interface] implementation that does nothing.
|
// Empty is the [Interface] implementation that does nothing.
|
||||||
@@ -69,6 +70,30 @@ type Neighbor struct {
|
|||||||
MAC net.HardwareAddr
|
MAC net.HardwareAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newNeighbor returns the new initialized [Neighbor] by parsing string
|
||||||
|
// representations of IP and MAC addresses.
|
||||||
|
func newNeighbor(host, ipStr, macStr string) (n *Neighbor, err error) {
|
||||||
|
defer func() { err = errors.Annotate(err, "getting arp neighbor: %w") }()
|
||||||
|
|
||||||
|
ip, err := netip.ParseAddr(ipStr)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error, as it will get annotated.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mac, err := net.ParseMAC(macStr)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error, as it will get annotated.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Neighbor{
|
||||||
|
Name: host,
|
||||||
|
IP: ip,
|
||||||
|
MAC: mac,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Clone returns the deep copy of n.
|
// Clone returns the deep copy of n.
|
||||||
func (n Neighbor) Clone() (clone Neighbor) {
|
func (n Neighbor) Clone() (clone Neighbor) {
|
||||||
return Neighbor{
|
return Neighbor{
|
||||||
@@ -80,10 +105,10 @@ func (n Neighbor) Clone() (clone Neighbor) {
|
|||||||
|
|
||||||
// validatedHostname returns h if it's a valid hostname, or an empty string
|
// validatedHostname returns h if it's a valid hostname, or an empty string
|
||||||
// otherwise, logging the validation error.
|
// otherwise, logging the validation error.
|
||||||
func validatedHostname(h string) (host string) {
|
func validatedHostname(logger *slog.Logger, h string) (host string) {
|
||||||
err := netutil.ValidateHostname(h)
|
err := netutil.ValidateHostname(h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("arpdb: parsing arp output: host: %s", err)
|
logger.Debug("parsing host of arp output", slogutil.KeyError, err)
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -132,15 +157,18 @@ func (ns *neighs) reset(with []Neighbor) {
|
|||||||
// parseNeighsFunc parses the text from sc as if it'd be an output of some
|
// parseNeighsFunc parses the text from sc as if it'd be an output of some
|
||||||
// ARP-related command. lenHint is a hint for the size of the allocated slice
|
// ARP-related command. lenHint is a hint for the size of the allocated slice
|
||||||
// of Neighbors.
|
// of Neighbors.
|
||||||
type parseNeighsFunc func(sc *bufio.Scanner, lenHint int) (ns []Neighbor)
|
//
|
||||||
|
// TODO(s.chzhen): Return []*Neighbor instead.
|
||||||
|
type parseNeighsFunc func(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor)
|
||||||
|
|
||||||
// cmdARPDB is the implementation of the [Interface] that uses command line to
|
// cmdARPDB is the implementation of the [Interface] that uses command line to
|
||||||
// retrieve data.
|
// retrieve data.
|
||||||
type cmdARPDB struct {
|
type cmdARPDB struct {
|
||||||
parse parseNeighsFunc
|
logger *slog.Logger
|
||||||
ns *neighs
|
parse parseNeighsFunc
|
||||||
cmd string
|
ns *neighs
|
||||||
args []string
|
cmd string
|
||||||
|
args []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
@@ -158,7 +186,7 @@ func (arp *cmdARPDB) Refresh() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sc := bufio.NewScanner(bytes.NewReader(out))
|
sc := bufio.NewScanner(bytes.NewReader(out))
|
||||||
ns := arp.parse(sc, arp.ns.len())
|
ns := arp.parse(arp.logger, sc, arp.ns.len())
|
||||||
if err = sc.Err(); err != nil {
|
if err = sc.Err(); err != nil {
|
||||||
// TODO(e.burkov): This error seems unreachable. Investigate.
|
// TODO(e.burkov): This error seems unreachable. Investigate.
|
||||||
return fmt.Errorf("scanning the output: %w", err)
|
return fmt.Errorf("scanning the output: %w", err)
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ package arpdb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"net"
|
"log/slog"
|
||||||
"net/netip"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newARPDB() (arp *cmdARPDB) {
|
func newARPDB(logger *slog.Logger) (arp *cmdARPDB) {
|
||||||
return &cmdARPDB{
|
return &cmdARPDB{
|
||||||
parse: parseArpA,
|
logger: logger,
|
||||||
|
parse: parseArpA,
|
||||||
ns: &neighs{
|
ns: &neighs{
|
||||||
mu: &sync.RWMutex{},
|
mu: &sync.RWMutex{},
|
||||||
ns: make([]Neighbor, 0),
|
ns: make([]Neighbor, 0),
|
||||||
@@ -33,7 +33,7 @@ func newARPDB() (arp *cmdARPDB) {
|
|||||||
// The expected input format:
|
// The expected input format:
|
||||||
//
|
//
|
||||||
// host.name (192.168.0.1) at ff:ff:ff:ff:ff:ff on en0 ifscope [ethernet]
|
// host.name (192.168.0.1) at ff:ff:ff:ff:ff:ff on en0 ifscope [ethernet]
|
||||||
func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
func parseArpA(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
||||||
ns = make([]Neighbor, 0, lenHint)
|
ns = make([]Neighbor, 0, lenHint)
|
||||||
for sc.Scan() {
|
for sc.Scan() {
|
||||||
ln := sc.Text()
|
ln := sc.Text()
|
||||||
@@ -48,26 +48,15 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ip, err := netip.ParseAddr(ipStr[1 : len(ipStr)-1])
|
host := validatedHostname(logger, fields[0])
|
||||||
|
n, err := newNeighbor(host, ipStr[1:len(ipStr)-1], fields[3])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("arpdb: parsing arp output: ip: %s", err)
|
logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
hwStr := fields[3]
|
ns = append(ns, *n)
|
||||||
mac, err := net.ParseMAC(hwStr)
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("arpdb: parsing arp output: mac: %s", err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ns = append(ns, Neighbor{
|
|
||||||
IP: ip,
|
|
||||||
MAC: mac,
|
|
||||||
Name: validatedHostname(fields[0]),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ns
|
return ns
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -61,7 +62,7 @@ func (s mapShell) RunCmd(cmd string, args ...string) (code int, out []byte, err
|
|||||||
|
|
||||||
func Test_New(t *testing.T) {
|
func Test_New(t *testing.T) {
|
||||||
var a Interface
|
var a Interface
|
||||||
require.NotPanics(t, func() { a = New() })
|
require.NotPanics(t, func() { a = New(slogutil.NewDiscardLogger()) })
|
||||||
|
|
||||||
assert.NotNil(t, a)
|
assert.NotNil(t, a)
|
||||||
}
|
}
|
||||||
@@ -201,8 +202,9 @@ func Test_NewARPDBs(t *testing.T) {
|
|||||||
|
|
||||||
func TestCmdARPDB_arpa(t *testing.T) {
|
func TestCmdARPDB_arpa(t *testing.T) {
|
||||||
a := &cmdARPDB{
|
a := &cmdARPDB{
|
||||||
cmd: "cmd",
|
logger: slogutil.NewDiscardLogger(),
|
||||||
parse: parseArpA,
|
cmd: "cmd",
|
||||||
|
parse: parseArpA,
|
||||||
ns: &neighs{
|
ns: &neighs{
|
||||||
mu: &sync.RWMutex{},
|
mu: &sync.RWMutex{},
|
||||||
ns: make([]Neighbor, 0),
|
ns: make([]Neighbor, 0),
|
||||||
|
|||||||
@@ -6,17 +6,18 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/stringutil"
|
"github.com/AdguardTeam/golibs/stringutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newARPDB() (arp *arpdbs) {
|
func newARPDB(logger *slog.Logger) (arp *arpdbs) {
|
||||||
// Use the common storage among the implementations.
|
// Use the common storage among the implementations.
|
||||||
ns := &neighs{
|
ns := &neighs{
|
||||||
mu: &sync.RWMutex{},
|
mu: &sync.RWMutex{},
|
||||||
@@ -39,9 +40,10 @@ func newARPDB() (arp *arpdbs) {
|
|||||||
},
|
},
|
||||||
// Then, try "arp -a -n".
|
// Then, try "arp -a -n".
|
||||||
&cmdARPDB{
|
&cmdARPDB{
|
||||||
parse: parseF,
|
logger: logger,
|
||||||
ns: ns,
|
parse: parseF,
|
||||||
cmd: "arp",
|
ns: ns,
|
||||||
|
cmd: "arp",
|
||||||
// Use -n flag to avoid resolving the hostnames of the neighbors.
|
// Use -n flag to avoid resolving the hostnames of the neighbors.
|
||||||
// By default ARP attempts to resolve the hostnames via DNS. See
|
// By default ARP attempts to resolve the hostnames via DNS. See
|
||||||
// man 8 arp.
|
// man 8 arp.
|
||||||
@@ -51,10 +53,11 @@ func newARPDB() (arp *arpdbs) {
|
|||||||
},
|
},
|
||||||
// Finally, try "ip neigh".
|
// Finally, try "ip neigh".
|
||||||
&cmdARPDB{
|
&cmdARPDB{
|
||||||
parse: parseIPNeigh,
|
logger: logger,
|
||||||
ns: ns,
|
parse: parseIPNeigh,
|
||||||
cmd: "ip",
|
ns: ns,
|
||||||
args: []string{"neigh"},
|
cmd: "ip",
|
||||||
|
args: []string{"neigh"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -131,7 +134,7 @@ func (arp *fsysARPDB) Neighbors() (ns []Neighbor) {
|
|||||||
//
|
//
|
||||||
// IP address HW type Flags HW address Mask Device
|
// IP address HW type Flags HW address Mask Device
|
||||||
// 192.168.11.98 0x1 0x2 5a:92:df:a9:7e:28 * wan
|
// 192.168.11.98 0x1 0x2 5a:92:df:a9:7e:28 * wan
|
||||||
func parseArpAWrt(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
func parseArpAWrt(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
||||||
if !sc.Scan() {
|
if !sc.Scan() {
|
||||||
// Skip the header.
|
// Skip the header.
|
||||||
return
|
return
|
||||||
@@ -146,25 +149,14 @@ func parseArpAWrt(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ip, err := netip.ParseAddr(fields[0])
|
n, err := newNeighbor("", fields[0], fields[3])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("arpdb: parsing arp output: ip: %s", err)
|
logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
hwStr := fields[3]
|
ns = append(ns, *n)
|
||||||
mac, err := net.ParseMAC(hwStr)
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("arpdb: parsing arp output: mac: %s", err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ns = append(ns, Neighbor{
|
|
||||||
IP: ip,
|
|
||||||
MAC: mac,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ns
|
return ns
|
||||||
@@ -174,7 +166,7 @@ func parseArpAWrt(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
|||||||
// expected input format:
|
// expected input format:
|
||||||
//
|
//
|
||||||
// hostname (192.168.1.1) at ab:cd:ef:ab:cd:ef [ether] on enp0s3
|
// hostname (192.168.1.1) at ab:cd:ef:ab:cd:ef [ether] on enp0s3
|
||||||
func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
func parseArpA(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
||||||
ns = make([]Neighbor, 0, lenHint)
|
ns = make([]Neighbor, 0, lenHint)
|
||||||
for sc.Scan() {
|
for sc.Scan() {
|
||||||
ln := sc.Text()
|
ln := sc.Text()
|
||||||
@@ -189,26 +181,15 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ip, err := netip.ParseAddr(ipStr[1 : len(ipStr)-1])
|
host := validatedHostname(logger, fields[0])
|
||||||
|
n, err := newNeighbor(host, ipStr[1:len(ipStr)-1], fields[3])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("arpdb: parsing arp output: ip: %s", err)
|
logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
hwStr := fields[3]
|
ns = append(ns, *n)
|
||||||
mac, err := net.ParseMAC(hwStr)
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("arpdb: parsing arp output: mac: %s", err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ns = append(ns, Neighbor{
|
|
||||||
IP: ip,
|
|
||||||
MAC: mac,
|
|
||||||
Name: validatedHostname(fields[0]),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ns
|
return ns
|
||||||
@@ -218,7 +199,7 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
|||||||
// expected input format:
|
// expected input format:
|
||||||
//
|
//
|
||||||
// 192.168.1.1 dev enp0s3 lladdr ab:cd:ef:ab:cd:ef REACHABLE
|
// 192.168.1.1 dev enp0s3 lladdr ab:cd:ef:ab:cd:ef REACHABLE
|
||||||
func parseIPNeigh(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
func parseIPNeigh(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
||||||
ns = make([]Neighbor, 0, lenHint)
|
ns = make([]Neighbor, 0, lenHint)
|
||||||
for sc.Scan() {
|
for sc.Scan() {
|
||||||
ln := sc.Text()
|
ln := sc.Text()
|
||||||
@@ -228,27 +209,14 @@ func parseIPNeigh(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
n := Neighbor{}
|
n, err := newNeighbor("", fields[0], fields[4])
|
||||||
|
|
||||||
ip, err := netip.ParseAddr(fields[0])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("arpdb: parsing arp output: ip: %s", err)
|
logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
} else {
|
|
||||||
n.IP = ip
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mac, err := net.ParseMAC(fields[4])
|
ns = append(ns, *n)
|
||||||
if err != nil {
|
|
||||||
log.Debug("arpdb: parsing arp output: mac: %s", err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
n.MAC = mac
|
|
||||||
}
|
|
||||||
|
|
||||||
ns = append(ns, n)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ns
|
return ns
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -69,9 +70,10 @@ func TestCmdARPDB_linux(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("wrt", func(t *testing.T) {
|
t.Run("wrt", func(t *testing.T) {
|
||||||
a := &cmdARPDB{
|
a := &cmdARPDB{
|
||||||
parse: parseArpAWrt,
|
logger: slogutil.NewDiscardLogger(),
|
||||||
cmd: "arp",
|
parse: parseArpAWrt,
|
||||||
args: []string{"-a"},
|
cmd: "arp",
|
||||||
|
args: []string{"-a"},
|
||||||
ns: &neighs{
|
ns: &neighs{
|
||||||
mu: &sync.RWMutex{},
|
mu: &sync.RWMutex{},
|
||||||
ns: make([]Neighbor, 0),
|
ns: make([]Neighbor, 0),
|
||||||
@@ -86,9 +88,10 @@ func TestCmdARPDB_linux(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("ip_neigh", func(t *testing.T) {
|
t.Run("ip_neigh", func(t *testing.T) {
|
||||||
a := &cmdARPDB{
|
a := &cmdARPDB{
|
||||||
parse: parseIPNeigh,
|
logger: slogutil.NewDiscardLogger(),
|
||||||
cmd: "ip",
|
parse: parseIPNeigh,
|
||||||
args: []string{"neigh"},
|
cmd: "ip",
|
||||||
|
args: []string{"neigh"},
|
||||||
ns: &neighs{
|
ns: &neighs{
|
||||||
mu: &sync.RWMutex{},
|
mu: &sync.RWMutex{},
|
||||||
ns: make([]Neighbor, 0),
|
ns: make([]Neighbor, 0),
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ package arpdb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"net"
|
"log/slog"
|
||||||
"net/netip"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newARPDB() (arp *cmdARPDB) {
|
func newARPDB(logger *slog.Logger) (arp *cmdARPDB) {
|
||||||
return &cmdARPDB{
|
return &cmdARPDB{
|
||||||
parse: parseArpA,
|
logger: logger,
|
||||||
|
parse: parseArpA,
|
||||||
ns: &neighs{
|
ns: &neighs{
|
||||||
mu: &sync.RWMutex{},
|
mu: &sync.RWMutex{},
|
||||||
ns: make([]Neighbor, 0),
|
ns: make([]Neighbor, 0),
|
||||||
@@ -34,7 +34,7 @@ func newARPDB() (arp *cmdARPDB) {
|
|||||||
//
|
//
|
||||||
// Host Ethernet Address Netif Expire Flags
|
// Host Ethernet Address Netif Expire Flags
|
||||||
// 192.168.1.1 ab:cd:ef:ab:cd:ef em0 19m59s
|
// 192.168.1.1 ab:cd:ef:ab:cd:ef em0 19m59s
|
||||||
func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
func parseArpA(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
||||||
// Skip the header.
|
// Skip the header.
|
||||||
if !sc.Scan() {
|
if !sc.Scan() {
|
||||||
return nil
|
return nil
|
||||||
@@ -49,27 +49,14 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
n := Neighbor{}
|
n, err := newNeighbor("", fields[0], fields[1])
|
||||||
|
|
||||||
ip, err := netip.ParseAddr(fields[0])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("arpdb: parsing arp output: ip: %s", err)
|
logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
} else {
|
|
||||||
n.IP = ip
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mac, err := net.ParseMAC(fields[1])
|
ns = append(ns, *n)
|
||||||
if err != nil {
|
|
||||||
log.Debug("arpdb: parsing arp output: mac: %s", err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
n.MAC = mac
|
|
||||||
}
|
|
||||||
|
|
||||||
ns = append(ns, n)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ns
|
return ns
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ package arpdb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"net"
|
"log/slog"
|
||||||
"net/netip"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newARPDB() (arp *cmdARPDB) {
|
func newARPDB(logger *slog.Logger) (arp *cmdARPDB) {
|
||||||
return &cmdARPDB{
|
return &cmdARPDB{
|
||||||
parse: parseArpA,
|
logger: logger,
|
||||||
|
parse: parseArpA,
|
||||||
ns: &neighs{
|
ns: &neighs{
|
||||||
mu: &sync.RWMutex{},
|
mu: &sync.RWMutex{},
|
||||||
ns: make([]Neighbor, 0),
|
ns: make([]Neighbor, 0),
|
||||||
@@ -31,7 +31,7 @@ func newARPDB() (arp *cmdARPDB) {
|
|||||||
// Internet Address Physical Address Type
|
// Internet Address Physical Address Type
|
||||||
// 192.168.56.1 0a-00-27-00-00-00 dynamic
|
// 192.168.56.1 0a-00-27-00-00-00 dynamic
|
||||||
// 192.168.56.255 ff-ff-ff-ff-ff-ff static
|
// 192.168.56.255 ff-ff-ff-ff-ff-ff static
|
||||||
func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
func parseArpA(logger *slog.Logger, sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
||||||
ns = make([]Neighbor, 0, lenHint)
|
ns = make([]Neighbor, 0, lenHint)
|
||||||
for sc.Scan() {
|
for sc.Scan() {
|
||||||
ln := sc.Text()
|
ln := sc.Text()
|
||||||
@@ -44,24 +44,14 @@ func parseArpA(sc *bufio.Scanner, lenHint int) (ns []Neighbor) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ip, err := netip.ParseAddr(fields[0])
|
n, err := newNeighbor("", fields[0], fields[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("arpdb: parsing arp output: ip: %s", err)
|
logger.Debug("parsing arp output", "line", ln, slogutil.KeyError, err)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
mac, err := net.ParseMAC(fields[1])
|
ns = append(ns, *n)
|
||||||
if err != nil {
|
|
||||||
log.Debug("arpdb: parsing arp output: mac: %s", err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
ns = append(ns, Neighbor{
|
|
||||||
IP: ip,
|
|
||||||
MAC: mac,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ns
|
return ns
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,6 +40,10 @@ func (EmptyAddrProc) Close() (_ error) { return nil }
|
|||||||
|
|
||||||
// DefaultAddrProcConfig is the configuration structure for address processors.
|
// DefaultAddrProcConfig is the configuration structure for address processors.
|
||||||
type DefaultAddrProcConfig struct {
|
type DefaultAddrProcConfig struct {
|
||||||
|
// BaseLogger is used to create loggers with custom prefixes for sources of
|
||||||
|
// information about runtime clients. It must not be nil.
|
||||||
|
BaseLogger *slog.Logger
|
||||||
|
|
||||||
// DialContext is used to create TCP connections to WHOIS servers.
|
// DialContext is used to create TCP connections to WHOIS servers.
|
||||||
// DialContext must not be nil if [DefaultAddrProcConfig.UseWHOIS] is true.
|
// DialContext must not be nil if [DefaultAddrProcConfig.UseWHOIS] is true.
|
||||||
DialContext aghnet.DialContextFunc
|
DialContext aghnet.DialContextFunc
|
||||||
@@ -147,6 +153,7 @@ func NewDefaultAddrProc(c *DefaultAddrProcConfig) (p *DefaultAddrProc) {
|
|||||||
|
|
||||||
if c.UseRDNS {
|
if c.UseRDNS {
|
||||||
p.rdns = rdns.New(&rdns.Config{
|
p.rdns = rdns.New(&rdns.Config{
|
||||||
|
Logger: c.BaseLogger.With(slogutil.KeyPrefix, "rdns"),
|
||||||
Exchanger: c.Exchanger,
|
Exchanger: c.Exchanger,
|
||||||
CacheSize: defaultCacheSize,
|
CacheSize: defaultCacheSize,
|
||||||
CacheTTL: defaultIPTTL,
|
CacheTTL: defaultIPTTL,
|
||||||
@@ -154,7 +161,7 @@ func NewDefaultAddrProc(c *DefaultAddrProcConfig) (p *DefaultAddrProc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.UseWHOIS {
|
if c.UseWHOIS {
|
||||||
p.whois = newWHOIS(c.DialContext)
|
p.whois = newWHOIS(c.BaseLogger.With(slogutil.KeyPrefix, "whois"), c.DialContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
go p.process(c.CatchPanics)
|
go p.process(c.CatchPanics)
|
||||||
@@ -168,7 +175,7 @@ func NewDefaultAddrProc(c *DefaultAddrProcConfig) (p *DefaultAddrProc) {
|
|||||||
|
|
||||||
// newWHOIS returns a whois.Interface instance using the given function for
|
// newWHOIS returns a whois.Interface instance using the given function for
|
||||||
// dialing.
|
// dialing.
|
||||||
func newWHOIS(dialFunc aghnet.DialContextFunc) (w whois.Interface) {
|
func newWHOIS(logger *slog.Logger, dialFunc aghnet.DialContextFunc) (w whois.Interface) {
|
||||||
// TODO(s.chzhen): Consider making configurable.
|
// TODO(s.chzhen): Consider making configurable.
|
||||||
const (
|
const (
|
||||||
// defaultTimeout is the timeout for WHOIS requests.
|
// defaultTimeout is the timeout for WHOIS requests.
|
||||||
@@ -186,6 +193,7 @@ func newWHOIS(dialFunc aghnet.DialContextFunc) (w whois.Interface) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return whois.New(&whois.Config{
|
return whois.New(&whois.Config{
|
||||||
|
Logger: logger,
|
||||||
DialContext: dialFunc,
|
DialContext: dialFunc,
|
||||||
ServerAddr: whois.DefaultServer,
|
ServerAddr: whois.DefaultServer,
|
||||||
Port: whois.DefaultPort,
|
Port: whois.DefaultPort,
|
||||||
@@ -227,9 +235,11 @@ func (p *DefaultAddrProc) process(catchPanics bool) {
|
|||||||
|
|
||||||
log.Info("clients: processing addresses")
|
log.Info("clients: processing addresses")
|
||||||
|
|
||||||
|
ctx := context.TODO()
|
||||||
|
|
||||||
for ip := range p.clientIPs {
|
for ip := range p.clientIPs {
|
||||||
host := p.processRDNS(ip)
|
host := p.processRDNS(ctx, ip)
|
||||||
info := p.processWHOIS(ip)
|
info := p.processWHOIS(ctx, ip)
|
||||||
|
|
||||||
p.addrUpdater.UpdateAddress(ip, host, info)
|
p.addrUpdater.UpdateAddress(ip, host, info)
|
||||||
}
|
}
|
||||||
@@ -239,7 +249,7 @@ func (p *DefaultAddrProc) process(catchPanics bool) {
|
|||||||
|
|
||||||
// processRDNS resolves the clients' IP addresses using reverse DNS. host is
|
// processRDNS resolves the clients' IP addresses using reverse DNS. host is
|
||||||
// empty if there were errors or if the information hasn't changed.
|
// empty if there were errors or if the information hasn't changed.
|
||||||
func (p *DefaultAddrProc) processRDNS(ip netip.Addr) (host string) {
|
func (p *DefaultAddrProc) processRDNS(ctx context.Context, ip netip.Addr) (host string) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
log.Debug("clients: processing %s with rdns", ip)
|
log.Debug("clients: processing %s with rdns", ip)
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -251,7 +261,7 @@ func (p *DefaultAddrProc) processRDNS(ip netip.Addr) (host string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
host, changed := p.rdns.Process(ip)
|
host, changed := p.rdns.Process(ctx, ip)
|
||||||
if !changed {
|
if !changed {
|
||||||
host = ""
|
host = ""
|
||||||
}
|
}
|
||||||
@@ -268,7 +278,7 @@ func (p *DefaultAddrProc) shouldResolve(ip netip.Addr) (ok bool) {
|
|||||||
// processWHOIS looks up the information about clients' IP addresses in the
|
// processWHOIS looks up the information about clients' IP addresses in the
|
||||||
// WHOIS databases. info is nil if there were errors or if the information
|
// WHOIS databases. info is nil if there were errors or if the information
|
||||||
// hasn't changed.
|
// hasn't changed.
|
||||||
func (p *DefaultAddrProc) processWHOIS(ip netip.Addr) (info *whois.Info) {
|
func (p *DefaultAddrProc) processWHOIS(ctx context.Context, ip netip.Addr) (info *whois.Info) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
log.Debug("clients: processing %s with whois", ip)
|
log.Debug("clients: processing %s with whois", ip)
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -277,7 +287,7 @@ func (p *DefaultAddrProc) processWHOIS(ip netip.Addr) (info *whois.Info) {
|
|||||||
|
|
||||||
// TODO(s.chzhen): Move the timeout logic from WHOIS configuration to the
|
// TODO(s.chzhen): Move the timeout logic from WHOIS configuration to the
|
||||||
// context.
|
// context.
|
||||||
info, changed := p.whois.Process(context.Background(), ip)
|
info, changed := p.whois.Process(ctx, ip)
|
||||||
if !changed {
|
if !changed {
|
||||||
info = nil
|
info = nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/client"
|
"github.com/AdguardTeam/AdGuardHome/internal/client"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil/fakenet"
|
"github.com/AdguardTeam/golibs/testutil/fakenet"
|
||||||
@@ -99,6 +100,7 @@ func TestDefaultAddrProc_Process_rDNS(t *testing.T) {
|
|||||||
updInfoCh := make(chan *whois.Info, 1)
|
updInfoCh := make(chan *whois.Info, 1)
|
||||||
|
|
||||||
p := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{
|
p := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{
|
||||||
|
BaseLogger: slogutil.NewDiscardLogger(),
|
||||||
DialContext: func(_ context.Context, _, _ string) (conn net.Conn, err error) {
|
DialContext: func(_ context.Context, _, _ string) (conn net.Conn, err error) {
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
},
|
},
|
||||||
@@ -208,6 +210,7 @@ func TestDefaultAddrProc_Process_WHOIS(t *testing.T) {
|
|||||||
updInfoCh := make(chan *whois.Info, 1)
|
updInfoCh := make(chan *whois.Info, 1)
|
||||||
|
|
||||||
p := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{
|
p := client.NewDefaultAddrProc(&client.DefaultAddrProcConfig{
|
||||||
|
BaseLogger: slogutil.NewDiscardLogger(),
|
||||||
DialContext: func(_ context.Context, _, _ string) (conn net.Conn, err error) {
|
DialContext: func(_ context.Context, _, _ string) (conn net.Conn, err error) {
|
||||||
return whoisConn, nil
|
return whoisConn, nil
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"encoding"
|
"encoding"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||||
)
|
)
|
||||||
@@ -120,6 +121,7 @@ func (r *Runtime) Info() (cs Source, host string) {
|
|||||||
|
|
||||||
// SetInfo sets a host as a client information from the cs.
|
// SetInfo sets a host as a client information from the cs.
|
||||||
func (r *Runtime) SetInfo(cs Source, hosts []string) {
|
func (r *Runtime) SetInfo(cs Source, hosts []string) {
|
||||||
|
// TODO(s.chzhen): Use contract where hosts must contain non-empty host.
|
||||||
if len(hosts) == 1 && hosts[0] == "" {
|
if len(hosts) == 1 && hosts[0] == "" {
|
||||||
hosts = []string{}
|
hosts = []string{}
|
||||||
}
|
}
|
||||||
@@ -175,3 +177,15 @@ func (r *Runtime) isEmpty() (ok bool) {
|
|||||||
func (r *Runtime) Addr() (ip netip.Addr) {
|
func (r *Runtime) Addr() (ip netip.Addr) {
|
||||||
return r.ip
|
return r.ip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clone returns a deep copy of the runtime client.
|
||||||
|
func (r *Runtime) Clone() (c *Runtime) {
|
||||||
|
return &Runtime{
|
||||||
|
ip: r.ip,
|
||||||
|
whois: r.whois.Clone(),
|
||||||
|
arp: slices.Clone(r.arp),
|
||||||
|
rdns: slices.Clone(r.rdns),
|
||||||
|
dhcp: slices.Clone(r.dhcp),
|
||||||
|
hostsFile: slices.Clone(r.hostsFile),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/container"
|
"github.com/AdguardTeam/golibs/container"
|
||||||
@@ -31,8 +32,6 @@ type Storage struct {
|
|||||||
index *index
|
index *index
|
||||||
|
|
||||||
// runtimeIndex contains information about runtime clients.
|
// runtimeIndex contains information about runtime clients.
|
||||||
//
|
|
||||||
// TODO(s.chzhen): Use it.
|
|
||||||
runtimeIndex *RuntimeIndex
|
runtimeIndex *RuntimeIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,20 +235,75 @@ func (s *Storage) ClientRuntime(ip netip.Addr) (rc *Runtime) {
|
|||||||
return s.runtimeIndex.Client(ip)
|
return s.runtimeIndex.Client(ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddRuntime saves the runtime client information in the storage. IP address
|
// UpdateRuntime updates the stored runtime client with information from rc. If
|
||||||
// of a client must be unique. rc must not be nil.
|
// no such client exists, saves the copy of rc in storage. rc must not be nil.
|
||||||
//
|
func (s *Storage) UpdateRuntime(rc *Runtime) (added bool) {
|
||||||
// TODO(s.chzhen): Use it.
|
|
||||||
func (s *Storage) AddRuntime(rc *Runtime) {
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
s.runtimeIndex.Add(rc)
|
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.
|
// SizeRuntime returns the number of the runtime clients.
|
||||||
//
|
|
||||||
// TODO(s.chzhen): Use it.
|
|
||||||
func (s *Storage) SizeRuntime() (n int) {
|
func (s *Storage) SizeRuntime() (n int) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
@@ -258,8 +312,6 @@ func (s *Storage) SizeRuntime() (n int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RangeRuntime calls f for each runtime client in an undefined order.
|
// RangeRuntime calls f for each runtime client in an undefined order.
|
||||||
//
|
|
||||||
// TODO(s.chzhen): Use it.
|
|
||||||
func (s *Storage) RangeRuntime(f func(rc *Runtime) (cont bool)) {
|
func (s *Storage) RangeRuntime(f func(rc *Runtime) (cont bool)) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
@@ -267,16 +319,6 @@ func (s *Storage) RangeRuntime(f func(rc *Runtime) (cont bool)) {
|
|||||||
s.runtimeIndex.Range(f)
|
s.runtimeIndex.Range(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteRuntime removes the runtime client by ip.
|
|
||||||
//
|
|
||||||
// TODO(s.chzhen): Use it.
|
|
||||||
func (s *Storage) DeleteRuntime(ip netip.Addr) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
s.runtimeIndex.Delete(ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteBySource removes all runtime clients that have information only from
|
// DeleteBySource removes all runtime clients that have information only from
|
||||||
// the specified source and returns the number of removed clients.
|
// the specified source and returns the number of removed clients.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/client"
|
"github.com/AdguardTeam/AdGuardHome/internal/client"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -25,9 +26,19 @@ func newStorage(tb testing.TB, m []*client.Persistent) (s *client.Storage) {
|
|||||||
require.NoError(tb, s.Add(c))
|
require.NoError(tb, s.Add(c))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
require.Equal(tb, len(m), s.Size())
|
||||||
|
|
||||||
return s
|
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
|
// mustParseMAC is wrapper around [net.ParseMAC] that panics if there is an
|
||||||
// error.
|
// error.
|
||||||
func mustParseMAC(s string) (mac net.HardwareAddr) {
|
func mustParseMAC(s string) (mac net.HardwareAddr) {
|
||||||
@@ -43,6 +54,9 @@ func TestStorage_Add(t *testing.T) {
|
|||||||
const (
|
const (
|
||||||
existingName = "existing_name"
|
existingName = "existing_name"
|
||||||
existingClientID = "existing_client_id"
|
existingClientID = "existing_client_id"
|
||||||
|
|
||||||
|
allowedTag = "tag"
|
||||||
|
notAllowedTag = "not_allowed_tag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -60,7 +74,7 @@ func TestStorage_Add(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s := client.NewStorage(&client.Config{
|
s := client.NewStorage(&client.Config{
|
||||||
AllowedTags: nil,
|
AllowedTags: []string{allowedTag},
|
||||||
})
|
})
|
||||||
err := s.Add(existingClient)
|
err := s.Add(existingClient)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -119,6 +133,15 @@ func TestStorage_Add(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErrMsg: `adding client: another client "existing_name" ` +
|
wantErrMsg: `adding client: another client "existing_name" ` +
|
||||||
`uses the same ClientID "existing_client_id"`,
|
`uses the same ClientID "existing_client_id"`,
|
||||||
|
}, {
|
||||||
|
name: "not_allowed_tag",
|
||||||
|
cli: &client.Persistent{
|
||||||
|
Name: "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 {
|
for _, tc := range testCases {
|
||||||
@@ -341,6 +364,127 @@ func TestStorage_FindLoose(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStorage_FindByName(t *testing.T) {
|
||||||
|
const (
|
||||||
|
cliIP1 = "1.1.1.1"
|
||||||
|
cliIP2 = "2.2.2.2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
clientExistingName = "client_existing"
|
||||||
|
clientAnotherExistingName = "client_another_existing"
|
||||||
|
nonExistingClientName = "client_non_existing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
clientExisting = &client.Persistent{
|
||||||
|
Name: clientExistingName,
|
||||||
|
IPs: []netip.Addr{netip.MustParseAddr(cliIP1)},
|
||||||
|
}
|
||||||
|
|
||||||
|
clientAnotherExisting = &client.Persistent{
|
||||||
|
Name: clientAnotherExistingName,
|
||||||
|
IPs: []netip.Addr{netip.MustParseAddr(cliIP2)},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
clients := []*client.Persistent{
|
||||||
|
clientExisting,
|
||||||
|
clientAnotherExisting,
|
||||||
|
}
|
||||||
|
s := newStorage(t, clients)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
want *client.Persistent
|
||||||
|
name string
|
||||||
|
clientName string
|
||||||
|
}{{
|
||||||
|
name: "existing",
|
||||||
|
clientName: clientExistingName,
|
||||||
|
want: clientExisting,
|
||||||
|
}, {
|
||||||
|
name: "another_existing",
|
||||||
|
clientName: clientAnotherExistingName,
|
||||||
|
want: clientAnotherExisting,
|
||||||
|
}, {
|
||||||
|
name: "non_existing",
|
||||||
|
clientName: nonExistingClientName,
|
||||||
|
want: nil,
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
c, ok := s.FindByName(tc.clientName)
|
||||||
|
if tc.want == nil {
|
||||||
|
assert.False(t, ok)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, tc.want, c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStorage_FindByMAC(t *testing.T) {
|
||||||
|
var (
|
||||||
|
cliMAC = mustParseMAC("11:11:11:11:11:11")
|
||||||
|
cliAnotherMAC = mustParseMAC("22:22:22:22:22:22")
|
||||||
|
nonExistingClientMAC = mustParseMAC("33:33:33:33:33:33")
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
clientExisting = &client.Persistent{
|
||||||
|
Name: "client",
|
||||||
|
MACs: []net.HardwareAddr{cliMAC},
|
||||||
|
}
|
||||||
|
|
||||||
|
clientAnotherExisting = &client.Persistent{
|
||||||
|
Name: "another_client",
|
||||||
|
MACs: []net.HardwareAddr{cliAnotherMAC},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
clients := []*client.Persistent{
|
||||||
|
clientExisting,
|
||||||
|
clientAnotherExisting,
|
||||||
|
}
|
||||||
|
s := newStorage(t, clients)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
want *client.Persistent
|
||||||
|
name string
|
||||||
|
clientMAC net.HardwareAddr
|
||||||
|
}{{
|
||||||
|
name: "existing",
|
||||||
|
clientMAC: cliMAC,
|
||||||
|
want: clientExisting,
|
||||||
|
}, {
|
||||||
|
name: "another_existing",
|
||||||
|
clientMAC: cliAnotherMAC,
|
||||||
|
want: clientAnotherExisting,
|
||||||
|
}, {
|
||||||
|
name: "non_existing",
|
||||||
|
clientMAC: nonExistingClientMAC,
|
||||||
|
want: nil,
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
c, ok := s.FindByMAC(tc.clientMAC)
|
||||||
|
if tc.want == nil {
|
||||||
|
assert.False(t, ok)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, tc.want, c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStorage_Update(t *testing.T) {
|
func TestStorage_Update(t *testing.T) {
|
||||||
const (
|
const (
|
||||||
clientName = "client_name"
|
clientName = "client_name"
|
||||||
@@ -479,3 +623,157 @@ func TestStorage_RangeByName(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package dhcpsvc
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
@@ -23,7 +24,8 @@ type Config struct {
|
|||||||
// clients' hostnames.
|
// clients' hostnames.
|
||||||
LocalDomainName string
|
LocalDomainName string
|
||||||
|
|
||||||
// TODO(e.burkov): Add DB path.
|
// DBFilePath is the path to the database file containing the DHCP leases.
|
||||||
|
DBFilePath string
|
||||||
|
|
||||||
// ICMPTimeout is the timeout for checking another DHCP server's presence.
|
// ICMPTimeout is the timeout for checking another DHCP server's presence.
|
||||||
ICMPTimeout time.Duration
|
ICMPTimeout time.Duration
|
||||||
@@ -64,6 +66,12 @@ func (conf *Config) Validate() (err error) {
|
|||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is a best-effort check for the file accessibility. The file will be
|
||||||
|
// checked again when it is opened later.
|
||||||
|
if _, err = os.Stat(conf.DBFilePath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
errs = append(errs, fmt.Errorf("db file path %q: %w", conf.DBFilePath, err))
|
||||||
|
}
|
||||||
|
|
||||||
if len(conf.Interfaces) == 0 {
|
if len(conf.Interfaces) == 0 {
|
||||||
errs = append(errs, errNoInterfaces)
|
errs = append(errs, errNoInterfaces)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package dhcpsvc_test
|
package dhcpsvc_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
|
||||||
@@ -8,6 +9,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestConfig_Validate(t *testing.T) {
|
func TestConfig_Validate(t *testing.T) {
|
||||||
|
leasesPath := filepath.Join(t.TempDir(), "leases.json")
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
conf *dhcpsvc.Config
|
conf *dhcpsvc.Config
|
||||||
@@ -25,6 +28,7 @@ func TestConfig_Validate(t *testing.T) {
|
|||||||
conf: &dhcpsvc.Config{
|
conf: &dhcpsvc.Config{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Interfaces: testInterfaceConf,
|
Interfaces: testInterfaceConf,
|
||||||
|
DBFilePath: leasesPath,
|
||||||
},
|
},
|
||||||
wantErrMsg: `bad domain name "": domain name is empty`,
|
wantErrMsg: `bad domain name "": domain name is empty`,
|
||||||
}, {
|
}, {
|
||||||
@@ -32,6 +36,7 @@ func TestConfig_Validate(t *testing.T) {
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
LocalDomainName: testLocalTLD,
|
LocalDomainName: testLocalTLD,
|
||||||
Interfaces: nil,
|
Interfaces: nil,
|
||||||
|
DBFilePath: leasesPath,
|
||||||
},
|
},
|
||||||
name: "no_interfaces",
|
name: "no_interfaces",
|
||||||
wantErrMsg: "no interfaces specified",
|
wantErrMsg: "no interfaces specified",
|
||||||
@@ -40,6 +45,7 @@ func TestConfig_Validate(t *testing.T) {
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
LocalDomainName: testLocalTLD,
|
LocalDomainName: testLocalTLD,
|
||||||
Interfaces: nil,
|
Interfaces: nil,
|
||||||
|
DBFilePath: leasesPath,
|
||||||
},
|
},
|
||||||
name: "no_interfaces",
|
name: "no_interfaces",
|
||||||
wantErrMsg: "no interfaces specified",
|
wantErrMsg: "no interfaces specified",
|
||||||
@@ -50,6 +56,7 @@ func TestConfig_Validate(t *testing.T) {
|
|||||||
Interfaces: map[string]*dhcpsvc.InterfaceConfig{
|
Interfaces: map[string]*dhcpsvc.InterfaceConfig{
|
||||||
"eth0": nil,
|
"eth0": nil,
|
||||||
},
|
},
|
||||||
|
DBFilePath: leasesPath,
|
||||||
},
|
},
|
||||||
name: "nil_interface",
|
name: "nil_interface",
|
||||||
wantErrMsg: `interface "eth0": config is nil`,
|
wantErrMsg: `interface "eth0": config is nil`,
|
||||||
@@ -63,6 +70,7 @@ func TestConfig_Validate(t *testing.T) {
|
|||||||
IPv6: &dhcpsvc.IPv6Config{Enabled: false},
|
IPv6: &dhcpsvc.IPv6Config{Enabled: false},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
DBFilePath: leasesPath,
|
||||||
},
|
},
|
||||||
name: "nil_ipv4",
|
name: "nil_ipv4",
|
||||||
wantErrMsg: `interface "eth0": ipv4: config is nil`,
|
wantErrMsg: `interface "eth0": ipv4: config is nil`,
|
||||||
@@ -76,6 +84,7 @@ func TestConfig_Validate(t *testing.T) {
|
|||||||
IPv6: nil,
|
IPv6: nil,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
DBFilePath: leasesPath,
|
||||||
},
|
},
|
||||||
name: "nil_ipv6",
|
name: "nil_ipv6",
|
||||||
wantErrMsg: `interface "eth0": ipv6: config is nil`,
|
wantErrMsg: `interface "eth0": ipv6: config is nil`,
|
||||||
|
|||||||
195
internal/dhcpsvc/db.go
Normal file
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
|
||||||
@@ -50,7 +50,7 @@ type Interface interface {
|
|||||||
IPByHost(host string) (ip netip.Addr)
|
IPByHost(host string) (ip netip.Addr)
|
||||||
|
|
||||||
// Leases returns all the active DHCP leases. The returned slice should be
|
// Leases returns all the active DHCP leases. The returned slice should be
|
||||||
// a clone.
|
// a clone. The order of leases is undefined.
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): Consider implementing iterating methods with appropriate
|
// TODO(e.burkov): Consider implementing iterating methods with appropriate
|
||||||
// signatures instead of cloning the whole list.
|
// signatures instead of cloning the whole list.
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -3,42 +3,74 @@ package dhcpsvc
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"slices"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// netInterface is a common part of any network interface within the DHCP
|
// macKey contains hardware address as byte array of 6, 8, or 20 bytes.
|
||||||
// server.
|
//
|
||||||
|
// TODO(e.burkov): Move to aghnet or even to netutil.
|
||||||
|
type macKey any
|
||||||
|
|
||||||
|
// macToKey converts mac into macKey, which is used as the key for the lease
|
||||||
|
// maps. mac must be a valid hardware address of length 6, 8, or 20 bytes, see
|
||||||
|
// [netutil.ValidateMAC].
|
||||||
|
func macToKey(mac net.HardwareAddr) (key macKey) {
|
||||||
|
switch len(mac) {
|
||||||
|
case 6:
|
||||||
|
return [6]byte(mac)
|
||||||
|
case 8:
|
||||||
|
return [8]byte(mac)
|
||||||
|
case 20:
|
||||||
|
return [20]byte(mac)
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("invalid mac address %#v", mac))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// netInterface is a common part of any interface within the DHCP server.
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): Add other methods as [DHCPServer] evolves.
|
// TODO(e.burkov): Add other methods as [DHCPServer] evolves.
|
||||||
type netInterface struct {
|
type netInterface struct {
|
||||||
// logger logs the events related to the network interface.
|
// logger logs the events related to the network interface.
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
|
||||||
|
// leases is the set of DHCP leases assigned to this interface.
|
||||||
|
leases map[macKey]*Lease
|
||||||
|
|
||||||
// name is the name of the network interface.
|
// name is the name of the network interface.
|
||||||
name string
|
name string
|
||||||
|
|
||||||
// leases is a set of leases sorted by hardware address.
|
|
||||||
leases []*Lease
|
|
||||||
|
|
||||||
// leaseTTL is the default Time-To-Live value for leases.
|
// leaseTTL is the default Time-To-Live value for leases.
|
||||||
leaseTTL time.Duration
|
leaseTTL time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset clears all the slices in iface for reuse.
|
// newNetInterface creates a new netInterface with the given name, leaseTTL, and
|
||||||
func (iface *netInterface) reset() {
|
// logger.
|
||||||
iface.leases = iface.leases[:0]
|
func newNetInterface(name string, l *slog.Logger, leaseTTL time.Duration) (iface *netInterface) {
|
||||||
|
return &netInterface{
|
||||||
|
logger: l,
|
||||||
|
leases: map[macKey]*Lease{},
|
||||||
|
name: name,
|
||||||
|
leaseTTL: leaseTTL,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// insertLease inserts the given lease into iface. It returns an error if the
|
// reset clears all the slices in iface for reuse.
|
||||||
|
func (iface *netInterface) reset() {
|
||||||
|
clear(iface.leases)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addLease inserts the given lease into iface. It returns an error if the
|
||||||
// lease can't be inserted.
|
// lease can't be inserted.
|
||||||
func (iface *netInterface) insertLease(l *Lease) (err error) {
|
func (iface *netInterface) addLease(l *Lease) (err error) {
|
||||||
i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC)
|
mk := macToKey(l.HWAddr)
|
||||||
|
_, found := iface.leases[mk]
|
||||||
if found {
|
if found {
|
||||||
return fmt.Errorf("lease for mac %s already exists", l.HWAddr)
|
return fmt.Errorf("lease for mac %s already exists", l.HWAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
iface.leases = slices.Insert(iface.leases, i, l)
|
iface.leases[mk] = l
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -46,12 +78,13 @@ func (iface *netInterface) insertLease(l *Lease) (err error) {
|
|||||||
// updateLease replaces an existing lease within iface with the given one. It
|
// updateLease replaces an existing lease within iface with the given one. It
|
||||||
// returns an error if there is no lease with such hardware address.
|
// returns an error if there is no lease with such hardware address.
|
||||||
func (iface *netInterface) updateLease(l *Lease) (prev *Lease, err error) {
|
func (iface *netInterface) updateLease(l *Lease) (prev *Lease, err error) {
|
||||||
i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC)
|
mk := macToKey(l.HWAddr)
|
||||||
|
prev, found := iface.leases[mk]
|
||||||
if !found {
|
if !found {
|
||||||
return nil, fmt.Errorf("no lease for mac %s", l.HWAddr)
|
return nil, fmt.Errorf("no lease for mac %s", l.HWAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
prev, iface.leases[i] = iface.leases[i], l
|
iface.leases[mk] = l
|
||||||
|
|
||||||
return prev, nil
|
return prev, nil
|
||||||
}
|
}
|
||||||
@@ -59,12 +92,13 @@ func (iface *netInterface) updateLease(l *Lease) (prev *Lease, err error) {
|
|||||||
// removeLease removes an existing lease from iface. It returns an error if
|
// removeLease removes an existing lease from iface. It returns an error if
|
||||||
// there is no lease equal to l.
|
// there is no lease equal to l.
|
||||||
func (iface *netInterface) removeLease(l *Lease) (err error) {
|
func (iface *netInterface) removeLease(l *Lease) (err error) {
|
||||||
i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC)
|
mk := macToKey(l.HWAddr)
|
||||||
|
_, found := iface.leases[mk]
|
||||||
if !found {
|
if !found {
|
||||||
return fmt.Errorf("no lease for mac %s", l.HWAddr)
|
return fmt.Errorf("no lease for mac %s", l.HWAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
iface.leases = slices.Delete(iface.leases, i, i+1)
|
delete(iface.leases, mk)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package dhcpsvc
|
package dhcpsvc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -45,8 +44,3 @@ func (l *Lease) Clone() (clone *Lease) {
|
|||||||
IsStatic: l.IsStatic,
|
IsStatic: l.IsStatic,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// compareLeaseMAC compares two [Lease]s by hardware address.
|
|
||||||
func compareLeaseMAC(a, b *Lease) (res int) {
|
|
||||||
return bytes.Compare(a.HWAddr, b.HWAddr)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func (idx *leaseIndex) add(l *Lease, iface *netInterface) (err error) {
|
|||||||
return fmt.Errorf("lease for hostname %s already exists", l.Hostname)
|
return fmt.Errorf("lease for hostname %s already exists", l.Hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = iface.insertLease(l)
|
err = iface.addLease(l)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -124,3 +124,18 @@ func (idx *leaseIndex) update(l *Lease, iface *netInterface) (err error) {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rangeLeases calls f for each lease in idx in an unspecified order until f
|
||||||
|
// returns false.
|
||||||
|
func (idx *leaseIndex) rangeLeases(f func(l *Lease) (cont bool)) {
|
||||||
|
for _, l := range idx.byName {
|
||||||
|
if !f(l) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// len returns the number of leases in idx.
|
||||||
|
func (idx *leaseIndex) len() (l uint) {
|
||||||
|
return uint(len(idx.byAddr))
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ type DHCPServer struct {
|
|||||||
// hostnames.
|
// hostnames.
|
||||||
localTLD string
|
localTLD string
|
||||||
|
|
||||||
|
// dbFilePath is the path to the database file containing the DHCP leases.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Consider extracting the database logic into a separate
|
||||||
|
// interface to prevent packages that only need lease data from depending on
|
||||||
|
// the entire server and to simplify testing.
|
||||||
|
dbFilePath string
|
||||||
|
|
||||||
// leasesMu protects the leases index as well as leases in the interfaces.
|
// leasesMu protects the leases index as well as leases in the interfaces.
|
||||||
leasesMu *sync.RWMutex
|
leasesMu *sync.RWMutex
|
||||||
|
|
||||||
@@ -34,10 +41,10 @@ type DHCPServer struct {
|
|||||||
leases *leaseIndex
|
leases *leaseIndex
|
||||||
|
|
||||||
// interfaces4 is the set of IPv4 interfaces sorted by interface name.
|
// interfaces4 is the set of IPv4 interfaces sorted by interface name.
|
||||||
interfaces4 netInterfacesV4
|
interfaces4 dhcpInterfacesV4
|
||||||
|
|
||||||
// interfaces6 is the set of IPv6 interfaces sorted by interface name.
|
// interfaces6 is the set of IPv6 interfaces sorted by interface name.
|
||||||
interfaces6 netInterfacesV6
|
interfaces6 dhcpInterfacesV6
|
||||||
|
|
||||||
// icmpTimeout is the timeout for checking another DHCP server's presence.
|
// icmpTimeout is the timeout for checking another DHCP server's presence.
|
||||||
icmpTimeout time.Duration
|
icmpTimeout time.Duration
|
||||||
@@ -56,28 +63,9 @@ func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(e.burkov): Add validations scoped to the network interfaces set.
|
ifaces4, ifaces6, err := newInterfaces(ctx, l, conf.Interfaces)
|
||||||
ifaces4 := make(netInterfacesV4, 0, len(conf.Interfaces))
|
if err != nil {
|
||||||
ifaces6 := make(netInterfacesV6, 0, len(conf.Interfaces))
|
// Don't wrap the error since it's informative enough as is.
|
||||||
var errs []error
|
|
||||||
|
|
||||||
mapsutil.SortedRange(conf.Interfaces, func(name string, iface *InterfaceConfig) (cont bool) {
|
|
||||||
var i4 *netInterfaceV4
|
|
||||||
i4, err = newNetInterfaceV4(ctx, l, name, iface.IPv4)
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("interface %q: ipv4: %w", name, err))
|
|
||||||
} else if i4 != nil {
|
|
||||||
ifaces4 = append(ifaces4, i4)
|
|
||||||
}
|
|
||||||
|
|
||||||
i6 := newNetInterfaceV6(ctx, l, name, iface.IPv6)
|
|
||||||
if i6 != nil {
|
|
||||||
ifaces6 = append(ifaces6, i6)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
if err = errors.Join(errs...); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,13 +81,55 @@ func New(ctx context.Context, conf *Config) (srv *DHCPServer, err error) {
|
|||||||
interfaces4: ifaces4,
|
interfaces4: ifaces4,
|
||||||
interfaces6: ifaces6,
|
interfaces6: ifaces6,
|
||||||
icmpTimeout: conf.ICMPTimeout,
|
icmpTimeout: conf.ICMPTimeout,
|
||||||
|
dbFilePath: conf.DBFilePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(e.burkov): Load leases.
|
err = srv.dbLoad(ctx)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error since it's informative enough as is.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return srv, nil
|
return srv, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newInterfaces creates interfaces for the given map of interface names to
|
||||||
|
// their configurations.
|
||||||
|
func newInterfaces(
|
||||||
|
ctx context.Context,
|
||||||
|
l *slog.Logger,
|
||||||
|
ifaces map[string]*InterfaceConfig,
|
||||||
|
) (v4 dhcpInterfacesV4, v6 dhcpInterfacesV6, err error) {
|
||||||
|
defer func() { err = errors.Annotate(err, "creating interfaces: %w") }()
|
||||||
|
|
||||||
|
// TODO(e.burkov): Add validations scoped to the network interfaces set.
|
||||||
|
v4 = make(dhcpInterfacesV4, 0, len(ifaces))
|
||||||
|
v6 = make(dhcpInterfacesV6, 0, len(ifaces))
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
mapsutil.SortedRange(ifaces, func(name string, iface *InterfaceConfig) (cont bool) {
|
||||||
|
var i4 *dhcpInterfaceV4
|
||||||
|
i4, err = newDHCPInterfaceV4(ctx, l, name, iface.IPv4)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("interface %q: ipv4: %w", name, err))
|
||||||
|
} else if i4 != nil {
|
||||||
|
v4 = append(v4, i4)
|
||||||
|
}
|
||||||
|
|
||||||
|
i6 := newDHCPInterfaceV6(ctx, l, name, iface.IPv6)
|
||||||
|
if i6 != nil {
|
||||||
|
v6 = append(v6, i6)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if err = errors.Join(errs...); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return v4, v6, nil
|
||||||
|
}
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): Uncomment when the [Interface] interface is implemented.
|
// TODO(e.burkov): Uncomment when the [Interface] interface is implemented.
|
||||||
@@ -115,16 +145,11 @@ func (srv *DHCPServer) Leases() (leases []*Lease) {
|
|||||||
srv.leasesMu.RLock()
|
srv.leasesMu.RLock()
|
||||||
defer srv.leasesMu.RUnlock()
|
defer srv.leasesMu.RUnlock()
|
||||||
|
|
||||||
for _, iface := range srv.interfaces4 {
|
srv.leases.rangeLeases(func(l *Lease) (cont bool) {
|
||||||
for _, lease := range iface.leases {
|
leases = append(leases, l.Clone())
|
||||||
leases = append(leases, lease.Clone())
|
|
||||||
}
|
return true
|
||||||
}
|
})
|
||||||
for _, iface := range srv.interfaces6 {
|
|
||||||
for _, lease := range iface.leases {
|
|
||||||
leases = append(leases, lease.Clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return leases
|
return leases
|
||||||
}
|
}
|
||||||
@@ -167,22 +192,35 @@ func (srv *DHCPServer) IPByHost(host string) (ip netip.Addr) {
|
|||||||
|
|
||||||
// Reset implements the [Interface] interface for *DHCPServer.
|
// Reset implements the [Interface] interface for *DHCPServer.
|
||||||
func (srv *DHCPServer) Reset(ctx context.Context) (err error) {
|
func (srv *DHCPServer) Reset(ctx context.Context) (err error) {
|
||||||
|
defer func() { err = errors.Annotate(err, "resetting leases: %w") }()
|
||||||
|
|
||||||
srv.leasesMu.Lock()
|
srv.leasesMu.Lock()
|
||||||
defer srv.leasesMu.Unlock()
|
defer srv.leasesMu.Unlock()
|
||||||
|
|
||||||
for _, iface := range srv.interfaces4 {
|
srv.resetLeases()
|
||||||
iface.reset()
|
err = srv.dbStore(ctx)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error since there is already an annotation deferred.
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
for _, iface := range srv.interfaces6 {
|
|
||||||
iface.reset()
|
|
||||||
}
|
|
||||||
srv.leases.clear()
|
|
||||||
|
|
||||||
srv.logger.DebugContext(ctx, "reset leases")
|
srv.logger.DebugContext(ctx, "reset leases")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resetLeases resets the leases for all network interfaces of the server. It
|
||||||
|
// expects the DHCPServer.leasesMu to be locked.
|
||||||
|
func (srv *DHCPServer) resetLeases() {
|
||||||
|
for _, iface := range srv.interfaces4 {
|
||||||
|
iface.common.reset()
|
||||||
|
}
|
||||||
|
for _, iface := range srv.interfaces6 {
|
||||||
|
iface.common.reset()
|
||||||
|
}
|
||||||
|
srv.leases.clear()
|
||||||
|
}
|
||||||
|
|
||||||
// AddLease implements the [Interface] interface for *DHCPServer.
|
// AddLease implements the [Interface] interface for *DHCPServer.
|
||||||
func (srv *DHCPServer) AddLease(ctx context.Context, l *Lease) (err error) {
|
func (srv *DHCPServer) AddLease(ctx context.Context, l *Lease) (err error) {
|
||||||
defer func() { err = errors.Annotate(err, "adding lease: %w") }()
|
defer func() { err = errors.Annotate(err, "adding lease: %w") }()
|
||||||
@@ -190,7 +228,7 @@ func (srv *DHCPServer) AddLease(ctx context.Context, l *Lease) (err error) {
|
|||||||
addr := l.IP
|
addr := l.IP
|
||||||
iface, err := srv.ifaceForAddr(addr)
|
iface, err := srv.ifaceForAddr(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Don't wrap the error since there is already an annotation deferred.
|
// Don't wrap the error since it's already informative enough as is.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +241,12 @@ func (srv *DHCPServer) AddLease(ctx context.Context, l *Lease) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = srv.dbStore(ctx)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error since it's already informative enough as is.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
iface.logger.DebugContext(
|
iface.logger.DebugContext(
|
||||||
ctx, "added lease",
|
ctx, "added lease",
|
||||||
"hostname", l.Hostname,
|
"hostname", l.Hostname,
|
||||||
@@ -223,7 +267,7 @@ func (srv *DHCPServer) UpdateStaticLease(ctx context.Context, l *Lease) (err err
|
|||||||
addr := l.IP
|
addr := l.IP
|
||||||
iface, err := srv.ifaceForAddr(addr)
|
iface, err := srv.ifaceForAddr(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Don't wrap the error since there is already an annotation deferred.
|
// Don't wrap the error since it's already informative enough as is.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +280,12 @@ func (srv *DHCPServer) UpdateStaticLease(ctx context.Context, l *Lease) (err err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = srv.dbStore(ctx)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error since it's already informative enough as is.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
iface.logger.DebugContext(
|
iface.logger.DebugContext(
|
||||||
ctx, "updated lease",
|
ctx, "updated lease",
|
||||||
"hostname", l.Hostname,
|
"hostname", l.Hostname,
|
||||||
@@ -254,7 +304,7 @@ func (srv *DHCPServer) RemoveLease(ctx context.Context, l *Lease) (err error) {
|
|||||||
addr := l.IP
|
addr := l.IP
|
||||||
iface, err := srv.ifaceForAddr(addr)
|
iface, err := srv.ifaceForAddr(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Don't wrap the error since there is already an annotation deferred.
|
// Don't wrap the error since it's already informative enough as is.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,6 +317,12 @@ func (srv *DHCPServer) RemoveLease(ctx context.Context, l *Lease) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = srv.dbStore(ctx)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error since it's already informative enough as is.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
iface.logger.DebugContext(
|
iface.logger.DebugContext(
|
||||||
ctx, "removed lease",
|
ctx, "removed lease",
|
||||||
"hostname", l.Hostname,
|
"hostname", l.Hostname,
|
||||||
|
|||||||
@@ -1,72 +1,41 @@
|
|||||||
package dhcpsvc_test
|
package dhcpsvc_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"io/fs"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
|
||||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// testLocalTLD is a common local TLD for tests.
|
// testdata is a filesystem containing data for tests.
|
||||||
const testLocalTLD = "local"
|
var testdata = os.DirFS("testdata")
|
||||||
|
|
||||||
// testTimeout is a common timeout for tests and contexts.
|
// newTempDB copies the leases database file located in the testdata FS, under
|
||||||
const testTimeout time.Duration = 10 * time.Second
|
// tb.Name()/leases.json, to a temporary directory and returns the path to the
|
||||||
|
// copied file.
|
||||||
|
func newTempDB(tb testing.TB) (dst string) {
|
||||||
|
tb.Helper()
|
||||||
|
|
||||||
// discardLog is a logger to discard test output.
|
const filename = "leases.json"
|
||||||
var discardLog = slogutil.NewDiscardLogger()
|
|
||||||
|
|
||||||
// testInterfaceConf is a common set of interface configurations for tests.
|
data, err := fs.ReadFile(testdata, path.Join(tb.Name(), filename))
|
||||||
var testInterfaceConf = map[string]*dhcpsvc.InterfaceConfig{
|
require.NoError(tb, err)
|
||||||
"eth0": {
|
|
||||||
IPv4: &dhcpsvc.IPv4Config{
|
|
||||||
Enabled: true,
|
|
||||||
GatewayIP: netip.MustParseAddr("192.168.0.1"),
|
|
||||||
SubnetMask: netip.MustParseAddr("255.255.255.0"),
|
|
||||||
RangeStart: netip.MustParseAddr("192.168.0.2"),
|
|
||||||
RangeEnd: netip.MustParseAddr("192.168.0.254"),
|
|
||||||
LeaseDuration: 1 * time.Hour,
|
|
||||||
},
|
|
||||||
IPv6: &dhcpsvc.IPv6Config{
|
|
||||||
Enabled: true,
|
|
||||||
RangeStart: netip.MustParseAddr("2001:db8::1"),
|
|
||||||
LeaseDuration: 1 * time.Hour,
|
|
||||||
RAAllowSLAAC: true,
|
|
||||||
RASLAACOnly: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"eth1": {
|
|
||||||
IPv4: &dhcpsvc.IPv4Config{
|
|
||||||
Enabled: true,
|
|
||||||
GatewayIP: netip.MustParseAddr("172.16.0.1"),
|
|
||||||
SubnetMask: netip.MustParseAddr("255.255.255.0"),
|
|
||||||
RangeStart: netip.MustParseAddr("172.16.0.2"),
|
|
||||||
RangeEnd: netip.MustParseAddr("172.16.0.255"),
|
|
||||||
LeaseDuration: 1 * time.Hour,
|
|
||||||
},
|
|
||||||
IPv6: &dhcpsvc.IPv6Config{
|
|
||||||
Enabled: true,
|
|
||||||
RangeStart: netip.MustParseAddr("2001:db9::1"),
|
|
||||||
LeaseDuration: 1 * time.Hour,
|
|
||||||
RAAllowSLAAC: true,
|
|
||||||
RASLAACOnly: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// mustParseMAC parses a hardware address from s and requires no errors.
|
dst = filepath.Join(tb.TempDir(), filename)
|
||||||
func mustParseMAC(t require.TestingT, s string) (mac net.HardwareAddr) {
|
|
||||||
mac, err := net.ParseMAC(s)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return mac
|
err = os.WriteFile(dst, data, dhcpsvc.DatabasePerm)
|
||||||
|
require.NoError(tb, err)
|
||||||
|
|
||||||
|
return dst
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
func TestNew(t *testing.T) {
|
||||||
@@ -103,6 +72,8 @@ func TestNew(t *testing.T) {
|
|||||||
RASLAACOnly: true,
|
RASLAACOnly: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
leasesPath := filepath.Join(t.TempDir(), "leases.json")
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
conf *dhcpsvc.Config
|
conf *dhcpsvc.Config
|
||||||
name string
|
name string
|
||||||
@@ -118,6 +89,7 @@ func TestNew(t *testing.T) {
|
|||||||
IPv6: validIPv6Conf,
|
IPv6: validIPv6Conf,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
DBFilePath: leasesPath,
|
||||||
},
|
},
|
||||||
name: "valid",
|
name: "valid",
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
@@ -132,6 +104,7 @@ func TestNew(t *testing.T) {
|
|||||||
IPv6: &dhcpsvc.IPv6Config{Enabled: false},
|
IPv6: &dhcpsvc.IPv6Config{Enabled: false},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
DBFilePath: leasesPath,
|
||||||
},
|
},
|
||||||
name: "disabled_interfaces",
|
name: "disabled_interfaces",
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
@@ -146,9 +119,10 @@ func TestNew(t *testing.T) {
|
|||||||
IPv6: validIPv6Conf,
|
IPv6: validIPv6Conf,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
DBFilePath: leasesPath,
|
||||||
},
|
},
|
||||||
name: "gateway_within_range",
|
name: "gateway_within_range",
|
||||||
wantErrMsg: `interface "eth0": ipv4: ` +
|
wantErrMsg: `creating interfaces: interface "eth0": ipv4: ` +
|
||||||
`gateway ip 192.168.0.100 in the ip range 192.168.0.1-192.168.0.254`,
|
`gateway ip 192.168.0.100 in the ip range 192.168.0.1-192.168.0.254`,
|
||||||
}, {
|
}, {
|
||||||
conf: &dhcpsvc.Config{
|
conf: &dhcpsvc.Config{
|
||||||
@@ -161,9 +135,10 @@ func TestNew(t *testing.T) {
|
|||||||
IPv6: validIPv6Conf,
|
IPv6: validIPv6Conf,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
DBFilePath: leasesPath,
|
||||||
},
|
},
|
||||||
name: "bad_start",
|
name: "bad_start",
|
||||||
wantErrMsg: `interface "eth0": ipv4: ` +
|
wantErrMsg: `creating interfaces: interface "eth0": ipv4: ` +
|
||||||
`range start 127.0.0.1 is not within 192.168.0.1/24`,
|
`range start 127.0.0.1 is not within 192.168.0.1/24`,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
@@ -180,32 +155,36 @@ func TestNew(t *testing.T) {
|
|||||||
func TestDHCPServer_AddLease(t *testing.T) {
|
func TestDHCPServer_AddLease(t *testing.T) {
|
||||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
|
||||||
|
leasesPath := filepath.Join(t.TempDir(), "leases.json")
|
||||||
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
|
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Logger: discardLog,
|
Logger: discardLog,
|
||||||
LocalDomainName: testLocalTLD,
|
LocalDomainName: testLocalTLD,
|
||||||
Interfaces: testInterfaceConf,
|
Interfaces: testInterfaceConf,
|
||||||
|
DBFilePath: leasesPath,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
host1 = "host1"
|
existHost = "host1"
|
||||||
host2 = "host2"
|
newHost = "host2"
|
||||||
host3 = "host3"
|
ipv6Host = "host3"
|
||||||
)
|
)
|
||||||
|
|
||||||
ip1 := netip.MustParseAddr("192.168.0.2")
|
var (
|
||||||
ip2 := netip.MustParseAddr("192.168.0.3")
|
existIP = netip.MustParseAddr("192.168.0.2")
|
||||||
ip3 := netip.MustParseAddr("2001:db8::2")
|
newIP = netip.MustParseAddr("192.168.0.3")
|
||||||
|
newIPv6 = netip.MustParseAddr("2001:db8::2")
|
||||||
|
|
||||||
mac1 := mustParseMAC(t, "01:02:03:04:05:06")
|
existMAC = mustParseMAC(t, "01:02:03:04:05:06")
|
||||||
mac2 := mustParseMAC(t, "06:05:04:03:02:01")
|
newMAC = mustParseMAC(t, "06:05:04:03:02:01")
|
||||||
mac3 := mustParseMAC(t, "02:03:04:05:06:07")
|
ipv6MAC = mustParseMAC(t, "02:03:04:05:06:07")
|
||||||
|
)
|
||||||
|
|
||||||
require.NoError(t, srv.AddLease(ctx, &dhcpsvc.Lease{
|
require.NoError(t, srv.AddLease(ctx, &dhcpsvc.Lease{
|
||||||
Hostname: host1,
|
Hostname: existHost,
|
||||||
IP: ip1,
|
IP: existIP,
|
||||||
HWAddr: mac1,
|
HWAddr: existMAC,
|
||||||
IsStatic: true,
|
IsStatic: true,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -216,61 +195,61 @@ func TestDHCPServer_AddLease(t *testing.T) {
|
|||||||
}{{
|
}{{
|
||||||
name: "outside_range",
|
name: "outside_range",
|
||||||
lease: &dhcpsvc.Lease{
|
lease: &dhcpsvc.Lease{
|
||||||
Hostname: host2,
|
Hostname: newHost,
|
||||||
IP: netip.MustParseAddr("1.2.3.4"),
|
IP: netip.MustParseAddr("1.2.3.4"),
|
||||||
HWAddr: mac2,
|
HWAddr: newMAC,
|
||||||
},
|
},
|
||||||
wantErrMsg: "adding lease: no interface for ip 1.2.3.4",
|
wantErrMsg: "adding lease: no interface for ip 1.2.3.4",
|
||||||
}, {
|
}, {
|
||||||
name: "duplicate_ip",
|
name: "duplicate_ip",
|
||||||
lease: &dhcpsvc.Lease{
|
lease: &dhcpsvc.Lease{
|
||||||
Hostname: host2,
|
Hostname: newHost,
|
||||||
IP: ip1,
|
IP: existIP,
|
||||||
HWAddr: mac2,
|
HWAddr: newMAC,
|
||||||
},
|
},
|
||||||
wantErrMsg: "adding lease: lease for ip " + ip1.String() +
|
wantErrMsg: "adding lease: lease for ip " + existIP.String() +
|
||||||
" already exists",
|
" already exists",
|
||||||
}, {
|
}, {
|
||||||
name: "duplicate_hostname",
|
name: "duplicate_hostname",
|
||||||
lease: &dhcpsvc.Lease{
|
lease: &dhcpsvc.Lease{
|
||||||
Hostname: host1,
|
Hostname: existHost,
|
||||||
IP: ip2,
|
IP: newIP,
|
||||||
HWAddr: mac2,
|
HWAddr: newMAC,
|
||||||
},
|
},
|
||||||
wantErrMsg: "adding lease: lease for hostname " + host1 +
|
wantErrMsg: "adding lease: lease for hostname " + existHost +
|
||||||
" already exists",
|
" already exists",
|
||||||
}, {
|
}, {
|
||||||
name: "duplicate_hostname_case",
|
name: "duplicate_hostname_case",
|
||||||
lease: &dhcpsvc.Lease{
|
lease: &dhcpsvc.Lease{
|
||||||
Hostname: strings.ToUpper(host1),
|
Hostname: strings.ToUpper(existHost),
|
||||||
IP: ip2,
|
IP: newIP,
|
||||||
HWAddr: mac2,
|
HWAddr: newMAC,
|
||||||
},
|
},
|
||||||
wantErrMsg: "adding lease: lease for hostname " +
|
wantErrMsg: "adding lease: lease for hostname " +
|
||||||
strings.ToUpper(host1) + " already exists",
|
strings.ToUpper(existHost) + " already exists",
|
||||||
}, {
|
}, {
|
||||||
name: "duplicate_mac",
|
name: "duplicate_mac",
|
||||||
lease: &dhcpsvc.Lease{
|
lease: &dhcpsvc.Lease{
|
||||||
Hostname: host2,
|
Hostname: newHost,
|
||||||
IP: ip2,
|
IP: newIP,
|
||||||
HWAddr: mac1,
|
HWAddr: existMAC,
|
||||||
},
|
},
|
||||||
wantErrMsg: "adding lease: lease for mac " + mac1.String() +
|
wantErrMsg: "adding lease: lease for mac " + existMAC.String() +
|
||||||
" already exists",
|
" already exists",
|
||||||
}, {
|
}, {
|
||||||
name: "valid",
|
name: "valid",
|
||||||
lease: &dhcpsvc.Lease{
|
lease: &dhcpsvc.Lease{
|
||||||
Hostname: host2,
|
Hostname: newHost,
|
||||||
IP: ip2,
|
IP: newIP,
|
||||||
HWAddr: mac2,
|
HWAddr: newMAC,
|
||||||
},
|
},
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
}, {
|
}, {
|
||||||
name: "valid_v6",
|
name: "valid_v6",
|
||||||
lease: &dhcpsvc.Lease{
|
lease: &dhcpsvc.Lease{
|
||||||
Hostname: host3,
|
Hostname: ipv6Host,
|
||||||
IP: ip3,
|
IP: newIPv6,
|
||||||
HWAddr: mac3,
|
HWAddr: ipv6MAC,
|
||||||
},
|
},
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
}}
|
}}
|
||||||
@@ -280,16 +259,21 @@ func TestDHCPServer_AddLease(t *testing.T) {
|
|||||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.AddLease(ctx, tc.lease))
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.AddLease(ctx, tc.lease))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert.NotEmpty(t, srv.Leases())
|
||||||
|
assert.FileExists(t, leasesPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDHCPServer_index(t *testing.T) {
|
func TestDHCPServer_index(t *testing.T) {
|
||||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
|
||||||
|
leasesPath := newTempDB(t)
|
||||||
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
|
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Logger: discardLog,
|
Logger: discardLog,
|
||||||
LocalDomainName: testLocalTLD,
|
LocalDomainName: testLocalTLD,
|
||||||
Interfaces: testInterfaceConf,
|
Interfaces: testInterfaceConf,
|
||||||
|
DBFilePath: leasesPath,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -301,46 +285,23 @@ func TestDHCPServer_index(t *testing.T) {
|
|||||||
host5 = "host5"
|
host5 = "host5"
|
||||||
)
|
)
|
||||||
|
|
||||||
ip1 := netip.MustParseAddr("192.168.0.2")
|
var (
|
||||||
ip2 := netip.MustParseAddr("192.168.0.3")
|
ip1 = netip.MustParseAddr("192.168.0.2")
|
||||||
ip3 := netip.MustParseAddr("172.16.0.3")
|
ip2 = netip.MustParseAddr("192.168.0.3")
|
||||||
ip4 := netip.MustParseAddr("172.16.0.4")
|
ip3 = netip.MustParseAddr("172.16.0.3")
|
||||||
|
ip4 = netip.MustParseAddr("172.16.0.4")
|
||||||
|
|
||||||
mac1 := mustParseMAC(t, "01:02:03:04:05:06")
|
mac1 = mustParseMAC(t, "01:02:03:04:05:06")
|
||||||
mac2 := mustParseMAC(t, "06:05:04:03:02:01")
|
mac2 = mustParseMAC(t, "06:05:04:03:02:01")
|
||||||
mac3 := mustParseMAC(t, "02:03:04:05:06:07")
|
mac3 = mustParseMAC(t, "02:03:04:05:06:07")
|
||||||
|
)
|
||||||
leases := []*dhcpsvc.Lease{{
|
|
||||||
Hostname: host1,
|
|
||||||
IP: ip1,
|
|
||||||
HWAddr: mac1,
|
|
||||||
IsStatic: true,
|
|
||||||
}, {
|
|
||||||
Hostname: host2,
|
|
||||||
IP: ip2,
|
|
||||||
HWAddr: mac2,
|
|
||||||
IsStatic: true,
|
|
||||||
}, {
|
|
||||||
Hostname: host3,
|
|
||||||
IP: ip3,
|
|
||||||
HWAddr: mac3,
|
|
||||||
IsStatic: true,
|
|
||||||
}, {
|
|
||||||
Hostname: host4,
|
|
||||||
IP: ip4,
|
|
||||||
HWAddr: mac1,
|
|
||||||
IsStatic: true,
|
|
||||||
}}
|
|
||||||
for _, l := range leases {
|
|
||||||
require.NoError(t, srv.AddLease(ctx, l))
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("ip_idx", func(t *testing.T) {
|
t.Run("ip_idx", func(t *testing.T) {
|
||||||
assert.Equal(t, ip1, srv.IPByHost(host1))
|
assert.Equal(t, ip1, srv.IPByHost(host1))
|
||||||
assert.Equal(t, ip2, srv.IPByHost(host2))
|
assert.Equal(t, ip2, srv.IPByHost(host2))
|
||||||
assert.Equal(t, ip3, srv.IPByHost(host3))
|
assert.Equal(t, ip3, srv.IPByHost(host3))
|
||||||
assert.Equal(t, ip4, srv.IPByHost(host4))
|
assert.Equal(t, ip4, srv.IPByHost(host4))
|
||||||
assert.Equal(t, netip.Addr{}, srv.IPByHost(host5))
|
assert.Zero(t, srv.IPByHost(host5))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("name_idx", func(t *testing.T) {
|
t.Run("name_idx", func(t *testing.T) {
|
||||||
@@ -348,7 +309,7 @@ func TestDHCPServer_index(t *testing.T) {
|
|||||||
assert.Equal(t, host2, srv.HostByIP(ip2))
|
assert.Equal(t, host2, srv.HostByIP(ip2))
|
||||||
assert.Equal(t, host3, srv.HostByIP(ip3))
|
assert.Equal(t, host3, srv.HostByIP(ip3))
|
||||||
assert.Equal(t, host4, srv.HostByIP(ip4))
|
assert.Equal(t, host4, srv.HostByIP(ip4))
|
||||||
assert.Equal(t, "", srv.HostByIP(netip.Addr{}))
|
assert.Zero(t, srv.HostByIP(netip.Addr{}))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("mac_idx", func(t *testing.T) {
|
t.Run("mac_idx", func(t *testing.T) {
|
||||||
@@ -356,18 +317,20 @@ func TestDHCPServer_index(t *testing.T) {
|
|||||||
assert.Equal(t, mac2, srv.MACByIP(ip2))
|
assert.Equal(t, mac2, srv.MACByIP(ip2))
|
||||||
assert.Equal(t, mac3, srv.MACByIP(ip3))
|
assert.Equal(t, mac3, srv.MACByIP(ip3))
|
||||||
assert.Equal(t, mac1, srv.MACByIP(ip4))
|
assert.Equal(t, mac1, srv.MACByIP(ip4))
|
||||||
assert.Nil(t, srv.MACByIP(netip.Addr{}))
|
assert.Zero(t, srv.MACByIP(netip.Addr{}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDHCPServer_UpdateStaticLease(t *testing.T) {
|
func TestDHCPServer_UpdateStaticLease(t *testing.T) {
|
||||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
|
||||||
|
leasesPath := newTempDB(t)
|
||||||
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
|
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Logger: discardLog,
|
Logger: discardLog,
|
||||||
LocalDomainName: testLocalTLD,
|
LocalDomainName: testLocalTLD,
|
||||||
Interfaces: testInterfaceConf,
|
Interfaces: testInterfaceConf,
|
||||||
|
DBFilePath: leasesPath,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -380,36 +343,16 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
|
|||||||
host6 = "host6"
|
host6 = "host6"
|
||||||
)
|
)
|
||||||
|
|
||||||
ip1 := netip.MustParseAddr("192.168.0.2")
|
var (
|
||||||
ip2 := netip.MustParseAddr("192.168.0.3")
|
ip1 = netip.MustParseAddr("192.168.0.2")
|
||||||
ip3 := netip.MustParseAddr("192.168.0.4")
|
ip2 = netip.MustParseAddr("192.168.0.3")
|
||||||
ip4 := netip.MustParseAddr("2001:db8::2")
|
ip3 = netip.MustParseAddr("192.168.0.4")
|
||||||
ip5 := netip.MustParseAddr("2001:db8::3")
|
ip4 = netip.MustParseAddr("2001:db8::3")
|
||||||
|
|
||||||
mac1 := mustParseMAC(t, "01:02:03:04:05:06")
|
mac1 = mustParseMAC(t, "01:02:03:04:05:06")
|
||||||
mac2 := mustParseMAC(t, "01:02:03:04:05:07")
|
mac2 = mustParseMAC(t, "06:05:04:03:02:01")
|
||||||
mac3 := mustParseMAC(t, "06:05:04:03:02:01")
|
mac3 = mustParseMAC(t, "06:05:04:03:02:02")
|
||||||
mac4 := mustParseMAC(t, "06:05:04:03:02:02")
|
)
|
||||||
|
|
||||||
leases := []*dhcpsvc.Lease{{
|
|
||||||
Hostname: host1,
|
|
||||||
IP: ip1,
|
|
||||||
HWAddr: mac1,
|
|
||||||
IsStatic: true,
|
|
||||||
}, {
|
|
||||||
Hostname: host2,
|
|
||||||
IP: ip2,
|
|
||||||
HWAddr: mac2,
|
|
||||||
IsStatic: true,
|
|
||||||
}, {
|
|
||||||
Hostname: host4,
|
|
||||||
IP: ip4,
|
|
||||||
HWAddr: mac4,
|
|
||||||
IsStatic: true,
|
|
||||||
}}
|
|
||||||
for _, l := range leases {
|
|
||||||
require.NoError(t, srv.AddLease(ctx, l))
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -428,9 +371,9 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
|
|||||||
lease: &dhcpsvc.Lease{
|
lease: &dhcpsvc.Lease{
|
||||||
Hostname: host3,
|
Hostname: host3,
|
||||||
IP: ip3,
|
IP: ip3,
|
||||||
HWAddr: mac3,
|
HWAddr: mac2,
|
||||||
},
|
},
|
||||||
wantErrMsg: "updating static lease: no lease for mac " + mac3.String(),
|
wantErrMsg: "updating static lease: no lease for mac " + mac2.String(),
|
||||||
}, {
|
}, {
|
||||||
name: "duplicate_ip",
|
name: "duplicate_ip",
|
||||||
lease: &dhcpsvc.Lease{
|
lease: &dhcpsvc.Lease{
|
||||||
@@ -470,8 +413,8 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
|
|||||||
name: "valid_v6",
|
name: "valid_v6",
|
||||||
lease: &dhcpsvc.Lease{
|
lease: &dhcpsvc.Lease{
|
||||||
Hostname: host6,
|
Hostname: host6,
|
||||||
IP: ip5,
|
IP: ip4,
|
||||||
HWAddr: mac4,
|
HWAddr: mac3,
|
||||||
},
|
},
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
}}
|
}}
|
||||||
@@ -481,16 +424,20 @@ func TestDHCPServer_UpdateStaticLease(t *testing.T) {
|
|||||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.UpdateStaticLease(ctx, tc.lease))
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.UpdateStaticLease(ctx, tc.lease))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert.FileExists(t, leasesPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDHCPServer_RemoveLease(t *testing.T) {
|
func TestDHCPServer_RemoveLease(t *testing.T) {
|
||||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
|
||||||
|
leasesPath := newTempDB(t)
|
||||||
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
|
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Logger: discardLog,
|
Logger: discardLog,
|
||||||
LocalDomainName: testLocalTLD,
|
LocalDomainName: testLocalTLD,
|
||||||
Interfaces: testInterfaceConf,
|
Interfaces: testInterfaceConf,
|
||||||
|
DBFilePath: leasesPath,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -500,28 +447,15 @@ func TestDHCPServer_RemoveLease(t *testing.T) {
|
|||||||
host3 = "host3"
|
host3 = "host3"
|
||||||
)
|
)
|
||||||
|
|
||||||
ip1 := netip.MustParseAddr("192.168.0.2")
|
var (
|
||||||
ip2 := netip.MustParseAddr("192.168.0.3")
|
existIP = netip.MustParseAddr("192.168.0.2")
|
||||||
ip3 := netip.MustParseAddr("2001:db8::2")
|
newIP = netip.MustParseAddr("192.168.0.3")
|
||||||
|
newIPv6 = netip.MustParseAddr("2001:db8::2")
|
||||||
|
|
||||||
mac1 := mustParseMAC(t, "01:02:03:04:05:06")
|
existMAC = mustParseMAC(t, "01:02:03:04:05:06")
|
||||||
mac2 := mustParseMAC(t, "02:03:04:05:06:07")
|
newMAC = mustParseMAC(t, "02:03:04:05:06:07")
|
||||||
mac3 := mustParseMAC(t, "06:05:04:03:02:01")
|
ipv6MAC = mustParseMAC(t, "06:05:04:03:02:01")
|
||||||
|
)
|
||||||
leases := []*dhcpsvc.Lease{{
|
|
||||||
Hostname: host1,
|
|
||||||
IP: ip1,
|
|
||||||
HWAddr: mac1,
|
|
||||||
IsStatic: true,
|
|
||||||
}, {
|
|
||||||
Hostname: host3,
|
|
||||||
IP: ip3,
|
|
||||||
HWAddr: mac3,
|
|
||||||
IsStatic: true,
|
|
||||||
}}
|
|
||||||
for _, l := range leases {
|
|
||||||
require.NoError(t, srv.AddLease(ctx, l))
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -531,40 +465,40 @@ func TestDHCPServer_RemoveLease(t *testing.T) {
|
|||||||
name: "not_found_mac",
|
name: "not_found_mac",
|
||||||
lease: &dhcpsvc.Lease{
|
lease: &dhcpsvc.Lease{
|
||||||
Hostname: host1,
|
Hostname: host1,
|
||||||
IP: ip1,
|
IP: existIP,
|
||||||
HWAddr: mac2,
|
HWAddr: newMAC,
|
||||||
},
|
},
|
||||||
wantErrMsg: "removing lease: no lease for mac " + mac2.String(),
|
wantErrMsg: "removing lease: no lease for mac " + newMAC.String(),
|
||||||
}, {
|
}, {
|
||||||
name: "not_found_ip",
|
name: "not_found_ip",
|
||||||
lease: &dhcpsvc.Lease{
|
lease: &dhcpsvc.Lease{
|
||||||
Hostname: host1,
|
Hostname: host1,
|
||||||
IP: ip2,
|
IP: newIP,
|
||||||
HWAddr: mac1,
|
HWAddr: existMAC,
|
||||||
},
|
},
|
||||||
wantErrMsg: "removing lease: no lease for ip " + ip2.String(),
|
wantErrMsg: "removing lease: no lease for ip " + newIP.String(),
|
||||||
}, {
|
}, {
|
||||||
name: "not_found_host",
|
name: "not_found_host",
|
||||||
lease: &dhcpsvc.Lease{
|
lease: &dhcpsvc.Lease{
|
||||||
Hostname: host2,
|
Hostname: host2,
|
||||||
IP: ip1,
|
IP: existIP,
|
||||||
HWAddr: mac1,
|
HWAddr: existMAC,
|
||||||
},
|
},
|
||||||
wantErrMsg: "removing lease: no lease for hostname " + host2,
|
wantErrMsg: "removing lease: no lease for hostname " + host2,
|
||||||
}, {
|
}, {
|
||||||
name: "valid",
|
name: "valid",
|
||||||
lease: &dhcpsvc.Lease{
|
lease: &dhcpsvc.Lease{
|
||||||
Hostname: host1,
|
Hostname: host1,
|
||||||
IP: ip1,
|
IP: existIP,
|
||||||
HWAddr: mac1,
|
HWAddr: existMAC,
|
||||||
},
|
},
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
}, {
|
}, {
|
||||||
name: "valid_v6",
|
name: "valid_v6",
|
||||||
lease: &dhcpsvc.Lease{
|
lease: &dhcpsvc.Lease{
|
||||||
Hostname: host3,
|
Hostname: host3,
|
||||||
IP: ip3,
|
IP: newIPv6,
|
||||||
HWAddr: mac3,
|
HWAddr: ipv6MAC,
|
||||||
},
|
},
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
}}
|
}}
|
||||||
@@ -575,49 +509,64 @@ func TestDHCPServer_RemoveLease(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert.FileExists(t, leasesPath)
|
||||||
assert.Empty(t, srv.Leases())
|
assert.Empty(t, srv.Leases())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDHCPServer_Reset(t *testing.T) {
|
func TestDHCPServer_Reset(t *testing.T) {
|
||||||
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
leasesPath := newTempDB(t)
|
||||||
|
conf := &dhcpsvc.Config{
|
||||||
srv, err := dhcpsvc.New(ctx, &dhcpsvc.Config{
|
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Logger: discardLog,
|
Logger: discardLog,
|
||||||
LocalDomainName: testLocalTLD,
|
LocalDomainName: testLocalTLD,
|
||||||
Interfaces: testInterfaceConf,
|
Interfaces: testInterfaceConf,
|
||||||
})
|
DBFilePath: leasesPath,
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
leases := []*dhcpsvc.Lease{{
|
|
||||||
Hostname: "host1",
|
|
||||||
IP: netip.MustParseAddr("192.168.0.2"),
|
|
||||||
HWAddr: mustParseMAC(t, "01:02:03:04:05:06"),
|
|
||||||
IsStatic: true,
|
|
||||||
}, {
|
|
||||||
Hostname: "host2",
|
|
||||||
IP: netip.MustParseAddr("192.168.0.3"),
|
|
||||||
HWAddr: mustParseMAC(t, "06:05:04:03:02:01"),
|
|
||||||
IsStatic: true,
|
|
||||||
}, {
|
|
||||||
Hostname: "host3",
|
|
||||||
IP: netip.MustParseAddr("2001:db8::2"),
|
|
||||||
HWAddr: mustParseMAC(t, "02:03:04:05:06:07"),
|
|
||||||
IsStatic: true,
|
|
||||||
}, {
|
|
||||||
Hostname: "host4",
|
|
||||||
IP: netip.MustParseAddr("2001:db8::3"),
|
|
||||||
HWAddr: mustParseMAC(t, "06:05:04:03:02:02"),
|
|
||||||
IsStatic: true,
|
|
||||||
}}
|
|
||||||
|
|
||||||
for _, l := range leases {
|
|
||||||
require.NoError(t, srv.AddLease(ctx, l))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
require.Len(t, srv.Leases(), len(leases))
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
srv, err := dhcpsvc.New(ctx, conf)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
const leasesNum = 4
|
||||||
|
|
||||||
|
require.Len(t, srv.Leases(), leasesNum)
|
||||||
|
|
||||||
require.NoError(t, srv.Reset(ctx))
|
require.NoError(t, srv.Reset(ctx))
|
||||||
|
|
||||||
|
assert.FileExists(t, leasesPath)
|
||||||
assert.Empty(t, srv.Leases())
|
assert.Empty(t, srv.Leases())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer_Leases(t *testing.T) {
|
||||||
|
leasesPath := newTempDB(t)
|
||||||
|
conf := &dhcpsvc.Config{
|
||||||
|
Enabled: true,
|
||||||
|
Logger: discardLog,
|
||||||
|
LocalDomainName: testLocalTLD,
|
||||||
|
Interfaces: testInterfaceConf,
|
||||||
|
DBFilePath: leasesPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
|
||||||
|
srv, err := dhcpsvc.New(ctx, conf)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expiry, err := time.Parse(time.RFC3339, "2042-01-02T03:04:05Z")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
wantLeases := []*dhcpsvc.Lease{{
|
||||||
|
Expiry: expiry,
|
||||||
|
IP: netip.MustParseAddr("192.168.0.3"),
|
||||||
|
Hostname: "example.host",
|
||||||
|
HWAddr: mustParseMAC(t, "AA:AA:AA:AA:AA:AA"),
|
||||||
|
IsStatic: false,
|
||||||
|
}, {
|
||||||
|
Expiry: time.Time{},
|
||||||
|
IP: netip.MustParseAddr("192.168.0.4"),
|
||||||
|
Hostname: "example.static.host",
|
||||||
|
HWAddr: mustParseMAC(t, "BB:BB:BB:BB:BB:BB"),
|
||||||
|
IsStatic: true,
|
||||||
|
}}
|
||||||
|
assert.ElementsMatch(t, wantLeases, srv.Leases())
|
||||||
|
}
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -82,8 +82,12 @@ func (c *IPv4Config) validate() (err error) {
|
|||||||
return errors.Join(errs...)
|
return errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// netInterfaceV4 is a DHCP interface for IPv4 address family.
|
// dhcpInterfaceV4 is a DHCP interface for IPv4 address family.
|
||||||
type netInterfaceV4 struct {
|
type dhcpInterfaceV4 struct {
|
||||||
|
// common is the common part of any network interface within the DHCP
|
||||||
|
// server.
|
||||||
|
common *netInterface
|
||||||
|
|
||||||
// gateway is the IP address of the network gateway.
|
// gateway is the IP address of the network gateway.
|
||||||
gateway netip.Addr
|
gateway netip.Addr
|
||||||
|
|
||||||
@@ -101,25 +105,22 @@ type netInterfaceV4 struct {
|
|||||||
// explicitOpts are the user-configured options. It must not have
|
// explicitOpts are the user-configured options. It must not have
|
||||||
// intersections with implicitOpts.
|
// intersections with implicitOpts.
|
||||||
explicitOpts layers.DHCPOptions
|
explicitOpts layers.DHCPOptions
|
||||||
|
|
||||||
// netInterface is embedded here to provide some common network interface
|
|
||||||
// logic.
|
|
||||||
netInterface
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// newNetInterfaceV4 creates a new DHCP interface for IPv4 address family with
|
// newDHCPInterfaceV4 creates a new DHCP interface for IPv4 address family with
|
||||||
// the given configuration. It returns an error if the given configuration
|
// the given configuration. It returns an error if the given configuration
|
||||||
// can't be used.
|
// can't be used.
|
||||||
func newNetInterfaceV4(
|
func newDHCPInterfaceV4(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
l *slog.Logger,
|
l *slog.Logger,
|
||||||
name string,
|
name string,
|
||||||
conf *IPv4Config,
|
conf *IPv4Config,
|
||||||
) (i *netInterfaceV4, err error) {
|
) (i *dhcpInterfaceV4, err error) {
|
||||||
l = l.With(
|
l = l.With(
|
||||||
keyInterface, name,
|
keyInterface, name,
|
||||||
keyFamily, netutil.AddrFamilyIPv4,
|
keyFamily, netutil.AddrFamilyIPv4,
|
||||||
)
|
)
|
||||||
|
|
||||||
if !conf.Enabled {
|
if !conf.Enabled {
|
||||||
l.DebugContext(ctx, "disabled")
|
l.DebugContext(ctx, "disabled")
|
||||||
|
|
||||||
@@ -143,35 +144,31 @@ func newNetInterfaceV4(
|
|||||||
return nil, fmt.Errorf("gateway ip %s in the ip range %s", conf.GatewayIP, addrSpace)
|
return nil, fmt.Errorf("gateway ip %s in the ip range %s", conf.GatewayIP, addrSpace)
|
||||||
}
|
}
|
||||||
|
|
||||||
i = &netInterfaceV4{
|
i = &dhcpInterfaceV4{
|
||||||
gateway: conf.GatewayIP,
|
gateway: conf.GatewayIP,
|
||||||
subnet: subnet,
|
subnet: subnet,
|
||||||
addrSpace: addrSpace,
|
addrSpace: addrSpace,
|
||||||
netInterface: netInterface{
|
common: newNetInterface(name, l, conf.LeaseDuration),
|
||||||
name: name,
|
|
||||||
leaseTTL: conf.LeaseDuration,
|
|
||||||
logger: l,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
i.implicitOpts, i.explicitOpts = conf.options(ctx, l)
|
i.implicitOpts, i.explicitOpts = conf.options(ctx, l)
|
||||||
|
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// netInterfacesV4 is a slice of network interfaces of IPv4 address family.
|
// dhcpInterfacesV4 is a slice of network interfaces of IPv4 address family.
|
||||||
type netInterfacesV4 []*netInterfaceV4
|
type dhcpInterfacesV4 []*dhcpInterfaceV4
|
||||||
|
|
||||||
// find returns the first network interface within ifaces containing ip. It
|
// find returns the first network interface within ifaces containing ip. It
|
||||||
// returns false if there is no such interface.
|
// returns false if there is no such interface.
|
||||||
func (ifaces netInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok bool) {
|
func (ifaces dhcpInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok bool) {
|
||||||
i := slices.IndexFunc(ifaces, func(iface *netInterfaceV4) (contains bool) {
|
i := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV4) (contains bool) {
|
||||||
return iface.subnet.Contains(ip)
|
return iface.subnet.Contains(ip)
|
||||||
})
|
})
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ifaces[i].netInterface, true
|
return ifaces[i].common, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// options returns the implicit and explicit options for the interface. The two
|
// options returns the implicit and explicit options for the interface. The two
|
||||||
|
|||||||
@@ -62,10 +62,12 @@ func (c *IPv6Config) validate() (err error) {
|
|||||||
return errors.Join(errs...)
|
return errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// netInterfaceV6 is a DHCP interface for IPv6 address family.
|
// dhcpInterfaceV6 is a DHCP interface for IPv6 address family.
|
||||||
//
|
type dhcpInterfaceV6 struct {
|
||||||
// TODO(e.burkov): Add options.
|
// common is the common part of any network interface within the DHCP
|
||||||
type netInterfaceV6 struct {
|
// server.
|
||||||
|
common *netInterface
|
||||||
|
|
||||||
// rangeStart is the first IP address in the range.
|
// rangeStart is the first IP address in the range.
|
||||||
rangeStart netip.Addr
|
rangeStart netip.Addr
|
||||||
|
|
||||||
@@ -78,10 +80,6 @@ type netInterfaceV6 struct {
|
|||||||
// intersections with implicitOpts.
|
// intersections with implicitOpts.
|
||||||
explicitOpts layers.DHCPv6Options
|
explicitOpts layers.DHCPv6Options
|
||||||
|
|
||||||
// netInterface is embedded here to provide some common network interface
|
|
||||||
// logic.
|
|
||||||
netInterface
|
|
||||||
|
|
||||||
// raSLAACOnly defines if DHCP should send ICMPv6.RA packets without MO
|
// raSLAACOnly defines if DHCP should send ICMPv6.RA packets without MO
|
||||||
// flags.
|
// flags.
|
||||||
raSLAACOnly bool
|
raSLAACOnly bool
|
||||||
@@ -90,16 +88,16 @@ type netInterfaceV6 struct {
|
|||||||
raAllowSLAAC bool
|
raAllowSLAAC bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// newNetInterfaceV6 creates a new DHCP interface for IPv6 address family with
|
// newDHCPInterfaceV6 creates a new DHCP interface for IPv6 address family with
|
||||||
// the given configuration.
|
// the given configuration.
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): Validate properly.
|
// TODO(e.burkov): Validate properly.
|
||||||
func newNetInterfaceV6(
|
func newDHCPInterfaceV6(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
l *slog.Logger,
|
l *slog.Logger,
|
||||||
name string,
|
name string,
|
||||||
conf *IPv6Config,
|
conf *IPv6Config,
|
||||||
) (i *netInterfaceV6) {
|
) (i *dhcpInterfaceV6) {
|
||||||
l = l.With(keyInterface, name, keyFamily, netutil.AddrFamilyIPv6)
|
l = l.With(keyInterface, name, keyFamily, netutil.AddrFamilyIPv6)
|
||||||
if !conf.Enabled {
|
if !conf.Enabled {
|
||||||
l.DebugContext(ctx, "disabled")
|
l.DebugContext(ctx, "disabled")
|
||||||
@@ -107,13 +105,9 @@ func newNetInterfaceV6(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
i = &netInterfaceV6{
|
i = &dhcpInterfaceV6{
|
||||||
rangeStart: conf.RangeStart,
|
rangeStart: conf.RangeStart,
|
||||||
netInterface: netInterface{
|
common: newNetInterface(name, l, conf.LeaseDuration),
|
||||||
name: name,
|
|
||||||
leaseTTL: conf.LeaseDuration,
|
|
||||||
logger: l,
|
|
||||||
},
|
|
||||||
raSLAACOnly: conf.RASLAACOnly,
|
raSLAACOnly: conf.RASLAACOnly,
|
||||||
raAllowSLAAC: conf.RAAllowSLAAC,
|
raAllowSLAAC: conf.RAAllowSLAAC,
|
||||||
}
|
}
|
||||||
@@ -122,12 +116,12 @@ func newNetInterfaceV6(
|
|||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
// netInterfacesV4 is a slice of network interfaces of IPv4 address family.
|
// dhcpInterfacesV6 is a slice of network interfaces of IPv6 address family.
|
||||||
type netInterfacesV6 []*netInterfaceV6
|
type dhcpInterfacesV6 []*dhcpInterfaceV6
|
||||||
|
|
||||||
// find returns the first network interface within ifaces containing ip. It
|
// find returns the first network interface within ifaces containing ip. It
|
||||||
// returns false if there is no such interface.
|
// returns false if there is no such interface.
|
||||||
func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool) {
|
func (ifaces dhcpInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool) {
|
||||||
// prefLen is the length of prefix to match ip against.
|
// prefLen is the length of prefix to match ip against.
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): DHCPv6 inherits the weird behavior of legacy
|
// TODO(e.burkov): DHCPv6 inherits the weird behavior of legacy
|
||||||
@@ -136,7 +130,7 @@ func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool
|
|||||||
// be used instead.
|
// be used instead.
|
||||||
const prefLen = netutil.IPv6BitLen - 8
|
const prefLen = netutil.IPv6BitLen - 8
|
||||||
|
|
||||||
i := slices.IndexFunc(ifaces, func(iface *netInterfaceV6) (contains bool) {
|
i := slices.IndexFunc(ifaces, func(iface *dhcpInterfaceV6) (contains bool) {
|
||||||
return !ip.Less(iface.rangeStart) &&
|
return !ip.Less(iface.rangeStart) &&
|
||||||
netip.PrefixFrom(iface.rangeStart, prefLen).Contains(ip)
|
netip.PrefixFrom(iface.rangeStart, prefLen).Contains(ip)
|
||||||
})
|
})
|
||||||
@@ -144,7 +138,7 @@ func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ifaces[i].netInterface, true
|
return ifaces[i].common, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// options returns the implicit and explicit options for the interface. The two
|
// options returns the implicit and explicit options for the interface. The two
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/quic-go/quic-go"
|
"github.com/quic-go/quic-go"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -217,7 +218,8 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
srv := &Server{
|
srv := &Server{
|
||||||
conf: ServerConfig{TLSConfig: tlsConf},
|
conf: ServerConfig{TLSConfig: tlsConf},
|
||||||
|
baseLogger: slogutil.NewDiscardLogger(),
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/AdguardTeam/golibs/container"
|
"github.com/AdguardTeam/golibs/container"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/stringutil"
|
"github.com/AdguardTeam/golibs/stringutil"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
@@ -158,7 +159,7 @@ type Config struct {
|
|||||||
// IpsetList is the ipset configuration that allows AdGuard Home to add IP
|
// IpsetList is the ipset configuration that allows AdGuard Home to add IP
|
||||||
// addresses of the specified domain names to an ipset list. Syntax:
|
// addresses of the specified domain names to an ipset list. Syntax:
|
||||||
//
|
//
|
||||||
// DOMAIN[,DOMAIN].../IPSET_NAME
|
// DOMAIN[,DOMAIN].../IPSET_NAME[,IPSET_NAME]...
|
||||||
//
|
//
|
||||||
// This field is ignored if [IpsetListFileName] is set.
|
// This field is ignored if [IpsetListFileName] is set.
|
||||||
IpsetList []string `yaml:"ipset"`
|
IpsetList []string `yaml:"ipset"`
|
||||||
@@ -301,6 +302,8 @@ type ServerConfig struct {
|
|||||||
|
|
||||||
// UpstreamMode is a enumeration of upstream mode representations. See
|
// UpstreamMode is a enumeration of upstream mode representations. See
|
||||||
// [proxy.UpstreamModeType].
|
// [proxy.UpstreamModeType].
|
||||||
|
//
|
||||||
|
// TODO(d.kolyshev): Consider using [proxy.UpstreamMode].
|
||||||
type UpstreamMode string
|
type UpstreamMode string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -315,6 +318,7 @@ func (s *Server) newProxyConfig() (conf *proxy.Config, err error) {
|
|||||||
trustedPrefixes := netutil.UnembedPrefixes(srvConf.TrustedProxies)
|
trustedPrefixes := netutil.UnembedPrefixes(srvConf.TrustedProxies)
|
||||||
|
|
||||||
conf = &proxy.Config{
|
conf = &proxy.Config{
|
||||||
|
Logger: s.baseLogger.With(slogutil.KeyPrefix, "dnsproxy"),
|
||||||
HTTP3: srvConf.ServeHTTP3,
|
HTTP3: srvConf.ServeHTTP3,
|
||||||
Ratelimit: int(srvConf.Ratelimit),
|
Ratelimit: int(srvConf.Ratelimit),
|
||||||
RatelimitSubnetLenIPv4: srvConf.RatelimitSubnetLenIPv4,
|
RatelimitSubnetLenIPv4: srvConf.RatelimitSubnetLenIPv4,
|
||||||
@@ -420,8 +424,6 @@ func parseBogusNXDOMAIN(confBogusNXDOMAIN []string) (subnets []netip.Prefix, err
|
|||||||
return subnets, nil
|
return subnets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultBlockedResponseTTL = 3600
|
|
||||||
|
|
||||||
// initDefaultSettings initializes default settings if nothing
|
// initDefaultSettings initializes default settings if nothing
|
||||||
// is configured
|
// is configured
|
||||||
func (s *Server) initDefaultSettings() {
|
func (s *Server) initDefaultSettings() {
|
||||||
@@ -452,24 +454,24 @@ func (s *Server) initDefaultSettings() {
|
|||||||
|
|
||||||
// prepareIpsetListSettings reads and prepares the ipset configuration either
|
// prepareIpsetListSettings reads and prepares the ipset configuration either
|
||||||
// from a file or from the data in the configuration file.
|
// from a file or from the data in the configuration file.
|
||||||
func (s *Server) prepareIpsetListSettings() (err error) {
|
func (s *Server) prepareIpsetListSettings() (ipsets []string, err error) {
|
||||||
fn := s.conf.IpsetListFileName
|
fn := s.conf.IpsetListFileName
|
||||||
if fn == "" {
|
if fn == "" {
|
||||||
return s.ipset.init(s.conf.IpsetList)
|
return s.conf.IpsetList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// #nosec G304 -- Trust the path explicitly given by the user.
|
// #nosec G304 -- Trust the path explicitly given by the user.
|
||||||
data, err := os.ReadFile(fn)
|
data, err := os.ReadFile(fn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ipsets := stringutil.SplitTrimmed(string(data), "\n")
|
ipsets = stringutil.SplitTrimmed(string(data), "\n")
|
||||||
ipsets = stringutil.FilterOut(ipsets, IsCommentOrEmpty)
|
ipsets = slices.DeleteFunc(ipsets, IsCommentOrEmpty)
|
||||||
|
|
||||||
log.Debug("dns: using %d ipset rules from file %q", len(ipsets), fn)
|
log.Debug("dns: using %d ipset rules from file %q", len(ipsets), fn)
|
||||||
|
|
||||||
return s.ipset.init(ipsets)
|
return ipsets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadUpstreams parses upstream DNS servers from the configured file or from
|
// loadUpstreams parses upstream DNS servers from the configured file or from
|
||||||
@@ -690,7 +692,7 @@ func matchesDomainWildcard(host, pat string) (ok bool) {
|
|||||||
// the DNS names and patterns from certificate. dnsNames must be sorted.
|
// the DNS names and patterns from certificate. dnsNames must be sorted.
|
||||||
func anyNameMatches(dnsNames []string, sni string) (ok bool) {
|
func anyNameMatches(dnsNames []string, sni string) (ok bool) {
|
||||||
// Check sni is either a valid hostname or a valid IP address.
|
// Check sni is either a valid hostname or a valid IP address.
|
||||||
if netutil.ValidateHostname(sni) != nil && net.ParseIP(sni) == nil {
|
if !netutil.IsValidHostname(sni) && !netutil.IsValidIPString(sni) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DialContext is an [aghnet.DialContextFunc] that uses s to resolve hostnames.
|
// DialContext is an [aghnet.DialContextFunc] that uses s to resolve hostnames.
|
||||||
@@ -28,7 +29,7 @@ func (s *Server) DialContext(ctx context.Context, network, addr string) (conn ne
|
|||||||
Timeout: time.Minute * 5,
|
Timeout: time.Minute * 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
if net.ParseIP(host) != nil {
|
if netutil.IsValidIPString(host) {
|
||||||
return dialer.DialContext(ctx, network, addr)
|
return dialer.DialContext(ctx, network, addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -27,6 +28,7 @@ import (
|
|||||||
"github.com/AdguardTeam/golibs/cache"
|
"github.com/AdguardTeam/golibs/cache"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil/sysresolv"
|
"github.com/AdguardTeam/golibs/netutil/sysresolv"
|
||||||
"github.com/AdguardTeam/golibs/stringutil"
|
"github.com/AdguardTeam/golibs/stringutil"
|
||||||
@@ -121,12 +123,17 @@ type Server struct {
|
|||||||
// access drops disallowed clients.
|
// access drops disallowed clients.
|
||||||
access *accessManager
|
access *accessManager
|
||||||
|
|
||||||
|
// baseLogger is used to create loggers for other entities. It should not
|
||||||
|
// have a prefix and must not be nil.
|
||||||
|
baseLogger *slog.Logger
|
||||||
|
|
||||||
// localDomainSuffix is the suffix used to detect internal hosts. It
|
// localDomainSuffix is the suffix used to detect internal hosts. It
|
||||||
// must be a valid domain name plus dots on each side.
|
// must be a valid domain name plus dots on each side.
|
||||||
localDomainSuffix string
|
localDomainSuffix string
|
||||||
|
|
||||||
// ipset processes DNS requests using ipset data.
|
// ipset processes DNS requests using ipset data. It must not be nil after
|
||||||
ipset ipsetCtx
|
// initialization. See [newIpsetHandler].
|
||||||
|
ipset *ipsetHandler
|
||||||
|
|
||||||
// privateNets is the configured set of IP networks considered private.
|
// privateNets is the configured set of IP networks considered private.
|
||||||
privateNets netutil.SubnetSet
|
privateNets netutil.SubnetSet
|
||||||
@@ -197,6 +204,10 @@ type DNSCreateParams struct {
|
|||||||
PrivateNets netutil.SubnetSet
|
PrivateNets netutil.SubnetSet
|
||||||
Anonymizer *aghnet.IPMut
|
Anonymizer *aghnet.IPMut
|
||||||
EtcHosts *aghnet.HostsContainer
|
EtcHosts *aghnet.HostsContainer
|
||||||
|
|
||||||
|
// Logger is used as a base logger. It must not be nil.
|
||||||
|
Logger *slog.Logger
|
||||||
|
|
||||||
LocalDomain string
|
LocalDomain string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +244,7 @@ func NewServer(p DNSCreateParams) (s *Server, err error) {
|
|||||||
stats: p.Stats,
|
stats: p.Stats,
|
||||||
queryLog: p.QueryLog,
|
queryLog: p.QueryLog,
|
||||||
privateNets: p.PrivateNets,
|
privateNets: p.PrivateNets,
|
||||||
|
baseLogger: p.Logger,
|
||||||
// TODO(e.burkov): Use some case-insensitive string comparison.
|
// TODO(e.burkov): Use some case-insensitive string comparison.
|
||||||
localDomainSuffix: strings.ToLower(localDomainSuffix),
|
localDomainSuffix: strings.ToLower(localDomainSuffix),
|
||||||
etcHosts: etcHosts,
|
etcHosts: etcHosts,
|
||||||
@@ -596,11 +608,18 @@ func (s *Server) prepareLocalResolvers() (uc *proxy.UpstreamConfig, err error) {
|
|||||||
// the primary DNS proxy instance. It assumes s.serverLock is locked or the
|
// the primary DNS proxy instance. It assumes s.serverLock is locked or the
|
||||||
// Server not running.
|
// Server not running.
|
||||||
func (s *Server) prepareInternalDNS() (err error) {
|
func (s *Server) prepareInternalDNS() (err error) {
|
||||||
err = s.prepareIpsetListSettings()
|
ipsetList, err := s.prepareIpsetListSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("preparing ipset settings: %w", err)
|
return fmt.Errorf("preparing ipset settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ipsetLogger := s.baseLogger.With(slogutil.KeyPrefix, "ipset")
|
||||||
|
s.ipset, err = newIpsetHandler(context.TODO(), ipsetLogger, ipsetList)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
bootOpts := &upstream.Options{
|
bootOpts := &upstream.Options{
|
||||||
Timeout: DefaultTimeout,
|
Timeout: DefaultTimeout,
|
||||||
HTTPVersions: UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams),
|
HTTPVersions: UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams),
|
||||||
@@ -664,6 +683,7 @@ func (s *Server) setupAddrProc() {
|
|||||||
s.addrProc = client.EmptyAddrProc{}
|
s.addrProc = client.EmptyAddrProc{}
|
||||||
} else {
|
} else {
|
||||||
c := s.conf.AddrProcConf
|
c := s.conf.AddrProcConf
|
||||||
|
c.BaseLogger = s.baseLogger
|
||||||
c.DialContext = s.DialContext
|
c.DialContext = s.DialContext
|
||||||
c.PrivateSubnets = s.privateNets
|
c.PrivateSubnets = s.privateNets
|
||||||
c.UsePrivateRDNS = s.conf.UsePrivateRDNS
|
c.UsePrivateRDNS = s.conf.UsePrivateRDNS
|
||||||
@@ -707,6 +727,7 @@ func validateBlockingMode(
|
|||||||
func (s *Server) prepareInternalProxy() (err error) {
|
func (s *Server) prepareInternalProxy() (err error) {
|
||||||
srvConf := s.conf
|
srvConf := s.conf
|
||||||
conf := &proxy.Config{
|
conf := &proxy.Config{
|
||||||
|
Logger: s.baseLogger.With(slogutil.KeyPrefix, "dnsproxy"),
|
||||||
CacheEnabled: true,
|
CacheEnabled: true,
|
||||||
CacheSizeBytes: 4096,
|
CacheSizeBytes: 4096,
|
||||||
PrivateRDNSUpstreamConfig: srvConf.PrivateRDNSUpstreamConfig,
|
PrivateRDNSUpstreamConfig: srvConf.PrivateRDNSUpstreamConfig,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
|
||||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
@@ -99,6 +100,7 @@ func createTestServer(
|
|||||||
DHCPServer: dhcp,
|
DHCPServer: dhcp,
|
||||||
DNSFilter: f,
|
DNSFilter: f,
|
||||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -339,7 +341,10 @@ func TestServer_timeout(t *testing.T) {
|
|||||||
ServePlainDNS: true,
|
ServePlainDNS: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := NewServer(DNSCreateParams{DNSFilter: createTestDNSFilter(t)})
|
s, err := NewServer(DNSCreateParams{
|
||||||
|
DNSFilter: createTestDNSFilter(t),
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = s.Prepare(srvConf)
|
err = s.Prepare(srvConf)
|
||||||
@@ -349,7 +354,10 @@ func TestServer_timeout(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("default", func(t *testing.T) {
|
t.Run("default", func(t *testing.T) {
|
||||||
s, err := NewServer(DNSCreateParams{DNSFilter: createTestDNSFilter(t)})
|
s, err := NewServer(DNSCreateParams{
|
||||||
|
DNSFilter: createTestDNSFilter(t),
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
s.conf.Config.UpstreamMode = UpstreamModeLoadBalance
|
s.conf.Config.UpstreamMode = UpstreamModeLoadBalance
|
||||||
@@ -376,7 +384,9 @@ func TestServer_Prepare_fallbacks(t *testing.T) {
|
|||||||
ServePlainDNS: true,
|
ServePlainDNS: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := NewServer(DNSCreateParams{})
|
s, err := NewServer(DNSCreateParams{
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = s.Prepare(srvConf)
|
err = s.Prepare(srvConf)
|
||||||
@@ -962,6 +972,7 @@ func TestBlockedCustomIP(t *testing.T) {
|
|||||||
DHCPServer: dhcp,
|
DHCPServer: dhcp,
|
||||||
DNSFilter: f,
|
DNSFilter: f,
|
||||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -1127,6 +1138,7 @@ func TestRewrite(t *testing.T) {
|
|||||||
DHCPServer: dhcp,
|
DHCPServer: dhcp,
|
||||||
DNSFilter: f,
|
DNSFilter: f,
|
||||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -1256,6 +1268,7 @@ func TestPTRResponseFromDHCPLeases(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
LocalDomain: localDomain,
|
LocalDomain: localDomain,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1341,6 +1354,7 @@ func TestPTRResponseFromHosts(t *testing.T) {
|
|||||||
DHCPServer: dhcp,
|
DHCPServer: dhcp,
|
||||||
DNSFilter: flt,
|
DNSFilter: flt,
|
||||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -1392,24 +1406,29 @@ func TestNewServer(t *testing.T) {
|
|||||||
in DNSCreateParams
|
in DNSCreateParams
|
||||||
wantErrMsg string
|
wantErrMsg string
|
||||||
}{{
|
}{{
|
||||||
name: "success",
|
name: "success",
|
||||||
in: DNSCreateParams{},
|
in: DNSCreateParams{
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
|
},
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
}, {
|
}, {
|
||||||
name: "success_local_tld",
|
name: "success_local_tld",
|
||||||
in: DNSCreateParams{
|
in: DNSCreateParams{
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
LocalDomain: "mynet",
|
LocalDomain: "mynet",
|
||||||
},
|
},
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
}, {
|
}, {
|
||||||
name: "success_local_domain",
|
name: "success_local_domain",
|
||||||
in: DNSCreateParams{
|
in: DNSCreateParams{
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
LocalDomain: "my.local.net",
|
LocalDomain: "my.local.net",
|
||||||
},
|
},
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
}, {
|
}, {
|
||||||
name: "bad_local_domain",
|
name: "bad_local_domain",
|
||||||
in: DNSCreateParams{
|
in: DNSCreateParams{
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
LocalDomain: "!!!",
|
LocalDomain: "!!!",
|
||||||
},
|
},
|
||||||
wantErrMsg: `local domain: bad domain name "!!!": ` +
|
wantErrMsg: `local domain: bad domain name "!!!": ` +
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -57,6 +58,7 @@ func TestHandleDNSRequest_handleDNSRequest(t *testing.T) {
|
|||||||
},
|
},
|
||||||
DNSFilter: f,
|
DNSFilter: f,
|
||||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -229,6 +231,7 @@ func TestHandleDNSRequest_filterDNSResponse(t *testing.T) {
|
|||||||
DHCPServer: &testDHCP{},
|
DHCPServer: &testDHCP{},
|
||||||
DNSFilter: f,
|
DNSFilter: f,
|
||||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
|||||||
}, {
|
}, {
|
||||||
name: "upstream_dns_bad",
|
name: "upstream_dns_bad",
|
||||||
wantSet: `validating dns config: upstream servers: parsing error at index 0: ` +
|
wantSet: `validating dns config: upstream servers: parsing error at index 0: ` +
|
||||||
`cannot prepare the upstream: invalid address !!!: bad hostname "!!!": ` +
|
`cannot prepare the upstream: invalid address !!!: bad domain name "!!!": ` +
|
||||||
`bad top-level domain name label "!!!": bad top-level domain name label rune '!'`,
|
`bad top-level domain name label "!!!": bad top-level domain name label rune '!'`,
|
||||||
}, {
|
}, {
|
||||||
name: "bootstraps_bad",
|
name: "bootstraps_bad",
|
||||||
|
|||||||
@@ -1,28 +1,43 @@
|
|||||||
package dnsforward
|
package dnsforward
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/ipset"
|
"github.com/AdguardTeam/AdGuardHome/internal/ipset"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ipsetCtx is the ipset context. ipsetMgr can be nil.
|
// ipsetHandler is the ipset context. ipsetMgr can be nil.
|
||||||
type ipsetCtx struct {
|
type ipsetHandler struct {
|
||||||
ipsetMgr ipset.Manager
|
ipsetMgr ipset.Manager
|
||||||
|
logger *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// init initializes the ipset context. It is not safe for concurrent use.
|
// newIpsetHandler returns a new initialized [ipsetHandler]. It is not safe for
|
||||||
//
|
// concurrent use.
|
||||||
// TODO(a.garipov): Rewrite into a simple constructor?
|
func newIpsetHandler(
|
||||||
func (c *ipsetCtx) init(ipsetConf []string) (err error) {
|
ctx context.Context,
|
||||||
c.ipsetMgr, err = ipset.NewManager(ipsetConf)
|
logger *slog.Logger,
|
||||||
if errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrPermission) {
|
ipsetList []string,
|
||||||
|
) (h *ipsetHandler, err error) {
|
||||||
|
h = &ipsetHandler{
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
conf := &ipset.Config{
|
||||||
|
Logger: logger,
|
||||||
|
Lines: ipsetList,
|
||||||
|
}
|
||||||
|
h.ipsetMgr, err = ipset.NewManager(ctx, conf)
|
||||||
|
if errors.Is(err, os.ErrInvalid) ||
|
||||||
|
errors.Is(err, os.ErrPermission) ||
|
||||||
|
errors.Is(err, errors.ErrUnsupported) {
|
||||||
// ipset cannot currently be initialized if the server was installed
|
// ipset cannot currently be initialized if the server was installed
|
||||||
// from Snap or when the user or the binary doesn't have the required
|
// from Snap or when the user or the binary doesn't have the required
|
||||||
// permissions, or when the kernel doesn't support netfilter.
|
// permissions, or when the kernel doesn't support netfilter.
|
||||||
@@ -31,30 +46,28 @@ func (c *ipsetCtx) init(ipsetConf []string) (err error) {
|
|||||||
//
|
//
|
||||||
// TODO(a.garipov): The Snap problem can probably be solved if we add
|
// TODO(a.garipov): The Snap problem can probably be solved if we add
|
||||||
// the netlink-connector interface plug.
|
// the netlink-connector interface plug.
|
||||||
log.Info("ipset: warning: cannot initialize: %s", err)
|
logger.WarnContext(ctx, "cannot initialize", slogutil.KeyError, err)
|
||||||
|
|
||||||
return nil
|
return h, nil
|
||||||
} else if errors.Is(err, errors.ErrUnsupported) {
|
|
||||||
log.Info("ipset: warning: %s", err)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return fmt.Errorf("initializing ipset: %w", err)
|
return nil, fmt.Errorf("initializing ipset: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// close closes the Linux Netfilter connections. close can be called on a nil
|
||||||
|
// handler.
|
||||||
|
func (h *ipsetHandler) close() (err error) {
|
||||||
|
if h != nil && h.ipsetMgr != nil {
|
||||||
|
return h.ipsetMgr.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// close closes the Linux Netfilter connections.
|
// dctxIsFilled returns true if dctx has enough information to process.
|
||||||
func (c *ipsetCtx) close() (err error) {
|
func dctxIsFilled(dctx *dnsContext) (ok bool) {
|
||||||
if c.ipsetMgr != nil {
|
|
||||||
return c.ipsetMgr.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ipsetCtx) dctxIsfilled(dctx *dnsContext) (ok bool) {
|
|
||||||
return dctx != nil &&
|
return dctx != nil &&
|
||||||
dctx.responseFromUpstream &&
|
dctx.responseFromUpstream &&
|
||||||
dctx.proxyCtx != nil &&
|
dctx.proxyCtx != nil &&
|
||||||
@@ -65,8 +78,8 @@ func (c *ipsetCtx) dctxIsfilled(dctx *dnsContext) (ok bool) {
|
|||||||
|
|
||||||
// skipIpsetProcessing returns true when the ipset processing can be skipped for
|
// skipIpsetProcessing returns true when the ipset processing can be skipped for
|
||||||
// this request.
|
// this request.
|
||||||
func (c *ipsetCtx) skipIpsetProcessing(dctx *dnsContext) (ok bool) {
|
func (h *ipsetHandler) skipIpsetProcessing(dctx *dnsContext) (ok bool) {
|
||||||
if c == nil || c.ipsetMgr == nil || !c.dctxIsfilled(dctx) {
|
if h == nil || h.ipsetMgr == nil || !dctxIsFilled(dctx) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,31 +121,31 @@ func ipsFromAnswer(ans []dns.RR) (ip4s, ip6s []net.IP) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// process adds the resolved IP addresses to the domain's ipsets, if any.
|
// process adds the resolved IP addresses to the domain's ipsets, if any.
|
||||||
func (c *ipsetCtx) process(dctx *dnsContext) (rc resultCode) {
|
func (h *ipsetHandler) process(dctx *dnsContext) (rc resultCode) {
|
||||||
log.Debug("dnsforward: ipset: started processing")
|
// TODO(s.chzhen): Use passed context.
|
||||||
defer log.Debug("dnsforward: ipset: finished processing")
|
ctx := context.TODO()
|
||||||
|
h.logger.DebugContext(ctx, "started processing")
|
||||||
|
defer h.logger.DebugContext(ctx, "finished processing")
|
||||||
|
|
||||||
if c.skipIpsetProcessing(dctx) {
|
if h.skipIpsetProcessing(dctx) {
|
||||||
return resultCodeSuccess
|
return resultCodeSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("ipset: starting processing")
|
|
||||||
|
|
||||||
req := dctx.proxyCtx.Req
|
req := dctx.proxyCtx.Req
|
||||||
host := req.Question[0].Name
|
host := req.Question[0].Name
|
||||||
host = strings.TrimSuffix(host, ".")
|
host = strings.TrimSuffix(host, ".")
|
||||||
host = strings.ToLower(host)
|
host = strings.ToLower(host)
|
||||||
|
|
||||||
ip4s, ip6s := ipsFromAnswer(dctx.proxyCtx.Res.Answer)
|
ip4s, ip6s := ipsFromAnswer(dctx.proxyCtx.Res.Answer)
|
||||||
n, err := c.ipsetMgr.Add(host, ip4s, ip6s)
|
n, err := h.ipsetMgr.Add(ctx, host, ip4s, ip6s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Consider ipset errors non-critical to the request.
|
// Consider ipset errors non-critical to the request.
|
||||||
log.Error("dnsforward: ipset: adding host ips: %s", err)
|
h.logger.ErrorContext(ctx, "adding host ips", slogutil.KeyError, err)
|
||||||
|
|
||||||
return resultCodeSuccess
|
return resultCodeSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("dnsforward: ipset: added %d new ipset entries", n)
|
h.logger.DebugContext(ctx, "added new ipset entries", "num", n)
|
||||||
|
|
||||||
return resultCodeSuccess
|
return resultCodeSuccess
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package dnsforward
|
package dnsforward
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -16,7 +18,7 @@ type fakeIpsetMgr struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add implements the aghnet.IpsetManager interface for *fakeIpsetMgr.
|
// Add implements the aghnet.IpsetManager interface for *fakeIpsetMgr.
|
||||||
func (m *fakeIpsetMgr) Add(host string, ip4s, ip6s []net.IP) (n int, err error) {
|
func (m *fakeIpsetMgr) Add(_ context.Context, host string, ip4s, ip6s []net.IP) (n int, err error) {
|
||||||
m.ip4s = append(m.ip4s, ip4s...)
|
m.ip4s = append(m.ip4s, ip4s...)
|
||||||
m.ip6s = append(m.ip6s, ip6s...)
|
m.ip6s = append(m.ip6s, ip6s...)
|
||||||
|
|
||||||
@@ -58,7 +60,9 @@ func TestIpsetCtx_process(t *testing.T) {
|
|||||||
responseFromUpstream: true,
|
responseFromUpstream: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
ictx := &ipsetCtx{}
|
ictx := &ipsetHandler{
|
||||||
|
logger: slogutil.NewDiscardLogger(),
|
||||||
|
}
|
||||||
rc := ictx.process(dctx)
|
rc := ictx.process(dctx)
|
||||||
assert.Equal(t, resultCodeSuccess, rc)
|
assert.Equal(t, resultCodeSuccess, rc)
|
||||||
|
|
||||||
@@ -77,8 +81,9 @@ func TestIpsetCtx_process(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m := &fakeIpsetMgr{}
|
m := &fakeIpsetMgr{}
|
||||||
ictx := &ipsetCtx{
|
ictx := &ipsetHandler{
|
||||||
ipsetMgr: m,
|
ipsetMgr: m,
|
||||||
|
logger: slogutil.NewDiscardLogger(),
|
||||||
}
|
}
|
||||||
|
|
||||||
rc := ictx.process(dctx)
|
rc := ictx.process(dctx)
|
||||||
@@ -101,8 +106,9 @@ func TestIpsetCtx_process(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m := &fakeIpsetMgr{}
|
m := &fakeIpsetMgr{}
|
||||||
ictx := &ipsetCtx{
|
ictx := &ipsetHandler{
|
||||||
ipsetMgr: m,
|
ipsetMgr: m,
|
||||||
|
logger: slogutil.NewDiscardLogger(),
|
||||||
}
|
}
|
||||||
|
|
||||||
rc := ictx.process(dctx)
|
rc := ictx.process(dctx)
|
||||||
@@ -124,8 +130,9 @@ func TestIpsetCtx_SkipIpsetProcessing(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m := &fakeIpsetMgr{}
|
m := &fakeIpsetMgr{}
|
||||||
ictx := &ipsetCtx{
|
ictx := &ipsetHandler{
|
||||||
ipsetMgr: m,
|
ipsetMgr: m,
|
||||||
|
logger: slogutil.NewDiscardLogger(),
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func (s *Server) genDNSFilterMessage(
|
|||||||
return s.replyCompressed(req)
|
return s.replyCompressed(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.newMsgNODATA(req)
|
return s.NewMsgNODATA(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch res.Reason {
|
switch res.Reason {
|
||||||
@@ -344,51 +344,6 @@ func (s *Server) makeResponseREFUSED(req *dns.Msg) *dns.Msg {
|
|||||||
return s.reply(req, dns.RcodeRefused)
|
return s.reply(req, dns.RcodeRefused)
|
||||||
}
|
}
|
||||||
|
|
||||||
// newMsgNODATA returns a properly initialized NODATA response.
|
|
||||||
//
|
|
||||||
// See https://www.rfc-editor.org/rfc/rfc2308#section-2.2.
|
|
||||||
func (s *Server) newMsgNODATA(req *dns.Msg) (resp *dns.Msg) {
|
|
||||||
resp = s.reply(req, dns.RcodeSuccess)
|
|
||||||
resp.Ns = s.genSOA(req)
|
|
||||||
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) genSOA(request *dns.Msg) []dns.RR {
|
|
||||||
zone := ""
|
|
||||||
if len(request.Question) > 0 {
|
|
||||||
zone = request.Question[0].Name
|
|
||||||
}
|
|
||||||
|
|
||||||
soa := dns.SOA{
|
|
||||||
// values copied from verisign's nonexistent .com domain
|
|
||||||
// their exact values are not important in our use case because they are used for domain transfers between primary/secondary DNS servers
|
|
||||||
Refresh: 1800,
|
|
||||||
Retry: 900,
|
|
||||||
Expire: 604800,
|
|
||||||
Minttl: 86400,
|
|
||||||
// copied from AdGuard DNS
|
|
||||||
Ns: "fake-for-negative-caching.adguard.com.",
|
|
||||||
Serial: 100500,
|
|
||||||
// rest is request-specific
|
|
||||||
Hdr: dns.RR_Header{
|
|
||||||
Name: zone,
|
|
||||||
Rrtype: dns.TypeSOA,
|
|
||||||
Ttl: s.dnsFilter.BlockedResponseTTL(),
|
|
||||||
Class: dns.ClassINET,
|
|
||||||
},
|
|
||||||
Mbox: "hostmaster.", // zone will be appended later if it's not empty or "."
|
|
||||||
}
|
|
||||||
if soa.Hdr.Ttl == 0 {
|
|
||||||
soa.Hdr.Ttl = defaultBlockedResponseTTL
|
|
||||||
}
|
|
||||||
if len(zone) > 0 && zone[0] != '.' {
|
|
||||||
soa.Mbox += zone
|
|
||||||
}
|
|
||||||
|
|
||||||
return []dns.RR{&soa}
|
|
||||||
}
|
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ proxy.MessageConstructor = (*Server)(nil)
|
var _ proxy.MessageConstructor = (*Server)(nil)
|
||||||
|
|
||||||
@@ -425,3 +380,52 @@ func (s *Server) NewMsgNOTIMPLEMENTED(req *dns.Msg) (resp *dns.Msg) {
|
|||||||
|
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewMsgNODATA implements the [proxy.MessageConstructor] interface for *Server.
|
||||||
|
func (s *Server) NewMsgNODATA(req *dns.Msg) (resp *dns.Msg) {
|
||||||
|
resp = s.reply(req, dns.RcodeSuccess)
|
||||||
|
resp.Ns = s.genSOA(req)
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) genSOA(req *dns.Msg) []dns.RR {
|
||||||
|
zone := ""
|
||||||
|
if len(req.Question) > 0 {
|
||||||
|
zone = req.Question[0].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultBlockedResponseTTL = 3600
|
||||||
|
|
||||||
|
soa := dns.SOA{
|
||||||
|
// Values copied from verisign's nonexistent.com domain.
|
||||||
|
//
|
||||||
|
// Their exact values are not important in our use case because they are
|
||||||
|
// used for domain transfers between primary/secondary DNS servers.
|
||||||
|
Refresh: 1800,
|
||||||
|
Retry: 900,
|
||||||
|
Expire: 604800,
|
||||||
|
Minttl: 86400,
|
||||||
|
// copied from AdGuard DNS
|
||||||
|
Ns: "fake-for-negative-caching.adguard.com.",
|
||||||
|
Serial: 100500,
|
||||||
|
// rest is request-specific
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: zone,
|
||||||
|
Rrtype: dns.TypeSOA,
|
||||||
|
Ttl: s.dnsFilter.BlockedResponseTTL(),
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
},
|
||||||
|
// zone will be appended later if it's not ".".
|
||||||
|
Mbox: "hostmaster.",
|
||||||
|
}
|
||||||
|
if soa.Hdr.Ttl == 0 {
|
||||||
|
soa.Hdr.Ttl = defaultBlockedResponseTTL
|
||||||
|
}
|
||||||
|
|
||||||
|
if zone != "." {
|
||||||
|
soa.Mbox += zone
|
||||||
|
}
|
||||||
|
|
||||||
|
return []dns.RR{&soa}
|
||||||
|
}
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ func (s *Server) processInitial(dctx *dnsContext) (rc resultCode) {
|
|||||||
q := pctx.Req.Question[0]
|
q := pctx.Req.Question[0]
|
||||||
qt := q.Qtype
|
qt := q.Qtype
|
||||||
if s.conf.AAAADisabled && qt == dns.TypeAAAA {
|
if s.conf.AAAADisabled && qt == dns.TypeAAAA {
|
||||||
_ = proxy.CheckDisabledAAAARequest(pctx, true)
|
pctx.Res = s.NewMsgNODATA(pctx.Req)
|
||||||
|
|
||||||
return resultCodeFinish
|
return resultCodeFinish
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/AdguardTeam/urlfilter/rules"
|
"github.com/AdguardTeam/urlfilter/rules"
|
||||||
@@ -430,6 +431,7 @@ func TestServer_ProcessDHCPHosts_localRestriction(t *testing.T) {
|
|||||||
dnsFilter: createTestDNSFilter(t),
|
dnsFilter: createTestDNSFilter(t),
|
||||||
dhcpServer: dhcp,
|
dhcpServer: dhcp,
|
||||||
localDomainSuffix: localDomainSuffix,
|
localDomainSuffix: localDomainSuffix,
|
||||||
|
baseLogger: slogutil.NewDiscardLogger(),
|
||||||
}
|
}
|
||||||
|
|
||||||
req := &dns.Msg{
|
req := &dns.Msg{
|
||||||
@@ -565,6 +567,7 @@ func TestServer_ProcessDHCPHosts(t *testing.T) {
|
|||||||
dnsFilter: createTestDNSFilter(t),
|
dnsFilter: createTestDNSFilter(t),
|
||||||
dhcpServer: testDHCP,
|
dhcpServer: testDHCP,
|
||||||
localDomainSuffix: tc.suffix,
|
localDomainSuffix: tc.suffix,
|
||||||
|
baseLogger: slogutil.NewDiscardLogger(),
|
||||||
}
|
}
|
||||||
|
|
||||||
req := &dns.Msg{
|
req := &dns.Msg{
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/stats"
|
"github.com/AdguardTeam/AdGuardHome/internal/stats"
|
||||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -202,6 +203,7 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) {
|
|||||||
ql := &testQueryLog{}
|
ql := &testQueryLog{}
|
||||||
st := &testStats{}
|
st := &testStats{}
|
||||||
srv := &Server{
|
srv := &Server{
|
||||||
|
baseLogger: slogutil.NewDiscardLogger(),
|
||||||
queryLog: ql,
|
queryLog: ql,
|
||||||
stats: st,
|
stats: st,
|
||||||
anonymizer: aghnet.NewIPMut(nil),
|
anonymizer: aghnet.NewIPMut(nil),
|
||||||
|
|||||||
@@ -150,12 +150,12 @@ func setProxyUpstreamMode(
|
|||||||
) (err error) {
|
) (err error) {
|
||||||
switch upstreamMode {
|
switch upstreamMode {
|
||||||
case UpstreamModeParallel:
|
case UpstreamModeParallel:
|
||||||
conf.UpstreamMode = proxy.UModeParallel
|
conf.UpstreamMode = proxy.UpstreamModeParallel
|
||||||
case UpstreamModeFastestAddr:
|
case UpstreamModeFastestAddr:
|
||||||
conf.UpstreamMode = proxy.UModeFastestAddr
|
conf.UpstreamMode = proxy.UpstreamModeFastestAddr
|
||||||
conf.FastestPingTimeout = fastestTimeout
|
conf.FastestPingTimeout = fastestTimeout
|
||||||
case UpstreamModeLoadBalance:
|
case UpstreamModeLoadBalance:
|
||||||
conf.UpstreamMode = proxy.UModeLoadBalance
|
conf.UpstreamMode = proxy.UpstreamModeLoadBalance
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unexpected value %q", upstreamMode)
|
return fmt.Errorf("unexpected value %q", upstreamMode)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ func fromCacheItem(item *cacheItem) (data []byte) {
|
|||||||
data = binary.BigEndian.AppendUint64(data, uint64(expiry))
|
data = binary.BigEndian.AppendUint64(data, uint64(expiry))
|
||||||
|
|
||||||
for _, v := range item.hashes {
|
for _, v := range item.hashes {
|
||||||
// nolint:looppointer // The subslice of v is used for a copy.
|
|
||||||
data = append(data, v[:]...)
|
data = append(data, v[:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +62,6 @@ func (c *Checker) findInCache(
|
|||||||
|
|
||||||
i := 0
|
i := 0
|
||||||
for _, hash := range hashes {
|
for _, hash := range hashes {
|
||||||
// nolint:looppointer // The has subslice is used for a cache lookup.
|
|
||||||
data := c.cache.Get(hash[:prefixLen])
|
data := c.cache.Get(hash[:prefixLen])
|
||||||
if data == nil {
|
if data == nil {
|
||||||
hashes[i] = hash
|
hashes[i] = hash
|
||||||
@@ -98,7 +96,6 @@ func (c *Checker) storeInCache(hashesToRequest, respHashes []hostnameHash) {
|
|||||||
|
|
||||||
for _, hash := range respHashes {
|
for _, hash := range respHashes {
|
||||||
var pref prefix
|
var pref prefix
|
||||||
// nolint:looppointer // The hash subslice is used for a copy.
|
|
||||||
copy(pref[:], hash[:])
|
copy(pref[:], hash[:])
|
||||||
|
|
||||||
hashToStore[pref] = append(hashToStore[pref], hash)
|
hashToStore[pref] = append(hashToStore[pref], hash)
|
||||||
@@ -109,11 +106,9 @@ func (c *Checker) storeInCache(hashesToRequest, respHashes []hostnameHash) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, hash := range hashesToRequest {
|
for _, hash := range hashesToRequest {
|
||||||
// nolint:looppointer // The hash subslice is used for a cache lookup.
|
|
||||||
val := c.cache.Get(hash[:prefixLen])
|
val := c.cache.Get(hash[:prefixLen])
|
||||||
if val == nil {
|
if val == nil {
|
||||||
var pref prefix
|
var pref prefix
|
||||||
// nolint:looppointer // The hash subslice is used for a copy.
|
|
||||||
copy(pref[:], hash[:])
|
copy(pref[:], hash[:])
|
||||||
|
|
||||||
c.setCache(pref, nil)
|
c.setCache(pref, nil)
|
||||||
|
|||||||
@@ -173,7 +173,6 @@ func (c *Checker) getQuestion(hashes []hostnameHash) (q string) {
|
|||||||
b := &strings.Builder{}
|
b := &strings.Builder{}
|
||||||
|
|
||||||
for _, hash := range hashes {
|
for _, hash := range hashes {
|
||||||
// nolint:looppointer // The hash subslice is used for hex encoding.
|
|
||||||
stringutil.WriteToBuilder(b, hex.EncodeToString(hash[:prefixLen]), ".")
|
stringutil.WriteToBuilder(b, hex.EncodeToString(hash[:prefixLen]), ".")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type SafeSearchConfig struct {
|
|||||||
|
|
||||||
Bing bool `yaml:"bing" json:"bing"`
|
Bing bool `yaml:"bing" json:"bing"`
|
||||||
DuckDuckGo bool `yaml:"duckduckgo" json:"duckduckgo"`
|
DuckDuckGo bool `yaml:"duckduckgo" json:"duckduckgo"`
|
||||||
|
Ecosia bool `yaml:"ecosia" json:"ecosia"`
|
||||||
Google bool `yaml:"google" json:"google"`
|
Google bool `yaml:"google" json:"google"`
|
||||||
Pixabay bool `yaml:"pixabay" json:"pixabay"`
|
Pixabay bool `yaml:"pixabay" json:"pixabay"`
|
||||||
Yandex bool `yaml:"yandex" json:"yandex"`
|
Yandex bool `yaml:"yandex" json:"yandex"`
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ var pixabay string
|
|||||||
//go:embed rules/duckduckgo.txt
|
//go:embed rules/duckduckgo.txt
|
||||||
var duckduckgo string
|
var duckduckgo string
|
||||||
|
|
||||||
|
//go:embed rules/ecosia.txt
|
||||||
|
var ecosia string
|
||||||
|
|
||||||
//go:embed rules/yandex.txt
|
//go:embed rules/yandex.txt
|
||||||
var yandex string
|
var yandex string
|
||||||
|
|
||||||
@@ -27,6 +30,7 @@ var youtube string
|
|||||||
var safeSearchRules = map[Service]string{
|
var safeSearchRules = map[Service]string{
|
||||||
Bing: bing,
|
Bing: bing,
|
||||||
DuckDuckGo: duckduckgo,
|
DuckDuckGo: duckduckgo,
|
||||||
|
Ecosia: ecosia,
|
||||||
Google: google,
|
Google: google,
|
||||||
Pixabay: pixabay,
|
Pixabay: pixabay,
|
||||||
Yandex: yandex,
|
Yandex: yandex,
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|www.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com
|
|www.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com
|
||||||
|
|edgeservices.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com
|
||||||
|
|||||||
1
internal/filtering/safesearch/rules/ecosia.txt
Normal file
1
internal/filtering/safesearch/rules/ecosia.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|www.ecosia.org^$dnsrewrite=NOERROR;CNAME;strict-safe-search.ecosia.org
|
||||||
@@ -46,6 +46,9 @@
|
|||||||
|www.google.co.uz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
|www.google.co.uz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||||
|www.google.co.ve^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
|www.google.co.ve^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||||
|www.google.co.vi^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
|www.google.co.vi^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||||
|
|www.google.co.za^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||||
|
|www.google.co.zm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||||
|
|www.google.co.zw^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||||
|www.google.com.af^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
|www.google.com.af^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||||
|www.google.com.ag^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
|www.google.com.ag^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||||
|www.google.com.ai^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
|www.google.com.ai^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type Service string
|
|||||||
const (
|
const (
|
||||||
Bing Service = "bing"
|
Bing Service = "bing"
|
||||||
DuckDuckGo Service = "duckduckgo"
|
DuckDuckGo Service = "duckduckgo"
|
||||||
|
Ecosia Service = "ecosia"
|
||||||
Google Service = "google"
|
Google Service = "google"
|
||||||
Pixabay Service = "pixabay"
|
Pixabay Service = "pixabay"
|
||||||
Yandex Service = "yandex"
|
Yandex Service = "yandex"
|
||||||
@@ -41,6 +42,8 @@ func isServiceProtected(s filtering.SafeSearchConfig, service Service) (ok bool)
|
|||||||
return s.Bing
|
return s.Bing
|
||||||
case DuckDuckGo:
|
case DuckDuckGo:
|
||||||
return s.DuckDuckGo
|
return s.DuckDuckGo
|
||||||
|
case Ecosia:
|
||||||
|
return s.Ecosia
|
||||||
case Google:
|
case Google:
|
||||||
return s.Google
|
return s.Google
|
||||||
case Pixabay:
|
case Pixabay:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ var defaultSafeSearchConf = filtering.SafeSearchConfig{
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
Bing: true,
|
Bing: true,
|
||||||
DuckDuckGo: true,
|
DuckDuckGo: true,
|
||||||
|
Ecosia: true,
|
||||||
Google: true,
|
Google: true,
|
||||||
Pixabay: true,
|
Pixabay: true,
|
||||||
Yandex: true,
|
Yandex: true,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ var testConf = filtering.SafeSearchConfig{
|
|||||||
|
|
||||||
Bing: true,
|
Bing: true,
|
||||||
DuckDuckGo: true,
|
DuckDuckGo: true,
|
||||||
|
Ecosia: true,
|
||||||
Google: true,
|
Google: true,
|
||||||
Pixabay: true,
|
Pixabay: true,
|
||||||
Yandex: true,
|
Yandex: true,
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ func glGetTokenDate(file string) uint32 {
|
|||||||
|
|
||||||
buf := bytes.NewBuffer(bs)
|
buf := bytes.NewBuffer(bs)
|
||||||
|
|
||||||
|
// TODO(a.garipov): Get rid of github.com/josharian/native dependency.
|
||||||
err = binary.Read(buf, native.Endian, &dateToken)
|
err = binary.Read(buf, native.Endian, &dateToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("decoding token: %s", err)
|
log.Error("decoding token: %s", err)
|
||||||
|
|||||||
@@ -47,9 +47,6 @@ type clientsContainer struct {
|
|||||||
// storage stores information about persistent clients.
|
// storage stores information about persistent clients.
|
||||||
storage *client.Storage
|
storage *client.Storage
|
||||||
|
|
||||||
// runtimeIndex stores information about runtime clients.
|
|
||||||
runtimeIndex *client.RuntimeIndex
|
|
||||||
|
|
||||||
// dhcp is the DHCP service implementation.
|
// dhcp is the DHCP service implementation.
|
||||||
dhcp DHCP
|
dhcp DHCP
|
||||||
|
|
||||||
@@ -105,8 +102,6 @@ func (clients *clientsContainer) Init(
|
|||||||
return errors.Error("clients container already initialized")
|
return errors.Error("clients container already initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
clients.runtimeIndex = client.NewRuntimeIndex()
|
|
||||||
|
|
||||||
clients.storage = client.NewStorage(&client.Config{
|
clients.storage = client.NewStorage(&client.Config{
|
||||||
AllowedTags: clientTags,
|
AllowedTags: clientTags,
|
||||||
})
|
})
|
||||||
@@ -358,7 +353,7 @@ func (clients *clientsContainer) clientSource(ip netip.Addr) (src client.Source)
|
|||||||
return client.SourcePersistent
|
return client.SourcePersistent
|
||||||
}
|
}
|
||||||
|
|
||||||
rc := clients.runtimeIndex.Client(ip)
|
rc := clients.storage.ClientRuntime(ip)
|
||||||
if rc != nil {
|
if rc != nil {
|
||||||
src, _ = rc.Info()
|
src, _ = rc.Info()
|
||||||
}
|
}
|
||||||
@@ -539,22 +534,9 @@ func (clients *clientsContainer) findDHCP(ip netip.Addr) (c *client.Persistent,
|
|||||||
return clients.storage.FindByMAC(foundMAC)
|
return clients.storage.FindByMAC(foundMAC)
|
||||||
}
|
}
|
||||||
|
|
||||||
// runtimeClient returns a runtime client from internal index. Note that it
|
|
||||||
// doesn't include DHCP clients.
|
|
||||||
func (clients *clientsContainer) runtimeClient(ip netip.Addr) (rc *client.Runtime) {
|
|
||||||
if ip == (netip.Addr{}) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
clients.lock.Lock()
|
|
||||||
defer clients.lock.Unlock()
|
|
||||||
|
|
||||||
return clients.runtimeIndex.Client(ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
// findRuntimeClient finds a runtime client by their IP.
|
// findRuntimeClient finds a runtime client by their IP.
|
||||||
func (clients *clientsContainer) findRuntimeClient(ip netip.Addr) (rc *client.Runtime) {
|
func (clients *clientsContainer) findRuntimeClient(ip netip.Addr) (rc *client.Runtime) {
|
||||||
rc = clients.runtimeClient(ip)
|
rc = clients.storage.ClientRuntime(ip)
|
||||||
host := clients.dhcp.HostByIP(ip)
|
host := clients.dhcp.HostByIP(ip)
|
||||||
|
|
||||||
if host != "" {
|
if host != "" {
|
||||||
@@ -580,20 +562,11 @@ func (clients *clientsContainer) setWHOISInfo(ip netip.Addr, wi *whois.Info) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rc := clients.runtimeIndex.Client(ip)
|
rc := client.NewRuntime(ip)
|
||||||
if rc == nil {
|
|
||||||
// Create a RuntimeClient implicitly so that we don't do this check
|
|
||||||
// again.
|
|
||||||
rc = client.NewRuntime(ip)
|
|
||||||
clients.runtimeIndex.Add(rc)
|
|
||||||
|
|
||||||
log.Debug("clients: set whois info for runtime client with ip %s: %+v", ip, wi)
|
|
||||||
} else {
|
|
||||||
host, _ := rc.Info()
|
|
||||||
log.Debug("clients: set whois info for runtime client %s: %+v", host, wi)
|
|
||||||
}
|
|
||||||
|
|
||||||
rc.SetWHOIS(wi)
|
rc.SetWHOIS(wi)
|
||||||
|
clients.storage.UpdateRuntime(rc)
|
||||||
|
|
||||||
|
log.Debug("clients: set whois info for runtime client with ip %s: %+v", ip, wi)
|
||||||
}
|
}
|
||||||
|
|
||||||
// addHost adds a new IP-hostname pairing. The priorities of the sources are
|
// addHost adds a new IP-hostname pairing. The priorities of the sources are
|
||||||
@@ -644,26 +617,20 @@ func (clients *clientsContainer) addHostLocked(
|
|||||||
host string,
|
host string,
|
||||||
src client.Source,
|
src client.Source,
|
||||||
) (ok bool) {
|
) (ok bool) {
|
||||||
rc := clients.runtimeIndex.Client(ip)
|
rc := client.NewRuntime(ip)
|
||||||
if rc == nil {
|
rc.SetInfo(src, []string{host})
|
||||||
if src < client.SourceDHCP {
|
if dhcpHost := clients.dhcp.HostByIP(ip); dhcpHost != "" {
|
||||||
if clients.dhcp.HostByIP(ip) != "" {
|
rc.SetInfo(client.SourceDHCP, []string{dhcpHost})
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rc = client.NewRuntime(ip)
|
|
||||||
clients.runtimeIndex.Add(rc)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rc.SetInfo(src, []string{host})
|
clients.storage.UpdateRuntime(rc)
|
||||||
|
|
||||||
log.Debug(
|
log.Debug(
|
||||||
"clients: adding client info %s -> %q %q [%d]",
|
"clients: adding client info %s -> %q %q [%d]",
|
||||||
ip,
|
ip,
|
||||||
src,
|
src,
|
||||||
host,
|
host,
|
||||||
clients.runtimeIndex.Size(),
|
clients.storage.SizeRuntime(),
|
||||||
)
|
)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -675,23 +642,22 @@ func (clients *clientsContainer) addFromHostsFile(hosts *hostsfile.DefaultStorag
|
|||||||
clients.lock.Lock()
|
clients.lock.Lock()
|
||||||
defer clients.lock.Unlock()
|
defer clients.lock.Unlock()
|
||||||
|
|
||||||
deleted := clients.runtimeIndex.DeleteBySource(client.SourceHostsFile)
|
var rcs []*client.Runtime
|
||||||
log.Debug("clients: removed %d client aliases from system hosts file", deleted)
|
|
||||||
|
|
||||||
added := 0
|
|
||||||
hosts.RangeNames(func(addr netip.Addr, names []string) (cont bool) {
|
hosts.RangeNames(func(addr netip.Addr, names []string) (cont bool) {
|
||||||
// Only the first name of the first record is considered a canonical
|
// Only the first name of the first record is considered a canonical
|
||||||
// hostname for the IP address.
|
// hostname for the IP address.
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): Consider using all the names from all the records.
|
// TODO(e.burkov): Consider using all the names from all the records.
|
||||||
if clients.addHostLocked(addr, names[0], client.SourceHostsFile) {
|
rc := client.NewRuntime(addr)
|
||||||
added++
|
rc.SetInfo(client.SourceHostsFile, []string{names[0]})
|
||||||
}
|
|
||||||
|
rcs = append(rcs, rc)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Debug("clients: added %d client aliases from system hosts file", added)
|
added, removed := clients.storage.BatchUpdateBySource(client.SourceHostsFile, rcs)
|
||||||
|
log.Debug("clients: added %d, removed %d client aliases from system hosts file", added, removed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// addFromSystemARP adds the IP-hostname pairings from the output of the arp -a
|
// addFromSystemARP adds the IP-hostname pairings from the output of the arp -a
|
||||||
@@ -715,17 +681,16 @@ func (clients *clientsContainer) addFromSystemARP() {
|
|||||||
clients.lock.Lock()
|
clients.lock.Lock()
|
||||||
defer clients.lock.Unlock()
|
defer clients.lock.Unlock()
|
||||||
|
|
||||||
deleted := clients.runtimeIndex.DeleteBySource(client.SourceARP)
|
var rcs []*client.Runtime
|
||||||
log.Debug("clients: removed %d client aliases from arp neighborhood", deleted)
|
|
||||||
|
|
||||||
added := 0
|
|
||||||
for _, n := range ns {
|
for _, n := range ns {
|
||||||
if clients.addHostLocked(n.IP, n.Name, client.SourceARP) {
|
rc := client.NewRuntime(n.IP)
|
||||||
added++
|
rc.SetInfo(client.SourceARP, []string{n.Name})
|
||||||
}
|
|
||||||
|
rcs = append(rcs, rc)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("clients: added %d client aliases from arp neighborhood", added)
|
added, removed := clients.storage.BatchUpdateBySource(client.SourceARP, rcs)
|
||||||
|
log.Debug("clients: added %d, removed %d client aliases from arp neighborhood", added, removed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// close gracefully closes all the client-specific upstream configurations of
|
// close gracefully closes all the client-specific upstream configurations of
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ func TestClientsWHOIS(t *testing.T) {
|
|||||||
t.Run("new_client", func(t *testing.T) {
|
t.Run("new_client", func(t *testing.T) {
|
||||||
ip := netip.MustParseAddr("1.1.1.255")
|
ip := netip.MustParseAddr("1.1.1.255")
|
||||||
clients.setWHOISInfo(ip, whois)
|
clients.setWHOISInfo(ip, whois)
|
||||||
rc := clients.runtimeIndex.Client(ip)
|
rc := clients.storage.ClientRuntime(ip)
|
||||||
require.NotNil(t, rc)
|
require.NotNil(t, rc)
|
||||||
|
|
||||||
assert.Equal(t, whois, rc.WHOIS())
|
assert.Equal(t, whois, rc.WHOIS())
|
||||||
@@ -252,7 +252,7 @@ func TestClientsWHOIS(t *testing.T) {
|
|||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
|
|
||||||
clients.setWHOISInfo(ip, whois)
|
clients.setWHOISInfo(ip, whois)
|
||||||
rc := clients.runtimeIndex.Client(ip)
|
rc := clients.storage.ClientRuntime(ip)
|
||||||
require.NotNil(t, rc)
|
require.NotNil(t, rc)
|
||||||
|
|
||||||
assert.Equal(t, whois, rc.WHOIS())
|
assert.Equal(t, whois, rc.WHOIS())
|
||||||
@@ -269,7 +269,7 @@ func TestClientsWHOIS(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
clients.setWHOISInfo(ip, whois)
|
clients.setWHOISInfo(ip, whois)
|
||||||
rc := clients.runtimeIndex.Client(ip)
|
rc := clients.storage.ClientRuntime(ip)
|
||||||
require.Nil(t, rc)
|
require.Nil(t, rc)
|
||||||
|
|
||||||
assert.True(t, clients.storage.RemoveByName("client1"))
|
assert.True(t, clients.storage.RemoveByName("client1"))
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
clients.runtimeIndex.Range(func(rc *client.Runtime) (cont bool) {
|
clients.storage.RangeRuntime(func(rc *client.Runtime) (cont bool) {
|
||||||
src, host := rc.Info()
|
src, host := rc.Info()
|
||||||
cj := runtimeClientJSON{
|
cj := runtimeClientJSON{
|
||||||
WHOIS: whoisOrEmpty(rc),
|
WHOIS: whoisOrEmpty(rc),
|
||||||
@@ -248,6 +248,7 @@ func copySafeSearch(
|
|||||||
if conf.Enabled {
|
if conf.Enabled {
|
||||||
conf.Bing = true
|
conf.Bing = true
|
||||||
conf.DuckDuckGo = true
|
conf.DuckDuckGo = true
|
||||||
|
conf.Ecosia = true
|
||||||
conf.Google = true
|
conf.Google = true
|
||||||
conf.Pixabay = true
|
conf.Pixabay = true
|
||||||
conf.Yandex = true
|
conf.Yandex = true
|
||||||
|
|||||||
@@ -423,6 +423,7 @@ var config = &configuration{
|
|||||||
Enabled: false,
|
Enabled: false,
|
||||||
Bing: true,
|
Bing: true,
|
||||||
DuckDuckGo: true,
|
DuckDuckGo: true,
|
||||||
|
Ecosia: true,
|
||||||
Google: true,
|
Google: true,
|
||||||
Pixabay: true,
|
Pixabay: true,
|
||||||
Yandex: true,
|
Yandex: true,
|
||||||
|
|||||||
@@ -433,7 +433,7 @@ func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request
|
|||||||
// moment we'll allow setting up TLS in the initial configuration or the
|
// moment we'll allow setting up TLS in the initial configuration or the
|
||||||
// configuration itself will use HTTPS protocol, because the underlying
|
// configuration itself will use HTTPS protocol, because the underlying
|
||||||
// functions potentially restart the HTTPS server.
|
// functions potentially restart the HTTPS server.
|
||||||
err = startMods()
|
err = startMods(web.logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Context.firstRun = true
|
Context.firstRun = true
|
||||||
copyInstallSettings(config, curConfig)
|
copyInstallSettings(config, curConfig)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package home
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -19,6 +20,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/stats"
|
"github.com/AdguardTeam/AdGuardHome/internal/stats"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/ameshkov/dnscrypt/v2"
|
"github.com/ameshkov/dnscrypt/v2"
|
||||||
yaml "gopkg.in/yaml.v3"
|
yaml "gopkg.in/yaml.v3"
|
||||||
@@ -43,8 +45,8 @@ func onConfigModified() {
|
|||||||
|
|
||||||
// initDNS updates all the fields of the [Context] needed to initialize the DNS
|
// initDNS updates all the fields of the [Context] needed to initialize the DNS
|
||||||
// server and initializes it at last. It also must not be called unless
|
// server and initializes it at last. It also must not be called unless
|
||||||
// [config] and [Context] are initialized.
|
// [config] and [Context] are initialized. l must not be nil.
|
||||||
func initDNS() (err error) {
|
func initDNS(l *slog.Logger) (err error) {
|
||||||
anonymizer := config.anonymizer()
|
anonymizer := config.anonymizer()
|
||||||
|
|
||||||
statsDir, querylogDir, err := checkStatsAndQuerylogDirs(&Context, config)
|
statsDir, querylogDir, err := checkStatsAndQuerylogDirs(&Context, config)
|
||||||
@@ -53,6 +55,7 @@ func initDNS() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
statsConf := stats.Config{
|
statsConf := stats.Config{
|
||||||
|
Logger: l.With(slogutil.KeyPrefix, "stats"),
|
||||||
Filename: filepath.Join(statsDir, "stats.db"),
|
Filename: filepath.Join(statsDir, "stats.db"),
|
||||||
Limit: config.Stats.Interval.Duration,
|
Limit: config.Stats.Interval.Duration,
|
||||||
ConfigModified: onConfigModified,
|
ConfigModified: onConfigModified,
|
||||||
@@ -113,13 +116,16 @@ func initDNS() (err error) {
|
|||||||
anonymizer,
|
anonymizer,
|
||||||
httpRegister,
|
httpRegister,
|
||||||
tlsConf,
|
tlsConf,
|
||||||
|
l,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// initDNSServer initializes the [context.dnsServer]. To only use the internal
|
// initDNSServer initializes the [context.dnsServer]. To only use the internal
|
||||||
// proxy, none of the arguments are required, but tlsConf still must not be nil,
|
// proxy, none of the arguments are required, but tlsConf and l still must not
|
||||||
// in other cases all the arguments also must not be nil. It also must not be
|
// be nil, in other cases all the arguments also must not be nil. It also must
|
||||||
// called unless [config] and [Context] are initialized.
|
// not be called unless [config] and [Context] are initialized.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Use [dnsforward.DNSCreateParams] as a parameter.
|
||||||
func initDNSServer(
|
func initDNSServer(
|
||||||
filters *filtering.DNSFilter,
|
filters *filtering.DNSFilter,
|
||||||
sts stats.Interface,
|
sts stats.Interface,
|
||||||
@@ -128,8 +134,10 @@ func initDNSServer(
|
|||||||
anonymizer *aghnet.IPMut,
|
anonymizer *aghnet.IPMut,
|
||||||
httpReg aghhttp.RegisterFunc,
|
httpReg aghhttp.RegisterFunc,
|
||||||
tlsConf *tlsConfigSettings,
|
tlsConf *tlsConfigSettings,
|
||||||
|
l *slog.Logger,
|
||||||
) (err error) {
|
) (err error) {
|
||||||
Context.dnsServer, err = dnsforward.NewServer(dnsforward.DNSCreateParams{
|
Context.dnsServer, err = dnsforward.NewServer(dnsforward.DNSCreateParams{
|
||||||
|
Logger: l,
|
||||||
DNSFilter: filters,
|
DNSFilter: filters,
|
||||||
Stats: sts,
|
Stats: sts,
|
||||||
QueryLog: qlog,
|
QueryLog: qlog,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -38,6 +39,7 @@ import (
|
|||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/hostsfile"
|
"github.com/AdguardTeam/golibs/hostsfile"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/osutil"
|
"github.com/AdguardTeam/golibs/osutil"
|
||||||
)
|
)
|
||||||
@@ -90,6 +92,8 @@ func (c *homeContext) getDataDir() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Context - a global context object
|
// Context - a global context object
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Refactor.
|
||||||
var Context homeContext
|
var Context homeContext
|
||||||
|
|
||||||
// Main is the entry point
|
// Main is the entry point
|
||||||
@@ -273,7 +277,7 @@ func setupOpts(opts options) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// initContextClients initializes Context clients and related fields.
|
// initContextClients initializes Context clients and related fields.
|
||||||
func initContextClients() (err error) {
|
func initContextClients(logger *slog.Logger) (err error) {
|
||||||
err = setupDNSFilteringConf(config.Filtering)
|
err = setupDNSFilteringConf(config.Filtering)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Don't wrap the error, because it's informative enough as is.
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
@@ -297,7 +301,7 @@ func initContextClients() (err error) {
|
|||||||
|
|
||||||
var arpDB arpdb.Interface
|
var arpDB arpdb.Interface
|
||||||
if config.Clients.Sources.ARP {
|
if config.Clients.Sources.ARP {
|
||||||
arpDB = arpdb.New()
|
arpDB = arpdb.New(logger.With(slogutil.KeyError, "arpdb"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return Context.clients.Init(
|
return Context.clients.Init(
|
||||||
@@ -482,7 +486,12 @@ func checkPorts() (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func initWeb(opts options, clientBuildFS fs.FS, upd *updater.Updater) (web *webAPI, err error) {
|
func initWeb(
|
||||||
|
opts options,
|
||||||
|
clientBuildFS fs.FS,
|
||||||
|
upd *updater.Updater,
|
||||||
|
l *slog.Logger,
|
||||||
|
) (web *webAPI, err error) {
|
||||||
var clientFS fs.FS
|
var clientFS fs.FS
|
||||||
if opts.localFrontend {
|
if opts.localFrontend {
|
||||||
log.Info("warning: using local frontend files")
|
log.Info("warning: using local frontend files")
|
||||||
@@ -524,7 +533,7 @@ func initWeb(opts options, clientBuildFS fs.FS, upd *updater.Updater) (web *webA
|
|||||||
serveHTTP3: config.DNS.ServeHTTP3,
|
serveHTTP3: config.DNS.ServeHTTP3,
|
||||||
}
|
}
|
||||||
|
|
||||||
web = newWebAPI(webConf)
|
web = newWebAPI(webConf, l)
|
||||||
if web == nil {
|
if web == nil {
|
||||||
return nil, fmt.Errorf("initializing web: %w", err)
|
return nil, fmt.Errorf("initializing web: %w", err)
|
||||||
}
|
}
|
||||||
@@ -547,10 +556,15 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
|
|||||||
// Configure config filename.
|
// Configure config filename.
|
||||||
initConfigFilename(opts)
|
initConfigFilename(opts)
|
||||||
|
|
||||||
|
ls := getLogSettings(opts)
|
||||||
|
|
||||||
// Configure log level and output.
|
// Configure log level and output.
|
||||||
err = configureLogger(opts)
|
err = configureLogger(ls)
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
|
// TODO(a.garipov): Use slog everywhere.
|
||||||
|
slogLogger := newSlogLogger(ls)
|
||||||
|
|
||||||
// Print the first message after logger is configured.
|
// Print the first message after logger is configured.
|
||||||
log.Info(version.Full())
|
log.Info(version.Full())
|
||||||
log.Debug("current working directory is %s", Context.workDir)
|
log.Debug("current working directory is %s", Context.workDir)
|
||||||
@@ -569,7 +583,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
|
|||||||
// data first, but also to avoid relying on automatic Go init() function.
|
// data first, but also to avoid relying on automatic Go init() function.
|
||||||
filtering.InitModule()
|
filtering.InitModule()
|
||||||
|
|
||||||
err = initContextClients()
|
err = initContextClients(slogLogger)
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
err = setupOpts(opts)
|
err = setupOpts(opts)
|
||||||
@@ -604,7 +618,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
|
|||||||
|
|
||||||
// TODO(e.burkov): This could be made earlier, probably as the option's
|
// TODO(e.burkov): This could be made earlier, probably as the option's
|
||||||
// effect.
|
// effect.
|
||||||
cmdlineUpdate(opts, upd)
|
cmdlineUpdate(opts, upd, slogLogger)
|
||||||
|
|
||||||
if !Context.firstRun {
|
if !Context.firstRun {
|
||||||
// Save the updated config.
|
// Save the updated config.
|
||||||
@@ -632,11 +646,11 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
|
|||||||
onConfigModified()
|
onConfigModified()
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.web, err = initWeb(opts, clientBuildFS, upd)
|
Context.web, err = initWeb(opts, clientBuildFS, upd, slogLogger)
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
if !Context.firstRun {
|
if !Context.firstRun {
|
||||||
err = initDNS()
|
err = initDNS(slogLogger)
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
Context.tls.start()
|
Context.tls.start()
|
||||||
@@ -697,9 +711,10 @@ func (c *configuration) anonymizer() (ipmut *aghnet.IPMut) {
|
|||||||
return aghnet.NewIPMut(anonFunc)
|
return aghnet.NewIPMut(anonFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// startMods initializes and starts the DNS server after installation.
|
// startMods initializes and starts the DNS server after installation. l must
|
||||||
func startMods() (err error) {
|
// not be nil.
|
||||||
err = initDNS()
|
func startMods(l *slog.Logger) (err error) {
|
||||||
|
err = initDNS(l)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -959,8 +974,8 @@ type jsonError struct {
|
|||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdlineUpdate updates current application and exits.
|
// cmdlineUpdate updates current application and exits. l must not be nil.
|
||||||
func cmdlineUpdate(opts options, upd *updater.Updater) {
|
func cmdlineUpdate(opts options, upd *updater.Updater, l *slog.Logger) {
|
||||||
if !opts.performUpdate {
|
if !opts.performUpdate {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -970,7 +985,7 @@ func cmdlineUpdate(opts options, upd *updater.Updater) {
|
|||||||
//
|
//
|
||||||
// TODO(e.burkov): We could probably initialize the internal resolver
|
// TODO(e.burkov): We could probably initialize the internal resolver
|
||||||
// separately.
|
// separately.
|
||||||
err := initDNSServer(nil, nil, nil, nil, nil, nil, &tlsConfigSettings{})
|
err := initDNSServer(nil, nil, nil, nil, nil, nil, &tlsConfigSettings{}, l)
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
log.Info("cmdline update: performing update")
|
log.Info("cmdline update: performing update")
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ package home
|
|||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"gopkg.in/natefinch/lumberjack.v2"
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
@@ -16,10 +18,21 @@ import (
|
|||||||
// for logger output.
|
// for logger output.
|
||||||
const configSyslog = "syslog"
|
const configSyslog = "syslog"
|
||||||
|
|
||||||
// configureLogger configures logger level and output.
|
// newSlogLogger returns new [*slog.Logger] configured with the given settings.
|
||||||
func configureLogger(opts options) (err error) {
|
func newSlogLogger(ls *logSettings) (l *slog.Logger) {
|
||||||
ls := getLogSettings(opts)
|
if !ls.Enabled {
|
||||||
|
return slogutil.NewDiscardLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
return slogutil.New(&slogutil.Config{
|
||||||
|
Format: slogutil.FormatAdGuardLegacy,
|
||||||
|
AddTimestamp: true,
|
||||||
|
Verbose: ls.Verbose,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// configureLogger configures logger level and output.
|
||||||
|
func configureLogger(ls *logSettings) (err error) {
|
||||||
// Configure logger level.
|
// Configure logger level.
|
||||||
if !ls.Enabled {
|
if !ls.Enabled {
|
||||||
log.SetLevel(log.OFF)
|
log.SetLevel(log.OFF)
|
||||||
@@ -60,7 +73,7 @@ func configureLogger(opts options) (err error) {
|
|||||||
MaxAge: ls.MaxAge,
|
MaxAge: ls.MaxAge,
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// getLogSettings returns a log settings object properly initialized from opts.
|
// getLogSettings returns a log settings object properly initialized from opts.
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/ioutil"
|
"github.com/AdguardTeam/golibs/ioutil"
|
||||||
|
"github.com/c2h5oh/datasize"
|
||||||
)
|
)
|
||||||
|
|
||||||
// middlerware is a wrapper function signature.
|
// middlerware is a wrapper function signature.
|
||||||
type middleware func(http.Handler) http.Handler
|
type middleware func(http.Handler) http.Handler
|
||||||
|
|
||||||
// withMiddlewares consequently wraps h with all the middlewares.
|
// withMiddlewares consequently wraps h with all the middlewares.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Use [httputil.Wrap].
|
||||||
func withMiddlewares(h http.Handler, middlewares ...middleware) (wrapped http.Handler) {
|
func withMiddlewares(h http.Handler, middlewares ...middleware) (wrapped http.Handler) {
|
||||||
wrapped = h
|
wrapped = h
|
||||||
|
|
||||||
@@ -23,11 +26,11 @@ func withMiddlewares(h http.Handler, middlewares ...middleware) (wrapped http.Ha
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// defaultReqBodySzLim is the default maximum request body size.
|
// defaultReqBodySzLim is the default maximum request body size.
|
||||||
defaultReqBodySzLim = 64 * 1024
|
defaultReqBodySzLim datasize.ByteSize = 64 * datasize.KB
|
||||||
|
|
||||||
// largerReqBodySzLim is the maximum request body size for APIs expecting
|
// largerReqBodySzLim is the maximum request body size for APIs expecting
|
||||||
// larger requests.
|
// larger requests.
|
||||||
largerReqBodySzLim = 4 * 1024 * 1024
|
largerReqBodySzLim datasize.ByteSize = 4 * datasize.MB
|
||||||
)
|
)
|
||||||
|
|
||||||
// expectsLargerRequests shows if this request should use a larger body size
|
// expectsLargerRequests shows if this request should use a larger body size
|
||||||
@@ -38,26 +41,28 @@ const (
|
|||||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/2666 and
|
// See https://github.com/AdguardTeam/AdGuardHome/issues/2666 and
|
||||||
// https://github.com/AdguardTeam/AdGuardHome/issues/2675.
|
// https://github.com/AdguardTeam/AdGuardHome/issues/2675.
|
||||||
func expectsLargerRequests(r *http.Request) (ok bool) {
|
func expectsLargerRequests(r *http.Request) (ok bool) {
|
||||||
m := r.Method
|
if r.Method != http.MethodPost {
|
||||||
if m != http.MethodPost {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
p := r.URL.Path
|
switch r.URL.Path {
|
||||||
return p == "/control/access/set" ||
|
case "/control/access/set", "/control/filtering/set_rules":
|
||||||
p == "/control/filtering/set_rules"
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// limitRequestBody wraps underlying handler h, making it's request's body Read
|
// limitRequestBody wraps underlying handler h, making it's request's body Read
|
||||||
// method limited.
|
// method limited.
|
||||||
func limitRequestBody(h http.Handler) (limited http.Handler) {
|
func limitRequestBody(h http.Handler) (limited http.Handler) {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var szLim uint64 = defaultReqBodySzLim
|
szLim := defaultReqBodySzLim
|
||||||
if expectsLargerRequests(r) {
|
if expectsLargerRequests(r) {
|
||||||
szLim = largerReqBodySzLim
|
szLim = largerReqBodySzLim
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := ioutil.LimitReader(r.Body, szLim)
|
reader := ioutil.LimitReader(r.Body, szLim.Bytes())
|
||||||
|
|
||||||
// HTTP handlers aren't supposed to call r.Body.Close(), so just
|
// HTTP handlers aren't supposed to call r.Body.Close(), so just
|
||||||
// replace the body in a clone.
|
// replace the body in a clone.
|
||||||
|
|||||||
@@ -14,29 +14,29 @@ import (
|
|||||||
|
|
||||||
func TestLimitRequestBody(t *testing.T) {
|
func TestLimitRequestBody(t *testing.T) {
|
||||||
errReqLimitReached := &ioutil.LimitError{
|
errReqLimitReached := &ioutil.LimitError{
|
||||||
Limit: defaultReqBodySzLim,
|
Limit: defaultReqBodySzLim.Bytes(),
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
|
wantErr error
|
||||||
name string
|
name string
|
||||||
body string
|
body string
|
||||||
want []byte
|
want []byte
|
||||||
wantErr error
|
|
||||||
}{{
|
}{{
|
||||||
|
wantErr: nil,
|
||||||
name: "not_so_big",
|
name: "not_so_big",
|
||||||
body: "somestr",
|
body: "somestr",
|
||||||
want: []byte("somestr"),
|
want: []byte("somestr"),
|
||||||
wantErr: nil,
|
|
||||||
}, {
|
}, {
|
||||||
|
wantErr: errReqLimitReached,
|
||||||
name: "so_big",
|
name: "so_big",
|
||||||
body: string(make([]byte, defaultReqBodySzLim+1)),
|
body: string(make([]byte, defaultReqBodySzLim+1)),
|
||||||
want: make([]byte, defaultReqBodySzLim),
|
want: make([]byte, defaultReqBodySzLim),
|
||||||
wantErr: errReqLimitReached,
|
|
||||||
}, {
|
}, {
|
||||||
|
wantErr: nil,
|
||||||
name: "empty",
|
name: "empty",
|
||||||
body: "",
|
body: "",
|
||||||
want: []byte(nil),
|
want: []byte(nil),
|
||||||
wantErr: nil,
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
makeHandler := func(t *testing.T, err *error) http.HandlerFunc {
|
makeHandler := func(t *testing.T, err *error) http.HandlerFunc {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -16,7 +17,7 @@ import (
|
|||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/pprofutil"
|
"github.com/AdguardTeam/golibs/netutil/httputil"
|
||||||
"github.com/NYTimes/gziphandler"
|
"github.com/NYTimes/gziphandler"
|
||||||
"github.com/quic-go/quic-go/http3"
|
"github.com/quic-go/quic-go/http3"
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
@@ -90,17 +91,22 @@ type webAPI struct {
|
|||||||
// TODO(a.garipov): Refactor all these servers.
|
// TODO(a.garipov): Refactor all these servers.
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
|
|
||||||
|
// logger is a slog logger used in webAPI. It must not be nil.
|
||||||
|
logger *slog.Logger
|
||||||
|
|
||||||
// httpsServer is the server that handles HTTPS traffic. If it is not nil,
|
// httpsServer is the server that handles HTTPS traffic. If it is not nil,
|
||||||
// [Web.http3Server] must also not be nil.
|
// [Web.http3Server] must also not be nil.
|
||||||
httpsServer httpsServer
|
httpsServer httpsServer
|
||||||
}
|
}
|
||||||
|
|
||||||
// newWebAPI creates a new instance of the web UI and API server.
|
// newWebAPI creates a new instance of the web UI and API server. l must not be
|
||||||
func newWebAPI(conf *webConfig) (w *webAPI) {
|
// nil.
|
||||||
|
func newWebAPI(conf *webConfig, l *slog.Logger) (w *webAPI) {
|
||||||
log.Info("web: initializing")
|
log.Info("web: initializing")
|
||||||
|
|
||||||
w = &webAPI{
|
w = &webAPI{
|
||||||
conf: conf,
|
conf: conf,
|
||||||
|
logger: l,
|
||||||
}
|
}
|
||||||
|
|
||||||
clientFS := http.FileServer(http.FS(conf.clientFS))
|
clientFS := http.FileServer(http.FS(conf.clientFS))
|
||||||
@@ -327,7 +333,7 @@ func startPprof(port uint16) {
|
|||||||
runtime.SetMutexProfileFraction(1)
|
runtime.SetMutexProfileFraction(1)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
pprofutil.RoutePprof(mux)
|
httputil.RoutePprof(mux)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer log.OnPanic("pprof server")
|
defer log.OnPanic("pprof server")
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
package ipset
|
package ipset
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,24 +12,33 @@ import (
|
|||||||
// TODO(a.garipov): Perhaps generalize this into some kind of a NetFilter type,
|
// TODO(a.garipov): Perhaps generalize this into some kind of a NetFilter type,
|
||||||
// since ipset is exclusive to Linux?
|
// since ipset is exclusive to Linux?
|
||||||
type Manager interface {
|
type Manager interface {
|
||||||
Add(host string, ip4s, ip6s []net.IP) (n int, err error)
|
Add(ctx context.Context, host string, ip4s, ip6s []net.IP) (n int, err error)
|
||||||
Close() (err error)
|
Close() (err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManager returns a new ipset manager. IPv4 addresses are added to an
|
// Config is the configuration structure for the ipset manager.
|
||||||
// ipset with an ipv4 family; IPv6 addresses, to an ipv6 ipset. ipset must
|
type Config struct {
|
||||||
// exist.
|
// Logger is used for logging the operation of the ipset manager. It must
|
||||||
|
// not be nil.
|
||||||
|
Logger *slog.Logger
|
||||||
|
|
||||||
|
// Lines is the ipset configuration with the following syntax:
|
||||||
|
//
|
||||||
|
// DOMAIN[,DOMAIN].../IPSET_NAME[,IPSET_NAME]...
|
||||||
|
//
|
||||||
|
// Lines must not contain any blank lines or comments.
|
||||||
|
Lines []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager returns a new ipset manager. IPv4 addresses are added to an ipset
|
||||||
|
// with an ipv4 family; IPv6 addresses, to an ipv6 ipset. ipset must exist.
|
||||||
//
|
//
|
||||||
// The syntax of the ipsetConf is:
|
// If conf.Lines is empty, mgr and err are nil. The error's chain contains
|
||||||
//
|
|
||||||
// DOMAIN[,DOMAIN].../IPSET_NAME[,IPSET_NAME]...
|
|
||||||
//
|
|
||||||
// If ipsetConf is empty, msg and err are nil. The error's chain contains
|
|
||||||
// [errors.ErrUnsupported] if current OS is not supported.
|
// [errors.ErrUnsupported] if current OS is not supported.
|
||||||
func NewManager(ipsetConf []string) (mgr Manager, err error) {
|
func NewManager(ctx context.Context, conf *Config) (mgr Manager, err error) {
|
||||||
if len(ipsetConf) == 0 {
|
if len(conf.Lines) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return newManager(ipsetConf)
|
return newManager(ctx, conf)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,16 @@ package ipset
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/container"
|
"github.com/AdguardTeam/golibs/container"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/digineo/go-ipset/v2"
|
"github.com/digineo/go-ipset/v2"
|
||||||
"github.com/mdlayher/netlink"
|
"github.com/mdlayher/netlink"
|
||||||
"github.com/ti-mo/netfilter"
|
"github.com/ti-mo/netfilter"
|
||||||
@@ -34,8 +36,8 @@ import (
|
|||||||
// resolved IP addresses.
|
// resolved IP addresses.
|
||||||
|
|
||||||
// newManager returns a new Linux ipset manager.
|
// newManager returns a new Linux ipset manager.
|
||||||
func newManager(ipsetConf []string) (set Manager, err error) {
|
func newManager(ctx context.Context, conf *Config) (set Manager, err error) {
|
||||||
return newManagerWithDialer(ipsetConf, defaultDial)
|
return newManagerWithDialer(ctx, conf, defaultDial)
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultDial is the default netfilter dialing function.
|
// defaultDial is the default netfilter dialing function.
|
||||||
@@ -180,6 +182,8 @@ type manager struct {
|
|||||||
nameToIpset map[string]props
|
nameToIpset map[string]props
|
||||||
domainToIpsets map[string][]props
|
domainToIpsets map[string][]props
|
||||||
|
|
||||||
|
logger *slog.Logger
|
||||||
|
|
||||||
dial dialer
|
dial dialer
|
||||||
|
|
||||||
// mu protects all properties below.
|
// mu protects all properties below.
|
||||||
@@ -254,7 +258,7 @@ func parseIpsetConfigLine(confStr string) (hosts, ipsetNames []string, err error
|
|||||||
|
|
||||||
// parseIpsetConfig parses the ipset configuration and stores ipsets. It
|
// parseIpsetConfig parses the ipset configuration and stores ipsets. It
|
||||||
// returns an error if the configuration can't be used.
|
// returns an error if the configuration can't be used.
|
||||||
func (m *manager) parseIpsetConfig(ipsetConf []string) (err error) {
|
func (m *manager) parseIpsetConfig(ctx context.Context, ipsetConf []string) (err error) {
|
||||||
// The family doesn't seem to matter when we use a header query, so query
|
// The family doesn't seem to matter when we use a header query, so query
|
||||||
// only the IPv4 one.
|
// only the IPv4 one.
|
||||||
//
|
//
|
||||||
@@ -278,7 +282,7 @@ func (m *manager) parseIpsetConfig(ipsetConf []string) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ipsets []props
|
var ipsets []props
|
||||||
ipsets, err = m.ipsets(ipsetNames, currentlyKnown)
|
ipsets, err = m.ipsets(ctx, ipsetNames, currentlyKnown)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getting ipsets from config line at idx %d: %w", i, err)
|
return fmt.Errorf("getting ipsets from config line at idx %d: %w", i, err)
|
||||||
}
|
}
|
||||||
@@ -328,7 +332,11 @@ func (m *manager) ipsetProps(name string) (p props, err error) {
|
|||||||
|
|
||||||
// ipsets returns ipset properties of currently known ipsets. It also makes an
|
// ipsets returns ipset properties of currently known ipsets. It also makes an
|
||||||
// additional ipset header data query if needed.
|
// additional ipset header data query if needed.
|
||||||
func (m *manager) ipsets(names []string, currentlyKnown map[string]props) (sets []props, err error) {
|
func (m *manager) ipsets(
|
||||||
|
ctx context.Context,
|
||||||
|
names []string,
|
||||||
|
currentlyKnown map[string]props,
|
||||||
|
) (sets []props, err error) {
|
||||||
for _, n := range names {
|
for _, n := range names {
|
||||||
p, ok := currentlyKnown[n]
|
p, ok := currentlyKnown[n]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -336,10 +344,12 @@ func (m *manager) ipsets(names []string, currentlyKnown map[string]props) (sets
|
|||||||
}
|
}
|
||||||
|
|
||||||
if p.family != netfilter.ProtoIPv4 && p.family != netfilter.ProtoIPv6 {
|
if p.family != netfilter.ProtoIPv4 && p.family != netfilter.ProtoIPv6 {
|
||||||
log.Debug("ipset: getting properties: %q %q unexpected ipset family %q",
|
m.logger.DebugContext(
|
||||||
p.name,
|
ctx,
|
||||||
p.typeName,
|
"got unexpected ipset family while getting set properties",
|
||||||
p.family,
|
"set_name", p.name,
|
||||||
|
"set_type", p.typeName,
|
||||||
|
"set_family", p.family,
|
||||||
)
|
)
|
||||||
|
|
||||||
p, err = m.ipsetProps(n)
|
p, err = m.ipsetProps(n)
|
||||||
@@ -357,7 +367,7 @@ func (m *manager) ipsets(names []string, currentlyKnown map[string]props) (sets
|
|||||||
|
|
||||||
// newManagerWithDialer returns a new Linux ipset manager using the provided
|
// newManagerWithDialer returns a new Linux ipset manager using the provided
|
||||||
// dialer.
|
// dialer.
|
||||||
func newManagerWithDialer(ipsetConf []string, dial dialer) (mgr Manager, err error) {
|
func newManagerWithDialer(ctx context.Context, conf *Config, dial dialer) (mgr Manager, err error) {
|
||||||
defer func() { err = errors.Annotate(err, "ipset: %w") }()
|
defer func() { err = errors.Annotate(err, "ipset: %w") }()
|
||||||
|
|
||||||
m := &manager{
|
m := &manager{
|
||||||
@@ -366,6 +376,8 @@ func newManagerWithDialer(ipsetConf []string, dial dialer) (mgr Manager, err err
|
|||||||
nameToIpset: make(map[string]props),
|
nameToIpset: make(map[string]props),
|
||||||
domainToIpsets: make(map[string][]props),
|
domainToIpsets: make(map[string][]props),
|
||||||
|
|
||||||
|
logger: conf.Logger,
|
||||||
|
|
||||||
dial: dial,
|
dial: dial,
|
||||||
|
|
||||||
addedIPs: container.NewMapSet[ipInIpsetEntry](),
|
addedIPs: container.NewMapSet[ipInIpsetEntry](),
|
||||||
@@ -376,7 +388,7 @@ func newManagerWithDialer(ipsetConf []string, dial dialer) (mgr Manager, err err
|
|||||||
if errors.Is(err, unix.EPROTONOSUPPORT) {
|
if errors.Is(err, unix.EPROTONOSUPPORT) {
|
||||||
// The implementation doesn't support this protocol version. Just
|
// The implementation doesn't support this protocol version. Just
|
||||||
// issue a warning.
|
// issue a warning.
|
||||||
log.Info("ipset: dialing netfilter: warning: %s", err)
|
m.logger.WarnContext(ctx, "dialing netfilter", slogutil.KeyError, err)
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -384,12 +396,12 @@ func newManagerWithDialer(ipsetConf []string, dial dialer) (mgr Manager, err err
|
|||||||
return nil, fmt.Errorf("dialing netfilter: %w", err)
|
return nil, fmt.Errorf("dialing netfilter: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.parseIpsetConfig(ipsetConf)
|
err = m.parseIpsetConfig(ctx, conf.Lines)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("getting ipsets: %w", err)
|
return nil, fmt.Errorf("getting ipsets: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("ipset: initialized")
|
m.logger.DebugContext(ctx, "initialized")
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
@@ -476,6 +488,7 @@ func (m *manager) addIPs(host string, set props, ips []net.IP) (n int, err error
|
|||||||
|
|
||||||
// addToSets adds the IP addresses to the corresponding ipset.
|
// addToSets adds the IP addresses to the corresponding ipset.
|
||||||
func (m *manager) addToSets(
|
func (m *manager) addToSets(
|
||||||
|
ctx context.Context,
|
||||||
host string,
|
host string,
|
||||||
ip4s []net.IP,
|
ip4s []net.IP,
|
||||||
ip6s []net.IP,
|
ip6s []net.IP,
|
||||||
@@ -498,7 +511,13 @@ func (m *manager) addToSets(
|
|||||||
return n, fmt.Errorf("%q %q unexpected family %q", set.name, set.typeName, set.family)
|
return n, fmt.Errorf("%q %q unexpected family %q", set.name, set.typeName, set.family)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("ipset: added %d ips to set %q %q", nn, set.name, set.typeName)
|
m.logger.DebugContext(
|
||||||
|
ctx,
|
||||||
|
"added ips to set",
|
||||||
|
"ips_num", nn,
|
||||||
|
"set_name", set.name,
|
||||||
|
"set_type", set.typeName,
|
||||||
|
)
|
||||||
|
|
||||||
n += nn
|
n += nn
|
||||||
}
|
}
|
||||||
@@ -507,7 +526,7 @@ func (m *manager) addToSets(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add implements the [Manager] interface for *manager.
|
// Add implements the [Manager] interface for *manager.
|
||||||
func (m *manager) Add(host string, ip4s, ip6s []net.IP) (n int, err error) {
|
func (m *manager) Add(ctx context.Context, host string, ip4s, ip6s []net.IP) (n int, err error) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
@@ -516,9 +535,9 @@ func (m *manager) Add(host string, ip4s, ip6s []net.IP) (n int, err error) {
|
|||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("ipset: found %d sets", len(sets))
|
m.logger.DebugContext(ctx, "found sets", "set_num", len(sets))
|
||||||
|
|
||||||
return m.addToSets(host, ip4s, ip6s, sets)
|
return m.addToSets(ctx, host, ip4s, ip6s, sets)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close implements the [Manager] interface for *manager.
|
// Close implements the [Manager] interface for *manager.
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/digineo/go-ipset/v2"
|
"github.com/digineo/go-ipset/v2"
|
||||||
"github.com/mdlayher/netlink"
|
"github.com/mdlayher/netlink"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -15,6 +18,9 @@ import (
|
|||||||
"github.com/ti-mo/netfilter"
|
"github.com/ti-mo/netfilter"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// testTimeout is a common timeout for tests and contexts.
|
||||||
|
const testTimeout = 1 * time.Second
|
||||||
|
|
||||||
// fakeConn is a fake ipsetConn for tests.
|
// fakeConn is a fake ipsetConn for tests.
|
||||||
type fakeConn struct {
|
type fakeConn struct {
|
||||||
ipv4Header *ipset.HeaderPolicy
|
ipv4Header *ipset.HeaderPolicy
|
||||||
@@ -58,7 +64,7 @@ func (c *fakeConn) listAll() (sets []props, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestManager_Add(t *testing.T) {
|
func TestManager_Add(t *testing.T) {
|
||||||
ipsetConf := []string{
|
ipsetList := []string{
|
||||||
"example.com,example.net/ipv4set",
|
"example.com,example.net/ipv4set",
|
||||||
"example.org,example.biz/ipv6set",
|
"example.org,example.biz/ipv6set",
|
||||||
}
|
}
|
||||||
@@ -89,7 +95,11 @@ func TestManager_Add(t *testing.T) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := newManagerWithDialer(ipsetConf, fakeDial)
|
conf := &Config{
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
|
Lines: ipsetList,
|
||||||
|
}
|
||||||
|
m, err := newManagerWithDialer(testutil.ContextWithTimeout(t, testTimeout), conf, fakeDial)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ip4 := net.IP{1, 2, 3, 4}
|
ip4 := net.IP{1, 2, 3, 4}
|
||||||
@@ -100,7 +110,7 @@ func TestManager_Add(t *testing.T) {
|
|||||||
0x00, 0x00, 0x56, 0x78,
|
0x00, 0x00, 0x56, 0x78,
|
||||||
}
|
}
|
||||||
|
|
||||||
n, err := m.Add("example.net", []net.IP{ip4}, nil)
|
n, err := m.Add(testutil.ContextWithTimeout(t, testTimeout), "example.net", []net.IP{ip4}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, 1, n)
|
assert.Equal(t, 1, n)
|
||||||
@@ -110,7 +120,7 @@ func TestManager_Add(t *testing.T) {
|
|||||||
gotIP4 := ipv4Entries[0].IP.Value
|
gotIP4 := ipv4Entries[0].IP.Value
|
||||||
assert.Equal(t, ip4, gotIP4)
|
assert.Equal(t, ip4, gotIP4)
|
||||||
|
|
||||||
n, err = m.Add("example.biz", nil, []net.IP{ip6})
|
n, err = m.Add(testutil.ContextWithTimeout(t, testTimeout), "example.biz", nil, []net.IP{ip6})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, 1, n)
|
assert.Equal(t, 1, n)
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
package ipset
|
package ipset
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newManager(_ []string) (mgr Manager, err error) {
|
func newManager(_ context.Context, _ *Config) (mgr Manager, err error) {
|
||||||
return nil, aghos.Unsupported("ipset")
|
return nil, aghos.Unsupported("ipset")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import (
|
|||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/mathutil"
|
"github.com/AdguardTeam/golibs/mathutil"
|
||||||
"github.com/AdguardTeam/golibs/pprofutil"
|
"github.com/AdguardTeam/golibs/netutil/httputil"
|
||||||
httptreemux "github.com/dimfeld/httptreemux/v5"
|
httptreemux "github.com/dimfeld/httptreemux/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ func (svc *Service) setupPprof(c *PprofConfig) {
|
|||||||
runtime.SetMutexProfileFraction(1)
|
runtime.SetMutexProfileFraction(1)
|
||||||
|
|
||||||
pprofMux := http.NewServeMux()
|
pprofMux := http.NewServeMux()
|
||||||
pprofutil.RoutePprof(pprofMux)
|
httputil.RoutePprof(pprofMux)
|
||||||
|
|
||||||
svc.pprofPort = c.Port
|
svc.pprofPort = c.Port
|
||||||
addr := netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), c.Port)
|
addr := netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), c.Port)
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
|
"github.com/AdguardTeam/golibs/container"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
@@ -32,7 +32,7 @@ type queryLog struct {
|
|||||||
|
|
||||||
// buffer contains recent log entries. The entries in this buffer must not
|
// buffer contains recent log entries. The entries in this buffer must not
|
||||||
// be modified.
|
// be modified.
|
||||||
buffer *aghalg.RingBuffer[*logEntry]
|
buffer *container.RingBuffer[*logEntry]
|
||||||
|
|
||||||
// logFile is the path to the log file.
|
// logFile is the path to the log file.
|
||||||
logFile string
|
logFile string
|
||||||
@@ -225,7 +225,7 @@ func (l *queryLog) Add(params *AddParams) {
|
|||||||
l.bufferLock.Lock()
|
l.bufferLock.Lock()
|
||||||
defer l.bufferLock.Unlock()
|
defer l.bufferLock.Unlock()
|
||||||
|
|
||||||
l.buffer.Append(entry)
|
l.buffer.Push(entry)
|
||||||
|
|
||||||
if !l.flushPending && fileIsEnabled && l.buffer.Len() >= memSize {
|
if !l.flushPending && fileIsEnabled && l.buffer.Len() >= memSize {
|
||||||
l.flushPending = true
|
l.flushPending = true
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
|
"github.com/AdguardTeam/golibs/container"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
@@ -153,7 +153,7 @@ func newQueryLog(conf Config) (l *queryLog, err error) {
|
|||||||
l = &queryLog{
|
l = &queryLog{
|
||||||
findClient: findClient,
|
findClient: findClient,
|
||||||
|
|
||||||
buffer: aghalg.NewRingBuffer[*logEntry](memSize),
|
buffer: container.NewRingBuffer[*logEntry](memSize),
|
||||||
|
|
||||||
conf: &Config{},
|
conf: &Config{},
|
||||||
confMu: &sync.RWMutex{},
|
confMu: &sync.RWMutex{},
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
package rdns
|
package rdns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/bluele/gcache"
|
"github.com/bluele/gcache"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,7 +16,7 @@ import (
|
|||||||
type Interface interface {
|
type Interface interface {
|
||||||
// Process makes rDNS request and returns domain name. changed indicates
|
// Process makes rDNS request and returns domain name. changed indicates
|
||||||
// that domain name was updated since last request.
|
// that domain name was updated since last request.
|
||||||
Process(ip netip.Addr) (host string, changed bool)
|
Process(ctx context.Context, ip netip.Addr) (host string, changed bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty is an empty [Interface] implementation which does nothing.
|
// Empty is an empty [Interface] implementation which does nothing.
|
||||||
@@ -24,7 +26,7 @@ type Empty struct{}
|
|||||||
var _ Interface = (*Empty)(nil)
|
var _ Interface = (*Empty)(nil)
|
||||||
|
|
||||||
// Process implements the [Interface] interface for Empty.
|
// Process implements the [Interface] interface for Empty.
|
||||||
func (Empty) Process(_ netip.Addr) (host string, changed bool) {
|
func (Empty) Process(_ context.Context, _ netip.Addr) (host string, changed bool) {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +39,10 @@ type Exchanger interface {
|
|||||||
|
|
||||||
// Config is the configuration structure for Default.
|
// Config is the configuration structure for Default.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
// Logger is used for logging the operation of the reverse DNS lookup
|
||||||
|
// queries. It must not be nil.
|
||||||
|
Logger *slog.Logger
|
||||||
|
|
||||||
// Exchanger resolves IP addresses to domain names.
|
// Exchanger resolves IP addresses to domain names.
|
||||||
Exchanger Exchanger
|
Exchanger Exchanger
|
||||||
|
|
||||||
@@ -50,6 +56,10 @@ type Config struct {
|
|||||||
|
|
||||||
// Default is the default rDNS query processor.
|
// Default is the default rDNS query processor.
|
||||||
type Default struct {
|
type Default struct {
|
||||||
|
// logger is used for logging the operation of the reverse DNS lookup
|
||||||
|
// queries. It must not be nil.
|
||||||
|
logger *slog.Logger
|
||||||
|
|
||||||
// cache is the cache containing IP addresses of clients. An active IP
|
// cache is the cache containing IP addresses of clients. An active IP
|
||||||
// address is resolved once again after it expires. If IP address couldn't
|
// address is resolved once again after it expires. If IP address couldn't
|
||||||
// be resolved, it stays here for some time to prevent further attempts to
|
// be resolved, it stays here for some time to prevent further attempts to
|
||||||
@@ -66,6 +76,7 @@ type Default struct {
|
|||||||
// New returns a new default rDNS query processor. conf must not be nil.
|
// New returns a new default rDNS query processor. conf must not be nil.
|
||||||
func New(conf *Config) (r *Default) {
|
func New(conf *Config) (r *Default) {
|
||||||
return &Default{
|
return &Default{
|
||||||
|
logger: conf.Logger,
|
||||||
cache: gcache.New(conf.CacheSize).LRU().Build(),
|
cache: gcache.New(conf.CacheSize).LRU().Build(),
|
||||||
exchanger: conf.Exchanger,
|
exchanger: conf.Exchanger,
|
||||||
cacheTTL: conf.CacheTTL,
|
cacheTTL: conf.CacheTTL,
|
||||||
@@ -76,15 +87,15 @@ func New(conf *Config) (r *Default) {
|
|||||||
var _ Interface = (*Default)(nil)
|
var _ Interface = (*Default)(nil)
|
||||||
|
|
||||||
// Process implements the [Interface] interface for Default.
|
// Process implements the [Interface] interface for Default.
|
||||||
func (r *Default) Process(ip netip.Addr) (host string, changed bool) {
|
func (r *Default) Process(ctx context.Context, ip netip.Addr) (host string, changed bool) {
|
||||||
fromCache, expired := r.findInCache(ip)
|
fromCache, expired := r.findInCache(ctx, ip)
|
||||||
if !expired {
|
if !expired {
|
||||||
return fromCache, false
|
return fromCache, false
|
||||||
}
|
}
|
||||||
|
|
||||||
host, ttl, err := r.exchanger.Exchange(ip)
|
host, ttl, err := r.exchanger.Exchange(ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("rdns: resolving %q: %s", ip, err)
|
r.logger.DebugContext(ctx, "resolving", "ip", ip, slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ttl = max(ttl, r.cacheTTL)
|
ttl = max(ttl, r.cacheTTL)
|
||||||
@@ -96,7 +107,7 @@ func (r *Default) Process(ip netip.Addr) (host string, changed bool) {
|
|||||||
|
|
||||||
err = r.cache.Set(ip, item)
|
err = r.cache.Set(ip, item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("rdns: cache: adding item %q: %s", ip, err)
|
r.logger.DebugContext(ctx, "adding item to cache", "key", ip, slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(e.burkov): The name doesn't change if it's neither stored in cache
|
// TODO(e.burkov): The name doesn't change if it's neither stored in cache
|
||||||
@@ -106,22 +117,22 @@ func (r *Default) Process(ip netip.Addr) (host string, changed bool) {
|
|||||||
|
|
||||||
// findInCache finds domain name in the cache. expired is true if host is not
|
// findInCache finds domain name in the cache. expired is true if host is not
|
||||||
// valid anymore.
|
// valid anymore.
|
||||||
func (r *Default) findInCache(ip netip.Addr) (host string, expired bool) {
|
func (r *Default) findInCache(ctx context.Context, ip netip.Addr) (host string, expired bool) {
|
||||||
val, err := r.cache.Get(ip)
|
val, err := r.cache.Get(ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, gcache.KeyNotFoundError) {
|
if !errors.Is(err, gcache.KeyNotFoundError) {
|
||||||
log.Debug("rdns: cache: retrieving %q: %s", ip, err)
|
r.logger.DebugContext(
|
||||||
|
ctx,
|
||||||
|
"retrieving item from cache",
|
||||||
|
"key", ip,
|
||||||
|
slogutil.KeyError, err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", true
|
return "", true
|
||||||
}
|
}
|
||||||
|
|
||||||
item, ok := val.(*cacheItem)
|
item := val.(*cacheItem)
|
||||||
if !ok {
|
|
||||||
log.Debug("rdns: cache: %q bad type %T", ip, val)
|
|
||||||
|
|
||||||
return "", true
|
|
||||||
}
|
|
||||||
|
|
||||||
return item.host, time.Now().After(item.expiry)
|
return item.host, time.Now().After(item.expiry)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,14 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/rdns"
|
"github.com/AdguardTeam/AdGuardHome/internal/rdns"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// testTimeout is a common timeout for tests and contexts.
|
||||||
|
const testTimeout = 1 * time.Second
|
||||||
|
|
||||||
func TestDefault_Process(t *testing.T) {
|
func TestDefault_Process(t *testing.T) {
|
||||||
ip1 := netip.MustParseAddr("1.2.3.4")
|
ip1 := netip.MustParseAddr("1.2.3.4")
|
||||||
revAddr1, err := netutil.IPToReversedAddr(ip1.AsSlice())
|
revAddr1, err := netutil.IPToReversedAddr(ip1.AsSlice())
|
||||||
@@ -71,14 +75,14 @@ func TestDefault_Process(t *testing.T) {
|
|||||||
Exchanger: &aghtest.Exchanger{OnExchange: onExchange},
|
Exchanger: &aghtest.Exchanger{OnExchange: onExchange},
|
||||||
})
|
})
|
||||||
|
|
||||||
got, changed := r.Process(tc.addr)
|
got, changed := r.Process(testutil.ContextWithTimeout(t, testTimeout), tc.addr)
|
||||||
require.True(t, changed)
|
require.True(t, changed)
|
||||||
|
|
||||||
assert.Equal(t, tc.want, got)
|
assert.Equal(t, tc.want, got)
|
||||||
assert.Equal(t, 1, hit)
|
assert.Equal(t, 1, hit)
|
||||||
|
|
||||||
// From cache.
|
// From cache.
|
||||||
got, changed = r.Process(tc.addr)
|
got, changed = r.Process(testutil.ContextWithTimeout(t, testTimeout), tc.addr)
|
||||||
require.False(t, changed)
|
require.False(t, changed)
|
||||||
|
|
||||||
assert.Equal(t, tc.want, got)
|
assert.Equal(t, tc.want, got)
|
||||||
@@ -101,7 +105,7 @@ func TestDefault_Process(t *testing.T) {
|
|||||||
Exchanger: zeroTTLExchanger,
|
Exchanger: zeroTTLExchanger,
|
||||||
})
|
})
|
||||||
|
|
||||||
got, changed := r.Process(ip1)
|
got, changed := r.Process(testutil.ContextWithTimeout(t, testTimeout), ip1)
|
||||||
require.True(t, changed)
|
require.True(t, changed)
|
||||||
assert.Equal(t, revAddr1, got)
|
assert.Equal(t, revAddr1, got)
|
||||||
|
|
||||||
@@ -109,14 +113,15 @@ func TestDefault_Process(t *testing.T) {
|
|||||||
return revAddr2, time.Hour, nil
|
return revAddr2, time.Hour, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||||
got, changed = r.Process(ip1)
|
got, changed = r.Process(ctx, ip1)
|
||||||
assert.True(t, changed)
|
assert.True(t, changed)
|
||||||
assert.Equal(t, revAddr2, got)
|
assert.Equal(t, revAddr2, got)
|
||||||
}, 2*cacheTTL, time.Millisecond*100)
|
}, 2*cacheTTL, time.Millisecond*100)
|
||||||
|
|
||||||
assert.Never(t, func() (changed bool) {
|
assert.Never(t, func() (changed bool) {
|
||||||
_, changed = r.Process(ip1)
|
_, changed = r.Process(testutil.ContextWithTimeout(t, testTimeout), ip1)
|
||||||
|
|
||||||
return changed
|
return changed
|
||||||
}, 2*cacheTTL, time.Millisecond*100)
|
}, 2*cacheTTL, time.Millisecond*100)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,6 +50,8 @@ type StatsResp struct {
|
|||||||
func (s *StatsCtx) handleStats(w http.ResponseWriter, r *http.Request) {
|
func (s *StatsCtx) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
resp *StatsResp
|
resp *StatsResp
|
||||||
ok bool
|
ok bool
|
||||||
@@ -62,12 +63,17 @@ func (s *StatsCtx) handleStats(w http.ResponseWriter, r *http.Request) {
|
|||||||
resp, ok = s.getData(uint32(s.limit.Hours()))
|
resp, ok = s.getData(uint32(s.limit.Hours()))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
log.Debug("stats: prepared data in %v", time.Since(start))
|
s.logger.DebugContext(
|
||||||
|
ctx,
|
||||||
|
"prepared data",
|
||||||
|
"elapsed", timeutil.Duration{Duration: time.Since(start)},
|
||||||
|
)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
// Don't bring the message to the lower case since it's a part of UI
|
// Don't bring the message to the lower case since it's a part of UI
|
||||||
// text for the moment.
|
// text for the moment.
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "Couldn't get statistics data")
|
const msg = "Couldn't get statistics data"
|
||||||
|
aghhttp.ErrorAndLog(ctx, s.logger, r, w, http.StatusInternalServerError, msg)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -146,16 +152,18 @@ func (s *StatsCtx) handleGetStatsConfig(w http.ResponseWriter, r *http.Request)
|
|||||||
//
|
//
|
||||||
// Deprecated: Remove it when migration to the new API is over.
|
// Deprecated: Remove it when migration to the new API is over.
|
||||||
func (s *StatsCtx) handleStatsConfig(w http.ResponseWriter, r *http.Request) {
|
func (s *StatsCtx) handleStatsConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
reqData := configResp{}
|
reqData := configResp{}
|
||||||
err := json.NewDecoder(r.Body).Decode(&reqData)
|
err := json.NewDecoder(r.Body).Decode(&reqData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "json decode: %s", err)
|
aghhttp.ErrorAndLog(ctx, s.logger, r, w, http.StatusBadRequest, "json decode: %s", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !checkInterval(reqData.IntervalDays) {
|
if !checkInterval(reqData.IntervalDays) {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "Unsupported interval")
|
aghhttp.ErrorAndLog(ctx, s.logger, r, w, http.StatusBadRequest, "Unsupported interval")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -173,17 +181,19 @@ func (s *StatsCtx) handleStatsConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
// handlePutStatsConfig is the handler for the PUT /control/stats/config/update
|
// handlePutStatsConfig is the handler for the PUT /control/stats/config/update
|
||||||
// HTTP API.
|
// HTTP API.
|
||||||
func (s *StatsCtx) handlePutStatsConfig(w http.ResponseWriter, r *http.Request) {
|
func (s *StatsCtx) handlePutStatsConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
reqData := getConfigResp{}
|
reqData := getConfigResp{}
|
||||||
err := json.NewDecoder(r.Body).Decode(&reqData)
|
err := json.NewDecoder(r.Body).Decode(&reqData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "json decode: %s", err)
|
aghhttp.ErrorAndLog(ctx, s.logger, r, w, http.StatusBadRequest, "json decode: %s", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
engine, err := aghnet.NewIgnoreEngine(reqData.Ignored)
|
engine, err := aghnet.NewIgnoreEngine(reqData.Ignored)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusUnprocessableEntity, "ignored: %s", err)
|
aghhttp.ErrorAndLog(ctx, s.logger, r, w, http.StatusUnprocessableEntity, "ignored: %s", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -191,13 +201,21 @@ func (s *StatsCtx) handlePutStatsConfig(w http.ResponseWriter, r *http.Request)
|
|||||||
ivl := time.Duration(reqData.Interval) * time.Millisecond
|
ivl := time.Duration(reqData.Interval) * time.Millisecond
|
||||||
err = validateIvl(ivl)
|
err = validateIvl(ivl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusUnprocessableEntity, "unsupported interval: %s", err)
|
aghhttp.ErrorAndLog(
|
||||||
|
ctx,
|
||||||
|
s.logger,
|
||||||
|
r,
|
||||||
|
w,
|
||||||
|
http.StatusUnprocessableEntity,
|
||||||
|
"unsupported interval: %s",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if reqData.Enabled == aghalg.NBNull {
|
if reqData.Enabled == aghalg.NBNull {
|
||||||
aghhttp.Error(r, w, http.StatusUnprocessableEntity, "enabled is null")
|
aghhttp.ErrorAndLog(ctx, s.logger, r, w, http.StatusUnprocessableEntity, "enabled is null")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -216,7 +234,15 @@ func (s *StatsCtx) handlePutStatsConfig(w http.ResponseWriter, r *http.Request)
|
|||||||
func (s *StatsCtx) handleStatsReset(w http.ResponseWriter, r *http.Request) {
|
func (s *StatsCtx) handleStatsReset(w http.ResponseWriter, r *http.Request) {
|
||||||
err := s.clear()
|
err := s.clear()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "stats: %s", err)
|
aghhttp.ErrorAndLog(
|
||||||
|
r.Context(),
|
||||||
|
s.logger,
|
||||||
|
r,
|
||||||
|
w,
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
"stats: %s",
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -24,6 +25,7 @@ func TestHandleStatsConfig(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
conf := Config{
|
conf := Config{
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
UnitID: func() (id uint32) { return 0 },
|
UnitID: func() (id uint32) { return 0 },
|
||||||
ConfigModified: func() {},
|
ConfigModified: func() {},
|
||||||
ShouldCountClient: func([]string) bool { return true },
|
ShouldCountClient: func([]string) bool { return true },
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
package stats
|
package stats
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -14,7 +16,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
"go.etcd.io/bbolt"
|
"go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
@@ -43,6 +45,10 @@ func validateIvl(ivl time.Duration) (err error) {
|
|||||||
//
|
//
|
||||||
// Do not alter any fields of this structure after using it.
|
// Do not alter any fields of this structure after using it.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
// Logger is used for logging the operation of the statistics management.
|
||||||
|
// It must not be nil.
|
||||||
|
Logger *slog.Logger
|
||||||
|
|
||||||
// UnitID is the function to generate the identifier for current unit. If
|
// UnitID is the function to generate the identifier for current unit. If
|
||||||
// nil, the default function is used, see newUnitID.
|
// nil, the default function is used, see newUnitID.
|
||||||
UnitID UnitIDGenFunc
|
UnitID UnitIDGenFunc
|
||||||
@@ -96,6 +102,10 @@ type Interface interface {
|
|||||||
// StatsCtx collects the statistics and flushes it to the database. Its default
|
// StatsCtx collects the statistics and flushes it to the database. Its default
|
||||||
// flushing interval is one hour.
|
// flushing interval is one hour.
|
||||||
type StatsCtx struct {
|
type StatsCtx struct {
|
||||||
|
// logger is used for logging the operation of the statistics management.
|
||||||
|
// It must not be nil.
|
||||||
|
logger *slog.Logger
|
||||||
|
|
||||||
// currMu protects curr.
|
// currMu protects curr.
|
||||||
currMu *sync.RWMutex
|
currMu *sync.RWMutex
|
||||||
// curr is the actual statistics collection result.
|
// curr is the actual statistics collection result.
|
||||||
@@ -150,6 +160,7 @@ func New(conf Config) (s *StatsCtx, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s = &StatsCtx{
|
s = &StatsCtx{
|
||||||
|
logger: conf.Logger,
|
||||||
currMu: &sync.RWMutex{},
|
currMu: &sync.RWMutex{},
|
||||||
httpRegister: conf.HTTPRegister,
|
httpRegister: conf.HTTPRegister,
|
||||||
configModified: conf.ConfigModified,
|
configModified: conf.ConfigModified,
|
||||||
@@ -178,21 +189,21 @@ func New(conf Config) (s *StatsCtx, err error) {
|
|||||||
|
|
||||||
tx, err := s.db.Load().Begin(true)
|
tx, err := s.db.Load().Begin(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("stats: opening a transaction: %w", err)
|
return nil, fmt.Errorf("opening a transaction: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleted := deleteOldUnits(tx, id-uint32(s.limit.Hours())-1)
|
deleted := s.deleteOldUnits(tx, id-uint32(s.limit.Hours())-1)
|
||||||
udb = loadUnitFromDB(tx, id)
|
udb = s.loadUnitFromDB(tx, id)
|
||||||
|
|
||||||
err = finishTxn(tx, deleted > 0)
|
err = finishTxn(tx, deleted > 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("stats: %s", err)
|
s.logger.Error("finishing transacation", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.curr = newUnit(id)
|
s.curr = newUnit(id)
|
||||||
s.curr.deserialize(udb)
|
s.curr.deserialize(udb)
|
||||||
|
|
||||||
log.Debug("stats: initialized")
|
s.logger.Debug("initialized")
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
@@ -228,8 +239,6 @@ func (s *StatsCtx) Start() {
|
|||||||
|
|
||||||
// Close implements the [io.Closer] interface for *StatsCtx.
|
// Close implements the [io.Closer] interface for *StatsCtx.
|
||||||
func (s *StatsCtx) Close() (err error) {
|
func (s *StatsCtx) Close() (err error) {
|
||||||
defer func() { err = errors.Annotate(err, "stats: closing: %w") }()
|
|
||||||
|
|
||||||
db := s.db.Swap(nil)
|
db := s.db.Swap(nil)
|
||||||
if db == nil {
|
if db == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -237,7 +246,7 @@ func (s *StatsCtx) Close() (err error) {
|
|||||||
defer func() {
|
defer func() {
|
||||||
cerr := db.Close()
|
cerr := db.Close()
|
||||||
if cerr == nil {
|
if cerr == nil {
|
||||||
log.Debug("stats: database closed")
|
s.logger.Debug("database closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = errors.WithDeferred(err, cerr)
|
err = errors.WithDeferred(err, cerr)
|
||||||
@@ -254,7 +263,7 @@ func (s *StatsCtx) Close() (err error) {
|
|||||||
|
|
||||||
udb := s.curr.serialize()
|
udb := s.curr.serialize()
|
||||||
|
|
||||||
return udb.flushUnitToDB(tx, s.curr.id)
|
return s.flushUnitToDB(udb, tx, s.curr.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update implements the [Interface] interface for *StatsCtx. e must not be
|
// Update implements the [Interface] interface for *StatsCtx. e must not be
|
||||||
@@ -269,7 +278,7 @@ func (s *StatsCtx) Update(e *Entry) {
|
|||||||
|
|
||||||
err := e.validate()
|
err := e.validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("stats: updating: validating entry: %s", err)
|
s.logger.Debug("validating entry", slogutil.KeyError, err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -278,7 +287,7 @@ func (s *StatsCtx) Update(e *Entry) {
|
|||||||
defer s.currMu.Unlock()
|
defer s.currMu.Unlock()
|
||||||
|
|
||||||
if s.curr == nil {
|
if s.curr == nil {
|
||||||
log.Error("stats: current unit is nil")
|
s.logger.Error("current unit is nil")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -333,8 +342,8 @@ func (s *StatsCtx) TopClientsIP(maxCount uint) (ips []netip.Addr) {
|
|||||||
|
|
||||||
// deleteOldUnits walks the buckets available to tx and deletes old units. It
|
// deleteOldUnits walks the buckets available to tx and deletes old units. It
|
||||||
// returns the number of deletions performed.
|
// returns the number of deletions performed.
|
||||||
func deleteOldUnits(tx *bbolt.Tx, firstID uint32) (deleted int) {
|
func (s *StatsCtx) deleteOldUnits(tx *bbolt.Tx, firstID uint32) (deleted int) {
|
||||||
log.Debug("stats: deleting old units until id %d", firstID)
|
s.logger.Debug("deleting old units up to", "unit", firstID)
|
||||||
|
|
||||||
// TODO(a.garipov): See if this is actually necessary. Looks like a rather
|
// TODO(a.garipov): See if this is actually necessary. Looks like a rather
|
||||||
// bizarre solution.
|
// bizarre solution.
|
||||||
@@ -348,12 +357,12 @@ func deleteOldUnits(tx *bbolt.Tx, firstID uint32) (deleted int) {
|
|||||||
|
|
||||||
err = tx.DeleteBucket(name)
|
err = tx.DeleteBucket(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("stats: deleting bucket: %s", err)
|
s.logger.Debug("deleting bucket", slogutil.KeyError, err)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("stats: deleted unit %d (name %x)", nameID, name)
|
s.logger.Debug("deleted unit", "name_id", nameID, "name", fmt.Sprintf("%x", name))
|
||||||
|
|
||||||
deleted++
|
deleted++
|
||||||
|
|
||||||
@@ -362,7 +371,7 @@ func deleteOldUnits(tx *bbolt.Tx, firstID uint32) (deleted int) {
|
|||||||
|
|
||||||
err := tx.ForEach(walk)
|
err := tx.ForEach(walk)
|
||||||
if err != nil && !errors.Is(err, errStop) {
|
if err != nil && !errors.Is(err, errStop) {
|
||||||
log.Debug("stats: deleting units: %s", err)
|
s.logger.Debug("deleting units", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return deleted
|
return deleted
|
||||||
@@ -371,20 +380,29 @@ func deleteOldUnits(tx *bbolt.Tx, firstID uint32) (deleted int) {
|
|||||||
// openDB returns an error if the database can't be opened from the specified
|
// openDB returns an error if the database can't be opened from the specified
|
||||||
// file. It's safe for concurrent use.
|
// file. It's safe for concurrent use.
|
||||||
func (s *StatsCtx) openDB() (err error) {
|
func (s *StatsCtx) openDB() (err error) {
|
||||||
log.Debug("stats: opening database")
|
s.logger.Debug("opening database")
|
||||||
|
|
||||||
var db *bbolt.DB
|
var db *bbolt.DB
|
||||||
db, err = bbolt.Open(s.filename, 0o644, nil)
|
db, err = bbolt.Open(s.filename, 0o644, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "invalid argument" {
|
if err.Error() == "invalid argument" {
|
||||||
log.Error("AdGuard Home cannot be initialized due to an incompatible file system.\nPlease read the explanation here: https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#limitations")
|
const lines = `AdGuard Home cannot be initialized due to an incompatible file system.
|
||||||
|
Please read the explanation here: https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#limitations`
|
||||||
|
|
||||||
|
// TODO(s.chzhen): Use passed context.
|
||||||
|
slogutil.PrintLines(
|
||||||
|
context.TODO(),
|
||||||
|
s.logger,
|
||||||
|
slog.LevelError,
|
||||||
|
"opening database",
|
||||||
|
lines,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use defer to unlock the mutex as soon as possible.
|
defer s.logger.Debug("database opened")
|
||||||
defer log.Debug("stats: database opened")
|
|
||||||
|
|
||||||
s.db.Store(db)
|
s.db.Store(db)
|
||||||
|
|
||||||
@@ -424,34 +442,37 @@ func (s *StatsCtx) flushDB(id, limit uint32, ptr *unit) (cont bool, sleepFor tim
|
|||||||
isCommitable := true
|
isCommitable := true
|
||||||
tx, err := db.Begin(true)
|
tx, err := db.Begin(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("stats: opening transaction: %s", err)
|
s.logger.Error("opening transaction", slogutil.KeyError, err)
|
||||||
|
|
||||||
return true, 0
|
return true, 0
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err = finishTxn(tx, isCommitable); err != nil {
|
if err = finishTxn(tx, isCommitable); err != nil {
|
||||||
log.Error("stats: %s", err)
|
s.logger.Error("finishing transaction", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
s.curr = newUnit(id)
|
s.curr = newUnit(id)
|
||||||
|
|
||||||
flushErr := ptr.serialize().flushUnitToDB(tx, ptr.id)
|
udb := ptr.serialize()
|
||||||
|
flushErr := s.flushUnitToDB(udb, tx, ptr.id)
|
||||||
if flushErr != nil {
|
if flushErr != nil {
|
||||||
log.Error("stats: flushing unit: %s", flushErr)
|
s.logger.Error("flushing unit", slogutil.KeyError, flushErr)
|
||||||
isCommitable = false
|
isCommitable = false
|
||||||
}
|
}
|
||||||
|
|
||||||
delErr := tx.DeleteBucket(idToUnitName(id - limit))
|
delErr := tx.DeleteBucket(idToUnitName(id - limit))
|
||||||
|
|
||||||
if delErr != nil {
|
if delErr != nil {
|
||||||
// TODO(e.burkov): Improve the algorithm of deleting the oldest bucket
|
// TODO(e.burkov): Improve the algorithm of deleting the oldest bucket
|
||||||
// to avoid the error.
|
// to avoid the error.
|
||||||
if errors.Is(delErr, bbolt.ErrBucketNotFound) {
|
lvl := slog.LevelWarn
|
||||||
log.Debug("stats: warning: deleting unit: %s", delErr)
|
if !errors.Is(delErr, bbolt.ErrBucketNotFound) {
|
||||||
} else {
|
|
||||||
isCommitable = false
|
isCommitable = false
|
||||||
log.Error("stats: deleting unit: %s", delErr)
|
lvl = slog.LevelError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.logger.Log(context.TODO(), lvl, "deleting bucket", slogutil.KeyError, delErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, 0
|
return true, 0
|
||||||
@@ -467,7 +488,7 @@ func (s *StatsCtx) periodicFlush() {
|
|||||||
cont, sleepFor = s.flush()
|
cont, sleepFor = s.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("periodic flushing finished")
|
s.logger.Debug("periodic flushing finished")
|
||||||
}
|
}
|
||||||
|
|
||||||
// setLimit sets the limit. s.lock is expected to be locked.
|
// setLimit sets the limit. s.lock is expected to be locked.
|
||||||
@@ -477,16 +498,16 @@ func (s *StatsCtx) setLimit(limit time.Duration) {
|
|||||||
if limit != 0 {
|
if limit != 0 {
|
||||||
s.enabled = true
|
s.enabled = true
|
||||||
s.limit = limit
|
s.limit = limit
|
||||||
log.Debug("stats: set limit: %d days", limit/timeutil.Day)
|
s.logger.Debug("setting limit in days", "num", limit/timeutil.Day)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.enabled = false
|
s.enabled = false
|
||||||
log.Debug("stats: disabled")
|
s.logger.Debug("disabled")
|
||||||
|
|
||||||
if err := s.clear(); err != nil {
|
if err := s.clear(); err != nil {
|
||||||
log.Error("stats: %s", err)
|
s.logger.Error("clearing", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,7 +520,7 @@ func (s *StatsCtx) clear() (err error) {
|
|||||||
var tx *bbolt.Tx
|
var tx *bbolt.Tx
|
||||||
tx, err = db.Begin(true)
|
tx, err = db.Begin(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("stats: opening a transaction: %s", err)
|
s.logger.Error("opening transaction", slogutil.KeyError, err)
|
||||||
} else if err = finishTxn(tx, false); err != nil {
|
} else if err = finishTxn(tx, false); err != nil {
|
||||||
// Don't wrap the error since it's informative enough as is.
|
// Don't wrap the error since it's informative enough as is.
|
||||||
return err
|
return err
|
||||||
@@ -513,21 +534,21 @@ func (s *StatsCtx) clear() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// All active transactions are now closed.
|
// All active transactions are now closed.
|
||||||
log.Debug("stats: database closed")
|
s.logger.Debug("database closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.Remove(s.filename)
|
err = os.Remove(s.filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("stats: %s", err)
|
s.logger.Error("removing", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.openDB()
|
err = s.openDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("stats: opening database: %s", err)
|
s.logger.Error("opening database", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use defer to unlock the mutex as soon as possible.
|
// Use defer to unlock the mutex as soon as possible.
|
||||||
defer log.Debug("stats: cleared")
|
defer s.logger.Debug("cleared")
|
||||||
|
|
||||||
s.currMu.Lock()
|
s.currMu.Lock()
|
||||||
defer s.currMu.Unlock()
|
defer s.currMu.Unlock()
|
||||||
@@ -548,7 +569,7 @@ func (s *StatsCtx) loadUnits(limit uint32) (units []*unitDB, curID uint32) {
|
|||||||
// taken into account.
|
// taken into account.
|
||||||
tx, err := db.Begin(true)
|
tx, err := db.Begin(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("stats: opening transaction: %s", err)
|
s.logger.Error("opening transaction", slogutil.KeyError, err)
|
||||||
|
|
||||||
return nil, 0
|
return nil, 0
|
||||||
}
|
}
|
||||||
@@ -568,7 +589,7 @@ func (s *StatsCtx) loadUnits(limit uint32) (units []*unitDB, curID uint32) {
|
|||||||
units = make([]*unitDB, 0, limit)
|
units = make([]*unitDB, 0, limit)
|
||||||
firstID := curID - limit + 1
|
firstID := curID - limit + 1
|
||||||
for i := firstID; i != curID; i++ {
|
for i := firstID; i != curID; i++ {
|
||||||
u := loadUnitFromDB(tx, i)
|
u := s.loadUnitFromDB(tx, i)
|
||||||
if u == nil {
|
if u == nil {
|
||||||
u = &unitDB{NResult: make([]uint64, resultLast)}
|
u = &unitDB{NResult: make([]uint64, resultLast)}
|
||||||
}
|
}
|
||||||
@@ -577,7 +598,7 @@ func (s *StatsCtx) loadUnits(limit uint32) (units []*unitDB, curID uint32) {
|
|||||||
|
|
||||||
err = finishTxn(tx, false)
|
err = finishTxn(tx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("stats: %s", err)
|
s.logger.Error("finishing transaction", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cur != nil {
|
if cur != nil {
|
||||||
@@ -585,7 +606,8 @@ func (s *StatsCtx) loadUnits(limit uint32) (units []*unitDB, curID uint32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if unitsLen := len(units); unitsLen != int(limit) {
|
if unitsLen := len(units); unitsLen != int(limit) {
|
||||||
log.Fatalf("loaded %d units whilst the desired number is %d", unitsLen, limit)
|
// Should not happen.
|
||||||
|
panic(fmt.Errorf("loaded %d units when the desired number is %d", unitsLen, limit))
|
||||||
}
|
}
|
||||||
|
|
||||||
return units, curID
|
return units, curID
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user