Sync v2.0

This commit is contained in:
Andrey Meshkov 2022-08-26 14:18:35 +03:00
parent 4943bdc429
commit b6a98906a5
2171 changed files with 45308 additions and 544284 deletions

27
.gitignore vendored
View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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>.

View File

@ -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
View File

@ -0,0 +1 @@
See Adguard Home [`HACKING.md`](https://github.com/AdguardTeam/AdGuardHome/blob/master/HACKING.md).

70
Makefile Normal file
View 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
View File

@ -1,98 +1,99 @@
&nbsp;
<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? Whats 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.

View File

@ -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
View 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'

View File

@ -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.

View File

@ -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
}

View File

@ -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())
}

View File

@ -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
})
}

View File

@ -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.",
})
)

View File

@ -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)
}

View File

@ -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
})
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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
})
}

View File

@ -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
}

View File

@ -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")
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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.

View File

@ -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",
}

View File

@ -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
}

View File

@ -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")))
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
# AdGuard DNS Prometheus Metrics
**TODO(a.garipov):** Describe the metrics.

272
doc/querylog.md Normal file
View 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
View File

@ -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

721
go.sum

File diff suppressed because it is too large Load Diff

6
go.work Normal file
View File

@ -0,0 +1,6 @@
go 1.19
use (
.
./internal/dnsserver
)

165
go.work.sum Normal file
View 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=

View File

@ -1,5 +0,0 @@
# health
Fork of https://github.com/coredns/coredns/tree/master/plugin/health
In order to change the URL.

View File

@ -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
}

View File

@ -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.",
})
)

View File

@ -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
}

View File

@ -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.

View File

@ -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
}

View File

@ -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())
}

View File

@ -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
}

View File

@ -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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View 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
View 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
}

View 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
View 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
View 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)
}

View 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
View 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

View 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
View 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
View 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
View 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
}

View 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
View 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
}

View 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
View 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)
}

View 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
View 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
View 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
View 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
View 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
}

View 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())
}

View 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
View 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
View 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),
}
}

View 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
View 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)
}

View 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
View 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,
}
}

View 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
View 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
}

View 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>
}

View 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())
}

View 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
View 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()
}
}

View 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"
)

View File

@ -0,0 +1,11 @@
package backend_test
import (
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
)
func TestMain(m *testing.M) {
agdtest.DiscardLogOutput(m)
}

View 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