mirror of
https://github.com/AdguardTeam/AdGuardDNS.git
synced 2025-02-20 11:23:36 +08:00
Sync v2.0
This commit is contained in:
parent
4943bdc429
commit
b6a98906a5
27
.gitignore
vendored
27
.gitignore
vendored
@ -1,9 +1,20 @@
|
||||
.DS_Store
|
||||
.idea/
|
||||
bin
|
||||
# Please, DO NOT put your text editors' temporary files here. The more are
|
||||
# added, the harder it gets to maintain and manage projects' gitignores. Put
|
||||
# them into your global gitignore file instead.
|
||||
#
|
||||
# See https://stackoverflow.com/a/7335487/1892060.
|
||||
#
|
||||
# Only build, run, and test outputs here. Sorted. With negations at the
|
||||
# bottom to make sure they take effect.
|
||||
*.out
|
||||
*.test
|
||||
/bin/
|
||||
/filters/
|
||||
/test/
|
||||
/github-mirror/
|
||||
AdGuardDNS
|
||||
example.crt
|
||||
example.key
|
||||
tests/parental-all-domains.txt
|
||||
tests/safebrowsing-all-domains.txt
|
||||
tests/dnsdb.bin
|
||||
asn.mmdb
|
||||
config.yml
|
||||
country.mmdb
|
||||
dnsdb.bolt
|
||||
querylog.jsonl
|
||||
|
1236
CHANGELOG.md
Normal file
1236
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
@ -7,17 +7,15 @@
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@ -72,7 +60,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@ -617,58 +615,3 @@ reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
62
Corefile
62
Corefile
@ -1,62 +0,0 @@
|
||||
.:53, tls://.:853, https://.:443, quic://.:784 {
|
||||
tls tests/test.crt tests/test.key
|
||||
|
||||
ratelimit 50 10000 {
|
||||
whitelist 127.0.0.1
|
||||
}
|
||||
refuseany
|
||||
|
||||
dnsfilter {
|
||||
filter tests/dns.txt
|
||||
safebrowsing tests/sb.txt
|
||||
parental tests/parental.txt
|
||||
safesearch
|
||||
geoip tests/GeoIP2-Country-Test.mmdb
|
||||
}
|
||||
|
||||
file tests/dnscheck.txt dnscheck-default.adguard.com
|
||||
|
||||
lrucache 50000
|
||||
|
||||
upstream 8.8.8.8:53 {
|
||||
fallback 1.1.1.1:53
|
||||
}
|
||||
log
|
||||
|
||||
info {
|
||||
domain adguard.com
|
||||
type test
|
||||
protocol auto
|
||||
addr 176.103.130.135
|
||||
}
|
||||
|
||||
health 127.0.0.1:8181
|
||||
pprof 127.0.0.1:6053
|
||||
|
||||
prometheus 127.0.0.1:9153
|
||||
dnsdb 127.0.0.1:9154 tests/dnsdb.bin
|
||||
}
|
||||
|
||||
.:5333 {
|
||||
ratelimit 50 {
|
||||
whitelist 127.0.0.1
|
||||
}
|
||||
refuseany
|
||||
|
||||
dnsfilter {
|
||||
filter tests/dns.txt
|
||||
filter tests/sb.txt
|
||||
safebrowsing tests/sb.txt
|
||||
parental tests/parental.txt
|
||||
}
|
||||
|
||||
lrucache 50000
|
||||
|
||||
upstream 8.8.8.8:53 {
|
||||
fallback 1.1.1.1:53
|
||||
}
|
||||
log
|
||||
|
||||
prometheus 127.0.0.1:9153
|
||||
dnsdb 127.0.0.1:9154 tests/dnsdb.bin
|
||||
}
|
1
HACKING.md
Normal file
1
HACKING.md
Normal file
@ -0,0 +1 @@
|
||||
See Adguard Home [`HACKING.md`](https://github.com/AdguardTeam/AdGuardHome/blob/master/HACKING.md).
|
70
Makefile
Normal file
70
Makefile
Normal file
@ -0,0 +1,70 @@
|
||||
# Keep the Makefile POSIX-compliant. We currently allow hyphens in
|
||||
# target names, but that may change in the future.
|
||||
#
|
||||
# See https://pubs.opengroup.org/onlinepubs/9699919799/utilities/make.html.
|
||||
.POSIX:
|
||||
|
||||
# Don't name this macro "GO", because GNU Make apparenly makes it an
|
||||
# exported environment variable with the literal value of "${GO:-go}",
|
||||
# which is not what we need. Use a dot in the name to make sure that
|
||||
# users don't have an environment variable with the same name.
|
||||
#
|
||||
# See https://unix.stackexchange.com/q/646255/105635.
|
||||
GO.MACRO = $${GO:-go}
|
||||
GOPROXY = https://goproxy.cn|https://proxy.golang.org|direct
|
||||
GOAMD64 = v1
|
||||
|
||||
RACE = 0
|
||||
VERBOSE = 0
|
||||
|
||||
BRANCH = $$( git rev-parse --abbrev-ref HEAD )
|
||||
VERSION = 0
|
||||
REVISION = $$( git rev-parse --short HEAD )
|
||||
|
||||
ENV = env\
|
||||
BRANCH="$(BRANCH)"\
|
||||
GO="$(GO.MACRO)"\
|
||||
GOAMD64='$(GOAMD64)'\
|
||||
GOPROXY='$(GOPROXY)'\
|
||||
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
|
||||
RACE='$(RACE)'\
|
||||
REVISION="$(REVISION)"\
|
||||
VERBOSE='$(VERBOSE)'\
|
||||
VERSION="$(VERSION)"\
|
||||
|
||||
# Keep the line above blank.
|
||||
|
||||
# Keep this target first, so that a naked make invocation triggers
|
||||
# a full build.
|
||||
build: go-deps go-build
|
||||
|
||||
init: ; git config core.hooksPath ./scripts/hooks
|
||||
|
||||
test: go-test
|
||||
|
||||
go-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh
|
||||
go-deps: ; $(ENV) "$(SHELL)" ./scripts/make/go-deps.sh
|
||||
go-lint: ; $(ENV) "$(SHELL)" ./scripts/make/go-lint.sh
|
||||
go-test: ; $(ENV) RACE='1' "$(SHELL)" ./scripts/make/go-test.sh
|
||||
go-bench: ; $(ENV) "$(SHELL)" ./scripts/make/go-bench.sh
|
||||
go-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-tools.sh
|
||||
|
||||
go-gen:
|
||||
cd ./internal/agd/ && "$(GO.MACRO)" run ./country_generate.go
|
||||
cd ./internal/geoip/ && "$(GO.MACRO)" run ./asntops_generate.go
|
||||
|
||||
go-check: go-tools go-lint go-test
|
||||
|
||||
# A quick check to make sure that all operating systems relevant to the
|
||||
# development of the project can be typechecked and built successfully.
|
||||
go-os-check:
|
||||
env GOOS='darwin' "$(GO.MACRO)" vet ./internal/...
|
||||
env GOOS='linux' "$(GO.MACRO)" vet ./internal/...
|
||||
# Additionally, check the AdGuard Home OSs in the dnsserver module.
|
||||
env GOOS='freebsd' "$(GO.MACRO)" vet ./internal/dnsserver/...
|
||||
env GOOS='openbsd' "$(GO.MACRO)" vet ./internal/dnsserver/...
|
||||
env GOOS='windows' "$(GO.MACRO)" vet ./internal/dnsserver/...
|
||||
|
||||
txt-lint: ; $(ENV) "$(SHELL)" ./scripts/make/txt-lint.sh
|
||||
|
||||
sync-github: ; $(ENV) "$(SHELL)" ./scripts/make/github-sync.sh
|
125
README.md
125
README.md
@ -1,98 +1,99 @@
|
||||
|
||||
<p align="center">
|
||||
<img src="https://cdn.adguard.com/public/Adguard/Common/adguard_dns.svg" width="300px" alt="AdGuard Home" />
|
||||
</p>
|
||||
<h3 align="center">A new approach to privacy-oriented DNS</h3>
|
||||
<p align="center">
|
||||
Public DNS resolver that protects you from ad trackers
|
||||
</p>
|
||||
# AdGuard DNS
|
||||
|
||||
<p align="center">
|
||||
<br/>
|
||||
<div align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.adtidy.org/website/images/AdGuardDNS_light.svg">
|
||||
<img alt="AdGuard DNS" src="https://cdn.adtidy.org/website/images/AdGuardDNS_black.svg" width="300px"/>
|
||||
</picture>
|
||||
</div>
|
||||
<br/>
|
||||
<div align="center">
|
||||
<a href="https://adguard-dns.io/">AdGuard DNS Website</a> |
|
||||
<a href="https://reddit.com/r/Adguard">Reddit</a> |
|
||||
<a href="https://twitter.com/AdGuard">Twitter</a>
|
||||
<br /><br />
|
||||
</p>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://cdn.adguard.com/public/Adguard/Common/adguard_dns_map.png" width="800" />
|
||||
</p>
|
||||
AdGuard DNS is an alternative solution for tracker blocking, privacy protection,
|
||||
and parental control. Easy to set up and free to use, it provides a necessary
|
||||
minimum of best protection against online ads, trackers, and phishing, no matter
|
||||
what platform and device you use.
|
||||
|
||||
# AdGuard DNS
|
||||
|
||||
AdGuard DNS is an alternative solution for trackers blocking, privacy protection, and parental control. Easy to set up and free to use, it provides a necessary minimum of best protection against online ads, trackers, and phishing, no matter what platform and device you use.
|
||||
|
||||
## DNS Privacy
|
||||
|
||||
If you use regular client-server protocol, you are at risk of your DNS requests being intercepted and, subsequently, eavesdropped and/or altered. For instance, in the US the Senate voted to eliminate rules that restricted ISPs from selling their users' browsing data. Moreover, DNS is often used for censorship and surveillance purposes on the government level.
|
||||
If you use regular client-server protocol, you are at risk of your DNS requests
|
||||
being intercepted and, subsequently, eavesdropped and/or altered. For instance,
|
||||
in the US the Senate voted to eliminate rules that restricted ISPs from selling
|
||||
their users' browsing data. Moreover, DNS is often used for censorship and
|
||||
surveillance purposes on the government level.
|
||||
|
||||
All of this is possible due to the lack of encryption, and AdGuard DNS provides a solution. It supports all known DNS encryption protocols including `DNS-over-HTTPS`, `DNS-over-TLS`, and `DNSCrypt`.
|
||||
All of this is possible due to the lack of encryption, and AdGuard DNS provides
|
||||
a solution. It supports all known DNS encryption protocols including
|
||||
DNS-over-HTTPS, DNS-over-TLS, DNS-over-QUIC, and DNSCrypt.
|
||||
|
||||
On top of that, AdGuard DNS provides "no logs" [privacy policy](https://adguard.com/en/privacy/dns.html) which means we do not record logs of your browsing activity.
|
||||
On top of that, AdGuard DNS provides no-logs [privacy policy] which means we do
|
||||
not record logs of your browsing activity.
|
||||
|
||||
## Additional Features
|
||||
[privacy policy]: https://adguard-dns.io/privacy.html
|
||||
|
||||
* **Blocking trackers network-wide** with no additional software required. You can even set it up on your router to block ads on all devices connected to your home Wi-Fi network.
|
||||
* Protection from phishing and hazardous websites and malvertising (malicious ads).
|
||||
* Use the **Family protection** mode of AdGuard DNS to block access to all websites with adult content and enforce safe search in the browser, in addition to the regular perks of ad blocking and browsing security.
|
||||
|
||||
**Can AdGuard DNS replace a traditional blocker?**
|
||||
<br/>
|
||||
It depends. DNS-level blocking lacks the flexibility of the traditional ad blockers. For instance, there is no cosmetic pages processing. So in general, traditional blockers provide higher quality.
|
||||
|
||||
## Why is AdGuard DNS free? What’s the catch?
|
||||
## Basic Features
|
||||
|
||||
We use AdGuard DNS functionality as a part of other AdGuard software, most of which are distributed on a pay-to-use basis. We might also develop a paid version of AdGuard DNS based on the current one, more advanced and with more features.
|
||||
* **Blocking trackers network-wide** with no additional software required.
|
||||
You can even set it up on your router to block ads on all devices connected
|
||||
to your home Wi-Fi network.
|
||||
|
||||
## Usage
|
||||
* Protection from phishing and hazardous websites and malvertising (malicious
|
||||
ads).
|
||||
|
||||
Please note that encrypted DNS protocols aren't supported at an operating system level so right now it requires the installation of additional software.
|
||||
* Use the **Family protection** mode of AdGuard DNS to block access to all
|
||||
websites with adult content and enforce safe search in the browser, in
|
||||
addition to the regular perks of ad blocking and browsing security.
|
||||
|
||||
Here's a list of the software that could be used:
|
||||
### Can AdGuard DNS replace a traditional blocker?
|
||||
|
||||
* Android 9 supports DNS-over-TLS natively
|
||||
* [AdGuard for Android](https://adguard.com/en/adguard-android/overview.html) supports `DNSCrypt` in the stable version, and supports `DNS-over-HTTPS` in the [nightly update channel](https://adguard.com/beta.html)
|
||||
* [AdGuard for iOS Pro](https://adguard.com/en/adguard-ios-pro/overview.html) supports `DNSCrypt`
|
||||
* [Intra](https://getintra.org/) adds `DNS-over-HTTPS` support to Android
|
||||
* [Mozilla Firefox](https://www.mozilla.org/firefox/) supports `DNS-over-HTTPS`
|
||||
* [AdGuard Home](https://github.com/AdguardTeam/AdguardHome) supports `DNS-over-TLS` and `DNS-over-HTTPS`
|
||||
* A lot more implementation can be [found here](https://dnscrypt.info/implementations) and [here](https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Clients)
|
||||
It depends. DNS-level blocking lacks the flexibility of the traditional ad
|
||||
blockers. For instance, there is no cosmetic pages processing. So in general,
|
||||
traditional blockers provide higher quality.
|
||||
|
||||
### Regular DNS
|
||||
|
||||
* `94.140.14.14` or `94.140.15.15` for "Default";
|
||||
* `94.140.14.15` or `94.140.15.16` for "Family protection";
|
||||
* `94.140.14.140` or `94.140.14.141` for "Non-filtering".
|
||||
|
||||
### DNS-over-HTTPS
|
||||
## Personal DNS server
|
||||
|
||||
* Use `https://dns.adguard.com/dns-query` for "Default";
|
||||
* Use `https://dns-family.adguard.com/dns-query` for "Family protection" mode;
|
||||
* Use `https://dns-unfiltered.adguard.com/dns-query` for "Non-filtering" mode;
|
||||
<figure>
|
||||
<img alt="A screenshot of the AdGuard DNS dashboard" src="https://cdn.adguard.com/content/blog/articles/stats_en.png" width="800px"/>
|
||||
<figcaption>AdGuard DNS dashboard</figcaption>
|
||||
</figure>
|
||||
|
||||
### DNS-over-TLS
|
||||
You can sign up for a personal AdGuard DNS account and get access to the
|
||||
following features:
|
||||
|
||||
* Use `dns.adguard.com` string for "Default";
|
||||
* Use `dns-family.adguard.com` for "Family protection";
|
||||
* Use `dns-unfiltered.adguard.com` for "Non-filtering";
|
||||
* Manage devices and their settings in one place.
|
||||
|
||||
### DNSCrypt
|
||||
* Manage blocklists that are used to block ads.
|
||||
|
||||
"Default":
|
||||
`sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20`
|
||||
* View statistics on the DNS queries, companies, countries your devices try to
|
||||
connect to.
|
||||
|
||||
"Family protection":
|
||||
`sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMjo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ`
|
||||
* You can also maintain your own set of rules in the "User rules" section.
|
||||
AdGuard DNS provides a flexible [rules system].
|
||||
|
||||
"Non-filtering":
|
||||
`sdns://AQcAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzNjo1NDQzILXoRNa4Oj4-EmjraB--pw3jxfpo29aIFB2_LsBmstr6JTIuZG5zY3J5cHQudW5maWx0ZXJlZC5uczEuYWRndWFyZC5jb20`
|
||||
* AdGuard DNS also provides an [API] that can be used to integrate with it if
|
||||
you need that.
|
||||
|
||||
## Dependencies
|
||||
[rules system]: https://adguard-dns.io/kb/general/dns-filtering-syntax/
|
||||
[API]: https://adguard-dns.io/kb/private-dns/api/
|
||||
|
||||
AdGuard DNS shares a lot of code with [AdGuard Home](https://github.com/AdguardTeam/AdGuardHome) and uses pretty much [the same open source libraries](https://github.com/AdguardTeam/AdGuardHome#acknowledgments).
|
||||
|
||||
Additionally, AdGuard DNS is built on [CoreDNS](https://coredns.io/).
|
||||
|
||||
## Reporting issues
|
||||
## Software License
|
||||
|
||||
If you run into any problem or have a suggestion, head to [this page](https://github.com/AdguardTeam/AdGuardDNS/issues) and click on the New issue button.
|
||||
Copyright (C) 2022 AdGuard Software Ltd.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, version 3.
|
||||
|
@ -1,11 +0,0 @@
|
||||
## Patches
|
||||
|
||||
Changes that we applied to the dependencies.
|
||||
|
||||
1. Patched version of CoreDNS:
|
||||
* QUIC support: https://github.com/ameshkov/coredns/commit/f68e85dc5881503c2a0acd5b79ab45a393f3c51c
|
||||
* Always compress DNS responses: https://github.com/ameshkov/coredns/commit/0c4bc69162ac07aaf85504ca65d14c9ee7a6be74
|
||||
|
||||
2. "health" plugin fork
|
||||
|
||||
Use "/health-check" instead of "/health"
|
328
config.dist.yml
Normal file
328
config.dist.yml
Normal file
@ -0,0 +1,328 @@
|
||||
# See README.md for a full documentation of the configuration file, its types
|
||||
# and values.
|
||||
|
||||
# Rate limiting configuration. It controls how we should mitigate DNS
|
||||
# amplification attacks.
|
||||
ratelimit:
|
||||
# Flag to refuse ANY type request.
|
||||
refuseany: true
|
||||
# If response is larger than this, it is counted as several responses.
|
||||
response_size_estimate: 1KB
|
||||
# Rate of requests per second for one subnet.
|
||||
rps: 30
|
||||
# The time during which to count the number of times a client has hit the
|
||||
# rate limit for a back off.
|
||||
#
|
||||
# TODO(a.garipov): Rename to "backoff_period" along with others.
|
||||
back_off_period: 10m
|
||||
# How many times a client hits the rate limit before being held in the back
|
||||
# off.
|
||||
back_off_count: 1000
|
||||
# How much a client that has hit the rate limit too often stays in the back
|
||||
# off.
|
||||
back_off_duration: 30m
|
||||
# The lengths of the subnet prefixes used to calculate rate limiter bucket
|
||||
# keys for IPv4 and IPv6 addresses correspondingly.
|
||||
ipv4_subnet_key_len: 24
|
||||
ipv6_subnet_key_len: 48
|
||||
|
||||
# Configuration for the allowlist.
|
||||
allowlist:
|
||||
# Lists of CIDRs or IPs ratelimit should be disabled for.
|
||||
list:
|
||||
- '127.0.0.1'
|
||||
- '127.0.0.1/24'
|
||||
# Time between two updates of allow list.
|
||||
refresh_interval: 1h
|
||||
|
||||
# DNS cache configuration.
|
||||
cache:
|
||||
# The type of cache to use. Can be 'simple' (a simple LRU cache) or 'ecs'
|
||||
# (a ECS-aware LRU cache). If set to 'ecs', ecs_size must be greater than
|
||||
# zero.
|
||||
type: 'simple'
|
||||
# The total number of items in the cache for hostnames with no ECS support.
|
||||
size: 10000
|
||||
# The total number of items in the cache for hostnames with ECS support.
|
||||
ecs_size: 10000
|
||||
|
||||
# DNS upstream configuration.
|
||||
upstream:
|
||||
server: '127.0.0.9:53'
|
||||
timeout: 2s
|
||||
fallback:
|
||||
- 1.1.1.1:53
|
||||
- 8.8.8.8:53
|
||||
healthcheck:
|
||||
enabled: true
|
||||
interval: 2s
|
||||
timeout: 1s
|
||||
backoff_duration: 30s
|
||||
domain_template: '${RANDOM}.neverssl.com'
|
||||
|
||||
# Common DNS HTTP backend service configuration.
|
||||
backend:
|
||||
# Timeout for all outgoing backend HTTP requests. Set to `0s` to disable
|
||||
# timeouts.
|
||||
timeout: 10s
|
||||
# How often AdGuard DNS checks the backend for data updates.
|
||||
#
|
||||
# TODO(a.garipov): Replace with a better update mechanism in the future.
|
||||
refresh_interval: 15s
|
||||
# How often AdGuard DNS performs full synchronization.
|
||||
full_refresh_interval: 24h
|
||||
# How often AdGuard DNS sends the billing statistics to the backend.
|
||||
bill_stat_interval: 15s
|
||||
|
||||
# Query logging configuration.
|
||||
query_log:
|
||||
file:
|
||||
# If true, enable writing JSONL logs to a file.
|
||||
enabled: true
|
||||
|
||||
# Common GeoIP database configuration.
|
||||
geoip:
|
||||
# The size of the host lookup cache.
|
||||
host_cache_size: 100000
|
||||
# The size of the IP lookup cache.
|
||||
ip_cache_size: 100000
|
||||
# Interval between the GeoIP database refreshes.
|
||||
refresh_interval: 1h
|
||||
|
||||
# DNS checking configuration.
|
||||
check:
|
||||
# Domains to use for DNS checking.
|
||||
domains:
|
||||
- dnscheck.adguard-dns.com
|
||||
- dnscheck.adguard.com
|
||||
# Location of this node.
|
||||
node_location: 'ams'
|
||||
# Name of this node.
|
||||
node_name: 'eu-1.dns.example.com'
|
||||
# IPs to respond with.
|
||||
ipv4:
|
||||
- 1.2.3.4
|
||||
- 5.6.7.8
|
||||
ipv6:
|
||||
- 1234::cdee
|
||||
- 1234::cdef
|
||||
# For how long to keep the information about the client.
|
||||
ttl: 30s
|
||||
|
||||
# Web/HTTP(S) service configuration. All non-root requests to the main service
|
||||
# not matching the static_content map are shown a 404 page. In special
|
||||
# case of `/robots.txt` request the special response is served.
|
||||
web:
|
||||
# Optional linked IP web server configuration. static_content is not served
|
||||
# on these addresses.
|
||||
linked_ip:
|
||||
bind:
|
||||
- address: '127.0.0.1:9080'
|
||||
- address: '127.0.0.1:9443'
|
||||
certificates:
|
||||
- certificate: './test/cert.crt'
|
||||
key: './test/cert.key'
|
||||
# Optional safe browsing web server configuration. static_content is not
|
||||
# served on these addresses. The addresses should be the same as in the
|
||||
# safe_browsing object.
|
||||
safe_browsing:
|
||||
bind:
|
||||
- address: '127.0.0.1:9081'
|
||||
- address: '127.0.0.1:9444'
|
||||
certificates:
|
||||
- certificate: './test/cert.crt'
|
||||
key: './test/cert.key'
|
||||
block_page: './test/block_page_sb.html'
|
||||
# Optional adult blocking web server configuration. static_content is not
|
||||
# served on these addresses. The addresses should be the same as in the
|
||||
# adult_blocking object.
|
||||
adult_blocking:
|
||||
bind:
|
||||
- address: '127.0.0.1:9082'
|
||||
- address: '127.0.0.1:9445'
|
||||
certificates:
|
||||
- certificate: './test/cert.crt'
|
||||
key: './test/cert.key'
|
||||
block_page: './test/block_page_adult.html'
|
||||
# Listen addresses for the web service in addition to the ones in the
|
||||
# DNS-over-HTTPS handlers.
|
||||
non_doh_bind:
|
||||
- address: '127.0.0.1:9083'
|
||||
- address: '127.0.0.1:9446'
|
||||
certificates:
|
||||
- certificate: './test/cert.crt'
|
||||
key: './test/cert.key'
|
||||
# Static content map. Not served on the linked_ip, safe_browsing and adult_blocking
|
||||
# servers. Paths must not cross the ones used by the DNS-over-HTTPS server.
|
||||
static_content:
|
||||
'/favicon.ico':
|
||||
content_type: 'image/x-icon'
|
||||
content: ''
|
||||
# If not defined, AdGuard DNS will respond with a 404 page to all such
|
||||
# requests.
|
||||
root_redirect_url: 'https://adguard-dns.com'
|
||||
# Path to the 404 page HTML file. If not set, a simple plain text 404
|
||||
# response will be served.
|
||||
error_404: './test/error_404.html'
|
||||
# Same as error_404, but for the 500 status.
|
||||
error_500: './test/error_500.html'
|
||||
# Timeout for server operations
|
||||
timeout: 1m
|
||||
|
||||
# AdGuard general safe browsing filter configuration.
|
||||
safe_browsing:
|
||||
url: 'https://raw.githubusercontent.com/ameshkov/PersonalFilters/master/safebrowsing_test.txt'
|
||||
block_host: 'standard-block.dns.adguard.com'
|
||||
cache_size: 1024
|
||||
cache_ttl: 1h
|
||||
refresh_interval: 1h
|
||||
|
||||
# AdGuard adult content blocking filter configuration.
|
||||
adult_blocking:
|
||||
url: 'https://raw.githubusercontent.com/ameshkov/PersonalFilters/master/adult_test.txt'
|
||||
block_host: 'family-block.dns.adguard.com'
|
||||
cache_size: 1024
|
||||
cache_ttl: 1h
|
||||
refresh_interval: 1h
|
||||
|
||||
# Settings for rule-list-based filters.
|
||||
filters:
|
||||
# The TTL to set for responses to requests for filtered domains.
|
||||
response_ttl: 5m
|
||||
# The size of the LRU cache of compiled filtering engines for profiles with
|
||||
# custom filtering rules.
|
||||
custom_filter_cache_size: 1024
|
||||
# How often to update filters from the index. See the documentation for the
|
||||
# FILTER_INDEX_URL environment variable.
|
||||
refresh_interval: 1h
|
||||
# The timeout for the entire filter update operation. Be aware that each
|
||||
# individual refresh operation also has its own hardcoded 30s timeout.
|
||||
refresh_timeout: 5m
|
||||
|
||||
# Filtering groups are a set of different filtering configurations. These
|
||||
# filtering configurations are then used by server_groups.
|
||||
filtering_groups:
|
||||
- id: 'default'
|
||||
parental:
|
||||
enabled: false
|
||||
rule_lists:
|
||||
enabled: true
|
||||
# IDs must be the same as those of the filtering rule lists received from
|
||||
# the filter index.
|
||||
ids:
|
||||
- 'adguard_dns_filter'
|
||||
safe_browsing:
|
||||
enabled: true
|
||||
block_private_relay: false
|
||||
- id: 'family'
|
||||
parental:
|
||||
enabled: true
|
||||
block_adult: true
|
||||
general_safe_search: true
|
||||
youtube_safe_search: true
|
||||
rule_lists:
|
||||
enabled: true
|
||||
ids:
|
||||
- 'adguard_dns_filter'
|
||||
safe_browsing:
|
||||
enabled: true
|
||||
block_private_relay: false
|
||||
- id: 'non_filtering'
|
||||
rule_lists:
|
||||
enabled: false
|
||||
parental:
|
||||
enabled: false
|
||||
safe_browsing:
|
||||
enabled: false
|
||||
block_private_relay: false
|
||||
|
||||
# Server groups and servers.
|
||||
server_groups:
|
||||
- name: 'adguard_dns_default'
|
||||
# This filtering_group is used for all anonymous clients.
|
||||
filtering_group: 'default'
|
||||
tls:
|
||||
certificates:
|
||||
- certificate: './test/cert.crt'
|
||||
key: './test/cert.key'
|
||||
session_keys:
|
||||
- './test/tls_key_1'
|
||||
- './test/tls_key_2'
|
||||
device_id_wildcards:
|
||||
- '*.dns.example.com'
|
||||
ddr:
|
||||
enabled: true
|
||||
# Device ID domain name suffix to DDR record template mapping. Keep in
|
||||
# sync with servers and device_id_wildcards.
|
||||
device_records:
|
||||
'*.d.dns.example.com':
|
||||
doh_path: '/dns-query{?dns}'
|
||||
https_port: 443
|
||||
quic_port: 853
|
||||
tls_port: 853
|
||||
ipv4_hints:
|
||||
- '127.0.0.1'
|
||||
ipv6_hints:
|
||||
- '::1'
|
||||
# Public domain name to DDR record template mapping. Keep in sync with
|
||||
# servers.
|
||||
public_records:
|
||||
'dns.example.com':
|
||||
doh_path: '/dns-query{?dns}'
|
||||
https_port: 443
|
||||
quic_port: 853
|
||||
tls_port: 853
|
||||
ipv4_hints:
|
||||
- '127.0.0.1'
|
||||
ipv6_hints:
|
||||
- '::1'
|
||||
servers:
|
||||
- name: 'default_dns'
|
||||
# See README for the list of protocol values.
|
||||
protocol: 'dns'
|
||||
linked_ip_enabled: true
|
||||
bind_addresses:
|
||||
- '127.0.0.1:53'
|
||||
- name: 'default_dot'
|
||||
protocol: 'tls'
|
||||
linked_ip_enabled: false
|
||||
bind_addresses:
|
||||
- '127.0.0.1:853'
|
||||
- name: 'default_doh'
|
||||
protocol: 'https'
|
||||
linked_ip_enabled: false
|
||||
bind_addresses:
|
||||
- '127.0.0.1:443'
|
||||
- name: 'default_doq'
|
||||
protocol: 'quic'
|
||||
linked_ip_enabled: false
|
||||
bind_addresses:
|
||||
- '127.0.0.1:784'
|
||||
- '127.0.0.1:853'
|
||||
- name: 'default_dnscrypt'
|
||||
protocol: 'dnscrypt'
|
||||
linked_ip_enabled: false
|
||||
bind_addresses:
|
||||
- '127.0.0.1:5443'
|
||||
dnscrypt:
|
||||
# See https://github.com/ameshkov/dnscrypt/blob/master/README.md#configure.
|
||||
config_path: ./test/dnscrypt.yml
|
||||
- name: 'default_dnscrypt_inline'
|
||||
protocol: 'dnscrypt'
|
||||
linked_ip_enabled: false
|
||||
bind_addresses:
|
||||
- '127.0.0.1:5444'
|
||||
dnscrypt:
|
||||
inline:
|
||||
provider_name: '2.dnscrypt-cert.example.org'
|
||||
public_key: 'F11DDBCC4817E543845FDDD4CB881849B64226F3DE397625669D87B919BC4FB0'
|
||||
private_key: '5752095FFA56D963569951AFE70FE1690F378D13D8AD6F8054DFAA100907F8B6F11DDBCC4817E543845FDDD4CB881849B64226F3DE397625669D87B919BC4FB0'
|
||||
resolver_secret: '9E46E79FEB3AB3D45F4EB3EA957DEAF5D9639A0179F1850AFABA7E58F87C74C4'
|
||||
resolver_public: '9327C5E64783E19C339BD6B680A56DB85521CC6E4E0CA5DF5274E2D3CE026C6B'
|
||||
es_version: 1
|
||||
certificate_ttl: 8760h
|
||||
|
||||
# Connectivity check configuration.
|
||||
connectivity_check:
|
||||
probe_ipv4: '8.8.8.8:53'
|
||||
probe_ipv6: '[2001:4860:4860::8888]:53'
|
@ -1,23 +0,0 @@
|
||||
# dnsdb
|
||||
|
||||
A simple plugin that records domain names and their IP/CNAME addresses.
|
||||
This data can then be retrieved by requesting a specified endpoint.
|
||||
|
||||
```
|
||||
dnsdb [ADDR] [PATH]
|
||||
```
|
||||
|
||||
* `[ADDR]` -- local address where you'll be able to retrieve the dnsdb data
|
||||
* `[PATH]` -- path where we will create the local database
|
||||
|
||||
> Every time when you request the dnsdb data, the local database is re-created.
|
||||
> It is not supposed to be persistent, this is just a cache.
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
dnsdb 127.0.0.1:9154 /var/tmp/dnsdb.bin
|
||||
```
|
||||
|
||||
* `http://127.0.0.1:9154/csv` -- here you'll be able to retrieve the data.
|
||||
* `/var/tmp/dnsdb.bin` -- here the local db will be created.
|
258
dnsdb/db.go
258
dnsdb/db.go
@ -1,258 +0,0 @@
|
||||
package dnsdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
"github.com/miekg/dns"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
const recordsBucket = "Records"
|
||||
|
||||
type dnsDB struct {
|
||||
path string // path to the database file
|
||||
|
||||
db *bolt.DB
|
||||
buffer map[string][]Record
|
||||
|
||||
bufferLock sync.Mutex
|
||||
dbLock sync.Mutex
|
||||
}
|
||||
|
||||
// NewDB creates a new instance of the DNSDB
|
||||
func newDB(path string) (*dnsDB, error) {
|
||||
clog.Infof("Initializing DNSDB: %s", path)
|
||||
d := &dnsDB{
|
||||
path: path,
|
||||
buffer: map[string][]Record{},
|
||||
}
|
||||
|
||||
err := d.InitDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clog.Infof("Finished initializing DNSDB: %s", path)
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// InitDB initializes the database file
|
||||
func (d *dnsDB) InitDB() error {
|
||||
// database is always created from scratch
|
||||
_ = os.Remove(d.path)
|
||||
|
||||
db, err := bolt.Open(d.path, 0644, nil)
|
||||
if err != nil {
|
||||
clog.Errorf("Failed to initialize existing DB: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucket([]byte(recordsBucket))
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
clog.Errorf("Failed to create DB bucket: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
d.db = db
|
||||
return nil
|
||||
}
|
||||
|
||||
// RotateDB closes the current DB, renames it to a temporary file,
|
||||
// initializes a new empty DB, and returns the path to that temporary file
|
||||
func (d *dnsDB) RotateDB() (string, error) {
|
||||
d.dbLock.Lock()
|
||||
defer d.dbLock.Unlock()
|
||||
|
||||
err := d.db.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Moving the old DB to a new location before returning it
|
||||
tmpDir := os.TempDir()
|
||||
tmpPath := path.Join(tmpDir, path.Base(fmt.Sprintf("%s.%d", d.path, time.Now().Unix())))
|
||||
err = os.Rename(d.path, tmpPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Re-creating the database
|
||||
err = d.InitDB()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
dbSizeGauge.Set(0)
|
||||
dbRotateTimestamp.SetToCurrentTime()
|
||||
return tmpPath, nil
|
||||
}
|
||||
|
||||
// RecordMsg saves a DNS response to the buffer
|
||||
// this buffer will be then dumped to the database
|
||||
func (d *dnsDB) RecordMsg(m *dns.Msg) {
|
||||
if !m.Response {
|
||||
// Not a response anyway
|
||||
return
|
||||
}
|
||||
if len(m.Question) != 1 {
|
||||
// Invalid DNS request
|
||||
return
|
||||
}
|
||||
|
||||
q := m.Question[0]
|
||||
if q.Qtype != dns.TypeA && q.Qtype != dns.TypeAAAA {
|
||||
// Only record A and AAAA
|
||||
return
|
||||
}
|
||||
if m.Rcode != dns.RcodeSuccess {
|
||||
// Discard unsuccessful responses
|
||||
return
|
||||
}
|
||||
|
||||
name := strings.TrimSuffix(q.Name, ".")
|
||||
key := d.key(name, q.Qtype)
|
||||
|
||||
d.bufferLock.Lock()
|
||||
if v, ok := d.buffer[key]; ok {
|
||||
// Increment hits count
|
||||
for i := 0; i < len(v); i++ {
|
||||
v[i].Hits++
|
||||
}
|
||||
d.bufferLock.Unlock()
|
||||
// Already buffered, doing nothing
|
||||
return
|
||||
}
|
||||
d.bufferLock.Unlock()
|
||||
|
||||
records := d.toDBRecords(m, q)
|
||||
d.saveToBuffer(name, q.Qtype, records)
|
||||
}
|
||||
|
||||
// Save - saves the buffered records to the local bolt database
|
||||
func (d *dnsDB) Save() {
|
||||
clog.Infof("Saving the buffer to the DNSDB")
|
||||
start := time.Now()
|
||||
|
||||
var buffer map[string][]Record
|
||||
|
||||
// Copy the old buffer
|
||||
d.bufferLock.Lock()
|
||||
buffer = d.buffer
|
||||
d.buffer = map[string][]Record{}
|
||||
bufferSizeGauge.Set(0)
|
||||
d.bufferLock.Unlock()
|
||||
|
||||
if len(buffer) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Start writing
|
||||
d.dbLock.Lock()
|
||||
defer d.dbLock.Unlock()
|
||||
|
||||
err := d.db.Batch(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(recordsBucket))
|
||||
|
||||
for k, v := range buffer {
|
||||
dbKey := []byte(k)
|
||||
|
||||
// First - look for existing records in the bucket
|
||||
val := b.Get(dbKey)
|
||||
if val != nil {
|
||||
recs, err := DecodeRecords(val)
|
||||
if err != nil || len(recs) == 0 {
|
||||
// Do nothing
|
||||
clog.Errorf("Failed to decode records for %s: %s", k, err)
|
||||
} else {
|
||||
// Use the "Hits" counter from the first record
|
||||
// to set the proper "Hits" count
|
||||
for _, r := range v {
|
||||
r.Hits = r.Hits + recs[0].Hits
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now encode the records list
|
||||
dbValue, err := EncodeRecords(v)
|
||||
if err != nil {
|
||||
clog.Errorf("Failed to encode value for %s: %s", k, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Save the updated list to the DB
|
||||
err = b.Put(dbKey, dbValue)
|
||||
if err != nil {
|
||||
clog.Errorf("Failed to save data for %s: %s", k, err)
|
||||
}
|
||||
}
|
||||
|
||||
dbSizeGauge.Set(float64(b.Stats().KeyN))
|
||||
return nil
|
||||
})
|
||||
|
||||
elapsedDBSave.Observe(time.Since(start).Seconds())
|
||||
|
||||
if err != nil {
|
||||
clog.Errorf("Error while updating the DB: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dnsDB) saveToBuffer(name string, qtype uint16, records []Record) {
|
||||
d.bufferLock.Lock()
|
||||
d.buffer[d.key(name, qtype)] = records
|
||||
bufferSizeGauge.Inc()
|
||||
d.bufferLock.Unlock()
|
||||
}
|
||||
|
||||
func (d *dnsDB) key(name string, qtype uint16) string {
|
||||
t, _ := dns.TypeToString[qtype]
|
||||
return name + "_" + t
|
||||
}
|
||||
|
||||
// toDBRecords converts DNS message to an array to "record"
|
||||
func (d *dnsDB) toDBRecords(m *dns.Msg, q dns.Question) []Record {
|
||||
if len(m.Answer) == 0 {
|
||||
rec := d.toDBRecord(m, q, nil)
|
||||
return []Record{rec}
|
||||
}
|
||||
|
||||
records := []Record{}
|
||||
for _, rr := range m.Answer {
|
||||
rec := d.toDBRecord(m, q, rr)
|
||||
records = append(records, rec)
|
||||
}
|
||||
|
||||
return records
|
||||
}
|
||||
|
||||
func (d *dnsDB) toDBRecord(m *dns.Msg, q dns.Question, rr dns.RR) Record {
|
||||
rec := Record{}
|
||||
rec.DomainName = strings.TrimSuffix(q.Name, ".")
|
||||
rec.RCode = m.Rcode
|
||||
rec.Hits = 1
|
||||
if rr == nil {
|
||||
rec.RRType = q.Qtype
|
||||
rec.Answer = ""
|
||||
} else {
|
||||
rec.RRType = rr.Header().Rrtype
|
||||
|
||||
switch v := rr.(type) {
|
||||
case *dns.CNAME:
|
||||
rec.Answer = strings.TrimSuffix(v.Target, ".")
|
||||
case *dns.A:
|
||||
rec.Answer = v.A.String()
|
||||
case *dns.AAAA:
|
||||
rec.Answer = v.AAAA.String()
|
||||
}
|
||||
}
|
||||
return rec
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package dnsdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDbRotateAndSave(t *testing.T) {
|
||||
path := filepath.Join(os.TempDir(), "db.bin")
|
||||
defer func() {
|
||||
_ = os.Remove(path)
|
||||
}()
|
||||
|
||||
db, err := newDB(path)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, db)
|
||||
|
||||
// Test DNS message
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion("badhost.", dns.TypeA)
|
||||
res := new(dns.Msg)
|
||||
res.SetReply(m)
|
||||
res.Response, m.RecursionAvailable = true, true
|
||||
|
||||
res.Answer = []dns.RR{
|
||||
test.A("badhost. 0 IN A 37.220.26.135"),
|
||||
}
|
||||
|
||||
// Record this message twice
|
||||
db.RecordMsg(res)
|
||||
db.RecordMsg(res)
|
||||
|
||||
// Check buffer size
|
||||
assert.Equal(t, 1, len(db.buffer))
|
||||
|
||||
// Save to the DB
|
||||
db.Save()
|
||||
|
||||
// Rotate
|
||||
dbPath, err := db.RotateDB()
|
||||
assert.Nil(t, err)
|
||||
defer func() {
|
||||
_ = os.Remove(dbPath)
|
||||
}()
|
||||
|
||||
// Write CSV
|
||||
buf := bytes.NewBufferString("")
|
||||
err = dnsDBToCSV(dbPath, buf)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Check CSV
|
||||
assert.Equal(t, "badhost,A,NOERROR,37.220.26.135,2\n", buf.String())
|
||||
}
|
149
dnsdb/listen.go
149
dnsdb/listen.go
@ -1,149 +0,0 @@
|
||||
package dnsdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/csv"
|
||||
"encoding/gob"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
// startListener starts HTTP listener that will rotate the database
|
||||
// and return it's contents
|
||||
func startListener(addr string, db *dnsDB) error {
|
||||
if addr == "" {
|
||||
clog.Infof("No dnsdb HTTP listener configured")
|
||||
return nil
|
||||
}
|
||||
|
||||
clog.Infof("Starting dnsdb HTTP listener on %s", addr)
|
||||
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
server := &dbServer{
|
||||
db: db,
|
||||
}
|
||||
|
||||
srv := &http.Server{Handler: server}
|
||||
go func() {
|
||||
_ = srv.Serve(ln)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
type dbServer struct {
|
||||
db *dnsDB
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (c *dbServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/csv" {
|
||||
http.Error(rw, "Not Found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Disallow parallel requests as this request
|
||||
// changes the inner state of the DNSDB
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
// Flush the current buffer to the database
|
||||
c.db.Save()
|
||||
|
||||
path, err := c.db.RotateDB()
|
||||
if err != nil {
|
||||
clog.Errorf("Failed to rotate DNSDB: %s", err)
|
||||
http.Error(rw, "Failed to rotate DNSDB", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
// Remove the temporary database -- we don't need it anymore
|
||||
_ = os.Remove(path)
|
||||
}()
|
||||
|
||||
// Now serve the content
|
||||
rw.Header().Set("Content-Type", "text/plain")
|
||||
|
||||
var writer io.Writer
|
||||
writer = rw
|
||||
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
rw.Header().Set("Content-Encoding", "gzip")
|
||||
gz := gzip.NewWriter(rw)
|
||||
defer gz.Close()
|
||||
writer = gz
|
||||
}
|
||||
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
err = dnsDBToCSV(path, writer)
|
||||
if err != nil {
|
||||
clog.Errorf("Failed to convert DB to CSV: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// dnsDBToCSV converts the DNSDB to CSV
|
||||
func dnsDBToCSV(path string, writer io.Writer) error {
|
||||
db, err := bolt.Open(path, 0644, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = db.Close()
|
||||
}()
|
||||
|
||||
return db.Batch(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(recordsBucket))
|
||||
if b == nil {
|
||||
return errors.New("records bucket not found")
|
||||
}
|
||||
|
||||
csvWriter := csv.NewWriter(writer)
|
||||
defer csvWriter.Flush()
|
||||
|
||||
// Iterating over all records
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
buf := bytes.NewBuffer(v)
|
||||
dec := gob.NewDecoder(buf)
|
||||
var recs []Record
|
||||
err := dec.Decode(&recs)
|
||||
if err != nil {
|
||||
clog.Errorf("Failed to decode DNSDB record: %s", err)
|
||||
// Don't interrupt - we'd better write other records
|
||||
continue
|
||||
}
|
||||
|
||||
for _, r := range recs {
|
||||
csvRec := []string{
|
||||
r.DomainName,
|
||||
dns.TypeToString[r.RRType],
|
||||
dns.RcodeToString[r.RCode],
|
||||
r.Answer,
|
||||
strconv.FormatInt(r.Hits, 10),
|
||||
}
|
||||
err = csvWriter.Write(csvRec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package dnsdb
|
||||
|
||||
import (
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
dbSizeGauge = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "dnsdb",
|
||||
Name: "db_size",
|
||||
Help: "Count of records in the local DNSDB.",
|
||||
})
|
||||
bufferSizeGauge = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "dnsdb",
|
||||
Name: "buffer_size",
|
||||
Help: "Count of records in the temporary buffer.",
|
||||
})
|
||||
dbRotateTimestamp = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "dnsdb",
|
||||
Name: "rotate_time",
|
||||
Help: "Time when the database was rotated.",
|
||||
})
|
||||
elapsedDBSave = promauto.NewHistogram(prometheus.HistogramOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "dnsdb",
|
||||
Name: "elapsed_db_save",
|
||||
Help: "Time elapsed on saving buffer to the database.",
|
||||
})
|
||||
)
|
@ -1,52 +0,0 @@
|
||||
package dnsdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// plug represents the plugin itself
|
||||
type plug struct {
|
||||
Next plugin.Handler
|
||||
|
||||
addr string // Address for the HTTP server that serves the DB data
|
||||
path string // Path to the DNSDB instance
|
||||
}
|
||||
|
||||
// Name returns name of the plugin as seen in Corefile and plugin.cfg
|
||||
func (p *plug) Name() string { return "dnsdb" }
|
||||
|
||||
// ServeDNS handles the DNS request and records it to the DNSDB
|
||||
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
cw := &DBWriter{
|
||||
ResponseWriter: w,
|
||||
db: dnsDBMap[p.addr],
|
||||
}
|
||||
return plugin.NextOrFailure(p.Name(), p.Next, ctx, cw, r)
|
||||
}
|
||||
|
||||
// Recorder is a type of ResponseWriter that captures
|
||||
// the rcode code written to it and also the size of the message
|
||||
// written in the response. A rcode code does not have
|
||||
// to be written, however, in which case 0 must be assumed.
|
||||
// It is best to have the constructor initialize this type
|
||||
// with that default status code.
|
||||
type DBWriter struct {
|
||||
dns.ResponseWriter
|
||||
db *dnsDB
|
||||
}
|
||||
|
||||
// WriteMsg records the status code and calls the
|
||||
// underlying ResponseWriter's WriteMsg method.
|
||||
func (r *DBWriter) WriteMsg(res *dns.Msg) error {
|
||||
r.db.RecordMsg(res)
|
||||
return r.ResponseWriter.WriteMsg(res)
|
||||
}
|
||||
|
||||
// Write is a wrapper that records the length of the message that gets written.
|
||||
func (r *DBWriter) Write(buf []byte) (int, error) {
|
||||
// Doing nothing in this case
|
||||
return r.ResponseWriter.Write(buf)
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
package dnsdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestPluginRecordMsg(t *testing.T) {
|
||||
path := filepath.Join(os.TempDir(), "db.bin")
|
||||
defer func() {
|
||||
_ = os.Remove(path)
|
||||
}()
|
||||
|
||||
configText := fmt.Sprintf(`dnsdb %s`, path)
|
||||
c := caddy.NewTestController("dns", configText)
|
||||
c.ServerBlockKeys = []string{""}
|
||||
|
||||
p, err := parse(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Emulate a DNS response
|
||||
p.Next = backendResponse()
|
||||
ctx := context.TODO()
|
||||
|
||||
// Test DNS message
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("badhost.", dns.TypeA)
|
||||
|
||||
resp := test.ResponseWriter{}
|
||||
rrw := dnstest.NewRecorder(&resp)
|
||||
|
||||
// Call the plugin
|
||||
rcode, err := p.ServeDNS(ctx, rrw, req)
|
||||
if err != nil {
|
||||
t.Fatalf("ServeDNS returned error: %s", err)
|
||||
}
|
||||
if rcode != rrw.Rcode {
|
||||
t.Fatalf("ServeDNS return value %d that does not match captured rcode %d", rcode, rrw.Rcode)
|
||||
}
|
||||
|
||||
// Get the db
|
||||
db, ok := dnsDBMap[""]
|
||||
assert.True(t, ok)
|
||||
assert.NotNil(t, db)
|
||||
|
||||
// Assert that everything was written properly
|
||||
assert.Equal(t, 1, len(db.buffer))
|
||||
|
||||
rec, _ := db.buffer["badhost_A"]
|
||||
assert.NotNil(t, rec)
|
||||
assert.Equal(t, 1, len(rec))
|
||||
assert.Equal(t, "badhost", rec[0].DomainName)
|
||||
}
|
||||
|
||||
// Return response with an A record
|
||||
func backendResponse() plugin.Handler {
|
||||
return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Response, m.RecursionAvailable = true, true
|
||||
|
||||
m.Answer = []dns.RR{
|
||||
test.A("badhost. 0 IN A 37.220.26.135"),
|
||||
}
|
||||
_ = w.WriteMsg(m)
|
||||
return dns.RcodeSuccess, nil
|
||||
})
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package dnsdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
)
|
||||
|
||||
// Record of the DNS DB
|
||||
type Record struct {
|
||||
DomainName string // DomainName -- fqdn version
|
||||
RRType uint16 // RRType - either A, AAAA, or CNAME
|
||||
RCode int // RCode - DNS response RCode
|
||||
Answer string // Answer - IP or hostname
|
||||
Hits int64 // How many times this record was served
|
||||
}
|
||||
|
||||
// EncodeRecords encodes an array of records to a byte array
|
||||
func EncodeRecords(recs []Record) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
enc := gob.NewEncoder(&buf)
|
||||
err := enc.Encode(recs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// DecodeRecords decodes an array of records from a byte array
|
||||
func DecodeRecords(b []byte) ([]Record, error) {
|
||||
buf := bytes.NewBuffer(b)
|
||||
dec := gob.NewDecoder(buf)
|
||||
var recs []Record
|
||||
err := dec.Decode(&recs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return recs, nil
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
package dnsdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
)
|
||||
|
||||
const bufferRotationPeriod = 15 * time.Minute
|
||||
|
||||
var (
|
||||
// Keeping one dnsDB instance per address
|
||||
dnsDBMap = map[string]*dnsDB{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("dnsdb", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
clog.Infof("Initializing the dnsdb plugin for %s", c.ServerBlockKeys[c.ServerBlockKeyIndex])
|
||||
p, err := parse(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := dnsserver.GetConfig(c)
|
||||
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
p.Next = next
|
||||
return p
|
||||
})
|
||||
|
||||
clog.Infof("Finished initializing the dnsfilter plugin for %s", c.ServerBlockKeys[c.ServerBlockKeyIndex])
|
||||
return nil
|
||||
}
|
||||
|
||||
func parse(c *caddy.Controller) (*plug, error) {
|
||||
p := &plug{}
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 1 {
|
||||
p.path = args[0]
|
||||
}
|
||||
|
||||
if len(args) == 2 {
|
||||
p.addr = args[0]
|
||||
p.path = args[1]
|
||||
}
|
||||
|
||||
if len(args) == 0 || len(args) > 2 {
|
||||
return nil, fmt.Errorf("cannot initialize DNSDB plugin - invalid args: %v", args)
|
||||
}
|
||||
}
|
||||
|
||||
if db, ok := dnsDBMap[p.addr]; ok {
|
||||
if db.path != p.path {
|
||||
return nil, fmt.Errorf("dnsdb with a different path already listens to %s", p.addr)
|
||||
}
|
||||
} else {
|
||||
// Init the new dnsDB
|
||||
d, err := newDB(p.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dnsDBMap[p.addr] = d
|
||||
|
||||
// Start the listener
|
||||
err = startListener(p.addr, d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(bufferRotationPeriod)
|
||||
go func() {
|
||||
time.Sleep(bufferRotationPeriod)
|
||||
for t := range ticker.C {
|
||||
_ = t // we don't print the ticker time, so assign this `t` variable to underscore `_` to avoid error
|
||||
d.Save()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
# dnsfilter
|
||||
|
||||
This plugin implements the filtering logic.
|
||||
|
||||
It uses local blacklists to make a decision on whether the DNS request should be blocked or bypassed.
|
||||
|
||||
```
|
||||
dnsfilter {
|
||||
filter [PATH] [URL TTL]
|
||||
safebrowsing [PATH] [HOST] [URL TTL]
|
||||
parental [PATH] [HOST] [URL TTL]
|
||||
safesearch
|
||||
}
|
||||
```
|
||||
|
||||
* `filter [PATH]` -- path to the blacklist that will be used for blocking ads and trackers
|
||||
* `filter [URL TTL]` -- URL to the filter list and TTL. Once in in `TTL` seconds we will
|
||||
try to reload the filter from the specified URL.
|
||||
* `safebrowsing [PATH] [HOST]`
|
||||
* path to the blacklist that will be used for blocking malicious/phishing domains
|
||||
* hostname that we will use for DNS response when we block malicious/phishing domains
|
||||
* `parental [PATH] [HOST]`
|
||||
* path to the blacklist that will be used for blocking adult websites
|
||||
* hostname that we will use for DNS response when we block adult websites
|
||||
* `safesearch` - if specified, we'll enforce safe search on the popular search engines
|
@ -1,289 +0,0 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
safeservices "github.com/AdguardTeam/AdGuardDNS/dnsfilter/safe_services"
|
||||
"github.com/AdguardTeam/urlfilter/rules"
|
||||
|
||||
"github.com/AdguardTeam/urlfilter"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
"github.com/coredns/coredns/request"
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
const NotFiltered = -100
|
||||
|
||||
// https://support.mozilla.org/en-US/kb/configuring-networks-disable-dns-over-https
|
||||
const FirefoxCanaryDomain = "use-application-dns.net"
|
||||
const sbTXTSuffix = ".sb.dns.adguard.com"
|
||||
const pcTXTSuffix = ".pc.dns.adguard.com"
|
||||
|
||||
// ServeDNS handles the DNS request and refuses if it's in filterlists
|
||||
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
if len(r.Question) != 1 {
|
||||
// google DNS, bind and others do the same
|
||||
return dns.RcodeFormatError, fmt.Errorf("got a DNS request with more than one Question")
|
||||
}
|
||||
|
||||
// measure time spent in dnsfilter
|
||||
start := time.Now()
|
||||
|
||||
rcode, err := p.handleTXT(ctx, w, r)
|
||||
if rcode == NotFiltered {
|
||||
// pass the request to an upstream server and receive response
|
||||
rec := responseRecorder{
|
||||
ResponseWriter: w,
|
||||
}
|
||||
rcode, err = plugin.NextOrFailure(p.Name(), p.Next, ctx, &rec, r)
|
||||
|
||||
// measure time spent in dnsfilter
|
||||
startFiltering := time.Now()
|
||||
|
||||
if err == nil && rec.resp != nil {
|
||||
// check if request or response should be blocked
|
||||
rcode2, err2 := p.filterRequest(ctx, w, r, rec.resp)
|
||||
if rcode2 != NotFiltered {
|
||||
incFiltered(w)
|
||||
rcode = rcode2
|
||||
err = err2
|
||||
rec.resp = nil // filterRequest() has already written the response
|
||||
}
|
||||
|
||||
elapsedFilterTime.Observe(time.Since(startFiltering).Seconds())
|
||||
}
|
||||
if rec.resp != nil {
|
||||
err2 := w.WriteMsg(rec.resp) // pass through the original response
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// increment requests counters
|
||||
incRequests(w)
|
||||
elapsedTime.Observe(time.Since(start).Seconds())
|
||||
|
||||
if err != nil {
|
||||
errorsTotal.Inc()
|
||||
}
|
||||
|
||||
return rcode, err
|
||||
}
|
||||
|
||||
// Stores DNS response object
|
||||
type responseRecorder struct {
|
||||
dns.ResponseWriter
|
||||
resp *dns.Msg
|
||||
}
|
||||
|
||||
func (r *responseRecorder) WriteMsg(res *dns.Msg) error {
|
||||
r.resp = res
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *plug) replyTXT(w dns.ResponseWriter, r *dns.Msg, txtData []string) error {
|
||||
txt := dns.TXT{}
|
||||
txt.Hdr = dns.RR_Header{
|
||||
Name: r.Question[0].Name,
|
||||
Rrtype: dns.TypeTXT,
|
||||
Ttl: p.settings.BlockedTTL,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
txt.Txt = txtData
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Authoritative = true
|
||||
m.RecursionAvailable = true
|
||||
m.Compress = true
|
||||
m.Answer = append(m.Answer, &txt)
|
||||
|
||||
state := request.Request{W: w, Req: r}
|
||||
state.SizeAndDo(m)
|
||||
return state.W.WriteMsg(m)
|
||||
}
|
||||
|
||||
// Respond to TXT requests for safe-browsing and parental services.
|
||||
// Return NotFiltered if request wasn't handled.
|
||||
func (p *plug) handleTXT(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
host := strings.ToLower(strings.TrimSuffix(r.Question[0].Name, "."))
|
||||
|
||||
if p.settings.SafeBrowsingEnabled &&
|
||||
r.Question[0].Qtype == dns.TypeTXT &&
|
||||
strings.HasSuffix(host, sbTXTSuffix) {
|
||||
|
||||
requestsSafeBrowsingTXT.Inc()
|
||||
|
||||
hashStr := host[:len(host)-len(sbTXTSuffix)]
|
||||
txtData, _ := p.getSafeBrowsingEngine().data.MatchHashes(hashStr)
|
||||
err := p.replyTXT(w, r, txtData)
|
||||
if err != nil {
|
||||
clog.Infof("SafeBrowsing: WriteMsg(): %s\n", err)
|
||||
return dns.RcodeServerFailure, fmt.Errorf("SafeBrowsing: WriteMsg(): %s", err)
|
||||
}
|
||||
return dns.RcodeSuccess, nil
|
||||
}
|
||||
|
||||
if p.settings.ParentalEnabled &&
|
||||
r.Question[0].Qtype == dns.TypeTXT &&
|
||||
strings.HasSuffix(host, pcTXTSuffix) {
|
||||
|
||||
requestsParentalTXT.Inc()
|
||||
|
||||
hashStr := host[:len(host)-len(pcTXTSuffix)]
|
||||
txtData, _ := p.getParentalEngine().data.MatchHashes(hashStr)
|
||||
err := p.replyTXT(w, r, txtData)
|
||||
if err != nil {
|
||||
clog.Infof("Parental: WriteMsg(): %s\n", err)
|
||||
return dns.RcodeServerFailure, fmt.Errorf("parental: WriteMsg(): %s", err)
|
||||
}
|
||||
return dns.RcodeSuccess, nil
|
||||
}
|
||||
|
||||
return NotFiltered, nil
|
||||
}
|
||||
|
||||
// filterRequest applies dnsfilter rules to the request. If the request should be blocked,
|
||||
// it writes the response right away. Otherwise, it returns NotFiltered instead of the response code,
|
||||
// which means that the request should processed further by the next plugins in the chain.
|
||||
func (p *plug) filterRequest(ctx context.Context, w dns.ResponseWriter, req *dns.Msg, res *dns.Msg) (int, error) {
|
||||
question := req.Question[0]
|
||||
host := strings.ToLower(strings.TrimSuffix(question.Name, "."))
|
||||
|
||||
if (question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA) &&
|
||||
host == FirefoxCanaryDomain {
|
||||
return p.writeNXDomain(ctx, w, req)
|
||||
}
|
||||
|
||||
// is it a safesearch domain?
|
||||
if p.settings.SafeSearchEnabled {
|
||||
if replacementHost, ok := safeservices.SafeSearchDomains[host]; ok {
|
||||
safeSearch.Inc()
|
||||
|
||||
return p.replaceHostWithValAndReply(ctx, w, req, host, replacementHost, question)
|
||||
}
|
||||
}
|
||||
|
||||
// is it blocked by safebrowsing?
|
||||
if p.settings.SafeBrowsingEnabled && p.getSafeBrowsingEngine().data.MatchHost(host) {
|
||||
filteredSafeBrowsing.Inc()
|
||||
|
||||
// return cname safebrowsing.block.dns.adguard.com
|
||||
replacementHost := p.settings.SafeBrowsingBlockHost
|
||||
return p.replaceHostWithValAndReply(ctx, w, req, host, replacementHost, question)
|
||||
}
|
||||
|
||||
// is it blocked by parental control?
|
||||
if p.settings.ParentalEnabled && p.getParentalEngine().data.MatchHost(host) {
|
||||
filteredParental.Inc()
|
||||
|
||||
// return cname family.block.dns.adguard.com
|
||||
replacementHost := p.settings.ParentalBlockHost
|
||||
return p.replaceHostWithValAndReply(ctx, w, req, host, replacementHost, question)
|
||||
}
|
||||
|
||||
// is it blocked by filtering rules
|
||||
ok, rule := p.matchesEngine(p.getBlockingEngine(), host, true)
|
||||
if ok {
|
||||
filteredLists.Inc()
|
||||
return p.writeBlacklistedResponse(ctx, w, req)
|
||||
}
|
||||
|
||||
if rule != nil {
|
||||
if f, ok := rule.(*rules.NetworkRule); ok {
|
||||
if f.Whitelist {
|
||||
// Do nothing if this is a whitelist rule
|
||||
return NotFiltered, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try checking DNS response now
|
||||
matched, rcode, err := p.filterResponse(ctx, w, req, res)
|
||||
if matched {
|
||||
return rcode, err
|
||||
}
|
||||
|
||||
// indicate that the next plugin must be called
|
||||
return NotFiltered, nil
|
||||
}
|
||||
|
||||
// If response contains CNAME, A or AAAA records, we apply filtering to each canonical host name or IP address.
|
||||
func (p *plug) filterResponse(ctx context.Context, w dns.ResponseWriter, req *dns.Msg, resp *dns.Msg) (bool, int, error) {
|
||||
for _, a := range resp.Answer {
|
||||
host := ""
|
||||
|
||||
switch v := a.(type) {
|
||||
case *dns.CNAME:
|
||||
clog.Debugf("Checking CNAME %s for %s", v.Target, v.Hdr.Name)
|
||||
host = strings.TrimSuffix(v.Target, ".")
|
||||
|
||||
case *dns.A:
|
||||
host = v.A.String()
|
||||
clog.Debugf("Checking record A (%s) for %s", host, v.Hdr.Name)
|
||||
|
||||
case *dns.AAAA:
|
||||
host = v.AAAA.String()
|
||||
clog.Debugf("Checking record AAAA (%s) for %s", host, v.Hdr.Name)
|
||||
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if ok, _ := p.matchesEngine(p.getBlockingEngine(), host, true); ok {
|
||||
clog.Debugf("Matched %s by response: %s", req.Question[0].Name, host)
|
||||
filteredLists.Inc()
|
||||
rcode, err := p.writeBlacklistedResponse(ctx, w, req)
|
||||
return true, rcode, err
|
||||
}
|
||||
}
|
||||
|
||||
return false, 0, nil
|
||||
}
|
||||
|
||||
// matchesEngine checks if there's a match for the specified host
|
||||
// note, that if it matches a whitelist rule, the function returns false
|
||||
// recordStats -- if true, we record hit for the matching rule
|
||||
// returns true if request should be blocked
|
||||
func (p *plug) matchesEngine(engine *urlfilter.DNSEngine, host string, recordStats bool) (bool, rules.Rule) {
|
||||
if engine == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
res, ok := engine.Match(host, nil)
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if res.NetworkRule != nil {
|
||||
if recordStats {
|
||||
recordRuleHit(res.NetworkRule.RuleText)
|
||||
}
|
||||
|
||||
if res.NetworkRule.Whitelist {
|
||||
return false, res.NetworkRule
|
||||
}
|
||||
|
||||
return true, res.NetworkRule
|
||||
}
|
||||
|
||||
var matchingRule rules.Rule
|
||||
if len(res.HostRulesV4) > 0 {
|
||||
matchingRule = res.HostRulesV4[0]
|
||||
} else if len(res.HostRulesV6) > 0 {
|
||||
matchingRule = res.HostRulesV6[0]
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if recordStats {
|
||||
recordRuleHit(matchingRule.Text())
|
||||
}
|
||||
|
||||
return true, matchingRule
|
||||
}
|
@ -1,481 +0,0 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEtcHostsFilter(t *testing.T) {
|
||||
text := []byte("127.0.0.1 doubleclick.net\n" + "127.0.0.1 example.org example.net www.example.org www.example.net")
|
||||
tmpfile, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = tmpfile.Write(text); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = tmpfile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
configText := fmt.Sprintf("dnsfilter {\nfilter %s\n}", tmpfile.Name())
|
||||
c := caddy.NewTestController("dns", configText)
|
||||
c.ServerBlockKeys = []string{""}
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p.Next = zeroTTLBackend()
|
||||
ctx := context.TODO()
|
||||
|
||||
for _, testcase := range []struct {
|
||||
host string
|
||||
filtered bool
|
||||
}{
|
||||
{"www.doubleclick.net", false},
|
||||
{"doubleclick.net", true},
|
||||
{"www2.example.org", false},
|
||||
{"www2.example.net", false},
|
||||
{"test.www.example.org", false},
|
||||
{"test.www.example.net", false},
|
||||
{"example.org", true},
|
||||
{"example.net", true},
|
||||
{"www.example.org", true},
|
||||
{"www.example.net", true},
|
||||
} {
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion(testcase.host+".", dns.TypeA)
|
||||
|
||||
resp := test.ResponseWriter{}
|
||||
rrw := dnstest.NewRecorder(&resp)
|
||||
rcode, err := p.ServeDNS(ctx, rrw, req)
|
||||
if err != nil {
|
||||
t.Fatalf("ServeDNS returned error: %s", err)
|
||||
}
|
||||
if rcode != rrw.Rcode {
|
||||
t.Fatalf("ServeDNS return value for host %s has rcode %d that does not match captured rcode %d", testcase.host, rcode, rrw.Rcode)
|
||||
}
|
||||
A, ok := rrw.Msg.Answer[0].(*dns.A)
|
||||
if !ok {
|
||||
t.Fatalf("Host %s expected to have result A", testcase.host)
|
||||
}
|
||||
ip := net.IPv4(0, 0, 0, 0)
|
||||
filtered := ip.Equal(A.A)
|
||||
if testcase.filtered && testcase.filtered != filtered {
|
||||
t.Fatalf("Host %s expected to be filtered, instead it is not filtered", testcase.host)
|
||||
}
|
||||
if !testcase.filtered && testcase.filtered != filtered {
|
||||
t.Fatalf("Host %s expected to be not filtered, instead it is filtered", testcase.host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeSearchFilter(t *testing.T) {
|
||||
configText := `dnsfilter {
|
||||
safesearch
|
||||
}`
|
||||
c := caddy.NewTestController("dns", configText)
|
||||
c.ServerBlockKeys = []string{""}
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p.Next = zeroTTLBackend()
|
||||
ctx := context.TODO()
|
||||
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("www.google.com.", dns.TypeA)
|
||||
|
||||
resp := test.ResponseWriter{}
|
||||
rrw := dnstest.NewRecorder(&resp)
|
||||
rcode, err := p.ServeDNS(ctx, rrw, req)
|
||||
if err != nil {
|
||||
t.Fatalf("ServeDNS returned error: %s", err)
|
||||
}
|
||||
if rcode != rrw.Rcode {
|
||||
t.Fatalf("ServeDNS return value %d that does not match captured rcode %d", rcode, rrw.Rcode)
|
||||
}
|
||||
|
||||
assertResponseIP(t, rrw.Msg, "forcesafesearch.google.com")
|
||||
}
|
||||
|
||||
// 4-character hash
|
||||
func TestSafeBrowsingEngine(t *testing.T) {
|
||||
configText := `dnsfilter {
|
||||
safebrowsing ../tests/sb.txt example.net
|
||||
}`
|
||||
c := caddy.NewTestController("dns", configText)
|
||||
c.ServerBlockKeys = []string{""}
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hash0 := sha256.Sum256([]byte("asdf.testsb.example.org"))
|
||||
q0 := hex.EncodeToString(hash0[0:2])
|
||||
hash1 := sha256.Sum256([]byte("testsb.example.org"))
|
||||
q1 := hex.EncodeToString(hash1[0:2])
|
||||
hash2 := sha256.Sum256([]byte("example.org"))
|
||||
q2 := hex.EncodeToString(hash2[0:2])
|
||||
result, _ := p.getSafeBrowsingEngine().data.MatchHashes(q0 + "." + q1 + "." + q2)
|
||||
assert.True(t, len(result) == 1)
|
||||
shash := hex.EncodeToString(hash1[:])
|
||||
assert.True(t, result[0] == shash)
|
||||
|
||||
assert.True(t, p.getSafeBrowsingEngine().data.MatchHost("testsb.example.org"))
|
||||
assert.True(t, !p.getSafeBrowsingEngine().data.MatchHost("example.org"))
|
||||
}
|
||||
|
||||
// 8-character hash (legacy mode)
|
||||
func TestSafeBrowsingEngineLegacy(t *testing.T) {
|
||||
configText := `dnsfilter {
|
||||
safebrowsing ../tests/sb.txt example.net
|
||||
}`
|
||||
c := caddy.NewTestController("dns", configText)
|
||||
c.ServerBlockKeys = []string{""}
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hash0 := sha256.Sum256([]byte("asdf.testsb.example.org"))
|
||||
q0 := hex.EncodeToString(hash0[0:4])
|
||||
hash1 := sha256.Sum256([]byte("testsb.example.org"))
|
||||
q1 := hex.EncodeToString(hash1[0:4])
|
||||
hash2 := sha256.Sum256([]byte("example.org"))
|
||||
q2 := hex.EncodeToString(hash2[0:4])
|
||||
result, _ := p.getSafeBrowsingEngine().data.MatchHashes(q0 + "." + q1 + "." + q2)
|
||||
assert.True(t, len(result) == 1)
|
||||
shash := hex.EncodeToString(hash1[:])
|
||||
assert.True(t, result[0] == shash)
|
||||
|
||||
assert.True(t, p.getSafeBrowsingEngine().data.MatchHost("testsb.example.org"))
|
||||
assert.True(t, !p.getSafeBrowsingEngine().data.MatchHost("example.org"))
|
||||
}
|
||||
|
||||
func TestSafeBrowsingFilter(t *testing.T) {
|
||||
configText := `dnsfilter {
|
||||
safebrowsing ../tests/sb.txt example.net
|
||||
}`
|
||||
c := caddy.NewTestController("dns", configText)
|
||||
c.ServerBlockKeys = []string{""}
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p.Next = zeroTTLBackend()
|
||||
ctx := context.TODO()
|
||||
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("testsb.example.org.", dns.TypeA)
|
||||
|
||||
resp := test.ResponseWriter{}
|
||||
rrw := dnstest.NewRecorder(&resp)
|
||||
rcode, err := p.ServeDNS(ctx, rrw, req)
|
||||
if err != nil {
|
||||
t.Fatalf("ServeDNS returned error: %s", err)
|
||||
}
|
||||
if rcode != rrw.Rcode {
|
||||
t.Fatalf("ServeDNS return value %d that does not match captured rcode %d", rcode, rrw.Rcode)
|
||||
}
|
||||
|
||||
assertResponseIP(t, rrw.Msg, "example.net")
|
||||
}
|
||||
|
||||
// Send a TXT request with a hash prefix, receive response and find the target hash there
|
||||
func TestSafeBrowsingFilterTXT(t *testing.T) {
|
||||
configText := `dnsfilter {
|
||||
safebrowsing ../tests/sb.txt example.net
|
||||
}`
|
||||
c := caddy.NewTestController("dns", configText)
|
||||
c.ServerBlockKeys = []string{""}
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p.Next = zeroTTLBackend()
|
||||
ctx := context.TODO()
|
||||
|
||||
hash := sha256.Sum256([]byte("testsb.example.org"))
|
||||
q := hex.EncodeToString(hash[0:2])
|
||||
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion(q+sbTXTSuffix+".", dns.TypeTXT)
|
||||
|
||||
resp := test.ResponseWriter{}
|
||||
rrw := dnstest.NewRecorder(&resp)
|
||||
rcode, err := p.ServeDNS(ctx, rrw, req)
|
||||
if err != nil {
|
||||
t.Fatalf("ServeDNS returned error: %s", err)
|
||||
}
|
||||
if rcode != rrw.Rcode {
|
||||
t.Fatalf("ServeDNS return value %d that does not match captured rcode %d", rcode, rrw.Rcode)
|
||||
}
|
||||
|
||||
assertResponseTXT(t, rrw.Msg, hex.EncodeToString(hash[:]))
|
||||
}
|
||||
|
||||
func TestParentalEngine(t *testing.T) {
|
||||
configText := `dnsfilter {
|
||||
parental ../tests/parental.txt example.net
|
||||
}`
|
||||
c := caddy.NewTestController("dns", configText)
|
||||
c.ServerBlockKeys = []string{""}
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hash0 := sha256.Sum256([]byte("asdf.testparental.example.org"))
|
||||
q0 := hex.EncodeToString(hash0[0:2])
|
||||
hash1 := sha256.Sum256([]byte("testparental.example.org"))
|
||||
q1 := hex.EncodeToString(hash1[0:2])
|
||||
hash2 := sha256.Sum256([]byte("example.org"))
|
||||
q2 := hex.EncodeToString(hash2[0:2])
|
||||
result, _ := p.getParentalEngine().data.MatchHashes(q0 + "." + q1 + "." + q2)
|
||||
assert.True(t, len(result) == 1)
|
||||
shash := hex.EncodeToString(hash1[:])
|
||||
assert.True(t, result[0] == shash)
|
||||
|
||||
assert.True(t, p.getParentalEngine().data.MatchHost("testparental.example.org"))
|
||||
assert.True(t, !p.getParentalEngine().data.MatchHost("example.org"))
|
||||
}
|
||||
|
||||
func TestParentalFilter(t *testing.T) {
|
||||
configText := `dnsfilter {
|
||||
parental ../tests/parental.txt example.net
|
||||
}`
|
||||
c := caddy.NewTestController("dns", configText)
|
||||
c.ServerBlockKeys = []string{""}
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p.Next = zeroTTLBackend()
|
||||
ctx := context.TODO()
|
||||
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("testparental.example.org.", dns.TypeA)
|
||||
|
||||
resp := test.ResponseWriter{}
|
||||
rrw := dnstest.NewRecorder(&resp)
|
||||
rcode, err := p.ServeDNS(ctx, rrw, req)
|
||||
if err != nil {
|
||||
t.Fatalf("ServeDNS returned error: %s", err)
|
||||
}
|
||||
if rcode != rrw.Rcode {
|
||||
t.Fatalf("ServeDNS return value %d that does not match captured rcode %d", rcode, rrw.Rcode)
|
||||
}
|
||||
|
||||
assertResponseIP(t, rrw.Msg, "example.net")
|
||||
}
|
||||
|
||||
// Send a TXT request with a hash prefix, receive response and find the target hash there
|
||||
func TestParentalFilterTXT(t *testing.T) {
|
||||
configText := `dnsfilter {
|
||||
parental ../tests/parental.txt example.net
|
||||
}`
|
||||
c := caddy.NewTestController("dns", configText)
|
||||
c.ServerBlockKeys = []string{""}
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p.Next = zeroTTLBackend()
|
||||
ctx := context.TODO()
|
||||
|
||||
hash := sha256.Sum256([]byte("testparental.example.org"))
|
||||
q := hex.EncodeToString(hash[0:2])
|
||||
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion(q+pcTXTSuffix+".", dns.TypeTXT)
|
||||
|
||||
resp := test.ResponseWriter{}
|
||||
rrw := dnstest.NewRecorder(&resp)
|
||||
rcode, err := p.ServeDNS(ctx, rrw, req)
|
||||
if err != nil {
|
||||
t.Fatalf("ServeDNS returned error: %s", err)
|
||||
}
|
||||
if rcode != rrw.Rcode {
|
||||
t.Fatalf("ServeDNS return value %d that does not match captured rcode %d", rcode, rrw.Rcode)
|
||||
}
|
||||
|
||||
assertResponseTXT(t, rrw.Msg, hex.EncodeToString(hash[:]))
|
||||
}
|
||||
|
||||
// 'badhost' has a canonical name 'badhost.eulerian.net' which is blocked by filters
|
||||
func TestCNAMEFilter(t *testing.T) {
|
||||
configText := `dnsfilter {
|
||||
filter ../tests/dns.txt
|
||||
}`
|
||||
c := caddy.NewTestController("dns", configText)
|
||||
c.ServerBlockKeys = []string{""}
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p.Next = backendCNAME()
|
||||
ctx := context.TODO()
|
||||
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("badhost.", dns.TypeA)
|
||||
|
||||
resp := test.ResponseWriter{}
|
||||
rrw := dnstest.NewRecorder(&resp)
|
||||
rcode, err := p.ServeDNS(ctx, rrw, req)
|
||||
if err != nil {
|
||||
t.Fatalf("ServeDNS returned error: %s", err)
|
||||
}
|
||||
if rcode != rrw.Rcode {
|
||||
t.Fatalf("ServeDNS return value %d that does not match captured rcode %d", rcode, rrw.Rcode)
|
||||
}
|
||||
|
||||
assert.True(t, len(rrw.Msg.Answer) != 0)
|
||||
haveA := false
|
||||
for _, rec := range rrw.Msg.Answer {
|
||||
if a, ok := rec.(*dns.A); ok {
|
||||
haveA = true
|
||||
assert.True(t, a.A.Equal(net.IP{0, 0, 0, 0}))
|
||||
}
|
||||
}
|
||||
assert.True(t, haveA)
|
||||
}
|
||||
|
||||
// 'badhost' has an IP '37.220.26.135' which is blocked by filters
|
||||
func TestResponseFilter(t *testing.T) {
|
||||
configText := `dnsfilter {
|
||||
filter ../tests/dns.txt
|
||||
}`
|
||||
c := caddy.NewTestController("dns", configText)
|
||||
c.ServerBlockKeys = []string{""}
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p.Next = backendBlockByIP()
|
||||
ctx := context.TODO()
|
||||
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("badhost.", dns.TypeA)
|
||||
|
||||
resp := test.ResponseWriter{}
|
||||
rrw := dnstest.NewRecorder(&resp)
|
||||
rcode, err := p.ServeDNS(ctx, rrw, req)
|
||||
if err != nil {
|
||||
t.Fatalf("ServeDNS returned error: %s", err)
|
||||
}
|
||||
if rcode != rrw.Rcode {
|
||||
t.Fatalf("ServeDNS return value %d that does not match captured rcode %d", rcode, rrw.Rcode)
|
||||
}
|
||||
|
||||
assert.True(t, len(rrw.Msg.Answer) != 0)
|
||||
haveA := false
|
||||
for _, rec := range rrw.Msg.Answer {
|
||||
if a, ok := rec.(*dns.A); ok {
|
||||
haveA = true
|
||||
assert.True(t, a.A.Equal(net.IP{0, 0, 0, 0}))
|
||||
}
|
||||
}
|
||||
assert.True(t, haveA)
|
||||
}
|
||||
|
||||
func assertResponseIP(t *testing.T, m *dns.Msg, expectedHost string) {
|
||||
addrs, _ := net.LookupIP(expectedHost)
|
||||
|
||||
if len(m.Answer) == 0 {
|
||||
t.Fatalf("no answer instead of %s", expectedHost)
|
||||
}
|
||||
|
||||
for _, rec := range m.Answer {
|
||||
if a, ok := rec.(*dns.A); ok {
|
||||
for _, ip := range addrs {
|
||||
if ip.Equal(a.A) {
|
||||
// Found matching IP, all good
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("could not find %s IP addresses", expectedHost)
|
||||
}
|
||||
|
||||
func assertResponseTXT(t *testing.T, m *dns.Msg, hash string) {
|
||||
for _, rec := range m.Answer {
|
||||
if txt, ok := rec.(*dns.TXT); ok {
|
||||
for _, t := range txt.Txt {
|
||||
if t == hash {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("invalid TXT response")
|
||||
}
|
||||
|
||||
func zeroTTLBackend() plugin.Handler {
|
||||
return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Response, m.RecursionAvailable = true, true
|
||||
|
||||
m.Answer = []dns.RR{test.A("example.org. 0 IN A 127.0.0.53")}
|
||||
_ = w.WriteMsg(m)
|
||||
return dns.RcodeSuccess, nil
|
||||
})
|
||||
}
|
||||
|
||||
// Return response with CNAME and A records
|
||||
func backendCNAME() plugin.Handler {
|
||||
return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Response, m.RecursionAvailable = true, true
|
||||
|
||||
m.Answer = []dns.RR{
|
||||
test.CNAME("badhost. 0 IN CNAME badhost.eulerian.net."),
|
||||
test.A("badhost.eulerian.net. 0 IN A 127.0.0.53"),
|
||||
}
|
||||
_ = w.WriteMsg(m)
|
||||
return dns.RcodeSuccess, nil
|
||||
})
|
||||
}
|
||||
|
||||
// Return response with an A record
|
||||
func backendBlockByIP() plugin.Handler {
|
||||
return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Response, m.RecursionAvailable = true, true
|
||||
|
||||
m.Answer = []dns.RR{
|
||||
test.A("badhost. 0 IN A 37.220.26.135"),
|
||||
}
|
||||
_ = w.WriteMsg(m)
|
||||
return dns.RcodeSuccess, nil
|
||||
})
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
safeservices "github.com/AdguardTeam/AdGuardDNS/dnsfilter/safe_services"
|
||||
"github.com/AdguardTeam/urlfilter"
|
||||
)
|
||||
|
||||
// Filtering engines are stored in this global map in order to avoid
|
||||
// excessive memory usage.
|
||||
// The key is path to the file with blocking rules.
|
||||
var enginesMap = make(map[string]*engineInfo)
|
||||
var enginesMapGuard = sync.Mutex{}
|
||||
|
||||
// engineInfo contains all the necessary information about DNS engines configuration.
|
||||
// we use it to periodically reload DNS engines.
|
||||
type engineInfo struct {
|
||||
filtersPaths []string
|
||||
dnsEngine *urlfilter.DNSEngine
|
||||
|
||||
data *safeservices.SafeService
|
||||
}
|
||||
|
||||
// getSafeBrowsingEngine returns the safebrowsing filtering engineInfo
|
||||
func (p *plug) getSafeBrowsingEngine() *engineInfo {
|
||||
enginesMapGuard.Lock()
|
||||
e, ok := enginesMap[p.settings.SafeBrowsingFilterPath]
|
||||
enginesMapGuard.Unlock()
|
||||
if ok {
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getParentalEngine returns the parental filtering engineInfo
|
||||
func (p *plug) getParentalEngine() *engineInfo {
|
||||
enginesMapGuard.Lock()
|
||||
e, ok := enginesMap[p.settings.ParentalFilterPath]
|
||||
enginesMapGuard.Unlock()
|
||||
if ok {
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getBlockingEngines returns the list of blocking engines
|
||||
func (p *plug) getBlockingEngine() *urlfilter.DNSEngine {
|
||||
enginesMapGuard.Lock()
|
||||
e, ok := enginesMap[p.settings.filterPathsKey]
|
||||
enginesMapGuard.Unlock()
|
||||
if ok {
|
||||
return e.dnsEngine
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func engineExists(key string) bool {
|
||||
enginesMapGuard.Lock()
|
||||
_, ok := enginesMap[key]
|
||||
enginesMapGuard.Unlock()
|
||||
return ok
|
||||
}
|
123
dnsfilter/geo.go
123
dnsfilter/geo.go
@ -1,123 +0,0 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
"github.com/miekg/dns"
|
||||
geoip2 "github.com/oschwald/geoip2-golang"
|
||||
)
|
||||
|
||||
// geoIP - global struct that holds GeoIP settings
|
||||
var geoIP = &GeoIP{}
|
||||
|
||||
// geoIPReloadCheckPeriod is a period that we use
|
||||
// if geoIP database has been changed
|
||||
const geoIPReloadCheckPeriod = time.Hour * 24
|
||||
|
||||
type GeoIP struct {
|
||||
dbPath string
|
||||
|
||||
reader *geoip2.Reader
|
||||
lastModTime time.Time
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func initGeoIP(settings plugSettings) error {
|
||||
if settings.GeoIPPath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if geoIP.reader != nil {
|
||||
// Already initialized
|
||||
clog.Info("GeoIP database has been already initialized")
|
||||
return nil
|
||||
}
|
||||
clog.Infof("Initializing GeoIP database: %s", settings.GeoIPPath)
|
||||
|
||||
geoIP.dbPath = settings.GeoIPPath
|
||||
fi, err := os.Stat(geoIP.dbPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
geoIP.lastModTime = fi.ModTime()
|
||||
|
||||
r, err := geoip2.Open(settings.GeoIPPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
geoIP.reader = r
|
||||
|
||||
go func() {
|
||||
for range time.Tick(geoIPReloadCheckPeriod) {
|
||||
geoIP.reload()
|
||||
}
|
||||
}()
|
||||
|
||||
clog.Infof("GeoIP database has been initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// getGeoData - gets geo data of the request IP
|
||||
// returns false if it cannot be determined
|
||||
// returns bool, country, continent
|
||||
func (g *GeoIP) getGeoData(w dns.ResponseWriter) (bool, string, string) {
|
||||
if geoIP.reader == nil {
|
||||
return false, "", ""
|
||||
}
|
||||
|
||||
g.RLock()
|
||||
defer g.RUnlock()
|
||||
|
||||
var ip net.IP
|
||||
|
||||
addr := w.RemoteAddr()
|
||||
switch v := addr.(type) {
|
||||
case *net.TCPAddr:
|
||||
ip = v.IP
|
||||
case *net.UDPAddr:
|
||||
ip = v.IP
|
||||
default:
|
||||
return false, "", ""
|
||||
}
|
||||
|
||||
c, err := geoIP.reader.Country(ip)
|
||||
if err != nil {
|
||||
clog.Errorf("failed to do the GeoIP lookup: %v", err)
|
||||
return false, "", ""
|
||||
}
|
||||
|
||||
country := c.Country.IsoCode
|
||||
continent := c.Continent.Code
|
||||
return true, country, continent
|
||||
}
|
||||
|
||||
// reload - periodically checks if we should reload GeoIP database
|
||||
func (g *GeoIP) reload() {
|
||||
fi, err := os.Stat(g.dbPath)
|
||||
if err != nil {
|
||||
clog.Errorf("failed to check GeoIP file state: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
lastModTime := fi.ModTime()
|
||||
if !lastModTime.After(g.lastModTime) {
|
||||
return
|
||||
}
|
||||
|
||||
clog.Info("Reloading GeoIP database")
|
||||
r, err := geoip2.Open(geoIP.dbPath)
|
||||
if err != nil {
|
||||
clog.Errorf("failed to load new GeoIP database: %v", err)
|
||||
}
|
||||
|
||||
g.Lock()
|
||||
geoIP.reader = r
|
||||
geoIP.lastModTime = lastModTime
|
||||
g.Unlock()
|
||||
clog.Info("GeoIP database has been reloaded")
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type TestGeoWriter struct {
|
||||
addr net.Addr
|
||||
dns.ResponseWriter
|
||||
}
|
||||
|
||||
func (w *TestGeoWriter) RemoteAddr() net.Addr {
|
||||
return w.addr
|
||||
}
|
||||
|
||||
func TestGeoIP(t *testing.T) {
|
||||
settings := plugSettings{
|
||||
GeoIPPath: "../tests/GeoIP2-Country-Test.mmdb",
|
||||
}
|
||||
|
||||
err := initGeoIP(settings)
|
||||
assert.Nil(t, err)
|
||||
|
||||
ok, country, continent := geoIP.getGeoData(&TestGeoWriter{
|
||||
addr: &net.TCPAddr{IP: net.IP{127, 0, 0, 1}},
|
||||
})
|
||||
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "", country)
|
||||
assert.Equal(t, "", continent)
|
||||
|
||||
ok, country, continent = geoIP.getGeoData(&TestGeoWriter{
|
||||
addr: &net.TCPAddr{IP: net.IP{81, 2, 69, 142}},
|
||||
})
|
||||
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "GB", country)
|
||||
assert.Equal(t, "EU", continent)
|
||||
}
|
@ -1,207 +0,0 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
"github.com/coredns/coredns/request"
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// lookup host, but return answer as if it was a result of different lookup
|
||||
// TODO: works only on A and AAAA, the go stdlib resolver can't do arbitrary types
|
||||
func lookupReplaced(host string, question dns.Question) ([]dns.RR, error) {
|
||||
var records []dns.RR
|
||||
var res *net.Resolver // nil resolver is default resolver
|
||||
switch question.Qtype {
|
||||
case dns.TypeA:
|
||||
addrs, err := res.LookupIPAddr(context.TODO(), host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if addr.IP.To4() != nil {
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s A %s", question.Name, addr.IP.String()))
|
||||
if err != nil {
|
||||
return nil, err // fail entire request, TODO: return partial request?
|
||||
}
|
||||
records = append(records, rr)
|
||||
}
|
||||
}
|
||||
case dns.TypeAAAA:
|
||||
addrs, err := res.LookupIPAddr(context.TODO(), host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if addr.IP.To4() == nil {
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s AAAA %s", question.Name, addr.IP.String()))
|
||||
if err != nil {
|
||||
return nil, err // fail entire request, TODO: return partial request?
|
||||
}
|
||||
records = append(records, rr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (p *plug) replaceHostWithValAndReply(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, host string, val string, question dns.Question) (int, error) {
|
||||
// check if it's a domain name or IP address
|
||||
addr := net.ParseIP(val)
|
||||
var records []dns.RR
|
||||
// log.Println("Will give", val, "instead of", host) // debug logging
|
||||
if addr != nil {
|
||||
// this is an IP address, return it
|
||||
result, err := dns.NewRR(fmt.Sprintf("%s %d A %s", host, p.settings.BlockedTTL, val))
|
||||
if err != nil {
|
||||
clog.Infof("Got error %s\n", err)
|
||||
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
|
||||
}
|
||||
records = append(records, result)
|
||||
} else {
|
||||
// this is a domain name, need to look it up
|
||||
var err error
|
||||
records, err = lookupReplaced(dns.Fqdn(val), question)
|
||||
if err != nil {
|
||||
clog.Infof("Got error %s\n", err)
|
||||
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
|
||||
}
|
||||
}
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.RecursionAvailable = true
|
||||
m.Compress = true
|
||||
m.Answer = append(m.Answer, records...)
|
||||
state := request.Request{W: w, Req: r}
|
||||
state.SizeAndDo(m)
|
||||
err := state.W.WriteMsg(m)
|
||||
if err != nil {
|
||||
clog.Infof("Got error %s\n", err)
|
||||
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
|
||||
}
|
||||
return dns.RcodeSuccess, nil
|
||||
}
|
||||
|
||||
// generate SOA record that makes DNS clients cache NXdomain results
|
||||
// the only value that is important is TTL in header, other values like refresh, retry, expire and minttl are irrelevant
|
||||
func (p *plug) genSOA(request *dns.Msg) []dns.RR {
|
||||
zone := ""
|
||||
if len(request.Question) > 0 {
|
||||
zone = request.Question[0].Name
|
||||
}
|
||||
|
||||
soa := dns.SOA{
|
||||
// values copied from verisign's nonexistent .com domain
|
||||
// their exact values are not important in our use case because they are used for domain transfers between primary/secondary DNS servers
|
||||
Refresh: 1800,
|
||||
Retry: 900,
|
||||
Expire: 604800,
|
||||
Minttl: 86400,
|
||||
// copied from AdGuard DNS
|
||||
Ns: "fake-for-negative-caching.adguard.com.",
|
||||
Serial: 100500,
|
||||
// rest is request-specific
|
||||
Hdr: dns.RR_Header{
|
||||
Name: zone,
|
||||
Rrtype: dns.TypeSOA,
|
||||
Ttl: p.settings.BlockedTTL,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
Mbox: "hostmaster.", // zone will be appended later if it's not empty or "."
|
||||
}
|
||||
if soa.Hdr.Ttl == 0 {
|
||||
soa.Hdr.Ttl = p.settings.BlockedTTL
|
||||
}
|
||||
if len(zone) > 0 && zone[0] != '.' {
|
||||
soa.Mbox += zone
|
||||
}
|
||||
return []dns.RR{&soa}
|
||||
}
|
||||
|
||||
func (p *plug) genARecord(request *dns.Msg, ip net.IP) *dns.Msg {
|
||||
resp := dns.Msg{}
|
||||
resp.SetReply(request)
|
||||
resp.Answer = append(resp.Answer, p.genAAnswer(request, ip))
|
||||
resp.RecursionAvailable = true
|
||||
resp.Compress = true
|
||||
return &resp
|
||||
}
|
||||
|
||||
func (p *plug) genAAAARecord(request *dns.Msg, ip net.IP) *dns.Msg {
|
||||
resp := dns.Msg{}
|
||||
resp.SetReply(request)
|
||||
resp.Answer = append(resp.Answer, p.genAAAAAnswer(request, ip))
|
||||
resp.RecursionAvailable = true
|
||||
resp.Compress = true
|
||||
return &resp
|
||||
}
|
||||
|
||||
func (p *plug) genAAnswer(req *dns.Msg, ip net.IP) *dns.A {
|
||||
answer := new(dns.A)
|
||||
answer.Hdr = dns.RR_Header{
|
||||
Name: req.Question[0].Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Ttl: p.settings.BlockedTTL,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
answer.A = ip
|
||||
return answer
|
||||
}
|
||||
|
||||
func (p *plug) genAAAAAnswer(req *dns.Msg, ip net.IP) *dns.AAAA {
|
||||
answer := new(dns.AAAA)
|
||||
answer.Hdr = dns.RR_Header{
|
||||
Name: req.Question[0].Name,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Ttl: p.settings.BlockedTTL,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
answer.AAAA = ip
|
||||
return answer
|
||||
}
|
||||
|
||||
func (p *plug) genNXDomain(request *dns.Msg) *dns.Msg {
|
||||
resp := dns.Msg{}
|
||||
resp.SetRcode(request, dns.RcodeNameError)
|
||||
resp.RecursionAvailable = true
|
||||
resp.Ns = p.genSOA(request)
|
||||
return &resp
|
||||
}
|
||||
|
||||
func (p *plug) writeNXDomain(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
m := p.genNXDomain(r)
|
||||
|
||||
state := request.Request{W: w, Req: r}
|
||||
state.SizeAndDo(m)
|
||||
err := state.W.WriteMsg(m)
|
||||
if err != nil {
|
||||
clog.Warningf("Got error %s\n", err)
|
||||
return dns.RcodeServerFailure, err
|
||||
}
|
||||
return dns.RcodeNameError, nil
|
||||
}
|
||||
|
||||
func (p *plug) writeBlacklistedResponse(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
var reply *dns.Msg
|
||||
|
||||
switch r.Question[0].Qtype {
|
||||
case dns.TypeA:
|
||||
reply = p.genARecord(r, []byte{0, 0, 0, 0})
|
||||
case dns.TypeAAAA:
|
||||
reply = p.genAAAARecord(r, net.IPv6zero)
|
||||
default:
|
||||
reply = p.genNXDomain(r)
|
||||
}
|
||||
|
||||
state := request.Request{W: w, Req: r}
|
||||
state.SizeAndDo(reply)
|
||||
err := state.W.WriteMsg(reply)
|
||||
if err != nil {
|
||||
clog.Warningf("Got error %s\n", err)
|
||||
return dns.RcodeServerFailure, err
|
||||
}
|
||||
return reply.Rcode, nil
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
// Variables declared for monitoring.
|
||||
var (
|
||||
requests = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "dnsfilter",
|
||||
Name: "requests_total",
|
||||
Help: "Count of requests seen by dnsfilter per continent, country.",
|
||||
}, []string{"continent", "country"})
|
||||
|
||||
filtered = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "dnsfilter",
|
||||
Name: "filtered_total",
|
||||
Help: "Count of requests filtered by dnsfilter per continent, country.",
|
||||
}, []string{"continent", "country"})
|
||||
|
||||
filteredLists = newCounter("filtered_lists_total", "Count of requests filtered by dnsfilter using lists.")
|
||||
filteredSafeBrowsing = newCounter("filtered_safebrowsing_total", "Count of requests filtered by dnsfilter using safebrowsing.")
|
||||
filteredParental = newCounter("filtered_parental_total", "Count of requests filtered by dnsfilter using parental.")
|
||||
safeSearch = newCounter("safesearch_total", "Count of requests replaced by dnsfilter safesearch.")
|
||||
|
||||
errorsTotal = newCounter("errors_total", "Count of requests that dnsfilter couldn't process because of transitive errors.")
|
||||
|
||||
requestsSafeBrowsingTXT = newCounter("requests_safebrowsing", "Safe-browsing TXT requests number.")
|
||||
requestsParentalTXT = newCounter("requests_parental", "Parental-control TXT requests number.")
|
||||
|
||||
elapsedTime = newHistogram("request_duration", "Histogram of the time (in seconds) each request took.")
|
||||
elapsedFilterTime = newHistogram("filter_duration", "Histogram of the time (in seconds) filtering of each request took.")
|
||||
|
||||
engineTimestamp = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "dnsfilter",
|
||||
Name: "engine_timestamp",
|
||||
Help: "Last time when the engines were initialized.",
|
||||
}, []string{"filter"})
|
||||
|
||||
engineSize = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "dnsfilter",
|
||||
Name: "engine_size",
|
||||
Help: "Count of rules in the filtering engine.",
|
||||
}, []string{"filter"})
|
||||
|
||||
engineStatus = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "dnsfilter",
|
||||
Name: "engine_status",
|
||||
Help: "Status of the filtering engine (1 for loaded successfully).",
|
||||
}, []string{"filter"})
|
||||
|
||||
statsCacheSize = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "dnsfilter",
|
||||
Name: "stats_cache_size",
|
||||
Help: "Count of recorded rule hits not yet dumped.",
|
||||
})
|
||||
|
||||
statsUploadStatus = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "dnsfilter",
|
||||
Name: "stats_upload_status",
|
||||
Help: "Status of the last stats upload.",
|
||||
})
|
||||
|
||||
statsUploadTimestamp = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "dnsfilter",
|
||||
Name: "stats_upload_timestamp",
|
||||
Help: "Time when stats where uploaded last time.",
|
||||
})
|
||||
)
|
||||
|
||||
func newCounter(name string, help string) prometheus.Counter {
|
||||
return promauto.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "dnsfilter",
|
||||
Name: name,
|
||||
Help: help,
|
||||
})
|
||||
}
|
||||
|
||||
func newHistogram(name string, help string) prometheus.Histogram {
|
||||
return promauto.NewHistogram(prometheus.HistogramOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "dnsfilter",
|
||||
Name: name,
|
||||
Help: help,
|
||||
})
|
||||
}
|
||||
|
||||
// incRequests - increments requests metric (if necessary)
|
||||
func incRequests(w dns.ResponseWriter) {
|
||||
_, country, continent := geoIP.getGeoData(w)
|
||||
requests.WithLabelValues(continent, country).Inc()
|
||||
}
|
||||
|
||||
// incFiltered - increments filtered metric (if necessary)
|
||||
func incFiltered(w dns.ResponseWriter) {
|
||||
_, country, continent := geoIP.getGeoData(w)
|
||||
filtered.WithLabelValues(continent, country).Inc()
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
"github.com/joomcode/errorx"
|
||||
)
|
||||
|
||||
// updateCheckPeriod is a period that dnsfilter uses to check for filters updates
|
||||
const updateCheckPeriod = time.Minute * 10
|
||||
|
||||
// updatesMap is used to store filter lists update information
|
||||
// every 10 minutes we're checking if it's time to check for the filter list updates
|
||||
// if it's time, and if the filter list was successfully updated,
|
||||
// we reload filtering engines
|
||||
var updatesMap = make(map[string]*updateInfo)
|
||||
var updatesMapGuard = sync.Mutex{}
|
||||
|
||||
// Start reloading goroutine right away
|
||||
func init() {
|
||||
go reload()
|
||||
}
|
||||
|
||||
func reload() {
|
||||
// Wait first time
|
||||
time.Sleep(updateCheckPeriod)
|
||||
|
||||
for range time.Tick(updateCheckPeriod) {
|
||||
if updateCheck() {
|
||||
reloadEngines()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateCheck - checks and download updates if necessary
|
||||
// returns true if at least one filter list was updated
|
||||
func updateCheck() bool {
|
||||
wasUpdated := false
|
||||
|
||||
updatesMapGuard.Lock()
|
||||
for key, u := range updatesMap {
|
||||
updated, err := u.update()
|
||||
if err != nil {
|
||||
clog.Errorf("Failed to check updates for %s: %v", key, err)
|
||||
}
|
||||
if updated {
|
||||
wasUpdated = true
|
||||
}
|
||||
}
|
||||
updatesMapGuard.Unlock()
|
||||
|
||||
return wasUpdated
|
||||
}
|
||||
|
||||
// reloadEngines reloads all filter lists from the files
|
||||
func reloadEngines() {
|
||||
clog.Info("Start reloading filters")
|
||||
enginesMapCopy := make(map[string]*engineInfo)
|
||||
|
||||
enginesMapGuard.Lock()
|
||||
for key, engine := range enginesMap {
|
||||
enginesMapCopy[key] = engine
|
||||
}
|
||||
enginesMapGuard.Unlock()
|
||||
|
||||
for key, engine := range enginesMapCopy {
|
||||
// TODO: maybe panic would be better here?
|
||||
_ = reloadEngine(key, engine)
|
||||
}
|
||||
clog.Info("Finished reloading filters")
|
||||
}
|
||||
|
||||
// reloadEngine reloads DNS engine and replaces it in the enginesMap
|
||||
func reloadEngine(key string, engine *engineInfo) error {
|
||||
clog.Infof("Reloading filtering engine for %s", key)
|
||||
|
||||
cnt := 0
|
||||
var newEngine *engineInfo
|
||||
if engine.dnsEngine == nil {
|
||||
engine, count, err := createSecurityServiceEngine(key)
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "cannot create DNS engine: %s", engine.filtersPaths[0])
|
||||
}
|
||||
newEngine = engine
|
||||
cnt = count
|
||||
} else {
|
||||
e, count, err := newDNSEngine(engine.filtersPaths)
|
||||
if err != nil {
|
||||
engineStatus.WithLabelValues(key).Set(float64(0))
|
||||
clog.Errorf("failed to reload engine: %s", err)
|
||||
return errorx.Decorate(err, "failed to reload engine for %s", key)
|
||||
}
|
||||
newEngine = &engineInfo{
|
||||
dnsEngine: e,
|
||||
filtersPaths: engine.filtersPaths,
|
||||
}
|
||||
cnt = count
|
||||
}
|
||||
|
||||
enginesMapGuard.Lock()
|
||||
enginesMap[key] = newEngine
|
||||
enginesMapGuard.Unlock()
|
||||
|
||||
engineStatus.WithLabelValues(key).Set(float64(1))
|
||||
engineSize.WithLabelValues(key).Set(float64(cnt))
|
||||
engineTimestamp.WithLabelValues(key).SetToCurrentTime()
|
||||
clog.Infof("Finished reloading filtering engine for %s", key)
|
||||
return nil
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
# Parental Control and SafeBrowsing
|
||||
|
||||
## Initialization
|
||||
|
||||
Input data is a file with the list of host names that must be blocked (both PC & SB services have their own filter file):
|
||||
|
||||
badsite1
|
||||
badsite2
|
||||
...
|
||||
|
||||
When PC/SB services are initializing they:
|
||||
|
||||
* get the total number of lines in file and create a hash map
|
||||
* read the file line by line
|
||||
* get SHA256 hash sum of the host name
|
||||
* add the sum value into the hash map as shown below
|
||||
|
||||
Suppose that there are 2 host names with similar hash sums:
|
||||
|
||||
01abcdef1234...
|
||||
01abcdef0987...
|
||||
|
||||
Add these hashes to the hash map like so that:
|
||||
|
||||
* the key equals to bytes [0..1] of each hash sum
|
||||
* the value equals to an array of bytes [2..31] of each hash sum
|
||||
|
||||
e.g.:
|
||||
|
||||
"01ab" -> []{
|
||||
"cdef1234...",
|
||||
"cdef0987..."
|
||||
}
|
||||
|
||||
And for a faster search we sort the hashes:
|
||||
|
||||
"01ab" -> []{
|
||||
"cdef0987..."
|
||||
"cdef1234...",
|
||||
}
|
||||
|
||||
## DNS messages
|
||||
|
||||
To check if the host is blocked, a client sends a TXT record with the Name field equal to the hash value of the host name.
|
||||
|
||||
DNS Question:
|
||||
NAME=[0x04 "01ab" 0x04 "2345" 0x02 "sb" 0x03 "dns" 0x07 "adguard" 0x03 "com" 0x00]
|
||||
TYPE=TXT
|
||||
CLASS=IN
|
||||
|
||||
Legacy mode is also supported where the length of 1 hash is 8 characters, not 4.
|
||||
|
||||
For the server to distinguish between SB or PC requests, the Name field in the question has either "pc" or "sb" suffix. For example, the Name in the previous request, only now for PC service, will look like this:
|
||||
|
||||
NAME=[0x04 "01ab" 0x04 "2345" 0x02 "pc" 0x03 "dns" 0x07 "adguard" 0x03 "com" 0x00]
|
||||
|
||||
In this request a client wants to check 2 domains with the hash sums starting with "01ab" and "2345".
|
||||
|
||||
The response to this request is the list of SHA256 hash values that start with "01ab" and "2345".
|
||||
|
||||
DNS Answers:
|
||||
[0]:
|
||||
NAME=[0x04 "01ab" 0x04 "2345" 0x02 "sb" ...]
|
||||
TYPE=TXT
|
||||
CLASS=IN
|
||||
TTL=...
|
||||
LENGTH=...
|
||||
DATA=["01abcdef1234...", "01abcdef0987...", "23456789abcd..." ]
|
||||
|
||||
Upon receiving the response the client compares each hash value with its target host.
|
||||
If the hash values match, it means that this host is blocked by PC/SB services.
|
||||
|
||||
Note that since neither the client nor the server trasmits the full host name along with its hash sum, there may be a chance of a hash collision and so the host which is not in the blocklist will be treated as blocked.
|
@ -1,216 +0,0 @@
|
||||
package safeservices
|
||||
|
||||
var SafeSearchDomains = map[string]string{
|
||||
"yandex.com": "213.180.193.56",
|
||||
"yandex.ru": "213.180.193.56",
|
||||
"yandex.ua": "213.180.193.56",
|
||||
"yandex.by": "213.180.193.56",
|
||||
"yandex.kz": "213.180.193.56",
|
||||
"www.yandex.com": "213.180.193.56",
|
||||
"www.yandex.ru": "213.180.193.56",
|
||||
"www.yandex.ua": "213.180.193.56",
|
||||
"www.yandex.by": "213.180.193.56",
|
||||
"www.yandex.kz": "213.180.193.56",
|
||||
|
||||
"www.bing.com": "strict.bing.com",
|
||||
|
||||
"duckduckgo.com": "safe.duckduckgo.com",
|
||||
"www.duckduckgo.com": "safe.duckduckgo.com",
|
||||
"start.duckduckgo.com": "safe.duckduckgo.com",
|
||||
|
||||
"www.google.com": "forcesafesearch.google.com",
|
||||
"www.google.ad": "forcesafesearch.google.com",
|
||||
"www.google.ae": "forcesafesearch.google.com",
|
||||
"www.google.com.af": "forcesafesearch.google.com",
|
||||
"www.google.com.ag": "forcesafesearch.google.com",
|
||||
"www.google.com.ai": "forcesafesearch.google.com",
|
||||
"www.google.al": "forcesafesearch.google.com",
|
||||
"www.google.am": "forcesafesearch.google.com",
|
||||
"www.google.co.ao": "forcesafesearch.google.com",
|
||||
"www.google.com.ar": "forcesafesearch.google.com",
|
||||
"www.google.as": "forcesafesearch.google.com",
|
||||
"www.google.at": "forcesafesearch.google.com",
|
||||
"www.google.com.au": "forcesafesearch.google.com",
|
||||
"www.google.az": "forcesafesearch.google.com",
|
||||
"www.google.ba": "forcesafesearch.google.com",
|
||||
"www.google.com.bd": "forcesafesearch.google.com",
|
||||
"www.google.be": "forcesafesearch.google.com",
|
||||
"www.google.bf": "forcesafesearch.google.com",
|
||||
"www.google.bg": "forcesafesearch.google.com",
|
||||
"www.google.com.bh": "forcesafesearch.google.com",
|
||||
"www.google.bi": "forcesafesearch.google.com",
|
||||
"www.google.bj": "forcesafesearch.google.com",
|
||||
"www.google.com.bn": "forcesafesearch.google.com",
|
||||
"www.google.com.bo": "forcesafesearch.google.com",
|
||||
"www.google.com.br": "forcesafesearch.google.com",
|
||||
"www.google.bs": "forcesafesearch.google.com",
|
||||
"www.google.bt": "forcesafesearch.google.com",
|
||||
"www.google.co.bw": "forcesafesearch.google.com",
|
||||
"www.google.by": "forcesafesearch.google.com",
|
||||
"www.google.com.bz": "forcesafesearch.google.com",
|
||||
"www.google.ca": "forcesafesearch.google.com",
|
||||
"www.google.cd": "forcesafesearch.google.com",
|
||||
"www.google.cf": "forcesafesearch.google.com",
|
||||
"www.google.cg": "forcesafesearch.google.com",
|
||||
"www.google.ch": "forcesafesearch.google.com",
|
||||
"www.google.ci": "forcesafesearch.google.com",
|
||||
"www.google.co.ck": "forcesafesearch.google.com",
|
||||
"www.google.cl": "forcesafesearch.google.com",
|
||||
"www.google.cm": "forcesafesearch.google.com",
|
||||
"www.google.cn": "forcesafesearch.google.com",
|
||||
"www.google.com.co": "forcesafesearch.google.com",
|
||||
"www.google.co.cr": "forcesafesearch.google.com",
|
||||
"www.google.com.cu": "forcesafesearch.google.com",
|
||||
"www.google.cv": "forcesafesearch.google.com",
|
||||
"www.google.com.cy": "forcesafesearch.google.com",
|
||||
"www.google.cz": "forcesafesearch.google.com",
|
||||
"www.google.de": "forcesafesearch.google.com",
|
||||
"www.google.dj": "forcesafesearch.google.com",
|
||||
"www.google.dk": "forcesafesearch.google.com",
|
||||
"www.google.dm": "forcesafesearch.google.com",
|
||||
"www.google.com.do": "forcesafesearch.google.com",
|
||||
"www.google.dz": "forcesafesearch.google.com",
|
||||
"www.google.com.ec": "forcesafesearch.google.com",
|
||||
"www.google.ee": "forcesafesearch.google.com",
|
||||
"www.google.com.eg": "forcesafesearch.google.com",
|
||||
"www.google.es": "forcesafesearch.google.com",
|
||||
"www.google.com.et": "forcesafesearch.google.com",
|
||||
"www.google.fi": "forcesafesearch.google.com",
|
||||
"www.google.com.fj": "forcesafesearch.google.com",
|
||||
"www.google.fm": "forcesafesearch.google.com",
|
||||
"www.google.fr": "forcesafesearch.google.com",
|
||||
"www.google.ga": "forcesafesearch.google.com",
|
||||
"www.google.ge": "forcesafesearch.google.com",
|
||||
"www.google.gg": "forcesafesearch.google.com",
|
||||
"www.google.com.gh": "forcesafesearch.google.com",
|
||||
"www.google.com.gi": "forcesafesearch.google.com",
|
||||
"www.google.gl": "forcesafesearch.google.com",
|
||||
"www.google.gm": "forcesafesearch.google.com",
|
||||
"www.google.gp": "forcesafesearch.google.com",
|
||||
"www.google.gr": "forcesafesearch.google.com",
|
||||
"www.google.com.gt": "forcesafesearch.google.com",
|
||||
"www.google.gy": "forcesafesearch.google.com",
|
||||
"www.google.com.hk": "forcesafesearch.google.com",
|
||||
"www.google.hn": "forcesafesearch.google.com",
|
||||
"www.google.hr": "forcesafesearch.google.com",
|
||||
"www.google.ht": "forcesafesearch.google.com",
|
||||
"www.google.hu": "forcesafesearch.google.com",
|
||||
"www.google.co.id": "forcesafesearch.google.com",
|
||||
"www.google.ie": "forcesafesearch.google.com",
|
||||
"www.google.co.il": "forcesafesearch.google.com",
|
||||
"www.google.im": "forcesafesearch.google.com",
|
||||
"www.google.co.in": "forcesafesearch.google.com",
|
||||
"www.google.iq": "forcesafesearch.google.com",
|
||||
"www.google.is": "forcesafesearch.google.com",
|
||||
"www.google.it": "forcesafesearch.google.com",
|
||||
"www.google.je": "forcesafesearch.google.com",
|
||||
"www.google.com.jm": "forcesafesearch.google.com",
|
||||
"www.google.jo": "forcesafesearch.google.com",
|
||||
"www.google.co.jp": "forcesafesearch.google.com",
|
||||
"www.google.co.ke": "forcesafesearch.google.com",
|
||||
"www.google.com.kh": "forcesafesearch.google.com",
|
||||
"www.google.ki": "forcesafesearch.google.com",
|
||||
"www.google.kg": "forcesafesearch.google.com",
|
||||
"www.google.co.kr": "forcesafesearch.google.com",
|
||||
"www.google.com.kw": "forcesafesearch.google.com",
|
||||
"www.google.kz": "forcesafesearch.google.com",
|
||||
"www.google.la": "forcesafesearch.google.com",
|
||||
"www.google.com.lb": "forcesafesearch.google.com",
|
||||
"www.google.li": "forcesafesearch.google.com",
|
||||
"www.google.lk": "forcesafesearch.google.com",
|
||||
"www.google.co.ls": "forcesafesearch.google.com",
|
||||
"www.google.lt": "forcesafesearch.google.com",
|
||||
"www.google.lu": "forcesafesearch.google.com",
|
||||
"www.google.lv": "forcesafesearch.google.com",
|
||||
"www.google.com.ly": "forcesafesearch.google.com",
|
||||
"www.google.co.ma": "forcesafesearch.google.com",
|
||||
"www.google.md": "forcesafesearch.google.com",
|
||||
"www.google.me": "forcesafesearch.google.com",
|
||||
"www.google.mg": "forcesafesearch.google.com",
|
||||
"www.google.mk": "forcesafesearch.google.com",
|
||||
"www.google.ml": "forcesafesearch.google.com",
|
||||
"www.google.com.mm": "forcesafesearch.google.com",
|
||||
"www.google.mn": "forcesafesearch.google.com",
|
||||
"www.google.ms": "forcesafesearch.google.com",
|
||||
"www.google.com.mt": "forcesafesearch.google.com",
|
||||
"www.google.mu": "forcesafesearch.google.com",
|
||||
"www.google.mv": "forcesafesearch.google.com",
|
||||
"www.google.mw": "forcesafesearch.google.com",
|
||||
"www.google.com.mx": "forcesafesearch.google.com",
|
||||
"www.google.com.my": "forcesafesearch.google.com",
|
||||
"www.google.co.mz": "forcesafesearch.google.com",
|
||||
"www.google.com.na": "forcesafesearch.google.com",
|
||||
"www.google.com.nf": "forcesafesearch.google.com",
|
||||
"www.google.com.ng": "forcesafesearch.google.com",
|
||||
"www.google.com.ni": "forcesafesearch.google.com",
|
||||
"www.google.ne": "forcesafesearch.google.com",
|
||||
"www.google.nl": "forcesafesearch.google.com",
|
||||
"www.google.no": "forcesafesearch.google.com",
|
||||
"www.google.com.np": "forcesafesearch.google.com",
|
||||
"www.google.nr": "forcesafesearch.google.com",
|
||||
"www.google.nu": "forcesafesearch.google.com",
|
||||
"www.google.co.nz": "forcesafesearch.google.com",
|
||||
"www.google.com.om": "forcesafesearch.google.com",
|
||||
"www.google.com.pa": "forcesafesearch.google.com",
|
||||
"www.google.com.pe": "forcesafesearch.google.com",
|
||||
"www.google.com.pg": "forcesafesearch.google.com",
|
||||
"www.google.com.ph": "forcesafesearch.google.com",
|
||||
"www.google.com.pk": "forcesafesearch.google.com",
|
||||
"www.google.pl": "forcesafesearch.google.com",
|
||||
"www.google.pn": "forcesafesearch.google.com",
|
||||
"www.google.com.pr": "forcesafesearch.google.com",
|
||||
"www.google.ps": "forcesafesearch.google.com",
|
||||
"www.google.pt": "forcesafesearch.google.com",
|
||||
"www.google.com.py": "forcesafesearch.google.com",
|
||||
"www.google.com.qa": "forcesafesearch.google.com",
|
||||
"www.google.ro": "forcesafesearch.google.com",
|
||||
"www.google.ru": "forcesafesearch.google.com",
|
||||
"www.google.rw": "forcesafesearch.google.com",
|
||||
"www.google.com.sa": "forcesafesearch.google.com",
|
||||
"www.google.com.sb": "forcesafesearch.google.com",
|
||||
"www.google.sc": "forcesafesearch.google.com",
|
||||
"www.google.se": "forcesafesearch.google.com",
|
||||
"www.google.com.sg": "forcesafesearch.google.com",
|
||||
"www.google.sh": "forcesafesearch.google.com",
|
||||
"www.google.si": "forcesafesearch.google.com",
|
||||
"www.google.sk": "forcesafesearch.google.com",
|
||||
"www.google.com.sl": "forcesafesearch.google.com",
|
||||
"www.google.sn": "forcesafesearch.google.com",
|
||||
"www.google.so": "forcesafesearch.google.com",
|
||||
"www.google.sm": "forcesafesearch.google.com",
|
||||
"www.google.sr": "forcesafesearch.google.com",
|
||||
"www.google.st": "forcesafesearch.google.com",
|
||||
"www.google.com.sv": "forcesafesearch.google.com",
|
||||
"www.google.td": "forcesafesearch.google.com",
|
||||
"www.google.tg": "forcesafesearch.google.com",
|
||||
"www.google.co.th": "forcesafesearch.google.com",
|
||||
"www.google.com.tj": "forcesafesearch.google.com",
|
||||
"www.google.tk": "forcesafesearch.google.com",
|
||||
"www.google.tl": "forcesafesearch.google.com",
|
||||
"www.google.tm": "forcesafesearch.google.com",
|
||||
"www.google.tn": "forcesafesearch.google.com",
|
||||
"www.google.to": "forcesafesearch.google.com",
|
||||
"www.google.com.tr": "forcesafesearch.google.com",
|
||||
"www.google.tt": "forcesafesearch.google.com",
|
||||
"www.google.com.tw": "forcesafesearch.google.com",
|
||||
"www.google.co.tz": "forcesafesearch.google.com",
|
||||
"www.google.com.ua": "forcesafesearch.google.com",
|
||||
"www.google.co.ug": "forcesafesearch.google.com",
|
||||
"www.google.co.uk": "forcesafesearch.google.com",
|
||||
"www.google.com.uy": "forcesafesearch.google.com",
|
||||
"www.google.co.uz": "forcesafesearch.google.com",
|
||||
"www.google.com.vc": "forcesafesearch.google.com",
|
||||
"www.google.co.ve": "forcesafesearch.google.com",
|
||||
"www.google.vg": "forcesafesearch.google.com",
|
||||
"www.google.co.vi": "forcesafesearch.google.com",
|
||||
"www.google.com.vn": "forcesafesearch.google.com",
|
||||
"www.google.vu": "forcesafesearch.google.com",
|
||||
"www.google.ws": "forcesafesearch.google.com",
|
||||
"www.google.rs": "forcesafesearch.google.com",
|
||||
|
||||
"www.youtube.com": "restrictmoderate.youtube.com",
|
||||
"m.youtube.com": "restrictmoderate.youtube.com",
|
||||
"youtubei.googleapis.com": "restrictmoderate.youtube.com",
|
||||
"youtube.googleapis.com": "restrictmoderate.youtube.com",
|
||||
"www.youtube-nocookie.com": "restrictmoderate.youtube.com",
|
||||
}
|
@ -1,183 +0,0 @@
|
||||
// Safe Browsing and Parental Control services
|
||||
|
||||
package safeservices
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
)
|
||||
|
||||
// SafeService - safe service object
|
||||
type SafeService struct {
|
||||
// Data for safe-browsing and parental
|
||||
// The key is the first 2 bytes of hash value.
|
||||
// The value is a byte-array with 30-byte chunks of data.
|
||||
// These chunks are sorted alphabetically.
|
||||
// map[2_BYTE_HASH_CHUNK] = 30_BYTE_HASH1_CHUNK 30_BYTE_HASH2_CHUNK ...
|
||||
data map[uint16][]byte
|
||||
}
|
||||
|
||||
// Return the next non-empty line
|
||||
func nextLine(reader *bufio.Reader) string {
|
||||
for {
|
||||
bytes, err := reader.ReadBytes('\n')
|
||||
if len(bytes) != 0 {
|
||||
if err == nil {
|
||||
return string(bytes[:len(bytes)-1])
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get key for hash map
|
||||
func getKey(hash2 []byte) uint16 {
|
||||
return binary.BigEndian.Uint16(hash2)
|
||||
}
|
||||
|
||||
type hashSort struct {
|
||||
data []byte // 30-byte chunks
|
||||
}
|
||||
|
||||
func (hs *hashSort) Len() int {
|
||||
return len(hs.data) / 30
|
||||
}
|
||||
func (hs *hashSort) Less(i, j int) bool {
|
||||
r := bytes.Compare(hs.data[i*30:i*30+30], hs.data[j*30:j*30+30])
|
||||
return r < 0
|
||||
}
|
||||
func (hs *hashSort) Swap(i, j int) {
|
||||
tmp := make([]byte, 30)
|
||||
copy(tmp, hs.data[i*30:i*30+30])
|
||||
copy(hs.data[i*30:i*30+30], hs.data[j*30:j*30+30])
|
||||
copy(hs.data[j*30:j*30+30], tmp)
|
||||
}
|
||||
|
||||
// CreateMap - read input file and fill the hash map
|
||||
func CreateMap(file string) (*SafeService, int, error) {
|
||||
clog.Infof("Initializing: %s", file)
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
reader := bufio.NewReaderSize(f, 4*1024)
|
||||
|
||||
lines := 0
|
||||
for {
|
||||
ln := nextLine(reader)
|
||||
if len(ln) == 0 {
|
||||
break
|
||||
}
|
||||
lines++
|
||||
}
|
||||
|
||||
data := make(map[uint16][]byte, lines)
|
||||
|
||||
_, _ = f.Seek(0, 0)
|
||||
reader.Reset(f)
|
||||
|
||||
for {
|
||||
ln := nextLine(reader)
|
||||
if len(ln) == 0 {
|
||||
break
|
||||
}
|
||||
ln = strings.TrimSpace(ln)
|
||||
if len(ln) == 0 || ln[0] == '#' {
|
||||
continue
|
||||
}
|
||||
|
||||
hash := sha256.Sum256([]byte(ln))
|
||||
key := getKey(hash[0:2])
|
||||
ar, _ := data[key]
|
||||
ar = append(ar, hash[2:]...)
|
||||
data[key] = ar
|
||||
}
|
||||
|
||||
// sort the 30-byte chunks within the map's values
|
||||
for k, v := range data {
|
||||
hashSorter := hashSort{data: v}
|
||||
sort.Sort(&hashSorter)
|
||||
data[k] = hashSorter.data
|
||||
}
|
||||
|
||||
clog.Infof("Finished initialization: processed %d entries", lines)
|
||||
return &SafeService{data: data}, lines, nil
|
||||
}
|
||||
|
||||
// MatchHashes - get the list of hash values matching the input string
|
||||
func (ss *SafeService) MatchHashes(hashStr string) ([]string, error) {
|
||||
result := []string{}
|
||||
|
||||
hashChunks := strings.Split(hashStr, ".")
|
||||
|
||||
for _, chunk := range hashChunks {
|
||||
hash2, err := hex.DecodeString(chunk)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
if len(hash2) == 4 { // legacy mode
|
||||
hash2 = hash2[0:2]
|
||||
}
|
||||
|
||||
if len(hash2) != 2 {
|
||||
return []string{}, fmt.Errorf("bad hash length: %d", len(hash2))
|
||||
}
|
||||
|
||||
hashes, _ := ss.data[getKey(hash2)]
|
||||
i := 0
|
||||
for i != len(hashes) {
|
||||
hash30 := hashes[i : i+30]
|
||||
i += 30
|
||||
hash := hash2
|
||||
hash = append(hash, hash30...)
|
||||
result = append(result, hex.EncodeToString(hash))
|
||||
}
|
||||
}
|
||||
|
||||
clog.Debugf("SB/PC: matched: %s: %v", hashStr, result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Search 30-byte data in array of 30-byte chunks
|
||||
func searchHash(hashes []byte, search []byte) bool {
|
||||
start := 0
|
||||
end := len(hashes) / 30
|
||||
for start != end {
|
||||
i := start + (end-start)/2
|
||||
r := bytes.Compare(hashes[i*30:i*30+30], search)
|
||||
if r == 0 {
|
||||
return true
|
||||
} else if r > 0 {
|
||||
end = i
|
||||
} else {
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MatchHost - return TRUE if the host is found
|
||||
func (ss *SafeService) MatchHost(host string) bool {
|
||||
hashHost := sha256.Sum256([]byte(host))
|
||||
hashes, _ := ss.data[getKey(hashHost[0:2])]
|
||||
|
||||
if searchHash(hashes, hashHost[2:32]) {
|
||||
clog.Debugf("SB/PC: matched: %s", host)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
package safeservices
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPrepareData(t *testing.T) {
|
||||
// fill with test data
|
||||
hashes := make([]byte, 30*5)
|
||||
i := 0
|
||||
copy(hashes[i:i+30], []byte("123456789012345678901234567898"))
|
||||
i += 30
|
||||
copy(hashes[i:i+30], []byte("123456789012345678901234567894"))
|
||||
i += 30
|
||||
copy(hashes[i:i+30], []byte("123456789012345678901234567896"))
|
||||
i += 30
|
||||
copy(hashes[i:i+30], []byte("123456789012345678901234567892"))
|
||||
i += 30
|
||||
copy(hashes[i:i+30], []byte("123456789012345678901234567890"))
|
||||
|
||||
// sort
|
||||
hashSorter := hashSort{data: hashes}
|
||||
sort.Sort(&hashSorter)
|
||||
hashes = hashSorter.data
|
||||
|
||||
// check sorting
|
||||
i = 0
|
||||
assert.Equal(t, "123456789012345678901234567890", string(hashes[i:i+30]))
|
||||
i += 30
|
||||
assert.Equal(t, "123456789012345678901234567892", string(hashes[i:i+30]))
|
||||
i += 30
|
||||
assert.Equal(t, "123456789012345678901234567894", string(hashes[i:i+30]))
|
||||
i += 30
|
||||
assert.Equal(t, "123456789012345678901234567896", string(hashes[i:i+30]))
|
||||
i += 30
|
||||
assert.Equal(t, "123456789012345678901234567898", string(hashes[i:i+30]))
|
||||
i += 30
|
||||
|
||||
assert.False(t, searchHash(hashes, []byte("123456789012345678901234567891")))
|
||||
assert.True(t, searchHash(hashes, []byte("123456789012345678901234567890")))
|
||||
assert.True(t, searchHash(hashes, []byte("123456789012345678901234567892")))
|
||||
assert.True(t, searchHash(hashes, []byte("123456789012345678901234567894")))
|
||||
assert.True(t, searchHash(hashes, []byte("123456789012345678901234567896")))
|
||||
assert.True(t, searchHash(hashes, []byte("123456789012345678901234567898")))
|
||||
}
|
@ -1,382 +0,0 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
safeservices "github.com/AdguardTeam/AdGuardDNS/dnsfilter/safe_services"
|
||||
"github.com/AdguardTeam/urlfilter/filterlist"
|
||||
|
||||
"github.com/joomcode/errorx"
|
||||
|
||||
"github.com/AdguardTeam/urlfilter"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("dnsfilter", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
// plugSettings is the dnsfilter plugin settings
|
||||
type plugSettings struct {
|
||||
SafeSearchEnabled bool // If true -- safe search is enabled
|
||||
|
||||
SafeBrowsingEnabled bool // If true -- check requests against the safebrowsing filter list
|
||||
SafeBrowsingBlockHost string // Hostname to use for requests blocked by safebrowsing
|
||||
SafeBrowsingFilterPath string // Path to the safebrowsing filter list
|
||||
|
||||
ParentalEnabled bool // If true -- check requests against the parental filter list
|
||||
ParentalBlockHost string // Hostname to use for requests blocked by parental control
|
||||
ParentalFilterPath string // Path to the parental filter list
|
||||
|
||||
BlockedTTL uint32 // in seconds, default 3600
|
||||
FilterPaths []string // List of filter lists for blocking ad/tracker request
|
||||
|
||||
GeoIPPath string // Path to the GeoIP2 database
|
||||
|
||||
// Update - map of update info for the filter lists
|
||||
// It includes safebrowsing and parental filter lists
|
||||
// Key is path to the filter list file
|
||||
Update map[string]*updateInfo
|
||||
|
||||
// filterPathsKey is a key for the enginesMap to store the blockFilterEngine.
|
||||
// it is composed from sorted FilterPaths joined by '#'
|
||||
filterPathsKey string
|
||||
}
|
||||
|
||||
// plug represents the plugin itself
|
||||
type plug struct {
|
||||
Next plugin.Handler
|
||||
settings plugSettings
|
||||
}
|
||||
|
||||
// Name returns name of the plugin as seen in Corefile and plugin.cfg
|
||||
func (p *plug) Name() string { return "dnsfilter" }
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
clog.Infof("Initializing the dnsfilter plugin for %s", c.ServerBlockKeys[c.ServerBlockKeyIndex])
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := dnsserver.GetConfig(c)
|
||||
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
p.Next = next
|
||||
return p
|
||||
})
|
||||
|
||||
c.OnStartup(func() error {
|
||||
// Set to 1 by default
|
||||
statsUploadStatus.Set(float64(1))
|
||||
return nil
|
||||
})
|
||||
clog.Infof("Finished initializing the dnsfilter plugin for %s", c.ServerBlockKeys[c.ServerBlockKeyIndex])
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupPlugin initializes the CoreDNS plugin
|
||||
func setupPlugin(c *caddy.Controller) (*plug, error) {
|
||||
settings, err := parseSettings(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// It's important to call it before the initEnginesMap
|
||||
// Because at this point we may need to download filter lists
|
||||
// and this must be done before we attempt to init engines
|
||||
err = initUpdatesMap(settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = initEnginesMap(settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = initGeoIP(settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clog.Infof("Initialized dnsfilter settings for server block %d", c.ServerBlockIndex)
|
||||
return &plug{settings: settings}, nil
|
||||
}
|
||||
|
||||
func parseSettings(c *caddy.Controller) (plugSettings, error) {
|
||||
settings := defaultPluginSettings
|
||||
|
||||
for c.Next() {
|
||||
for c.NextBlock() {
|
||||
blockValue := c.Val()
|
||||
switch blockValue {
|
||||
case "safebrowsing":
|
||||
err := setupSafeBrowsing(c, &settings)
|
||||
if err != nil {
|
||||
return settings, err
|
||||
}
|
||||
case "safesearch":
|
||||
clog.Info("Safe search is enabled")
|
||||
settings.SafeSearchEnabled = true
|
||||
case "parental":
|
||||
err := setupParental(c, &settings)
|
||||
if err != nil {
|
||||
return settings, err
|
||||
}
|
||||
case "geoip":
|
||||
if !c.NextArg() || len(c.Val()) == 0 {
|
||||
clog.Info("GeoIP database is not configured")
|
||||
return settings, c.ArgErr()
|
||||
}
|
||||
settings.GeoIPPath = c.Val()
|
||||
case "blocked_ttl":
|
||||
if !c.NextArg() {
|
||||
return settings, c.ArgErr()
|
||||
}
|
||||
blockedTTL, err := strconv.ParseUint(c.Val(), 10, 32)
|
||||
if err != nil {
|
||||
return settings, c.ArgErr()
|
||||
}
|
||||
clog.Infof("Blocked request TTL is %d", blockedTTL)
|
||||
settings.BlockedTTL = uint32(blockedTTL)
|
||||
case "filter":
|
||||
if !c.NextArg() || len(c.Val()) == 0 {
|
||||
return settings, c.ArgErr()
|
||||
}
|
||||
|
||||
// Initialize filter and add it to the list
|
||||
path := c.Val()
|
||||
settings.FilterPaths = append(settings.FilterPaths, path)
|
||||
clog.Infof("Added filter list %s", path)
|
||||
|
||||
err := setupUpdateInfo(path, &settings, c)
|
||||
if err != nil {
|
||||
clog.Errorf("Failed to setup update info: %v", err)
|
||||
return settings, c.ArgErr()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(settings.FilterPaths)
|
||||
settings.filterPathsKey = strings.Join(settings.FilterPaths, "#")
|
||||
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// defaultPluginSettings -- settings to use if nothing is configured
|
||||
var defaultPluginSettings = plugSettings{
|
||||
SafeSearchEnabled: false,
|
||||
SafeBrowsingEnabled: false,
|
||||
SafeBrowsingBlockHost: "standard-block.dns.adguard.com",
|
||||
SafeBrowsingFilterPath: "",
|
||||
ParentalEnabled: false,
|
||||
ParentalBlockHost: "family-block.dns.adguard.com",
|
||||
ParentalFilterPath: "",
|
||||
BlockedTTL: 3600, // in seconds
|
||||
FilterPaths: make([]string, 0),
|
||||
Update: map[string]*updateInfo{},
|
||||
}
|
||||
|
||||
// setupUpdateInfo configures updateInfo for the specified filter list
|
||||
func setupUpdateInfo(path string, settings *plugSettings, c *caddy.Controller) error {
|
||||
u := &updateInfo{
|
||||
path: path,
|
||||
ttl: 1 * time.Hour,
|
||||
lastTimeUpdated: time.Now(),
|
||||
}
|
||||
|
||||
if c.NextArg() && len(c.Val()) > 0 {
|
||||
u.url = c.Val()
|
||||
clog.Infof("%s update URL is %s", path, u.url)
|
||||
}
|
||||
|
||||
if c.NextArg() && len(c.Val()) > 0 {
|
||||
ttl, err := strconv.Atoi(c.Val())
|
||||
if err != nil || ttl <= 0 {
|
||||
return c.ArgErr()
|
||||
}
|
||||
clog.Infof("%s filter list TTL is %d seconds", path, ttl)
|
||||
u.ttl = time.Duration(ttl) * time.Second
|
||||
}
|
||||
|
||||
if _, found := settings.Update[u.path]; !found && u.url != "" {
|
||||
settings.Update[u.path] = u
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupSafeBrowsing loads safebrowsing settings
|
||||
func setupSafeBrowsing(c *caddy.Controller, settings *plugSettings) error {
|
||||
clog.Info("SafeBrowsing is enabled")
|
||||
settings.SafeBrowsingEnabled = true
|
||||
|
||||
if !c.NextArg() || len(c.Val()) == 0 {
|
||||
clog.Info("SafeBrowsing filter list is not configured")
|
||||
return c.ArgErr()
|
||||
}
|
||||
settings.SafeBrowsingFilterPath = c.Val()
|
||||
clog.Infof("SafeBrowsing filter list is set to %s", settings.SafeBrowsingFilterPath)
|
||||
|
||||
if c.NextArg() && len(c.Val()) > 0 {
|
||||
settings.SafeBrowsingBlockHost = c.Val()
|
||||
clog.Infof("SafeBrowsing block host is set to %s", settings.SafeBrowsingBlockHost)
|
||||
}
|
||||
|
||||
return setupUpdateInfo(settings.SafeBrowsingFilterPath, settings, c)
|
||||
}
|
||||
|
||||
// setupParental loads parental control settings
|
||||
func setupParental(c *caddy.Controller, settings *plugSettings) error {
|
||||
clog.Info("Parental control is enabled")
|
||||
settings.ParentalEnabled = true
|
||||
|
||||
if !c.NextArg() || len(c.Val()) == 0 {
|
||||
clog.Info("Parental control filter list is not configured")
|
||||
return c.ArgErr()
|
||||
}
|
||||
settings.ParentalFilterPath = c.Val()
|
||||
clog.Infof("Parental control filter list is set to %s", settings.ParentalFilterPath)
|
||||
|
||||
if c.NextArg() && len(c.Val()) > 0 {
|
||||
settings.ParentalBlockHost = c.Val()
|
||||
clog.Infof("Parental control block host is set to %s", settings.ParentalBlockHost)
|
||||
}
|
||||
|
||||
return setupUpdateInfo(settings.ParentalFilterPath, settings, c)
|
||||
}
|
||||
|
||||
// newDNSEngine initializes a DNS engine using a list of specified filters
|
||||
// it returns the DNS engine, and the number of lines in the filter files
|
||||
func newDNSEngine(paths []string) (*urlfilter.DNSEngine, int, error) {
|
||||
var lists []filterlist.RuleList
|
||||
linesCount := 0
|
||||
|
||||
for i, path := range paths {
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, 0, errorx.Decorate(err, "cannot read from %s", path)
|
||||
}
|
||||
linesCount += bytes.Count(b, []byte{'\n'})
|
||||
if linesCount == 0 {
|
||||
return nil, 0, fmt.Errorf("empty file %s", path)
|
||||
}
|
||||
|
||||
list := &filterlist.StringRuleList{
|
||||
ID: i,
|
||||
RulesText: string(b),
|
||||
IgnoreCosmetic: true,
|
||||
}
|
||||
lists = append(lists, list)
|
||||
}
|
||||
|
||||
storage, err := filterlist.NewRuleStorage(lists)
|
||||
if err != nil {
|
||||
return nil, 0, errorx.Decorate(err, "cannot create rule storage")
|
||||
}
|
||||
|
||||
return urlfilter.NewDNSEngine(storage), linesCount, nil
|
||||
}
|
||||
|
||||
func createSecurityServiceEngine(filename string) (*engineInfo, int, error) {
|
||||
rules, cnt, err := safeservices.CreateMap(filename)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
e := &engineInfo{
|
||||
filtersPaths: []string{filename},
|
||||
data: rules,
|
||||
}
|
||||
return e, cnt, nil
|
||||
}
|
||||
|
||||
// initEnginesMap initializes urlfilter filtering engines using the settings
|
||||
// loaded from the plugin configuration
|
||||
func initEnginesMap(settings plugSettings) error {
|
||||
if settings.SafeBrowsingEnabled {
|
||||
if !engineExists(settings.SafeBrowsingFilterPath) {
|
||||
clog.Infof("Initializing SafeBrowsing filtering engine for %s", settings.SafeBrowsingFilterPath)
|
||||
engine, cnt, err := createSecurityServiceEngine(settings.SafeBrowsingFilterPath)
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "cannot create safebrowsing DNS engine")
|
||||
}
|
||||
enginesMap[settings.SafeBrowsingFilterPath] = engine
|
||||
engineStatus.WithLabelValues(settings.SafeBrowsingFilterPath).Set(float64(1))
|
||||
engineSize.WithLabelValues(settings.SafeBrowsingFilterPath).Set(float64(cnt))
|
||||
engineTimestamp.WithLabelValues(settings.SafeBrowsingFilterPath).SetToCurrentTime()
|
||||
clog.Infof("Finished initializing SafeBrowsing filtering engine")
|
||||
}
|
||||
}
|
||||
|
||||
if settings.ParentalEnabled {
|
||||
if !engineExists(settings.ParentalFilterPath) {
|
||||
clog.Infof("Initializing Parental filtering engine for %s", settings.ParentalFilterPath)
|
||||
engine, cnt, err := createSecurityServiceEngine(settings.ParentalFilterPath)
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "cannot create parental control DNS engine")
|
||||
}
|
||||
enginesMap[settings.ParentalFilterPath] = engine
|
||||
engineStatus.WithLabelValues(settings.ParentalFilterPath).Set(float64(1))
|
||||
engineSize.WithLabelValues(settings.ParentalFilterPath).Set(float64(cnt))
|
||||
engineTimestamp.WithLabelValues(settings.ParentalFilterPath).SetToCurrentTime()
|
||||
clog.Infof("Finished initializing Parental filtering engine")
|
||||
}
|
||||
}
|
||||
|
||||
if !engineExists(settings.filterPathsKey) {
|
||||
clog.Infof("Initializing blocking filtering engine for %s", settings.filterPathsKey)
|
||||
engine, cnt, err := newDNSEngine(settings.FilterPaths)
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "cannot create blocking DNS engine")
|
||||
}
|
||||
enginesMap[settings.filterPathsKey] = &engineInfo{
|
||||
dnsEngine: engine,
|
||||
filtersPaths: settings.FilterPaths,
|
||||
}
|
||||
engineStatus.WithLabelValues(settings.filterPathsKey).Set(float64(1))
|
||||
engineSize.WithLabelValues(settings.filterPathsKey).Set(float64(cnt))
|
||||
engineTimestamp.WithLabelValues(settings.filterPathsKey).SetToCurrentTime()
|
||||
clog.Infof("Finished initializing blocking filtering engine")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// initUpdatesMap initializes the "updateMap" which is used for periodic updates check
|
||||
func initUpdatesMap(settings plugSettings) error {
|
||||
if len(settings.Update) == 0 {
|
||||
// Do nothing if there are no registered updaters
|
||||
return nil
|
||||
}
|
||||
|
||||
updatesMapGuard.Lock()
|
||||
defer updatesMapGuard.Unlock()
|
||||
|
||||
// Go through the list of updateInfo objects
|
||||
// Check if the file exists. If not, download
|
||||
for _, u := range settings.Update {
|
||||
if _, ok := updatesMap[u.path]; !ok {
|
||||
// Add to the map if it's not already there
|
||||
updatesMap[u.path] = u
|
||||
|
||||
// Try to do the initial update (for the case when the file does not exist)
|
||||
_, err := u.update()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
for i, testcase := range []struct {
|
||||
config string
|
||||
failing bool
|
||||
}{
|
||||
{`dnsfilter`, false},
|
||||
{`dnsfilter {
|
||||
filter /dev/nonexistent/abcdef
|
||||
}`, true},
|
||||
{`dnsfilter {
|
||||
filter ../tests/dns.txt
|
||||
}`, false},
|
||||
{`dnsfilter {
|
||||
safebrowsing ../tests/sb.txt
|
||||
filter ../tests/dns.txt
|
||||
}`, false},
|
||||
{`dnsfilter {
|
||||
parental ../tests/parental.txt
|
||||
filter ../tests/dns.txt
|
||||
}`, false},
|
||||
{`dnsfilter {
|
||||
parental ../tests/parental.txt
|
||||
filter ../tests/dns.txt
|
||||
geoip ../tests/GeoIP2-Country-Test.mmdb
|
||||
}`, false},
|
||||
} {
|
||||
c := caddy.NewTestController("dns", testcase.config)
|
||||
c.ServerBlockKeys = []string{""}
|
||||
err := setup(c)
|
||||
if err != nil {
|
||||
if !testcase.failing {
|
||||
t.Fatalf("Test #%d expected no errors, but got: %v", i, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if testcase.failing {
|
||||
t.Fatalf("Test #%d expected to fail but it didn't", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupUpdate(t *testing.T) {
|
||||
l := testStartFilterServer()
|
||||
defer func() {
|
||||
_ = l.Close()
|
||||
_ = os.Remove("testsb.txt")
|
||||
_ = os.Remove("testdns.txt")
|
||||
_ = os.Remove("testparental.txt")
|
||||
}()
|
||||
|
||||
port := l.Addr().(*net.TCPAddr).Port
|
||||
cfg := fmt.Sprintf(`dnsfilter {
|
||||
safebrowsing testsb.txt example.org http://127.0.0.1:%d/filter.txt 3600
|
||||
filter testdns.txt http://127.0.0.1:%d/filter.txt 3600
|
||||
parental testparental.txt example.org http://127.0.0.1:%d/filter.txt 3600
|
||||
}`, port, port, port)
|
||||
|
||||
c := caddy.NewTestController("dns", cfg)
|
||||
c.ServerBlockKeys = []string{""}
|
||||
|
||||
err := setup(c)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Check that filters were downloaded
|
||||
assert.FileExists(t, "testsb.txt")
|
||||
assert.FileExists(t, "testdns.txt")
|
||||
assert.FileExists(t, "testparental.txt")
|
||||
|
||||
// Check that they were added to the updatesMap
|
||||
updatesMapGuard.Lock()
|
||||
assert.Contains(t, updatesMap, "testsb.txt")
|
||||
assert.Contains(t, updatesMap, "testdns.txt")
|
||||
assert.Contains(t, updatesMap, "testparental.txt")
|
||||
updatesMapGuard.Unlock()
|
||||
|
||||
// Check that enginesMap contain necessary elements
|
||||
enginesMapGuard.Lock()
|
||||
assert.Contains(t, enginesMap, "testsb.txt")
|
||||
assert.Contains(t, enginesMap, "testdns.txt")
|
||||
assert.Contains(t, enginesMap, "testparental.txt")
|
||||
enginesMapGuard.Unlock()
|
||||
|
||||
// TTL is not expired yet
|
||||
wasUpdated := updateCheck()
|
||||
assert.False(t, wasUpdated)
|
||||
|
||||
// Trigger filters updates
|
||||
updatesMapGuard.Lock()
|
||||
for _, u := range updatesMap {
|
||||
u.lastTimeUpdated = time.Now().Add(-u.ttl).Add(-1 * time.Second)
|
||||
}
|
||||
updatesMapGuard.Unlock()
|
||||
|
||||
// Check updates
|
||||
wasUpdated = updateCheck()
|
||||
assert.True(t, wasUpdated)
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
)
|
||||
|
||||
// AdGuard Simplified domain names filter list ID
|
||||
const filterListID = 15
|
||||
const statsURL = "https://chrome.adtidy.org/api/1.0/rulestats.html"
|
||||
const uploadPeriod = 10 * time.Minute
|
||||
|
||||
type Stats struct {
|
||||
FilterLists map[int]map[string]int `json:"filters"`
|
||||
RecordedHits int64 `json:"-"`
|
||||
}
|
||||
|
||||
var stats = &Stats{
|
||||
FilterLists: map[int]map[string]int{},
|
||||
}
|
||||
var statsGuard = sync.Mutex{}
|
||||
|
||||
func init() {
|
||||
go func() {
|
||||
for range time.Tick(uploadPeriod) {
|
||||
clog.Info("Uploading stats")
|
||||
err := uploadStats()
|
||||
if err != nil {
|
||||
clog.Errorf("error while uploading status: %s", err)
|
||||
} else {
|
||||
clog.Info("Finished uploading stats successfully")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// recordRuleHit records a new rule hit and increments the stats
|
||||
func recordRuleHit(ruleText string) {
|
||||
statsGuard.Lock()
|
||||
defer statsGuard.Unlock()
|
||||
|
||||
v, ok := stats.FilterLists[filterListID]
|
||||
if !ok {
|
||||
v = map[string]int{}
|
||||
stats.FilterLists[filterListID] = v
|
||||
}
|
||||
|
||||
hits, ok := v[ruleText]
|
||||
if !ok {
|
||||
hits = 0
|
||||
}
|
||||
v[ruleText] = hits + 1
|
||||
stats.RecordedHits++
|
||||
statsCacheSize.Set(float64(stats.RecordedHits))
|
||||
}
|
||||
|
||||
// uploadStats resets the current stats and sends them to the server
|
||||
func uploadStats() error {
|
||||
statsGuard.Lock()
|
||||
statsToUpload := stats
|
||||
stats = &Stats{
|
||||
FilterLists: map[int]map[string]int{},
|
||||
}
|
||||
statsGuard.Unlock()
|
||||
b, err := json.Marshal(statsToUpload)
|
||||
if err != nil {
|
||||
statsUploadStatus.Set(0)
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, statsURL, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
statsUploadStatus.Set(0)
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
var resp *http.Response
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
statsUploadStatus.Set(0)
|
||||
return err
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
|
||||
statsUploadStatus.Set(1)
|
||||
statsUploadTimestamp.SetToCurrentTime()
|
||||
return nil
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStats(t *testing.T) {
|
||||
stats = &Stats{
|
||||
FilterLists: map[int]map[string]int{},
|
||||
}
|
||||
|
||||
recordRuleHit("||example.org^")
|
||||
recordRuleHit("||example.org^")
|
||||
recordRuleHit("||example.org^")
|
||||
recordRuleHit("||example.com^")
|
||||
|
||||
b, err := json.Marshal(stats)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, `{"filters":{"15":{"||example.com^":1,"||example.org^":3}}}`, string(b))
|
||||
assert.Equal(t, int64(4), stats.RecordedHits)
|
||||
|
||||
err = uploadStats()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(0), stats.RecordedHits)
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
)
|
||||
|
||||
// don't allow too small files
|
||||
const minFileSize = 1024
|
||||
|
||||
type updateInfo struct {
|
||||
path string // path to the filter list file
|
||||
url string // url to load filter list from
|
||||
ttl time.Duration // update check period
|
||||
lastTimeUpdated time.Time // last update we tried to check updates
|
||||
}
|
||||
|
||||
// update does the update if necessary
|
||||
// returns true if update was performed, otherwise - false
|
||||
func (u *updateInfo) update() (bool, error) {
|
||||
shouldUpdate := false
|
||||
|
||||
if _, err := os.Stat(u.path); os.IsNotExist(err) {
|
||||
clog.Infof("File %s does not exist, we should download the filter list", u.path)
|
||||
shouldUpdate = true
|
||||
} else if u.lastTimeUpdated.Add(u.ttl).Before(time.Now()) {
|
||||
clog.Infof("Time to download updates for %s", u.path)
|
||||
shouldUpdate = true
|
||||
}
|
||||
|
||||
if shouldUpdate {
|
||||
err := u.download()
|
||||
u.lastTimeUpdated = time.Now()
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// download downloads the file from URL and replaces it in the path
|
||||
func (u *updateInfo) download() error {
|
||||
clog.Infof("Downloading filter %s", u.path)
|
||||
|
||||
client := new(http.Client)
|
||||
|
||||
request, err := http.NewRequest("GET", u.url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request.Header.Add("Accept-Encoding", "gzip")
|
||||
|
||||
// Make the request
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = response.Body.Close() }()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("failed to download %s, response status is %d", u.url, response.StatusCode)
|
||||
}
|
||||
|
||||
// Check that the server actually sent compressed data
|
||||
var reader io.ReadCloser
|
||||
switch response.Header.Get("Content-Encoding") {
|
||||
case "gzip":
|
||||
reader, err = gzip.NewReader(response.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
reader = response.Body
|
||||
}
|
||||
defer func() { _ = reader.Close() }()
|
||||
|
||||
// Start reading the response to a temp file
|
||||
tmpFilePath := u.path + ".tmp"
|
||||
tmpFile, err := os.OpenFile(tmpFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
defer func() {
|
||||
_ = tmpFile.Close()
|
||||
_ = os.Remove(tmpFilePath)
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write the content to that file
|
||||
// nolint (gosec)
|
||||
written, err := io.Copy(tmpFile, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if written < minFileSize {
|
||||
return fmt.Errorf("the file downloaded from %s is too small: %d", u.url, written)
|
||||
}
|
||||
|
||||
clog.Infof("Downloaded update for %s, size=%d", u.path, written)
|
||||
|
||||
// Now replace the file
|
||||
_ = tmpFile.Close()
|
||||
return os.Rename(tmpFilePath, u.path)
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUpdateDownload(t *testing.T) {
|
||||
l := testStartFilterServer()
|
||||
defer func() {
|
||||
_ = l.Close()
|
||||
}()
|
||||
|
||||
u := &updateInfo{
|
||||
path: "testfilter.txt",
|
||||
url: fmt.Sprintf("http://127.0.0.1:%d/filter.txt", l.Addr().(*net.TCPAddr).Port),
|
||||
ttl: time.Minute,
|
||||
}
|
||||
defer func() {
|
||||
_ = os.Remove(u.path)
|
||||
}()
|
||||
|
||||
err := u.download()
|
||||
assert.Nil(t, err)
|
||||
assert.FileExists(t, u.path)
|
||||
}
|
||||
|
||||
func testStartFilterServer() net.Listener {
|
||||
content := ""
|
||||
for i := 0; i < 1000; i++ {
|
||||
content = content + "this is test line\n"
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/filter.txt", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(content))
|
||||
})
|
||||
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
srv := &http.Server{Handler: mux}
|
||||
|
||||
go func() { _ = srv.Serve(listener) }()
|
||||
return listener
|
||||
}
|
818
doc/configuration.md
Normal file
818
doc/configuration.md
Normal file
@ -0,0 +1,818 @@
|
||||
# AdGuard DNS Configuration File
|
||||
|
||||
Besides the [environment][env], AdGuard DNS uses a [YAML][yaml] file to store
|
||||
configuration. See file [`config.dist.yml`][dist] for a full example of a
|
||||
configuration file with comments.
|
||||
|
||||
## Contents
|
||||
|
||||
* [Rate Limiting](#ratelimit)
|
||||
* [Cache](#cache)
|
||||
* [Upstream](#upstream)
|
||||
* [Healthcheck](#upstream-healthcheck)
|
||||
* [Backend](#backend)
|
||||
* [Query Log](#query_log)
|
||||
* [GeoIP Database](#geoip)
|
||||
* [DNS Server Check](#check)
|
||||
* [Web API](#web)
|
||||
* [Safe Browsing](#safe_browsing)
|
||||
* [Adult Content Blocking](#adult_blocking)
|
||||
* [Filters](#filters)
|
||||
* [Filtering Groups](#filtering_groups)
|
||||
* [Server Groups](#server_groups)
|
||||
* [TLS](#server_groups-*-tls)
|
||||
* [DDR](#server_groups-*-ddr)
|
||||
* [Servers](#server_groups-*-servers-*)
|
||||
* [Connectivity Check](#connectivity-check)
|
||||
|
||||
[dist]: ../config.dist.yml
|
||||
[env]: environment.md
|
||||
[yaml]: https://yaml.org/
|
||||
|
||||
|
||||
|
||||
## <a href="#ratelimit" id="ratelimit" name="ratelimit">Rate Limiting</a>
|
||||
|
||||
The `ratelimit` object has the following properties:
|
||||
|
||||
* <a href="#ratelimit-refuseany" id="ratelimit-refuseany" name="ratelimit-refuseany">`refuseany`</a>:
|
||||
If true, refuse DNS queries with the `ANY` (aka `*`) type.
|
||||
|
||||
**Example:** `true`.
|
||||
|
||||
* <a href="#ratelimit-response_size_estimate" id="ratelimit-response_size_estimate" name="ratelimit-response_size_estimate">`response_size_estimate`</a>:
|
||||
The size of one DNS response for the purposes of rate limiting. If a DNS
|
||||
response is larger than this value, it is counted as several responses.
|
||||
|
||||
**Example:** `1KB`.
|
||||
|
||||
* <a href="#ratelimit-back_off_period" id="ratelimit-back_off_period" name="ratelimit-back_off_period">`back_off_period`</a>:
|
||||
The time during which to count the number of requests that a client has sent
|
||||
over the RPS.
|
||||
|
||||
**Example:** `10m`.
|
||||
|
||||
* <a href="#ratelimit-back_off_duration" id="ratelimit-back_off_duration" name="ratelimit-back_off_duration">`back_off_duration`</a>:
|
||||
How long a client that has hit the RPS too often stays in the backoff state.
|
||||
|
||||
**Example:** `30m`.
|
||||
|
||||
* <a href="#ratelimit-rps" id="ratelimit-rps" name="ratelimit-rps">`rps`</a>:
|
||||
The rate of requests per second for one subnet. Requests above this are
|
||||
counted in the backoff count.
|
||||
|
||||
**Example:** `30`.
|
||||
|
||||
* <a href="#ratelimit-back_off_count" id="ratelimit-back_off_count" name="ratelimit-back_off_count">`back_off_count`</a>:
|
||||
Maximum number of requests a client can make above the RPS within
|
||||
a `back_off_period`. When a client exceeds this limit, requests aren't
|
||||
allowed from client's subnet until `back_off_duration` ends.
|
||||
|
||||
**Example:** `1000`.
|
||||
|
||||
* <a href="#ratelimit-allowlist" id="ratelimit-allowlist" name="ratelimit-allowlist">`allowlist`</a>:
|
||||
The allowlist configuration object. It has the following fields:
|
||||
|
||||
* <a href="#ratelimit-allowlist-list" id="ratelimit-allowlist-list" name="ratelimit-allowlist-list">`list`</a>:
|
||||
The array of the allowed IPs or CIDRs.
|
||||
|
||||
**Property example:**
|
||||
|
||||
```yaml
|
||||
'list':
|
||||
- '192.168.1.4'
|
||||
- '192.175.2.1/16'
|
||||
```
|
||||
|
||||
* <a href="#ratelimit-allowlist-refresh_interval" id="ratelimit-allowlist-refresh_interval" name="ratelimit-allowlist-refresh_interval">`refresh_interval`</a>:
|
||||
How often AdGuard DNS refreshes the dynamic part of its allowlist from
|
||||
the data received from the `CONSUL_URL`, as a human-readable duration.
|
||||
|
||||
**Example:** `30s`.
|
||||
|
||||
* <a href="#ratelimit-ipv4_subnet_key_len" id="ratelimit-ipv4_subnet_key_len" name="ratelimit-ipv4_subnet_key_len">`ipv4_subnet_key_len`</a>:
|
||||
The length of the subnet prefix used to calculate rate limiter bucket keys
|
||||
for IPv4 addresses.
|
||||
|
||||
**Example:** `24`.
|
||||
|
||||
* <a href="#ratelimit-ipv6_subnet_key_len" id="ratelimit-ipv6_subnet_key_len" name="ratelimit-ipv6_subnet_key_len">`ipv6_subnet_key_len`</a>:
|
||||
Same as `ipv4_subnet_key_len` above but for IPv6 addresses.
|
||||
|
||||
**Example:** `48`.
|
||||
|
||||
For example, if `back_off_period` is `1m`, `back_off_count` is `10`, and `rps`
|
||||
is `5`, a client (meaning all IP addresses within the subnet defined by
|
||||
`ipv4_subnet_key_len` and `ipv6_subnet_key_len`) that made 15 requests in one
|
||||
second or 6 requests (one above `rps`) every second for 10 seconds within one
|
||||
minute, the client is blocked for `back_off_duration`.
|
||||
|
||||
|
||||
|
||||
## <a href="#cache" id="cache" name="cache">Cache</a>
|
||||
|
||||
The `cache` object has the following properties:
|
||||
|
||||
* <a href="#cache-type" id="cache-type" name="cache-type">`type`</a>:
|
||||
The type of cache to use. Can be `simple` (a simple LRU cache) or `ecs` (a
|
||||
ECS-aware LRU cache). If set to `ecs`, `ecs_size` must be greater than
|
||||
zero.
|
||||
|
||||
**Example:** `simple`.
|
||||
|
||||
* <a href="#cache-size" id="cache-size" name="cache-size">`size`</a>:
|
||||
The total number of items in the cache for hostnames with no ECS support.
|
||||
Must be greater than or equal to zero. If zero, cache is disabled.
|
||||
|
||||
**Example:** `10000`.
|
||||
|
||||
* <a href="#cache-ecs_size" id="cache-ecs_size" name="cache-ecs_size">`ecs_size`</a>:
|
||||
The total number of items in the cache for hostnames with ECS support.
|
||||
|
||||
**Example:** `10000`.
|
||||
|
||||
|
||||
|
||||
## <a href="#upstream" id="upstream" name="upstream">Upstream</a>
|
||||
|
||||
The `upstream` object has the following properties:
|
||||
|
||||
* <a href="#upstream-server" id="upstream-server" name="upstream-server">`server`</a>:
|
||||
The address of the main upstream server, in the `ip:port` format.
|
||||
|
||||
**Example:** `8.8.8.8:53` or `[2001:4860:4860::8844]:53`.
|
||||
|
||||
* <a href="#upstream-timeout" id="upstream-timeout" name="upstream-timeout">`timeout`</a>:
|
||||
Timeout for all outgoing DNS requests, as a human-readable duration.
|
||||
|
||||
**Example:** `2s`.
|
||||
|
||||
* <a href="#upstream-fallback" id="upstream-fallback" name="upstream-fallback">`fallback`</a>:
|
||||
The array of addresses of the fallback upstream servers, in the `ip:port`
|
||||
format. These are use used in case a network error occurs while requesting
|
||||
the main upstream server.
|
||||
|
||||
**Example:** `['1.1.1.1:53', '[2001:4860:4860::8888]:53']`.
|
||||
|
||||
* `healthcheck`: Healthcheck configuration. See
|
||||
[below](#upstream-healthcheck).
|
||||
|
||||
|
||||
|
||||
### <a href="#upstream-healthcheck" id="upstream-healthcheck" name="upstream-healthcheck">Healthcheck</a>
|
||||
|
||||
If `enabled` is true, the upstream healthcheck is enabled. The healthcheck
|
||||
worker probes the main upstream with an `A` query for a domain created from
|
||||
`domain_template`. If there is an error, timeout, or a response different from
|
||||
a `NOERROR` one then the main upstream is considered down, and all requests are
|
||||
redirected to fallback upstream servers for the time set by `backoff_duration`.
|
||||
Afterwards, if a worker probe is successful, AdGuard DNS considers the
|
||||
connection to the main upstream as restored, and requests are routed back to it.
|
||||
|
||||
* <a href="#u-h-enabled" id="u-h-enabled" name="u-h-enabled">`enabled`</a>:
|
||||
|
||||
If true, the upstream healthcheck is enabled.
|
||||
|
||||
**Example:** `true`.
|
||||
|
||||
* <a href="#u-h-interval" id="u-h-interval" name="u-h-interval">`interval`</a>:
|
||||
How often AdGuard DNS makes upstream healthcheck requests, as a
|
||||
human-readable duration.
|
||||
|
||||
**Example:** `2s`.
|
||||
|
||||
* <a href="#u-h-timeout" id="u-h-timeout" name="u-h-timeout">`timeout`</a>:
|
||||
Timeout for all outgoing healthcheck requests, as a human-readable duration.
|
||||
|
||||
**Example:** `1s`.
|
||||
|
||||
* <a href="#u-h-backoff_duration" id="u-h-backoff_duration" name="u-h-backoff_duration">`backoff_duration`</a>:
|
||||
Backoff duration after failed healthcheck request, as a human-readable
|
||||
duration. If the main upstream is down, AdGuardDNS does not return back to
|
||||
using it until this time has passed. The healthcheck is still performed,
|
||||
and each failed check advances the backoff.
|
||||
|
||||
**Example:** `30s`.
|
||||
|
||||
* <a href="#u-h-domain_template" id="u-h-domain_template" name="u-h-domain_template">`domain_template`</a>:
|
||||
The template for domains used to perform healthcheck queries. If the
|
||||
`domain_template` contains the string `${RANDOM}`, all occurrences of this
|
||||
string are replaced with a random string (currently, a hexadecimal form of a
|
||||
64-bit integer) on every healthcheck query. Queries must return a `NOERROR`
|
||||
response.
|
||||
|
||||
**Example:** `${RANDOM}.neverssl.com`.
|
||||
|
||||
|
||||
|
||||
## <a href="#backend" id="backend" name="backend">Backend</a>
|
||||
|
||||
The `backend` object has the following properties:
|
||||
|
||||
* <a href="#backend-timeout" id="backend-timeout" name="backend-timeout">`timeout`</a>:
|
||||
Timeout for all outgoing HTTP requests to the backend, as a human-readable
|
||||
duration. Set to `0s` to disable timeouts.
|
||||
|
||||
**Example:** `10s`.
|
||||
|
||||
* <a href="#backend-refresh_interval" id="backend-refresh_interval" name="backend-refresh_interval">`refresh_interval`</a>:
|
||||
How often AdGuard DNS checks the backend for data updates, as a
|
||||
human-readable duration.
|
||||
|
||||
**Example:** `1m`.
|
||||
|
||||
* <a href="#backend-full_refresh_interval" id="backend-full_refresh_interval" name="backend-full_refresh_interval">`full_refresh_interval`</a>:
|
||||
How often AdGuard DNS performs full synchronization, as a human-readable
|
||||
duration.
|
||||
|
||||
**Example:** `24h`.
|
||||
|
||||
* <a href="#backend-bill_stat_interval" id="backend-bill_stat_interval" name="backend-bill_stat_interval">`bill_stat_interval`</a>:
|
||||
How often AdGuard DNS sends the billing statistics to the backend, as
|
||||
a human-readable duration.
|
||||
|
||||
**Example:** `1m`.
|
||||
|
||||
|
||||
|
||||
## <a href="#query_log" id="query_log" name="query_log">Query Log</a>
|
||||
|
||||
The `query_log` object has the following properties:
|
||||
|
||||
* <a href="#query_log-file" id="query_log-file" name="query_log-file">`file`</a>:
|
||||
The file query log configuration object. It has the following properties:
|
||||
|
||||
* <a href="#q-file-enabled" id="q-file-enabled" name="q-file-enabled">`enabled`</a>:
|
||||
If true, the JSONL file query logging is enabled.
|
||||
|
||||
**Property example:**
|
||||
|
||||
```yaml
|
||||
'file':
|
||||
'enabled': true
|
||||
```
|
||||
|
||||
|
||||
|
||||
## <a href="#geoip" id="geoip" name="geoip">GeoIP Database</a>
|
||||
|
||||
The `geoip` object has the following properties:
|
||||
|
||||
* <a href="#geoip-host_cache_size" id="geoip-host_cache_size" name="geoip-host_cache_size">`host_cache_size`</a>:
|
||||
The size of the host lookup cache, in entries.
|
||||
|
||||
**Example:** `100000`.
|
||||
|
||||
* <a href="#geoip-ip_cache_size" id="geoip-ip_cache_size" name="geoip-ip_cache_size">`ip_cache_size`</a>:
|
||||
The size of the IP lookup cache, in entries.
|
||||
|
||||
**Example:** `100000`.
|
||||
|
||||
* <a href="#geoip-refresh_interval" id="geoip-refresh_interval" name="geoip-refresh_interval">`refresh_interval`</a>:
|
||||
Interval between the GeoIP database refreshes, as a human-readable duration.
|
||||
|
||||
**Example:** `5m`.
|
||||
|
||||
|
||||
|
||||
## <a href="#check" id="check" name="check">DNS Server Check</a>
|
||||
|
||||
The `check` object has the following properties:
|
||||
|
||||
* <a href="#check-domains" id="check-domains" name="check-domains">`domains`</a>:
|
||||
The domain suffixes to which random IDs are prepended using a hyphen.
|
||||
|
||||
**Property example:**
|
||||
|
||||
```yaml
|
||||
'domains':
|
||||
- 'dnscheck.example.com'
|
||||
- 'checkdns.example.com'
|
||||
```
|
||||
|
||||
* <a href="#check-node_location" id="check-node_location" name="check-node_location">`node_location`</a>:
|
||||
The location code of this server node.
|
||||
|
||||
**Example:** `ams`.
|
||||
|
||||
* <a href="#check-node_name" id="check-node_name" name="check-node_name">`node_name`</a>:
|
||||
The name of this server node.
|
||||
|
||||
**Example:** `eu-1.dns.example.com`.
|
||||
|
||||
* <a href="#check-ttl" id="check-ttl" name="check-ttl">`ttl`</a>:
|
||||
For how long to keep the information about a single user in Consul KV, as
|
||||
a human-readable duration. Note the actual TTL may be up to twice as long
|
||||
due to Consul's peculiarities.
|
||||
|
||||
**Example:** `30s`.
|
||||
|
||||
* <a href="#check-ipv4" id="check-ipv4" name="check-ipv4">`ipv4` and `ipv6`</a>:
|
||||
Arrays of IPv4 or IPv6 addresses with which to respond to `A` and `AAAA`
|
||||
queries correspondingly. Generally, those should be the IP addresses of the
|
||||
AdGuard DNS [main HTTP API][http-dnscheck] for the DNS server check feature
|
||||
to work properly. In a development setup, that means the localhost
|
||||
addresses.
|
||||
|
||||
**Property examples:**
|
||||
|
||||
```yaml
|
||||
'ipv4':
|
||||
- '1.2.3.4'
|
||||
- '5.6.7.8'
|
||||
'ipv6':
|
||||
- '1234::cdee'
|
||||
- '1234::cdef'
|
||||
```
|
||||
|
||||
[http-dnscheck]: http.md#dnscheck-test
|
||||
|
||||
|
||||
|
||||
## <a href="#web" id="web" name="web">Web API</a>
|
||||
|
||||
The optional `web` object has the following properties:
|
||||
|
||||
* <a href="#web-linked_ip" id="web-linked_ip" name="web-linked_ip">`linked_ip`</a>:
|
||||
The optional linked IP and dynamic DNS (DDNS, DynDNS) web server
|
||||
configuration. The [static content](#web-static_content) is not served on
|
||||
these addresses.
|
||||
|
||||
See the [full description of this API][http-linked-ip-proxy] on the HTTP API
|
||||
page.
|
||||
|
||||
Property `bind` has the same format as [`non_doh_bind`](#web-non_doh_bind)
|
||||
below.
|
||||
|
||||
**Property example:**
|
||||
|
||||
```yaml
|
||||
'linked_ip':
|
||||
'bind':
|
||||
- 'address': '127.0.0.1:80'
|
||||
- 'address': '127.0.0.1:443'
|
||||
'certificates':
|
||||
- 'certificate': './test/cert.crt'
|
||||
'key': './test/cert.key'
|
||||
```
|
||||
|
||||
* <a href="#web-safe_browsing" id="web-safe_browsing" name="web-safe_browsing">`safe_browsing`</a>:
|
||||
The optional safe browsing web server configurations. Every request is
|
||||
responded with the content from the file to which the `block_page` property
|
||||
points.
|
||||
|
||||
See the [full description of this API][http-block-pages] on the HTTP API
|
||||
page.
|
||||
|
||||
Property `bind` has the same format as [`non_doh_bind`](#web-non_doh_bind)
|
||||
below. The addresses should be different from the `adult_blocking` server,
|
||||
and the same as the ones of the `block_host` property in the
|
||||
[`safe_browsing`](#safe_browsing) and [`adult_blocking`](#adult_blocking)
|
||||
objects correspondingly.
|
||||
|
||||
While this object is optional, both `bind` and `block_page` properties
|
||||
within them are required.
|
||||
|
||||
**Property examples:**
|
||||
|
||||
```yaml
|
||||
'safe_browsing':
|
||||
'bind':
|
||||
- 'address': '127.0.0.1:80'
|
||||
- 'address': '127.0.0.1:443'
|
||||
'certificates':
|
||||
- 'certificate': './test/cert.crt'
|
||||
'key': './test/cert.key'
|
||||
'block_page': '/var/www/block_page.html'
|
||||
```
|
||||
|
||||
* <a href="#web-adult_blocking" id="web-adult_blocking" name="web-adult_blocking">`adult_blocking`</a>:
|
||||
The optional adult blocking web server configurations. The format of the
|
||||
values is the same as in the [`safe_browsing`](#web-safe_browsing) object
|
||||
above.
|
||||
|
||||
* <a href="#web-non_doh_bind" id="web-non_doh_bind" name="web-non_doh_bind">`non_doh_bind`</a>:
|
||||
The optional listen addresses and optional TLS configuration for the web
|
||||
service in addition to the ones in the DNS-over-HTTPS handlers. The
|
||||
`certificates` array has the same format as the one in a server group's [TLS
|
||||
settings](#sg-*-tls). In the special case of `GET /robots.txt` requests, a
|
||||
special response is served; this response could be overwritten with static
|
||||
content.
|
||||
|
||||
**Property example:**
|
||||
|
||||
```yaml
|
||||
'non_doh_bind':
|
||||
- 'address': '127.0.0.1:80'
|
||||
- 'address': '127.0.0.1:443'
|
||||
'certificates':
|
||||
- 'certificate': './test/cert.crt'
|
||||
'key': './test/cert.key'
|
||||
```
|
||||
|
||||
* <a href="#web-static_content" id="web-static_content" name="web-static_content">`static_content`</a>:
|
||||
The optional inline static content mapping. Not served on the `linked_ip`,
|
||||
`safe_browsing` and `adult_blocking` servers. Paths must not duplicate the
|
||||
ones used by the DNS-over-HTTPS server.
|
||||
|
||||
**Property example:**
|
||||
|
||||
```yaml
|
||||
'static_content':
|
||||
'/favicon.ico':
|
||||
'content_type': 'image/x-icon'
|
||||
'content': 'base64content'
|
||||
```
|
||||
|
||||
* <a href="#web-root_redirect_url" id="web-root_redirect_url" name="web-root_redirect_url">`root_redirect_url`</a>:
|
||||
The optional URL to which non-DNS and non-Debug HTTP requests are
|
||||
redirected. If not set, AdGuard DNS will respond with a 404 status to all
|
||||
such requests.
|
||||
|
||||
**Example:** `https://adguard-dns.com/`.
|
||||
|
||||
* <a href="#web-error_404" id="web-error_404" name="web-error_404">`error_404` and `error_500`</a>:
|
||||
The optional paths to the 404 page and the 500 page HTML files
|
||||
correspondingly. If not set, a simple plain text 404 or 500 page is served.
|
||||
|
||||
**Example:** `/var/www/404.html`.
|
||||
|
||||
* <a href="#web-timeout" id="web-timeout" name="web-timeout">`timeout`</a>:
|
||||
The timeout for server operations, as a human-readable duration.
|
||||
|
||||
**Example:** `30s`.
|
||||
|
||||
[http-block-pages]: http.md#block-pages
|
||||
[http-dnscheck-test]: http.md#dhscheck-test
|
||||
[http-linked-ip-proxy]: http.md#linked-ip-proxy
|
||||
|
||||
|
||||
|
||||
## <a href="#safe_browsing" id="safe_browsing" name="safe_browsing">Safe Browsing</a>
|
||||
|
||||
The `safe_browsing` object has the following properties:
|
||||
|
||||
* <a href="#safe_browsing-block_host" id="safe_browsing-block_host" name="safe_browsing-block_host">`block_host`</a>:
|
||||
The host with which to respond to any requests that match the filter.
|
||||
|
||||
**Example:** `standard-block.dns.adguard.com`.
|
||||
|
||||
* <a href="#safe_browsing-cache_size" id="safe_browsing-cache_size" name="safe_browsing-cache_size">`cache_size`</a>:
|
||||
The size of the response cache, in entries.
|
||||
|
||||
**WARNING: CURRENTLY IGNORED!** See AGDNS-398.
|
||||
|
||||
**Example:** `1024`.
|
||||
|
||||
* <a href="#safe_browsing-cache_ttl" id="safe_browsing-cache_ttl" name="safe_browsing-cache_ttl">`cache_ttl`</a>:
|
||||
The TTL of the response cache, as a human-readable duration.
|
||||
|
||||
**Example:** `1h`.
|
||||
|
||||
* <a href="#safe_browsing-url" id="safe_browsing-url" name="safe_browsing-url">`url`</a>:
|
||||
The URL from which the contents can be updated. The URL must reply with
|
||||
a 200 status code.
|
||||
|
||||
**Example:** `https://example.com/safe_browsing.txt`.
|
||||
|
||||
* <a href="#safe_browsing-refresh_interval" id="safe_browsing-refresh_interval" name="safe_browsing-refresh_interval">`refresh_interval`</a>:
|
||||
How often AdGuard DNS refreshes the filter.
|
||||
|
||||
**Example:** `1m`.
|
||||
|
||||
|
||||
|
||||
## <a href="#adult_blocking" id="adult_blocking" name="adult_blocking">Adult Content Blocking</a>
|
||||
|
||||
The `adult_blocking` object has the same properties as the
|
||||
[`safe_browsing`](#safe_browsing) one above.
|
||||
|
||||
|
||||
|
||||
## <a href="#filters" id="filters" name="filters">Filter Lists</a>
|
||||
|
||||
The `filters` object has the following properties:
|
||||
|
||||
* <a href="#filters-response_ttl" id="filters-response_ttl" name="filters-response_ttl">`response_ttl`</a>:
|
||||
The default TTL to set for responses to queries for blocked or modified
|
||||
domains, as a human-readable duration. It is used for anonymous users. For
|
||||
users with profiles, the TTL from their profile settings are used.
|
||||
|
||||
**Example:** `10s`.
|
||||
|
||||
* <a href="#filters-custom_filter_cache_size" id="filters-custom_filter_cache_size" name="filters-custom_filter_cache_size">`custom_filter_cache_size`</a>:
|
||||
The size of the LRU cache of compiled filtering rule engines for profiles
|
||||
with custom filtering rules, in entries. Zero means no caching, which slows
|
||||
down queries.
|
||||
|
||||
**Example:** `1024`.
|
||||
|
||||
* <a href="#filters-refresh_interval" id="filters-refresh_interval" name="filters-refresh_interval">`refresh_interval`</a>:
|
||||
How often AdGuard DNS refreshes the rule-list filters from the filter index,
|
||||
as well as the blocked services list from the [blocked list
|
||||
index][env-blocked_services)].
|
||||
|
||||
**Example:** `1h`.
|
||||
|
||||
* <a href="#filters-refresh_timeout" id="filters-refresh_timeout" name="filters-refresh_timeout">`refresh_timeout`</a>:
|
||||
The timeout for the *entire* filter update operation, as a human-readable
|
||||
duration. Be aware that each individual refresh operation also has its own
|
||||
hardcoded 30s timeout.
|
||||
|
||||
**Example:** `5m`.
|
||||
|
||||
|
||||
|
||||
## <a href="#filtering_groups" id="filtering_groups" name="filtering_groups">Filtering Groups</a>
|
||||
|
||||
The items of the `filtering_groups` array have the following properties:
|
||||
|
||||
* <a href="#fg-*-id" id="fg-*-id" name="fg-*-id">`id`</a>:
|
||||
The unique ID of this filtering group.
|
||||
|
||||
**Example:** `default`.
|
||||
|
||||
* <a href="#fg-*-rule_lists" id="fg-*-rule_lists" name="fg-*-rule_lists">`rule_lists`</a>:
|
||||
Filtering rule lists settings. This object has the following properties:
|
||||
|
||||
* <a href="#fg-*-rl-enabled" id="fg-*-rl-enabled" name="fg-*-rl-enabled">`enabled`</a>:
|
||||
Shows if rule-list filtering should be enforced. If it is set to
|
||||
`false`, the rest of the settings are ignored.
|
||||
|
||||
**Example:** `true`.
|
||||
|
||||
* <a href="#fg-*-rl-ids" id="fg-*-rl-ids" name="fg-*-rl-ids">`ids`</a>:
|
||||
The array of rule-list IDs used in this filtering group.
|
||||
|
||||
**Example:** `[adguard_dns_default]`.
|
||||
|
||||
* <a href="#fg-*-parental" id="fg-*-parental" name="fg-*-parental">`parental`</a>:
|
||||
Parental protection settings. This object has the following properties:
|
||||
|
||||
* <a href="#fg-*-p-enabled" id="fg-*-p-enabled" name="fg-*-p-enabled">`enabled`</a>:
|
||||
Shows if any kind of parental protection filtering should be enforced at
|
||||
all. If it is set to `false`, the rest of the settings are ignored.
|
||||
|
||||
**Example:** `true`.
|
||||
|
||||
* <a href="#fg-*-p-block_adult" id="fg-*-p-block_adult" name="fg-*-p-block_adult">`block_adult`</a>:
|
||||
If true, adult content blocking is enabled for this filtering group by
|
||||
default. Requires `enabled` to also be true.
|
||||
|
||||
**Example:** `true`.
|
||||
|
||||
* <a href="#fg-*-p-general_safe_search" id="fg-*-p-general_safe_search" name="fg-*-p-general_safe_search">`general_safe_search`</a>:
|
||||
If true, general safe search is enabled for this filtering group by
|
||||
default. Requires `enabled` to also be true.
|
||||
|
||||
**Example:** `true`.
|
||||
|
||||
* <a href="#fg-*-p-youtube_safe_search" id="fg-*-p-youtube_safe_search" name="fg-*-p-youtube_safe_search">`youtube_safe_search`</a>:
|
||||
If true, YouTube safe search is enabled for this filtering group by
|
||||
default. Requires `enabled` to also be true.
|
||||
|
||||
**Example:** `true`.
|
||||
|
||||
* <a href="#fg-*-safe_browsing" id="fg-*-safe_browsing" name="fg-*-safe_browsing">`safe_browsing`</a>:
|
||||
General safe browsing settings. This object has the following properties:
|
||||
|
||||
* <a href="#fg-*-sb-enabled" id="fg-*-sb-enabled" name="fg-*-sb-enabled">`enabled`</a>:
|
||||
Shows if the general safe browsing filtering should be enforced. If it
|
||||
is set to `false`, the rest of the settings are ignored.
|
||||
|
||||
**Example:** `true`.
|
||||
|
||||
* <a href="#fg-*-block_private_relay" id="fg-*-block_private_relay" name="fg-*-block_private_relay">`private_relay`</a>:
|
||||
If true, Apple Private Relay queries are blocked for requests using this
|
||||
filtering group.
|
||||
|
||||
**Example:** `false`.
|
||||
|
||||
|
||||
|
||||
## <a href="#server_groups" id="server_groups" name="server_groups">Server Groups</a>
|
||||
|
||||
The items of the `server_groups` array have the following properties:
|
||||
|
||||
* <a href="#sg-*-name" id="sg-*-name" name="sg-*-name">`name`</a>:
|
||||
The unique name of this server group.
|
||||
|
||||
**Example:** `adguard_dns_default`.
|
||||
|
||||
* <a href="#sg-*-filtering_group" id="sg-*-filtering_group" name="sg-*-filtering_group">`filtering_group`</a>:
|
||||
The default filtering group for this server group. It is used for anonymous
|
||||
users.
|
||||
|
||||
**Example:** `default`.
|
||||
|
||||
* `tls`: The optional TLS configuration object. See
|
||||
[below](#server_groups-*-tls).
|
||||
|
||||
* `ddr`: The DDR configuration object. See [below](#server_groups-*-ddr).
|
||||
|
||||
* `servers`: Server configuration for this filtering group. See
|
||||
[below](#server_groups-*-servers-*).
|
||||
|
||||
|
||||
|
||||
### <a href="#server_groups-*-tls" id="server_groups-*-tls" name="server_groups-*-tls">TLS</a>
|
||||
|
||||
* <a href="#sg-*-tls-certificates" id="sg-*-tls-certificates" name="sg-*-tls-certificates">`certificates`</a>:
|
||||
The array of objects with paths to the certificate and the private key for
|
||||
this server group.
|
||||
|
||||
**Property example:**
|
||||
|
||||
```yaml
|
||||
'certificates':
|
||||
- 'certificate': '/etc/dns/cert.crt'
|
||||
'key': '/etc/dns/cert.key'
|
||||
```
|
||||
|
||||
* <a href="#sg-*-tls-session_keys" id="sg-*-tls-session_keys" name="sg-*-tls-session_keys">`session_keys`</a>:
|
||||
The array of file paths from which the each server's TLS session keys are
|
||||
updated. Session ticket key files must contain at least 32 bytes.
|
||||
|
||||
**Property example:**
|
||||
|
||||
```yaml
|
||||
'session_keys':
|
||||
- './private/key_1'
|
||||
- './private/key_2'
|
||||
```
|
||||
|
||||
* <a href="#sg-*-tls-device_id_wildcards" id="sg-*-tls-device_id_wildcards" name="sg-*-tls-device_id_wildcards">`device_id_wildcards`</a>:
|
||||
The array of domain name wildcards to use to detect clients' device IDs.
|
||||
Use this to prevent conflicts when using certificates for subdomains.
|
||||
|
||||
**Property example:**
|
||||
|
||||
```yaml
|
||||
'device_id_wildcards':
|
||||
- '*.d.dns.example.com'
|
||||
```
|
||||
|
||||
|
||||
|
||||
### <a href="#server_groups-*-ddr" id="server_groups-*-ddr" name="server_groups-*-ddr">DDR</a>
|
||||
|
||||
The DDR configuration object. Many of these data duplicate data from objects in
|
||||
the [`servers`](#server_groups-*-servers-*) array. This was done because there
|
||||
was an opinion that a more restrictive configuration that automatically
|
||||
collected the required data was not self-describing and flexible enough.
|
||||
|
||||
* <a href="#sg-*-ddr-enabled" id="sg-*-ddr-enabled" name="sg-*-ddr-enabled">`enabled`</a>:
|
||||
Shows if DDR queries are processed. If it is set to `false`, DDR domain
|
||||
name queries receive an `NXDOMAIN` response.
|
||||
|
||||
**Example:** `true`.
|
||||
|
||||
* <a href="#sg-*-ddr-device_records" id="sg-*-ddr-device_records" name="sg-*-ddr-device_records">`device_records`</a>:
|
||||
The device ID wildcard to record template mapping. The keys should
|
||||
generally be kept in sync with the
|
||||
[`device_id_wildcards`](#sg-*-tls-device_id_wildcards) field of the `tls`
|
||||
object.
|
||||
|
||||
The values have the following properties:
|
||||
|
||||
* <a href="#sg-*-ddr-dr-*-doh_path" id="sg-*-ddr-dr-*-doh_path" name="sg-*-ddr-dr-*-doh_path">`doh_path`</a>:
|
||||
The path template for the DoH DDR SVCB records. It is optional, unless
|
||||
`https_port` below is set.
|
||||
|
||||
* <a href="#sg-*-ddr-dr-*-https_port" id="sg-*-ddr-dr-*-https_port" name="sg-*-ddr-dr-*-https_port">`https_port`</a>:
|
||||
The optional port to use in DDR responses about the DoH resolver. If it
|
||||
is zero, the DoH resolver address is not included into the answer. A
|
||||
non-zero `https_port` should not be the same as `tls_port` below.
|
||||
|
||||
* <a href="#sg-*-ddr-dr-*-quic_port" id="sg-*-ddr-dr-*-quic_port" name="sg-*-ddr-dr-*-quic_port">`quic_port`</a>:
|
||||
The optional port to use in DDR responses about the DoQ resolver. If it
|
||||
is zero, the DoQ resolver address is not included into the answer.
|
||||
|
||||
* <a href="#sg-*-ddr-dr-*-tls_port" id="sg-*-ddr-dr-*-tls_port" name="sg-*-ddr-dr-*-tls_port">`tls_port`</a>:
|
||||
The optional port to use in DDR responses about the DoT resolver. If it
|
||||
is zero, the DoT resolver address is not included into the answer. A
|
||||
non-zero `tls_port` should not be the same as `https_port` above.
|
||||
|
||||
* <a href="#sg-*-ddr-dr-*-ipv4_hints" id="sg-*-ddr-dr-*-ipv4_hints" name="sg-*-ddr-dr-*-ipv4_hints">`ipv4_hints`</a>:
|
||||
The optional hints about the IPv4-addresses of the server.
|
||||
|
||||
* <a href="#sg-*-ddr-dr-*-ipv6_hints" id="sg-*-ddr-dr-*-ipv6_hints" name="sg-*-ddr-dr-*-ipv6_hints">`ipv6_hints`</a>:
|
||||
The optional hints about the IPv6-addresses of the server.
|
||||
|
||||
**Property example:**
|
||||
|
||||
```yaml
|
||||
'device_records':
|
||||
'*.d.dns.example.com':
|
||||
doh_path: '/dns-query{?dns}'
|
||||
https_port: 443
|
||||
quic_port: 853
|
||||
tls_port: 853
|
||||
ipv4_hints:
|
||||
- 1.2.3.4
|
||||
ipv6_hints:
|
||||
- '2001::1234'
|
||||
'*.e.dns.example.org':
|
||||
doh_path: '/dns-query{?dns}'
|
||||
https_port: 10443
|
||||
quic_port: 10853
|
||||
tls_port: 10853
|
||||
ipv4_hints:
|
||||
- 5.6.7.8
|
||||
ipv6_hints:
|
||||
- '2001::5678'
|
||||
```
|
||||
|
||||
|
||||
* <a href="#sg-*-ddr-public_records" id="sg-*-ddr-public_records" name="sg-*-ddr-public_records">`public_records`</a>:
|
||||
The public domain name to DDR record template mapping. The format of the
|
||||
values is the same as in the [`device_records`](#sg-*-ddr-device_records)
|
||||
above.
|
||||
|
||||
|
||||
|
||||
### <a href="#server_groups-*-servers-*" id="server_groups-*-servers-*" name="server_groups-*-servers-*">Servers</a>
|
||||
|
||||
The items of the `servers` array have the following properties:
|
||||
|
||||
* <a href="#sg-s-*-name" id="sg-s-*-name" name="sg-s-*-name">`name`</a>:
|
||||
The unique name of this server.
|
||||
|
||||
**Example:** `default_dns`.
|
||||
|
||||
* <a href="#sg-s-*-protocol" id="sg-s-*-protocol" name="sg-s-*-protocol">`protocol`</a>:
|
||||
The protocol to use on this server. The following values are supported:
|
||||
|
||||
* `dns`
|
||||
* `dnscrypt`
|
||||
* `https`
|
||||
* `quic`
|
||||
* `tls`
|
||||
|
||||
**Example:** `dns`.
|
||||
|
||||
* <a href="#sg-s-*-linked_ip_enabled" id="sg-s-*-linked_ip_enabled" name="sg-s-*-linked_ip_enabled">`linked_ip_enabled`</a>:
|
||||
If true, use the profiles' linked IPs to detect.
|
||||
|
||||
**Default:** `false`.
|
||||
|
||||
**Example:** `true`.
|
||||
|
||||
* <a href="#sg-s-*-bind_addresses" id="sg-s-*-bind_addresses" name="sg-s-*-bind_addresses">`bind_addresses`</a>:
|
||||
The array of `ip:port` addresses to listen on.
|
||||
|
||||
**Example:** `[127.0.0.1:53, 192.168.1.1:53]`.
|
||||
|
||||
* <a href="#sg-s-*-dnscrypt" id="sg-s-*-dnscrypt" name="sg-s-*-dnscrypt">`dnscrypt`</a>:
|
||||
The optional DNSCrypt configuration object. It has the following
|
||||
properties:
|
||||
|
||||
* <a href="#sg-s-*-dnscrypt-config_path" id="sg-s-*-dnscrypt-config_path" name="sg-s-*-dnscrypt-config_path">`config_path`</a>:
|
||||
The path to the DNSCrypt configuration file. See the [configuration
|
||||
section][dnscconf] of the DNSCrypt module.
|
||||
|
||||
Must not be set if `inline` is set.
|
||||
|
||||
**Example:** `/etc/dns/dnscrypt.yml`
|
||||
|
||||
* <a href="#sg-s-*-dnscrypt-inline" id="sg-s-*-dnscrypt-inline" name="sg-s-*-dnscrypt-inline">`inline`</a>:
|
||||
The DNSCrypt configuration, inline. See the [configuration
|
||||
section][dnscconf] of the DNSCrypt module.
|
||||
|
||||
Must not be set if `config_path` is set.
|
||||
|
||||
**Property example:**
|
||||
|
||||
```yaml
|
||||
'inline':
|
||||
'provider_name': '2.dnscrypt-cert.example.org'
|
||||
'public_key': 'F11DDBCC4817E543845FDDD4CB881849B64226F3DE397625669D87B919BC4FB0'
|
||||
'private_key': '5752095FFA56D963569951AFE70FE1690F378D13D8AD6F8054DFAA100907F8B6F11DDBCC4817E543845FDDD4CB881849B64226F3DE397625669D87B919BC4FB0'
|
||||
'resolver_secret': '9E46E79FEB3AB3D45F4EB3EA957DEAF5D9639A0179F1850AFABA7E58F87C74C4'
|
||||
'resolver_public': '9327C5E64783E19C339BD6B680A56DB85521CC6E4E0CA5DF5274E2D3CE026C6B'
|
||||
'es_version': 1
|
||||
'certificate_ttl': 8760h
|
||||
```
|
||||
|
||||
[dnscconf]: https://github.com/ameshkov/dnscrypt/blob/master/README.md#configure
|
||||
|
||||
|
||||
|
||||
## <a href="#connectivity-check" id="connectivity-check" name="connectivity-check">Connectivity Check</a>
|
||||
|
||||
The `connectivity_check` object has the following properties:
|
||||
|
||||
* <a href="#connectivity_check-probe_ipv4" id="connectivity_check-probe_ipv4" name="connectivity_check-probe_ipv4">`probe_ipv4`</a>:
|
||||
The IPv4 address with port to which a connectivity check is performed.
|
||||
|
||||
**Example:** `8.8.8.8:53`.
|
||||
|
||||
* <a href="#connectivity_check-probe_ipv6" id="connectivity_check-probe_ipv6" name="connectivity_check-probe_ipv6">`probe_ipv6`</a>:
|
||||
The optional IPv6 address with port to which a connectivity check is
|
||||
performed. This field is required in case of any IPv6 address in
|
||||
[`bind_addresses`](#sg-s-*-bind_addresses).
|
||||
|
||||
**Example:** `[2001:4860:4860::8888]:53`.
|
||||
|
||||
[env-backend]: environment.md#BACKEND_ENDPOINT
|
||||
[env-blocked_services]: environment.md#BLOCKED_SERVICE_INDEX_URL
|
152
doc/debugdns.md
Normal file
152
doc/debugdns.md
Normal file
@ -0,0 +1,152 @@
|
||||
# AdGuard DNS Query Debugging API
|
||||
|
||||
You can debug AdGuard DNS queries by performing a query with the `CHAOS` class:
|
||||
|
||||
```sh
|
||||
dig CH A 'example.com' @dns.adguard-dns.com
|
||||
```
|
||||
|
||||
An example of the reply from AdGuard DNS:
|
||||
|
||||
```none
|
||||
;; Warning: Message parser reports malformed message packet.
|
||||
|
||||
; <<>> DiG 9.10.6 <<>> @127.0.0.1 -p 8182 example.com CH
|
||||
; (1 server found)
|
||||
;; global options: +cmd
|
||||
;; Got answer:
|
||||
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 40344
|
||||
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 3
|
||||
|
||||
;; OPT PSEUDOSECTION:
|
||||
; EDNS: version: 0, flags: do; udp: 4096
|
||||
;; QUESTION SECTION:
|
||||
;example.com. CH A
|
||||
|
||||
;; ANSWER SECTION:
|
||||
example.com. 17597 IN A 93.184.216.34
|
||||
|
||||
;; ADDITIONAL SECTION:
|
||||
client-ip.adguard-dns.com. 10 CH TXT "127.0.0.1"
|
||||
resp.res-type.adguard-dns.com. 10 CH TXT "normal"
|
||||
|
||||
;; Query time: 26 msec
|
||||
;; SERVER: dns.adguard-dns.com#53(127.0.0.1)
|
||||
;; WHEN: Wed Oct 27 16:54:47 MSK 2021
|
||||
;; MSG SIZE rcvd: 166
|
||||
```
|
||||
|
||||
In the `ANSWER SECTION`, the usual `IN` reply is returned.
|
||||
|
||||
In the `ADDITIONAL SECTION`, the following debug information is returned:
|
||||
|
||||
* <a href="#additional-client-ip" id="additional-client-ip" name="additional-client-ip">`client-ip`</a>:
|
||||
The IP address of the client. The full name is `client-ip.adguard-dns.com`.
|
||||
|
||||
**Example:**
|
||||
|
||||
```none
|
||||
client-ip.adguard-dns.com. 10 CH TXT "127.0.0.1"
|
||||
```
|
||||
|
||||
* <a href="#additional-device-id" id="additional-device-id" name="additional-device-id">`device-id`</a>:
|
||||
The ID of the device as detected by the server, if any. The full name is
|
||||
`device-id.adguard-dns.com`.
|
||||
|
||||
**Example:**
|
||||
|
||||
```none
|
||||
device-id.adguard-dns.com. 10 CH TXT "dev1234"
|
||||
```
|
||||
|
||||
* <a href="#additional-profile-id" id="additional-profile-id" name="additional-profile-id">`profile-id`</a>:
|
||||
The ID of the profile (aka “DNS server” on the UI) of the AdGuard DNS
|
||||
server. The full name is `profile-id.adguard-dns.com`.
|
||||
|
||||
**Example:**
|
||||
|
||||
```none
|
||||
profile-id.adguard-dns.com. 10 CH TXT "prof1234"
|
||||
```
|
||||
|
||||
* <a href="#additional-country" id="additional-country" name="additional-country">`country`</a>:
|
||||
User's country code. The full name is `country.adguard-dns.com`.
|
||||
|
||||
**Example:**
|
||||
|
||||
```none
|
||||
country.adguard-dns.com. 10 CH TXT "CY"
|
||||
```
|
||||
|
||||
* <a href="#additional-asn" id="additional-asn" name="additional-asn">`asn`</a>:
|
||||
User's autonomous system number (ASN). The full name is
|
||||
`asn.adguard-dns.com`.
|
||||
|
||||
**Example:**
|
||||
|
||||
```none
|
||||
asn.adguard-dns.com. 10 CH TXT "1234"
|
||||
```
|
||||
|
||||
The following debug records can have one of two prefixes: `req` or `resp`. The
|
||||
prefix depends on whether the filtering was applied to the request or the
|
||||
response.
|
||||
|
||||
* <a href="#additional-res-type" id="additional-res-type" name="additional-res-type">`res-type`</a>:
|
||||
The `type` of response. The full name is
|
||||
`(req|resp).res-type.adguard-dns.com`. Can be the following types:
|
||||
|
||||
* <a href="#additional-res-type-normal" id="additional-res-type-normal" name="additional-res-type-normal">`normal`</a>:
|
||||
The request or response was not filtered.
|
||||
|
||||
* <a href="#additional-res-type-blocked" id="additional-res-type-blocked" name="additional-res-type-blocked">`blocked`</a>:
|
||||
The request or response was blocked by a filter list or parental
|
||||
protection.
|
||||
|
||||
* <a href="#additional-res-type-allowed" id="additional-res-type-allowed" name="additional-res-type-allowed">`allowed`</a>:
|
||||
The request or response was allowed by an exception rule.
|
||||
|
||||
* <a href="#additional-res-type-modified" id="additional-res-type-modified" name="additional-res-type-modified">`modified`</a>:
|
||||
The query has been rewritten by a rewrite rule or parental protection.
|
||||
|
||||
**Example:**
|
||||
|
||||
```none
|
||||
req.res-type.adguard-dns.com. 10 CH TXT "blocked"
|
||||
```
|
||||
|
||||
* <a href="#additional-rule" id="additional-rule" name="additional-rule">`rule`</a>:
|
||||
The rule that was applied to the query. The full name is
|
||||
`(req|resp).rule.adguard-dns.com`. Rules that are longer than 255 bytes are
|
||||
split into several consecutive strings.
|
||||
|
||||
**Example:**
|
||||
|
||||
Rule shorter than 255 bytes:
|
||||
|
||||
```none
|
||||
req.rule.adguard-dns.com. 10 CH TXT "||example.com^"
|
||||
```
|
||||
|
||||
Rule longer than 255 bytes:
|
||||
|
||||
```none
|
||||
req.rule.adguard-dns.com. 0 CH TXT "||heregoesthefirstpartoftherule"
|
||||
"heregoesthesecondpartoftherule"
|
||||
```
|
||||
|
||||
* <a href="#additional-rule-list-id" id="additional-rule-list-id" name="additional-rule-list-id">`rule-list-id`</a>:
|
||||
The ID of the rule list that was applied, if any. The full name is
|
||||
`(req|resp).rule-list-id.adguard-dns.com`.
|
||||
|
||||
|
||||
**Example:**
|
||||
|
||||
```none
|
||||
req.rule-list-id.adguard-dns.com. 10 CH TXT "adguard_dns_filter"
|
||||
```
|
||||
|
||||
The TTL of these responses is taken from parameter
|
||||
[`filters.response_ttl`][conf-filters-ttl] in the configuration file.
|
||||
|
||||
[conf-filters-ttl]: configuration.md#filters-response_ttl
|
54
doc/debughttp.md
Normal file
54
doc/debughttp.md
Normal file
@ -0,0 +1,54 @@
|
||||
# AdGuard DNS Debug HTTP API
|
||||
|
||||
The AdGuard DNS debug HTTP API is served on [`LISTEN_PORT`][env-listen_port] and
|
||||
contains various private debugging information.
|
||||
|
||||
## Contents
|
||||
|
||||
* [`GET /dnsdb/csv`](#dnsdb-csv)
|
||||
* [`GET /health-check`](#health-check)
|
||||
* [`GET /metrics`](#metrics)
|
||||
* [`GET /debug/pprof`](#pprof)
|
||||
|
||||
[env-listen_port]: environment.md#LISTEN_PORT
|
||||
|
||||
|
||||
|
||||
## <a href="#dnsdb-csv" id="dnsdb-csv" name="dnsdb-csv">`GET /dnsdb/csv`</a>
|
||||
|
||||
The CSV dump of the current DNSDB statistics. Example of the output:
|
||||
|
||||
```csv
|
||||
example.com,A,NOERROR,93.184.216.34,42
|
||||
example.com,AAAA,NOERROR,2606:2800:220:1:248:1893:25c8:1946,123
|
||||
```
|
||||
|
||||
The response is sent with the `Transfer-Encoding` set to `chunked` and with an
|
||||
HTTP trailer named `X-Error` which describes errors that might have occurred
|
||||
during the database dump.
|
||||
|
||||
**NOTE:** For legacy software reasons, despite the endpoint being a `GET` one,
|
||||
it rotates the database, and so changes the internal state.
|
||||
|
||||
|
||||
|
||||
## <a href="#health-check" id="health-check" name="health-check">`GET /health-check`</a>
|
||||
|
||||
A simple health check API. Always responds with a `200 OK` status and the
|
||||
plain-text body `OK`.
|
||||
|
||||
|
||||
|
||||
## <a href="#metrics" id="metrics" name="metrics">`GET /metrics`</a>
|
||||
|
||||
Prometheus metrics HTTP API. See the [metrics page][metrics] for more details.
|
||||
|
||||
[metrics]: metrics.md
|
||||
|
||||
|
||||
|
||||
## <a href="#pprof" id="pprof" name="pprof">`GET /debug/pprof`</a>
|
||||
|
||||
The HTTP interface of Go's [PProf HTTP API][pprof api].
|
||||
|
||||
[pprof api]: https://pkg.go.dev/net/http/pprof
|
274
doc/development.md
Normal file
274
doc/development.md
Normal file
@ -0,0 +1,274 @@
|
||||
# AdGuard DNS Development Setup
|
||||
|
||||
Development is supported on Linux and macOS (aka Darwin) systems.
|
||||
|
||||
1. Install Go 1.18 or later.
|
||||
|
||||
1. Call `make init` to set up the Git pre-commit hook.
|
||||
|
||||
1. Call `make go-tools` to install analyzers and other tools into the `bin`
|
||||
directory.
|
||||
|
||||
|
||||
|
||||
## <a href="#makefile" id="makefile" name="makefile">Common Makefile Macros And Targets</a>
|
||||
|
||||
Most development tasks are done through the use of our Makefile. Please keep
|
||||
the Makefile POSIX-compliant and portable.
|
||||
|
||||
|
||||
|
||||
### <a href="#makefile-macros" id="makefile-macros" name="makefile-macros">Macros</a>
|
||||
|
||||
This is not an extensive list. See `../Makefile` and the scripts in the
|
||||
`../scripts/make/` directory.
|
||||
|
||||
<dl>
|
||||
<dt><code>OUT</code></dt>
|
||||
<dd>
|
||||
The name of the binary to build. Default: <code>./AdGuardDNS</code>.
|
||||
</dd>
|
||||
<dt><code>RACE</code></dt>
|
||||
<dd>
|
||||
Set to <code>1</code> to enable the race detector. The race detector is
|
||||
always enabled for <code>make go-test</code>.
|
||||
</dd>
|
||||
<dt><code>VERBOSE</code></dt>
|
||||
<dd>
|
||||
Set to <code>1</code> to enable verbose mode. Default: <code>0</code>.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
|
||||
|
||||
### <a href="#makefile-targets" id="makefile-targets" name="makefile-targets">Targets</a>
|
||||
|
||||
This is not an extensive list. See `../Makefile`.
|
||||
|
||||
<dl>
|
||||
<dt><code>make init</code></dt>
|
||||
<dd>
|
||||
Set up the pre-commit hook that runs checks, linters, and tests.
|
||||
</dd>
|
||||
<dt><code>make go-build</code></dt>
|
||||
<dd>
|
||||
Build the binary. See also the <code>OUT</code> and <code>RACE</code>
|
||||
macros.
|
||||
</dd>
|
||||
<dt><code>make go-gen</code></dt>
|
||||
<dd>
|
||||
Regenerate the automatically generated Go files. Those generated files
|
||||
are <code>../internal/agd/country_generate.go</code> and
|
||||
<code>../internal/geoip/asntops_generate.go</code>. They need to be
|
||||
periodically updated.
|
||||
</dd>
|
||||
<dt><code>make go-lint</code></dt>
|
||||
<dd>
|
||||
Run Go checkers and static analysis.
|
||||
</dd>
|
||||
<dt><code>make go-test</code></dt>
|
||||
<dd>
|
||||
Run Go tests.
|
||||
</dd>
|
||||
<dt><code>make go-bench</code></dt>
|
||||
<dd>
|
||||
Run Go benchmarks.
|
||||
</dd>
|
||||
<dt><code>make go-tools</code></dt>
|
||||
<dd>
|
||||
Install the Go static analysis tools locally.
|
||||
</dd>
|
||||
<dt><code>make test</code></dt>
|
||||
<dd>
|
||||
Currently does the same thing as <code>make go-test</code> but is
|
||||
defined both because it's a common target and also in case code in
|
||||
another language appears in the future.
|
||||
</dd>
|
||||
<dt><code>make txt-lint</code></dt>
|
||||
<dd>
|
||||
Run plain text checkers.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
|
||||
|
||||
## <a href="#run" id="run" name="run">How To Run AdGuard DNS</a>
|
||||
|
||||
This is an example on how to run AdGuard DNS locally.
|
||||
|
||||
|
||||
|
||||
### <a href="#run-1" id="run-1" name="run-1">Step 1: Prepare The TLS Certificate And The Key</a>
|
||||
|
||||
Keeping the test files in the `test` directory since it's added to `.gitignore`:
|
||||
|
||||
```sh
|
||||
mkdir test
|
||||
cd test
|
||||
```
|
||||
|
||||
Generate the TLS certificate and the key:
|
||||
|
||||
```sh
|
||||
openssl req -nodes -new -x509 -keyout cert.key -out cert.crt
|
||||
```
|
||||
|
||||
Also, generate TLS session tickets:
|
||||
|
||||
```sh
|
||||
openssl rand 32 > ./tls_key_1
|
||||
openssl rand 32 > ./tls_key_2
|
||||
```
|
||||
|
||||
|
||||
|
||||
### <a href="#run-2" id="run-2" name="run-2">Step 2: Prepare The DNSCrypt Configuration</a>
|
||||
|
||||
Install the [`dnscrypt`][dnsc] tool:
|
||||
|
||||
* On macOS, install from Brew:
|
||||
|
||||
```sh
|
||||
brew install ameshkov/tap/dnscrypt
|
||||
```
|
||||
|
||||
* On other unixes, such as Linux, [download][dnscdl] and install the latest
|
||||
release manually.
|
||||
|
||||
Then, generate the configuration:
|
||||
|
||||
```sh
|
||||
dnscrypt generate -p testdns -o ./dnscrypt.yml
|
||||
```
|
||||
|
||||
|
||||
|
||||
### <a href="#run-3" id="run-3" name="run-3">Step 3: Prepare The Configuration File</a>
|
||||
|
||||
```sh
|
||||
cd ../
|
||||
cp -f config.dist.yml config.yml
|
||||
```
|
||||
|
||||
|
||||
|
||||
### <a href="#run-4" id="run-4" name="run-4">Step 4: Prepare The Test Data</a>
|
||||
|
||||
```sh
|
||||
echo '<html><body>Dangerous content ahead</body></html>' > ./test/block_page_sb.html
|
||||
echo '<html><body>Adult content ahead</body></html>' > ./test/block_page_adult.html
|
||||
echo '<html><body>Error 404</body></html>' > ./test/error_404.html
|
||||
echo '<html><body>Error 500</body></html>' > ./test/error_500.html
|
||||
```
|
||||
|
||||
|
||||
|
||||
### <a href="#run-5" id="run-5" name="run-5">Step 5: Compile AdGuard DNS</a>
|
||||
|
||||
```sh
|
||||
make build
|
||||
```
|
||||
|
||||
|
||||
|
||||
### <a href="#run-6" id="run-6" name="run-6">Step 6: Prepare Cache Data And GeoIP</a>
|
||||
|
||||
We'll use the test versions of the GeoIP databases here.
|
||||
|
||||
```sh
|
||||
rm -f -r ./test/cache/
|
||||
mkdir ./test/cache
|
||||
curl 'https://raw.githubusercontent.com/maxmind/MaxMind-DB/main/test-data/GeoIP2-Country-Test.mmdb' -o ./test/GeoIP2-Country-Test.mmdb
|
||||
curl 'https://raw.githubusercontent.com/maxmind/MaxMind-DB/main/test-data/GeoLite2-ASN-Test.mmdb' -o ./test/GeoLite2-ASN-Test.mmdb
|
||||
```
|
||||
|
||||
|
||||
|
||||
### <a href="#run-7" id="run-7" name="run-7">Step 7: Run AdGuard DNS</a>
|
||||
|
||||
You'll need to supply the following:
|
||||
|
||||
* [`BACKEND_ENDPOINT`](#env-BACKEND_ENDPOINT)
|
||||
* [`CONSUL_ALLOWLIST_URL`](#env-CONSUL_ALLOWLIST_URL)
|
||||
* [`GENERAL_SAFE_SEARCH_URL`](#env-GENERAL_SAFE_SEARCH_URL)
|
||||
* [`YOUTUBE_SAFE_SEARCH_URL`](#env-YOUTUBE_SAFE_SEARCH_URL)
|
||||
|
||||
See the [external HTTP API documentation][externalhttp].
|
||||
|
||||
You may need to change the listen ports in `config.yml` which are less than 1024
|
||||
to some other ports. Otherwise, `sudo` or `doas` is required to run
|
||||
`AdGuardDNS`.
|
||||
|
||||
Examples below are for the configuration with the following changes:
|
||||
|
||||
* Plain DNS: `53` → `5354`
|
||||
* DoT: `853` → `8853`
|
||||
* DoH: `443` → `8443`
|
||||
* DoQ: `853` → `8853`
|
||||
|
||||
```sh
|
||||
env \
|
||||
BACKEND_ENDPOINT='PUT BACKEND URL HERE' \
|
||||
BLOCKED_SERVICE_INDEX_URL='https://atropnikov.github.io/HostlistsRegistry/assets/services.json'\
|
||||
CONSUL_ALLOWLIST_URL='PUT CONSUL ALLOWLIST URL HERE' \
|
||||
CONFIG_PATH='./config.yml' \
|
||||
DNSDB_PATH='./test/cache/dnsdb.bolt' \
|
||||
FILTER_INDEX_URL='https://atropnikov.github.io/HostlistsRegistry/assets/filters.json' \
|
||||
FILTER_CACHE_PATH='./test/cache' \
|
||||
GENERAL_SAFE_SEARCH_URL='https://adguardteam.github.io/HostlistsRegistry/assets/engines_safe_search.txt' \
|
||||
GEOIP_ASN_PATH='./test/GeoLite2-ASN-Test.mmdb' \
|
||||
GEOIP_COUNTRY_PATH='./test/GeoIP2-Country-Test.mmdb' \
|
||||
QUERYLOG_PATH='./test/cache/querylog.jsonl' \
|
||||
LISTEN_ADDR='127.0.0.1' \
|
||||
LISTEN_PORT='8081' \
|
||||
RULESTAT_URL='https://testchrome.adtidy.org/api/1.0/rulestats.html' \
|
||||
SENTRY_DSN='https://1:1@localhost/1' \
|
||||
VERBOSE='1' \
|
||||
YOUTUBE_SAFE_SEARCH_URL='https://adguardteam.github.io/HostlistsRegistry/assets/youtube_safe_search.txt' \
|
||||
./AdGuardDNS
|
||||
```
|
||||
|
||||
[externalhttp]: externalhttp.md
|
||||
|
||||
|
||||
|
||||
### <a href="#run-8" id="run-8" name="run-8">Step 8: Test Your Instance</a>
|
||||
|
||||
Plain DNS:
|
||||
|
||||
```sh
|
||||
dnslookup example.org 127.0.0.1:5354
|
||||
```
|
||||
|
||||
DoT:
|
||||
|
||||
```sh
|
||||
VERIFY=0 dnslookup example.org tls://127.0.0.1:8853
|
||||
```
|
||||
|
||||
DoH:
|
||||
|
||||
```sh
|
||||
VERIFY=0 dnslookup example.org https://127.0.0.1:8443/dns-query
|
||||
```
|
||||
|
||||
DoQ:
|
||||
|
||||
```sh
|
||||
VERIFY=0 dnslookup example.org quic://127.0.0.1:8853
|
||||
```
|
||||
|
||||
Open `http://127.0.0.1:8081/metrics` to see the server's metrics.
|
||||
|
||||
DNSCrypt is a bit trickier. You need to open `dnscrypt.yml` and use values from
|
||||
there to generate an SDNS stamp on <https://dnscrypt.info/stamps>.
|
||||
|
||||
**NOTE:** The example below is for a test configuration that won't work for
|
||||
you.
|
||||
|
||||
```sh
|
||||
dnslookup example.org sdns://AQcAAAAAAAAADjEyNy4wLjAuMTo1NDQzIAbKgP3dmXybr1DaKIFgKjsc8zSFX4rgT_hFgymSq6w1FzIuZG5zY3J5cHQtY2VydC50ZXN0ZG5z
|
||||
```
|
||||
|
||||
[dnsc]: https://github.com/ameshkov/dnscrypt
|
||||
[dnscdl]: https://github.com/ameshkov/dnscrypt/releases
|
235
doc/environment.md
Normal file
235
doc/environment.md
Normal file
@ -0,0 +1,235 @@
|
||||
# AdGuard DNS Environment Configuration
|
||||
|
||||
AdGuard DNS uses [environment variables][wiki-env] to store some of the more
|
||||
sensitive configuration. All other configuration is stored in the
|
||||
[configuration file][conf].
|
||||
|
||||
## Contents
|
||||
|
||||
* [`BACKEND_ENDPOINT`](#BACKEND_ENDPOINT)
|
||||
* [`BLOCKED_SERVICE_INDEX_URL`](#BLOCKED_SERVICE_INDEX_URL)
|
||||
* [`CONSUL_ALLOWLIST_URL`](#CONSUL_ALLOWLIST_URL)
|
||||
* [`CONSUL_DNSCHECK_KV_URL`](#CONSUL_DNSCHECK_KV_URL)
|
||||
* [`CONSUL_DNSCHECK_SESSION_URL`](#CONSUL_DNSCHECK_SESSION_URL)
|
||||
* [`CONFIG_PATH`](#CONFIG_PATH)
|
||||
* [`DNSDB_PATH`](#DNSDB_PATH)
|
||||
* [`FILTER_INDEX_URL`](#FILTER_INDEX_URL)
|
||||
* [`FILTER_CACHE_PATH`](#FILTER_CACHE_PATH)
|
||||
* [`GENERAL_SAFE_SEARCH_URL`](#GENERAL_SAFE_SEARCH_URL)
|
||||
* [`GEOIP_ASN_PATH` and `GEOIP_COUNTRY_PATH`](#GEOIP_ASN_PATH)
|
||||
* [`LISTEN_ADDR`](#LISTEN_ADDR)
|
||||
* [`LISTEN_PORT`](#LISTEN_PORT)
|
||||
* [`LOG_TIMESTAMP`](#LOG_TIMESTAMP)
|
||||
* [`QUERYLOG_PATH`](#QUERYLOG_PATH)
|
||||
* [`RULESTAT_URL`](#RULESTAT_URL)
|
||||
* [`SENTRY_DSN`](#SENTRY_DSN)
|
||||
* [`SSL_KEY_LOG_FILE`](#SSL_KEY_LOG_FILE)
|
||||
* [`VERBOSE`](#VERBOSE)
|
||||
* [`YOUTUBE_SAFE_SEARCH_URL`](#YOUTUBE_SAFE_SEARCH_URL)
|
||||
|
||||
[conf]: configuration.md
|
||||
[wiki-env]: https://en.wikipedia.org/wiki/Environment_variable
|
||||
|
||||
|
||||
|
||||
## <a href="#BACKEND_ENDPOINT" id="BACKEND_ENDPOINT" name="BACKEND_ENDPOINT">`BACKEND_ENDPOINT`</a>
|
||||
|
||||
The base backend URL to which API paths are appended. The backend endpoints
|
||||
apart from the `/ddns/`and `/linkip/` ones must reply with a 200 status code on
|
||||
success.
|
||||
|
||||
**Default:** No default value, the variable is **required.**
|
||||
|
||||
|
||||
|
||||
## <a href="#BLOCKED_SERVICE_INDEX_URL" id="BLOCKED_SERVICE_INDEX_URL" name="BLOCKED_SERVICE_INDEX_URL">`BLOCKED_SERVICE_INDEX_URL`</a>
|
||||
|
||||
The URL of the blocked service index file server. See the [external HTTP API
|
||||
requirements section][ext-blocked] on the expected format of the response.
|
||||
|
||||
**Default:** No default value, the variable is **required.**
|
||||
|
||||
[ext-blocked]: externalhttp.md#filters-blocked-services
|
||||
|
||||
|
||||
|
||||
## <a href="#CONFIG_PATH" id="CONFIG_PATH" name="CONFIG_PATH">`CONFIG_PATH`</a>
|
||||
|
||||
The path to the configuration file.
|
||||
|
||||
**Default:** `./config.yml`.
|
||||
|
||||
|
||||
|
||||
## <a href="#CONSUL_ALLOWLIST_URL" id="CONSUL_ALLOWLIST_URL" name="CONSUL_ALLOWLIST_URL">`CONSUL_ALLOWLIST_URL`</a>
|
||||
|
||||
The URL of the Consul instance serving the dynamic part of the rate-limit
|
||||
allowlist. See the [external HTTP API requirements section][ext-consul] on the
|
||||
expected format of the response.
|
||||
|
||||
**Default:** No default value, the variable is **required.**
|
||||
|
||||
[ext-consul]: externalhttp.md#consul
|
||||
|
||||
|
||||
|
||||
## <a href="#CONSUL_DNSCHECK_KV_URL" id="CONSUL_DNSCHECK_KV_URL" name="CONSUL_DNSCHECK_KV_URL">`CONSUL_DNSCHECK_KV_URL`</a>
|
||||
|
||||
The URL of the KV API of the Consul instance used as a key-value database for
|
||||
the DNS server checking. It must end with `/kv/<NAMESPACE>` where `<NAMESPACE>`
|
||||
is any non-empty namespace. If not specified, the
|
||||
[`CONSUL_DNSCHECK_SESSION_URL`](#CONSUL_DNSCHECK_SESSION_URL) is also
|
||||
omitted.
|
||||
|
||||
**Default:** **Unset.**
|
||||
|
||||
**Example:** `http://localhost:8500/v1/kv/test`
|
||||
|
||||
|
||||
|
||||
## <a href="#CONSUL_DNSCHECK_SESSION_URL" id="CONSUL_DNSCHECK_SESSION_URL" name="CONSUL_DNSCHECK_SESSION_URL">`CONSUL_DNSCHECK_SESSION_URL`</a>
|
||||
|
||||
The URL of the session API of the Consul instance used as a key-value database
|
||||
for the DNS server checking. If not specified, the
|
||||
[`CONSUL_DNSCHECK_KV_URL`](#CONSUL_DNSCHECK_KV_URL) is also omitted.
|
||||
|
||||
**Default:** **Unset.**
|
||||
|
||||
**Example:** `http://localhost:8500/v1/session/create`
|
||||
|
||||
|
||||
|
||||
## <a href="#DNSDB_PATH" id="DNSDB_PATH" name="DNSDB_PATH">`DNSDB_PATH`</a>
|
||||
|
||||
The path to the DNSDB BoltDB database. If empty or unset, DNSDB statistics
|
||||
collection is disabled.
|
||||
|
||||
**Default:** **Unset.**
|
||||
|
||||
**Example:** `./dnsdb.bolt`.
|
||||
|
||||
|
||||
|
||||
## <a href="#FILTER_CACHE_PATH" id="FILTER_CACHE_PATH" name="FILTER_CACHE_PATH">`FILTER_CACHE_PATH`</a>
|
||||
|
||||
The path to the directory with the filter lists cache.
|
||||
|
||||
**Default:** `./filters/`.
|
||||
|
||||
|
||||
|
||||
## <a href="#FILTER_INDEX_URL" id="FILTER_INDEX_URL" name="FILTER_INDEX_URL">`FILTER_INDEX_URL`</a>
|
||||
|
||||
The URL of the filtering rule index file server. See the [external HTTP API
|
||||
requirements section][ext-lists] on the expected format of the response.
|
||||
|
||||
**Default:** No default value, the variable is **required.**
|
||||
|
||||
[ext-lists]: externalhttp.md#filters-lists
|
||||
|
||||
|
||||
|
||||
## <a href="#GENERAL_SAFE_SEARCH_URL" id="GENERAL_SAFE_SEARCH_URL" name="GENERAL_SAFE_SEARCH_URL">`GENERAL_SAFE_SEARCH_URL`</a>
|
||||
|
||||
The URL of the list of general safe search rewriting rules. See the [external
|
||||
HTTP API requirements section][ext-general] on the expected format of the
|
||||
response.
|
||||
|
||||
**Default:** No default value, the variable is **required.**
|
||||
|
||||
[ext-general]: externalhttp.md#filters-safe-search
|
||||
|
||||
|
||||
|
||||
## <a href="#GEOIP_ASN_PATH" id="GEOIP_ASN_PATH" name="GEOIP_ASN_PATH">`GEOIP_ASN_PATH` and `GEOIP_COUNTRY_PATH`</a>
|
||||
|
||||
Paths to the files containing MaxMind GeoIP databases: for ASNs and for
|
||||
countries and continents respectively.
|
||||
|
||||
**Default:** `./asn.mmdb` and `./country.mmdb`.
|
||||
|
||||
|
||||
|
||||
## <a href="#LOG_TIMESTAMP" id="LOG_TIMESTAMP" name="LOG_TIMESTAMP">`LOG_TIMESTAMP`</a>
|
||||
|
||||
If `1`, show timestamps in the plain text logs. If `0`, don't show the
|
||||
timestamps.
|
||||
|
||||
**Default:** `1`.
|
||||
|
||||
|
||||
|
||||
## <a href="#LISTEN_ADDR" id="LISTEN_ADDR" name="LISTEN_ADDR">`LISTEN_ADDR`</a>
|
||||
|
||||
The IP address on which to bind the [debug HTTP API][debughttp].
|
||||
|
||||
**Default:** `127.0.0.1`.
|
||||
|
||||
[debughttp]: debughttp.md
|
||||
|
||||
|
||||
|
||||
## <a href="#LISTEN_PORT" id="LISTEN_PORT" name="LISTEN_PORT">`LISTEN_PORT`</a>
|
||||
|
||||
The port on which to bind the [debug HTTP API][debughttp], which includes the
|
||||
health check, Prometheus, `pprof`, and other endpoints.
|
||||
|
||||
**Default:** `8181`.
|
||||
|
||||
|
||||
|
||||
## <a href="#QUERYLOG_PATH" id="QUERYLOG_PATH" name="QUERYLOG_PATH">`QUERYLOG_PATH`</a>
|
||||
|
||||
The path to the file into which the query log is going to be written.
|
||||
|
||||
**Default:** `./querylog.jsonl`.
|
||||
|
||||
|
||||
|
||||
## <a href="#RULESTAT_URL" id="RULESTAT_URL" name="RULESTAT_URL">`RULESTAT_URL`</a>
|
||||
|
||||
The URL to send filtering rule list statistics to. If empty or unset, the
|
||||
collection of filtering rule statistics is disabled. See the [external HTTP API
|
||||
requirements section][ext-rulestat] on the expected format of the response.
|
||||
|
||||
**Default:** **Unset.**
|
||||
|
||||
**Example:** `https://stats.example.com/db`
|
||||
|
||||
[ext-rulestat]: externalhttp.md#rulestat
|
||||
|
||||
|
||||
|
||||
## <a href="#SENTRY_DSN" id="SENTRY_DSN" name="SENTRY_DSN">`SENTRY_DSN`</a>
|
||||
|
||||
Sentry error collector address. The special value `stderr` makes AdGuard DNS
|
||||
print these errors to standard error.
|
||||
|
||||
**Default:** `stderr`.
|
||||
|
||||
|
||||
|
||||
## <a href="#SSL_KEY_LOG_FILE" id="SSL_KEY_LOG_FILE" name="SSL_KEY_LOG_FILE">`SSL_KEY_LOG_FILE`</a>
|
||||
|
||||
If set, TLS key logs are written to this file to allow other programs (i.e.
|
||||
Wireshark) to decrypt packets. **Must only be used for debug purposes**.
|
||||
|
||||
**Default:** **Unset.**
|
||||
|
||||
|
||||
|
||||
## <a href="#VERBOSE" id="VERBOSE" name="VERBOSE">`VERBOSE`</a>
|
||||
|
||||
When set to `1`, enable verbose logging. When set to `0`, disable it.
|
||||
|
||||
**Default:** `0`.
|
||||
|
||||
|
||||
|
||||
## <a href="#YOUTUBE_SAFE_SEARCH_URL" id="YOUTUBE_SAFE_SEARCH_URL" name="YOUTUBE_SAFE_SEARCH_URL">`YOUTUBE_SAFE_SEARCH_URL`</a>
|
||||
|
||||
The URL of the list of YouTube-specific safe search rewriting rules. See the
|
||||
[external HTTP API requirements section][ext-general] on the expected format of
|
||||
the response.
|
||||
|
||||
**Default:** No default value, the variable is **required.**
|
262
doc/externalhttp.md
Normal file
262
doc/externalhttp.md
Normal file
@ -0,0 +1,262 @@
|
||||
# AdGuard DNS External HTTP API Requirements
|
||||
|
||||
AdGuard DNS uses information from external HTTP APIs for filtering and other
|
||||
pieces of its functionality. Whenever it makes requests to these services,
|
||||
AdGuard DNS sets the `User-Agent` header. All services described in this
|
||||
document should set the `Server` header in their replies.
|
||||
|
||||
<!--
|
||||
TODO(a.garipov): Reinspect uses of “should” and “must” throughout this
|
||||
document.
|
||||
-->
|
||||
|
||||
|
||||
|
||||
## Contents
|
||||
|
||||
* [Backend And Linked IP Service](#backend)
|
||||
* [`GET /dns_api/v1/settings`](#backend-get-v1-settings)
|
||||
* [`POST /dns_api/v1/settings`](#backend-post-v1-devices_activity)
|
||||
* [Proxied Linked IP and Dynamic DNS (DDNS) Endpoints](#backend-linkip)
|
||||
* [Consul Key-Value Storage](#consul)
|
||||
* [Filtering](#filters)
|
||||
* [Blocked Services](#filters-blocked-services)
|
||||
* [Filtering Rule Lists](#filters-lists)
|
||||
* [Safe Search](#filters-safe-search)
|
||||
* [Rule Statistics Service](#rulestat)
|
||||
|
||||
|
||||
|
||||
## <a href="#backend" id="backend" name="backend">Backend And Linked IP Service</a>
|
||||
|
||||
This is the service to which the [`BACKEND_ENDPOINT`][env-backend] environment
|
||||
variable points. This service must provide two endpoints:
|
||||
|
||||
|
||||
|
||||
### <a href="#backend-get-v1-settings" id="backend-get-v1-settings" name="backend-get-v1-settings">`GET /dns_api/v1/settings`</a>
|
||||
|
||||
This endpoint must respond with a `200 OK` response code and a JSON document in
|
||||
the following format:
|
||||
|
||||
```json
|
||||
{
|
||||
"sync_time": 1624443079309,
|
||||
"settings": [
|
||||
{
|
||||
"dns_id": "83f3ea8f",
|
||||
"filtering_enabled": true,
|
||||
"query_log_enabled": true,
|
||||
"safe_browsing":
|
||||
{
|
||||
"enabled": true
|
||||
},
|
||||
"deleted": true,
|
||||
"block_private_relay": false,
|
||||
"devices": [
|
||||
{
|
||||
"id": "0d7724fa",
|
||||
"name": "Device 1",
|
||||
"filtering_enabled": true
|
||||
"linked_ip": "1.2.3.4"
|
||||
}
|
||||
],
|
||||
"parental": {
|
||||
"enabled": false,
|
||||
"block_adult": false,
|
||||
"general_safe_search": false,
|
||||
"youtube_safe_search": false,
|
||||
"blocked_services": [
|
||||
"youtube"
|
||||
],
|
||||
"schedule": {
|
||||
"tmz": "GMT",
|
||||
"mon": [
|
||||
"0s",
|
||||
"59m"
|
||||
],
|
||||
"tue": [
|
||||
"0s",
|
||||
"59m"
|
||||
],
|
||||
"wed": [
|
||||
"0s",
|
||||
"59m"
|
||||
],
|
||||
"thu": [
|
||||
"0s",
|
||||
"59m"
|
||||
],
|
||||
"fri": [
|
||||
"0s",
|
||||
"59m"
|
||||
]
|
||||
}
|
||||
},
|
||||
"rule_lists": {
|
||||
"enabled": true,
|
||||
"ids": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
"filtered_response_ttl": 3600,
|
||||
"custom_rules": [
|
||||
"||example.org^"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### <a href="#backend-post-v1-devices_activity" id="backend-post-v1-devices_activity" name="backend-post-v1-devices_activity">`POST /dns_api/v1/devices_activity`</a>
|
||||
|
||||
This endpoint must respond with a `200 OK` response code and accept a JSON
|
||||
document in the following format:
|
||||
|
||||
```json
|
||||
{
|
||||
"devices": [
|
||||
{
|
||||
"client_country": "AU",
|
||||
"device_id": "abcd1234",
|
||||
"time_ms": 1624443079309,
|
||||
"asn": 1234,
|
||||
"queries": 1000,
|
||||
"proto": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### <a href="#backend-linkip" id="backend-linkip" name="backend-linkip">Proxied Linked IP and Dynamic DNS (DDNS) Endpoints</a>
|
||||
|
||||
The same service defined by the [`BACKEND_ENDPOINT`][env-backend] environment
|
||||
variable should define the following endpoints:
|
||||
|
||||
* `GET /linkip/{device_id}/{encrypted}/status`;
|
||||
* `GET /linkip/{device_id}/{encrypted}`;
|
||||
* `POST /ddns/{device_id}/{encrypted}/{domain}`;
|
||||
* `POST /linkip/{device_id}/{encrypted}`.
|
||||
|
||||
The AdGuard DNS proxy will add the `X-Forwarded-For` header with the IP address
|
||||
of the original client.
|
||||
|
||||
[env-backend]: environment.md#BACKEND_ENDPOINT
|
||||
|
||||
|
||||
|
||||
## <a href="#consul" id="consul" name="consul">Consul Key-Value Storage</a>
|
||||
|
||||
A [Consul][consul-io] service can be used for the DNS server check and dynamic
|
||||
rate-limit allowlist features. Currently used endpoints can be seen in the
|
||||
documentation of the [`CONSUL_ALLOWLIST_URL`][env-consul-allowlist],
|
||||
[`CONSUL_DNSCHECK_KV_URL`][env-consul-dnscheck-kv], and
|
||||
[`CONSUL_DNSCHECK_SESSION_URL`][env-consul-dnscheck-session] environment
|
||||
variables.
|
||||
|
||||
[consul-io]: https://www.consul.io/
|
||||
[env-consul-allowlist]: environment.md#CONSUL_ALLOWLIST_URL
|
||||
[env-consul-dnscheck-kv]: environment.md#CONSUL_DNSCHECK_KV_URL
|
||||
[env-consul-dnscheck-session]: environment.md#CONSUL_DNSCHECK_SESSION_URL
|
||||
|
||||
|
||||
|
||||
## <a href="#filters" id="filters" name="filters">Filtering</a>
|
||||
|
||||
### <a href="#filters-blocked-services" id="filters-blocked-services" name="filters-blocked-services">Blocked Services</a>
|
||||
|
||||
This endpoint, defined by [`BLOCKED_SERVICE_INDEX_URL`][env-services], must
|
||||
respond with a `200 OK` response code and a JSON document in the following
|
||||
format:
|
||||
|
||||
```json
|
||||
{
|
||||
"blocked_services": [
|
||||
{
|
||||
"id": "my_filter",
|
||||
"rules": [
|
||||
"||example.com^",
|
||||
"||example.net^",
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
All properties must be filled with valid IDs and rules. Additional fields in
|
||||
objects are ignored.
|
||||
|
||||
|
||||
|
||||
### <a href="#filters-lists" id="filters-lists" name="filters-lists">Filtering Rule Lists</a>
|
||||
|
||||
This endpoint, defined by [`FILTER_INDEX_URL`][env-filters], must respond with a
|
||||
`200 OK` response code and a JSON document in the following format:
|
||||
|
||||
```json
|
||||
{
|
||||
"filters": [
|
||||
{
|
||||
"downloadUrl": "https://cdn.example.com/assets/my_filter.txt",
|
||||
"id": "my_filter"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
All properties must be filled with valid IDs and URLs. Additional fields in
|
||||
objects are ignored.
|
||||
|
||||
|
||||
|
||||
### <a href="#filters-safe-search" id="filters-safe-search" name="filters-safe-search">Safe Search</a>
|
||||
|
||||
These endpoints, defined by [`GENERAL_SAFE_SEARCH_URL`][env-general] and
|
||||
[`YOUTUBE_SAFE_SEARCH_URL`][env-youtube], must respond with a `200 OK` response
|
||||
code and filtering rule lists with [`$dnsrewrite`][rules-dnsrewrite] rules for
|
||||
`A`, `AAAA`, or `CNAME` types. For example, for YouTube:
|
||||
|
||||
```none
|
||||
|m.youtube.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||
|www.youtube-nocookie.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||
|www.youtube.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||
|youtube.googleapis.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||
|youtubei.googleapis.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||
```
|
||||
|
||||
[env-filters]: environment.md#FILTER_INDEX_URL
|
||||
[env-general]: environment.md#GENERAL_SAFE_SEARCH_URL
|
||||
[env-services]: environment.md#BLOCKED_SERVICE_INDEX_URL
|
||||
[env-youtube]: environment.md#YOUTUBE_SAFE_SEARCH_URL
|
||||
|
||||
<!--
|
||||
TODO(a.garipov): Replace with a link to the new KB when it is finished.
|
||||
-->
|
||||
[rules-dnsrewrite]: https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists#dnsrewrite
|
||||
|
||||
|
||||
|
||||
## <a href="#rulestat" id="rulestat" name="rulestat">Rule Statistics Service</a>
|
||||
|
||||
This endpoint, defined by [`RULESTAT_URL`][env-rulestat], must respond with a
|
||||
`200 OK` response code and accept a JSON document in the following format:
|
||||
|
||||
```json
|
||||
{
|
||||
"filters": [
|
||||
{
|
||||
"15": {
|
||||
"||example.com^": 1234,
|
||||
"||example.net^": 5678,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The objects may include new properties in the future.
|
||||
|
||||
[env-rulestat]: environment.md#RULESTAT_URL
|
150
doc/http.md
Normal file
150
doc/http.md
Normal file
@ -0,0 +1,150 @@
|
||||
# AdGuard DNS HTTP API
|
||||
|
||||
The main HTTP API is served on the same port as the DNS-over-HTTP servers as
|
||||
well as on other addresses, if the [web configuration][conf-web] is set
|
||||
appropriately.
|
||||
|
||||
## Contents
|
||||
|
||||
* [Block Pages](#block-pages)
|
||||
* [DNS Server Check](#dnscheck-test)
|
||||
* [Linked IP Proxy](#linked-ip-proxy)
|
||||
* [Static Content](#static-content)
|
||||
|
||||
[conf-web]: configuration.md#web
|
||||
|
||||
|
||||
|
||||
## <a href="#block-pages" id="block-pages" name="block-pages">Block Pages</a>
|
||||
|
||||
The safe browsing and adult blocking servers. Every request is responded with
|
||||
the content from the configured file, with the exception of `GET /favicon.ico`
|
||||
and `GET /robots.txt` requests, which are handled separately:
|
||||
|
||||
* `GET /favicon.ico` requests are responded with a plain-text `404 Not Found`
|
||||
response.
|
||||
|
||||
* `GET /robots.txt` requests are responded with:
|
||||
|
||||
```none
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
```
|
||||
|
||||
The [static content](#static-content) is not served on these servers.
|
||||
|
||||
|
||||
|
||||
## <a href="#dnscheck-test" id="dnscheck-test" name="dnscheck-test">DNS Server Check</a>
|
||||
|
||||
`GET /dnscheck/test` is the DNS server check HTTP API. It should be requested
|
||||
with a random ID prepended to one of the [check domains][conf-check-domains]
|
||||
with a hyphen. The random ID must have from 4 to 63 characters and only include
|
||||
the alphanumeric characters and a hyphen.
|
||||
|
||||
Example of the request:
|
||||
|
||||
```sh
|
||||
curl 'https://0123-abcd-dnscheck.example.com/dnscheck/test'
|
||||
```
|
||||
|
||||
Example of the output:
|
||||
|
||||
```json
|
||||
{
|
||||
"client_ip": "1.2.3.4",
|
||||
"device_id": "abcd1234",
|
||||
"profile_id": "defa5678",
|
||||
"protocol": "dot",
|
||||
"node_location": "ams",
|
||||
"node_name": "eu-1.dns.example.com",
|
||||
"server_group_name": "adguard_dns_default",
|
||||
"server_name": "default_dns"
|
||||
}
|
||||
```
|
||||
|
||||
The `protocol` field can have one of the following values:
|
||||
|
||||
<dl>
|
||||
<dt>
|
||||
<code>"dns-tcp"</code>
|
||||
</dt>
|
||||
<dd>
|
||||
Plain DNS over TCP.
|
||||
</dd>
|
||||
<dt>
|
||||
<code>"dns-udp"</code>
|
||||
</dt>
|
||||
<dd>
|
||||
Plain DNS over UDP.
|
||||
</dd>
|
||||
<dt>
|
||||
<code>"dnscrypt-tcp"</code>
|
||||
</dt>
|
||||
<dd>
|
||||
DNSCrypt over TCP.
|
||||
</dd>
|
||||
<dt>
|
||||
<code>"dnscrypt-udp"</code>
|
||||
</dt>
|
||||
<dd>
|
||||
DNSCrypt over UDP.
|
||||
</dd>
|
||||
<dt>
|
||||
<code>"doh"</code>
|
||||
</dt>
|
||||
<dd>
|
||||
DNS-over-HTTP.
|
||||
</dd>
|
||||
<dt>
|
||||
<code>"doq"</code>
|
||||
</dt>
|
||||
<dd>
|
||||
DNS-over-QUIC.
|
||||
</dd>
|
||||
<dt>
|
||||
<code>"dot"</code>
|
||||
</dt>
|
||||
<dd>
|
||||
DNS-over-TLS.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
[conf-check-domains]: configuration.md#check-domains
|
||||
|
||||
|
||||
|
||||
## <a href="#linked-ip-proxy" id="linked-ip-proxy" name="linked-ip-proxy">Linked IP Proxy</a>
|
||||
|
||||
The linked IP and Dynamic DNS (DDNS, DynDNS) HTTP proxy. If the [linked
|
||||
IP configuration][conf-web-linked_ip] is not empty, the following queries are
|
||||
either processed or proxied to [`BACKEND_ENDPOINT`][env-backend].
|
||||
|
||||
* `GET /robots.txt`: a special response is served, see below;
|
||||
* `GET /linkip/{device_id}/{encrypted}/status`: proxied;
|
||||
* `GET /linkip/{device_id}/{encrypted}`: proxied;
|
||||
* `POST /ddns/{device_id}/{encrypted}/{domain}`: proxied;
|
||||
* `POST /linkip/{device_id}/{encrypted}`: proxied.
|
||||
|
||||
In the case of a `GET /robots.txt` request, the following content is served:
|
||||
|
||||
```none
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
```
|
||||
|
||||
The [static content](#static-content) is not served on the linked IP addresses.
|
||||
|
||||
[conf-web-linked_ip]: configuration.md#web-linked_ip
|
||||
[env-backend]: environment.md#BACKEND_ENDPOINT
|
||||
|
||||
|
||||
|
||||
## <a href="#static-content" id="static-content" name="static-content">Static Content</a>
|
||||
|
||||
The static content server. Enabled if the [static content
|
||||
configuration][conf-web-static_content] is not empty. Static content is not
|
||||
served on the linked IP proxy server and the safe browsing and adult blocking
|
||||
servers.
|
||||
|
||||
[conf-web-static_content]: configuration.md#web-static_content
|
3
doc/metrics.md
Normal file
3
doc/metrics.md
Normal file
@ -0,0 +1,3 @@
|
||||
# AdGuard DNS Prometheus Metrics
|
||||
|
||||
**TODO(a.garipov):** Describe the metrics.
|
272
doc/querylog.md
Normal file
272
doc/querylog.md
Normal file
@ -0,0 +1,272 @@
|
||||
# AdGuard DNS Query Log Format
|
||||
|
||||
The query log is written in the [JSONL][jsonl] (JSON Lines) format. The log
|
||||
entries are designed to be concise and easily compressable. An example of the
|
||||
log output:
|
||||
|
||||
```jsonl
|
||||
{"u":"ABCD","b":"prof1234","i":"dev1234","c":"RU","d":"US","n":"example.com.","l":"cdef5678","m":"||example.com^","t":1628590394000,"a":1234,"e":5,"q":1,"f":2,"s":0,"p":2,"r":0}
|
||||
{"u":"DEFG","b":"prof1234","i":"dev1234","c":"RU","d":"JP","n":"example.org.","l":"hijk9012","m":"||example.org^","t":1628590394100,"a":6789,"e":6,"q":1,"f":2,"s":0,"p":2,"r":0}
|
||||
```
|
||||
|
||||
AdGuard DNS opens and closes the log file on each write to prevent issues with
|
||||
external log rotation.
|
||||
|
||||
[jsonl]: https://jsonlines.org/
|
||||
|
||||
|
||||
|
||||
## <a href="#properties" id="properties" name="properties">Properties</a>
|
||||
|
||||
Property names have been chosen to be single-letter but still have mnemonic
|
||||
rules to remember, which property means what. The properties are:
|
||||
|
||||
* <a href="#properties-u" id="properties-u" name="properties-u">`u`</a>:
|
||||
The unique ID of the request. The short name `u` stands for “unique”.
|
||||
|
||||
**Example:** `"ABCD1234"`
|
||||
|
||||
* <a href="#properties-b" id="properties-b" name="properties-b">`b`</a>:
|
||||
The detected profile ID (also known as DNS ID and DNS Server ID), if any.
|
||||
The short name `b` stands for “buyer”.
|
||||
|
||||
**Example:** `"prof1234"`
|
||||
|
||||
* <a href="#properties-i" id="properties-i" name="properties-i">`i`</a>:
|
||||
The detected device ID, if any. The short name `i` stands for “ID”.
|
||||
|
||||
**Example:** `"dev1234"`
|
||||
|
||||
* <a href="#properties-c" id="properties-c" name="properties-c">`c`</a>:
|
||||
The detected country of the client's IP address as an [ISO 3166-1
|
||||
alpha-2][wiki-iso] country code, if any. If none could be detected, this
|
||||
property is absent. The short name `c` stands for “client country”.
|
||||
|
||||
**NOTE:** AdGuard DNS uses the common user-assigned ISO 3166-1 alpha-2 code
|
||||
`XK` for the partially-recognized state of the Republic of Kosovo.
|
||||
|
||||
**Example:** `"AU"`
|
||||
|
||||
* <a href="#properties-d" id="properties-d" name="properties-d">`d`</a>:
|
||||
The detected country of the first IP address in the response sent to the
|
||||
client, as an [ISO 3166-1 alpha-2][wiki-iso] country code, if any. If none
|
||||
could be detected, this property is absent. The short name `d` stands for
|
||||
“destination”.
|
||||
|
||||
**NOTE:** AdGuard DNS uses the common user-assigned ISO 3166-1 alpha-2 code
|
||||
`XK` for the partially-recognized state of the Republic of Kosovo.
|
||||
|
||||
**Example:** `"US"`
|
||||
|
||||
* <a href="#properties-n" id="properties-n" name="properties-n">`n`</a>:
|
||||
The requested resource name. The short name `n` stands for “name”.
|
||||
|
||||
**Example:** `"example.com."`
|
||||
|
||||
* <a href="#properties-l" id="properties-l" name="properties-l">`l`</a>:
|
||||
The ID of the first filter the rules of which matched this query. If no
|
||||
rules matched, this property is omitted. The short name `l` stands for
|
||||
“list of filter rules”.
|
||||
|
||||
**Example:** `"adguard_dns_filter"`
|
||||
|
||||
The special reserved values are:
|
||||
|
||||
* `blocked_service`: the request was blocked by the service blocker. The
|
||||
property `m` contains the ID of that blocked service.
|
||||
|
||||
* `custom`: the request was filtered by a custom profile rule.
|
||||
|
||||
* `adult_blocking`: the request was filtered by the adult content blocking
|
||||
filter.
|
||||
|
||||
* `safe_browsing`: the request was filtered by the safe browsing filter.
|
||||
|
||||
* `general_safe_search`: the request was modified by the general safe
|
||||
search filter.
|
||||
|
||||
* `youtube_safe_search`: the request was modified by the YouTube safe
|
||||
search filter.
|
||||
|
||||
* <a href="#properties-m" id="properties-m" name="properties-m">`m`</a>:
|
||||
The text of the first rule that matched this query or the ID of the blocked
|
||||
service, if the ID of the filtering rule list is `blocked_service`. If no
|
||||
rules matched, this property is omitted. The short name `m` stands for
|
||||
“match”.
|
||||
|
||||
**Object examples:**
|
||||
|
||||
```json
|
||||
{
|
||||
"l": "adguard_dns_filter",
|
||||
"m": "||example.com^",
|
||||
"...": "..."
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"l": "blocked_service",
|
||||
"m": "example",
|
||||
"...": "..."
|
||||
}
|
||||
```
|
||||
|
||||
* <a href="#properties-t" id="properties-t" name="properties-t">`t`</a>:
|
||||
The [Unix time][wiki-unix] at which the request was received, in
|
||||
milliseconds. The short name `t` stands for “time”.
|
||||
|
||||
**Example:** `1629974298000`
|
||||
|
||||
* <a href="#properties-a" id="properties-a" name="properties-a">`a`</a>:
|
||||
The detected [autonomous system][wiki-asn] number (aka ASN) of the client's
|
||||
IP address, if any. If none could be detected, this property is absent.
|
||||
The short name `a` stands for “ASN”.
|
||||
|
||||
**Example:** `1234`
|
||||
|
||||
* <a href="#properties-e" id="properties-e" name="properties-e">`e`</a>:
|
||||
The time passed since the beginning of the request processing, in
|
||||
milliseconds. The short name `e` stands for “elapsed”.
|
||||
|
||||
**Example:** `3`
|
||||
|
||||
* <a href="#properties-q" id="properties-q" name="properties-q">`q`</a>:
|
||||
The type of the resource record of the query. The short name `q` stands for
|
||||
“question”.
|
||||
|
||||
**Example:** `1`
|
||||
|
||||
See [this Wikipedia list][wiki-dnsrr] for numeric values and their meanings.
|
||||
|
||||
* <a href="#properties-f" id="properties-f" name="properties-f">`f`</a>:
|
||||
The action taken with this request. The short name `f` stands for
|
||||
“filtering”. The possible values are:
|
||||
|
||||
<dl>
|
||||
<dt>
|
||||
<code>0</code>
|
||||
</dt>
|
||||
<dd>
|
||||
Invalid or unknown action. Typically, this value is never used.
|
||||
</dd>
|
||||
<dt>
|
||||
<code>1</code>
|
||||
</dt>
|
||||
<dd>
|
||||
No filtering.
|
||||
<dt>
|
||||
<code>2</code>
|
||||
</dt>
|
||||
<dd>
|
||||
The request (question) is blocked.
|
||||
</dd>
|
||||
<dt>
|
||||
<code>3</code>
|
||||
</dt>
|
||||
<dd>
|
||||
The response (answer) is blocked.
|
||||
</dd>
|
||||
<dt>
|
||||
<code>4</code>
|
||||
</dt>
|
||||
<dd>
|
||||
The request (question) is allowed by an allowlist rule.
|
||||
</dd>
|
||||
<dt>
|
||||
<code>5</code>
|
||||
</dt>
|
||||
<dd>
|
||||
The response (answer) is allowed by an allowlist rule.
|
||||
</dd>
|
||||
<dt>
|
||||
<code>6</code>
|
||||
</dt>
|
||||
<dd>
|
||||
The request (question) or response (answer) was modified or
|
||||
rewritten by a safety filter or a DNS rewrite rule.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
**Example:** `2`
|
||||
|
||||
* <a href="#properties-s" id="properties-s" name="properties-s">`s`</a>:
|
||||
The status of whether the response was validated with DNSSEC. `0` means no,
|
||||
`1` means yes. The short name `s` stands for “secure”.
|
||||
|
||||
**Example:** `1`
|
||||
|
||||
* <a href="#properties-p" id="properties-p" name="properties-p">`p`</a>:
|
||||
The DNS protocol used to process this request. The short name `p` stands
|
||||
for “protocol”. The possible values are:
|
||||
|
||||
<dl>
|
||||
<dt>
|
||||
<code>0</code>
|
||||
</dt>
|
||||
<dd>
|
||||
Invalid or unknown protocol. Typically, this value is never used.
|
||||
</dd>
|
||||
<dt>
|
||||
<code>1</code>
|
||||
</dt>
|
||||
<dd>
|
||||
Plain DNS over TCP.
|
||||
</dd>
|
||||
<dt>
|
||||
<code>2</code>
|
||||
</dt>
|
||||
<dd>
|
||||
Plain DNS over UDP.
|
||||
</dd>
|
||||
<dt>
|
||||
<code>3</code>
|
||||
</dt>
|
||||
<dd>
|
||||
DNS-over-HTTP.
|
||||
</dd>
|
||||
<dt>
|
||||
<code>4</code>
|
||||
</dt>
|
||||
<dd>
|
||||
DNS-over-QUIC.
|
||||
</dd>
|
||||
<dt>
|
||||
<code>5</code>
|
||||
</dt>
|
||||
<dd>
|
||||
DNS-over-TLS.
|
||||
</dd>
|
||||
<dt>
|
||||
<code>6</code>
|
||||
</dt>
|
||||
<dd>
|
||||
DNSCrypt over TCP.
|
||||
</dd>
|
||||
<dt>
|
||||
<code>7</code>
|
||||
</dt>
|
||||
<dd>
|
||||
DNSCrypt over UDP.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
**Example:** `2`
|
||||
|
||||
* <a href="#properties-r" id="properties-r" name="properties-r">`r`</a>:
|
||||
The response code (aka `RCODE`) sent to the client. The short name `r`
|
||||
stands for “response”.
|
||||
|
||||
**Example:** `0`
|
||||
|
||||
See [this IANA list][iana-rcode] for numeric values and their meanings.
|
||||
|
||||
See also [file `internal/querylog/entry.go`][file-entry.go] for an explanation
|
||||
of the properties, their names, and mnemonics.
|
||||
|
||||
[file-entry.go]: ../internal/querylog/entry.go
|
||||
[iana-rcode]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6
|
||||
[wiki-asn]: https://en.wikipedia.org/wiki/Autonomous_system_(Internet)
|
||||
[wiki-dnsrr]: https://en.wikipedia.org/wiki/List_of_DNS_record_types
|
||||
[wiki-iso]: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
||||
[wiki-unix]: https://en.wikipedia.org/wiki/Unix_time
|
74
go.mod
74
go.mod
@ -1,24 +1,62 @@
|
||||
module github.com/AdguardTeam/AdGuardDNS
|
||||
|
||||
go 1.13
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/AdguardTeam/urlfilter v0.10.0
|
||||
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6
|
||||
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833
|
||||
github.com/caddyserver/caddy v1.0.5
|
||||
github.com/coredns/coredns v1.6.9
|
||||
github.com/joomcode/errorx v1.0.1
|
||||
github.com/miekg/dns v1.1.31
|
||||
github.com/oschwald/geoip2-golang v1.4.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.7.1
|
||||
github.com/stretchr/testify v1.5.1
|
||||
go.etcd.io/bbolt v1.3.4
|
||||
go.uber.org/atomic v1.6.0
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974
|
||||
golang.org/x/tools v0.0.0-20201028025901-8cd080b735b3 // indirect
|
||||
github.com/AdguardTeam/AdGuardDNS/internal/dnsserver v0.100.0
|
||||
github.com/AdguardTeam/golibs v0.10.9
|
||||
github.com/AdguardTeam/urlfilter v0.16.0
|
||||
github.com/ameshkov/dnscrypt/v2 v2.2.3
|
||||
github.com/bluele/gcache v0.0.2
|
||||
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b
|
||||
github.com/caarlos0/env/v6 v6.9.3
|
||||
github.com/getsentry/raven-go v0.2.0
|
||||
github.com/google/renameio v1.0.1
|
||||
github.com/miekg/dns v1.1.50
|
||||
github.com/oschwald/maxminddb-golang v1.10.0
|
||||
github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible
|
||||
github.com/prometheus/client_golang v1.13.0
|
||||
github.com/prometheus/client_model v0.2.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
|
||||
golang.org/x/net v0.0.0-20220812174116-3211cb980234
|
||||
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
replace github.com/coredns/coredns => github.com/ameshkov/coredns v1.2.5-0.20201214113603-34360d0c4346
|
||||
require (
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
|
||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
|
||||
github.com/ameshkov/dnsstamps v1.0.3 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/cheekybits/genny v1.0.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/lucas-clemente/quic-go v0.28.1 // indirect
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
|
||||
github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/AdguardTeam/AdGuardDNS/internal/dnsserver => ./internal/dnsserver
|
||||
|
165
go.work.sum
Normal file
165
go.work.sum
Normal file
@ -0,0 +1,165 @@
|
||||
cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8=
|
||||
cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA=
|
||||
cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ=
|
||||
cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU=
|
||||
cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3 h1:hJiie5Bf3QucGRa4ymsAUOxyhYwGEz1xrsVk0P8erlw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0 h1:SPOUaucgtVls75mg+X7CXigS71EnsfVUK/2CgVrwqgw=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412 h1:GvWw74lx5noHocd+f6HBMXK6DuggBB1dhVkuGZbv7qM=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c h1:ivON6cwHK1OH26MZyWDCnbTRZZf0IhNsENoNAKFS1g4=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999 h1:OR8VhtwhcAI3U48/rzBsVOuHi0zDPzYI1xASVcdSgR8=
|
||||
github.com/AdguardTeam/golibs v0.10.7/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
|
||||
github.com/AdguardTeam/gomitmproxy v0.2.0 h1:rvCOf17pd1/CnMyMQW891zrEiIQBpQ8cIGjKN9pinUU=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625 h1:ckJgFhFWywOx+YLEMIJsTb+NV6NexWICk5+AMSuz3ss=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 h1:D21IyuvjDCshj1/qq+pCNd3VZOAEI9jy6Bi131YlXgI=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
|
||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d h1:t5Wuyh53qYyg9eqn4BbnlIT+vmhyww0TatL+zT3uWgI=
|
||||
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4 h1:rEvIZUSZ3fx39WIi3JkQqQBitGwpELBIYWeBVh6wn+E=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/gliderlabs/ssh v0.1.1 h1:j3L6gSLQalDETeEg/Jg0mGY0/y/N6zI2xX1978P0Uqw=
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I=
|
||||
github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
|
||||
github.com/go-kit/log v0.2.0 h1:7i2K3eKTos3Vc0enKCfnVcgHh2olr/MyfboYq7cAcFw=
|
||||
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 h1:2hRPrmiwPrp3fQX967rNJIhQPtiGXdlQWAxKbKw3VHA=
|
||||
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
|
||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
||||
github.com/google/martian/v3 v3.0.0 h1:pMen7vLs8nvgEYhywH3KDWJIJTeEr2ULsVWHWYHQyBs=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99 h1:Ak8CrdlwwXwAZxzS66vgPt4U8yUZX7JwLvVR58FN5jM=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0 h1:WcmKMm43DR7RdtlkEXQJyo5ws8iTp98CyhCCbOHMvNI=
|
||||
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1 h1:ujPKutqRlJtcfWk6toYVYagwra7HQHbXOaS171b4Tg8=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
|
||||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pty v1.1.3 h1:/Um6a/ZmD5tF7peoOJ5oN5KMQ0DrGVQSXLNwyckutPk=
|
||||
github.com/lucas-clemente/quic-go v0.25.0/go.mod h1:YtzP8bxRVCBlO77yRanE264+fY/T2U9ZlW1AaHOsMOg=
|
||||
github.com/lucas-clemente/quic-go v0.27.1/go.mod h1:AzgQoPda7N+3IqMMMkywBKggIFo2KT6pfnlrQ2QieeI=
|
||||
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe h1:W/GaMY0y69G4cFlmsC6B9sbuo2fP8OFP1ABjt4kPz+w=
|
||||
github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs=
|
||||
github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.4/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.0/go.mod h1:fz4HIxByo+LlWcreM4CZOYNuz3taBQ8rN2X6FqvaWo8=
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.1/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s=
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.0-beta.1/go.mod h1:PUhIQk19LoFt2174H4+an8TYvWOGjb/hHwphBeaDHwI=
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.0/go.mod h1:PUhIQk19LoFt2174H4+an8TYvWOGjb/hHwphBeaDHwI=
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.1/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44=
|
||||
github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 h1:D6paGObi5Wud7xg83MaEFyjxQB1W5bz5d0IFppr+ymk=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab h1:eFXv9Nu1lGbrNbj619aWwZfVF5HBrm9Plte8aNptuTI=
|
||||
github.com/openzipkin/zipkin-go v0.1.1 h1:A/ADD6HaPnAKj3yS7HjGHRK77qi41Hi0DirOOIQAeIw=
|
||||
github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4 h1:Fth6mevc5rX7glNLpbAMJnqKlfIkcTjZCSHEeqvKbcI=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48 h1:vabduItPAIz9px5iryD5peyx7O3Ya8TBThapgXim98o=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470 h1:qb9IthCFBmROJ6YBS31BEMeSYjOscSiG+EO+JVNTz64=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc=
|
||||
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d h1:Yoy/IzG4lULT6qZg62sVC+qyBL8DQkmD2zv6i7OImrc=
|
||||
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c h1:UOk+nlt1BJtTcH15CT7iNO7YVWTfTv/DNwEAQHLIaDQ=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b h1:vYEG87HxbU6dXj5npkeulCS96Dtz5xg3jcfCgpcvbIw=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20 h1:7pDq9pAMCQgRohFmd25X8hIH8VxmT3TaDm+r9LHxgBk=
|
||||
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9 h1:MPblCbqA5+z6XARjScMfz1TqtJC7TuTRj0U9VqIBs6k=
|
||||
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50 h1:crYRwvwjdVh1biHzzciFHe8DrZcYrVcZFlJtykhRctg=
|
||||
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc h1:eHRtZoIi6n9Wo1uR+RU44C247msLWwyA89hVKwRLkMk=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 h1:SWV2fHctRpRrp49VXJ6UZja7gU9QLHwRpIPBN89SKEo=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9 h1:fxoFD0in0/CBzXoyNhMTjvBZYW6ilSnTw7N7y/8vkmM=
|
||||
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191 h1:T4wuULTrzCKMFlg3HmKHgXAF8oStFb/+lOIupLV2v+o=
|
||||
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241 h1:Y+TeIabU8sJD10Qwd/zMty2/LEaT9GNDaA6nyZf+jgo=
|
||||
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122 h1:TQVQrsyNaimGwF7bIhzoVC9QkKm4KsWd8cECGzFx8gI=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2 h1:bu666BQci+y4S0tVRVjsHUeRon6vUXmsGBwdowgMrg4=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82 h1:LneqU9PHDsg/AkPDU3AkqMxnMYL+imaqkpflHu73us8=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537 h1:YGaxtkYjb8mnTvtufv2LKLwCQu2/C7qFB7UtrOlTWOY=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133 h1:JtcyT0rk/9PKOdnKQzuDR+FSjh7SGtJwpgVpfZBRKlQ=
|
||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8=
|
||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
|
||||
github.com/viant/assertly v0.4.8 h1:5x1GzBaRteIwTr5RAGFVG14uNeRFxVNbXPWrK2qAgpc=
|
||||
github.com/viant/toolbox v0.24.0 h1:6TteTDQ68CjgcCe8wH3D3ZhUQQOJXMTbj/D9rkk2a1k=
|
||||
github.com/yuin/goldmark v1.4.1 h1:/vn0k+RBvwlxEmP5E7SZMqNxPhfMVFEJiykr15/0XKM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5 h1:+hE86LblG4AyDgwMCLTE6FOlM9+qjHSYS+rKqxUVdsM=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d h1:E2M5QgjZ/Jg+ObCQAudsXxuTsLj7Nl5RV/lZcQZmKSo=
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220516155154-20f960328961/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852 h1:xYq6+9AtI+xP3M4r0N1hCkHrInHDBohhquRgx9Kk6gI=
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w=
|
||||
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 h1:PDIOdWxZ8eRizhKa1AAvY53xsvLB1cWorMjslvY3VA8=
|
||||
google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 h1:tmXTu+dfa+d9Evp8NpJdgOy6+rt8/x4yG7qPBrtNfLY=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
|
||||
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
|
||||
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
|
||||
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0 h1:eTiIR0CoWjGzJcnQ3OkhIl/b9GJovq4lSAVRt0ZFEG8=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c=
|
@ -1,5 +0,0 @@
|
||||
# health
|
||||
|
||||
Fork of https://github.com/coredns/coredns/tree/master/plugin/health
|
||||
|
||||
In order to change the URL.
|
@ -1,69 +0,0 @@
|
||||
// Package health implements an HTTP handler that responds to health checks.
|
||||
package health
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
"github.com/coredns/coredns/plugin/pkg/reuseport"
|
||||
)
|
||||
|
||||
var log = clog.NewWithPlugin("health")
|
||||
|
||||
// Health implements healthchecks by exporting a HTTP endpoint.
|
||||
type health struct {
|
||||
Addr string
|
||||
lameduck time.Duration
|
||||
|
||||
ln net.Listener
|
||||
nlSetup bool
|
||||
mux *http.ServeMux
|
||||
|
||||
stop chan bool
|
||||
}
|
||||
|
||||
func (h *health) OnStartup() error {
|
||||
if h.Addr == "" {
|
||||
h.Addr = ":8080"
|
||||
}
|
||||
h.stop = make(chan bool)
|
||||
ln, err := reuseport.Listen("tcp", h.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.ln = ln
|
||||
h.mux = http.NewServeMux()
|
||||
h.nlSetup = true
|
||||
|
||||
h.mux.HandleFunc("/health-check", func(w http.ResponseWriter, r *http.Request) {
|
||||
// We're always healthy.
|
||||
w.WriteHeader(http.StatusOK)
|
||||
io.WriteString(w, http.StatusText(http.StatusOK))
|
||||
})
|
||||
|
||||
go func() { http.Serve(h.ln, h.mux) }()
|
||||
go func() { h.overloaded() }()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *health) OnFinalShutdown() error {
|
||||
if !h.nlSetup {
|
||||
return nil
|
||||
}
|
||||
|
||||
if h.lameduck > 0 {
|
||||
log.Infof("Going into lameduck mode for %s", h.lameduck)
|
||||
time.Sleep(h.lameduck)
|
||||
}
|
||||
|
||||
h.ln.Close()
|
||||
|
||||
h.nlSetup = false
|
||||
close(h.stop)
|
||||
return nil
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// overloaded queries the health end point and updates a metrics showing how long it took.
|
||||
func (h *health) overloaded() {
|
||||
timeout := time.Duration(5 * time.Second)
|
||||
client := http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
url := "http://" + h.Addr
|
||||
tick := time.NewTicker(1 * time.Second)
|
||||
defer tick.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
start := time.Now()
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
HealthDuration.Observe(timeout.Seconds())
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
HealthDuration.Observe(time.Since(start).Seconds())
|
||||
|
||||
case <-h.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// HealthDuration is the metric used for exporting how fast we can retrieve the /health endpoint.
|
||||
HealthDuration = promauto.NewHistogram(prometheus.HistogramOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "health",
|
||||
Name: "request_duration_seconds",
|
||||
Buckets: plugin.TimeBuckets,
|
||||
Help: "Histogram of the time (in seconds) each request took.",
|
||||
})
|
||||
)
|
@ -1,66 +0,0 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
)
|
||||
|
||||
func init() { plugin.Register("health", setup) }
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
addr, lame, err := parse(c)
|
||||
if err != nil {
|
||||
return plugin.Error("health", err)
|
||||
}
|
||||
|
||||
h := &health{Addr: addr, stop: make(chan bool), lameduck: lame}
|
||||
|
||||
c.OnStartup(h.OnStartup)
|
||||
c.OnRestart(h.OnFinalShutdown)
|
||||
c.OnFinalShutdown(h.OnFinalShutdown)
|
||||
c.OnRestartFailed(h.OnStartup)
|
||||
|
||||
// Don't do AddPlugin, as health is not *really* a plugin just a separate webserver running.
|
||||
return nil
|
||||
}
|
||||
|
||||
func parse(c *caddy.Controller) (string, time.Duration, error) {
|
||||
addr := ""
|
||||
dur := time.Duration(0)
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
case 1:
|
||||
addr = args[0]
|
||||
if _, _, e := net.SplitHostPort(addr); e != nil {
|
||||
return "", 0, e
|
||||
}
|
||||
default:
|
||||
return "", 0, c.ArgErr()
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "lameduck":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 1 {
|
||||
return "", 0, c.ArgErr()
|
||||
}
|
||||
l, err := time.ParseDuration(args[0])
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("unable to parse lameduck duration value: '%v' : %v", args[0], err)
|
||||
}
|
||||
dur = l
|
||||
default:
|
||||
return "", 0, c.ArgErr()
|
||||
}
|
||||
}
|
||||
}
|
||||
return addr, dur, nil
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
# Info
|
||||
|
||||
This plugin makes it possible to check what AdGuard DNS server is in use.
|
||||
|
||||
```
|
||||
info {
|
||||
domain adguard.com
|
||||
type unfiltered
|
||||
protocol auto
|
||||
addr 176.103.130.136 176.103.130.137
|
||||
canary dnscheck.adguard.com
|
||||
}
|
||||
```
|
||||
|
||||
Discovery requests look like: `*-{protocol}-{type}-dnscheck.{domain}`.
|
||||
For instance, `12321-doh-unfiltered-dnscheck.adguard.com`. If the domain is queried
|
||||
using `doh` protocol from a server with type `unfiltered`, the request will return
|
||||
the specified `addr`. Otherwise, it will return `NXDOMAIN`.
|
||||
|
||||
* `domain` - registered domain that will be used in the discovery DNS queries.
|
||||
* `type` - server type (any string).
|
||||
* `protocol` - possible values are `dns`, `doh`, `dot`, `dnscrypt`, `auto`.
|
||||
|
||||
If it's set to `auto`, the plugin will try to detect the protocol by itself.
|
||||
If it's set to a specific protocol, the plugin won't try to detect anything.
|
||||
|
||||
* `addr` - the list of addresses to return in the discovery response.
|
||||
You can specify multiple addresses here.
|
||||
IPv4 addresses will be used for A responses, IPv6 - for AAAA.
|
||||
* `canary` - (optional) simple "canary" domain which only purpose is to test whether AdGuard DNS
|
||||
is enabled or not without any additional logic (protocol or type detection) on top of it.
|
||||
|
||||
> Note: canary domain is used by third-party services that may want to discover if AdGuard DNS is used or not.
|
||||
> For instance, Keenetic routers use it.
|
130
info/info.go
130
info/info.go
@ -1,130 +0,0 @@
|
||||
// Package info is a CoreDNS plugin for testing users' DNS settings.
|
||||
package info
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/util"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
"github.com/coredns/coredns/request"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type info struct {
|
||||
Next plugin.Handler
|
||||
|
||||
domain string // etld domain name for the check DNS requests
|
||||
protocol string // protocol (can be auto, dns, doh, doq, dot, dnscrypt)
|
||||
serverType string // server type (arbitrary string)
|
||||
canary string // canary domain
|
||||
|
||||
addrs4 []net.IP // list of IPv4 addresses to return in response to an A check request
|
||||
addrs6 []net.IP // list of IPv4 addresses to return in response to an AAAA check request
|
||||
}
|
||||
|
||||
// Name returns name of the plugin as seen in Corefile and plugin.cfg
|
||||
func (i *info) Name() string { return "info" }
|
||||
|
||||
// ServeDNS handles the DNS request and refuses if it's an ANY request
|
||||
func (i *info) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
if len(r.Question) != 1 {
|
||||
// google DNS, bind and others do the same
|
||||
return dns.RcodeFormatError, fmt.Errorf("got DNS request with != 1 questions")
|
||||
}
|
||||
|
||||
question := r.Question[0]
|
||||
host := strings.ToLower(strings.TrimSuffix(question.Name, "."))
|
||||
|
||||
if i.canary != "" && host == i.canary {
|
||||
return i.writeAnswer(w, r)
|
||||
}
|
||||
|
||||
protocol := i.getProtocol(ctx)
|
||||
checkDomain := fmt.Sprintf("-%s-%s-dnscheck.%s", protocol, i.serverType, i.domain)
|
||||
|
||||
if strings.HasSuffix(host, checkDomain) {
|
||||
return i.writeAnswer(w, r)
|
||||
}
|
||||
|
||||
return plugin.NextOrFailure(i.Name(), i.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
func (i *info) getProtocol(ctx context.Context) string {
|
||||
if i.protocol == "auto" {
|
||||
addr := util.GetServer(ctx)
|
||||
if strings.HasPrefix(addr, "tls") {
|
||||
return "dot"
|
||||
} else if strings.HasPrefix(addr, "https") {
|
||||
return "doh"
|
||||
} else if strings.HasPrefix(addr, "quic") {
|
||||
return "doq"
|
||||
}
|
||||
|
||||
return "dns"
|
||||
}
|
||||
|
||||
return i.protocol
|
||||
}
|
||||
|
||||
func (i *info) writeAnswer(w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
state := request.Request{W: w, Req: r}
|
||||
m := i.genAnswer(r)
|
||||
|
||||
state.SizeAndDo(m)
|
||||
err := state.W.WriteMsg(m)
|
||||
if err != nil {
|
||||
clog.Infof("Got error %s\n", err)
|
||||
return dns.RcodeServerFailure, err
|
||||
}
|
||||
return m.Rcode, nil
|
||||
}
|
||||
|
||||
func (i *info) genAnswer(r *dns.Msg) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(r, dns.RcodeSuccess)
|
||||
|
||||
qType := r.Question[0].Qtype
|
||||
|
||||
if qType == dns.TypeA && len(i.addrs4) > 0 {
|
||||
for _, ip := range i.addrs4 {
|
||||
m.Answer = append(m.Answer, i.genA(r, ip))
|
||||
}
|
||||
} else if qType == dns.TypeAAAA && len(i.addrs6) > 0 {
|
||||
for _, ip := range i.addrs6 {
|
||||
m.Answer = append(m.Answer, i.genAAAA(r, ip))
|
||||
}
|
||||
}
|
||||
|
||||
m.RecursionAvailable = true
|
||||
m.Compress = true
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (i *info) genA(r *dns.Msg, ip net.IP) *dns.A {
|
||||
answer := new(dns.A)
|
||||
answer.Hdr = dns.RR_Header{
|
||||
Name: r.Question[0].Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Ttl: 100,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
answer.A = ip
|
||||
return answer
|
||||
}
|
||||
|
||||
func (i *info) genAAAA(r *dns.Msg, ip net.IP) *dns.AAAA {
|
||||
answer := new(dns.AAAA)
|
||||
answer.Hdr = dns.RR_Header{
|
||||
Name: r.Question[0].Name,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Ttl: 100,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
answer.AAAA = ip
|
||||
return answer
|
||||
}
|
@ -1,114 +0,0 @@
|
||||
package info
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestInfoCheckRequest(t *testing.T) {
|
||||
cfg := `info {
|
||||
domain adguard.com
|
||||
canary dnscheck.adguard.com
|
||||
protocol auto
|
||||
type test
|
||||
addr 176.103.130.132 176.103.130.134 2a00:5a60::bad1:ff 2a00:5a60::bad2:ff
|
||||
}`
|
||||
c := caddy.NewTestController("info", cfg)
|
||||
c.ServerBlockKeys = []string{""}
|
||||
i, err := setupPlugin(c)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Prepare context
|
||||
srv := &dnsserver.Server{Addr: "https://"}
|
||||
ctx := context.WithValue(context.Background(), dnsserver.Key{}, srv)
|
||||
|
||||
// Prepare response writer
|
||||
resp := test.ResponseWriter{}
|
||||
rrw := dnstest.NewRecorder(&resp)
|
||||
|
||||
// --
|
||||
// Test type=A queries
|
||||
|
||||
// Prepare test request
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("32132124-doh-test-dnscheck.adguard.com", dns.TypeA)
|
||||
|
||||
// Pass to the plugin
|
||||
rCode, err := i.ServeDNS(ctx, rrw, req)
|
||||
|
||||
// Check rcode and error first
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, rCode)
|
||||
|
||||
// Now let's check the response
|
||||
assert.NotNil(t, rrw.Msg)
|
||||
assert.Equal(t, len(rrw.Msg.Answer), 2)
|
||||
|
||||
a1, ok := rrw.Msg.Answer[0].(*dns.A)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "176.103.130.132", a1.A.String())
|
||||
|
||||
a2, ok := rrw.Msg.Answer[1].(*dns.A)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "176.103.130.134", a2.A.String())
|
||||
|
||||
// --
|
||||
// Test type=AAAA queries
|
||||
|
||||
// Prepare test request
|
||||
req = new(dns.Msg)
|
||||
req.SetQuestion("32132124-doh-test-dnscheck.adguard.com", dns.TypeAAAA)
|
||||
|
||||
// Pass to the plugin
|
||||
rCode, err = i.ServeDNS(ctx, rrw, req)
|
||||
|
||||
// Check rcode and error first
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, rCode)
|
||||
|
||||
// Now let's check the response
|
||||
assert.NotNil(t, rrw.Msg)
|
||||
assert.Equal(t, len(rrw.Msg.Answer), 2)
|
||||
|
||||
aaaa1, ok := rrw.Msg.Answer[0].(*dns.AAAA)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "2a00:5a60::bad1:ff", aaaa1.AAAA.String())
|
||||
|
||||
aaaa2, ok := rrw.Msg.Answer[1].(*dns.AAAA)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "2a00:5a60::bad2:ff", aaaa2.AAAA.String())
|
||||
|
||||
// --
|
||||
// Test canary domain
|
||||
|
||||
// Prepare test request
|
||||
req = new(dns.Msg)
|
||||
req.SetQuestion("dnscheck.adguard.com", dns.TypeA)
|
||||
|
||||
// Pass to the plugin
|
||||
rCode, err = i.ServeDNS(ctx, rrw, req)
|
||||
|
||||
// Check rcode and error first
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, dns.RcodeSuccess, rCode)
|
||||
|
||||
// Now let's check the response
|
||||
assert.NotNil(t, rrw.Msg)
|
||||
assert.Equal(t, len(rrw.Msg.Answer), 2)
|
||||
|
||||
a1, ok = rrw.Msg.Answer[0].(*dns.A)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "176.103.130.132", a1.A.String())
|
||||
|
||||
a2, ok = rrw.Msg.Answer[1].(*dns.A)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "176.103.130.134", a2.A.String())
|
||||
}
|
113
info/setup.go
113
info/setup.go
@ -1,113 +0,0 @@
|
||||
package info
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("info", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
clog.Infof("Initializing the info plugin for %s", c.ServerBlockKeys[c.ServerBlockKeyIndex])
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := dnsserver.GetConfig(c)
|
||||
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
p.Next = next
|
||||
return p
|
||||
})
|
||||
|
||||
clog.Infof("Finished initializing the info plugin for %s", c.ServerBlockKeys[c.ServerBlockKeyIndex])
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupPlugin parses and validates the plugin configuration
|
||||
func setupPlugin(c *caddy.Controller) (*info, error) {
|
||||
i := &info{}
|
||||
|
||||
for c.Next() {
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "domain":
|
||||
if !c.NextArg() || len(c.Val()) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
i.domain = c.Val()
|
||||
case "type":
|
||||
if !c.NextArg() || len(c.Val()) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
i.serverType = c.Val()
|
||||
case "protocol":
|
||||
if !c.NextArg() || len(c.Val()) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
i.protocol = c.Val()
|
||||
case "canary":
|
||||
if !c.NextArg() || len(c.Val()) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
i.canary = c.Val()
|
||||
case "addr":
|
||||
args := c.RemainingArgs()
|
||||
for _, arg := range args {
|
||||
ip := net.ParseIP(arg)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("invalid IP %s", arg)
|
||||
}
|
||||
|
||||
if ip.To4() == nil {
|
||||
i.addrs6 = append(i.addrs6, ip)
|
||||
} else {
|
||||
i.addrs4 = append(i.addrs4, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return validate(i)
|
||||
}
|
||||
|
||||
func validate(i *info) (*info, error) {
|
||||
if i.domain == "" {
|
||||
return nil, errors.New("domain must be set")
|
||||
}
|
||||
|
||||
if i.serverType == "" {
|
||||
return nil, errors.New("server type must be set")
|
||||
}
|
||||
|
||||
switch i.protocol {
|
||||
case
|
||||
"auto",
|
||||
"dns",
|
||||
"dnscrypt",
|
||||
"doh",
|
||||
"doq",
|
||||
"dot":
|
||||
// Go on.
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid protocol %q", i.protocol)
|
||||
}
|
||||
|
||||
if len(i.addrs4) == 0 && len(i.addrs6) == 0 {
|
||||
return nil, errors.New("addr must be set")
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
package info
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
for i, testcase := range []struct {
|
||||
config string
|
||||
failing bool
|
||||
}{
|
||||
// Failing
|
||||
{`info`, true},
|
||||
{`info 100`, true},
|
||||
{`info {
|
||||
domain adguard.com
|
||||
}`, true},
|
||||
{`info {
|
||||
domain adguard.com
|
||||
protocol test
|
||||
}`, true},
|
||||
// Success
|
||||
{`info {
|
||||
domain adguard.com
|
||||
protocol auto
|
||||
type test
|
||||
addr 176.103.130.135
|
||||
}`, false},
|
||||
{`info {
|
||||
canary dnscheck.adguard.com
|
||||
domain adguard.com
|
||||
protocol auto
|
||||
type test
|
||||
addr 176.103.130.132 176.103.130.134 2a00:5a60::bad1:ff 2a00:5a60::bad2:ff
|
||||
}`, false},
|
||||
} {
|
||||
c := caddy.NewTestController("info", testcase.config)
|
||||
c.ServerBlockKeys = []string{""}
|
||||
_, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
if !testcase.failing {
|
||||
t.Fatalf("Test #%d expected no errors, but got: %v", i, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if testcase.failing {
|
||||
t.Fatalf("Test #%d expected to fail but it didn't", i)
|
||||
}
|
||||
}
|
||||
}
|
74
internal/agd/agd.go
Normal file
74
internal/agd/agd.go
Normal file
@ -0,0 +1,74 @@
|
||||
// Package agd contains common entities and interfaces of AdGuard DNS.
|
||||
package agd
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Common Constants, Types, And Utilities
|
||||
|
||||
// RequestID is the ID of a request. It is an opaque, randomly generated
|
||||
// string. API users should not rely on it being pseudorandom or
|
||||
// cryptographically random.
|
||||
type RequestID string
|
||||
|
||||
// NewRequestID returns a new pseudorandom RequestID. Prefer this to manual
|
||||
// conversion from other string types.
|
||||
func NewRequestID() (id RequestID) {
|
||||
// Generate a random 16-byte (128-bit) number, encode it into a URL-safe
|
||||
// Base64 string, and return it.
|
||||
const N = 16
|
||||
|
||||
var idData [N]byte
|
||||
_, err := rand.Read(idData[:])
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("generating random request id: %w", err))
|
||||
}
|
||||
|
||||
enc := base64.URLEncoding.WithPadding(base64.NoPadding)
|
||||
n := enc.EncodedLen(N)
|
||||
idData64 := make([]byte, n)
|
||||
enc.Encode(idData64, idData[:])
|
||||
|
||||
return RequestID(idData64)
|
||||
}
|
||||
|
||||
// unit is a convenient alias for struct{}.
|
||||
type unit = struct{}
|
||||
|
||||
// firstNonIDRune returns the first non-printable or non-ASCII rune and its
|
||||
// index. If slashes is true, it also looks for slashes. If there are no such
|
||||
// runes, i is -1.
|
||||
func firstNonIDRune(s string, slashes bool) (i int, r rune) {
|
||||
for i, r = range s {
|
||||
if r < '!' || r > '~' || (slashes && r == '/') {
|
||||
return i, r
|
||||
}
|
||||
}
|
||||
|
||||
return -1, 0
|
||||
}
|
||||
|
||||
// Unit name constants.
|
||||
const (
|
||||
UnitByte = "bytes"
|
||||
UnitRune = "runes"
|
||||
)
|
||||
|
||||
// ValidateInclusion returns an error if n is greater than max or less than min.
|
||||
// unitName is used for error messages, see UnitFoo constants.
|
||||
//
|
||||
// TODO(a.garipov): Consider switching min and max; the current order seems
|
||||
// confusing.
|
||||
func ValidateInclusion(n, max, min int, unitName string) (err error) {
|
||||
switch {
|
||||
case n > max:
|
||||
return fmt.Errorf("too long: got %d %s, max %d", n, unitName, max)
|
||||
case n < min:
|
||||
return fmt.Errorf("too short: got %d %s, min %d", n, unitName, min)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
28
internal/agd/agd_test.go
Normal file
28
internal/agd/agd_test.go
Normal file
@ -0,0 +1,28 @@
|
||||
package agd_test
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
|
||||
)
|
||||
|
||||
// Common Constants And Utilities
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
agdtest.DiscardLogOutput(m)
|
||||
}
|
||||
|
||||
// testTimeout is the timeout for common test operations.
|
||||
const testTimeout = 1 * time.Second
|
||||
|
||||
// testProfID is the profile ID for tests.
|
||||
const testProfID agd.ProfileID = "prof1234"
|
||||
|
||||
// testDevID is the device ID for tests.
|
||||
const testDevID agd.DeviceID = "dev1234"
|
||||
|
||||
// testClientIPv4 is the client IP for tests
|
||||
var testClientIPv4 = netip.AddrFrom4([4]byte{1, 2, 3, 4})
|
159
internal/agd/context.go
Normal file
159
internal/agd/context.go
Normal file
@ -0,0 +1,159 @@
|
||||
package agd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
)
|
||||
|
||||
// Common Context Helpers
|
||||
|
||||
// ctxKey is the type for all common context keys.
|
||||
type ctxKey uint8
|
||||
|
||||
const (
|
||||
ctxKeyReqID ctxKey = iota
|
||||
ctxKeyReqInfo
|
||||
)
|
||||
|
||||
// type check
|
||||
var _ fmt.Stringer = ctxKey(0)
|
||||
|
||||
// String implements the fmt.Stringer interface for ctxKey.
|
||||
func (k ctxKey) String() (s string) {
|
||||
switch k {
|
||||
case ctxKeyReqID:
|
||||
return "ctxKeyReqID"
|
||||
case ctxKeyReqInfo:
|
||||
return "ctxKeyReqInfo"
|
||||
default:
|
||||
panic(fmt.Errorf("bad ctx key value %d", k))
|
||||
}
|
||||
}
|
||||
|
||||
// panicBadType is a helper that panics with a message about the context key and
|
||||
// the expected type.
|
||||
func panicBadType(key ctxKey, v any) {
|
||||
panic(fmt.Errorf("bad type for %s: %T(%[2]v)", key, v))
|
||||
}
|
||||
|
||||
// WithRequestID returns a copy of the parent context with the request ID added.
|
||||
func WithRequestID(parent context.Context, id RequestID) (ctx context.Context) {
|
||||
return context.WithValue(parent, ctxKeyReqID, id)
|
||||
}
|
||||
|
||||
// RequestIDFromContext returns the request ID from the context, if any.
|
||||
func RequestIDFromContext(ctx context.Context) (id RequestID, ok bool) {
|
||||
const key = ctxKeyReqID
|
||||
v := ctx.Value(key)
|
||||
if v == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
id, ok = v.(RequestID)
|
||||
if !ok {
|
||||
panicBadType(key, v)
|
||||
}
|
||||
|
||||
return id, true
|
||||
}
|
||||
|
||||
// RequestInfo contains information about the current request. A RequestInfo
|
||||
// put into the context must not be modified.
|
||||
type RequestInfo struct {
|
||||
// Device is the found device. It is nil for anonymous requests. If Device
|
||||
// is present then Profile is also present.
|
||||
Device *Device
|
||||
|
||||
// Profile is the found profile. It is nil for anonymous requests. If
|
||||
// Profile is present then Device is also present.
|
||||
Profile *Profile
|
||||
|
||||
// Location is the GeoIP location data about the remote IP address, if any.
|
||||
Location *Location
|
||||
|
||||
// ECS contains the EDNS Client Subnet option information of the request, if
|
||||
// any.
|
||||
ECS *ECS
|
||||
|
||||
// FilteringGroup is the server's default filtering group.
|
||||
FilteringGroup *FilteringGroup
|
||||
|
||||
// Messages is the message constructor to be used for the filtered responses
|
||||
// to this request.
|
||||
Messages *dnsmsg.Constructor
|
||||
|
||||
// RemoteIP is the remote IP address of the client.
|
||||
RemoteIP netip.Addr
|
||||
|
||||
// ServerGroup is the name of the server group which handles this request.
|
||||
ServerGroup ServerGroupName
|
||||
|
||||
// Server is the name of the server which handles this request.
|
||||
Server ServerName
|
||||
|
||||
// ID is the unique ID of the request. It is resurfaced here to optimize
|
||||
// context lookups.
|
||||
ID RequestID
|
||||
|
||||
// Host is the lowercased, non-FQDN version of the hostname from the
|
||||
// question of the request.
|
||||
Host string
|
||||
|
||||
// QType is the type of question for this request.
|
||||
QType dnsmsg.RRType
|
||||
}
|
||||
|
||||
// ECS is the content of the EDNS Client Subnet option of a DNS message.
|
||||
//
|
||||
// See https://datatracker.ietf.org/doc/html/rfc7871#section-6.
|
||||
type ECS struct {
|
||||
// Location is the GeoIP location data about the IP address from the
|
||||
// request's ECS data, if any.
|
||||
Location *Location
|
||||
|
||||
// Subnet is the source subnet.
|
||||
Subnet netip.Prefix
|
||||
|
||||
// Scope is the scope prefix.
|
||||
Scope uint8
|
||||
}
|
||||
|
||||
// ContextWithRequestInfo returns a copy of the parent context with the request
|
||||
// and server group information added. ri must not be modified after calling
|
||||
// ContextWithRequestInfo.
|
||||
func ContextWithRequestInfo(parent context.Context, ri *RequestInfo) (ctx context.Context) {
|
||||
return context.WithValue(parent, ctxKeyReqInfo, ri)
|
||||
}
|
||||
|
||||
// RequestInfoFromContext returns the request information from the context, if
|
||||
// any. ri must not be modified.
|
||||
func RequestInfoFromContext(ctx context.Context) (ri *RequestInfo, ok bool) {
|
||||
const key = ctxKeyReqInfo
|
||||
v := ctx.Value(ctxKeyReqInfo)
|
||||
if v == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
ri, ok = v.(*RequestInfo)
|
||||
if !ok {
|
||||
panicBadType(key, v)
|
||||
}
|
||||
|
||||
return ri, true
|
||||
}
|
||||
|
||||
// MustRequestInfoFromContext is a helper that wraps a call to
|
||||
// RequestInfoFromContext and panics if the request information isn't in the
|
||||
// context. ri must not be modified.
|
||||
func MustRequestInfoFromContext(ctx context.Context) (ri *RequestInfo) {
|
||||
ri, ok := RequestInfoFromContext(ctx)
|
||||
if !ok {
|
||||
panic(errors.Error("no request info in context"))
|
||||
}
|
||||
|
||||
return ri
|
||||
}
|
1069
internal/agd/country.go
Normal file
1069
internal/agd/country.go
Normal file
File diff suppressed because it is too large
Load Diff
148
internal/agd/country_generate.go
Normal file
148
internal/agd/country_generate.go
Normal file
@ -0,0 +1,148 @@
|
||||
//go:build generate
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"net/http"
|
||||
"os"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, csvURL, nil)
|
||||
check(err)
|
||||
|
||||
req.Header.Add("User-Agent", agdhttp.UserAgent())
|
||||
|
||||
resp, err := c.Do(req)
|
||||
check(err)
|
||||
defer log.OnCloserError(resp.Body, log.ERROR)
|
||||
|
||||
out, err := os.OpenFile("./country.go", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o664)
|
||||
check(err)
|
||||
defer log.OnCloserError(out, log.ERROR)
|
||||
|
||||
r := csv.NewReader(resp.Body)
|
||||
rows, err := r.ReadAll()
|
||||
check(err)
|
||||
|
||||
// Skip the first row, as it is a header.
|
||||
rows = rows[1:]
|
||||
|
||||
// Sort by the code to make the output more predictable and easier to look
|
||||
// through.
|
||||
slices.SortFunc(rows, func(a, b []string) (less bool) {
|
||||
return a[1] < b[1]
|
||||
})
|
||||
|
||||
tmpl, err := template.New("main").Parse(tmplStr)
|
||||
check(err)
|
||||
|
||||
err = tmpl.Execute(out, rows)
|
||||
check(err)
|
||||
}
|
||||
|
||||
// csvURL is the default URL of the information about country codes.
|
||||
const csvURL = `https://raw.githubusercontent.com/lukes/ISO-3166-Countries-with-Regional-Codes/master/slim-2/slim-2.csv`
|
||||
|
||||
// tmplStr is the template of the generated Go code.
|
||||
const tmplStr = `// Code generated by go run ./country_generate.go; DO NOT EDIT.
|
||||
|
||||
package agd
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
)
|
||||
|
||||
// Country Codes
|
||||
|
||||
// Country represents an ISO 3166-1 alpha-2 country code.
|
||||
type Country string
|
||||
|
||||
// Country code constants. Note that these constants don't include the
|
||||
// user-assigned ones.
|
||||
const (
|
||||
// CountryNone is an invalid or unknown country code.
|
||||
CountryNone Country = ""{{ range . }}
|
||||
{{ $name := (index . 0) -}}
|
||||
{{ $code := (index . 1) -}}
|
||||
// Country{{$code}} is the ISO 3166-1 alpha-2 code for
|
||||
// {{ $name }}.
|
||||
Country{{$code}} Country = {{ printf "%q" $code }}{{ end }}
|
||||
|
||||
// CountryXK is the user-assigned ISO 3166-1 alpha-2 code for Republic of
|
||||
// Kosovo. Kosovo does not have a recognized ISO 3166 code, but it is still
|
||||
// an entity whose user-assigned code is relatively common.
|
||||
CountryXK Country = "XK"
|
||||
)
|
||||
|
||||
// NewCountry converts s into a Country while also validating it. Prefer to use
|
||||
// this instead of a plain conversion.
|
||||
func NewCountry(s string) (c Country, err error) {
|
||||
c = Country(s)
|
||||
if isUserAssigned(s) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
switch c {
|
||||
case
|
||||
{{ range . -}}
|
||||
{{ $code := (index . 1) -}}
|
||||
Country{{ $code }},
|
||||
{{ end -}}
|
||||
CountryNone:
|
||||
return c, nil
|
||||
default:
|
||||
return CountryNone, &NotACountryError{Code: s}
|
||||
}
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ encoding.TextUnmarshaler = (*Country)(nil)
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface for *Country.
|
||||
func (c *Country) UnmarshalText(b []byte) (err error) {
|
||||
if c == nil {
|
||||
return errors.Error("nil country")
|
||||
}
|
||||
|
||||
ctry, err := NewCountry(string(b))
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding country: %w", err)
|
||||
}
|
||||
|
||||
*c = ctry
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isUserAssigned returns true if s is a user-assigned ISO 3166-1 alpha-2
|
||||
// country code.
|
||||
func isUserAssigned(s string) (ok bool) {
|
||||
if len(s) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
return s == "AA" || s == "OO" || s == "ZZ" || s[0] == 'X' || (s[0] == 'Q' && s[1] >= 'M')
|
||||
}
|
||||
`
|
||||
|
||||
// check is a simple error checker.
|
||||
func check(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
98
internal/agd/device.go
Normal file
98
internal/agd/device.go
Normal file
@ -0,0 +1,98 @@
|
||||
package agd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
)
|
||||
|
||||
// Devices
|
||||
|
||||
// Device is a device of a device attached to a profile.
|
||||
type Device struct {
|
||||
// ID is the unique ID of the device.
|
||||
ID DeviceID
|
||||
|
||||
// LinkedIP, when non-nil, allows AdGuard DNS to identify a device by its IP
|
||||
// address when it can only use plain DNS.
|
||||
LinkedIP *netip.Addr
|
||||
|
||||
// Name is the human-readable name of the device.
|
||||
Name DeviceName
|
||||
|
||||
// FilteringEnabled defines whether queries from the device should be
|
||||
// filtered in any way at all.
|
||||
FilteringEnabled bool
|
||||
}
|
||||
|
||||
// DeviceID is the ID of a device attached to a profile. It is an opaque
|
||||
// string.
|
||||
type DeviceID string
|
||||
|
||||
// The maximum and minimum lengths of a device ID.
|
||||
const (
|
||||
MaxDeviceIDLen = 8
|
||||
MinDeviceIDLen = 1
|
||||
)
|
||||
|
||||
// NewDeviceID converts a simple string into a DeviceID and makes sure that it's
|
||||
// valid. This should be preferred to a simple type conversion.
|
||||
func NewDeviceID(s string) (id DeviceID, err error) {
|
||||
// Do not use errors.Annotate here, because it allocates even when the error
|
||||
// is nil.
|
||||
//
|
||||
// TODO(a.garipov): Find out, why does it allocate and perhaps file an
|
||||
// issue about that in the Go issue tracker.
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("bad device id %q: %w", s, err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = ValidateInclusion(len(s), MaxDeviceIDLen, MinDeviceIDLen, UnitByte)
|
||||
if err != nil {
|
||||
// The error will be wrapped by the deferred helper.
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = netutil.ValidateDomainNameLabel(s)
|
||||
if err != nil {
|
||||
// Unwrap the error to replace the domain name label wrapper message
|
||||
// with our own.
|
||||
return "", errors.Unwrap(err)
|
||||
}
|
||||
|
||||
return DeviceID(s), nil
|
||||
}
|
||||
|
||||
// DeviceName is the human-readable name of a device attached to a profile.
|
||||
type DeviceName string
|
||||
|
||||
// MaxDeviceNameRuneLen is the maximum length of a human-readable device name in
|
||||
// runes.
|
||||
const MaxDeviceNameRuneLen = 128
|
||||
|
||||
// NewDeviceName converts a simple string into a DeviceName and makes sure that
|
||||
// it's valid. This should be preferred to a simple type conversion.
|
||||
func NewDeviceName(s string) (n DeviceName, err error) {
|
||||
// Do not use errors.Annotate here, because it allocates even when the error
|
||||
// is nil.
|
||||
//
|
||||
// TODO(a.garipov): Same as the TODO in NewDeviceID.
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("bad device name %q: %w", s, err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = ValidateInclusion(utf8.RuneCountInString(s), MaxDeviceNameRuneLen, 0, UnitRune)
|
||||
if err != nil {
|
||||
// The error will be wrapped by the deferred helper.
|
||||
return "", err
|
||||
}
|
||||
|
||||
return DeviceName(s), nil
|
||||
}
|
58
internal/agd/device_test.go
Normal file
58
internal/agd/device_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
package agd_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewDeviceName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tooLong := strings.Repeat("a", 200)
|
||||
tooLongUnicode := strings.Repeat("ы", 200)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
in string
|
||||
wantErrMsg string
|
||||
}{{
|
||||
name: "empty",
|
||||
in: "",
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "normal",
|
||||
in: "Normal name",
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "normal_unicode",
|
||||
in: "Нормальное имя",
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "too_long",
|
||||
in: tooLong,
|
||||
wantErrMsg: `bad device name "` + tooLong + `": too long: got 200 runes, max 128`,
|
||||
}, {
|
||||
name: "too_long_unicode",
|
||||
in: tooLongUnicode,
|
||||
wantErrMsg: `bad device name "` + tooLongUnicode + `": too long: got 200 runes, max 128`,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
n, err := agd.NewDeviceName(tc.in)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
if tc.wantErrMsg == "" && tc.in != "" {
|
||||
assert.NotEmpty(t, n)
|
||||
} else {
|
||||
assert.Empty(t, n)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
38
internal/agd/dns.go
Normal file
38
internal/agd/dns.go
Normal file
@ -0,0 +1,38 @@
|
||||
package agd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
|
||||
)
|
||||
|
||||
// Common DNS Message Constants, Types, And Utilities
|
||||
|
||||
// Protocol is a DNS protocol. It is reexported here to lower the degree of
|
||||
// dependency on the dnsserver module.
|
||||
type Protocol = dnsserver.Protocol
|
||||
|
||||
// Protocol value constants. They are reexported here to lower the degree of
|
||||
// dependency on the dnsserver module.
|
||||
const (
|
||||
// NOTE: DO NOT change the numerical values or use iota, because other
|
||||
// packages and modules may depend on the numerical values. These numerical
|
||||
// values are a part of the API.
|
||||
|
||||
ProtoInvalid = dnsserver.ProtoInvalid
|
||||
ProtoDNSTCP = dnsserver.ProtoDNSTCP
|
||||
ProtoDNSUDP = dnsserver.ProtoDNSUDP
|
||||
ProtoDoH = dnsserver.ProtoDoH
|
||||
ProtoDoQ = dnsserver.ProtoDoQ
|
||||
ProtoDoT = dnsserver.ProtoDoT
|
||||
ProtoDNSCryptTCP = dnsserver.ProtoDNSCryptTCP
|
||||
ProtoDNSCryptUDP = dnsserver.ProtoDNSCryptUDP
|
||||
)
|
||||
|
||||
// Resolver is the DNS resolver interface.
|
||||
//
|
||||
// See go doc net.Resolver.
|
||||
type Resolver interface {
|
||||
LookupIP(ctx context.Context, network, host string) (ips []net.IP, err error)
|
||||
}
|
97
internal/agd/error.go
Normal file
97
internal/agd/error.go
Normal file
@ -0,0 +1,97 @@
|
||||
package agd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Common Errors
|
||||
|
||||
// ArgumentError is returned by functions when a value of an argument is
|
||||
// invalid.
|
||||
type ArgumentError struct {
|
||||
// Name is the name of the argument.
|
||||
Name string
|
||||
|
||||
// Message is an optional additional message.
|
||||
Message string
|
||||
}
|
||||
|
||||
// Error implements the error interface for *ArgumentError.
|
||||
func (err *ArgumentError) Error() (msg string) {
|
||||
if err.Message == "" {
|
||||
return fmt.Sprintf("argument %s is invalid", err.Name)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("argument %s is invalid: %s", err.Name, err.Message)
|
||||
}
|
||||
|
||||
// EntityName is the type for names of entities. Currently only used in errors.
|
||||
type EntityName string
|
||||
|
||||
// Current entity names.
|
||||
const (
|
||||
EntityNameDevice EntityName = "device"
|
||||
EntityNameProfile EntityName = "profile"
|
||||
)
|
||||
|
||||
// NotFoundError is an error returned by lookup methods when an entity wasn't
|
||||
// found.
|
||||
//
|
||||
// We use separate types that implement a common interface instead of a single
|
||||
// structure to reduce allocations.
|
||||
type NotFoundError interface {
|
||||
error
|
||||
|
||||
// EntityName returns the name of the entity that couldn't be found.
|
||||
EntityName() (e EntityName)
|
||||
}
|
||||
|
||||
// DeviceNotFoundError is a NotFoundError returned by lookup methods when
|
||||
// a device wasn't found.
|
||||
type DeviceNotFoundError struct{}
|
||||
|
||||
// type check
|
||||
var _ NotFoundError = DeviceNotFoundError{}
|
||||
|
||||
// Error implements the NotFoundError interface for DeviceNotFoundError.
|
||||
func (DeviceNotFoundError) Error() (msg string) { return "device not found" }
|
||||
|
||||
// EntityName implements the NotFoundError interface for DeviceNotFoundError.
|
||||
func (DeviceNotFoundError) EntityName() (e EntityName) { return EntityNameDevice }
|
||||
|
||||
// ProfileNotFoundError is a NotFoundError returned by lookup methods when
|
||||
// a profile wasn't found.
|
||||
type ProfileNotFoundError struct{}
|
||||
|
||||
// type check
|
||||
var _ NotFoundError = ProfileNotFoundError{}
|
||||
|
||||
// Error implements the NotFoundError interface for ProfileNotFoundError.
|
||||
func (ProfileNotFoundError) Error() (msg string) { return "profile not found" }
|
||||
|
||||
// EntityName implements the NotFoundError interface for ProfileNotFoundError.
|
||||
func (ProfileNotFoundError) EntityName() (e EntityName) { return EntityNameProfile }
|
||||
|
||||
// NotACountryError is returned from NewCountry when the string doesn't represent
|
||||
// a valid country.
|
||||
type NotACountryError struct {
|
||||
// Code is the code presented to NewCountry.
|
||||
Code string
|
||||
}
|
||||
|
||||
// Error implements the error interface for *NotACountryError.
|
||||
func (err *NotACountryError) Error() (msg string) {
|
||||
return fmt.Sprintf("%q is not a valid iso 3166-1 alpha-2 code", err.Code)
|
||||
}
|
||||
|
||||
// NotAContinentError is returned from NewContinent when the string doesn't
|
||||
// represent a valid continent.
|
||||
type NotAContinentError struct {
|
||||
// Code is the code presented to NewContinent.
|
||||
Code string
|
||||
}
|
||||
|
||||
// Error implements the error interface for *NotAContinentError.
|
||||
func (err *NotAContinentError) Error() (msg string) {
|
||||
return fmt.Sprintf("%q is not a valid continent code", err.Code)
|
||||
}
|
24
internal/agd/errorcollector.go
Normal file
24
internal/agd/errorcollector.go
Normal file
@ -0,0 +1,24 @@
|
||||
package agd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// Error Collector
|
||||
|
||||
// ErrorCollector collects information about errors, possibly sending them to
|
||||
// a remote location.
|
||||
type ErrorCollector interface {
|
||||
Collect(ctx context.Context, err error)
|
||||
}
|
||||
|
||||
// Collectf is a helper method for reporting non-critical errors. It writes the
|
||||
// resulting error into the log and also into the error collector.
|
||||
func Collectf(ctx context.Context, errColl ErrorCollector, format string, args ...any) {
|
||||
err := fmt.Errorf(format, args...)
|
||||
log.Error("%s", err)
|
||||
errColl.Collect(ctx, err)
|
||||
}
|
152
internal/agd/filterlist.go
Normal file
152
internal/agd/filterlist.go
Normal file
@ -0,0 +1,152 @@
|
||||
package agd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
)
|
||||
|
||||
// Filter Lists
|
||||
|
||||
// FilterList is a list of filter rules.
|
||||
type FilterList struct {
|
||||
// URL is the URL used to refresh the filter.
|
||||
URL *url.URL
|
||||
|
||||
// ID is the unique ID of this filter. It will also be used to create the
|
||||
// cache file.
|
||||
ID FilterListID
|
||||
|
||||
// RefreshIvl is the interval that defines how often a filter should be
|
||||
// refreshed. It is also used to check if the cached file is fresh enough.
|
||||
RefreshIvl time.Duration
|
||||
}
|
||||
|
||||
// FilterListID is the ID of a filter list. It is an opaque string.
|
||||
type FilterListID string
|
||||
|
||||
// Special FilterListID values shared across the AdGuard DNS system.
|
||||
//
|
||||
// DO NOT change these as other parts of the system depend on these values.
|
||||
const (
|
||||
// FilterListIDNone means that no filter were applied at all.
|
||||
FilterListIDNone FilterListID = ""
|
||||
|
||||
// FilterListIDBlockedService is the shared filter list ID used when a
|
||||
// request was blocked by the service blocker.
|
||||
FilterListIDBlockedService FilterListID = "blocked_service"
|
||||
|
||||
// FilterListIDCustom is the special shared filter list ID used when
|
||||
// a request was filtered by a custom profile rule.
|
||||
FilterListIDCustom FilterListID = "custom"
|
||||
|
||||
// FilterListIDAdultBlocking is the special shared filter list ID used when
|
||||
// a request was filtered by the adult content blocking filter.
|
||||
FilterListIDAdultBlocking FilterListID = "adult_blocking"
|
||||
|
||||
// FilterListIDSafeBrowsing is the special shared filter list ID used when
|
||||
// a request was filtered by the safe browsing filter.
|
||||
FilterListIDSafeBrowsing FilterListID = "safe_browsing"
|
||||
|
||||
// FilterListIDGeneralSafeSearch is the shared filter list ID used when
|
||||
// a request was modified by the general safe search filter.
|
||||
FilterListIDGeneralSafeSearch FilterListID = "general_safe_search"
|
||||
|
||||
// FilterListIDYoutubeSafeSearch is the special shared filter list ID used
|
||||
// when a request was modified by the YouTube safe search filter.
|
||||
FilterListIDYoutubeSafeSearch FilterListID = "youtube_safe_search"
|
||||
)
|
||||
|
||||
// The maximum and minimum lengths of a filter list ID.
|
||||
const (
|
||||
MaxFilterListIDLen = 128
|
||||
MinFilterListIDLen = 1
|
||||
)
|
||||
|
||||
// NewFilterListID converts a simple string into a FilterListID and makes sure
|
||||
// that it's valid. This should be preferred to a simple type conversion.
|
||||
func NewFilterListID(s string) (id FilterListID, err error) {
|
||||
defer func() { err = errors.Annotate(err, "bad filter list id %q: %w", s) }()
|
||||
|
||||
err = ValidateInclusion(len(s), MaxFilterListIDLen, MinFilterListIDLen, UnitByte)
|
||||
if err != nil {
|
||||
return FilterListIDNone, err
|
||||
}
|
||||
|
||||
// Allow only the printable, non-whitespace ASCII characters. Technically
|
||||
// we only need to exclude carriage return, line feed, and slash characters,
|
||||
// but let's be more strict just in case.
|
||||
if i, r := firstNonIDRune(s, true); i != -1 {
|
||||
return FilterListIDNone, fmt.Errorf("bad rune %q at index %d", r, i)
|
||||
}
|
||||
|
||||
return FilterListID(s), nil
|
||||
}
|
||||
|
||||
// FilterRuleText is the text of a single rule within a filter.
|
||||
type FilterRuleText string
|
||||
|
||||
// MaxFilterRuleTextRuneLen is the maximum length of a filter rule in runes.
|
||||
const MaxFilterRuleTextRuneLen = 1024
|
||||
|
||||
// NewFilterRuleText converts a simple string into a FilterRuleText and makes
|
||||
// sure that it's valid. This should be preferred to a simple type conversion.
|
||||
func NewFilterRuleText(s string) (t FilterRuleText, err error) {
|
||||
defer func() { err = errors.Annotate(err, "bad filter rule text %q: %w", s) }()
|
||||
|
||||
err = ValidateInclusion(utf8.RuneCountInString(s), MaxFilterRuleTextRuneLen, 0, UnitRune)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return FilterRuleText(s), nil
|
||||
}
|
||||
|
||||
// FilteringGroup represents a set of filtering settings.
|
||||
//
|
||||
// TODO(a.garipov): Consider making it closer to the config file and the backend
|
||||
// response by grouping parental, rule list, and safe browsing settings into
|
||||
// separate structs.
|
||||
type FilteringGroup struct {
|
||||
// ID is the unique ID of this filtering group.
|
||||
ID FilteringGroupID
|
||||
|
||||
// RuleListIDs are the filtering rule list IDs used for this filtering
|
||||
// group. They are ignored if RuleListsEnabled is false.
|
||||
RuleListIDs []FilterListID
|
||||
|
||||
// RuleListsEnabled shows whether the rule-list based filtering is enabled.
|
||||
// This must be true in order for all parameters below to work.
|
||||
RuleListsEnabled bool
|
||||
|
||||
// ParentalEnabled shows whether the parental protection functionality is
|
||||
// enabled. This must be true in order for all parameters below to
|
||||
// work.
|
||||
ParentalEnabled bool
|
||||
|
||||
// BlockAdult shows whether the adult content blocking safe browsing
|
||||
// filtering should be enforced.
|
||||
BlockAdult bool
|
||||
|
||||
// SafeBrowsingEnabled shows whether the general safe browsing filtering
|
||||
// should be enforced.
|
||||
SafeBrowsingEnabled bool
|
||||
|
||||
// GeneralSafeSearch shows whether the general safe search filtering should
|
||||
// be enforced.
|
||||
GeneralSafeSearch bool
|
||||
|
||||
// YoutubeSafeSearch shows whether the YouTube safe search filtering should
|
||||
// be enforced.
|
||||
YoutubeSafeSearch bool
|
||||
|
||||
// BlockPrivateRelay shows if Apple Private Relay is blocked for requests
|
||||
// using this filtering group.
|
||||
BlockPrivateRelay bool
|
||||
}
|
||||
|
||||
// FilteringGroupID is the ID of a filter group. It is an opaque string.
|
||||
type FilteringGroupID string
|
102
internal/agd/filterlist_test.go
Normal file
102
internal/agd/filterlist_test.go
Normal file
@ -0,0 +1,102 @@
|
||||
package agd_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewFilterListID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tooLong := strings.Repeat("a", agd.MaxFilterListIDLen+1)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
in string
|
||||
wantErrMsg string
|
||||
}{{
|
||||
name: "normal",
|
||||
in: "adguard_default_list",
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "too_short",
|
||||
in: "",
|
||||
wantErrMsg: `bad filter list id "": too short: got 0 bytes, min 1`,
|
||||
}, {
|
||||
name: "too_long",
|
||||
in: tooLong,
|
||||
wantErrMsg: `bad filter list id "` + tooLong + `": too long: got 129 bytes, max 128`,
|
||||
}, {
|
||||
name: "bad",
|
||||
in: "bad/name",
|
||||
wantErrMsg: `bad filter list id "bad/name": bad rune '/' at index 3`,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
id, err := agd.NewFilterListID(tc.in)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
if tc.wantErrMsg == "" && tc.in != "" {
|
||||
assert.NotEmpty(t, id)
|
||||
} else {
|
||||
assert.Empty(t, id)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFilterRuleText(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tooLong := strings.Repeat("a", agd.MaxFilterRuleTextRuneLen+1)
|
||||
tooLongUnicode := strings.Repeat("ы", agd.MaxFilterRuleTextRuneLen+1)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
in string
|
||||
wantErrMsg string
|
||||
}{{
|
||||
name: "normal",
|
||||
in: "||example.com^",
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "normal_unicode",
|
||||
in: "||пример.рф",
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "empty",
|
||||
in: "",
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "too_long",
|
||||
in: tooLong,
|
||||
wantErrMsg: `bad filter rule text "` + tooLong + `": too long: got 1025 runes, max 1024`,
|
||||
}, {
|
||||
name: "too_long_unicode",
|
||||
in: tooLongUnicode,
|
||||
wantErrMsg: `bad filter rule text "` + tooLongUnicode + `": too long: ` +
|
||||
`got 1025 runes, max 1024`,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
txt, err := agd.NewFilterRuleText(tc.in)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
if tc.wantErrMsg == "" && tc.in != "" {
|
||||
assert.NotEmpty(t, txt)
|
||||
} else {
|
||||
assert.Empty(t, txt)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
57
internal/agd/location.go
Normal file
57
internal/agd/location.go
Normal file
@ -0,0 +1,57 @@
|
||||
package agd
|
||||
|
||||
// Location Types And Constants
|
||||
|
||||
// Location represents the GeoIP location data about an IP address.
|
||||
type Location struct {
|
||||
Country Country
|
||||
Continent Continent
|
||||
ASN ASN
|
||||
}
|
||||
|
||||
// ASN is the autonomous system number of an IP address.
|
||||
//
|
||||
// See also https://datatracker.ietf.org/doc/html/rfc7300.
|
||||
type ASN uint32
|
||||
|
||||
// Continent represents a continent code used by MaxMind.
|
||||
type Continent string
|
||||
|
||||
// Continent code constants.
|
||||
const (
|
||||
// ContinentNone is an unknown continent code.
|
||||
ContinentNone Continent = ""
|
||||
// ContinentAF is Africa.
|
||||
ContinentAF Continent = "AF"
|
||||
// ContinentAN is Antarctica.
|
||||
ContinentAN Continent = "AN"
|
||||
// ContinentAS is Asia.
|
||||
ContinentAS Continent = "AS"
|
||||
// ContinentEU is Europe.
|
||||
ContinentEU Continent = "EU"
|
||||
// ContinentNA is North America.
|
||||
ContinentNA Continent = "NA"
|
||||
// ContinentOC is Oceania.
|
||||
ContinentOC Continent = "OC"
|
||||
// ContinentSA is South America.
|
||||
ContinentSA Continent = "SA"
|
||||
)
|
||||
|
||||
// NewContinent converts s into a Continent while also validating it. Prefer to
|
||||
// use this instead of a plain conversion.
|
||||
func NewContinent(s string) (c Continent, err error) {
|
||||
switch c = Continent(s); c {
|
||||
case
|
||||
ContinentAF,
|
||||
ContinentAN,
|
||||
ContinentAS,
|
||||
ContinentEU,
|
||||
ContinentNA,
|
||||
ContinentOC,
|
||||
ContinentSA,
|
||||
ContinentNone:
|
||||
return c, nil
|
||||
default:
|
||||
return ContinentNone, &NotAContinentError{Code: s}
|
||||
}
|
||||
}
|
18
internal/agd/os.go
Normal file
18
internal/agd/os.go
Normal file
@ -0,0 +1,18 @@
|
||||
package agd
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
)
|
||||
|
||||
// OS-Related Constants
|
||||
|
||||
// DefaultWOFlags is the default set of flags for opening a write-only files.
|
||||
const DefaultWOFlags = os.O_APPEND | os.O_CREATE | os.O_WRONLY
|
||||
|
||||
// DefaultPerm is the default set of permissions for non-executable files. Be
|
||||
// strict and allow only reading and writing for the file, and only to the user.
|
||||
const DefaultPerm fs.FileMode = 0o600
|
||||
|
||||
// DefaultDirPerm is the default set of permissions for directories.
|
||||
const DefaultDirPerm fs.FileMode = 0o700
|
234
internal/agd/profile.go
Normal file
234
internal/agd/profile.go
Normal file
@ -0,0 +1,234 @@
|
||||
package agd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
)
|
||||
|
||||
// Profiles
|
||||
|
||||
// Profile contains information about an AdGuard DNS profile. In other parts of
|
||||
// the infrastructure, a profile is also called a “DNS server”. We call it
|
||||
// profile, because it's less confusing.
|
||||
//
|
||||
// TODO(a.garipov): Consider making it closer to the config file and the backend
|
||||
// response by grouping parental, rule list, and safe browsing settings into
|
||||
// separate structs.
|
||||
type Profile struct {
|
||||
// Parental are the parental settings for this profile. They are ignored if
|
||||
// FilteringEnabled is set to false.
|
||||
Parental *ParentalProtectionSettings
|
||||
|
||||
// ID is the unique ID of this profile.
|
||||
ID ProfileID
|
||||
|
||||
// UpdateTime shows the last time this profile was updated from the backend.
|
||||
// This is NOT the time of update in the backend's database, since the
|
||||
// backend doesn't send this information.
|
||||
UpdateTime time.Time
|
||||
|
||||
// Devices are the devices attached to this profile. Every element of the
|
||||
// slice must be non-nil.
|
||||
Devices []*Device
|
||||
|
||||
// RuleListIDs are the IDs of the filtering rule lists enabled for this
|
||||
// profile. They are ignored if FilteringEnabled or RuleListsEnabled are
|
||||
// set to false.
|
||||
RuleListIDs []FilterListID
|
||||
|
||||
// CustomRules are the custom filtering rules for this profile. They are
|
||||
// ignored if RuleListsEnabled is set to false.
|
||||
CustomRules []FilterRuleText
|
||||
|
||||
// FilteredResponseTTL is the time-to-live value used for responses sent to
|
||||
// the devices of this profile.
|
||||
FilteredResponseTTL time.Duration
|
||||
|
||||
// FilteringEnabled defines whether queries from devices of this profile
|
||||
// should be filtered in any way at all.
|
||||
FilteringEnabled bool
|
||||
|
||||
// SafeBrowsingEnabled defines whether queries from devices of this profile
|
||||
// should be filtered using the safe browsing filter. Requires
|
||||
// FilteringEnabled to be set to true.
|
||||
SafeBrowsingEnabled bool
|
||||
|
||||
// RuleListsEnabled defines whether queries from devices of this profile
|
||||
// should be filtered using the filtering rule lists in RuleListIDs.
|
||||
// Requires FilteringEnabled to be set to true.
|
||||
RuleListsEnabled bool
|
||||
|
||||
// QueryLogEnabled defines whether query logs should be saved for the
|
||||
// devices of this profile.
|
||||
QueryLogEnabled bool
|
||||
|
||||
// Deleted shows if this profile is deleted.
|
||||
Deleted bool
|
||||
|
||||
// BlockPrivateRelay shows if Apple Private Relay queries are blocked for
|
||||
// requests from all devices in this profile.
|
||||
BlockPrivateRelay bool
|
||||
}
|
||||
|
||||
// ProfileID is the ID of a profile. It is an opaque string.
|
||||
//
|
||||
// In other parts of the infrastructure, it's also known as “DNS ID” and “DNS
|
||||
// Server ID”.
|
||||
type ProfileID string
|
||||
|
||||
// MaxProfileIDLen is the maximum length of a profile ID.
|
||||
const MaxProfileIDLen = 8
|
||||
|
||||
// NewProfileID converts a simple string into a ProfileID and makes sure that
|
||||
// it's valid. This should be preferred to a simple type conversion.
|
||||
func NewProfileID(s string) (id ProfileID, err error) {
|
||||
if err = ValidateInclusion(len(s), MaxProfileIDLen, 0, UnitByte); err != nil {
|
||||
return "", fmt.Errorf("bad profile id %q: %w", s, err)
|
||||
}
|
||||
|
||||
// For now, allow only the printable, non-whitespace ASCII characters.
|
||||
// Technically we only need to exclude carriage return and line feed
|
||||
// characters, but let's be more strict just in case.
|
||||
if i, r := firstNonIDRune(s, false); i != -1 {
|
||||
return "", fmt.Errorf("bad profile id: bad char %q at index %d", r, i)
|
||||
}
|
||||
|
||||
return ProfileID(s), nil
|
||||
}
|
||||
|
||||
// DayRange is a range within a single day. Start and End are minutes from the
|
||||
// start of day, with 0 being 00:00:00.(0) and 1439, 23:59:59.(9).
|
||||
//
|
||||
// Additionally, if both Start and End are set to math.MaxUint16, the range is
|
||||
// a special zero-length range. This is done to reduce the amount of pointers
|
||||
// and thus GC time.
|
||||
type DayRange struct {
|
||||
Start uint16
|
||||
End uint16
|
||||
}
|
||||
|
||||
// MaxDayRangeMinutes is the maximum value for DayRange.Start and DayRange.End
|
||||
// fields, excluding the zero-length range ones.
|
||||
const MaxDayRangeMinutes = 24*60 - 1
|
||||
|
||||
// ZeroLengthDayRange returns a new zero-length day range.
|
||||
func ZeroLengthDayRange() (r DayRange) {
|
||||
return DayRange{
|
||||
Start: math.MaxUint16,
|
||||
End: math.MaxUint16,
|
||||
}
|
||||
}
|
||||
|
||||
// IsZeroLength returns true if r is a zero-length range.
|
||||
func (r DayRange) IsZeroLength() (ok bool) {
|
||||
return r.Start == math.MaxUint16 && r.End == math.MaxUint16
|
||||
}
|
||||
|
||||
// Validate returns the day range validation errors, if any.
|
||||
func (r DayRange) Validate() (err error) {
|
||||
defer func() { err = errors.Annotate(err, "bad day range: %w") }()
|
||||
|
||||
switch {
|
||||
case r.IsZeroLength():
|
||||
return nil
|
||||
case r.End < r.Start:
|
||||
return fmt.Errorf("end %d less than start %d", r.End, r.Start)
|
||||
case r.Start > MaxDayRangeMinutes:
|
||||
return fmt.Errorf("start %d greater than %d", r.Start, MaxDayRangeMinutes)
|
||||
case r.End > MaxDayRangeMinutes:
|
||||
return fmt.Errorf("end %d greater than %d", r.End, MaxDayRangeMinutes)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WeeklySchedule is a schedule for one week. The index is the same as
|
||||
// time.Weekday values. That is, 0 is Sunday, 1 is Monday, etc. An empty
|
||||
// DayRange means that there is no schedule for this day.
|
||||
type WeeklySchedule [7]DayRange
|
||||
|
||||
// ParentalProtectionSchedule is the schedule of a client's parental protection.
|
||||
// All fields must not be nil.
|
||||
type ParentalProtectionSchedule struct {
|
||||
// Week is the parental protection schedule for every day of the week.
|
||||
Week *WeeklySchedule
|
||||
|
||||
// TimeZone is the profile's time zone.
|
||||
TimeZone *time.Location
|
||||
}
|
||||
|
||||
// Contains returns true if t is within the allowed schedule.
|
||||
func (s *ParentalProtectionSchedule) Contains(t time.Time) (ok bool) {
|
||||
t = t.In(s.TimeZone)
|
||||
r := s.Week[int(t.Weekday())]
|
||||
if r.IsZeroLength() {
|
||||
return false
|
||||
}
|
||||
|
||||
day := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, s.TimeZone)
|
||||
start := day.Add(time.Duration(r.Start) * time.Minute)
|
||||
end := day.Add(time.Duration(r.End+1)*time.Minute - 1*time.Nanosecond)
|
||||
|
||||
return !t.Before(start) && !t.After(end)
|
||||
}
|
||||
|
||||
// ParentalProtectionSettings are the parental protection settings of a profile.
|
||||
type ParentalProtectionSettings struct {
|
||||
Schedule *ParentalProtectionSchedule
|
||||
|
||||
// BlockedServices are the IDs of the services blocked for this profile.
|
||||
BlockedServices []BlockedServiceID
|
||||
|
||||
// Enabled tells whether the parental protection should be enabled at all.
|
||||
// This must be true in order for all parameters below to work.
|
||||
Enabled bool
|
||||
|
||||
// BlockAdult tells if AdGuard DNS should enforce blocking of adult content
|
||||
// using the safe browsing filter.
|
||||
BlockAdult bool
|
||||
|
||||
// GeneralSafeSearch tells if AdGuard DNS should enforce general safe search
|
||||
// in most search engines.
|
||||
GeneralSafeSearch bool
|
||||
|
||||
// YoutubeSafeSearch tells if AdGuard DNS should enforce safe search on
|
||||
// YouTube.
|
||||
YoutubeSafeSearch bool
|
||||
}
|
||||
|
||||
// BlockedServiceID is the ID of a blocked service. While these are usually
|
||||
// human-readable, clients should treat them as opaque strings.
|
||||
//
|
||||
// When a request is blocked by the service blocker, this ID is used as the
|
||||
// text of the blocking rule.
|
||||
type BlockedServiceID string
|
||||
|
||||
// The maximum and minimum lengths of a blocked service ID.
|
||||
const (
|
||||
MaxBlockedServiceIDLen = 64
|
||||
MinBlockedServiceIDLen = 1
|
||||
)
|
||||
|
||||
// NewBlockedServiceID converts a simple string into a BlockedServiceID and
|
||||
// makes sure that it's valid. This should be preferred to a simple type
|
||||
// conversion.
|
||||
func NewBlockedServiceID(s string) (id BlockedServiceID, err error) {
|
||||
defer func() { err = errors.Annotate(err, "bad blocked service id %q: %w", s) }()
|
||||
|
||||
err = ValidateInclusion(len(s), MaxBlockedServiceIDLen, MinBlockedServiceIDLen, UnitByte)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Allow only the printable, non-whitespace ASCII characters. Technically
|
||||
// we only need to exclude carriage return, line feed, and slash characters,
|
||||
// but let's be more strict just in case.
|
||||
if i, r := firstNonIDRune(s, true); i != -1 {
|
||||
return "", fmt.Errorf("bad char %q at index %d", r, i)
|
||||
}
|
||||
|
||||
return BlockedServiceID(s), nil
|
||||
}
|
153
internal/agd/profile_test.go
Normal file
153
internal/agd/profile_test.go
Normal file
@ -0,0 +1,153 @@
|
||||
package agd_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDayRange_Validate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
wantErrMsg string
|
||||
rng agd.DayRange
|
||||
}{{
|
||||
name: "ok",
|
||||
wantErrMsg: "",
|
||||
rng: agd.DayRange{Start: 11 * 60, End: 13*60 - 1},
|
||||
}, {
|
||||
name: "ok_zeroes",
|
||||
wantErrMsg: "",
|
||||
rng: agd.DayRange{Start: 0, End: 0},
|
||||
}, {
|
||||
name: "ok_max",
|
||||
wantErrMsg: "",
|
||||
rng: agd.DayRange{
|
||||
Start: agd.MaxDayRangeMinutes,
|
||||
End: agd.MaxDayRangeMinutes,
|
||||
},
|
||||
}, {
|
||||
name: "ok_zero_length",
|
||||
wantErrMsg: "",
|
||||
rng: agd.ZeroLengthDayRange(),
|
||||
}, {
|
||||
name: "err_before",
|
||||
wantErrMsg: "bad day range: end 0 less than start 1",
|
||||
rng: agd.DayRange{Start: 1, End: 0},
|
||||
}, {
|
||||
name: "err_bad_start",
|
||||
wantErrMsg: "bad day range: start 10000 greater than 1439",
|
||||
rng: agd.DayRange{Start: 10_000, End: 10_000},
|
||||
}, {
|
||||
name: "err_bad_end",
|
||||
wantErrMsg: "bad day range: end 10000 greater than 1439",
|
||||
rng: agd.DayRange{Start: 0, End: 10_000},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.rng.Validate()
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParentalProtectionSchedule_Contains(t *testing.T) {
|
||||
baseTime := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
otherTime := baseTime.Add(1 * timeutil.Day)
|
||||
|
||||
// NOTE: In the Etc area the sign of the offsets is flipped. So, Etc/GMT-3
|
||||
// is actually UTC+03:00.
|
||||
otherTZ := time.FixedZone("Etc/GMT-3", 3*60*60)
|
||||
|
||||
// baseSchedule, 12:00:00 to 13:59:59.
|
||||
baseSchedule := &agd.ParentalProtectionSchedule{
|
||||
Week: &agd.WeeklySchedule{
|
||||
time.Sunday: agd.ZeroLengthDayRange(),
|
||||
time.Monday: agd.ZeroLengthDayRange(),
|
||||
time.Tuesday: agd.ZeroLengthDayRange(),
|
||||
time.Wednesday: agd.ZeroLengthDayRange(),
|
||||
time.Thursday: agd.ZeroLengthDayRange(),
|
||||
|
||||
// baseTime is on Friday.
|
||||
time.Friday: agd.DayRange{12 * 60, 14*60 - 1},
|
||||
|
||||
time.Saturday: agd.ZeroLengthDayRange(),
|
||||
},
|
||||
TimeZone: time.UTC,
|
||||
}
|
||||
|
||||
// allDaySchedule, 00:00:00 to 23:59:59.
|
||||
allDaySchedule := &agd.ParentalProtectionSchedule{
|
||||
Week: &agd.WeeklySchedule{
|
||||
time.Sunday: agd.ZeroLengthDayRange(),
|
||||
time.Monday: agd.ZeroLengthDayRange(),
|
||||
time.Tuesday: agd.ZeroLengthDayRange(),
|
||||
time.Wednesday: agd.ZeroLengthDayRange(),
|
||||
time.Thursday: agd.ZeroLengthDayRange(),
|
||||
|
||||
// baseTime is on Friday.
|
||||
time.Friday: agd.DayRange{0, 24*60 - 1},
|
||||
|
||||
time.Saturday: agd.ZeroLengthDayRange(),
|
||||
},
|
||||
TimeZone: time.UTC,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
schedule *agd.ParentalProtectionSchedule
|
||||
assert assert.BoolAssertionFunc
|
||||
t time.Time
|
||||
name string
|
||||
}{{
|
||||
schedule: allDaySchedule,
|
||||
assert: assert.True,
|
||||
t: baseTime,
|
||||
name: "same_day_all_day",
|
||||
}, {
|
||||
schedule: baseSchedule,
|
||||
assert: assert.True,
|
||||
t: baseTime.Add(13 * time.Hour),
|
||||
name: "same_day_inside",
|
||||
}, {
|
||||
schedule: baseSchedule,
|
||||
assert: assert.False,
|
||||
t: baseTime.Add(11 * time.Hour),
|
||||
name: "same_day_outside",
|
||||
}, {
|
||||
schedule: allDaySchedule,
|
||||
assert: assert.False,
|
||||
t: otherTime,
|
||||
name: "other_day_all_day",
|
||||
}, {
|
||||
schedule: baseSchedule,
|
||||
assert: assert.False,
|
||||
t: otherTime.Add(13 * time.Hour),
|
||||
name: "other_day_inside",
|
||||
}, {
|
||||
schedule: baseSchedule,
|
||||
assert: assert.False,
|
||||
t: otherTime.Add(11 * time.Hour),
|
||||
name: "other_day_outside",
|
||||
}, {
|
||||
schedule: baseSchedule,
|
||||
assert: assert.True,
|
||||
t: baseTime.Add(13 * time.Hour).In(otherTZ),
|
||||
name: "same_day_inside_other_tz",
|
||||
}, {
|
||||
schedule: baseSchedule,
|
||||
assert: assert.False,
|
||||
t: baseTime.Add(11 * time.Hour).In(otherTZ),
|
||||
name: "same_day_outside_other_tz",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tc.assert(t, tc.schedule.Contains(tc.t))
|
||||
})
|
||||
}
|
||||
}
|
303
internal/agd/profiledb.go
Normal file
303
internal/agd/profiledb.go
Normal file
@ -0,0 +1,303 @@
|
||||
package agd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// Data Storage
|
||||
|
||||
// ProfileDB is the local database of profiles and other data.
|
||||
type ProfileDB interface {
|
||||
ProfileByDeviceID(ctx context.Context, id DeviceID) (p *Profile, d *Device, err error)
|
||||
ProfileByIP(ctx context.Context, ip netip.Addr) (p *Profile, d *Device, err error)
|
||||
}
|
||||
|
||||
// DefaultProfileDB is the default implementation of the ProfileDB interface
|
||||
// that can refresh itself from the provided storage.
|
||||
type DefaultProfileDB struct {
|
||||
// mapsMu protects the deviceToProfile, deviceIDToIP, and ipToDeviceID maps.
|
||||
mapsMu *sync.RWMutex
|
||||
|
||||
// refreshMu protects syncTime and syncTimeFull. These are only used within
|
||||
// Refresh, so this is also basically a refresh serializer.
|
||||
refreshMu *sync.Mutex
|
||||
|
||||
// storage returns the data for this profiledb.
|
||||
storage ProfileStorage
|
||||
|
||||
// deviceToProfile maps device IDs to their profiles. It is cleared lazily
|
||||
// whenever a device is found to be missing from a profile.
|
||||
deviceToProfile map[DeviceID]*Profile
|
||||
|
||||
// deviceIDToIP maps device IDs to their linked IP addresses. It is used to
|
||||
// take changes in IP address linking into account during refreshes. It is
|
||||
// cleared lazily whenever a device is found to be missing from a profile.
|
||||
deviceIDToIP map[DeviceID]netip.Addr
|
||||
|
||||
// ipToDeviceID maps linked IP addresses to the IDs of their devices. It is
|
||||
// cleared lazily whenever a device is found to be missing from a profile.
|
||||
ipToDeviceID map[netip.Addr]DeviceID
|
||||
|
||||
// syncTime is the time of the last synchronization. It is used in refresh
|
||||
// requests to the storage.
|
||||
syncTime time.Time
|
||||
|
||||
// syncTimeFull is the time of the last full synchronization.
|
||||
syncTimeFull time.Time
|
||||
|
||||
// fullSyncIvl is the interval between two full synchronizations with the
|
||||
// storage
|
||||
fullSyncIvl time.Duration
|
||||
}
|
||||
|
||||
// NewDefaultProfileDB returns a new default profile profiledb. The initial
|
||||
// refresh is performed immediately with the constant timeout of 1 minute,
|
||||
// beyond which an empty profiledb is returned. db is never nil.
|
||||
func NewDefaultProfileDB(
|
||||
ds ProfileStorage,
|
||||
fullRefreshIvl time.Duration,
|
||||
) (db *DefaultProfileDB, err error) {
|
||||
db = &DefaultProfileDB{
|
||||
mapsMu: &sync.RWMutex{},
|
||||
refreshMu: &sync.Mutex{},
|
||||
storage: ds,
|
||||
syncTime: time.Time{},
|
||||
syncTimeFull: time.Time{},
|
||||
deviceToProfile: make(map[DeviceID]*Profile),
|
||||
deviceIDToIP: make(map[DeviceID]netip.Addr),
|
||||
ipToDeviceID: make(map[netip.Addr]DeviceID),
|
||||
fullSyncIvl: fullRefreshIvl,
|
||||
}
|
||||
|
||||
// initialTimeout defines the maximum duration of the first attempt to load
|
||||
// the profiledb.
|
||||
const initialTimeout = 1 * time.Minute
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), initialTimeout)
|
||||
defer cancel()
|
||||
|
||||
log.Info("profiledb: initial refresh")
|
||||
|
||||
err = db.Refresh(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
log.Info("profiledb: warning: initial refresh timeout: %s", err)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("profiledb: initial refresh: %w", err)
|
||||
}
|
||||
|
||||
log.Info("profiledb: initial refresh succeeded")
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ Refresher = (*DefaultProfileDB)(nil)
|
||||
|
||||
// Refresh implements the Refresher interface for *DefaultProfileDB. It updates
|
||||
// the internal maps from the data it receives from the storage.
|
||||
func (db *DefaultProfileDB) Refresh(ctx context.Context) (err error) {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
metrics.ProfilesSyncTime.SetToCurrentTime()
|
||||
metrics.ProfilesCountGauge.Set(float64(len(db.deviceToProfile)))
|
||||
metrics.ProfilesSyncDuration.Observe(time.Since(startTime).Seconds())
|
||||
metrics.SetStatusGauge(metrics.ProfilesSyncStatus, err)
|
||||
}()
|
||||
|
||||
reqID := NewRequestID()
|
||||
ctx = WithRequestID(ctx, reqID)
|
||||
|
||||
defer func() { err = errors.Annotate(err, "req %s: %w", reqID) }()
|
||||
|
||||
db.refreshMu.Lock()
|
||||
defer db.refreshMu.Unlock()
|
||||
|
||||
isFullSync := time.Since(db.syncTimeFull) >= db.fullSyncIvl
|
||||
syncTime := db.syncTime
|
||||
if isFullSync {
|
||||
syncTime = time.Time{}
|
||||
}
|
||||
|
||||
var resp *PSProfilesResponse
|
||||
resp, err = db.storage.Profiles(ctx, &PSProfilesRequest{
|
||||
SyncTime: syncTime,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("updating profiles: %w", err)
|
||||
}
|
||||
|
||||
profiles := resp.Profiles
|
||||
devNum := db.setProfiles(profiles)
|
||||
log.Debug("profiledb: req %s: got %d profiles with %d devices", reqID, len(profiles), devNum)
|
||||
metrics.ProfilesNewCountGauge.Set(float64(len(profiles)))
|
||||
|
||||
db.syncTime = resp.SyncTime
|
||||
if isFullSync {
|
||||
db.syncTimeFull = time.Now()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setProfiles adds or updates the data for all profiles.
|
||||
func (db *DefaultProfileDB) setProfiles(profiles []*Profile) (devNum int) {
|
||||
db.mapsMu.Lock()
|
||||
defer db.mapsMu.Unlock()
|
||||
|
||||
for _, p := range profiles {
|
||||
devNum += len(p.Devices)
|
||||
for _, d := range p.Devices {
|
||||
db.deviceToProfile[d.ID] = p
|
||||
if d.LinkedIP == nil {
|
||||
// Delete any records from the device-to-IP map just in case
|
||||
// there used to be one.
|
||||
delete(db.deviceIDToIP, d.ID)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
newIP := *d.LinkedIP
|
||||
if prevIP, ok := db.deviceIDToIP[d.ID]; !ok || prevIP != newIP {
|
||||
// The IP has changed. Remove the previous records before
|
||||
// setting the new ones.
|
||||
delete(db.ipToDeviceID, prevIP)
|
||||
delete(db.deviceIDToIP, d.ID)
|
||||
}
|
||||
|
||||
db.ipToDeviceID[newIP] = d.ID
|
||||
db.deviceIDToIP[d.ID] = newIP
|
||||
}
|
||||
}
|
||||
|
||||
return devNum
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ ProfileDB = (*DefaultProfileDB)(nil)
|
||||
|
||||
// ProfileByDeviceID implements the ProfileDB interface for *DefaultProfileDB.
|
||||
func (db *DefaultProfileDB) ProfileByDeviceID(
|
||||
ctx context.Context,
|
||||
id DeviceID,
|
||||
) (p *Profile, d *Device, err error) {
|
||||
db.mapsMu.RLock()
|
||||
defer db.mapsMu.RUnlock()
|
||||
|
||||
return db.profileByDeviceID(ctx, id)
|
||||
}
|
||||
|
||||
// profileByDeviceID returns the profile and the device by the ID of the device,
|
||||
// if found. Any returned errors will have the underlying type of
|
||||
// NotFoundError. It assumes that db is currently locked for reading.
|
||||
func (db *DefaultProfileDB) profileByDeviceID(
|
||||
_ context.Context,
|
||||
id DeviceID,
|
||||
) (p *Profile, d *Device, err error) {
|
||||
// Do not use errors.Annotate here, because it allocates even when the error
|
||||
// is nil. Also do not use fmt.Errorf in a defer, because it allocates when
|
||||
// a device is not found, which is the most common case.
|
||||
//
|
||||
// TODO(a.garipov): Find out, why does it allocate and perhaps file an
|
||||
// issue about that in the Go issue tracker.
|
||||
|
||||
var ok bool
|
||||
p, ok = db.deviceToProfile[id]
|
||||
if !ok {
|
||||
return nil, nil, ProfileNotFoundError{}
|
||||
}
|
||||
|
||||
for _, pd := range p.Devices {
|
||||
if pd.ID == id {
|
||||
d = pd
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if d == nil {
|
||||
// Perhaps, the device has been deleted. May happen when the device was
|
||||
// found by a linked IP.
|
||||
return nil, nil, fmt.Errorf("rechecking devices: %w", DeviceNotFoundError{})
|
||||
}
|
||||
|
||||
return p, d, nil
|
||||
}
|
||||
|
||||
// ProfileByIP implements the ProfileDB interface for *DefaultProfileDB. ip
|
||||
// must be valid.
|
||||
func (db *DefaultProfileDB) ProfileByIP(
|
||||
ctx context.Context,
|
||||
ip netip.Addr,
|
||||
) (p *Profile, d *Device, err error) {
|
||||
// Do not use errors.Annotate here, because it allocates even when the error
|
||||
// is nil. Also do not use fmt.Errorf in a defer, because it allocates when
|
||||
// a device is not found, which is the most common case.
|
||||
|
||||
db.mapsMu.RLock()
|
||||
defer db.mapsMu.RUnlock()
|
||||
|
||||
id, ok := db.ipToDeviceID[ip]
|
||||
if !ok {
|
||||
return nil, nil, DeviceNotFoundError{}
|
||||
}
|
||||
|
||||
p, d, err = db.profileByDeviceID(ctx, id)
|
||||
if errors.Is(err, DeviceNotFoundError{}) {
|
||||
// Probably, the device has been deleted. Remove it from our profiledb
|
||||
// in a goroutine, since that requires a write lock.
|
||||
go db.removeDeviceByIP(id, ip)
|
||||
|
||||
// Go on and return the error.
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Don't add the device ID to the error here, since it is already added
|
||||
// by profileByDeviceID.
|
||||
return nil, nil, fmt.Errorf("profile by linked device id: %w", err)
|
||||
}
|
||||
|
||||
return p, d, nil
|
||||
}
|
||||
|
||||
// removeDeviceByIP removes the device with the given ID and linked IP address
|
||||
// from the profiledb. It is intended to be used as a goroutine.
|
||||
func (db *DefaultProfileDB) removeDeviceByIP(id DeviceID, ip netip.Addr) {
|
||||
defer log.OnPanicAndExit("removeDeviceByIP", 1)
|
||||
|
||||
db.mapsMu.Lock()
|
||||
defer db.mapsMu.Unlock()
|
||||
|
||||
delete(db.ipToDeviceID, ip)
|
||||
delete(db.deviceIDToIP, id)
|
||||
delete(db.deviceToProfile, id)
|
||||
}
|
||||
|
||||
// ProfileStorage is a storage of data about profiles and other entities.
|
||||
type ProfileStorage interface {
|
||||
// Profiles returns all profiles known to this particular data storage. req
|
||||
// must not be nil.
|
||||
Profiles(ctx context.Context, req *PSProfilesRequest) (resp *PSProfilesResponse, err error)
|
||||
}
|
||||
|
||||
// PSProfilesRequest is the ProfileStorage.Profiles request.
|
||||
type PSProfilesRequest struct {
|
||||
SyncTime time.Time
|
||||
}
|
||||
|
||||
// PSProfilesResponse is the ProfileStorage.Profiles response.
|
||||
type PSProfilesResponse struct {
|
||||
SyncTime time.Time
|
||||
Profiles []*Profile
|
||||
}
|
147
internal/agd/profiledb_test.go
Normal file
147
internal/agd/profiledb_test.go
Normal file
@ -0,0 +1,147 @@
|
||||
package agd_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// newDefaultProfileDB returns a new default profile database for tests.
|
||||
func newDefaultProfileDB(tb testing.TB, dev *agd.Device) (db *agd.DefaultProfileDB) {
|
||||
tb.Helper()
|
||||
|
||||
onProfiles := func(
|
||||
_ context.Context,
|
||||
_ *agd.PSProfilesRequest,
|
||||
) (resp *agd.PSProfilesResponse, err error) {
|
||||
return &agd.PSProfilesResponse{
|
||||
Profiles: []*agd.Profile{{
|
||||
ID: testProfID,
|
||||
Devices: []*agd.Device{dev},
|
||||
}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
ds := &agdtest.ProfileStorage{
|
||||
OnProfiles: onProfiles,
|
||||
}
|
||||
|
||||
db, err := agd.NewDefaultProfileDB(ds, 1*time.Minute)
|
||||
require.NoError(tb, err)
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func TestDefaultProfileDB(t *testing.T) {
|
||||
dev := &agd.Device{
|
||||
ID: testDevID,
|
||||
LinkedIP: &testClientIPv4,
|
||||
}
|
||||
|
||||
db := newDefaultProfileDB(t, dev)
|
||||
|
||||
t.Run("by_device_id", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
p, d, err := db.ProfileByDeviceID(ctx, testDevID)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, testProfID, p.ID)
|
||||
assert.Equal(t, d, dev)
|
||||
})
|
||||
|
||||
t.Run("by_ip", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
p, d, err := db.ProfileByIP(ctx, testClientIPv4)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, testProfID, p.ID)
|
||||
assert.Equal(t, d, dev)
|
||||
})
|
||||
}
|
||||
|
||||
var profSink *agd.Profile
|
||||
|
||||
var devSink *agd.Device
|
||||
|
||||
var errSink error
|
||||
|
||||
func BenchmarkDefaultProfileDB_ProfileByDeviceID(b *testing.B) {
|
||||
dev := &agd.Device{
|
||||
ID: testDevID,
|
||||
}
|
||||
|
||||
db := newDefaultProfileDB(b, dev)
|
||||
ctx := context.Background()
|
||||
|
||||
b.Run("success", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
profSink, devSink, errSink = db.ProfileByDeviceID(ctx, testDevID)
|
||||
}
|
||||
|
||||
assert.NotNil(b, profSink)
|
||||
assert.NotNil(b, devSink)
|
||||
assert.NoError(b, errSink)
|
||||
})
|
||||
|
||||
const wrongDevID = testDevID + "_bad"
|
||||
|
||||
b.Run("not_found", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
profSink, devSink, errSink = db.ProfileByDeviceID(ctx, wrongDevID)
|
||||
}
|
||||
|
||||
assert.Nil(b, profSink)
|
||||
assert.Nil(b, devSink)
|
||||
assert.ErrorAs(b, errSink, new(agd.NotFoundError))
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkDefaultProfileDB_ProfileByIP(b *testing.B) {
|
||||
dev := &agd.Device{
|
||||
ID: testDevID,
|
||||
LinkedIP: &testClientIPv4,
|
||||
}
|
||||
|
||||
db := newDefaultProfileDB(b, dev)
|
||||
ctx := context.Background()
|
||||
|
||||
b.Run("success", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
profSink, devSink, errSink = db.ProfileByIP(ctx, testClientIPv4)
|
||||
}
|
||||
|
||||
assert.NotNil(b, profSink)
|
||||
assert.NotNil(b, devSink)
|
||||
assert.NoError(b, errSink)
|
||||
})
|
||||
|
||||
wrongClientIP := netip.MustParseAddr("5.6.7.8")
|
||||
|
||||
b.Run("not_found", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
profSink, devSink, errSink = db.ProfileByIP(ctx, wrongClientIP)
|
||||
}
|
||||
|
||||
assert.Nil(b, profSink)
|
||||
assert.Nil(b, devSink)
|
||||
assert.ErrorAs(b, errSink, new(agd.NotFoundError))
|
||||
})
|
||||
}
|
158
internal/agd/refresh.go
Normal file
158
internal/agd/refresh.go
Normal file
@ -0,0 +1,158 @@
|
||||
package agd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// Refreshable Entities And Utilities
|
||||
|
||||
// Refresher is the interface for entities that can update themselves.
|
||||
type Refresher interface {
|
||||
Refresh(ctx context.Context) (err error)
|
||||
}
|
||||
|
||||
var _ Service = (*RefreshWorker)(nil)
|
||||
|
||||
// RefreshWorker is a Service that updates its refreshable entity every tick of
|
||||
// the provided ticker.
|
||||
type RefreshWorker struct {
|
||||
done chan unit
|
||||
context func() (ctx context.Context, cancel context.CancelFunc)
|
||||
logRoutine func(format string, args ...any)
|
||||
tick *time.Ticker
|
||||
refr Refresher
|
||||
errColl ErrorCollector
|
||||
name string
|
||||
|
||||
refrOnShutdown bool
|
||||
}
|
||||
|
||||
// RefreshWorkerConfig is the configuration structure for a *RefreshWorker.
|
||||
type RefreshWorkerConfig struct {
|
||||
// Context is used to provide a context for the Refresh method of Refresher.
|
||||
Context func() (ctx context.Context, cancel context.CancelFunc)
|
||||
|
||||
// Refresher is the entity being refreshed.
|
||||
Refresher Refresher
|
||||
|
||||
// ErrColl is used to collect errors during refreshes.
|
||||
ErrColl ErrorCollector
|
||||
|
||||
// Name is the name of this worker. It is used for logging and error
|
||||
// collecting.
|
||||
Name string
|
||||
|
||||
// Interval is the refresh interval. Must be greater than zero.
|
||||
Interval time.Duration
|
||||
|
||||
// RefreshOnShutdown, if true, instructs the worker to call the Refresher's
|
||||
// Refresh method before shutting down the worker. This is useful for items
|
||||
// that should persist to disk or remote storage before shutting down.
|
||||
RefreshOnShutdown bool
|
||||
|
||||
// RoutineLogsAreDebug, if true, instructs the worker to write initial and
|
||||
// final log messages for each singular refresh on the Debug level rather
|
||||
// than on the Info one. This is useful to prevent routine logs from
|
||||
// workers with a small interval from overflowing with messages.
|
||||
RoutineLogsAreDebug bool
|
||||
}
|
||||
|
||||
// NewRefreshWorker returns a new valid *RefreshWorker with the provided
|
||||
// parameters. c must not be nil.
|
||||
func NewRefreshWorker(c *RefreshWorkerConfig) (w *RefreshWorker) {
|
||||
// TODO(a.garipov): Add log.WithLevel.
|
||||
var logRoutine func(format string, args ...any)
|
||||
if c.RoutineLogsAreDebug {
|
||||
logRoutine = log.Debug
|
||||
} else {
|
||||
logRoutine = log.Info
|
||||
}
|
||||
|
||||
return &RefreshWorker{
|
||||
done: make(chan unit),
|
||||
context: c.Context,
|
||||
logRoutine: logRoutine,
|
||||
tick: time.NewTicker(c.Interval),
|
||||
refr: c.Refresher,
|
||||
errColl: c.ErrColl,
|
||||
name: c.Name,
|
||||
refrOnShutdown: c.RefreshOnShutdown,
|
||||
}
|
||||
}
|
||||
|
||||
// Start implements the Service interface for *RefreshWorker. err is always
|
||||
// nil.
|
||||
func (w *RefreshWorker) Start() (err error) {
|
||||
go w.refreshInALoop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown implements the Service interface for *RefreshWorker.
|
||||
func (w *RefreshWorker) Shutdown(ctx context.Context) (err error) {
|
||||
if w.refrOnShutdown {
|
||||
err = w.refr.Refresh(ctx)
|
||||
}
|
||||
|
||||
close(w.done)
|
||||
|
||||
w.tick.Stop()
|
||||
|
||||
name := w.name
|
||||
if err != nil {
|
||||
err = fmt.Errorf("refresh on shutdown: %w", err)
|
||||
log.Error("%s: shut down with error: %s", name, err)
|
||||
} else {
|
||||
log.Info("%s: shut down successfully", name)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// refreshInALoop refreshes the entity every tick of w.tick until Shutdown is
|
||||
// called.
|
||||
func (w *RefreshWorker) refreshInALoop() {
|
||||
name := w.name
|
||||
defer log.OnPanic(name)
|
||||
|
||||
log.Info("%s: starting refresh loop", name)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.done:
|
||||
log.Info("%s: finished refresh loop", name)
|
||||
|
||||
return
|
||||
case <-w.tick.C:
|
||||
w.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// refresh refreshes the entity and logs the status of the refresh.
|
||||
func (w *RefreshWorker) refresh() {
|
||||
name := w.name
|
||||
w.logRoutine("%s: refreshing", name)
|
||||
|
||||
// TODO(a.garipov): Consider adding a helper for enriching errors with
|
||||
// context deadline data without duplication. See an example in method
|
||||
// filter.refreshableFilter.refresh.
|
||||
ctx, cancel := w.context()
|
||||
defer cancel()
|
||||
|
||||
log.Debug("%s: starting refresh", name)
|
||||
err := w.refr.Refresh(ctx)
|
||||
log.Debug("%s: finished refresh", name)
|
||||
|
||||
if err != nil {
|
||||
Collectf(ctx, w.errColl, "%s: %w", name, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.logRoutine("%s: refreshed successfully", name)
|
||||
}
|
139
internal/agd/refresh_test.go
Normal file
139
internal/agd/refresh_test.go
Normal file
@ -0,0 +1,139 @@
|
||||
package agd_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRefreshWorker(t *testing.T) {
|
||||
// Test Constants
|
||||
|
||||
const (
|
||||
testIvl = 5 * time.Millisecond
|
||||
testIvlLong = 1 * time.Hour
|
||||
name = "test refresher"
|
||||
testError errors.Error = "test error"
|
||||
)
|
||||
|
||||
// Test Mocks
|
||||
|
||||
pt := testutil.PanicT{}
|
||||
refreshSync := make(chan agdtest.Signal, 1)
|
||||
|
||||
refr := &agdtest.Refresher{
|
||||
OnRefresh: func(_ context.Context) (err error) {
|
||||
agdtest.RequireSend(pt, refreshSync, testTimeout)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
errRefr := &agdtest.Refresher{
|
||||
OnRefresh: func(_ context.Context) (err error) {
|
||||
agdtest.RequireSend(pt, refreshSync, testTimeout)
|
||||
|
||||
return testError
|
||||
},
|
||||
}
|
||||
|
||||
errCh := make(chan agdtest.Signal, 1)
|
||||
errColl := &agdtest.ErrorCollector{
|
||||
OnCollect: func(_ context.Context, _ error) {
|
||||
agdtest.RequireSend(pt, errCh, testTimeout)
|
||||
},
|
||||
}
|
||||
|
||||
// Test Helpers
|
||||
|
||||
refrConf := func(
|
||||
refr agd.Refresher,
|
||||
ivl time.Duration,
|
||||
refrOnShutDown bool,
|
||||
) (conf *agd.RefreshWorkerConfig) {
|
||||
return &agd.RefreshWorkerConfig{
|
||||
Context: func() (ctx context.Context, cancel context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), testTimeout)
|
||||
},
|
||||
Refresher: refr,
|
||||
ErrColl: errColl,
|
||||
Name: name,
|
||||
Interval: ivl,
|
||||
RefreshOnShutdown: refrOnShutDown,
|
||||
RoutineLogsAreDebug: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
w := agd.NewRefreshWorker(refrConf(refr, testIvl, false))
|
||||
|
||||
err := w.Start()
|
||||
require.NoError(t, err)
|
||||
|
||||
agdtest.RequireReceive(pt, refreshSync, testTimeout)
|
||||
require.Empty(t, errCh)
|
||||
|
||||
shutdown, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
err = w.Shutdown(shutdown)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("success_on_shutdown", func(t *testing.T) {
|
||||
w := agd.NewRefreshWorker(refrConf(refr, testIvlLong, true))
|
||||
|
||||
err := w.Start()
|
||||
require.NoError(t, err)
|
||||
|
||||
shutdown, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
err = w.Shutdown(shutdown)
|
||||
require.NoError(t, err)
|
||||
|
||||
agdtest.RequireReceive(pt, refreshSync, testTimeout)
|
||||
require.Empty(t, errCh)
|
||||
})
|
||||
|
||||
t.Run("error", func(t *testing.T) {
|
||||
w := agd.NewRefreshWorker(refrConf(errRefr, testIvl, false))
|
||||
|
||||
err := w.Start()
|
||||
require.NoError(t, err)
|
||||
|
||||
agdtest.RequireReceive(pt, refreshSync, testTimeout)
|
||||
agdtest.RequireReceive(pt, errCh, testTimeout)
|
||||
|
||||
shutdown, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
err = w.Shutdown(shutdown)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("error_on_shutdown", func(t *testing.T) {
|
||||
w := agd.NewRefreshWorker(refrConf(errRefr, testIvlLong, true))
|
||||
|
||||
err := w.Start()
|
||||
require.NoError(t, err)
|
||||
|
||||
shutdown, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
err = w.Shutdown(shutdown)
|
||||
assert.ErrorIs(t, err, testError)
|
||||
|
||||
agdtest.RequireReceive(pt, refreshSync, testTimeout)
|
||||
require.Empty(t, errCh)
|
||||
})
|
||||
}
|
109
internal/agd/server.go
Normal file
109
internal/agd/server.go
Normal file
@ -0,0 +1,109 @@
|
||||
package agd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/netip"
|
||||
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
"github.com/ameshkov/dnscrypt/v2"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Servers And Server Groups
|
||||
|
||||
// ServerGroup is a group of DNS servers all of which use the same filtering
|
||||
// settings.
|
||||
type ServerGroup struct {
|
||||
// TLS are the TLS settings for this server group, if any.
|
||||
TLS *TLS
|
||||
|
||||
// DDR is the configuration for the server group's Discovery Of Designated
|
||||
// Resolvers (DDR) handlers. DDR is never nil.
|
||||
DDR *DDR
|
||||
|
||||
// Name is the unique name of the server group.
|
||||
Name ServerGroupName
|
||||
|
||||
// FilteringGroup is the ID of the filtering group for this server.
|
||||
FilteringGroup FilteringGroupID
|
||||
|
||||
// Servers are the settings for servers. Each element must be non-nil.
|
||||
Servers []*Server
|
||||
}
|
||||
|
||||
// ServerGroupName is the name of a server group.
|
||||
type ServerGroupName string
|
||||
|
||||
// TLS is the TLS configuration of a DNS server group.
|
||||
type TLS struct {
|
||||
// Conf is the server's TLS configuration.
|
||||
Conf *tls.Config
|
||||
|
||||
// DeviceIDWildcards are the domain wildcards used to detect device IDs from
|
||||
// clients' server names.
|
||||
DeviceIDWildcards []string
|
||||
|
||||
// SessionKeys are paths to files containing the TLS session keys for this
|
||||
// server.
|
||||
SessionKeys []string
|
||||
}
|
||||
|
||||
// DDR is the configuration for the server group's Discovery Of Designated
|
||||
// Resolvers (DDR) handlers.
|
||||
type DDR struct {
|
||||
// DeviceTargets is the set of all domain names, subdomains of which should
|
||||
// be checked for DDR queries with device IDs.
|
||||
DeviceTargets *stringutil.Set
|
||||
|
||||
// PublicTargets is the set of all public domain names, DDR queries for
|
||||
// which should be processed.
|
||||
PublicTargets *stringutil.Set
|
||||
|
||||
// DeviceRecordTemplates are used to respond to DDR queries from recognized
|
||||
// devices.
|
||||
DeviceRecordTemplates []*dns.SVCB
|
||||
|
||||
// PubilcRecordTemplates are used to respond to DDR queries from
|
||||
// unrecognized devices.
|
||||
PublicRecordTemplates []*dns.SVCB
|
||||
|
||||
// Enabled shows if DDR queries are processed. If it is false, DDR domain
|
||||
// name queries receive an NXDOMAIN response.
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
// Server represents a single DNS server. That is, an entity that binds to one
|
||||
// or more ports and serves DNS over a single protocol.
|
||||
type Server struct {
|
||||
// DNSCrypt are the DNSCrypt settings for this server, if any.
|
||||
DNSCrypt *DNSCryptConfig
|
||||
|
||||
// TLS is the TLS configuration for this server, if any.
|
||||
TLS *tls.Config
|
||||
|
||||
// Name is the unique name of the server. Not to be confused with a TLS
|
||||
// Server Name.
|
||||
Name ServerName
|
||||
|
||||
// BindAddresses are addresses this server binds to.
|
||||
BindAddresses []netip.AddrPort
|
||||
|
||||
// Protocol is the protocol of the server.
|
||||
Protocol Protocol
|
||||
|
||||
// LinkedIPEnabled shows if the linked IP addresses should be used to detect
|
||||
// profiles on this server.
|
||||
LinkedIPEnabled bool
|
||||
}
|
||||
|
||||
// ServerName is the name of a server.
|
||||
type ServerName string
|
||||
|
||||
// DNSCryptConfig is the DNSCrypt configuration of a DNS server.
|
||||
type DNSCryptConfig struct {
|
||||
// Cert is the DNSCrypt certificate.
|
||||
Cert *dnscrypt.Cert
|
||||
|
||||
// ProviderName is the name of the DNSCrypt provider.
|
||||
ProviderName string
|
||||
}
|
25
internal/agd/service.go
Normal file
25
internal/agd/service.go
Normal file
@ -0,0 +1,25 @@
|
||||
package agd
|
||||
|
||||
import "context"
|
||||
|
||||
// Service is the interface for API servers.
|
||||
type Service interface {
|
||||
// Start starts the service. It must not block.
|
||||
Start() (err error)
|
||||
|
||||
// Shutdown gracefully stops the service. ctx is used to determine
|
||||
// a timeout before trying to stop the service less gracefully.
|
||||
Shutdown(ctx context.Context) (err error)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ Service = EmptyService{}
|
||||
|
||||
// EmptyService is an agd.Service that does nothing.
|
||||
type EmptyService struct{}
|
||||
|
||||
// Start implements the Service interface for EmptyService.
|
||||
func (EmptyService) Start() (err error) { return nil }
|
||||
|
||||
// Shutdown implements the Service interface for EmptyService.
|
||||
func (EmptyService) Shutdown(_ context.Context) (err error) { return nil }
|
21
internal/agd/upstream.go
Normal file
21
internal/agd/upstream.go
Normal file
@ -0,0 +1,21 @@
|
||||
package agd
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Upstream
|
||||
|
||||
// Upstream module configuration.
|
||||
type Upstream struct {
|
||||
// Server is the upstream server we're using to forward DNS queries.
|
||||
Server netip.AddrPort
|
||||
|
||||
// FallbackServers is a list of the DNS servers we're using to fallback to
|
||||
// when the upstream server fails to respond.
|
||||
FallbackServers []netip.AddrPort
|
||||
|
||||
// Timeout is the timeout for all outgoing DNS requests.
|
||||
Timeout time.Duration
|
||||
}
|
33
internal/agd/version.go
Normal file
33
internal/agd/version.go
Normal file
@ -0,0 +1,33 @@
|
||||
package agd
|
||||
|
||||
// Versions
|
||||
|
||||
// These are set by the linker. Unfortunately, we cannot set constants during
|
||||
// linking, and Go doesn't have a concept of immutable variables, so to be
|
||||
// thorough we have to only export them through getters.
|
||||
var (
|
||||
branch string
|
||||
buildtime string
|
||||
revision string
|
||||
version string
|
||||
)
|
||||
|
||||
// Branch returns the compiled-in value of the Git branch.
|
||||
func Branch() (b string) {
|
||||
return branch
|
||||
}
|
||||
|
||||
// BuildTime returns the compiled-in value of the build time as a string.
|
||||
func BuildTime() (t string) {
|
||||
return buildtime
|
||||
}
|
||||
|
||||
// Revision returns the compiled-in value of the Git revision.
|
||||
func Revision() (r string) {
|
||||
return revision
|
||||
}
|
||||
|
||||
// Version returns the compiled-in value of the AdGuard DNS version as a string.
|
||||
func Version() (v string) {
|
||||
return version
|
||||
}
|
45
internal/agdhttp/agdhttp.go
Normal file
45
internal/agdhttp/agdhttp.go
Normal file
@ -0,0 +1,45 @@
|
||||
// Package agdhttp contains common constants, functions, and types for working
|
||||
// with HTTP.
|
||||
//
|
||||
// TODO(a.garipov): Consider moving all or some of this stuff to module golibs.
|
||||
package agdhttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||
)
|
||||
|
||||
// Common Constants, Functions And Types
|
||||
|
||||
// HTTP header name constants.
|
||||
const (
|
||||
HdrNameAcceptEncoding = "Accept-Encoding"
|
||||
HdrNameAccessControlAllowOrigin = "Access-Control-Allow-Origin"
|
||||
HdrNameContentType = "Content-Type"
|
||||
HdrNameContentEncoding = "Content-Encoding"
|
||||
HdrNameServer = "Server"
|
||||
HdrNameTrailer = "Trailer"
|
||||
HdrNameUserAgent = "User-Agent"
|
||||
|
||||
HdrNameXError = "X-Error"
|
||||
HdrNameXRequestID = "X-Request-Id"
|
||||
)
|
||||
|
||||
// HTTP header value constants.
|
||||
const (
|
||||
HdrValApplicationJSON = "application/json"
|
||||
HdrValTextCSV = "text/csv"
|
||||
HdrValTextHTML = "text/html"
|
||||
HdrValTextPlain = "text/plain"
|
||||
HdrValWildcard = "*"
|
||||
)
|
||||
|
||||
// RobotsDisallowAll is a predefined robots disallow all content.
|
||||
const RobotsDisallowAll = "User-agent: *\nDisallow: /\n"
|
||||
|
||||
// UserAgent returns the ID of the service as a User-Agent string. It can also
|
||||
// be used as the value of the Server HTTP header.
|
||||
func UserAgent() (ua string) {
|
||||
return fmt.Sprintf("AdGuardDNS/%s", agd.Version())
|
||||
}
|
11
internal/agdhttp/agdhttp_test.go
Normal file
11
internal/agdhttp/agdhttp_test.go
Normal file
@ -0,0 +1,11 @@
|
||||
package agdhttp_test
|
||||
|
||||
import "github.com/AdguardTeam/golibs/errors"
|
||||
|
||||
// Common Testing Constants And Variables
|
||||
|
||||
// testSrv is the common Server header value for tests.
|
||||
const testSrv = "testServer/1.0"
|
||||
|
||||
// testError is the common error for tests.
|
||||
const testError errors.Error = "test error"
|
105
internal/agdhttp/client.go
Normal file
105
internal/agdhttp/client.go
Normal file
@ -0,0 +1,105 @@
|
||||
package agdhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||
)
|
||||
|
||||
// Client is a wrapper around http.Client.
|
||||
type Client struct {
|
||||
http *http.Client
|
||||
userAgent string
|
||||
}
|
||||
|
||||
// ClientConfig is the configuration structure for Client.
|
||||
type ClientConfig struct {
|
||||
// Timeout is the timeout for all requests.
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// NewClient returns a new client. c must not be nil.
|
||||
func NewClient(conf *ClientConfig) (c *Client) {
|
||||
return &Client{
|
||||
http: &http.Client{
|
||||
Timeout: conf.Timeout,
|
||||
},
|
||||
userAgent: UserAgent(),
|
||||
}
|
||||
}
|
||||
|
||||
// Get is a wrapper around http.Client.Get.
|
||||
//
|
||||
// When err is nil, resp always contains a non-nil resp.Body. Caller should
|
||||
// close resp.Body when done reading from it.
|
||||
//
|
||||
// See also go doc http.Client.Get.
|
||||
func (c *Client) Get(ctx context.Context, u *url.URL) (resp *http.Response, err error) {
|
||||
return c.do(ctx, http.MethodGet, u, "", nil)
|
||||
}
|
||||
|
||||
// Post is a wrapper around http.Client.Post.
|
||||
//
|
||||
// When err is nil, resp always contains a non-nil resp.Body. Caller should
|
||||
// close resp.Body when done reading from it.
|
||||
//
|
||||
// See also go doc http.Client.Post.
|
||||
func (c *Client) Post(
|
||||
ctx context.Context,
|
||||
u *url.URL,
|
||||
contentType string,
|
||||
body io.Reader,
|
||||
) (resp *http.Response, err error) {
|
||||
return c.do(ctx, http.MethodPost, u, contentType, body)
|
||||
}
|
||||
|
||||
// Put is a wrapper around http.Client.Do.
|
||||
//
|
||||
// When err is nil, resp always contains a non-nil resp.Body. Caller should
|
||||
// close resp.Body when done reading from it.
|
||||
func (c *Client) Put(
|
||||
ctx context.Context,
|
||||
u *url.URL,
|
||||
contentType string,
|
||||
body io.Reader,
|
||||
) (resp *http.Response, err error) {
|
||||
return c.do(ctx, http.MethodPut, u, contentType, body)
|
||||
}
|
||||
|
||||
// do is a wrapper around http.Client.Do.
|
||||
func (c *Client) do(
|
||||
ctx context.Context,
|
||||
method string,
|
||||
u *url.URL,
|
||||
contentType string,
|
||||
body io.Reader,
|
||||
) (resp *http.Response, err error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, u.String(), body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating %s request to: %w", method, err)
|
||||
}
|
||||
|
||||
if contentType != "" {
|
||||
req.Header.Set(HdrNameContentType, contentType)
|
||||
}
|
||||
|
||||
reqID, ok := agd.RequestIDFromContext(ctx)
|
||||
if ok {
|
||||
req.Header.Set(HdrNameXRequestID, string(reqID))
|
||||
}
|
||||
|
||||
req.Header.Set(HdrNameUserAgent, c.userAgent)
|
||||
|
||||
resp, err = c.http.Do(req)
|
||||
if err != nil && resp != nil && resp.Header != nil {
|
||||
// A non-nil Response with a non-nil error only occurs when CheckRedirect fails.
|
||||
return resp, WrapServerError(err, resp)
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
78
internal/agdhttp/error.go
Normal file
78
internal/agdhttp/error.go
Normal file
@ -0,0 +1,78 @@
|
||||
package agdhttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
)
|
||||
|
||||
// Common HTTP Errors
|
||||
|
||||
// StatusError is returned by methods when the HTTP status code is different
|
||||
// from the expected.
|
||||
type StatusError struct {
|
||||
ServerName string
|
||||
Expected int
|
||||
Got int
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ error = (*StatusError)(nil)
|
||||
|
||||
// Error implements the error interface for *StatusError.
|
||||
func (err *StatusError) Error() (msg string) {
|
||||
return fmt.Sprintf(
|
||||
"server %q: status code error: expected %d, got %d",
|
||||
err.ServerName,
|
||||
err.Expected,
|
||||
err.Got,
|
||||
)
|
||||
}
|
||||
|
||||
// CheckStatus returns a non-nil error with the data from resp if the status
|
||||
// code in resp is not equal to expected. resp must be non-nil.
|
||||
//
|
||||
// Any error returned will have the underlying type of *StatusError.
|
||||
func CheckStatus(resp *http.Response, expected int) (err error) {
|
||||
if resp.StatusCode == expected {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &StatusError{
|
||||
ServerName: resp.Header.Get(HdrNameServer),
|
||||
Expected: expected,
|
||||
Got: resp.StatusCode,
|
||||
}
|
||||
}
|
||||
|
||||
// ServerError is returned as general error in case header Server was specified.
|
||||
type ServerError struct {
|
||||
Err error
|
||||
ServerName string
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ error = (*ServerError)(nil)
|
||||
|
||||
// Error implements the error interface for *ServerError.
|
||||
func (err *ServerError) Error() (msg string) {
|
||||
return fmt.Sprintf("server %q: %s", err.ServerName, err.Err)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ errors.Wrapper = (*ServerError)(nil)
|
||||
|
||||
// Unwrap implements the errors.Wrapper interface for *ServerError.
|
||||
func (err *ServerError) Unwrap() (unwrapped error) {
|
||||
return err.Err
|
||||
}
|
||||
|
||||
// WrapServerError wraps err inside a *ServerError including data from resp.
|
||||
// resp must not be nil.
|
||||
func WrapServerError(err error, resp *http.Response) (wrapped *ServerError) {
|
||||
return &ServerError{
|
||||
Err: err,
|
||||
ServerName: resp.Header.Get(HdrNameServer),
|
||||
}
|
||||
}
|
85
internal/agdhttp/error_test.go
Normal file
85
internal/agdhttp/error_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
package agdhttp_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCheckStatus(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
srv string
|
||||
wantErrMsg string
|
||||
exp int
|
||||
got int
|
||||
}{{
|
||||
name: "200_200",
|
||||
srv: testSrv,
|
||||
wantErrMsg: "",
|
||||
exp: 200,
|
||||
got: 200,
|
||||
}, {
|
||||
name: "200_404",
|
||||
srv: "",
|
||||
wantErrMsg: `server "": status code error: expected 200, got 404`,
|
||||
exp: 200,
|
||||
got: 404,
|
||||
}, {
|
||||
name: "200_404_srv",
|
||||
srv: testSrv,
|
||||
wantErrMsg: `server "` + testSrv + `": status code error: expected 200, got 404`,
|
||||
exp: 200,
|
||||
got: 404,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
resp := &http.Response{
|
||||
StatusCode: tc.got,
|
||||
Header: http.Header{
|
||||
agdhttp.HdrNameServer: []string{tc.srv},
|
||||
},
|
||||
}
|
||||
err := agdhttp.CheckStatus(resp, tc.exp)
|
||||
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
err error
|
||||
name string
|
||||
srv string
|
||||
wantErrMsg string
|
||||
}{{
|
||||
err: testError,
|
||||
name: "no_srv",
|
||||
srv: "",
|
||||
wantErrMsg: `server "": ` + string(testError),
|
||||
}, {
|
||||
err: testError,
|
||||
name: "with_srv",
|
||||
srv: testSrv,
|
||||
wantErrMsg: `server "` + testSrv + `": ` + string(testError),
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
resp := &http.Response{
|
||||
Header: http.Header{
|
||||
agdhttp.HdrNameServer: []string{tc.srv},
|
||||
},
|
||||
}
|
||||
err := agdhttp.WrapServerError(tc.err, resp)
|
||||
|
||||
assert.ErrorIs(t, err, tc.err)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
}
|
50
internal/agdhttp/url.go
Normal file
50
internal/agdhttp/url.go
Normal file
@ -0,0 +1,50 @@
|
||||
package agdhttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
)
|
||||
|
||||
// URL Types And Utilities
|
||||
|
||||
// ParseHTTPURL parses an absolute URL and makes sure that it is a valid HTTP(S)
|
||||
// URL. All returned errors will have the underlying type [*url.Error].
|
||||
//
|
||||
// TODO(a.garipov): Define as a type?
|
||||
func ParseHTTPURL(s string) (u *url.URL, err error) {
|
||||
u, err = url.Parse(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case u.Host == "":
|
||||
return nil, &url.Error{
|
||||
Op: "parse",
|
||||
URL: s,
|
||||
Err: errors.Error("empty host"),
|
||||
}
|
||||
case u.Scheme != "http" && u.Scheme != "https":
|
||||
return nil, &url.Error{
|
||||
Op: "parse",
|
||||
URL: s,
|
||||
Err: fmt.Errorf("bad scheme %q", u.Scheme),
|
||||
}
|
||||
default:
|
||||
return u, nil
|
||||
}
|
||||
}
|
||||
|
||||
// URL is a wrapper around *url.URL that can unmarshal itself from JSON or YAML.
|
||||
//
|
||||
// TODO(a.garipov): Move to netutil if we need it somewhere else.
|
||||
type URL struct {
|
||||
url.URL
|
||||
}
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface for *URL.
|
||||
func (u *URL) UnmarshalText(b []byte) (err error) {
|
||||
return u.UnmarshalBinary(b)
|
||||
}
|
80
internal/agdhttp/url_test.go
Normal file
80
internal/agdhttp/url_test.go
Normal file
@ -0,0 +1,80 @@
|
||||
package agdhttp_test
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseHTTPURL(t *testing.T) {
|
||||
goodURL := testURL()
|
||||
|
||||
badSchemeURL := netutil.CloneURL(goodURL)
|
||||
badSchemeURL.Scheme = "ftp"
|
||||
|
||||
relativeURL := &url.URL{
|
||||
Path: "/a/b/c/",
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
want *url.URL
|
||||
name string
|
||||
in string
|
||||
wantErrMsg string
|
||||
}{{
|
||||
want: goodURL,
|
||||
name: "ok",
|
||||
in: goodURL.String(),
|
||||
wantErrMsg: ``,
|
||||
}, {
|
||||
want: nil,
|
||||
name: "invalid",
|
||||
in: "\n",
|
||||
wantErrMsg: `parse "\n": net/url: invalid control character in URL`,
|
||||
}, {
|
||||
want: nil,
|
||||
name: "bad_scheme",
|
||||
in: badSchemeURL.String(),
|
||||
wantErrMsg: `parse "` + badSchemeURL.String() + `": bad scheme "ftp"`,
|
||||
}, {
|
||||
want: nil,
|
||||
name: "relative",
|
||||
in: relativeURL.Path,
|
||||
wantErrMsg: `parse "/a/b/c/": empty host`,
|
||||
}, {
|
||||
want: nil,
|
||||
name: "empty",
|
||||
in: "",
|
||||
wantErrMsg: `parse "": empty host`,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := agdhttp.ParseHTTPURL(tc.in)
|
||||
assert.Equal(t, tc.want, got)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestURL_UnmarshalText(t *testing.T) {
|
||||
u := &agdhttp.URL{
|
||||
URL: *testURL(),
|
||||
}
|
||||
testutil.AssertUnmarshalText(t, u.String(), u)
|
||||
}
|
||||
|
||||
func testURL() (u *url.URL) {
|
||||
return &url.URL{
|
||||
Scheme: "http",
|
||||
User: url.UserPassword("user", "pass"),
|
||||
Host: "example.com",
|
||||
Path: "/a/b/c/",
|
||||
RawQuery: "d=e",
|
||||
Fragment: "f",
|
||||
}
|
||||
}
|
57
internal/agdio/agdio.go
Normal file
57
internal/agdio/agdio.go
Normal file
@ -0,0 +1,57 @@
|
||||
// Package agdio contains extensions and utilities for package io from the
|
||||
// standard library.
|
||||
//
|
||||
// TODO(a.garipov): Move to module golibs.
|
||||
package agdio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// LimitError is returned when the Limit is reached.
|
||||
type LimitError struct {
|
||||
// Limit is the limit that triggered the error.
|
||||
Limit int64
|
||||
}
|
||||
|
||||
// Error implements the error interface for *LimitError.
|
||||
func (err *LimitError) Error() string {
|
||||
return fmt.Sprintf("cannot read more than %d bytes", err.Limit)
|
||||
}
|
||||
|
||||
// limitedReader is a wrapper for io.Reader that has a reading limit.
|
||||
type limitedReader struct {
|
||||
r io.Reader
|
||||
limit int64
|
||||
n int64
|
||||
}
|
||||
|
||||
// Read implements the io.Reader interface for *limitedReader.
|
||||
func (lr *limitedReader) Read(p []byte) (n int, err error) {
|
||||
if lr.n == 0 {
|
||||
return 0, &LimitError{
|
||||
Limit: lr.limit,
|
||||
}
|
||||
}
|
||||
|
||||
if int64(len(p)) > lr.n {
|
||||
p = p[0:lr.n]
|
||||
}
|
||||
|
||||
n, err = lr.r.Read(p)
|
||||
lr.n -= int64(n)
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
// LimitReader returns an io.Reader that reads up to n bytes. Once that limit
|
||||
// is reached, ErrLimit is returned from limited's Read method. Method
|
||||
// Read of limited is not safe for concurrent use. n must be non-negative.
|
||||
func LimitReader(r io.Reader, n int64) (limited io.Reader) {
|
||||
return &limitedReader{
|
||||
r: r,
|
||||
limit: n,
|
||||
n: n,
|
||||
}
|
||||
}
|
69
internal/agdio/agdio_test.go
Normal file
69
internal/agdio/agdio_test.go
Normal file
@ -0,0 +1,69 @@
|
||||
package agdio_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdio"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLimitedReader_Read(t *testing.T) {
|
||||
testCases := []struct {
|
||||
err error
|
||||
name string
|
||||
rStr string
|
||||
limit int64
|
||||
want int
|
||||
}{{
|
||||
err: nil,
|
||||
name: "perfectly_match",
|
||||
rStr: "abc",
|
||||
limit: 3,
|
||||
want: 3,
|
||||
}, {
|
||||
err: io.EOF,
|
||||
name: "eof",
|
||||
rStr: "",
|
||||
limit: 3,
|
||||
want: 0,
|
||||
}, {
|
||||
err: &agdio.LimitError{
|
||||
Limit: 0,
|
||||
},
|
||||
name: "limit_reached",
|
||||
rStr: "abc",
|
||||
limit: 0,
|
||||
want: 0,
|
||||
}, {
|
||||
err: nil,
|
||||
name: "truncated",
|
||||
rStr: "abc",
|
||||
limit: 2,
|
||||
want: 2,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
readCloser := io.NopCloser(strings.NewReader(tc.rStr))
|
||||
buf := make([]byte, tc.limit+1)
|
||||
|
||||
lreader := agdio.LimitReader(readCloser, tc.limit)
|
||||
n, err := lreader.Read(buf)
|
||||
require.Equal(t, tc.err, err)
|
||||
|
||||
assert.Equal(t, tc.want, n)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimitError_Error(t *testing.T) {
|
||||
err := &agdio.LimitError{
|
||||
Limit: 0,
|
||||
}
|
||||
|
||||
const want = "cannot read more than 0 bytes"
|
||||
assert.Equal(t, want, err.Error())
|
||||
}
|
191
internal/agdnet/agdnet.go
Normal file
191
internal/agdnet/agdnet.go
Normal file
@ -0,0 +1,191 @@
|
||||
// Package agdnet contains network-related utilities.
|
||||
//
|
||||
// TODO(a.garipov): Move stuff to netutil.
|
||||
package agdnet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AddrFamily is an IANA address family number.
|
||||
type AddrFamily uint16
|
||||
|
||||
// IANA address family numbers.
|
||||
//
|
||||
// See https://www.iana.org/assignments/address-family-numbers/address-family-numbers.xhtml.
|
||||
const (
|
||||
AddrFamilyNone AddrFamily = 0
|
||||
AddrFamilyIPv4 AddrFamily = 1
|
||||
AddrFamilyIPv6 AddrFamily = 2
|
||||
)
|
||||
|
||||
// String implements the fmt.Stringer interface for AddrFamily.
|
||||
func (f AddrFamily) String() (s string) {
|
||||
switch f {
|
||||
case AddrFamilyNone:
|
||||
return "none"
|
||||
case AddrFamilyIPv4:
|
||||
return "ipv4"
|
||||
case AddrFamilyIPv6:
|
||||
return "ipv6"
|
||||
default:
|
||||
return fmt.Sprintf("!bad_addr_fam_%d", f)
|
||||
}
|
||||
}
|
||||
|
||||
// androidMetricFQDNSuffix is the suffix of the FQDN in DNS queries for
|
||||
// metrics that the DNS resolver of the Android operating system seems to
|
||||
// send a lot and because of that we apply special rules to these queries.
|
||||
// Check out Android code to see how it's used:
|
||||
// https://cs.android.com/search?q=ds.metric.gstatic.com
|
||||
const androidMetricFQDNSuffix = "-ds.metric.gstatic.com."
|
||||
|
||||
// IsAndroidTLSMetricDomain returns true if the specified domain is the
|
||||
// Android's DNS-over-TLS metrics domain.
|
||||
func IsAndroidTLSMetricDomain(fqdn string) (ok bool) {
|
||||
fqdnLen := len(fqdn)
|
||||
sufLen := len(androidMetricFQDNSuffix)
|
||||
|
||||
return fqdnLen > sufLen && strings.EqualFold(fqdn[fqdnLen-sufLen:], androidMetricFQDNSuffix)
|
||||
}
|
||||
|
||||
// IsSubdomain returns true if domain is a subdomain of top.
|
||||
func IsSubdomain(domain, top string) (ok bool) {
|
||||
return len(domain) > len(top)+1 &&
|
||||
strings.HasSuffix(domain, top) &&
|
||||
domain[len(domain)-len(top)-1] == '.'
|
||||
}
|
||||
|
||||
// IsImmediateSubdomain returns true if domain is an immediate subdomain of top.
|
||||
//
|
||||
// TODO(a.garipov): Move to netutil.
|
||||
func IsImmediateSubdomain(domain, top string) (ok bool) {
|
||||
return IsSubdomain(domain, top) &&
|
||||
strings.Count(domain, ".") == strings.Count(top, ".")+1
|
||||
}
|
||||
|
||||
// ZeroSubnet returns an IP subnet with prefix 0 and all bytes of the IP address
|
||||
// set to 0. fam must be either AddrFamilyIPv4 or AddrFamilyIPv6.
|
||||
//
|
||||
// TODO(a.garipov): Move to netutil.
|
||||
func ZeroSubnet(fam AddrFamily) (n netip.Prefix) {
|
||||
switch fam {
|
||||
case AddrFamilyIPv4:
|
||||
return netip.PrefixFrom(netip.IPv4Unspecified(), 0)
|
||||
case AddrFamilyIPv6:
|
||||
return netip.PrefixFrom(netip.IPv6Unspecified(), 0)
|
||||
default:
|
||||
panic(fmt.Errorf("agdnet: unsupported addr fam %s", fam))
|
||||
}
|
||||
}
|
||||
|
||||
// IPNetToPrefix is a helper that converts a *net.IPNet into a netip.Prefix. If
|
||||
// subnet is nil, it returns netip.Prefix{}. fam must be either AddrFamilyIPv4
|
||||
// or AddrFamilyIPv6.
|
||||
func IPNetToPrefix(subnet *net.IPNet, fam AddrFamily) (p netip.Prefix, err error) {
|
||||
if subnet == nil {
|
||||
return netip.Prefix{}, nil
|
||||
}
|
||||
|
||||
addr, err := IPToAddr(subnet.IP, fam)
|
||||
if err != nil {
|
||||
return netip.Prefix{}, fmt.Errorf("bad ip for subnet %v: %w", subnet, err)
|
||||
}
|
||||
|
||||
ones, _ := subnet.Mask.Size()
|
||||
p = netip.PrefixFrom(addr, ones)
|
||||
if !p.IsValid() {
|
||||
return netip.Prefix{}, fmt.Errorf("bad subnet %v", subnet)
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// IPNetToPrefixNoMapped is like IPNetToPrefix but it detects the address family
|
||||
// automatically by assuming that every IPv6-mapped IPv4 address is actually an
|
||||
// IPv4 address. Do not use IPNetToPrefixNoMapped where this assumption isn't
|
||||
// safe.
|
||||
func IPNetToPrefixNoMapped(subnet *net.IPNet) (p netip.Prefix, err error) {
|
||||
if subnet == nil {
|
||||
return netip.Prefix{}, nil
|
||||
}
|
||||
|
||||
if ip4 := subnet.IP.To4(); ip4 != nil {
|
||||
subnet.IP = ip4
|
||||
|
||||
return IPNetToPrefix(subnet, AddrFamilyIPv4)
|
||||
}
|
||||
|
||||
return IPNetToPrefix(subnet, AddrFamilyIPv6)
|
||||
}
|
||||
|
||||
// IPToAddr converts a net.IP into a netip.Addr of the given family and returns
|
||||
// a meaningful error. fam must be either AddrFamilyIPv4 or AddrFamilyIPv6.
|
||||
func IPToAddr(ip net.IP, fam AddrFamily) (addr netip.Addr, err error) {
|
||||
switch fam {
|
||||
case AddrFamilyIPv4:
|
||||
// Make sure that we use the IPv4 form of the address to make sure that
|
||||
// netip.Addr doesn't turn out to be an IPv6 one when it really should
|
||||
// be an IPv4 one.
|
||||
ip4 := ip.To4()
|
||||
if ip4 == nil {
|
||||
return netip.Addr{}, fmt.Errorf("bad ipv4 net.IP %v", ip)
|
||||
}
|
||||
|
||||
ip = ip4
|
||||
case AddrFamilyIPv6:
|
||||
// Again, make sure that we use the correct form according to the
|
||||
// address family.
|
||||
ip = ip.To16()
|
||||
default:
|
||||
panic(fmt.Errorf("agdnet: unsupported addr fam %s", fam))
|
||||
}
|
||||
|
||||
addr, ok := netip.AddrFromSlice(ip)
|
||||
if !ok {
|
||||
return netip.Addr{}, fmt.Errorf("bad net.IP value %v", ip)
|
||||
}
|
||||
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// IPToAddrNoMapped is like IPToAddr but it detects the address family
|
||||
// automatically by assuming that every IPv6-mapped IPv4 address is actually an
|
||||
// IPv4 address. Do not use IPToAddrNoMapped where this assumption isn't safe.
|
||||
func IPToAddrNoMapped(ip net.IP) (addr netip.Addr, err error) {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
return IPToAddr(ip4, AddrFamilyIPv4)
|
||||
}
|
||||
|
||||
return IPToAddr(ip, AddrFamilyIPv6)
|
||||
}
|
||||
|
||||
// ParseSubnets parses IP networks, including single-address ones, from strings.
|
||||
func ParseSubnets(strs ...string) (subnets []netip.Prefix, err error) {
|
||||
subnets = make([]netip.Prefix, len(strs))
|
||||
for i, s := range strs {
|
||||
// Detect if this is a CIDR or an IP early, so that the path to
|
||||
// returning an error is shorter.
|
||||
if strings.Contains(s, "/") {
|
||||
subnets[i], err = netip.ParsePrefix(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("subnet at idx %d: %w", i, err)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var ip netip.Addr
|
||||
ip, err = netip.ParseAddr(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ip at idx %d: %w", i, err)
|
||||
}
|
||||
|
||||
subnets[i] = netip.PrefixFrom(ip, ip.BitLen())
|
||||
}
|
||||
|
||||
return subnets, nil
|
||||
}
|
175
internal/agdnet/agdnet_example_test.go
Normal file
175
internal/agdnet/agdnet_example_test.go
Normal file
@ -0,0 +1,175 @@
|
||||
package agdnet_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdnet"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
)
|
||||
|
||||
func ExampleIsAndroidTLSMetricDomain() {
|
||||
anAndroidDomain := "1234-ds.metric.gstatic.com."
|
||||
fmt.Printf("%-28s: %5t\n", anAndroidDomain, agdnet.IsAndroidTLSMetricDomain(anAndroidDomain))
|
||||
|
||||
notAnAndroidDomain := "www.example.com."
|
||||
fmt.Printf("%-28s: %5t\n", notAnAndroidDomain, agdnet.IsAndroidTLSMetricDomain(notAnAndroidDomain))
|
||||
|
||||
// Output:
|
||||
// 1234-ds.metric.gstatic.com. : true
|
||||
// www.example.com. : false
|
||||
}
|
||||
|
||||
func ExampleIsSubdomain() {
|
||||
fmt.Printf("%-14s: %5t\n", "same domain", agdnet.IsSubdomain("sub.example.com", "example.com"))
|
||||
fmt.Printf("%-14s: %5t\n", "not immediate", agdnet.IsSubdomain("subsub.sub.example.com", "example.com"))
|
||||
|
||||
fmt.Printf("%-14s: %5t\n", "empty", agdnet.IsSubdomain("", ""))
|
||||
fmt.Printf("%-14s: %5t\n", "same", agdnet.IsSubdomain("example.com", "example.com"))
|
||||
fmt.Printf("%-14s: %5t\n", "dot only", agdnet.IsSubdomain(".example.com", "example.com"))
|
||||
fmt.Printf("%-14s: %5t\n", "backwards", agdnet.IsSubdomain("example.com", "sub.example.com"))
|
||||
fmt.Printf("%-14s: %5t\n", "other domain", agdnet.IsSubdomain("sub.example.com", "example.org"))
|
||||
fmt.Printf("%-14s: %5t\n", "similar 1", agdnet.IsSubdomain("sub.myexample.com", "example.org"))
|
||||
fmt.Printf("%-14s: %5t\n", "similar 2", agdnet.IsSubdomain("sub.example.com", "myexample.org"))
|
||||
|
||||
// Output:
|
||||
// same domain : true
|
||||
// not immediate : true
|
||||
// empty : false
|
||||
// same : false
|
||||
// dot only : false
|
||||
// backwards : false
|
||||
// other domain : false
|
||||
// similar 1 : false
|
||||
// similar 2 : false
|
||||
}
|
||||
|
||||
func ExampleIsImmediateSubdomain() {
|
||||
fmt.Printf("%-14s: %5t\n", "same domain", agdnet.IsImmediateSubdomain("sub.example.com", "example.com"))
|
||||
|
||||
fmt.Printf("%-14s: %5t\n", "empty", agdnet.IsImmediateSubdomain("", ""))
|
||||
fmt.Printf("%-14s: %5t\n", "same", agdnet.IsImmediateSubdomain("example.com", "example.com"))
|
||||
fmt.Printf("%-14s: %5t\n", "dot only", agdnet.IsImmediateSubdomain(".example.com", "example.com"))
|
||||
fmt.Printf("%-14s: %5t\n", "backwards", agdnet.IsImmediateSubdomain("example.com", "sub.example.com"))
|
||||
fmt.Printf("%-14s: %5t\n", "other domain", agdnet.IsImmediateSubdomain("sub.example.com", "example.org"))
|
||||
fmt.Printf("%-14s: %5t\n", "not immediate", agdnet.IsImmediateSubdomain("subsub.sub.example.com", "example.com"))
|
||||
fmt.Printf("%-14s: %5t\n", "similar 1", agdnet.IsSubdomain("sub.myexample.com", "example.org"))
|
||||
fmt.Printf("%-14s: %5t\n", "similar 2", agdnet.IsSubdomain("sub.example.com", "myexample.org"))
|
||||
|
||||
// Output:
|
||||
// same domain : true
|
||||
// empty : false
|
||||
// same : false
|
||||
// dot only : false
|
||||
// backwards : false
|
||||
// other domain : false
|
||||
// not immediate : false
|
||||
// similar 1 : false
|
||||
// similar 2 : false
|
||||
}
|
||||
|
||||
func ExampleZeroSubnet() {
|
||||
fmt.Printf("%-5s: %9s\n", "ipv4", agdnet.ZeroSubnet(agdnet.AddrFamilyIPv4))
|
||||
fmt.Printf("%-5s: %9s\n", "ipv6", agdnet.ZeroSubnet(agdnet.AddrFamilyIPv6))
|
||||
|
||||
func() {
|
||||
defer func() { fmt.Println(recover()) }()
|
||||
|
||||
_ = agdnet.ZeroSubnet(42)
|
||||
}()
|
||||
|
||||
// Output:
|
||||
// ipv4 : 0.0.0.0/0
|
||||
// ipv6 : ::/0
|
||||
// agdnet: unsupported addr fam !bad_addr_fam_42
|
||||
}
|
||||
|
||||
func ExampleIPToAddr() {
|
||||
ip := net.IP{1, 2, 3, 4}
|
||||
addr, err := agdnet.IPToAddr(ip, agdnet.AddrFamilyIPv4)
|
||||
fmt.Println(addr, err)
|
||||
|
||||
addr, err = agdnet.IPToAddr(ip, agdnet.AddrFamilyIPv6)
|
||||
fmt.Println(addr, err)
|
||||
|
||||
addr, err = agdnet.IPToAddr(nil, agdnet.AddrFamilyIPv4)
|
||||
fmt.Println(addr, err)
|
||||
|
||||
// Output:
|
||||
// 1.2.3.4 <nil>
|
||||
// ::ffff:1.2.3.4 <nil>
|
||||
// invalid IP bad ipv4 net.IP <nil>
|
||||
}
|
||||
|
||||
func ExampleIPToAddrNoMapped() {
|
||||
addr, err := agdnet.IPToAddrNoMapped(net.IP{1, 2, 3, 4})
|
||||
fmt.Println(addr, err)
|
||||
|
||||
addrMapped, err := agdnet.IPToAddrNoMapped(net.IPv4(1, 2, 3, 4))
|
||||
fmt.Println(addr, err)
|
||||
fmt.Printf("%s == %s is %t\n", addr, addrMapped, addr == addrMapped)
|
||||
|
||||
addr, err = agdnet.IPToAddrNoMapped(net.ParseIP("1234::cdef"))
|
||||
fmt.Println(addr, err)
|
||||
|
||||
// Output:
|
||||
// 1.2.3.4 <nil>
|
||||
// 1.2.3.4 <nil>
|
||||
// 1.2.3.4 == 1.2.3.4 is true
|
||||
// 1234::cdef <nil>
|
||||
}
|
||||
|
||||
func ExampleIPNetToPrefix() {
|
||||
prefix, err := agdnet.IPNetToPrefix(nil, agdnet.AddrFamilyIPv4)
|
||||
fmt.Println(prefix, err)
|
||||
|
||||
prefix, err = agdnet.IPNetToPrefix(&net.IPNet{
|
||||
IP: nil,
|
||||
}, agdnet.AddrFamilyIPv4)
|
||||
fmt.Println(prefix, err)
|
||||
|
||||
prefix, err = agdnet.IPNetToPrefix(&net.IPNet{
|
||||
IP: net.IP{1, 2, 3, 0},
|
||||
Mask: net.CIDRMask(64, netutil.IPv6BitLen),
|
||||
}, agdnet.AddrFamilyIPv4)
|
||||
fmt.Println(prefix, err)
|
||||
|
||||
prefix, err = agdnet.IPNetToPrefix(&net.IPNet{
|
||||
IP: net.IP{1, 2, 3, 0},
|
||||
Mask: net.CIDRMask(24, netutil.IPv4BitLen),
|
||||
}, agdnet.AddrFamilyIPv4)
|
||||
fmt.Println(prefix, err)
|
||||
|
||||
// Output:
|
||||
// invalid Prefix <nil>
|
||||
// invalid Prefix bad ip for subnet <nil>: bad ipv4 net.IP <nil>
|
||||
// invalid Prefix bad subnet 1.2.3.0/0
|
||||
// 1.2.3.0/24 <nil>
|
||||
}
|
||||
|
||||
func ExampleIPNetToPrefixNoMapped() {
|
||||
prefix, err := agdnet.IPNetToPrefixNoMapped(&net.IPNet{
|
||||
IP: net.IP{1, 2, 3, 0},
|
||||
Mask: net.CIDRMask(24, netutil.IPv4BitLen),
|
||||
})
|
||||
fmt.Println(prefix, err)
|
||||
|
||||
prefixMapped, err := agdnet.IPNetToPrefixNoMapped(&net.IPNet{
|
||||
IP: net.IPv4(1, 2, 3, 0),
|
||||
Mask: net.CIDRMask(24, netutil.IPv4BitLen),
|
||||
})
|
||||
fmt.Println(prefix, err)
|
||||
fmt.Printf("%s == %s is %t\n", prefix, prefixMapped, prefix == prefixMapped)
|
||||
|
||||
prefix, err = agdnet.IPNetToPrefixNoMapped(&net.IPNet{
|
||||
IP: net.ParseIP("1234::cdef"),
|
||||
Mask: net.CIDRMask(64, netutil.IPv6BitLen),
|
||||
})
|
||||
fmt.Println(prefix, err)
|
||||
|
||||
// Output:
|
||||
// 1.2.3.0/24 <nil>
|
||||
// 1.2.3.0/24 <nil>
|
||||
// 1.2.3.0/24 == 1.2.3.0/24 is true
|
||||
// 1234::cdef/64 <nil>
|
||||
}
|
18
internal/agdtest/agdtest.go
Normal file
18
internal/agdtest/agdtest.go
Normal file
@ -0,0 +1,18 @@
|
||||
// Package agdtest contains simple mocks for common interfaces and other test
|
||||
// utilities.
|
||||
package agdtest
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// DiscardLogOutput runs tests with discarded logger's output.
|
||||
func DiscardLogOutput(m *testing.M) {
|
||||
log.SetOutput(io.Discard)
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
381
internal/agdtest/interface.go
Normal file
381
internal/agdtest/interface.go
Normal file
@ -0,0 +1,381 @@
|
||||
package agdtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdnet"
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/billstat"
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnscheck"
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsdb"
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/ratelimit"
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/querylog"
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/rulestat"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Interface Mocks
|
||||
//
|
||||
// Keep entities in this file in alphabetic order.
|
||||
|
||||
// agd.ErrorCollector
|
||||
|
||||
// type check
|
||||
var _ agd.ErrorCollector = (*ErrorCollector)(nil)
|
||||
|
||||
// ErrorCollector is an agd.ErrorCollector for tests.
|
||||
//
|
||||
// TODO(a.garipov): Actually test the error collection where this is used.
|
||||
type ErrorCollector struct {
|
||||
OnCollect func(ctx context.Context, err error)
|
||||
}
|
||||
|
||||
// Collect implements the agd.ErrorCollector interface for *ErrorCollector.
|
||||
func (c *ErrorCollector) Collect(ctx context.Context, err error) {
|
||||
c.OnCollect(ctx, err)
|
||||
}
|
||||
|
||||
// agd.ProfileDB
|
||||
|
||||
// type check
|
||||
var _ agd.ProfileDB = (*ProfileDB)(nil)
|
||||
|
||||
// ProfileDB is an agd.ProfileDB for tests.
|
||||
type ProfileDB struct {
|
||||
OnProfileByDeviceID func(
|
||||
ctx context.Context,
|
||||
id agd.DeviceID,
|
||||
) (p *agd.Profile, d *agd.Device, err error)
|
||||
OnProfileByIP func(
|
||||
ctx context.Context,
|
||||
ip netip.Addr,
|
||||
) (p *agd.Profile, d *agd.Device, err error)
|
||||
}
|
||||
|
||||
// ProfileByDeviceID implements the agd.ProfileDB interface for *ProfileDB.
|
||||
func (db *ProfileDB) ProfileByDeviceID(
|
||||
ctx context.Context,
|
||||
id agd.DeviceID,
|
||||
) (p *agd.Profile, d *agd.Device, err error) {
|
||||
return db.OnProfileByDeviceID(ctx, id)
|
||||
}
|
||||
|
||||
// ProfileByIP implements the agd.ProfileDB interface for *ProfileDB.
|
||||
func (db *ProfileDB) ProfileByIP(
|
||||
ctx context.Context,
|
||||
ip netip.Addr,
|
||||
) (p *agd.Profile, d *agd.Device, err error) {
|
||||
return db.OnProfileByIP(ctx, ip)
|
||||
}
|
||||
|
||||
// agd.ProfileStorage
|
||||
|
||||
// type check
|
||||
var _ agd.ProfileStorage = (*ProfileStorage)(nil)
|
||||
|
||||
// ProfileStorage is a agd.ProfileStorage for tests.
|
||||
type ProfileStorage struct {
|
||||
OnProfiles func(
|
||||
ctx context.Context,
|
||||
req *agd.PSProfilesRequest,
|
||||
) (resp *agd.PSProfilesResponse, err error)
|
||||
}
|
||||
|
||||
// Profiles implements the agd.ProfileStorage interface for *ProfileStorage.
|
||||
func (ds *ProfileStorage) Profiles(
|
||||
ctx context.Context,
|
||||
req *agd.PSProfilesRequest,
|
||||
) (resp *agd.PSProfilesResponse, err error) {
|
||||
return ds.OnProfiles(ctx, req)
|
||||
}
|
||||
|
||||
// agd.Refresher
|
||||
|
||||
// type check
|
||||
var _ agd.Refresher = (*Refresher)(nil)
|
||||
|
||||
// Refresher is an agd.Refresher for tests.
|
||||
type Refresher struct {
|
||||
OnRefresh func(ctx context.Context) (err error)
|
||||
}
|
||||
|
||||
// Refresh implements the agd.Refresher interface for *Refresher.
|
||||
func (r *Refresher) Refresh(ctx context.Context) (err error) {
|
||||
return r.OnRefresh(ctx)
|
||||
}
|
||||
|
||||
// agd.Resolver
|
||||
|
||||
// type check
|
||||
var _ agd.Resolver = (*Resolver)(nil)
|
||||
|
||||
// Resolver is an agd.Resolver for tests.
|
||||
type Resolver struct {
|
||||
OnLookupIP func(ctx context.Context, network, addr string) (ips []net.IP, err error)
|
||||
}
|
||||
|
||||
// LookupIP implements the agd.Resolver interface for *Resolver.
|
||||
func (r *Resolver) LookupIP(
|
||||
ctx context.Context,
|
||||
network string,
|
||||
addr string,
|
||||
) (ips []net.IP, err error) {
|
||||
return r.OnLookupIP(ctx, network, addr)
|
||||
}
|
||||
|
||||
// Package BillStat
|
||||
|
||||
// billstat.Recorder
|
||||
|
||||
// type check
|
||||
var _ billstat.Recorder = (*BillStatRecorder)(nil)
|
||||
|
||||
// BillStatRecorder is a billstat.Recorder for tests.
|
||||
type BillStatRecorder struct {
|
||||
OnRecord func(
|
||||
ctx context.Context,
|
||||
id agd.DeviceID,
|
||||
ctry agd.Country,
|
||||
asn agd.ASN,
|
||||
start time.Time,
|
||||
proto agd.Protocol,
|
||||
)
|
||||
}
|
||||
|
||||
// Record implements the billstat.Recorder interface for *BillStatRecorder.
|
||||
func (r *BillStatRecorder) Record(
|
||||
ctx context.Context,
|
||||
id agd.DeviceID,
|
||||
ctry agd.Country,
|
||||
asn agd.ASN,
|
||||
start time.Time,
|
||||
proto agd.Protocol,
|
||||
) {
|
||||
r.OnRecord(ctx, id, ctry, asn, start, proto)
|
||||
}
|
||||
|
||||
// billstat.Uploader
|
||||
|
||||
// type check
|
||||
var _ billstat.Uploader = (*BillStatUploader)(nil)
|
||||
|
||||
// BillStatUploader is a billstat.Uploader for tests.
|
||||
type BillStatUploader struct {
|
||||
OnUpload func(ctx context.Context, records billstat.Records) (err error)
|
||||
}
|
||||
|
||||
// Upload implements the billstat.Uploader interface for *BillStatUploader.
|
||||
func (b *BillStatUploader) Upload(ctx context.Context, records billstat.Records) (err error) {
|
||||
return b.OnUpload(ctx, records)
|
||||
}
|
||||
|
||||
// Package DNSCheck
|
||||
|
||||
// dnscheck.Interface
|
||||
|
||||
// type check
|
||||
var _ dnscheck.Interface = (*DNSCheck)(nil)
|
||||
|
||||
// DNSCheck is a dnscheck.Interface for tests.
|
||||
type DNSCheck struct {
|
||||
OnCheck func(ctx context.Context, req *dns.Msg, ri *agd.RequestInfo) (reqp *dns.Msg, err error)
|
||||
}
|
||||
|
||||
// Check implements the dnscheck.Interface interface for *DNSCheck.
|
||||
func (db *DNSCheck) Check(
|
||||
ctx context.Context,
|
||||
req *dns.Msg,
|
||||
ri *agd.RequestInfo,
|
||||
) (resp *dns.Msg, err error) {
|
||||
return db.OnCheck(ctx, req, ri)
|
||||
}
|
||||
|
||||
// Package DNSDB
|
||||
|
||||
// dnsdb.Interface
|
||||
|
||||
// type check
|
||||
var _ dnsdb.Interface = (*DNSDB)(nil)
|
||||
|
||||
// DNSDB is a dnsdb.Interface for tests.
|
||||
type DNSDB struct {
|
||||
OnRecord func(ctx context.Context, resp *dns.Msg, ri *agd.RequestInfo)
|
||||
}
|
||||
|
||||
// Record implements the dnsdb.Interface interface for *DNSDB.
|
||||
func (db *DNSDB) Record(ctx context.Context, resp *dns.Msg, ri *agd.RequestInfo) {
|
||||
db.OnRecord(ctx, resp, ri)
|
||||
}
|
||||
|
||||
// Package Filter
|
||||
|
||||
// filter.Interface
|
||||
|
||||
// type check
|
||||
var _ filter.Interface = (*Filter)(nil)
|
||||
|
||||
// Filter is a filter.Interface for tests.
|
||||
type Filter struct {
|
||||
OnFilterRequest func(
|
||||
ctx context.Context,
|
||||
req *dns.Msg,
|
||||
ri *agd.RequestInfo,
|
||||
) (r filter.Result, err error)
|
||||
OnFilterResponse func(
|
||||
ctx context.Context,
|
||||
resp *dns.Msg,
|
||||
ri *agd.RequestInfo,
|
||||
) (r filter.Result, err error)
|
||||
OnClose func() (err error)
|
||||
}
|
||||
|
||||
// FilterRequest implements the filter.Interface interface for *Filter.
|
||||
func (f *Filter) FilterRequest(
|
||||
ctx context.Context,
|
||||
req *dns.Msg,
|
||||
ri *agd.RequestInfo,
|
||||
) (r filter.Result, err error) {
|
||||
return f.OnFilterRequest(ctx, req, ri)
|
||||
}
|
||||
|
||||
// FilterResponse implements the filter.Interface interface for *Filter.
|
||||
func (f *Filter) FilterResponse(
|
||||
ctx context.Context,
|
||||
resp *dns.Msg,
|
||||
ri *agd.RequestInfo,
|
||||
) (r filter.Result, err error) {
|
||||
return f.OnFilterResponse(ctx, resp, ri)
|
||||
}
|
||||
|
||||
// Close implements the filter.Interface interface for *Filter.
|
||||
func (f *Filter) Close() (err error) {
|
||||
return f.OnClose()
|
||||
}
|
||||
|
||||
// filter.Storage
|
||||
|
||||
// type check
|
||||
var _ filter.Storage = (*FilterStorage)(nil)
|
||||
|
||||
// FilterStorage is an filter.Storage for tests.
|
||||
type FilterStorage struct {
|
||||
OnFilterFromContext func(ctx context.Context, ri *agd.RequestInfo) (f filter.Interface)
|
||||
OnHasListID func(id agd.FilterListID) (ok bool)
|
||||
}
|
||||
|
||||
// FilterFromContext implements the filter.Storage interface for *FilterStorage.
|
||||
func (s *FilterStorage) FilterFromContext(
|
||||
ctx context.Context,
|
||||
ri *agd.RequestInfo,
|
||||
) (f filter.Interface) {
|
||||
return s.OnFilterFromContext(ctx, ri)
|
||||
}
|
||||
|
||||
// HasListID implements the filter.Storage interface for *FilterStorage.
|
||||
func (s *FilterStorage) HasListID(id agd.FilterListID) (ok bool) {
|
||||
return s.OnHasListID(id)
|
||||
}
|
||||
|
||||
// Package GeoIP
|
||||
|
||||
// geoip.Interface
|
||||
|
||||
// type check
|
||||
var _ geoip.Interface = (*GeoIP)(nil)
|
||||
|
||||
// GeoIP is a geoip.Interface for tests.
|
||||
type GeoIP struct {
|
||||
OnSubnetByLocation func(
|
||||
c agd.Country,
|
||||
a agd.ASN,
|
||||
fam agdnet.AddrFamily,
|
||||
) (n netip.Prefix, err error)
|
||||
OnData func(host string, ip netip.Addr) (l *agd.Location, err error)
|
||||
}
|
||||
|
||||
// SubnetByLocation implements the geoip.Interface interface for *GeoIP.
|
||||
func (g *GeoIP) SubnetByLocation(
|
||||
c agd.Country,
|
||||
a agd.ASN,
|
||||
fam agdnet.AddrFamily,
|
||||
) (n netip.Prefix, err error) {
|
||||
return g.OnSubnetByLocation(c, a, fam)
|
||||
}
|
||||
|
||||
// Data implements the geoip.Interface interface for *GeoIP.
|
||||
func (g *GeoIP) Data(host string, ip netip.Addr) (l *agd.Location, err error) {
|
||||
return g.OnData(host, ip)
|
||||
}
|
||||
|
||||
// Package Querylog
|
||||
|
||||
// querylog.Interface
|
||||
|
||||
// type check
|
||||
var _ querylog.Interface = (*QueryLog)(nil)
|
||||
|
||||
// QueryLog is a querylog.Interface for tests.
|
||||
type QueryLog struct {
|
||||
OnWrite func(ctx context.Context, e *querylog.Entry) (err error)
|
||||
}
|
||||
|
||||
// Write implements the querylog.Interface interface for *QueryLog.
|
||||
func (ql *QueryLog) Write(ctx context.Context, e *querylog.Entry) (err error) {
|
||||
return ql.OnWrite(ctx, e)
|
||||
}
|
||||
|
||||
// Package RuleStat
|
||||
|
||||
// rulestat.Interface
|
||||
|
||||
// type check
|
||||
var _ rulestat.Interface = (*RuleStat)(nil)
|
||||
|
||||
// RuleStat is a rulestat.Interface for tests.
|
||||
type RuleStat struct {
|
||||
OnCollect func(ctx context.Context, id agd.FilterListID, text agd.FilterRuleText)
|
||||
}
|
||||
|
||||
// Collect implements the rulestat.Interface interface for *RuleStat.
|
||||
func (s *RuleStat) Collect(ctx context.Context, id agd.FilterListID, text agd.FilterRuleText) {
|
||||
s.OnCollect(ctx, id, text)
|
||||
}
|
||||
|
||||
// Module DNSServer
|
||||
|
||||
// Package RateLimit
|
||||
|
||||
// ratelimit.RateLimit
|
||||
|
||||
// type check
|
||||
var _ ratelimit.Interface = (*RateLimit)(nil)
|
||||
|
||||
// RateLimit is a ratelimit.Interface for tests.
|
||||
type RateLimit struct {
|
||||
OnIsRateLimited func(
|
||||
ctx context.Context,
|
||||
req *dns.Msg,
|
||||
ip netip.Addr,
|
||||
) (drop, allowlisted bool, err error)
|
||||
OnCountResponses func(ctx context.Context, resp *dns.Msg, ip netip.Addr)
|
||||
}
|
||||
|
||||
// IsRateLimited implements the ratelimit.Interface interface for *RateLimit.
|
||||
func (l *RateLimit) IsRateLimited(
|
||||
ctx context.Context,
|
||||
req *dns.Msg,
|
||||
ip netip.Addr,
|
||||
) (drop, allowlisted bool, err error) {
|
||||
return l.OnIsRateLimited(ctx, req, ip)
|
||||
}
|
||||
|
||||
// CountResponses implements the ratelimit.Interface interface for
|
||||
// *RateLimit.
|
||||
func (l *RateLimit) CountResponses(ctx context.Context, req *dns.Msg, ip netip.Addr) {
|
||||
l.OnCountResponses(ctx, req, ip)
|
||||
}
|
54
internal/agdtest/sync.go
Normal file
54
internal/agdtest/sync.go
Normal file
@ -0,0 +1,54 @@
|
||||
package agdtest
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Synchronization Utilities
|
||||
//
|
||||
// TODO(a.garipov): Add generic versions when we can.
|
||||
//
|
||||
// TODO(a.garipov): Add to golibs once the API is stabilized.
|
||||
|
||||
// Signal is a simple signal type alias for tests.
|
||||
type Signal = struct{}
|
||||
|
||||
// RequireSend waits until a signal is sent to ch or until the timeout is
|
||||
// reached. If the timeout is reached, the test is failed.
|
||||
func RequireSend(t require.TestingT, ch chan<- Signal, timeout time.Duration) {
|
||||
if h, ok := t.(interface{ Helper() }); ok {
|
||||
h.Helper()
|
||||
}
|
||||
|
||||
timer := time.NewTimer(timeout)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case ch <- Signal{}:
|
||||
// Go on.
|
||||
case <-timer.C:
|
||||
t.Errorf("did not send after %s", timeout)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireReceive waits until a signal is received from ch or until the timeout
|
||||
// is reached. If the timeout is reached, the test is failed.
|
||||
func RequireReceive(t require.TestingT, ch <-chan Signal, timeout time.Duration) {
|
||||
if h, ok := t.(interface{ Helper() }); ok {
|
||||
h.Helper()
|
||||
}
|
||||
|
||||
timer := time.NewTimer(timeout)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
// Go on.
|
||||
case <-timer.C:
|
||||
t.Errorf("did not receive after %s", timeout)
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
11
internal/backend/backend.go
Normal file
11
internal/backend/backend.go
Normal file
@ -0,0 +1,11 @@
|
||||
// Package backend contains implementations of several interfaces that send or
|
||||
// receive information to or from the business logic backend service.
|
||||
package backend
|
||||
|
||||
// Common Constants, Types, And Utilities
|
||||
|
||||
// Path constants.
|
||||
const (
|
||||
PathDNSAPIV1DevicesActivity = "/dns_api/v1/devices_activity"
|
||||
PathDNSAPIV1Settings = "/dns_api/v1/settings"
|
||||
)
|
11
internal/backend/backend_test.go
Normal file
11
internal/backend/backend_test.go
Normal file
@ -0,0 +1,11 @@
|
||||
package backend_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
agdtest.DiscardLogOutput(m)
|
||||
}
|
135
internal/backend/billstat.go
Normal file
135
internal/backend/billstat.go
Normal file
@ -0,0 +1,135 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
|
||||
"github.com/AdguardTeam/AdGuardDNS/internal/billstat"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// Billing Statistics Uploader
|
||||
|
||||
// BillStatConfig is the configuration structure for the business logic backend
|
||||
// billing statistics uploader.
|
||||
type BillStatConfig struct {
|
||||
// BaseEndpoint is the base URL to which API paths are appended.
|
||||
BaseEndpoint *url.URL
|
||||
}
|
||||
|
||||
// NewBillStat creates a new billing statistics uploader. c must not be nil.
|
||||
func NewBillStat(c *BillStatConfig) (b *BillStat) {
|
||||
return &BillStat{
|
||||
apiURL: c.BaseEndpoint.JoinPath(PathDNSAPIV1DevicesActivity),
|
||||
// Assume that the timeouts are handled by the context in Upload.
|
||||
http: agdhttp.NewClient(&agdhttp.ClientConfig{}),
|
||||
}
|
||||
}
|
||||
|
||||
// BillStat is the implementation of the [billstat.Uploader] interface that
|
||||
// uploads the billing statistics to the business logic backend. It is safe for
|
||||
// concurrent use.
|
||||
//
|
||||
// TODO(a.garipov): Consider uniting with [ProfileStorage] into a single
|
||||
// backend.Client.
|
||||
type BillStat struct {
|
||||
apiURL *url.URL
|
||||
http *agdhttp.Client
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ billstat.Uploader = (*BillStat)(nil)
|
||||
|
||||
// Upload implements the [billstat.Uploader] interface for *BillStat.
|
||||
func (b *BillStat) Upload(ctx context.Context, records billstat.Records) (err error) {
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
req := &v1DevicesActivityReq{
|
||||
Devices: billStatRecsToReq(records),
|
||||
}
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding billstat req: %w", err)
|
||||
}
|
||||
|
||||
reqURL := b.apiURL.Redacted()
|
||||
resp, err := b.http.Post(ctx, b.apiURL, agdhttp.HdrValApplicationJSON, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("sending to %s: %w", reqURL, err)
|
||||
}
|
||||
defer func() { err = errors.WithDeferred(err, resp.Body.Close()) }()
|
||||
|
||||
err = agdhttp.CheckStatus(resp, http.StatusOK)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// v1DevicesActivityReq is a request to the devices activity HTTP API.
|
||||
type v1DevicesActivityReq struct {
|
||||
Devices []*v1DevicesActivityReqDevice `json:"devices"`
|
||||
}
|
||||
|
||||
// v1DevicesActivityReqDevice is a single device within a request to the devices
|
||||
// activity HTTP API.
|
||||
type v1DevicesActivityReqDevice struct {
|
||||
// ClientCountry is the detected country of the client's IP address, if any.
|
||||
ClientCountry agd.Country `json:"client_country"`
|
||||
|
||||
// DeviceID is the ID of the device.
|
||||
DeviceID agd.DeviceID `json:"device_id"`
|
||||
|
||||
// Time is the time of the most recent query from the device, in Unix time
|
||||
// in milliseconds.
|
||||
Time int64 `json:"time_ms"`
|
||||
|
||||
// ASN is the detected ASN of the client's IP address, if any.
|
||||
ASN agd.ASN `json:"asn"`
|
||||
|
||||
// Queries is the total number of Queries the device has performed since the
|
||||
// most recent sync. This value is an int32 to be in sync with the business
|
||||
// logic backend which uses this type. Change it if it is changed there.
|
||||
Queries int32 `json:"queries"`
|
||||
|
||||
// Proto is the numeric value of the DNS protocol of the most recent query
|
||||
// from the device. It is a uint8 and not an agd.Protocol to make sure that
|
||||
// it always remains numeric even if we implement json.Marshal on
|
||||
// agd.Protocol in the future.
|
||||
Proto uint8 `json:"proto"`
|
||||
}
|
||||
|
||||
// billStatRecsToReq converts billing statistics records into devices for the
|
||||
// devices activity HTTP API.
|
||||
func billStatRecsToReq(records billstat.Records) (devices []*v1DevicesActivityReqDevice) {
|
||||
// Sort the keys to make the queries reproducible and testable.
|
||||
deviceIDs := maps.Keys(records)
|
||||
slices.Sort(deviceIDs)
|
||||
|
||||
devices = make([]*v1DevicesActivityReqDevice, 0, len(deviceIDs))
|
||||
for _, id := range deviceIDs {
|
||||
rec := records[id]
|
||||
devices = append(devices, &v1DevicesActivityReqDevice{
|
||||
ClientCountry: rec.Country,
|
||||
DeviceID: id,
|
||||
Time: rec.Time.UnixMilli(),
|
||||
ASN: rec.ASN,
|
||||
Queries: rec.Queries,
|
||||
Proto: uint8(rec.Proto),
|
||||
})
|
||||
}
|
||||
|
||||
return devices
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user