Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e32a37a747 | ||
|
|
7805a71332 | ||
|
|
6fb2aee210 | ||
|
|
ce9bb588ed | ||
|
|
55fb914537 | ||
|
|
6f7bfd6c9c |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -1,7 +1,7 @@
|
||||
'name': 'build'
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.21.8'
|
||||
'GO_VERSION': '1.22.2'
|
||||
'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'"
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -1,7 +1,7 @@
|
||||
'name': 'lint'
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.21.8'
|
||||
'GO_VERSION': '1.22.2'
|
||||
|
||||
'on':
|
||||
'push':
|
||||
|
||||
85
CHANGELOG.md
85
CHANGELOG.md
@@ -14,15 +14,68 @@ and this project adheres to
|
||||
<!--
|
||||
## [v0.108.0] - TBA
|
||||
|
||||
## [v0.107.46] - 2024-03-13 (APPROX.)
|
||||
## [v0.107.48] - 2024-04-24 (APPROX.)
|
||||
|
||||
See also the [v0.107.46 GitHub milestone][ms-v0.107.46].
|
||||
See also the [v0.107.48 GitHub milestone][ms-v0.107.48].
|
||||
|
||||
[ms-v0.107.46]: https://github.com/AdguardTeam/AdGuardHome/milestone/81?closed=1
|
||||
[ms-v0.107.48]: https://github.com/AdguardTeam/AdGuardHome/milestone/83?closed=1
|
||||
|
||||
NOTE: Add new changes BELOW THIS COMMENT.
|
||||
-->
|
||||
|
||||
<!--
|
||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
||||
-->
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
@@ -30,17 +83,29 @@ NOTE: Add new changes BELOW THIS COMMENT.
|
||||
- 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
|
||||
|
||||
<!--
|
||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
||||
-->
|
||||
[ms-v0.107.46]: https://github.com/AdguardTeam/AdGuardHome/milestone/81?closed=1
|
||||
|
||||
|
||||
|
||||
@@ -2831,11 +2896,13 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
|
||||
|
||||
|
||||
<!--
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.46...HEAD
|
||||
[v0.107.46]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.45...v0.107.46
|
||||
[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
|
||||
-->
|
||||
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.45...HEAD
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.47...HEAD
|
||||
[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
|
||||
|
||||
@@ -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
|
||||
## Pull requests
|
||||
|
||||
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.
|
||||
### Discuss your changes first
|
||||
|
||||
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
|
||||
|
||||
## Pull requests
|
||||
|
||||
### Discuss your changes first
|
||||
|
||||
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.
|
||||
|
||||
53
HACKING.md
53
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.
|
||||
|
||||
## <a href="#git" id="git" name="git">Git</a>
|
||||
## <a href="#git" id="git" name="git">Git</a>
|
||||
|
||||
This section was moved to [its own document][git].
|
||||
|
||||
## <a href="#go" id="go" name="go">Go</a>
|
||||
## <a href="#go" id="go" name="go">Go</a>
|
||||
|
||||
This section was moved to [its own document][go].
|
||||
|
||||
### <a href="#code" id="code" name="code">Code</a>
|
||||
### <a href="#code" id="code" name="code">Code</a>
|
||||
|
||||
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.
|
||||
|
||||
### <a href="#commenting" id="commenting" name="commenting">Commenting</a>
|
||||
### <a href="#commenting" id="commenting" name="commenting">Commenting</a>
|
||||
|
||||
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.
|
||||
|
||||
### <a href="#formatting" id="formatting" name="formatting">Formatting</a>
|
||||
### <a href="#formatting" id="formatting" name="formatting">Formatting</a>
|
||||
|
||||
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.
|
||||
|
||||
### <a href="#naming" id="naming" name="naming">Naming</a>
|
||||
### <a href="#naming" id="naming" name="naming">Naming</a>
|
||||
|
||||
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.
|
||||
|
||||
### <a href="#testing" id="testing" name="testing">Testing</a>
|
||||
### <a href="#testing" id="testing" name="testing">Testing</a>
|
||||
|
||||
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.
|
||||
|
||||
### <a href="#recommended-reading" id="recommended-reading" name="recommended-reading">Recommended Reading</a>
|
||||
### <a href="#recommended-reading" id="recommended-reading" name="recommended-reading">Recommended Reading</a>
|
||||
|
||||
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.
|
||||
|
||||
## <a href="#markdown" id="markdown" name="markdown">Markdown</a>
|
||||
## <a href="#markdown" id="markdown" name="markdown">Markdown</a>
|
||||
|
||||
This section was moved to [its own document][md].
|
||||
|
||||
## <a href="#shell-scripting" id="shell-scripting" name="shell-scripting">Shell Scripting</a>
|
||||
## <a href="#shell-scripting" id="shell-scripting" name="shell-scripting">Shell Scripting</a>
|
||||
|
||||
This section was moved to [its own document][sh].
|
||||
|
||||
### <a href="#shell-conditionals" id="shell-conditionals" name="shell-conditionals">Shell Conditionals</a>
|
||||
### <a href="#shell-conditionals" id="shell-conditionals" name="shell-conditionals">Shell Conditionals</a>
|
||||
|
||||
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.
|
||||
|
||||
## <a href="#text-including-comments" id="text-including-comments" name="text-including-comments">Text, Including Comments</a>
|
||||
## <a href="#text-including-comments" id="text-including-comments" name="text-including-comments">Text, Including Comments</a>
|
||||
|
||||
This section was moved to [its own document][txt].
|
||||
|
||||
## <a href="#yaml" id="yaml" name="yaml">YAML</a>
|
||||
## <a href="#yaml" id="yaml" name="yaml">YAML</a>
|
||||
|
||||
This section was moved to [its own document][yaml].
|
||||
|
||||
|
||||
17
Makefile
17
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.8
|
||||
GOTOOLCHAIN = go1.22.2
|
||||
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
|
||||
|
||||
473
README.md
473
README.md
@@ -1,85 +1,75 @@
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="doc/adguard_home_darkmode.svg">
|
||||
<img alt="AdGuard Home" src="doc/adguard_home_lightmode.svg" width="300px">
|
||||
</picture>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="doc/adguard_home_darkmode.svg">
|
||||
<img alt="AdGuard Home" src="doc/adguard_home_lightmode.svg" width="300px">
|
||||
</picture>
|
||||
</p>
|
||||
<h3 align="center">Privacy protection center for you and your devices</h3>
|
||||
<p align="center">
|
||||
Free and open source, powerful network-wide ads & trackers blocking DNS
|
||||
server.
|
||||
Free and open source, powerful network-wide ads & trackers blocking DNS server.
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://adguard.com/">AdGuard.com</a> |
|
||||
<a href="https://github.com/AdguardTeam/AdGuardHome/wiki">Wiki</a> |
|
||||
<a href="https://reddit.com/r/Adguard">Reddit</a> |
|
||||
<a href="https://twitter.com/AdGuard">Twitter</a> |
|
||||
<a href="https://t.me/adguard_en">Telegram</a>
|
||||
<br/><br/>
|
||||
<a href="https://codecov.io/github/AdguardTeam/AdGuardHome?branch=master">
|
||||
<img src="https://img.shields.io/codecov/c/github/AdguardTeam/AdGuardHome/master.svg" alt="Code Coverage"/>
|
||||
</a>
|
||||
<a href="https://goreportcard.com/report/AdguardTeam/AdGuardHome">
|
||||
<img src="https://goreportcard.com/badge/github.com/AdguardTeam/AdGuardHome" alt="Go Report Card"/>
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/adguard/adguardhome">
|
||||
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/adguard/adguardhome.svg?maxAge=604800"/>
|
||||
</a>
|
||||
<br/>
|
||||
<a href="https://github.com/AdguardTeam/AdGuardHome/releases">
|
||||
<img src="https://img.shields.io/github/release/AdguardTeam/AdGuardHome/all.svg" alt="Latest release"/>
|
||||
</a>
|
||||
<a href="https://snapcraft.io/adguard-home">
|
||||
<img alt="adguard-home" src="https://snapcraft.io/adguard-home/badge.svg"/>
|
||||
</a>
|
||||
<a href="https://adguard.com/">AdGuard.com</a> |
|
||||
<a href="https://github.com/AdguardTeam/AdGuardHome/wiki">Wiki</a> |
|
||||
<a href="https://reddit.com/r/Adguard">Reddit</a> |
|
||||
<a href="https://twitter.com/AdGuard">Twitter</a> |
|
||||
<a href="https://t.me/adguard_en">Telegram</a>
|
||||
<br/><br/>
|
||||
<a href="https://codecov.io/github/AdguardTeam/AdGuardHome?branch=master">
|
||||
<img src="https://img.shields.io/codecov/c/github/AdguardTeam/AdGuardHome/master.svg" alt="Code Coverage"/>
|
||||
</a>
|
||||
<a href="https://goreportcard.com/report/AdguardTeam/AdGuardHome">
|
||||
<img src="https://goreportcard.com/badge/github.com/AdguardTeam/AdGuardHome" alt="Go Report Card"/>
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/adguard/adguardhome">
|
||||
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/adguard/adguardhome.svg?maxAge=604800"/>
|
||||
</a>
|
||||
<br/>
|
||||
<a href="https://github.com/AdguardTeam/AdGuardHome/releases">
|
||||
<img src="https://img.shields.io/github/release/AdguardTeam/AdGuardHome/all.svg" alt="Latest release"/>
|
||||
</a>
|
||||
<a href="https://snapcraft.io/adguard-home">
|
||||
<img alt="adguard-home" src="https://snapcraft.io/adguard-home/badge.svg"/>
|
||||
</a>
|
||||
</p>
|
||||
<br/>
|
||||
<p align="center">
|
||||
<img src="https://cdn.adtidy.org/public/Adguard/Common/adguard_home.gif" width="800"/>
|
||||
<img src="https://cdn.adtidy.org/public/Adguard/Common/adguard_home.gif" width="800"/>
|
||||
</p>
|
||||
<hr/>
|
||||
|
||||
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)
|
||||
|
||||
## <a href="#getting-started" id="getting-started" name="getting-started">Getting Started</a>
|
||||
|
||||
* [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)
|
||||
|
||||
|
||||
|
||||
## <a href="#getting-started" id="getting-started" name="getting-started">Getting Started</a>
|
||||
|
||||
### <a href="#automated-install-linux-and-mac" id="automated-install-linux-and-mac" name="automated-install-linux-and-mac">Automated install (Linux/Unix/MacOS/FreeBSD/OpenBSD)</a>
|
||||
### <a href="#automated-install-linux-and-mac" id="automated-install-linux-and-mac" name="automated-install-linux-and-mac">Automated install (Linux/Unix/MacOS/FreeBSD/OpenBSD)</a>
|
||||
|
||||
To install with `curl` run the following command:
|
||||
|
||||
@@ -101,95 +91,70 @@ fetch -o - https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scri
|
||||
|
||||
The script also accepts some options:
|
||||
|
||||
* `-c <channel>` to use specified channel;
|
||||
* `-r` to reinstall AdGuard Home;
|
||||
* `-u` to uninstall AdGuard Home;
|
||||
* `-v` for verbose output.
|
||||
- `-c <channel>` 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.
|
||||
|
||||
### <a href="#alternative-methods" id="alternative-methods" name="alternative-methods">Alternative methods</a>
|
||||
|
||||
#### <a href="#manual-installation" id="manual-installation" name="manual-installation">Manual installation</a>
|
||||
|
||||
### <a href="#alternative-methods" id="alternative-methods" name="alternative-methods">Alternative methods</a>
|
||||
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.
|
||||
|
||||
#### <a href="#manual-installation" id="manual-installation" name="manual-installation">Manual installation</a>
|
||||
|
||||
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.
|
||||
|
||||
#### <a href="#docker" id="docker" name="docker">Docker</a>
|
||||
#### <a href="#docker" id="docker" name="docker">Docker</a>
|
||||
|
||||
You can use our official Docker image on [Docker Hub].
|
||||
|
||||
#### <a href="#snap-store" id="snap-store" name="snap-store">Snap Store</a>
|
||||
#### <a href="#snap-store" id="snap-store" name="snap-store">Snap Store</a>
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
### <a href="#guides" id="guides" name="guides">Guides</a>
|
||||
### <a href="#guides" id="guides" name="guides">Guides</a>
|
||||
|
||||
See our [Wiki][wiki].
|
||||
|
||||
[wiki]: https://github.com/AdguardTeam/AdGuardHome/wiki
|
||||
|
||||
### <a href="#api" id="api" name="api">API</a>
|
||||
|
||||
|
||||
### <a href="#api" id="api" name="api">API</a>
|
||||
|
||||
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/
|
||||
|
||||
## <a href="#comparison" id="comparison" name="comparison">Comparing AdGuard Home to other solutions</a>
|
||||
|
||||
### <a href="#comparison-adguard-dns" id="comparison-adguard-dns" name="comparison-adguard-dns">How is this different from public AdGuard DNS servers?</a>
|
||||
|
||||
## <a href="#comparison" id="comparison" name="comparison">Comparing AdGuard Home to other solutions</a>
|
||||
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:
|
||||
|
||||
### <a href="#comparison-adguard-dns" id="comparison-adguard-dns" name="comparison-adguard-dns">How is this different from public AdGuard DNS servers?</a>
|
||||
- Choose what exactly the server blocks and permits.
|
||||
|
||||
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:
|
||||
- Monitor your network activity.
|
||||
|
||||
* Choose what exactly the server blocks and permits.
|
||||
- Add your own custom filtering rules.
|
||||
|
||||
* Monitor your network activity.
|
||||
- **Most importantly, it's your own server, and you are the only one who's in control.**
|
||||
|
||||
* Add your own custom filtering rules.
|
||||
### <a href="#comparison-pi-hole" id="comparison-pi-hole" name="comparison-pi-hole">How does AdGuard Home compare to Pi-Hole</a>
|
||||
|
||||
* **Most importantly, it's your own server, and you are the only one who's in
|
||||
control.**
|
||||
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.
|
||||
|
||||
> [!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.
|
||||
|
||||
### <a href="#comparison-pi-hole" id="comparison-pi-hole" name="comparison-pi-hole">How does AdGuard Home compare to Pi-Hole</a>
|
||||
|
||||
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.
|
||||
|
||||
<aside>
|
||||
We're not going to stop here. DNS sinkholing is not a bad starting point, but
|
||||
this is just the beginning.
|
||||
</aside>
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
### <a href="#comparison-adblock" id="comparison-adblock" name="comparison-adblock">How does AdGuard Home compare to traditional ad blockers</a>
|
||||
### <a href="#comparison-adblock" id="comparison-adblock" name="comparison-adblock">How does AdGuard Home compare to traditional ad blockers</a>
|
||||
|
||||
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.
|
||||
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).
|
||||
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).
|
||||
|
||||
|
||||
|
||||
### <a href="#comparison-limitations" id="comparison-limitations" name="comparison-limitations">Known limitations</a>
|
||||
### <a href="#comparison-limitations" id="comparison-limitations" name="comparison-limitations">Known limitations</a>
|
||||
|
||||
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
|
||||
|
||||
## <a href="#how-to-build" id="how-to-build" name="how-to-build">How to build from source</a>
|
||||
|
||||
|
||||
## <a href="#how-to-build" id="how-to-build" name="how-to-build">How to build from source</a>
|
||||
|
||||
### <a href="#prerequisites" id="prerequisites" name="prerequisites">Prerequisites</a>
|
||||
### <a href="#prerequisites" id="prerequisites" name="prerequisites">Prerequisites</a>
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
### <a href="#building" id="building" name="building">Building</a>
|
||||
### <a href="#building" id="building" name="building">Building</a>
|
||||
|
||||
Open your terminal and execute these commands:
|
||||
|
||||
@@ -278,27 +220,22 @@ cd AdGuardHome
|
||||
make
|
||||
```
|
||||
|
||||
#### <a href="#building-node" id="building-node" name="building-node">Building with Node.js 17 and later</a>
|
||||
#### <a href="#building-node" id="building-node" name="building-node">Building with Node.js 17 and later</a>
|
||||
|
||||
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.
|
||||
|
||||
#### <a href="#building-cross" id="building-cross" name="building-cross">Building for a different platform</a>
|
||||
#### <a href="#building-cross" id="building-cross" name="building-cross">Building for a different platform</a>
|
||||
|
||||
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'
|
||||
```
|
||||
|
||||
#### <a href="#preparing-releases" id="preparing-releases" name="preparing-releases">Preparing releases</a>
|
||||
#### <a href="#preparing-releases" id="preparing-releases" name="preparing-releases">Preparing releases</a>
|
||||
|
||||
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].
|
||||
|
||||
#### <a href="#docker-image" id="docker-image" name="docker-image">Docker image</a>
|
||||
#### <a href="#docker-image" id="docker-image" name="docker-image">Docker image</a>
|
||||
|
||||
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].
|
||||
|
||||
#### <a href="#debugging-the-frontend" id="debugging-the-frontend" name="debugging-the-frontend">Debugging the frontend</a>
|
||||
#### <a href="#debugging-the-frontend" id="debugging-the-frontend" name="debugging-the-frontend">Debugging the frontend</a>
|
||||
|
||||
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,167 +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
|
||||
|
||||
|
||||
|
||||
## <a href="#contributing" id="contributing" name="contributing">Contributing</a>
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
### <a href="#test-unstable-versions" id="test-unstable-versions" name="test-unstable-versions">Test unstable versions</a>
|
||||
### <a href="#test-unstable-versions" id="test-unstable-versions" name="test-unstable-versions">Test unstable versions</a>
|
||||
|
||||
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://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -c beta
|
||||
```
|
||||
```sh
|
||||
curl -s -S -L https://raw.githubusercontent.com/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://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -c edge
|
||||
```
|
||||
```sh
|
||||
curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -c edge
|
||||
```
|
||||
|
||||
[wiki-platf]: https://github.com/AdguardTeam/AdGuardHome/wiki/Platforms
|
||||
|
||||
### <a href="#reporting-issues" id="reporting-issues" name="reporting-issues">Report issues</a>
|
||||
|
||||
|
||||
### <a href="#reporting-issues" id="reporting-issues" name="reporting-issues">Report issues</a>
|
||||
|
||||
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
|
||||
|
||||
### <a href="#translate" id="translate" name="translate">Help with translations</a>
|
||||
|
||||
|
||||
### <a href="#translate" id="translate" name="translate">Help with translations</a>
|
||||
|
||||
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
|
||||
|
||||
### <a href="#help-other" id="help-other" name="help-other">Other</a>
|
||||
|
||||
|
||||
### <a href="#help-other" id="help-other" name="help-other">Other</a>
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
## <a href="#uses" id="uses" name="uses">Projects that use AdGuard Home</a>
|
||||
|
||||
<!--
|
||||
TODO(a.garipov): Use reference links.
|
||||
-->
|
||||
Please note that these projects are not affiliated with AdGuard, but are made by third-party developers and fans.
|
||||
|
||||
* [AdGuard Home Remote](https://apps.apple.com/app/apple-store/id1543143740):
|
||||
iOS app by [Joost](https://rocketscience-it.nl/).
|
||||
- [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).
|
||||
- [Python library](https://github.com/frenck/python-adguardhome) by [@frenck](https://github.com/frenck).
|
||||
|
||||
* [Home Assistant add-on](https://github.com/hassio-addons/addon-adguard-home)
|
||||
by [@frenck](https://github.com/frenck).
|
||||
- [Home Assistant add-on](https://github.com/hassio-addons/addon-adguard-home) 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)).
|
||||
- [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/).
|
||||
|
||||
- [Browser Extension](https://github.com/satheshshiva/Adguard-Home-Browser-Ext) by [@satheshshiva](https://github.com/satheshshiva/).
|
||||
|
||||
- [Zabbix Template for AdGuard Home](https://github.com/diasdmhub/AdGuard_Home_Zabbix_Template) by [@diasdmhub](https://github.com/diasdmhub).
|
||||
|
||||
- [Chocolatey package](https://community.chocolatey.org/packages/adguardhome/) by [niks255](https://community.chocolatey.org/profiles/niks255).
|
||||
|
||||
## <a href="#acknowledgments" id="acknowledgments" name="acknowledgments">Acknowledgments</a>
|
||||
|
||||
<!--
|
||||
TODO(a.garipov): Use reference links.
|
||||
-->
|
||||
|
||||
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)
|
||||
- [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)
|
||||
|
||||
You might have seen that [CoreDNS] was mentioned here before, but we've stopped
|
||||
using it in AdGuard Home.
|
||||
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.
|
||||
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
|
||||
|
||||
## <a href="#privacy" id="privacy" name="privacy">Privacy</a>
|
||||
|
||||
|
||||
## <a href="#privacy" id="privacy" name="privacy">Privacy</a>
|
||||
|
||||
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
|
||||
|
||||
19
SECURITY.md
19
SECURITY.md
@@ -1,18 +1,13 @@
|
||||
# Security Policy
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
## Reporting vulnerabilities
|
||||
|
||||
Please send your vulnerability reports to <security@adguard.com>. To make sure
|
||||
that your report reaches us, please:
|
||||
Please send your vulnerability reports to <security@adguard.com>. 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 <security@adguard.com>. 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 <security@adguard.com>. Once again, make sure that the word “vulnerability” is in the subject line.
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
# Make sure to sync any changes with the branch overrides below.
|
||||
'variables':
|
||||
'channel': 'edge'
|
||||
'dockerGo': 'adguard/golang-ubuntu:8.1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:1.1'
|
||||
'dockerGo': 'adguard/go-builder:1.22.2--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.1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:1.1'
|
||||
'dockerGo': 'adguard/go-builder:1.22.2--1'
|
||||
# release-vX.Y.Z branches are the branches from which the actual final
|
||||
# release is built.
|
||||
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
|
||||
@@ -287,4 +281,5 @@
|
||||
# are the ones that actually get released.
|
||||
'variables':
|
||||
'channel': 'release'
|
||||
'dockerGo': 'adguard/golang-ubuntu:8.1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:1.1'
|
||||
'dockerGo': 'adguard/go-builder:1.22.2--1'
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# Make sure to sync any changes with the branch overrides below.
|
||||
'variables':
|
||||
'channel': 'edge'
|
||||
'dockerGo': 'adguard/golang-ubuntu:8.1'
|
||||
'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.1'
|
||||
'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.1'
|
||||
'dockerSnap': 'adguard/snap-builder:1.1'
|
||||
'snapcraftChannel': 'candidate'
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
'key': 'AHBRTSPECS'
|
||||
'name': 'AdGuard Home - Build and run tests'
|
||||
'variables':
|
||||
'dockerGo': 'adguard/golang-ubuntu:8.1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:1.1'
|
||||
'dockerGo': 'adguard/go-builder:1.22.2--1'
|
||||
'channel': 'development'
|
||||
|
||||
'stages':
|
||||
@@ -13,7 +14,14 @@
|
||||
'manual': false
|
||||
'final': false
|
||||
'jobs':
|
||||
- 'Test'
|
||||
- 'Test frontend'
|
||||
- 'Test backend'
|
||||
|
||||
- 'Frontend':
|
||||
manual: false
|
||||
final: false
|
||||
jobs:
|
||||
- 'Build frontend'
|
||||
|
||||
- 'Artifact':
|
||||
manual: false
|
||||
@@ -21,14 +29,12 @@
|
||||
jobs:
|
||||
- 'Artifact'
|
||||
|
||||
'Test':
|
||||
'Test frontend':
|
||||
'docker':
|
||||
'image': '${bamboo.dockerGo}'
|
||||
'image': '${bamboo.dockerFrontend}'
|
||||
'volumes':
|
||||
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
|
||||
'${system.GO_CACHE_DIR}': '${bamboo.cacheGo}'
|
||||
'${system.GO_PKG_CACHE_DIR}': '${bamboo.cacheGoPkg}'
|
||||
'key': 'TEST'
|
||||
'key': 'JSTEST'
|
||||
'other':
|
||||
'clean-working-dir': true
|
||||
'tasks':
|
||||
@@ -42,13 +48,91 @@
|
||||
|
||||
set -e -f -u -x
|
||||
|
||||
make VERBOSE=1 ci go-tools lint
|
||||
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': 'GOTEST'
|
||||
'other':
|
||||
'clean-working-dir': true
|
||||
'tasks':
|
||||
- 'checkout':
|
||||
'force-clean-build': true
|
||||
- 'script':
|
||||
'interpreter': 'SHELL'
|
||||
'scripts':
|
||||
- |
|
||||
#!/bin/sh
|
||||
|
||||
set -e -f -u -x
|
||||
|
||||
make\
|
||||
GOMAXPROCS=1\
|
||||
VERBOSE=1\
|
||||
go-deps go-tools go-lint
|
||||
|
||||
make\
|
||||
VERBOSE=1\
|
||||
go-test
|
||||
'final-tasks':
|
||||
- 'clean'
|
||||
'requirements':
|
||||
- 'adg-docker': 'true'
|
||||
|
||||
'Build frontend':
|
||||
'artifacts':
|
||||
- 'name': 'AdGuardHome frontend'
|
||||
'pattern': 'build/**'
|
||||
'shared': true
|
||||
'required': true
|
||||
'docker':
|
||||
'image': '${bamboo.dockerFrontend}'
|
||||
'volumes':
|
||||
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
|
||||
'key': 'BF'
|
||||
'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-build
|
||||
'requirements':
|
||||
- 'adg-docker': 'true'
|
||||
|
||||
'Artifact':
|
||||
'artifact-subscriptions':
|
||||
- 'artifact': 'AdGuardHome frontend'
|
||||
'artifacts':
|
||||
- 'name': 'AdGuardHome_windows_amd64'
|
||||
'pattern': 'dist/AdGuardHome_windows_amd64.zip'
|
||||
'shared': true
|
||||
'required': true
|
||||
- 'name': 'AdGuardHome_darwin_amd64'
|
||||
'pattern': 'dist/AdGuardHome_darwin_amd64.zip'
|
||||
'shared': true
|
||||
'required': true
|
||||
- 'name': 'AdGuardHome_linux_amd64'
|
||||
'pattern': 'dist/AdGuardHome_linux_amd64.tar.gz'
|
||||
'shared': true
|
||||
'required': true
|
||||
'docker':
|
||||
'image': '${bamboo.dockerGo}'
|
||||
'volumes':
|
||||
@@ -68,30 +152,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=${bamboo.channel}\
|
||||
SIGN=0\
|
||||
FRONTEND_PREBUILT=1\
|
||||
OS="windows darwin linux"\
|
||||
PARALLELISM=1\
|
||||
SIGN=0\
|
||||
VERBOSE=2\
|
||||
build-release
|
||||
'artifacts':
|
||||
- 'name': 'AdGuardHome_windows_amd64'
|
||||
'pattern': 'dist/AdGuardHome_windows_amd64.zip'
|
||||
'shared': true
|
||||
'required': true
|
||||
- 'name': 'AdGuardHome_darwin_amd64'
|
||||
'pattern': 'dist/AdGuardHome_darwin_amd64.zip'
|
||||
'shared': true
|
||||
'required': true
|
||||
- 'name': 'AdGuardHome_linux_amd64'
|
||||
'pattern': 'dist/AdGuardHome_linux_amd64.tar.gz'
|
||||
'shared': true
|
||||
'required': true
|
||||
'requirements':
|
||||
- 'adg-docker': 'true'
|
||||
|
||||
@@ -122,10 +191,9 @@
|
||||
# from the release branch and are used to build the release candidate
|
||||
# images.
|
||||
- '^rc-v[0-9]+\.[0-9]+\.[0-9]+':
|
||||
# Build betas on release branches manually.
|
||||
'triggers': []
|
||||
# Set the default release channel on the release branch to beta, as we
|
||||
# may need to build a few of these.
|
||||
'variables':
|
||||
'dockerGo': 'adguard/golang-ubuntu:8.1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:1.1'
|
||||
'dockerGo': 'adguard/go-builder:1.22.2--1'
|
||||
'channel': 'candidate'
|
||||
|
||||
@@ -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}} <i>(převzato z mezipaměti)</i>",
|
||||
"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ích</1>.",
|
||||
"confirm_dns_cache_clear": "Opravdu chcete vymazat mezipaměť DNS?",
|
||||
|
||||
@@ -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}} <i>(leveret fra cache)</i>",
|
||||
"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 indstillinger</1>.",
|
||||
"confirm_dns_cache_clear": "Sikker på, at DNS-cache skal ryddes?",
|
||||
|
||||
@@ -678,7 +678,7 @@
|
||||
"use_saved_key": "Zuvor gespeicherten Schlüssel verwenden",
|
||||
"parental_control": "Kindersicherung",
|
||||
"safe_browsing": "Internetsicherheit",
|
||||
"served_from_cache": "{{value}} <i>(aus dem Cache abgerufen)</i>",
|
||||
"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 Einstellungen</1> deaktivieren.",
|
||||
"confirm_dns_cache_clear": "Möchten Sie den DNS-Cache wirklich leeren?",
|
||||
|
||||
@@ -678,7 +678,7 @@
|
||||
"use_saved_key": "Use the previously saved key",
|
||||
"parental_control": "Parental Control",
|
||||
"safe_browsing": "Safe Browsing",
|
||||
"served_from_cache": "{{value}} <i>(served from cache)</i>",
|
||||
"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 settings</1>.",
|
||||
"confirm_dns_cache_clear": "Are you sure you want to clear DNS cache?",
|
||||
|
||||
@@ -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}} <i>(servido desde la caché)</i>",
|
||||
"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 general</1>.",
|
||||
"confirm_dns_cache_clear": "¿Estás seguro de que deseas borrar la caché DNS?",
|
||||
|
||||
@@ -678,7 +678,7 @@
|
||||
"use_saved_key": "Käytä aiemmin tallennettua avainta",
|
||||
"parental_control": "Lapsilukko",
|
||||
"safe_browsing": "Turvallinen selaus",
|
||||
"served_from_cache": "{{value}} <i>(jaettu välimuistista)</i>",
|
||||
"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ä asetuksista</1>.",
|
||||
"confirm_dns_cache_clear": "Haluatko varmasti tyhjentää DNS-välimuistin?",
|
||||
|
||||
@@ -678,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}} <i>(depuis le cache)</i>",
|
||||
"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éraux</1>.",
|
||||
"confirm_dns_cache_clear": "Voulez-vous vraiment vider le cache DNS ?",
|
||||
|
||||
@@ -117,8 +117,8 @@
|
||||
"refresh_statics": "Segarkan statistik",
|
||||
"dns_query": "Kueri DNS",
|
||||
"blocked_by": "<0>Diblokir oleh</0>",
|
||||
"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 <a>Penyaringan</a>.",
|
||||
"filters_block_toggle_hint": "Anda dapat menyiapkan aturan pemblokiran dalam pengaturan <a>Filter</a>.",
|
||||
"use_adguard_browsing_sec": "Gunakan layanan web Keamanan Penjelajahan AdGuard",
|
||||
"use_adguard_browsing_sec_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",
|
||||
@@ -291,7 +291,7 @@
|
||||
"custom_ip": "Custom IP",
|
||||
"blocking_ipv4": "Blokiran IPv4",
|
||||
"blocking_ipv6": "Blokiran IPv6",
|
||||
"blocked_response_ttl": "Respons TTL 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)",
|
||||
@@ -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",
|
||||
|
||||
@@ -678,7 +678,7 @@
|
||||
"use_saved_key": "Utilizza la chiave salvata in precedenza",
|
||||
"parental_control": "Controllo Parentale",
|
||||
"safe_browsing": "Navigazione Sicura",
|
||||
"served_from_cache": "{{value}} <i>(fornito dalla cache)</i>",
|
||||
"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 generali</1>.",
|
||||
"confirm_dns_cache_clear": "Sei sicuro di voler cancellare la cache DNS?",
|
||||
|
||||
@@ -678,7 +678,7 @@
|
||||
"use_saved_key": "以前に保存したキーを使用する",
|
||||
"parental_control": "ペアレンタルコントロール",
|
||||
"safe_browsing": "セーフブラウジング",
|
||||
"served_from_cache": "{{value}} <i>(キャッシュから応答)</i>",
|
||||
"served_from_cache_label": "キャッシュからの配信:",
|
||||
"form_error_password_length": "パスワードの長さは{{min}}〜{{max}}文字にしてください。",
|
||||
"anonymizer_notification": "【<0>注意</0>】IPの匿名化が有効になっています。 <1>一般設定</1>で無効にできます。",
|
||||
"confirm_dns_cache_clear": "DNS キャッシュをクリアしてもよろしいですか?",
|
||||
|
||||
@@ -678,7 +678,7 @@
|
||||
"use_saved_key": "이전에 저장했던 키 사용하기",
|
||||
"parental_control": "자녀 보호",
|
||||
"safe_browsing": "세이프 브라우징",
|
||||
"served_from_cache": "{{value}} <i>(캐시에서 제공)</i>",
|
||||
"served_from_cache_label": "캐시에서 가져옴",
|
||||
"form_error_password_length": "비밀번호는 {{min}}~{{max}}자 길이여야 합니다.",
|
||||
"anonymizer_notification": "<0>참고:</0> IP 익명화가 활성화되었습니다. <1>일반 설정</1>에서 비활성화할 수 있습니다.",
|
||||
"confirm_dns_cache_clear": "정말로 DNS 캐시를 지우시겠습니까?",
|
||||
|
||||
@@ -678,7 +678,7 @@
|
||||
"use_saved_key": "De eerder opgeslagen sleutel gebruiken",
|
||||
"parental_control": "Ouderlijk toezicht",
|
||||
"safe_browsing": "Veilig browsen",
|
||||
"served_from_cache": "{{value}} <i>(geleverd vanuit cache)</i>",
|
||||
"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 instellingen</1>.",
|
||||
"confirm_dns_cache_clear": "Weet je zeker dat je de DNS-cache wilt wissen?",
|
||||
|
||||
@@ -678,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}} <i>(podawane z pamięci podręcznej)</i>",
|
||||
"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ólnych</1>.",
|
||||
"confirm_dns_cache_clear": "Czy na pewno chcesz wyczyścić pamięć podręczną DNS?",
|
||||
|
||||
@@ -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}} <i>(servido do cache)</i>",
|
||||
"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 gerais</1>.",
|
||||
"confirm_dns_cache_clear": "Tem certeza de que deseja limpar o cache DNS?",
|
||||
|
||||
@@ -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}} <i>(servido do cache)</i>",
|
||||
"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 gerais</1>.",
|
||||
"confirm_dns_cache_clear": "Tem certeza de que quer limpar a cache DNS?",
|
||||
|
||||
@@ -678,7 +678,7 @@
|
||||
"use_saved_key": "Использовать сохранённый ранее ключ",
|
||||
"parental_control": "Родительский контроль",
|
||||
"safe_browsing": "Безопасный интернет",
|
||||
"served_from_cache": "{{value}} <i>(получено из кеша)</i>",
|
||||
"served_from_cache_label": "Получено из кеша",
|
||||
"form_error_password_length": "Пароль должен содержать от {{min}} до {{max}} символов",
|
||||
"anonymizer_notification": "<0>Внимание:</0> включена анонимизация IP-адресов. Вы можете отключить её в разделе <1>Основные настройки</1>.",
|
||||
"confirm_dns_cache_clear": "Вы уверены, что хотите очистить кеш DNS?",
|
||||
|
||||
@@ -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}} <i>(prevzatá z cache pamäte)</i>",
|
||||
"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 nastaveniach</1>.",
|
||||
"confirm_dns_cache_clear": "Naozaj chcete vymazať vyrovnávaciu pamäť DNS?",
|
||||
|
||||
@@ -678,7 +678,7 @@
|
||||
"use_saved_key": "Uporabi prej shranjeni ključ",
|
||||
"parental_control": "Starševski nadzor",
|
||||
"safe_browsing": "Varno brskanje",
|
||||
"served_from_cache": "{{value}} <i>(postreženo iz predpomnilnika)</i>",
|
||||
"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 nastavitvah</1>.",
|
||||
"confirm_dns_cache_clear": "Ali ste prepričani, da želite počistiti predpomnilnik DNS?",
|
||||
|
||||
@@ -678,7 +678,7 @@
|
||||
"use_saved_key": "Önceden kaydedilmiş anahtarı kullan",
|
||||
"parental_control": "Ebeveyn Denetimi",
|
||||
"safe_browsing": "Güvenli Gezinti",
|
||||
"served_from_cache": "{{value}} <i>(önbellekten kullanıldı)</i>",
|
||||
"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 ayarlardan</1> devre dışı bırakabilirsiniz.",
|
||||
"confirm_dns_cache_clear": "DNS önbelleğini temizlemek istediğinizden emin misiniz?",
|
||||
|
||||
@@ -678,7 +678,7 @@
|
||||
"use_saved_key": "Використати раніше збережений ключ",
|
||||
"parental_control": "Батьківський контроль",
|
||||
"safe_browsing": "Безпечний перегляд",
|
||||
"served_from_cache": "{{value}} <i>(отримано з кешу)</i>",
|
||||
"served_from_cache_label": "Отримано з кешу",
|
||||
"form_error_password_length": "Пароль має містити від {{min}} до {{max}} символів",
|
||||
"anonymizer_notification": "<0>Примітка:</0> IP-анонімізацію ввімкнено. Ви можете вимкнути його в <1>Загальні налаштування</1> .",
|
||||
"confirm_dns_cache_clear": "Ви впевнені, що бажаєте очистити кеш DNS?",
|
||||
|
||||
@@ -678,7 +678,7 @@
|
||||
"use_saved_key": "使用之前保存的密钥",
|
||||
"parental_control": "家长控制",
|
||||
"safe_browsing": "安全浏览",
|
||||
"served_from_cache": "{{value}}<i>(由缓存提供)</i>",
|
||||
"served_from_cache_label": "从缓存中",
|
||||
"form_error_password_length": "密码长度必须为 {{min}} 到 {{max}} 个字符",
|
||||
"anonymizer_notification": "<0>注意:</0> IP 匿名化已启用。您可以在<1>常规设置</1>中禁用它。",
|
||||
"confirm_dns_cache_clear": "您确定要清除 DNS 缓存吗?",
|
||||
|
||||
@@ -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": "您可在<a>過濾器</a>設定中設定封鎖規則。",
|
||||
@@ -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-TLS</0>(流量加密)",
|
||||
"example_upstream_doh": "<0>DNS-over-HTTPS</0>(流量加密)",
|
||||
"example_upstream_doh3": "使 DNS-over-HTTPS 強制使用 <0>HTTP/3</0> ,並禁止使用後備 HTTP/2 或更低版本;",
|
||||
"example_upstream_doq": "加密 <0>DNS-over-QUIC</0>",
|
||||
"example_upstream_sdns": "您可以使透過 <0>DNS Stamps</0> 來解析 <1>DNSCrypt</1> 或 <2>DNS-over-HTTPS</2>",
|
||||
"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-HTTPS</1> 或 <1>DNS-over-TLS</1>,您必須先在 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}} <i>(由快取回應)</i>",
|
||||
"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)"
|
||||
}
|
||||
|
||||
@@ -678,7 +678,7 @@
|
||||
"use_saved_key": "使用該先前已儲存的金鑰",
|
||||
"parental_control": "家長控制",
|
||||
"safe_browsing": "安全瀏覽",
|
||||
"served_from_cache": "{{value}} <i>(由快取提供)</i>",
|
||||
"served_from_cache_label": "從快取中",
|
||||
"form_error_password_length": "密碼長度必須為 {{min}} 到 {{max}} 個字符",
|
||||
"anonymizer_notification": "<0>注意:</0>IP 匿名化被啟用。您可在<1>一般設定</1>中禁用它。",
|
||||
"confirm_dns_cache_clear": "您確定您想要清除 DNS 快取嗎?",
|
||||
|
||||
@@ -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) });
|
||||
|
||||
@@ -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 = <span className="font-weight-bold">{statusLabel}</span>;
|
||||
const upstreamString = cached
|
||||
? t('served_from_cache', { value: upstream, i: <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: (
|
||||
<svg className="icons icon--20 icon--green mb-1">
|
||||
<use xlinkHref="#check" />
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
),
|
||||
elapsed: formattedElapsedMs,
|
||||
response_code: status,
|
||||
...(service_name && services.allServices
|
||||
|
||||
@@ -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: <i /> })
|
||||
: upstream;
|
||||
|
||||
const onBlockingForClientClick = () => {
|
||||
dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));
|
||||
@@ -192,7 +189,16 @@ const Row = memo(({
|
||||
className="link--green">{sourceData.name}
|
||||
</a>,
|
||||
response_details: 'title',
|
||||
install_settings_dns: upstreamString,
|
||||
install_settings_dns: upstream,
|
||||
...(cached
|
||||
&& {
|
||||
served_from_cache_label: (
|
||||
<svg className="icons icon--20 icon--green">
|
||||
<use xlinkHref="#check" />
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
),
|
||||
elapsed: formattedElapsedMs,
|
||||
...(rules.length > 0
|
||||
&& { rule_label: getRulesToFilterList(rules, filters, whitelistFilters) }
|
||||
|
||||
@@ -245,6 +245,10 @@ const Icons = () => (
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M12 13.5C11.1716 13.5 10.5 12.8284 10.5 12C10.5 11.1716 11.1716 10.5 12 10.5C12.8284 10.5 13.5 11.1716 13.5 12C13.5 12.8284 12.8284 13.5 12 13.5Z" fill="currentColor" />
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M12 20C11.1716 20 10.5 19.3284 10.5 18.5C10.5 17.6716 11.1716 17 12 17C12.8284 17 13.5 17.6716 13.5 18.5C13.5 19.3284 12.8284 20 12 20Z" fill="currentColor" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="check" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M5 11.7665L10.5878 17L19 8" />
|
||||
</symbol>
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
28
go.mod
28
go.mod
@@ -1,10 +1,10 @@
|
||||
module github.com/AdguardTeam/AdGuardHome
|
||||
|
||||
go 1.21.8
|
||||
go 1.22.2
|
||||
|
||||
require (
|
||||
github.com/AdguardTeam/dnsproxy v0.65.2
|
||||
github.com/AdguardTeam/golibs v0.20.1
|
||||
github.com/AdguardTeam/dnsproxy v0.67.0
|
||||
github.com/AdguardTeam/golibs v0.21.0
|
||||
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
|
||||
@@ -31,11 +31,11 @@ require (
|
||||
github.com/quic-go/quic-go v0.41.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
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.21.0
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225
|
||||
golang.org/x/net v0.23.0
|
||||
golang.org/x/sys v0.18.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
howett.net/plist v1.0.1
|
||||
@@ -48,19 +48,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/mod v0.16.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.18.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
gonum.org/v1/gonum v0.14.0 // indirect
|
||||
)
|
||||
|
||||
58
go.sum
58
go.sum
@@ -1,7 +1,7 @@
|
||||
github.com/AdguardTeam/dnsproxy v0.65.2 h1:D+BMw0Vu2lbQrYpoPctG2Xr+24KdfhgkzZb6QgPZheM=
|
||||
github.com/AdguardTeam/dnsproxy v0.65.2/go.mod h1:8NQTTNZY+qR9O1Fzgz3WQv30knfSgms68SRlzSnX74A=
|
||||
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.67.0 h1:7oKfcA8sm9d1N4qvhsNmQWBX4+fs3sX4cAnERmBXEbw=
|
||||
github.com/AdguardTeam/dnsproxy v0.67.0/go.mod h1:XLfD6IpSplUZZ+f5vhWSJW1mp4wm+KkHWiMo9w7U1Ls=
|
||||
github.com/AdguardTeam/golibs v0.21.0 h1:0swWyNaHTmT7aMwffKd9d54g4wBd8Oaj0fl+5l/PRdE=
|
||||
github.com/AdguardTeam/golibs v0.21.0/go.mod h1:/votX6WK1PdcZ3T2kBOPjPCGmfhlKixhI6ljYrFRPvI=
|
||||
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=
|
||||
@@ -121,32 +121,34 @@ 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.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
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.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.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.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
@@ -161,8 +163,8 @@ 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.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=
|
||||
@@ -170,8 +172,8 @@ 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/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.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
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=
|
||||
|
||||
@@ -5,9 +5,9 @@ package aghalg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// Coalesce returns the first non-zero value. It is named after function
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package aghalg_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// elements is a helper function that returns n elements of the buffer.
|
||||
|
||||
86
internal/aghalg/sortedmap.go
Normal file
86
internal/aghalg/sortedmap.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
95
internal/aghalg/sortedmap_test.go
Normal file
95
internal/aghalg/sortedmap_test.go
Normal file
@@ -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 := 0; i < 10; i++ {
|
||||
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()
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -154,8 +154,8 @@ func pathsToPatterns(fsys fs.FS, paths []string) (patterns []string, err error)
|
||||
}
|
||||
|
||||
// handleEvents concurrently handles the file system events. It closes the
|
||||
// update channel of HostsContainer when finishes. It's used to be called
|
||||
// within a separate goroutine.
|
||||
// update channel of HostsContainer when finishes. It is intended to be used as
|
||||
// a goroutine.
|
||||
func (hc *HostsContainer) handleEvents() {
|
||||
defer log.OnPanic(fmt.Sprintf("%s: handling events", hostsContainerPrefix))
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ func TestNewHostsContainer(t *testing.T) {
|
||||
}
|
||||
|
||||
hc, err := aghnet.NewHostsContainer(testFS, &aghtest.FSWatcher{
|
||||
OnStart: func() (_ error) { panic("not implemented") },
|
||||
OnEvents: onEvents,
|
||||
OnAdd: onAdd,
|
||||
OnClose: func() (err error) { return nil },
|
||||
@@ -93,6 +94,7 @@ func TestNewHostsContainer(t *testing.T) {
|
||||
t.Run("nil_fs", func(t *testing.T) {
|
||||
require.Panics(t, func() {
|
||||
_, _ = aghnet.NewHostsContainer(nil, &aghtest.FSWatcher{
|
||||
OnStart: func() (_ error) { panic("not implemented") },
|
||||
// Those shouldn't panic.
|
||||
OnEvents: func() (e <-chan struct{}) { return nil },
|
||||
OnAdd: func(name string) (err error) { return nil },
|
||||
@@ -111,6 +113,7 @@ func TestNewHostsContainer(t *testing.T) {
|
||||
const errOnAdd errors.Error = "error"
|
||||
|
||||
errWatcher := &aghtest.FSWatcher{
|
||||
OnStart: func() (_ error) { panic("not implemented") },
|
||||
OnEvents: func() (e <-chan struct{}) { panic("not implemented") },
|
||||
OnAdd: func(name string) (err error) { return errOnAdd },
|
||||
OnClose: func() (err error) { return nil },
|
||||
@@ -155,6 +158,7 @@ func TestHostsContainer_refresh(t *testing.T) {
|
||||
t.Cleanup(func() { close(eventsCh) })
|
||||
|
||||
w := &aghtest.FSWatcher{
|
||||
OnStart: func() (_ error) { panic("not implemented") },
|
||||
OnEvents: func() (e <-chan event) { return eventsCh },
|
||||
OnAdd: func(name string) (err error) {
|
||||
assert.Equal(t, "dir", name)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package aghnet
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/urlfilter"
|
||||
"github.com/AdguardTeam/urlfilter/filterlist"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// IgnoreEngine contains the list of rules for ignoring hostnames and matches
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/osutil"
|
||||
)
|
||||
|
||||
// DialContextFunc is the semantic alias for dialing functions, such as
|
||||
@@ -32,7 +33,7 @@ var (
|
||||
netInterfaceAddrs = net.InterfaceAddrs
|
||||
|
||||
// rootDirFS is the filesystem pointing to the root directory.
|
||||
rootDirFS = aghos.RootDirFS()
|
||||
rootDirFS = osutil.RootDirFS()
|
||||
)
|
||||
|
||||
// ErrNoStaticIPInfo is returned by IfaceHasStaticIP when no information about
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -6,8 +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/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
@@ -18,31 +20,38 @@ type event = struct{}
|
||||
// FSWatcher tracks all the fyle system events and notifies about those.
|
||||
//
|
||||
// TODO(e.burkov, a.garipov): Move into another package like aghfs.
|
||||
//
|
||||
// TODO(e.burkov): Add tests.
|
||||
type FSWatcher interface {
|
||||
// Start starts watching the added files.
|
||||
Start() (err error)
|
||||
|
||||
// Close stops watching the files and closes an update channel.
|
||||
io.Closer
|
||||
|
||||
// Events should return a read-only channel which notifies about events.
|
||||
// Events returns the channel to notify about the file system events.
|
||||
Events() (e <-chan event)
|
||||
|
||||
// Add should check if the file named name is accessible and starts tracking
|
||||
// it.
|
||||
// Add starts tracking the file. It returns an error if the file can't be
|
||||
// tracked. It must not be called after Start.
|
||||
Add(name string) (err error)
|
||||
}
|
||||
|
||||
// osWatcher tracks the file system provided by the OS.
|
||||
type osWatcher struct {
|
||||
// w is the actual notifier that is handled by osWatcher.
|
||||
w *fsnotify.Watcher
|
||||
// watcher is the actual notifier that is handled by osWatcher.
|
||||
watcher *fsnotify.Watcher
|
||||
|
||||
// events is the channel to notify.
|
||||
events chan event
|
||||
|
||||
// files is the set of tracked files.
|
||||
files *container.MapSet[string]
|
||||
}
|
||||
|
||||
const (
|
||||
// osWatcherPref is a prefix for logging and wrapping errors in osWathcer's
|
||||
// methods.
|
||||
osWatcherPref = "os watcher"
|
||||
)
|
||||
// osWatcherPref is a prefix for logging and wrapping errors in osWathcer's
|
||||
// methods.
|
||||
const osWatcherPref = "os watcher"
|
||||
|
||||
// NewOSWritesWatcher creates FSWatcher that tracks the real file system of the
|
||||
// OS and notifies only about writing events.
|
||||
@@ -55,25 +64,27 @@ func NewOSWritesWatcher() (w FSWatcher, err error) {
|
||||
return nil, fmt.Errorf("creating watcher: %w", err)
|
||||
}
|
||||
|
||||
fsw := &osWatcher{
|
||||
w: watcher,
|
||||
events: make(chan event, 1),
|
||||
}
|
||||
|
||||
go fsw.handleErrors()
|
||||
go fsw.handleEvents()
|
||||
|
||||
return fsw, nil
|
||||
return &osWatcher{
|
||||
watcher: watcher,
|
||||
events: make(chan event, 1),
|
||||
files: container.NewMapSet[string](),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// handleErrors handles accompanying errors. It used to be called in a separate
|
||||
// goroutine.
|
||||
func (w *osWatcher) handleErrors() {
|
||||
defer log.OnPanic(fmt.Sprintf("%s: handling errors", osWatcherPref))
|
||||
// type check
|
||||
var _ FSWatcher = (*osWatcher)(nil)
|
||||
|
||||
for err := range w.w.Errors {
|
||||
log.Error("%s: %s", osWatcherPref, err)
|
||||
}
|
||||
// Start implements the FSWatcher interface for *osWatcher.
|
||||
func (w *osWatcher) Start() (err error) {
|
||||
go w.handleErrors()
|
||||
go w.handleEvents()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close implements the FSWatcher interface for *osWatcher.
|
||||
func (w *osWatcher) Close() (err error) {
|
||||
return w.watcher.Close()
|
||||
}
|
||||
|
||||
// Events implements the FSWatcher interface for *osWatcher.
|
||||
@@ -81,34 +92,42 @@ func (w *osWatcher) Events() (e <-chan event) {
|
||||
return w.events
|
||||
}
|
||||
|
||||
// Add implements the FSWatcher interface for *osWatcher.
|
||||
// Add implements the [FSWatcher] interface for *osWatcher.
|
||||
//
|
||||
// TODO(e.burkov): Make it accept non-existing files to detect it's creating.
|
||||
func (w *osWatcher) Add(name string) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "%s: %w", osWatcherPref) }()
|
||||
|
||||
if _, err = fs.Stat(RootDirFS(), name); err != nil {
|
||||
fi, err := fs.Stat(osutil.RootDirFS(), name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking file %q: %w", name, err)
|
||||
}
|
||||
|
||||
return w.w.Add(filepath.Join("/", name))
|
||||
}
|
||||
name = filepath.Join("/", name)
|
||||
w.files.Add(name)
|
||||
|
||||
// Close implements the FSWatcher interface for *osWatcher.
|
||||
func (w *osWatcher) Close() (err error) {
|
||||
return w.w.Close()
|
||||
// Watch the directory and filter the events by the file name, since the
|
||||
// common recomendation to the fsnotify package is to watch the directory
|
||||
// instead of the file itself.
|
||||
//
|
||||
// See https://pkg.go.dev/github.com/fsnotify/fsnotify@v1.7.0#readme-watching-a-file-doesn-t-work-well.
|
||||
if !fi.IsDir() {
|
||||
name = filepath.Dir(name)
|
||||
}
|
||||
|
||||
return w.watcher.Add(name)
|
||||
}
|
||||
|
||||
// handleEvents notifies about the received file system's event if needed. It
|
||||
// used to be called in a separate goroutine.
|
||||
// is intended to be used as a goroutine.
|
||||
func (w *osWatcher) handleEvents() {
|
||||
defer log.OnPanic(fmt.Sprintf("%s: handling events", osWatcherPref))
|
||||
|
||||
defer close(w.events)
|
||||
|
||||
ch := w.w.Events
|
||||
ch := w.watcher.Events
|
||||
for e := range ch {
|
||||
if e.Op&fsnotify.Write == 0 {
|
||||
if e.Op&fsnotify.Write == 0 || !w.files.Has(e.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -131,3 +150,13 @@ func (w *osWatcher) handleEvents() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleErrors handles accompanying errors. It used to be called in a separate
|
||||
// goroutine.
|
||||
func (w *osWatcher) handleErrors() {
|
||||
defer log.OnPanic(fmt.Sprintf("%s: handling errors", osWatcherPref))
|
||||
|
||||
for err := range w.watcher.Errors {
|
||||
log.Error("%s: %s", osWatcherPref, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,17 +7,16 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// UnsupportedError is returned by functions and methods when a particular
|
||||
@@ -155,13 +154,6 @@ func IsOpenWrt() (ok bool) {
|
||||
return isOpenWrt()
|
||||
}
|
||||
|
||||
// RootDirFS returns the [fs.FS] rooted at the operating system's root. On
|
||||
// Windows it returns the fs.FS rooted at the volume of the system directory
|
||||
// (usually, C:).
|
||||
func RootDirFS() (fsys fs.FS) {
|
||||
return rootDirFS()
|
||||
}
|
||||
|
||||
// NotifyReconfigureSignal notifies c on receiving reconfigure signals.
|
||||
func NotifyReconfigureSignal(c chan<- os.Signal) {
|
||||
notifyReconfigureSignal(c)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/AdguardTeam/golibs/osutil"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
)
|
||||
|
||||
@@ -40,7 +41,7 @@ func isOpenWrt() (ok bool) {
|
||||
}
|
||||
|
||||
return nil, !stringutil.ContainsFold(string(data), osNameData), nil
|
||||
}).Walk(RootDirFS(), etcReleasePattern)
|
||||
}).Walk(osutil.RootDirFS(), etcReleasePattern)
|
||||
|
||||
return err == nil && ok
|
||||
}
|
||||
|
||||
@@ -3,17 +3,12 @@
|
||||
package aghos
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func rootDirFS() (fsys fs.FS) {
|
||||
return os.DirFS("/")
|
||||
}
|
||||
|
||||
func notifyReconfigureSignal(c chan<- os.Signal) {
|
||||
signal.Notify(c, unix.SIGHUP)
|
||||
}
|
||||
|
||||
@@ -3,29 +3,13 @@
|
||||
package aghos
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func rootDirFS() (fsys fs.FS) {
|
||||
// TODO(a.garipov): Use a better way if golang/go#44279 is ever resolved.
|
||||
sysDir, err := windows.GetSystemDirectory()
|
||||
if err != nil {
|
||||
log.Error("aghos: getting root filesystem: %s; using C:", err)
|
||||
|
||||
// Assume that C: is the safe default.
|
||||
return os.DirFS("C:")
|
||||
}
|
||||
|
||||
return os.DirFS(filepath.VolumeName(sysDir))
|
||||
}
|
||||
|
||||
func setRlimit(val uint64) (err error) {
|
||||
return Unsupported("setrlimit")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -26,14 +25,25 @@ import (
|
||||
|
||||
// FSWatcher is a fake [aghos.FSWatcher] implementation for tests.
|
||||
type FSWatcher struct {
|
||||
OnStart func() (err error)
|
||||
OnClose func() (err error)
|
||||
OnEvents func() (e <-chan struct{})
|
||||
OnAdd func(name string) (err error)
|
||||
OnClose func() (err error)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ aghos.FSWatcher = (*FSWatcher)(nil)
|
||||
|
||||
// Start implements the [aghos.FSWatcher] interface for *FSWatcher.
|
||||
func (w *FSWatcher) Start() (err error) {
|
||||
return w.OnStart()
|
||||
}
|
||||
|
||||
// Close implements the [aghos.FSWatcher] interface for *FSWatcher.
|
||||
func (w *FSWatcher) Close() (err error) {
|
||||
return w.OnClose()
|
||||
}
|
||||
|
||||
// Events implements the [aghos.FSWatcher] interface for *FSWatcher.
|
||||
func (w *FSWatcher) Events() (e <-chan struct{}) {
|
||||
return w.OnEvents()
|
||||
@@ -44,11 +54,6 @@ func (w *FSWatcher) Add(name string) (err error) {
|
||||
return w.OnAdd(name)
|
||||
}
|
||||
|
||||
// Close implements the [aghos.FSWatcher] interface for *FSWatcher.
|
||||
func (w *FSWatcher) Close() (err error) {
|
||||
return w.OnClose()
|
||||
}
|
||||
|
||||
// Package agh
|
||||
|
||||
// ServiceWithConfig is a fake [agh.ServiceWithConfig] implementation for tests.
|
||||
@@ -88,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) {
|
||||
@@ -108,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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -7,13 +7,14 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"golang.org/x/exp/slices"
|
||||
"github.com/AdguardTeam/golibs/osutil"
|
||||
)
|
||||
|
||||
// Variables and functions to substitute in tests.
|
||||
@@ -22,7 +23,7 @@ var (
|
||||
aghosRunCommand = aghos.RunCommand
|
||||
|
||||
// rootDirFS is the filesystem pointing to the root directory.
|
||||
rootDirFS = aghos.RootDirFS()
|
||||
rootDirFS = osutil.RootDirFS()
|
||||
)
|
||||
|
||||
// Interface stores and refreshes the network neighborhood reported by ARP
|
||||
|
||||
249
internal/client/index.go
Normal file
249
internal/client/index.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
for _, id := range c.ClientIDs {
|
||||
existing, ok := ci.clientIDToUID[id]
|
||||
if ok && existing != c.UID {
|
||||
p := ci.uidToClient[existing]
|
||||
|
||||
return fmt.Errorf("another client %q uses the same ID %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
|
||||
}
|
||||
|
||||
ci.subnetToUID.Range(func(pref netip.Prefix, id UID) (cont bool) {
|
||||
if pref.Contains(ip) {
|
||||
uid, found = id, true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if found {
|
||||
return ci.uidToClient[uid], true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
223
internal/client/index_internal_test.go
Normal file
223
internal/client/index_internal_test.go
Normal file
@@ -0,0 +1,223 @@
|
||||
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"
|
||||
)
|
||||
|
||||
clients := []*Persistent{{
|
||||
Name: "client1",
|
||||
IPs: []netip.Addr{
|
||||
netip.MustParseAddr(cliIP1),
|
||||
netip.MustParseAddr(cliIPv6),
|
||||
},
|
||||
}, {
|
||||
Name: "client2",
|
||||
IPs: []netip.Addr{netip.MustParseAddr(cliIP2)},
|
||||
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 {
|
||||
want *Persistent
|
||||
name string
|
||||
ids []string
|
||||
}{{
|
||||
name: "ipv4_ipv6",
|
||||
ids: []string{cliIP1, cliIPv6},
|
||||
want: clients[0],
|
||||
}, {
|
||||
name: "ipv4_subnet",
|
||||
ids: []string{cliIP2, cliSubnetIP},
|
||||
want: clients[1],
|
||||
}, {
|
||||
name: "mac",
|
||||
ids: []string{cliMAC},
|
||||
want: clients[2],
|
||||
}, {
|
||||
name: "client_id",
|
||||
ids: []string{cliID},
|
||||
want: clients[3],
|
||||
}}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -1,22 +1,22 @@
|
||||
package home
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"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"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// UID is the type for the unique IDs of persistent clients.
|
||||
@@ -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,16 +56,16 @@ 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
|
||||
// TODO(d.kolyshev): Make SafeSearchConf a pointer.
|
||||
SafeSearchConf filtering.SafeSearchConfig
|
||||
SafeSearch filtering.SafeSearch
|
||||
|
||||
// BlockedServices is the configuration of blocked services of a client.
|
||||
@@ -87,8 +97,8 @@ type persistentClient struct {
|
||||
IgnoreStatistics bool
|
||||
}
|
||||
|
||||
// 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 +112,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 +154,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 +180,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 +191,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 +224,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 +256,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 +267,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,
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -15,7 +16,6 @@ import (
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/google/renameio/v2/maybe"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -13,7 +14,6 @@ import (
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
@@ -19,7 +20,6 @@ import (
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type v4ServerConfJSON struct {
|
||||
@@ -592,7 +592,7 @@ func setOtherDHCPResult(ifaceName string, result *dhcpSearchResult) {
|
||||
}
|
||||
|
||||
// parseLease parses a lease from r. If there is no error returns DHCPServer
|
||||
// and *Lease. r must be non-nil.
|
||||
// and *Lease. r must be non-nil.
|
||||
func (s *server) parseLease(r io.Reader) (srv DHCPServer, lease *dhcpsvc.Lease, err error) {
|
||||
l := &leaseStatic{}
|
||||
err = json.NewDecoder(r).Decode(l)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -20,7 +21,6 @@ import (
|
||||
"github.com/go-ping/ping"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4/server4"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// v4Server is a DHCPv4 server.
|
||||
|
||||
@@ -2,11 +2,11 @@ package dhcpsvc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// Config is the configuration for the DHCP service.
|
||||
@@ -19,6 +19,8 @@ type Config struct {
|
||||
// clients' hostnames.
|
||||
LocalDomainName string
|
||||
|
||||
// TODO(e.burkov): Add DB path.
|
||||
|
||||
// ICMPTimeout is the timeout for checking another DHCP server's presence.
|
||||
ICMPTimeout time.Duration
|
||||
|
||||
@@ -68,12 +70,6 @@ func (conf *Config) Validate() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// newMustErr returns an error that indicates that valName must be as must
|
||||
// describes.
|
||||
func newMustErr(valName, must string, val fmt.Stringer) (err error) {
|
||||
return fmt.Errorf("%s %s must %s", valName, val, must)
|
||||
}
|
||||
|
||||
// validate returns an error in ic, if any.
|
||||
func (ic *InterfaceConfig) validate() (err error) {
|
||||
if ic == nil {
|
||||
|
||||
@@ -7,48 +7,16 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// Lease is a DHCP lease.
|
||||
// Interface is a DHCP service.
|
||||
//
|
||||
// TODO(e.burkov): Consider moving it to [agh], since it also may be needed in
|
||||
// [websvc].
|
||||
type Lease struct {
|
||||
// IP is the IP address leased to the client.
|
||||
IP netip.Addr
|
||||
|
||||
// Expiry is the expiration time of the lease.
|
||||
Expiry time.Time
|
||||
|
||||
// Hostname of the client.
|
||||
Hostname string
|
||||
|
||||
// HWAddr is the physical hardware address (MAC address).
|
||||
HWAddr net.HardwareAddr
|
||||
|
||||
// IsStatic defines if the lease is static.
|
||||
IsStatic bool
|
||||
}
|
||||
|
||||
// Clone returns a deep copy of l.
|
||||
func (l *Lease) Clone() (clone *Lease) {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &Lease{
|
||||
Expiry: l.Expiry,
|
||||
Hostname: l.Hostname,
|
||||
HWAddr: slices.Clone(l.HWAddr),
|
||||
IP: l.IP,
|
||||
IsStatic: l.IsStatic,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Separate HostByIP, MACByIP, IPByHost into a separate
|
||||
// interface. This is also applicable to Enabled method.
|
||||
//
|
||||
// TODO(e.burkov): Reconsider the requirements for the leases validity.
|
||||
type Interface interface {
|
||||
agh.ServiceWithConfig[*Config]
|
||||
|
||||
@@ -63,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
|
||||
@@ -71,26 +41,29 @@ type Interface interface {
|
||||
// hostname, either set or generated.
|
||||
IPByHost(host string) (ip netip.Addr)
|
||||
|
||||
// Leases returns all the active DHCP leases.
|
||||
// Leases returns all the active DHCP leases. The returned slice should be
|
||||
// a clone.
|
||||
//
|
||||
// TODO(e.burkov): Consider implementing iterating methods with appropriate
|
||||
// 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.
|
||||
//
|
||||
// TODO(e.burkov): If it's really needed?
|
||||
Reset() (err error)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package dhcpsvc
|
||||
|
||||
import "github.com/AdguardTeam/golibs/errors"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// errNilConfig is returned when a nil config met.
|
||||
@@ -9,3 +13,9 @@ const (
|
||||
// errNoInterfaces is returned when no interfaces found in configuration.
|
||||
errNoInterfaces errors.Error = "no interfaces specified"
|
||||
)
|
||||
|
||||
// newMustErr returns an error that indicates that valName must be as must
|
||||
// describes.
|
||||
func newMustErr(valName, must string, val fmt.Stringer) (err error) {
|
||||
return fmt.Errorf("%s %s must %s", valName, val, must)
|
||||
}
|
||||
|
||||
66
internal/dhcpsvc/interface.go
Normal file
66
internal/dhcpsvc/interface.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package dhcpsvc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
// netInterface is a common part of any network interface within the DHCP
|
||||
// server.
|
||||
//
|
||||
// TODO(e.burkov): Add other methods as [DHCPServer] evolves.
|
||||
type netInterface struct {
|
||||
// name is the name of the network interface.
|
||||
name string
|
||||
|
||||
// leases is a set of leases sorted by hardware address.
|
||||
leases []*Lease
|
||||
|
||||
// leaseTTL is the default Time-To-Live value for leases.
|
||||
leaseTTL time.Duration
|
||||
}
|
||||
|
||||
// reset clears all the slices in iface for reuse.
|
||||
func (iface *netInterface) reset() {
|
||||
iface.leases = iface.leases[:0]
|
||||
}
|
||||
|
||||
// insertLease inserts the given lease into iface. It returns an error if the
|
||||
// lease can't be inserted.
|
||||
func (iface *netInterface) insertLease(l *Lease) (err error) {
|
||||
i, found := slices.BinarySearchFunc(iface.leases, l, compareLeaseMAC)
|
||||
if found {
|
||||
return fmt.Errorf("lease for mac %s already exists", l.HWAddr)
|
||||
}
|
||||
|
||||
iface.leases = slices.Insert(iface.leases, i, l)
|
||||
|
||||
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
|
||||
}
|
||||
52
internal/dhcpsvc/lease.go
Normal file
52
internal/dhcpsvc/lease.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package dhcpsvc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Lease is a DHCP lease.
|
||||
//
|
||||
// TODO(e.burkov): Consider moving it to [agh], since it also may be needed in
|
||||
// [websvc].
|
||||
//
|
||||
// TODO(e.burkov): Add validation method.
|
||||
type Lease struct {
|
||||
// IP is the IP address leased to the client.
|
||||
IP netip.Addr
|
||||
|
||||
// Expiry is the expiration time of the lease.
|
||||
Expiry time.Time
|
||||
|
||||
// Hostname of the client.
|
||||
Hostname string
|
||||
|
||||
// HWAddr is the physical hardware address (MAC address).
|
||||
HWAddr net.HardwareAddr
|
||||
|
||||
// IsStatic defines if the lease is static.
|
||||
IsStatic bool
|
||||
}
|
||||
|
||||
// Clone returns a deep copy of l.
|
||||
func (l *Lease) Clone() (clone *Lease) {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &Lease{
|
||||
Expiry: l.Expiry,
|
||||
Hostname: l.Hostname,
|
||||
HWAddr: slices.Clone(l.HWAddr),
|
||||
IP: l.IP,
|
||||
IsStatic: l.IsStatic,
|
||||
}
|
||||
}
|
||||
|
||||
// compareLeaseMAC compares two [Lease]s by hardware address.
|
||||
func compareLeaseMAC(a, b *Lease) (res int) {
|
||||
return bytes.Compare(a.HWAddr, b.HWAddr)
|
||||
}
|
||||
126
internal/dhcpsvc/leaseindex.go
Normal file
126
internal/dhcpsvc/leaseindex.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -2,11 +2,15 @@ package dhcpsvc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// DHCPServer is a DHCP server for both IPv4 and IPv6 address families.
|
||||
@@ -15,18 +19,21 @@ type DHCPServer struct {
|
||||
// information about its clients.
|
||||
enabled *atomic.Bool
|
||||
|
||||
// localTLD is the top-level domain name to use for resolving DHCP
|
||||
// clients' hostnames.
|
||||
// localTLD is the top-level domain name to use for resolving DHCP clients'
|
||||
// hostnames.
|
||||
localTLD string
|
||||
|
||||
// leasesMu protects the leases index as well as leases in the interfaces.
|
||||
leasesMu *sync.RWMutex
|
||||
|
||||
// leases stores the DHCP leases for quick lookups.
|
||||
leases *leaseIndex
|
||||
|
||||
// interfaces4 is the set of IPv4 interfaces sorted by interface name.
|
||||
interfaces4 []*iface4
|
||||
interfaces4 netInterfacesV4
|
||||
|
||||
// interfaces6 is the set of IPv6 interfaces sorted by interface name.
|
||||
interfaces6 []*iface6
|
||||
|
||||
// leases is the set of active DHCP leases.
|
||||
leases []*Lease
|
||||
interfaces6 netInterfacesV6
|
||||
|
||||
// icmpTimeout is the timeout for checking another DHCP server's presence.
|
||||
icmpTimeout time.Duration
|
||||
@@ -42,26 +49,27 @@ func New(conf *Config) (srv *DHCPServer, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ifaces4 := make([]*iface4, len(conf.Interfaces))
|
||||
ifaces6 := make([]*iface6, len(conf.Interfaces))
|
||||
// TODO(e.burkov): Add validations scoped to the network interfaces set.
|
||||
ifaces4 := make(netInterfacesV4, 0, len(conf.Interfaces))
|
||||
ifaces6 := make(netInterfacesV6, 0, len(conf.Interfaces))
|
||||
|
||||
ifaceNames := maps.Keys(conf.Interfaces)
|
||||
slices.Sort(ifaceNames)
|
||||
|
||||
var i4 *iface4
|
||||
var i6 *iface6
|
||||
var i4 *netInterfaceV4
|
||||
var i6 *netInterfaceV6
|
||||
|
||||
for _, ifaceName := range ifaceNames {
|
||||
iface := conf.Interfaces[ifaceName]
|
||||
|
||||
i4, err = newIface4(ifaceName, iface.IPv4)
|
||||
i4, err = newNetInterfaceV4(ifaceName, iface.IPv4)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("interface %q: ipv4: %w", ifaceName, err)
|
||||
} else if i4 != nil {
|
||||
ifaces4 = append(ifaces4, i4)
|
||||
}
|
||||
|
||||
i6 = newIface6(ifaceName, iface.IPv6)
|
||||
i6 = newNetInterfaceV6(ifaceName, iface.IPv6)
|
||||
if i6 != nil {
|
||||
ifaces6 = append(ifaces6, i6)
|
||||
}
|
||||
@@ -70,13 +78,19 @@ func New(conf *Config) (srv *DHCPServer, err error) {
|
||||
enabled := &atomic.Bool{}
|
||||
enabled.Store(conf.Enabled)
|
||||
|
||||
return &DHCPServer{
|
||||
srv = &DHCPServer{
|
||||
enabled: enabled,
|
||||
localTLD: conf.LocalDomainName,
|
||||
leasesMu: &sync.RWMutex{},
|
||||
leases: newLeaseIndex(),
|
||||
interfaces4: ifaces4,
|
||||
interfaces6: ifaces6,
|
||||
localTLD: conf.LocalDomainName,
|
||||
icmpTimeout: conf.ICMPTimeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Load leases.
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// type check
|
||||
@@ -91,10 +105,140 @@ func (srv *DHCPServer) Enabled() (ok bool) {
|
||||
|
||||
// Leases implements the [Interface] interface for *DHCPServer.
|
||||
func (srv *DHCPServer) Leases() (leases []*Lease) {
|
||||
leases = make([]*Lease, 0, len(srv.leases))
|
||||
for _, lease := range srv.leases {
|
||||
leases = append(leases, lease.Clone())
|
||||
srv.leasesMu.RLock()
|
||||
defer srv.leasesMu.RUnlock()
|
||||
|
||||
for _, iface := range srv.interfaces4 {
|
||||
for _, lease := range iface.leases {
|
||||
leases = append(leases, lease.Clone())
|
||||
}
|
||||
}
|
||||
for _, iface := range srv.interfaces6 {
|
||||
for _, lease := range iface.leases {
|
||||
leases = append(leases, lease.Clone())
|
||||
}
|
||||
}
|
||||
|
||||
return leases
|
||||
}
|
||||
|
||||
// HostByIP implements the [Interface] interface for *DHCPServer.
|
||||
func (srv *DHCPServer) HostByIP(ip netip.Addr) (host string) {
|
||||
srv.leasesMu.RLock()
|
||||
defer srv.leasesMu.RUnlock()
|
||||
|
||||
if l, ok := srv.leases.leaseByAddr(ip); ok {
|
||||
return l.Hostname
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// MACByIP implements the [Interface] interface for *DHCPServer.
|
||||
func (srv *DHCPServer) MACByIP(ip netip.Addr) (mac net.HardwareAddr) {
|
||||
srv.leasesMu.RLock()
|
||||
defer srv.leasesMu.RUnlock()
|
||||
|
||||
if l, ok := srv.leases.leaseByAddr(ip); ok {
|
||||
return l.HWAddr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IPByHost implements the [Interface] interface for *DHCPServer.
|
||||
func (srv *DHCPServer) IPByHost(host string) (ip netip.Addr) {
|
||||
srv.leasesMu.RLock()
|
||||
defer srv.leasesMu.RUnlock()
|
||||
|
||||
if l, ok := srv.leases.leaseByName(host); ok {
|
||||
return l.IP
|
||||
}
|
||||
|
||||
return netip.Addr{}
|
||||
}
|
||||
|
||||
// Reset implements the [Interface] interface for *DHCPServer.
|
||||
func (srv *DHCPServer) Reset() (err error) {
|
||||
srv.leasesMu.Lock()
|
||||
defer srv.leasesMu.Unlock()
|
||||
|
||||
for _, iface := range srv.interfaces4 {
|
||||
iface.reset()
|
||||
}
|
||||
for _, iface := range srv.interfaces6 {
|
||||
iface.reset()
|
||||
}
|
||||
srv.leases.clear()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddLease implements the [Interface] interface for *DHCPServer.
|
||||
func (srv *DHCPServer) AddLease(l *Lease) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "adding lease: %w") }()
|
||||
|
||||
addr := l.IP
|
||||
iface, err := srv.ifaceForAddr(addr)
|
||||
if err != nil {
|
||||
// Don't wrap the error since there is already an annotation deferred.
|
||||
return err
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
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.leasesMu.Lock()
|
||||
defer srv.leasesMu.Unlock()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,17 +1,67 @@
|
||||
package dhcpsvc_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 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,
|
||||
@@ -113,3 +163,433 @@ func TestNew(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDHCPServer_AddLease(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, "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)
|
||||
|
||||
const (
|
||||
host1 = "host1"
|
||||
host2 = "host2"
|
||||
host3 = "host3"
|
||||
host4 = "host4"
|
||||
host5 = "host5"
|
||||
)
|
||||
|
||||
ip1 := netip.MustParseAddr("192.168.0.2")
|
||||
ip2 := netip.MustParseAddr("192.168.0.3")
|
||||
ip3 := netip.MustParseAddr("172.16.0.3")
|
||||
ip4 := netip.MustParseAddr("172.16.0.4")
|
||||
|
||||
mac1 := mustParseMAC(t, "01:02:03:04:05:06")
|
||||
mac2 := mustParseMAC(t, "06:05:04:03:02:01")
|
||||
mac3 := mustParseMAC(t, "02:03:04:05:06:07")
|
||||
|
||||
leases := []*dhcpsvc.Lease{{
|
||||
Hostname: host1,
|
||||
IP: ip1,
|
||||
HWAddr: mac1,
|
||||
IsStatic: true,
|
||||
}, {
|
||||
Hostname: host2,
|
||||
IP: ip2,
|
||||
HWAddr: mac2,
|
||||
IsStatic: true,
|
||||
}, {
|
||||
Hostname: host3,
|
||||
IP: ip3,
|
||||
HWAddr: mac3,
|
||||
IsStatic: true,
|
||||
}, {
|
||||
Hostname: host4,
|
||||
IP: ip4,
|
||||
HWAddr: mac1,
|
||||
IsStatic: true,
|
||||
}}
|
||||
for _, l := range leases {
|
||||
require.NoError(t, srv.AddLease(l))
|
||||
}
|
||||
|
||||
t.Run("ip_idx", func(t *testing.T) {
|
||||
assert.Equal(t, ip1, srv.IPByHost(host1))
|
||||
assert.Equal(t, ip2, srv.IPByHost(host2))
|
||||
assert.Equal(t, ip3, srv.IPByHost(host3))
|
||||
assert.Equal(t, ip4, srv.IPByHost(host4))
|
||||
assert.Equal(t, netip.Addr{}, srv.IPByHost(host5))
|
||||
})
|
||||
|
||||
t.Run("name_idx", func(t *testing.T) {
|
||||
assert.Equal(t, host1, srv.HostByIP(ip1))
|
||||
assert.Equal(t, host2, srv.HostByIP(ip2))
|
||||
assert.Equal(t, host3, srv.HostByIP(ip3))
|
||||
assert.Equal(t, host4, srv.HostByIP(ip4))
|
||||
assert.Equal(t, "", srv.HostByIP(netip.Addr{}))
|
||||
})
|
||||
|
||||
t.Run("mac_idx", func(t *testing.T) {
|
||||
assert.Equal(t, mac1, srv.MACByIP(ip1))
|
||||
assert.Equal(t, mac2, srv.MACByIP(ip2))
|
||||
assert.Equal(t, mac3, srv.MACByIP(ip3))
|
||||
assert.Equal(t, mac1, srv.MACByIP(ip4))
|
||||
assert.Nil(t, srv.MACByIP(netip.Addr{}))
|
||||
})
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/google/gopacket/layers"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// IPv4Config is the interface-specific configuration for DHCPv4.
|
||||
@@ -64,69 +64,6 @@ func (conf *IPv4Config) validate() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// iface4 is a DHCP interface for IPv4 address family.
|
||||
type iface4 struct {
|
||||
// gateway is the IP address of the network gateway.
|
||||
gateway netip.Addr
|
||||
|
||||
// subnet is the network subnet.
|
||||
subnet netip.Prefix
|
||||
|
||||
// addrSpace is the IPv4 address space allocated for leasing.
|
||||
addrSpace ipRange
|
||||
|
||||
// name is the name of the interface.
|
||||
name string
|
||||
|
||||
// implicitOpts are the options listed in Appendix A of RFC 2131 and
|
||||
// initialized with default values. It must not have intersections with
|
||||
// explicitOpts.
|
||||
implicitOpts layers.DHCPOptions
|
||||
|
||||
// explicitOpts are the user-configured options. It must not have
|
||||
// intersections with implicitOpts.
|
||||
explicitOpts layers.DHCPOptions
|
||||
|
||||
// leaseTTL is the time-to-live of dynamic leases on this interface.
|
||||
leaseTTL time.Duration
|
||||
}
|
||||
|
||||
// newIface4 creates a new DHCP interface for IPv4 address family with the given
|
||||
// configuration. It returns an error if the given configuration can't be used.
|
||||
func newIface4(name string, conf *IPv4Config) (i *iface4, err error) {
|
||||
if !conf.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
maskLen, _ := net.IPMask(conf.SubnetMask.AsSlice()).Size()
|
||||
subnet := netip.PrefixFrom(conf.GatewayIP, maskLen)
|
||||
|
||||
switch {
|
||||
case !subnet.Contains(conf.RangeStart):
|
||||
return nil, fmt.Errorf("range start %s is not within %s", conf.RangeStart, subnet)
|
||||
case !subnet.Contains(conf.RangeEnd):
|
||||
return nil, fmt.Errorf("range end %s is not within %s", conf.RangeEnd, subnet)
|
||||
}
|
||||
|
||||
addrSpace, err := newIPRange(conf.RangeStart, conf.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if addrSpace.contains(conf.GatewayIP) {
|
||||
return nil, fmt.Errorf("gateway ip %s in the ip range %s", conf.GatewayIP, addrSpace)
|
||||
}
|
||||
|
||||
i = &iface4{
|
||||
name: name,
|
||||
gateway: conf.GatewayIP,
|
||||
subnet: subnet,
|
||||
addrSpace: addrSpace,
|
||||
leaseTTL: conf.LeaseDuration,
|
||||
}
|
||||
i.implicitOpts, i.explicitOpts = conf.options()
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// options returns the implicit and explicit options for the interface. The two
|
||||
// lists are disjoint and the implicit options are initialized with default
|
||||
// values.
|
||||
@@ -318,3 +255,83 @@ func (conf *IPv4Config) options() (implicit, explicit layers.DHCPOptions) {
|
||||
func compareV4OptionCodes(a, b layers.DHCPOption) (res int) {
|
||||
return int(a.Type) - int(b.Type)
|
||||
}
|
||||
|
||||
// netInterfaceV4 is a DHCP interface for IPv4 address family.
|
||||
type netInterfaceV4 struct {
|
||||
// gateway is the IP address of the network gateway.
|
||||
gateway netip.Addr
|
||||
|
||||
// subnet is the network subnet.
|
||||
subnet netip.Prefix
|
||||
|
||||
// addrSpace is the IPv4 address space allocated for leasing.
|
||||
addrSpace ipRange
|
||||
|
||||
// implicitOpts are the options listed in Appendix A of RFC 2131 and
|
||||
// initialized with default values. It must not have intersections with
|
||||
// explicitOpts.
|
||||
implicitOpts layers.DHCPOptions
|
||||
|
||||
// explicitOpts are the user-configured options. It must not have
|
||||
// intersections with implicitOpts.
|
||||
explicitOpts layers.DHCPOptions
|
||||
|
||||
// netInterface is embedded here to provide some common network interface
|
||||
// logic.
|
||||
netInterface
|
||||
}
|
||||
|
||||
// newNetInterfaceV4 creates a new DHCP interface for IPv4 address family with
|
||||
// the given configuration. It returns an error if the given configuration
|
||||
// can't be used.
|
||||
func newNetInterfaceV4(name string, conf *IPv4Config) (i *netInterfaceV4, err error) {
|
||||
if !conf.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
maskLen, _ := net.IPMask(conf.SubnetMask.AsSlice()).Size()
|
||||
subnet := netip.PrefixFrom(conf.GatewayIP, maskLen)
|
||||
|
||||
switch {
|
||||
case !subnet.Contains(conf.RangeStart):
|
||||
return nil, fmt.Errorf("range start %s is not within %s", conf.RangeStart, subnet)
|
||||
case !subnet.Contains(conf.RangeEnd):
|
||||
return nil, fmt.Errorf("range end %s is not within %s", conf.RangeEnd, subnet)
|
||||
}
|
||||
|
||||
addrSpace, err := newIPRange(conf.RangeStart, conf.RangeEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if addrSpace.contains(conf.GatewayIP) {
|
||||
return nil, fmt.Errorf("gateway ip %s in the ip range %s", conf.GatewayIP, addrSpace)
|
||||
}
|
||||
|
||||
i = &netInterfaceV4{
|
||||
gateway: conf.GatewayIP,
|
||||
subnet: subnet,
|
||||
addrSpace: addrSpace,
|
||||
netInterface: netInterface{
|
||||
name: name,
|
||||
leaseTTL: conf.LeaseDuration,
|
||||
},
|
||||
}
|
||||
i.implicitOpts, i.explicitOpts = conf.options()
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// netInterfacesV4 is a slice of network interfaces of IPv4 address family.
|
||||
type netInterfacesV4 []*netInterfaceV4
|
||||
|
||||
// find returns the first network interface within ifaces containing ip. It
|
||||
// returns false if there is no such interface.
|
||||
func (ifaces netInterfacesV4) find(ip netip.Addr) (iface4 *netInterface, ok bool) {
|
||||
i := slices.IndexFunc(ifaces, func(iface *netInterfaceV4) (contains bool) {
|
||||
return iface.subnet.Contains(ip)
|
||||
})
|
||||
if i < 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return &ifaces[i].netInterface, true
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ package dhcpsvc
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/google/gopacket/layers"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// IPv6Config is the interface-specific configuration for DHCPv6.
|
||||
@@ -52,57 +53,6 @@ func (conf *IPv6Config) validate() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// iface6 is a DHCP interface for IPv6 address family.
|
||||
//
|
||||
// TODO(e.burkov): Add options.
|
||||
type iface6 struct {
|
||||
// rangeStart is the first IP address in the range.
|
||||
rangeStart netip.Addr
|
||||
|
||||
// name is the name of the interface.
|
||||
name string
|
||||
|
||||
// implicitOpts are the DHCPv6 options listed in RFC 8415 (and others) and
|
||||
// initialized with default values. It must not have intersections with
|
||||
// explicitOpts.
|
||||
implicitOpts layers.DHCPv6Options
|
||||
|
||||
// explicitOpts are the user-configured options. It must not have
|
||||
// intersections with implicitOpts.
|
||||
explicitOpts layers.DHCPv6Options
|
||||
|
||||
// leaseTTL is the time-to-live of dynamic leases on this interface.
|
||||
leaseTTL time.Duration
|
||||
|
||||
// raSLAACOnly defines if DHCP should send ICMPv6.RA packets without MO
|
||||
// flags.
|
||||
raSLAACOnly bool
|
||||
|
||||
// raAllowSLAAC defines if DHCP should send ICMPv6.RA packets with MO flags.
|
||||
raAllowSLAAC bool
|
||||
}
|
||||
|
||||
// newIface6 creates a new DHCP interface for IPv6 address family with the given
|
||||
// configuration.
|
||||
//
|
||||
// TODO(e.burkov): Validate properly.
|
||||
func newIface6(name string, conf *IPv6Config) (i *iface6) {
|
||||
if !conf.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
i = &iface6{
|
||||
name: name,
|
||||
rangeStart: conf.RangeStart,
|
||||
leaseTTL: conf.LeaseDuration,
|
||||
raSLAACOnly: conf.RASLAACOnly,
|
||||
raAllowSLAAC: conf.RAAllowSLAAC,
|
||||
}
|
||||
i.implicitOpts, i.explicitOpts = conf.options()
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// options returns the implicit and explicit options for the interface. The two
|
||||
// lists are disjoint and the implicit options are initialized with default
|
||||
// values.
|
||||
@@ -133,3 +83,79 @@ func (conf *IPv6Config) options() (implicit, explicit layers.DHCPv6Options) {
|
||||
func compareV6OptionCodes(a, b layers.DHCPv6Option) (res int) {
|
||||
return int(a.Code) - int(b.Code)
|
||||
}
|
||||
|
||||
// netInterfaceV6 is a DHCP interface for IPv6 address family.
|
||||
//
|
||||
// TODO(e.burkov): Add options.
|
||||
type netInterfaceV6 struct {
|
||||
// rangeStart is the first IP address in the range.
|
||||
rangeStart netip.Addr
|
||||
|
||||
// implicitOpts are the DHCPv6 options listed in RFC 8415 (and others) and
|
||||
// initialized with default values. It must not have intersections with
|
||||
// explicitOpts.
|
||||
implicitOpts layers.DHCPv6Options
|
||||
|
||||
// explicitOpts are the user-configured options. It must not have
|
||||
// intersections with implicitOpts.
|
||||
explicitOpts layers.DHCPv6Options
|
||||
|
||||
// netInterface is embedded here to provide some common network interface
|
||||
// logic.
|
||||
netInterface
|
||||
|
||||
// raSLAACOnly defines if DHCP should send ICMPv6.RA packets without MO
|
||||
// flags.
|
||||
raSLAACOnly bool
|
||||
|
||||
// raAllowSLAAC defines if DHCP should send ICMPv6.RA packets with MO flags.
|
||||
raAllowSLAAC bool
|
||||
}
|
||||
|
||||
// newNetInterfaceV6 creates a new DHCP interface for IPv6 address family with
|
||||
// the given configuration.
|
||||
//
|
||||
// TODO(e.burkov): Validate properly.
|
||||
func newNetInterfaceV6(name string, conf *IPv6Config) (i *netInterfaceV6) {
|
||||
if !conf.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
i = &netInterfaceV6{
|
||||
rangeStart: conf.RangeStart,
|
||||
netInterface: netInterface{
|
||||
name: name,
|
||||
leaseTTL: conf.LeaseDuration,
|
||||
},
|
||||
raSLAACOnly: conf.RASLAACOnly,
|
||||
raAllowSLAAC: conf.RAAllowSLAAC,
|
||||
}
|
||||
i.implicitOpts, i.explicitOpts = conf.options()
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// netInterfacesV4 is a slice of network interfaces of IPv4 address family.
|
||||
type netInterfacesV6 []*netInterfaceV6
|
||||
|
||||
// find returns the first network interface within ifaces containing ip. It
|
||||
// returns false if there is no such interface.
|
||||
func (ifaces netInterfacesV6) find(ip netip.Addr) (iface6 *netInterface, ok bool) {
|
||||
// prefLen is the length of prefix to match ip against.
|
||||
//
|
||||
// TODO(e.burkov): DHCPv6 inherits the weird behavior of legacy
|
||||
// implementation where the allocated range constrained by the first address
|
||||
// and the first address with last byte set to 0xff. Proper prefixes should
|
||||
// be used instead.
|
||||
const prefLen = netutil.IPv6BitLen - 8
|
||||
|
||||
i := slices.IndexFunc(ifaces, func(iface *netInterfaceV6) (contains bool) {
|
||||
return !ip.Less(iface.rangeStart) &&
|
||||
netip.PrefixFrom(iface.rangeStart, prefLen).Contains(ip)
|
||||
})
|
||||
if i < 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return &ifaces[i].netInterface, true
|
||||
}
|
||||
|
||||
@@ -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,7 +151,7 @@ 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()
|
||||
}
|
||||
|
||||
@@ -176,9 +175,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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -18,13 +19,13 @@ 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"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
"github.com/ameshkov/dnscrypt/v2"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// ClientsContainer provides information about preconfigured DNS clients.
|
||||
@@ -40,7 +41,7 @@ type ClientsContainer interface {
|
||||
) (conf *proxy.CustomUpstreamConfig, err error)
|
||||
}
|
||||
|
||||
// Config represents the DNS filtering configuration of AdGuard Home. The zero
|
||||
// Config represents the DNS filtering configuration of AdGuard Home. The zero
|
||||
// Config is empty and ready for use.
|
||||
type Config struct {
|
||||
// Callbacks for other modules
|
||||
@@ -357,10 +358,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,
|
||||
@@ -465,26 +462,27 @@ func (s *Server) prepareIpsetListSettings() (err error) {
|
||||
// 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 +513,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,9 +526,7 @@ 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
|
||||
@@ -582,11 +564,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 +584,7 @@ func (conf *ServerConfig) ourAddrsSet() (m addrPortSet, err error) {
|
||||
|
||||
return &combinedAddrPortSet{
|
||||
ports: unspecPorts,
|
||||
addrs: ifaceAddrs,
|
||||
addrs: container.NewMapSet(ifaceAddrs...),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package dnsforward
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func TestAnyNameMatches(t *testing.T) {
|
||||
|
||||
@@ -2,54 +2,56 @@ package dnsforward
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// upstreamConfigValidator parses the [*proxy.UpstreamConfig] and checks the
|
||||
// actual DNS availability of each upstream.
|
||||
// upstreamConfigValidator parses each section of an upstream configuration into
|
||||
// a corresponding [*proxy.UpstreamConfig] and checks the actual DNS
|
||||
// availability of each upstream.
|
||||
type upstreamConfigValidator struct {
|
||||
// general is the general upstream configuration.
|
||||
general []*upstreamResult
|
||||
// generalUpstreamResults contains upstream results of a general section.
|
||||
generalUpstreamResults map[string]*upstreamResult
|
||||
|
||||
// fallback is the fallback upstream configuration.
|
||||
fallback []*upstreamResult
|
||||
// fallbackUpstreamResults contains upstream results of a fallback section.
|
||||
fallbackUpstreamResults map[string]*upstreamResult
|
||||
|
||||
// private is the private upstream configuration.
|
||||
private []*upstreamResult
|
||||
// privateUpstreamResults contains upstream results of a private section.
|
||||
privateUpstreamResults map[string]*upstreamResult
|
||||
|
||||
// generalParseResults contains parsing results of a general section.
|
||||
generalParseResults []*parseResult
|
||||
|
||||
// fallbackParseResults contains parsing results of a fallback section.
|
||||
fallbackParseResults []*parseResult
|
||||
|
||||
// privateParseResults contains parsing results of a private section.
|
||||
privateParseResults []*parseResult
|
||||
}
|
||||
|
||||
// upstreamResult is a result of validation of an [upstream.Upstream] within an
|
||||
// upstreamResult is a result of parsing of an [upstream.Upstream] within an
|
||||
// [proxy.UpstreamConfig].
|
||||
type upstreamResult struct {
|
||||
// server is the parsed upstream. It is nil when there was an error during
|
||||
// parsing.
|
||||
// server is the parsed upstream.
|
||||
server upstream.Upstream
|
||||
|
||||
// err is the error either from parsing or from checking the upstream.
|
||||
// err is the upstream check error.
|
||||
err error
|
||||
|
||||
// original is the piece of configuration that have either been turned to an
|
||||
// upstream or caused an error.
|
||||
original string
|
||||
|
||||
// isSpecific is true if the upstream is domain-specific.
|
||||
isSpecific bool
|
||||
}
|
||||
|
||||
// compare compares two [upstreamResult]s. It returns 0 if they are equal, -1
|
||||
// if ur should be sorted before other, and 1 otherwise.
|
||||
//
|
||||
// TODO(e.burkov): Perhaps it makes sense to sort the results with errors near
|
||||
// the end.
|
||||
func (ur *upstreamResult) compare(other *upstreamResult) (res int) {
|
||||
return strings.Compare(ur.original, other.original)
|
||||
// parseResult contains a original piece of upstream configuration and a
|
||||
// corresponding error.
|
||||
type parseResult struct {
|
||||
err *proxy.ParseError
|
||||
original string
|
||||
}
|
||||
|
||||
// newUpstreamConfigValidator parses the upstream configuration and returns a
|
||||
@@ -61,97 +63,99 @@ func newUpstreamConfigValidator(
|
||||
private []string,
|
||||
opts *upstream.Options,
|
||||
) (cv *upstreamConfigValidator) {
|
||||
cv = &upstreamConfigValidator{}
|
||||
cv = &upstreamConfigValidator{
|
||||
generalUpstreamResults: map[string]*upstreamResult{},
|
||||
fallbackUpstreamResults: map[string]*upstreamResult{},
|
||||
privateUpstreamResults: map[string]*upstreamResult{},
|
||||
}
|
||||
|
||||
for _, line := range general {
|
||||
cv.general = cv.insertLineResults(cv.general, line, opts)
|
||||
}
|
||||
for _, line := range fallback {
|
||||
cv.fallback = cv.insertLineResults(cv.fallback, line, opts)
|
||||
}
|
||||
for _, line := range private {
|
||||
cv.private = cv.insertLineResults(cv.private, line, opts)
|
||||
}
|
||||
conf, err := proxy.ParseUpstreamsConfig(general, opts)
|
||||
cv.generalParseResults = collectErrResults(general, err)
|
||||
insertConfResults(conf, cv.generalUpstreamResults)
|
||||
|
||||
conf, err = proxy.ParseUpstreamsConfig(fallback, opts)
|
||||
cv.fallbackParseResults = collectErrResults(fallback, err)
|
||||
insertConfResults(conf, cv.fallbackUpstreamResults)
|
||||
|
||||
conf, err = proxy.ParseUpstreamsConfig(private, opts)
|
||||
cv.privateParseResults = collectErrResults(private, err)
|
||||
insertConfResults(conf, cv.privateUpstreamResults)
|
||||
|
||||
return cv
|
||||
}
|
||||
|
||||
// insertLineResults parses line and inserts the result into s. It can insert
|
||||
// multiple results as well as none.
|
||||
func (cv *upstreamConfigValidator) insertLineResults(
|
||||
s []*upstreamResult,
|
||||
line string,
|
||||
opts *upstream.Options,
|
||||
) (result []*upstreamResult) {
|
||||
upstreams, isSpecific, err := splitUpstreamLine(line)
|
||||
if err != nil {
|
||||
return cv.insert(s, &upstreamResult{
|
||||
err: err,
|
||||
original: line,
|
||||
})
|
||||
// collectErrResults parses err and returns parsing results containing the
|
||||
// original upstream configuration line and the corresponding error. err can be
|
||||
// nil.
|
||||
func collectErrResults(lines []string, err error) (results []*parseResult) {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, upstreamAddr := range upstreams {
|
||||
var res *upstreamResult
|
||||
if upstreamAddr != "#" {
|
||||
res = cv.parseUpstream(upstreamAddr, opts)
|
||||
} else if !isSpecific {
|
||||
res = &upstreamResult{
|
||||
err: errNotDomainSpecific,
|
||||
original: upstreamAddr,
|
||||
}
|
||||
} else {
|
||||
// limit is a maximum length for upstream configuration lines.
|
||||
const limit = 80
|
||||
|
||||
wrapper, ok := err.(errors.WrapperSlice)
|
||||
if !ok {
|
||||
log.Debug("dnsforward: configvalidator: unwrapping: %s", err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
errs := wrapper.Unwrap()
|
||||
results = make([]*parseResult, 0, len(errs))
|
||||
for i, e := range errs {
|
||||
var parseErr *proxy.ParseError
|
||||
if !errors.As(e, &parseErr) {
|
||||
log.Debug("dnsforward: configvalidator: inserting unexpected error %d: %s", i, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
res.isSpecific = isSpecific
|
||||
s = cv.insert(s, res)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// insert inserts r into slice in a sorted order, except duplicates. slice must
|
||||
// not be nil.
|
||||
func (cv *upstreamConfigValidator) insert(
|
||||
s []*upstreamResult,
|
||||
r *upstreamResult,
|
||||
) (result []*upstreamResult) {
|
||||
i, has := slices.BinarySearchFunc(s, r, (*upstreamResult).compare)
|
||||
if has {
|
||||
log.Debug("dnsforward: duplicate configuration %q", r.original)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
return slices.Insert(s, i, r)
|
||||
}
|
||||
|
||||
// parseUpstream parses addr and returns the result of parsing. It returns nil
|
||||
// if the specified server points at the default upstream server which is
|
||||
// validated separately.
|
||||
func (cv *upstreamConfigValidator) parseUpstream(
|
||||
addr string,
|
||||
opts *upstream.Options,
|
||||
) (r *upstreamResult) {
|
||||
// Check if the upstream has a valid protocol prefix.
|
||||
//
|
||||
// TODO(e.burkov): Validate the domain name.
|
||||
if proto, _, ok := strings.Cut(addr, "://"); ok {
|
||||
if !slices.Contains(protocols, proto) {
|
||||
return &upstreamResult{
|
||||
err: fmt.Errorf("bad protocol %q", proto),
|
||||
original: addr,
|
||||
}
|
||||
idx := parseErr.Idx
|
||||
line := []rune(lines[idx])
|
||||
if len(line) > limit {
|
||||
line = line[:limit]
|
||||
line[limit-1] = '…'
|
||||
}
|
||||
|
||||
results = append(results, &parseResult{
|
||||
original: string(line),
|
||||
err: parseErr,
|
||||
})
|
||||
}
|
||||
|
||||
ups, err := upstream.AddressToUpstream(addr, opts)
|
||||
return results
|
||||
}
|
||||
|
||||
return &upstreamResult{
|
||||
server: ups,
|
||||
err: err,
|
||||
original: addr,
|
||||
// insertConfResults parses conf and inserts the upstream result into results.
|
||||
// It can insert multiple results as well as none.
|
||||
func insertConfResults(conf *proxy.UpstreamConfig, results map[string]*upstreamResult) {
|
||||
insertListResults(conf.Upstreams, results, false)
|
||||
|
||||
for _, ups := range conf.DomainReservedUpstreams {
|
||||
insertListResults(ups, results, true)
|
||||
}
|
||||
|
||||
for _, ups := range conf.SpecifiedDomainUpstreams {
|
||||
insertListResults(ups, results, true)
|
||||
}
|
||||
}
|
||||
|
||||
// insertListResults constructs upstream results from the upstream list and
|
||||
// inserts them into results. It can insert multiple results as well as none.
|
||||
func insertListResults(ups []upstream.Upstream, results map[string]*upstreamResult, specific bool) {
|
||||
for _, u := range ups {
|
||||
addr := u.Address()
|
||||
_, ok := results[addr]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
|
||||
results[addr] = &upstreamResult{
|
||||
server: u,
|
||||
isSpecific: specific,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,35 +191,30 @@ func (cv *upstreamConfigValidator) check() {
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(cv.general) + len(cv.fallback) + len(cv.private))
|
||||
wg.Add(len(cv.generalUpstreamResults) +
|
||||
len(cv.fallbackUpstreamResults) +
|
||||
len(cv.privateUpstreamResults))
|
||||
|
||||
for _, res := range cv.general {
|
||||
go cv.checkSrv(res, wg, commonChecker)
|
||||
for _, res := range cv.generalUpstreamResults {
|
||||
go checkSrv(res, wg, commonChecker)
|
||||
}
|
||||
for _, res := range cv.fallback {
|
||||
go cv.checkSrv(res, wg, commonChecker)
|
||||
for _, res := range cv.fallbackUpstreamResults {
|
||||
go checkSrv(res, wg, commonChecker)
|
||||
}
|
||||
for _, res := range cv.private {
|
||||
go cv.checkSrv(res, wg, arpaChecker)
|
||||
for _, res := range cv.privateUpstreamResults {
|
||||
go checkSrv(res, wg, arpaChecker)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// checkSrv runs hc on the server from res, if any, and stores any occurred
|
||||
// error in res. wg is always marked done in the end. It used to be called in
|
||||
// a separate goroutine.
|
||||
func (cv *upstreamConfigValidator) checkSrv(
|
||||
res *upstreamResult,
|
||||
wg *sync.WaitGroup,
|
||||
hc *healthchecker,
|
||||
) {
|
||||
// error in res. wg is always marked done in the end. It is intended to be
|
||||
// used as a goroutine.
|
||||
func checkSrv(res *upstreamResult, wg *sync.WaitGroup, hc *healthchecker) {
|
||||
defer log.OnPanic(fmt.Sprintf("dnsforward: checking upstream %s", res.server.Address()))
|
||||
defer wg.Done()
|
||||
|
||||
if res.server == nil {
|
||||
return
|
||||
}
|
||||
|
||||
res.err = hc.check(res.server)
|
||||
if res.err != nil && res.isSpecific {
|
||||
res.err = domainSpecificTestError{Err: res.err}
|
||||
@@ -225,65 +224,126 @@ func (cv *upstreamConfigValidator) checkSrv(
|
||||
// close closes all the upstreams that were successfully parsed. It enriches
|
||||
// the results with deferred closing errors.
|
||||
func (cv *upstreamConfigValidator) close() {
|
||||
for _, slice := range [][]*upstreamResult{cv.general, cv.fallback, cv.private} {
|
||||
for _, r := range slice {
|
||||
if r.server != nil {
|
||||
r.err = errors.WithDeferred(r.err, r.server.Close())
|
||||
}
|
||||
all := []map[string]*upstreamResult{
|
||||
cv.generalUpstreamResults,
|
||||
cv.fallbackUpstreamResults,
|
||||
cv.privateUpstreamResults,
|
||||
}
|
||||
|
||||
for _, m := range all {
|
||||
for _, r := range m {
|
||||
r.err = errors.WithDeferred(r.err, r.server.Close())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sections of the upstream configuration according to the text label of the
|
||||
// localization.
|
||||
//
|
||||
// Keep in sync with client/src/__locales/en.json.
|
||||
//
|
||||
// TODO(s.chzhen): Refactor.
|
||||
const (
|
||||
generalTextLabel = "upstream_dns"
|
||||
fallbackTextLabel = "fallback_dns_title"
|
||||
privateTextLabel = "local_ptr_title"
|
||||
)
|
||||
|
||||
// status returns all the data collected during parsing, healthcheck, and
|
||||
// closing of the upstreams. The returned map is keyed by the original upstream
|
||||
// configuration piece and contains the corresponding error or "OK" if there was
|
||||
// no error.
|
||||
func (cv *upstreamConfigValidator) status() (results map[string]string) {
|
||||
result := map[string]string{}
|
||||
// Names of the upstream configuration sections for logging.
|
||||
const (
|
||||
generalSection = "general"
|
||||
fallbackSection = "fallback"
|
||||
privateSection = "private"
|
||||
)
|
||||
|
||||
for _, res := range cv.general {
|
||||
resultToStatus("general", res, result)
|
||||
results = map[string]string{}
|
||||
|
||||
for original, res := range cv.generalUpstreamResults {
|
||||
upstreamResultToStatus(generalSection, string(original), res, results)
|
||||
}
|
||||
for _, res := range cv.fallback {
|
||||
resultToStatus("fallback", res, result)
|
||||
for original, res := range cv.fallbackUpstreamResults {
|
||||
upstreamResultToStatus(fallbackSection, string(original), res, results)
|
||||
}
|
||||
for _, res := range cv.private {
|
||||
resultToStatus("private", res, result)
|
||||
for original, res := range cv.privateUpstreamResults {
|
||||
upstreamResultToStatus(privateSection, string(original), res, results)
|
||||
}
|
||||
|
||||
return result
|
||||
parseResultToStatus(generalTextLabel, generalSection, cv.generalParseResults, results)
|
||||
parseResultToStatus(fallbackTextLabel, fallbackSection, cv.fallbackParseResults, results)
|
||||
parseResultToStatus(privateTextLabel, privateSection, cv.privateParseResults, results)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// resultToStatus puts "OK" or an error message from res into resMap. section
|
||||
// is the name of the upstream configuration section, i.e. "general",
|
||||
// upstreamResultToStatus puts "OK" or an error message from res into resMap.
|
||||
// section is the name of the upstream configuration section, i.e. "general",
|
||||
// "fallback", or "private", and only used for logging.
|
||||
//
|
||||
// TODO(e.burkov): Currently, the HTTP handler expects that all the results are
|
||||
// put together in a single map, which may lead to collisions, see AG-27539.
|
||||
// Improve the results compilation.
|
||||
func resultToStatus(section string, res *upstreamResult, resMap map[string]string) {
|
||||
func upstreamResultToStatus(
|
||||
section string,
|
||||
original string,
|
||||
res *upstreamResult,
|
||||
resMap map[string]string,
|
||||
) {
|
||||
val := "OK"
|
||||
if res.err != nil {
|
||||
val = res.err.Error()
|
||||
}
|
||||
|
||||
prevVal := resMap[res.original]
|
||||
prevVal := resMap[original]
|
||||
switch prevVal {
|
||||
case "":
|
||||
resMap[res.original] = val
|
||||
resMap[original] = val
|
||||
case val:
|
||||
log.Debug("dnsforward: duplicating %s config line %q", section, res.original)
|
||||
log.Debug("dnsforward: duplicating %s config line %q", section, original)
|
||||
default:
|
||||
log.Debug(
|
||||
"dnsforward: warning: %s config line %q (%v) had different result %v",
|
||||
section,
|
||||
val,
|
||||
res.original,
|
||||
original,
|
||||
prevVal,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// parseResultToStatus puts parsing error messages from results into resMap.
|
||||
// section is the name of the upstream configuration section, i.e. "general",
|
||||
// "fallback", or "private", and only used for logging.
|
||||
//
|
||||
// Parsing error message has the following format:
|
||||
//
|
||||
// sectionTextLabel line: parsing error
|
||||
//
|
||||
// Where sectionTextLabel is a section text label of a localization and line is
|
||||
// a line number.
|
||||
func parseResultToStatus(
|
||||
textLabel string,
|
||||
section string,
|
||||
results []*parseResult,
|
||||
resMap map[string]string,
|
||||
) {
|
||||
for _, res := range results {
|
||||
original := res.original
|
||||
_, ok := resMap[original]
|
||||
if ok {
|
||||
log.Debug("dnsforward: duplicating %s parsing error %q", section, original)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
resMap[original] = fmt.Sprintf("%s %d: parsing error", textLabel, res.err.Idx+1)
|
||||
}
|
||||
}
|
||||
|
||||
// domainSpecificTestError is a wrapper for errors returned by checkDNS to mark
|
||||
// the tested upstream domain-specific and therefore consider its errors
|
||||
// non-critical.
|
||||
@@ -342,7 +402,7 @@ func (h *healthchecker) check(u upstream.Upstream) (err error) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't communicate with upstream: %w", err)
|
||||
} else if h.ansEmpty && len(reply.Answer) > 0 {
|
||||
return errWrongResponse
|
||||
return errors.Error("wrong response")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"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"
|
||||
@@ -101,21 +100,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,13 +249,16 @@ 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)
|
||||
resp.Answer = []dns.RR{localRR}
|
||||
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{
|
||||
Answer: []dns.RR{localRR},
|
||||
}).SetReply(m)
|
||||
|
||||
return resp, nil
|
||||
require.NoError(t, w.WriteMsg(resp))
|
||||
})
|
||||
localUpsAddr := aghtest.StartLocalhostUpstream(t, localUpsHdlr).String()
|
||||
|
||||
client := &dns.Client{
|
||||
Net: "tcp",
|
||||
@@ -279,25 +266,44 @@ func TestServer_HandleDNSRequest_dns64(t *testing.T) {
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newUps(tc.upsAns)}
|
||||
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{
|
||||
Answer: answer[sectionAnswer],
|
||||
Ns: answer[sectionAuthority],
|
||||
Extra: answer[sectionAdditional],
|
||||
}).SetReply(req)
|
||||
|
||||
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)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -30,7 +31,6 @@ import (
|
||||
"github.com/AdguardTeam/golibs/netutil/sysresolv"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// DefaultTimeout is the default upstream timeout
|
||||
@@ -308,13 +308,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 +322,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
|
||||
@@ -464,7 +464,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
|
||||
}
|
||||
@@ -517,35 +518,56 @@ func (s *Server) prepareLocalResolvers(
|
||||
return uc, nil
|
||||
}
|
||||
|
||||
// LocalResolversError is an error type for errors during local resolvers setup.
|
||||
// This is only needed to distinguish these errors from errors returned by
|
||||
// creating the proxy.
|
||||
type LocalResolversError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ error = (*LocalResolversError)(nil)
|
||||
|
||||
// Error implements the error interface for *LocalResolversError.
|
||||
func (err *LocalResolversError) Error() (s string) {
|
||||
return fmt.Sprintf("creating local resolvers: %s", err.Err)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ errors.Wrapper = (*LocalResolversError)(nil)
|
||||
|
||||
// Unwrap implements the [errors.Wrapper] interface for *LocalResolversError.
|
||||
func (err *LocalResolversError) Unwrap() error {
|
||||
return err.Err
|
||||
}
|
||||
|
||||
// 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)
|
||||
// It assumes s.serverLock is locked or s not running. It returns the upstream
|
||||
// configuration used for private PTR resolving, or nil if it's disabled. Note,
|
||||
// that it's safe to put nil into [proxy.Config.PrivateRDNSUpstreamConfig].
|
||||
func (s *Server) setupLocalResolvers(boot upstream.Resolver) (uc *proxy.UpstreamConfig, err error) {
|
||||
if !s.conf.UsePrivateRDNS {
|
||||
// It's safe to put nil into [proxy.Config.PrivateRDNSUpstreamConfig].
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
uc, err = s.prepareLocalResolvers(boot)
|
||||
if err != nil {
|
||||
// Don't wrap the error because it's informative enough as is.
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.localResolvers = &proxy.Proxy{
|
||||
Config: proxy.Config{
|
||||
UpstreamConfig: uc,
|
||||
},
|
||||
}
|
||||
|
||||
err = s.localResolvers.Init()
|
||||
localResolvers, err := proxy.New(&proxy.Config{
|
||||
UpstreamConfig: uc,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing proxy: %w", err)
|
||||
return nil, &LocalResolversError{Err: err}
|
||||
}
|
||||
|
||||
s.localResolvers = localResolvers
|
||||
|
||||
// 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
|
||||
return uc, nil
|
||||
}
|
||||
|
||||
// Prepare initializes parameters of s using data from conf. conf must not be
|
||||
@@ -586,21 +608,24 @@ 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.PrivateRDNSUpstreamConfig, err = s.setupLocalResolvers(boot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up resolvers: %w", err)
|
||||
}
|
||||
|
||||
err = s.setupFallbackDNS()
|
||||
proxyConfig.Fallbacks, err = s.setupFallbackDNS()
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up fallback dns servers: %w", err)
|
||||
}
|
||||
|
||||
dnsProxy, err := proxy.New(proxyConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating proxy: %w", err)
|
||||
}
|
||||
|
||||
s.dnsProxy = dnsProxy
|
||||
|
||||
s.recDetector.clear()
|
||||
|
||||
s.setupAddrProc()
|
||||
@@ -643,26 +668,25 @@ func (s *Server) prepareInternalDNS() (boot upstream.Resolver, err error) {
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -730,19 +754,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,
|
||||
}
|
||||
s.internalProxy, err = proxy.New(conf)
|
||||
|
||||
err = p.Init()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.internalProxy = p
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// Stop stops the DNS server.
|
||||
@@ -761,14 +775,17 @@ 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")
|
||||
if s.localResolvers != nil {
|
||||
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 +858,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)
|
||||
|
||||
@@ -5,9 +5,11 @@ import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
@@ -63,8 +65,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 +73,6 @@ func createTestServer(
|
||||
t *testing.T,
|
||||
filterConf *filtering.Config,
|
||||
forwardConf ServerConfig,
|
||||
localUps upstream.Upstream,
|
||||
) (s *Server) {
|
||||
t.Helper()
|
||||
|
||||
@@ -82,7 +82,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 +106,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 +169,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
|
||||
@@ -310,7 +298,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 +398,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 +478,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)
|
||||
|
||||
@@ -545,7 +533,7 @@ 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()
|
||||
@@ -628,7 +616,7 @@ func TestInvalidRequest(t *testing.T) {
|
||||
},
|
||||
},
|
||||
ServePlainDNS: true,
|
||||
}, nil)
|
||||
})
|
||||
startDeferStop(t, s)
|
||||
|
||||
addr := s.dnsProxy.Addr(proxy.ProtoUDP).String()
|
||||
@@ -662,7 +650,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,7 +686,7 @@ 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)
|
||||
@@ -773,7 +761,7 @@ func TestBlockCNAMEProtectionEnabled(t *testing.T) {
|
||||
},
|
||||
},
|
||||
ServePlainDNS: true,
|
||||
}, nil)
|
||||
})
|
||||
testUpstm := &aghtest.Upstream{
|
||||
CName: testCNAMEs,
|
||||
IPv4: testIPv4,
|
||||
@@ -811,7 +799,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 +874,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 +921,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 +1042,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 +1090,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)
|
||||
|
||||
@@ -1330,6 +1318,7 @@ func TestPTRResponseFromHosts(t *testing.T) {
|
||||
|
||||
var eventsCalledCounter uint32
|
||||
hc, err := aghnet.NewHostsContainer(testFS, &aghtest.FSWatcher{
|
||||
OnStart: func() (_ error) { panic("not implemented") },
|
||||
OnEvents: func() (e <-chan struct{}) {
|
||||
assert.Equal(t, uint32(1), atomic.AddUint32(&eventsCalledCounter, 1))
|
||||
|
||||
@@ -1481,6 +1470,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())
|
||||
@@ -1489,72 +1480,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 := aghalg.Coalesce(
|
||||
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 := aghalg.Coalesce(
|
||||
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
|
||||
@@ -1569,35 +1561,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,
|
||||
}, {
|
||||
@@ -1611,23 +1603,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)
|
||||
@@ -1637,8 +1634,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)
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
@@ -12,7 +13,6 @@ import (
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/urlfilter/rules"
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// beforeRequestHandler is the handler that is called before any other
|
||||
|
||||
@@ -6,16 +6,17 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"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"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// jsonDNSConfig is the JSON representation of the DNS server configuration.
|
||||
@@ -294,7 +295,7 @@ func (req *jsonDNSConfig) checkFallbacks() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = ValidateUpstreams(*req.Fallbacks)
|
||||
_, err = proxy.ParseUpstreamsConfig(*req.Fallbacks, &upstream.Options{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("fallback servers: %w", err)
|
||||
}
|
||||
@@ -344,7 +345,7 @@ func (req *jsonDNSConfig) validate(privateNets netutil.SubnetSet) (err error) {
|
||||
// validateUpstreamDNSServers returns an error if any field of req is invalid.
|
||||
func (req *jsonDNSConfig) validateUpstreamDNSServers(privateNets netutil.SubnetSet) (err error) {
|
||||
if req.Upstreams != nil {
|
||||
err = ValidateUpstreams(*req.Upstreams)
|
||||
_, err = proxy.ParseUpstreamsConfig(*req.Upstreams, &upstream.Options{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("upstream servers: %w", err)
|
||||
}
|
||||
@@ -473,8 +474,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 {
|
||||
@@ -580,9 +579,6 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
req.Upstreams = stringutil.FilterOut(req.Upstreams, IsCommentOrEmpty)
|
||||
req.FallbackDNS = stringutil.FilterOut(req.FallbackDNS, IsCommentOrEmpty)
|
||||
req.PrivateUpstreams = stringutil.FilterOut(req.PrivateUpstreams, IsCommentOrEmpty)
|
||||
req.BootstrapDNS = stringutil.FilterOut(req.BootstrapDNS, IsCommentOrEmpty)
|
||||
|
||||
opts := &upstream.Options{
|
||||
|
||||
@@ -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
|
||||
@@ -223,8 +227,9 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
||||
wantSet: "",
|
||||
}, {
|
||||
name: "upstream_dns_bad",
|
||||
wantSet: `validating dns config: ` +
|
||||
`upstream servers: validating upstream "!!!": not an ip:port`,
|
||||
wantSet: `validating dns config: upstream servers: parsing error at index 0: ` +
|
||||
`cannot prepare the upstream: invalid address !!!: bad hostname "!!!": ` +
|
||||
`bad top-level domain name label "!!!": bad top-level domain name label rune '!'`,
|
||||
}, {
|
||||
name: "bootstraps_bad",
|
||||
wantSet: `validating dns config: checking bootstrap a: not a bootstrap: ParseAddr("a"): ` +
|
||||
@@ -313,98 +318,6 @@ func TestIsCommentOrEmpty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateUpstreams(t *testing.T) {
|
||||
const sdnsStamp = `sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_J` +
|
||||
`S3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczE` +
|
||||
`uYWRndWFyZC5jb20`
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
wantErr string
|
||||
set []string
|
||||
}{{
|
||||
name: "empty",
|
||||
wantErr: ``,
|
||||
set: nil,
|
||||
}, {
|
||||
name: "comment",
|
||||
wantErr: ``,
|
||||
set: []string{"# comment"},
|
||||
}, {
|
||||
name: "no_default",
|
||||
wantErr: `no default upstreams specified`,
|
||||
set: []string{
|
||||
"[/host.com/]1.1.1.1",
|
||||
"[//]tls://1.1.1.1",
|
||||
"[/www.host.com/]#",
|
||||
"[/host.com/google.com/]8.8.8.8",
|
||||
"[/host/]" + sdnsStamp,
|
||||
},
|
||||
}, {
|
||||
name: "with_default",
|
||||
wantErr: ``,
|
||||
set: []string{
|
||||
"[/host.com/]1.1.1.1",
|
||||
"[//]tls://1.1.1.1",
|
||||
"[/www.host.com/]#",
|
||||
"[/host.com/google.com/]8.8.8.8",
|
||||
"[/host/]" + sdnsStamp,
|
||||
"8.8.8.8",
|
||||
},
|
||||
}, {
|
||||
name: "invalid",
|
||||
wantErr: `validating upstream "dhcp://fake.dns": bad protocol "dhcp"`,
|
||||
set: []string{"dhcp://fake.dns"},
|
||||
}, {
|
||||
name: "invalid",
|
||||
wantErr: `validating upstream "1.2.3.4.5": not an ip:port`,
|
||||
set: []string{"1.2.3.4.5"},
|
||||
}, {
|
||||
name: "invalid",
|
||||
wantErr: `validating upstream "123.3.7m": not an ip:port`,
|
||||
set: []string{"123.3.7m"},
|
||||
}, {
|
||||
name: "invalid",
|
||||
wantErr: `splitting upstream line "[/host.com]tls://dns.adguard.com": ` +
|
||||
`missing separator`,
|
||||
set: []string{"[/host.com]tls://dns.adguard.com"},
|
||||
}, {
|
||||
name: "invalid",
|
||||
wantErr: `validating upstream "[host.ru]#": not an ip:port`,
|
||||
set: []string{"[host.ru]#"},
|
||||
}, {
|
||||
name: "valid_default",
|
||||
wantErr: ``,
|
||||
set: []string{
|
||||
"1.1.1.1",
|
||||
"tls://1.1.1.1",
|
||||
"https://dns.adguard.com/dns-query",
|
||||
sdnsStamp,
|
||||
"udp://dns.google",
|
||||
"udp://8.8.8.8",
|
||||
"[/host.com/]1.1.1.1",
|
||||
"[//]tls://1.1.1.1",
|
||||
"[/www.host.com/]#",
|
||||
"[/host.com/google.com/]8.8.8.8",
|
||||
"[/host/]" + sdnsStamp,
|
||||
"[/пример.рф/]8.8.8.8",
|
||||
},
|
||||
}, {
|
||||
name: "bad_domain",
|
||||
wantErr: `splitting upstream line "[/!/]8.8.8.8": domain at index 0: ` +
|
||||
`bad domain name "!": bad top-level domain name label "!": ` +
|
||||
`bad top-level domain name label rune '!'`,
|
||||
set: []string{"[/!/]8.8.8.8"},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := ValidateUpstreams(tc.set)
|
||||
testutil.AssertErrorMsg(t, tc.wantErr, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateUpstreamsPrivate(t *testing.T) {
|
||||
ss := netutil.SubnetSetFunc(netutil.IsLocallyServed)
|
||||
|
||||
@@ -509,6 +422,7 @@ func TestServer_HandleTestUpstreamDNS(t *testing.T) {
|
||||
},
|
||||
},
|
||||
&aghtest.FSWatcher{
|
||||
OnStart: func() (_ error) { panic("not implemented") },
|
||||
OnEvents: func() (e <-chan struct{}) { return nil },
|
||||
OnAdd: func(_ string) (err error) { return nil },
|
||||
OnClose: func() (err error) { return nil },
|
||||
@@ -529,7 +443,7 @@ func TestServer_HandleTestUpstreamDNS(t *testing.T) {
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
},
|
||||
ServePlainDNS: true,
|
||||
}, nil)
|
||||
})
|
||||
srv.etcHosts = upstream.NewHostsResolver(hc)
|
||||
startDeferStop(t, srv)
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ package dnsforward
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"slices"
|
||||
|
||||
"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"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// makeResponse creates a DNS response by req and sets necessary flags. It also
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"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/AdguardTeam/urlfilter/rules"
|
||||
@@ -87,7 +86,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{
|
||||
@@ -188,7 +187,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 +247,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 +258,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 +278,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 +286,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 +294,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 +302,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 +310,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,41 +379,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),
|
||||
@@ -680,13 +666,16 @@ func TestServer_ProcessRestrictLocal(t *testing.T) {
|
||||
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 := aghalg.Coalesce(
|
||||
aghtest.MatchedResponse(req, dns.TypePTR, extPTRQuestion, extPTRAnswer),
|
||||
aghtest.MatchedResponse(req, dns.TypePTR, intPTRQuestion, intPTRAnswer),
|
||||
new(dns.Msg).SetRcode(req, dns.RcodeNameError),
|
||||
), nil
|
||||
)
|
||||
|
||||
require.NoError(testutil.PanicT{}, w.WriteMsg(resp))
|
||||
})
|
||||
localUpsAddr := aghtest.StartLocalhostUpstream(t, localUpsHdlr).String()
|
||||
|
||||
s := createTestServer(t, &filtering.Config{
|
||||
BlockingMode: filtering.BlockingModeDefault,
|
||||
@@ -696,12 +685,14 @@ 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 {
|
||||
@@ -764,6 +755,16 @@ func TestServer_ProcessLocalPTR_usingResolvers(t *testing.T) {
|
||||
const locDomain = "some.local."
|
||||
const reqAddr = "1.1.168.192.in-addr.arpa."
|
||||
|
||||
localUpsHdlr := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
resp := aghalg.Coalesce(
|
||||
aghtest.MatchedResponse(req, dns.TypePTR, reqAddr, locDomain),
|
||||
new(dns.Msg).SetRcode(req, dns.RcodeNameError),
|
||||
)
|
||||
|
||||
require.NoError(testutil.PanicT{}, w.WriteMsg(resp))
|
||||
})
|
||||
localUpsAddr := aghtest.StartLocalhostUpstream(t, localUpsHdlr).String()
|
||||
|
||||
s := createTestServer(
|
||||
t,
|
||||
&filtering.Config{
|
||||
@@ -776,14 +777,10 @@ func TestServer_ProcessLocalPTR_usingResolvers(t *testing.T) {
|
||||
UpstreamMode: UpstreamModeLoadBalance,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
},
|
||||
ServePlainDNS: true,
|
||||
UsePrivateRDNS: true,
|
||||
LocalPTRResolvers: []string{localUpsAddr},
|
||||
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
|
||||
}),
|
||||
)
|
||||
|
||||
var proxyCtx *proxy.DNSContext
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestGenAnswerHTTPS_andSVCB(t *testing.T) {
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
},
|
||||
ServePlainDNS: true,
|
||||
}, nil)
|
||||
})
|
||||
|
||||
req := &dns.Msg{
|
||||
Question: []dns.Question{{
|
||||
|
||||
@@ -2,10 +2,9 @@ package dnsforward
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
@@ -16,29 +15,6 @@ import (
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
// errNotDomainSpecific is returned when the upstream should be
|
||||
// domain-specific, but isn't.
|
||||
errNotDomainSpecific errors.Error = "not a domain-specific upstream"
|
||||
|
||||
// errMissingSeparator is returned when the domain-specific part of the
|
||||
// upstream configuration line isn't closed.
|
||||
errMissingSeparator errors.Error = "missing separator"
|
||||
|
||||
// errDupSeparator is returned when the domain-specific part of the upstream
|
||||
// configuration line contains more than one ending separator.
|
||||
errDupSeparator errors.Error = "duplicated separator"
|
||||
|
||||
// errNoDefaultUpstreams is returned when there are no default upstreams
|
||||
// specified in the upstream configuration.
|
||||
errNoDefaultUpstreams errors.Error = "no default upstreams specified"
|
||||
|
||||
// errWrongResponse is returned when the checked upstream replies in an
|
||||
// unexpected way.
|
||||
errWrongResponse errors.Error = "wrong response"
|
||||
)
|
||||
|
||||
// loadUpstreams parses upstream DNS servers from the configured file or from
|
||||
@@ -199,84 +175,12 @@ func IsCommentOrEmpty(s string) (ok bool) {
|
||||
return len(s) == 0 || s[0] == '#'
|
||||
}
|
||||
|
||||
// newUpstreamConfig validates upstreams and returns an appropriate upstream
|
||||
// configuration or nil if it can't be built.
|
||||
//
|
||||
// TODO(e.burkov): Perhaps proxy.ParseUpstreamsConfig should validate upstreams
|
||||
// slice already so that this function may be considered useless.
|
||||
func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err error) {
|
||||
// No need to validate comments and empty lines.
|
||||
upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty)
|
||||
if len(upstreams) == 0 {
|
||||
// Consider this case valid since it means the default server should be
|
||||
// used.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
err = validateUpstreamConfig(upstreams)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf, err = proxy.ParseUpstreamsConfig(
|
||||
upstreams,
|
||||
&upstream.Options{
|
||||
Bootstrap: net.DefaultResolver,
|
||||
Timeout: DefaultTimeout,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return nil, err
|
||||
} else if len(conf.Upstreams) == 0 {
|
||||
return nil, errNoDefaultUpstreams
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// validateUpstreamConfig validates each upstream from the upstream
|
||||
// configuration and returns an error if any upstream is invalid.
|
||||
//
|
||||
// TODO(e.burkov): Merge with [upstreamConfigValidator] somehow.
|
||||
func validateUpstreamConfig(conf []string) (err error) {
|
||||
for _, u := range conf {
|
||||
var ups []string
|
||||
var isSpecific bool
|
||||
ups, isSpecific, err = splitUpstreamLine(u)
|
||||
if err != nil {
|
||||
// Don't wrap the error since it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
for _, addr := range ups {
|
||||
_, err = validateUpstream(addr, isSpecific)
|
||||
if err != nil {
|
||||
return fmt.Errorf("validating upstream %q: %w", addr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateUpstreams validates each upstream and returns an error if any
|
||||
// upstream is invalid or if there are no default upstreams specified.
|
||||
//
|
||||
// TODO(e.burkov): Merge with [upstreamConfigValidator] somehow.
|
||||
func ValidateUpstreams(upstreams []string) (err error) {
|
||||
_, err = newUpstreamConfig(upstreams)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// 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 := newUpstreamConfig(upstreams)
|
||||
conf, err := proxy.ParseUpstreamsConfig(upstreams, &upstream.Options{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating config: %w", err)
|
||||
}
|
||||
@@ -308,66 +212,3 @@ func ValidateUpstreamsPrivate(upstreams []string, privateNets netutil.SubnetSet)
|
||||
|
||||
return errors.Annotate(errors.Join(errs...), "checking domain-specific upstreams: %w")
|
||||
}
|
||||
|
||||
// protocols are the supported URL schemes for upstreams.
|
||||
var protocols = []string{"h3", "https", "quic", "sdns", "tcp", "tls", "udp"}
|
||||
|
||||
// validateUpstream returns an error if u alongside with domains is not a valid
|
||||
// upstream configuration. useDefault is true if the upstream is
|
||||
// domain-specific and is configured to point at the default upstream server
|
||||
// which is validated separately. The upstream is considered domain-specific
|
||||
// only if domains is at least not nil.
|
||||
func validateUpstream(u string, isSpecific bool) (useDefault bool, err error) {
|
||||
// The special server address '#' means that default server must be used.
|
||||
if useDefault = u == "#" && isSpecific; useDefault {
|
||||
return useDefault, nil
|
||||
}
|
||||
|
||||
// Check if the upstream has a valid protocol prefix.
|
||||
//
|
||||
// TODO(e.burkov): Validate the domain name.
|
||||
if proto, _, ok := strings.Cut(u, "://"); ok {
|
||||
if !slices.Contains(protocols, proto) {
|
||||
return false, fmt.Errorf("bad protocol %q", proto)
|
||||
}
|
||||
} else if _, err = netip.ParseAddr(u); err == nil {
|
||||
return false, nil
|
||||
} else if _, err = netip.ParseAddrPort(u); err == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
// splitUpstreamLine returns the upstreams and the specified domains. domains
|
||||
// is nil when the upstream is not domains-specific. Otherwise it may also be
|
||||
// empty.
|
||||
func splitUpstreamLine(upstreamStr string) (upstreams []string, isSpecific bool, err error) {
|
||||
if !strings.HasPrefix(upstreamStr, "[/") {
|
||||
return []string{upstreamStr}, false, nil
|
||||
}
|
||||
|
||||
defer func() { err = errors.Annotate(err, "splitting upstream line %q: %w", upstreamStr) }()
|
||||
|
||||
doms, ups, found := strings.Cut(upstreamStr[2:], "/]")
|
||||
if !found {
|
||||
return nil, false, errMissingSeparator
|
||||
} else if strings.Contains(ups, "/]") {
|
||||
return nil, false, errDupSeparator
|
||||
}
|
||||
|
||||
for i, host := range strings.Split(doms, "/") {
|
||||
if host == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
err = netutil.ValidateDomainName(strings.TrimPrefix(host, "*."))
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("domain at index %d: %w", i, err)
|
||||
}
|
||||
|
||||
isSpecific = true
|
||||
}
|
||||
|
||||
return strings.Fields(ups), isSpecific, nil
|
||||
}
|
||||
|
||||
@@ -100,8 +100,7 @@ func TestUpstreamConfigValidator(t *testing.T) {
|
||||
name: "bad_specification",
|
||||
general: []string{"[/domain.example/]/]1.2.3.4"},
|
||||
want: map[string]string{
|
||||
"[/domain.example/]/]1.2.3.4": `splitting upstream line ` +
|
||||
`"[/domain.example/]/]1.2.3.4": duplicated separator`,
|
||||
"[/domain.example/]/]1.2.3.4": generalTextLabel + " 1: parsing error",
|
||||
},
|
||||
}, {
|
||||
name: "all_different",
|
||||
@@ -120,23 +119,9 @@ func TestUpstreamConfigValidator(t *testing.T) {
|
||||
fallback: []string{"[/example/" + goodUps},
|
||||
private: []string{"[/example//bad.123/]" + goodUps},
|
||||
want: map[string]string{
|
||||
`[/example/]/]` + goodUps: `splitting upstream line ` +
|
||||
`"[/example/]/]` + goodUps + `": duplicated separator`,
|
||||
`[/example/` + goodUps: `splitting upstream line ` +
|
||||
`"[/example/` + goodUps + `": missing separator`,
|
||||
`[/example//bad.123/]` + goodUps: `splitting upstream line ` +
|
||||
`"[/example//bad.123/]` + goodUps + `": domain at index 2: ` +
|
||||
`bad domain name "bad.123": ` +
|
||||
`bad top-level domain name label "123": all octets are numeric`,
|
||||
},
|
||||
}, {
|
||||
name: "non-specific_default",
|
||||
general: []string{
|
||||
"#",
|
||||
"[/example/]#",
|
||||
},
|
||||
want: map[string]string{
|
||||
"#": "not a domain-specific upstream",
|
||||
"[/example/]/]" + goodUps: generalTextLabel + " 1: parsing error",
|
||||
"[/example/" + goodUps: fallbackTextLabel + " 1: parsing error",
|
||||
"[/example//bad.123/]" + goodUps: privateTextLabel + " 1: parsing error",
|
||||
},
|
||||
}, {
|
||||
name: "bad_proto",
|
||||
@@ -144,7 +129,15 @@ func TestUpstreamConfigValidator(t *testing.T) {
|
||||
"bad://1.2.3.4",
|
||||
},
|
||||
want: map[string]string{
|
||||
"bad://1.2.3.4": `bad protocol "bad"`,
|
||||
"bad://1.2.3.4": generalTextLabel + " 1: parsing error",
|
||||
},
|
||||
}, {
|
||||
name: "truncated_line",
|
||||
general: []string{
|
||||
"This is a very long line. It will cause a parsing error and will be truncated here.",
|
||||
},
|
||||
want: map[string]string{
|
||||
"This is a very long line. It will cause a parsing error and will be truncated …": "upstream_dns 1: parsing error",
|
||||
},
|
||||
}}
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"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"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// serviceRules maps a service ID to its filtering 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)
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -6,27 +6,22 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// 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")),
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -19,30 +20,16 @@ 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"
|
||||
"github.com/AdguardTeam/urlfilter/rules"
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// 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
|
||||
@@ -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.
|
||||
@@ -637,7 +629,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 +697,7 @@ func matchBlockedServicesRules(
|
||||
|
||||
ruleText := rule.Text()
|
||||
res.Rules = []*ResultRule{{
|
||||
FilterListID: int64(rule.GetFilterListID()),
|
||||
FilterListID: rule.GetFilterListID(),
|
||||
Text: ruleText,
|
||||
}}
|
||||
|
||||
@@ -970,7 +962,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 +983,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 +1047,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 +1132,7 @@ func (d *DNSFilter) checkSafeBrowsing(
|
||||
res = Result{
|
||||
Rules: []*ResultRule{{
|
||||
Text: "adguard-malware-shavar",
|
||||
FilterListID: SafeBrowsingListID,
|
||||
FilterListID: rulelist.URLFilterIDSafeBrowsing,
|
||||
}},
|
||||
Reason: FilteredSafeBrowsing,
|
||||
IsFiltered: true,
|
||||
@@ -1171,7 +1164,7 @@ func (d *DNSFilter) checkParental(
|
||||
res = Result{
|
||||
Rules: []*ResultRule{{
|
||||
Text: "parental CATEGORY_BLACKLISTED",
|
||||
FilterListID: ParentalListID,
|
||||
FilterListID: rulelist.URLFilterIDParentalControl,
|
||||
}},
|
||||
Reason: FilteredParental,
|
||||
IsFiltered: true,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user