Sync v2.9.0

This commit is contained in:
Andrey Meshkov 2024-10-14 17:44:24 +03:00
parent 41f7e6cb22
commit da0cb6fd0e
323 changed files with 16294 additions and 13013 deletions

5
.gitignore vendored
View File

@ -10,11 +10,12 @@
*.test *.test
/bin/ /bin/
/filters/ /filters/
/test/
/github-mirror/ /github-mirror/
/test/
AdGuardDNS AdGuardDNS
agdns
asn.mmdb asn.mmdb
config.yaml config.yaml
country.mmdb country.mmdb
querylog.jsonl
profilecache.json profilecache.json
querylog.jsonl

25
.markdownlint.json Normal file
View File

@ -0,0 +1,25 @@
{
"ul-indent": {
"indent": 4
},
"ul-style": {
"style": "dash"
},
"emphasis-style": {
"style": "asterisk"
},
"no-duplicate-heading": {
"siblings_only": true
},
"no-inline-html": {
"allowed_elements": [
"a"
]
},
"no-trailing-spaces": {
"br_spaces": 0
},
"line-length": false,
"no-bare-urls": false,
"link-fragments": false
}

View File

@ -7,6 +7,88 @@ The format is **not** based on [Keep a Changelog][kec], since the project **does
[kec]: https://keepachangelog.com/en/1.0.0/ [kec]: https://keepachangelog.com/en/1.0.0/
[sem]: https://semver.org/spec/v2.0.0.html [sem]: https://semver.org/spec/v2.0.0.html
## AGDNS-2468 / Build 869
- The environment variable `PROFILES_MAX_RESP_SIZE` has been added. It sets the maximum size of the response from the profiles endpoint of the backend API. The default value is `8MB`.
## AGDNS-2427 / Build 854
- The environment variables `REDIS_ADDR`, `REDIS_KEY_PREFIX`, `REDIS_MAX_ACTIVE`, `REDIS_MAX_IDLE`, `REDIS_IDLE_TIMEOUT`, and `REDIS_PORT` have been added.
- The property `ttl` within the `check` is replaced by the object `kv` containing the previous `ttl` and the new property `type`. So replace this:
```yaml
check:
# …
ttl: 30s
```
with this:
```yaml
check:
kv:
type: 'consul'
ttl: 30s
# …
```
## AGDNS-2331 / Build 818
- Profile's file cache version was incremented. The new field `RateLimit` has been added to profile's object.
## AGDNS-2008 / Build 809
- The environment variables `WEB_STATIC_DIR` and `WEB_STATIC_DIR_ENABLED` have been added. If `WEB_STATIC_DIR_ENABLED` is set to `1`, `WEB_STATIC_DIR` must point to a directory, from which static files are served. The `web.static_content` property in the configuration file is also ignored when `WEB_STATIC_DIR_ENABLED` is set to `1`.
## AGDNS-2316 / Build 808
- The environment variables `BLOCKED_SERVICE_ENABLED`, `GENERAL_SAFE_SEARCH_ENABLED`, and `YOUTUBE_SAFE_SEARCH_ENABLED` have been added. If they are set to `0`, their corresponding `*_URL` environment variables can be empty.
## AGDNS-2312 / Build 807
- The environment variables `BILLSTAT_URL` and `PROFILES_URL` no longer required if there are no server groups with profiles enabled.
## AGDNS-2312 / Build 802
- The environment variables `ADULT_BLOCKING_ENABLED`, `NEW_REG_DOMAINS_ENABLED`, and `SAFE_BROWSING_ENABLED` have been added. If they are set to `0`, their corresponding `*_URL` environment variables can be empty.
## AGDNS-2302 / Build 801
- The environment variable `METRICS_NAMESPACE` has been added.
## AGDNS-2292 / Build 794
- The environment variable `PROFILES_ENABLED` has been removed.
- The objects within the `server_groups` array have a new property `profiles_enabled`. So replace this:
```yaml
server_groups:
- name: 'default'
# …
- name: 'client'
# …
```
with this:
```yaml
server_groups:
- name: 'default'
# …
profiles_enabled: false
- name: 'client'
# …
profiles_enabled: true
```
## AGDNS-2289 / Build 793
- The environment variable `FILTER_INDEX_URL` now accepts `file://` URIs to use local files as filtering-rule list indexes.
- All other `*_URL` environment variables are now validated to be HTTP(s) or gRPC(S) more strictly.
## AGDNS-2254 / Build 779 ## AGDNS-2254 / Build 779
- The environment variables `BILLSTAT_API_KEY` and `PROFILES_API_KEY` have been added. - The environment variables `BILLSTAT_API_KEY` and `PROFILES_API_KEY` have been added.
@ -21,11 +103,11 @@ The format is **not** based on [Keep a Changelog][kec], since the project **does
## AGDNS-2022 / Build 746 ## AGDNS-2022 / Build 746
- The property `block_page_redirect` of objects within `server_groups` array has been removed. - The property `block_page_redirect` of objects within the `server_groups` array has been removed.
## AGDNS-1981 / Build 744 ## AGDNS-1981 / Build 744
- The objects within `server_groups` array had a change in their `block_page_redirect` configuration, it now supports arrays of IP addresses in `ipv4` and `ipv6` fields. - The objects within the `server_groups` array had a change in their `block_page_redirect` configuration, it now supports arrays of IP addresses in `ipv4` and `ipv6` fields.
- Profile's file cache version was incremented. In case of `BlockingModeCustomIP` the `profile.blocking_mode` IPv4/IPv6 fields are now arrays of IP addresses. - Profile's file cache version was incremented. In case of `BlockingModeCustomIP` the `profile.blocking_mode` IPv4/IPv6 fields are now arrays of IP addresses.
@ -83,7 +165,7 @@ The format is **not** based on [Keep a Changelog][kec], since the project **does
## AGDNS-1954 / Build 719 ## AGDNS-1954 / Build 719
- The objects within `server_groups` array have a new property `block_page_redirect`: - The objects within the `server_groups` array have a new property `block_page_redirect`:
```yaml ```yaml
block_page_redirect: block_page_redirect:
@ -557,7 +639,7 @@ The format is **not** based on [Keep a Changelog][kec], since the project **does
This means that DNSDB is disabled by default. This means that DNSDB is disabled by default.
- The default configuration file path has been changed from `config.yml` to <code>./config.y<strong>a</strong>ml</code> for consistency with other services. - The default configuration file path has been changed from `./config.yml` to `./config.yaml` for consistency with other services.
## AGDNS-916 / Build 456 ## AGDNS-916 / Build 456
@ -1483,7 +1565,7 @@ The format is **not** based on [Keep a Changelog][kec], since the project **does
## AGDNS-246 / Build 83 ## AGDNS-246 / Build 83
- The new environment variable `RULESTAT_URL` has been added. Its default value is <code></code>, which means that no statistics are gathered. Adjust the value, if necessary. - The new environment variable `RULESTAT_URL` has been added. Its default value is an empty string, which means that no statistics are gathered. Adjust the value, if necessary.
## AGDNS-245 / Build 74 ## AGDNS-245 / Build 74

View File

@ -1 +1,3 @@
# Code guidelines
See the [Adguard Code Guidelines](https://github.com/AdguardTeam/CodeGuidelines/). See the [Adguard Code Guidelines](https://github.com/AdguardTeam/CodeGuidelines/).

View File

@ -8,7 +8,7 @@
# Makefile. Bump this number every time a significant change is made to # Makefile. Bump this number every time a significant change is made to
# this Makefile. # this Makefile.
# #
# AdGuard-Project-Version: 5 # AdGuard-Project-Version: 9
# Don't name these macros "GO" etc., because GNU Make apparently makes # Don't name these macros "GO" etc., because GNU Make apparently makes
# them exported environment variables with the literal value of # them exported environment variables with the literal value of
@ -20,25 +20,33 @@
GO.MACRO = $${GO:-go} GO.MACRO = $${GO:-go}
VERBOSE.MACRO = $${VERBOSE:-0} VERBOSE.MACRO = $${VERBOSE:-0}
BRANCH = $$( git rev-parse --abbrev-ref HEAD ) BRANCH = $${BRANCH:-$$(git rev-parse --abbrev-ref HEAD)}
GOAMD64 = v1 GOAMD64 = v1
GOPROXY = https://proxy.golang.org|direct GOPROXY = https://proxy.golang.org|direct
GOTOOLCHAIN = go1.22.5 GOTELEMETRY = off
GOTOOLCHAIN = go1.23.1
RACE = 0 RACE = 0
REVISION = $$( git rev-parse --short HEAD ) REVISION = $${REVISION:-$$(git rev-parse --short HEAD)}
VERSION = 0 VERSION = 0
ENV = env\ ENV = env \
BRANCH="$(BRANCH)"\ BRANCH="$(BRANCH)" \
GO="$(GO.MACRO)"\ GO="$(GO.MACRO)" \
GOAMD64='$(GOAMD64)'\ GOAMD64='$(GOAMD64)' \
GOPROXY='$(GOPROXY)'\ GOPROXY='$(GOPROXY)' \
GOTOOLCHAIN='$(GOTOOLCHAIN)'\ GOTELEMETRY='$(GOTELEMETRY)' \
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\ GOTOOLCHAIN='$(GOTOOLCHAIN)' \
RACE='$(RACE)'\ PATH="$${PWD}/bin:$$("$(GO.MACRO)" env GOPATH)/bin:$${PATH}" \
REVISION="$(REVISION)"\ RACE='$(RACE)' \
VERBOSE="$(VERBOSE.MACRO)"\ REVISION="$(REVISION)" \
VERSION="$(VERSION)"\ VERBOSE="$(VERBOSE.MACRO)" \
VERSION="$(VERSION)" \
# Keep the line above blank.
ENV_MISC = env \
PATH="$${PWD}/bin:$$("$(GO.MACRO)" env GOPATH)/bin:$${PATH}" \
VERBOSE="$(VERBOSE.MACRO)" \
# Keep the line above blank. # Keep the line above blank.
@ -53,12 +61,12 @@ test: go-test
go-bench: ; $(ENV) "$(SHELL)" ./scripts/make/go-bench.sh go-bench: ; $(ENV) "$(SHELL)" ./scripts/make/go-bench.sh
go-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh go-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh
go-deps: ; $(ENV) "$(SHELL)" ./scripts/make/go-deps.sh go-deps: ; $(ENV) "$(SHELL)" ./scripts/make/go-deps.sh
go-env: ; $(ENV) "$(GO.MACRO)" env
go-fuzz: ; $(ENV) "$(SHELL)" ./scripts/make/go-fuzz.sh go-fuzz: ; $(ENV) "$(SHELL)" ./scripts/make/go-fuzz.sh
go-gen: ; $(ENV) "$(SHELL)" ./scripts/make/go-gen.sh go-gen: ; $(ENV) "$(SHELL)" ./scripts/make/go-gen.sh
go-lint: ; $(ENV) "$(SHELL)" ./scripts/make/go-lint.sh go-lint: ; $(ENV) "$(SHELL)" ./scripts/make/go-lint.sh
go-test: ; $(ENV) RACE='1' "$(SHELL)" ./scripts/make/go-test.sh go-test: ; $(ENV) RACE='1' "$(SHELL)" ./scripts/make/go-test.sh
go-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-tools.sh go-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-tools.sh
go-upd-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-upd-tools.sh go-upd-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-upd-tools.sh
go-check: go-tools go-lint go-test go-check: go-tools go-lint go-test
@ -66,13 +74,18 @@ go-check: go-tools go-lint go-test
# A quick check to make sure that all operating systems relevant to the # A quick check to make sure that all operating systems relevant to the
# development of the project can be typechecked and built successfully. # development of the project can be typechecked and built successfully.
go-os-check: go-os-check:
env GOOS='darwin' "$(GO.MACRO)" vet ./internal/... ./internal/dnsserver/... $(ENV) GOOS='darwin' "$(GO.MACRO)" vet ./internal/... ./internal/dnsserver/...
env GOOS='linux' "$(GO.MACRO)" vet ./internal/... ./internal/dnsserver/... $(ENV) GOOS='linux' "$(GO.MACRO)" vet ./internal/... ./internal/dnsserver/...
# Additionally, check the AdGuard Home OSs in the dnsserver module. # Additionally, check the AdGuard Home OSs in the dnsserver module.
env GOOS='freebsd' "$(GO.MACRO)" vet ./internal/dnsserver/... $(ENV) GOOS='freebsd' "$(GO.MACRO)" vet ./internal/dnsserver/...
env GOOS='openbsd' "$(GO.MACRO)" vet ./internal/dnsserver/... $(ENV) GOOS='openbsd' "$(GO.MACRO)" vet ./internal/dnsserver/...
env GOOS='windows' "$(GO.MACRO)" vet ./internal/dnsserver/... $(ENV) GOOS='windows' "$(GO.MACRO)" vet ./internal/dnsserver/...
txt-lint: ; $(ENV) "$(SHELL)" ./scripts/make/txt-lint.sh txt-lint: ; $(ENV) "$(SHELL)" ./scripts/make/txt-lint.sh
md-lint: ; $(ENV_MISC) "$(SHELL)" ./scripts/make/md-lint.sh
sh-lint: ; $(ENV_MISC) "$(SHELL)" ./scripts/make/sh-lint.sh
# Targets related to AdGuard DNS start here.
sync-github: ; $(ENV) "$(SHELL)" ./scripts/make/github-sync.sh sync-github: ; $(ENV) "$(SHELL)" ./scripts/make/github-sync.sh

View File

@ -1,4 +1,6 @@
# AdGuard DNS # AdGuard DNS
<!-- markdownlint-disable MD033 -->
<br/> <br/>
<div align="center"> <div align="center">
@ -15,85 +17,60 @@
</div> </div>
<br/> <br/>
AdGuard DNS is an alternative solution for tracker blocking, privacy protection, <!-- markdownlint-enable MD033 -->
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 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.
## DNS privacy
## 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 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.
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 On top of that, AdGuard DNS provides no-logs [privacy policy] which means we do not record logs of your browsing activity.
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] which means we do
not record logs of your browsing activity.
[privacy policy]: https://adguard-dns.io/privacy.html [privacy policy]: https://adguard-dns.io/privacy.html
## Basic 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.
## Basic Features - Protection from phishing and hazardous websites and malvertising (malicious ads).
* **Blocking trackers network-wide** with no additional software required. - 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.
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?
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.
### Can AdGuard DNS replace a traditional blocker?
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.
## Personal DNS server ## Personal DNS server
<!-- markdownlint-disable MD033 -->
<figure> <figure>
<img alt="A screenshot of the AdGuard DNS dashboard" src="https://cdn.adguard.com/content/blog/articles/stats_en.png" width="800px"/> <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> <figcaption>AdGuard DNS dashboard</figcaption>
</figure> </figure>
You can sign up for a personal AdGuard DNS account and get access to the <!-- markdownlint-enable MD033 -->
following features:
* Manage devices and their settings in one place. You can sign up for a personal AdGuard DNS account and get access to the following features:
* Manage blocklists that are used to block ads. - Manage devices and their settings in one place.
* View statistics on the DNS queries, companies, countries your devices try to - Manage blocklists that are used to block ads.
connect to.
* You can also maintain your own set of rules in the "User rules" section. - View statistics on the DNS queries, companies, countries your devices try to connect to.
AdGuard DNS provides a flexible [rules system].
* AdGuard DNS also provides an [API] that can be used to integrate with it if - You can also maintain your own set of rules in the "User rules" section. AdGuard DNS provides a flexible [rules system].
you need that.
- AdGuard DNS also provides an [API] that can be used to integrate with it if you need that.
[rules system]: https://adguard-dns.io/kb/general/dns-filtering-syntax/ [rules system]: https://adguard-dns.io/kb/general/dns-filtering-syntax/
[API]: https://adguard-dns.io/kb/private-dns/api/overview/ [API]: https://adguard-dns.io/kb/private-dns/api/overview/
## Software license
Copyright (C) 2022-2024 AdGuard Software Ltd.
## Software License 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.
Copyright (C) 2022-2023 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

@ -166,6 +166,13 @@ geoip:
# DNS checking configuration. # DNS checking configuration.
check: check:
# Domains to use for DNS checking. # Domains to use for DNS checking.
kv:
# Defines the type of remote kay-value storage. Allowed values are
# "consul" and "redis".
type: 'consul'
# For how long to keep the information about the client.
ttl: 30s
# Domains to use for DNS checking.
domains: domains:
- dnscheck.adguard-dns.com - dnscheck.adguard-dns.com
- dnscheck.adguard.com - dnscheck.adguard.com
@ -180,8 +187,6 @@ check:
ipv6: ipv6:
- 1234::cdee - 1234::cdee
- 1234::cdef - 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 # 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 # not matching the static_content map are shown a 404 page. In special
@ -462,6 +467,7 @@ server_groups:
resolver_public: '9327C5E64783E19C339BD6B680A56DB85521CC6E4E0CA5DF5274E2D3CE026C6B' resolver_public: '9327C5E64783E19C339BD6B680A56DB85521CC6E4E0CA5DF5274E2D3CE026C6B'
es_version: 1 es_version: 1
certificate_ttl: 8760h certificate_ttl: 8760h
profiles_enabled: true
# Connectivity check configuration. # Connectivity check configuration.
connectivity_check: connectivity_check:

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,6 @@
# AdGuard DNS Query Debugging API # AdGuard DNS query debugging API
<!-- markdownlint-disable MD010 -->
You can debug AdGuard DNS queries by performing a query with the `CHAOS` class: You can debug AdGuard DNS queries by performing a query with the `CHAOS` class:
@ -41,8 +43,7 @@ In the `ANSWER SECTION`, the usual `IN` reply is returned.
In the `ADDITIONAL SECTION`, the following debug information 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>: - <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`.
The IP address of the client. The full name is `client-ip.adguard-dns.com`.
**Example:** **Example:**
@ -50,8 +51,7 @@ In the `ADDITIONAL SECTION`, the following debug information is returned:
client-ip.adguard-dns.com. 10 CH TXT "127.0.0.1" client-ip.adguard-dns.com. 10 CH TXT "127.0.0.1"
``` ```
* <a href="#additional-server-ip" id="additional-server-ip" name="additional-server-ip">`server-ip`</a>: - <a href="#additional-server-ip" id="additional-server-ip" name="additional-server-ip">`server-ip`</a>: The IP address of the server. The full name is `server-ip.adguard-dns.com`.
The IP address of the server. The full name is `server-ip.adguard-dns.com`.
**Example:** **Example:**
@ -59,9 +59,7 @@ In the `ADDITIONAL SECTION`, the following debug information is returned:
server-ip.adguard-dns.com. 10 CH TXT "127.0.0.1" server-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>: - <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`.
The ID of the device as detected by the server, if any. The full name is
`device-id.adguard-dns.com`.
**Example:** **Example:**
@ -69,9 +67,7 @@ In the `ADDITIONAL SECTION`, the following debug information is returned:
device-id.adguard-dns.com. 10 CH TXT "dev1234" 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>: - <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`.
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:** **Example:**
@ -79,8 +75,7 @@ In the `ADDITIONAL SECTION`, the following debug information is returned:
profile-id.adguard-dns.com. 10 CH TXT "prof1234" profile-id.adguard-dns.com. 10 CH TXT "prof1234"
``` ```
* <a href="#additional-country" id="additional-country" name="additional-country">`country`</a>: - <a href="#additional-country" id="additional-country" name="additional-country">`country`</a>: User's country code. The full name is `country.adguard-dns.com`.
User's country code. The full name is `country.adguard-dns.com`.
**Example:** **Example:**
@ -88,9 +83,7 @@ In the `ADDITIONAL SECTION`, the following debug information is returned:
country.adguard-dns.com. 10 CH TXT "CY" country.adguard-dns.com. 10 CH TXT "CY"
``` ```
* <a href="#additional-asn" id="additional-asn" name="additional-asn">`asn`</a>: - <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`.
User's autonomous system number (ASN). The full name is
`asn.adguard-dns.com`.
**Example:** **Example:**
@ -98,9 +91,7 @@ In the `ADDITIONAL SECTION`, the following debug information is returned:
asn.adguard-dns.com. 10 CH TXT "1234" asn.adguard-dns.com. 10 CH TXT "1234"
``` ```
* <a href="#additional-subdivision" id="additional-subdivision" name="additional-subdivision">`subdivision`</a>: - <a href="#additional-subdivision" id="additional-subdivision" name="additional-subdivision">`subdivision`</a>: User's location subdivision code. This field could be empty even if user's country code is present. The full name is `subdivision.adguard-dns.com`.
User's location subdivision code. This field could be empty even if user's
country code is present. The full name is `subdivision.adguard-dns.com`.
**Example:** **Example:**
@ -109,26 +100,17 @@ In the `ADDITIONAL SECTION`, the following debug information is returned:
subdivision.adguard-dns.com. 10 CH TXT "CA" subdivision.adguard-dns.com. 10 CH TXT "CA"
``` ```
The following debug records can have one of two prefixes: `req` or `resp`. The 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.
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>: - <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:
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>: - <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.
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>: - <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.
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>: - <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.
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>: - <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.
The query has been rewritten by a rewrite rule or parental protection.
**Example:** **Example:**
@ -136,10 +118,7 @@ response.
req.res-type.adguard-dns.com. 10 CH TXT "blocked" req.res-type.adguard-dns.com. 10 CH TXT "blocked"
``` ```
* <a href="#additional-rule" id="additional-rule" name="additional-rule">`rule`</a>: - <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.
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:** **Example:**
@ -156,10 +135,7 @@ response.
"heregoesthesecondpartoftherule" "heregoesthesecondpartoftherule"
``` ```
* <a href="#additional-rule-list-id" id="additional-rule-list-id" name="additional-rule-list-id">`rule-list-id`</a>: - <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`.
The ID of the rule list that was applied, if any. The full name is
`(req|resp).rule-list-id.adguard-dns.com`.
**Example:** **Example:**
@ -167,7 +143,6 @@ response.
req.rule-list-id.adguard-dns.com. 10 CH TXT "adguard_dns_filter" req.rule-list-id.adguard-dns.com. 10 CH TXT "adguard_dns_filter"
``` ```
The TTL of these responses is taken from parameter The TTL of these responses is taken from parameter [`filters.response_ttl`][conf-filters-ttl] in the configuration file.
[`filters.response_ttl`][conf-filters-ttl] in the configuration file.
[conf-filters-ttl]: configuration.md#filters-response_ttl [conf-filters-ttl]: configuration.md#filters-response_ttl

View File

@ -7,6 +7,7 @@ The AdGuard DNS debug HTTP API is served on [`LISTEN_PORT`][env-listen_port] and
- [`GET /health-check`](#health-check) - [`GET /health-check`](#health-check)
- [`GET /metrics`](#metrics) - [`GET /metrics`](#metrics)
- [`GET /debug/pprof`](#pprof) - [`GET /debug/pprof`](#pprof)
- [`POST /debug/api/cache/clear`](#api-cache-clear)
- [`POST /debug/api/refresh`](#api-refresh) - [`POST /debug/api/refresh`](#api-refresh)
- [`POST /dnsdb/csv`](#dnsdb-csv) - [`POST /dnsdb/csv`](#dnsdb-csv)
@ -28,14 +29,14 @@ The HTTP interface of Go's [PProf HTTP API][pprof api].
[pprof api]: https://pkg.go.dev/net/http/pprof [pprof api]: https://pkg.go.dev/net/http/pprof
## <a href="#api-refresh" id="api-refresh" name="api-refresh">`POST /debug/api/refresh`</a> ## <a href="#api-cache-clear" id="api-cache-clear" name="api-cache-clear">`POST /debug/api/cache/clear`</a>
Run some refresh jobs manually. This refresh does not alter the time of the next automatic refresh. Run some cache purges manually. The `ids` is an array of path patterns to match the cache IDs.
Example request: Example request:
```sh ```sh
curl -d '{"ids":["*"]}' -v "http://${LISTEN_ADDR}:${LISTEN_PORT}/debug/api/refresh" curl -d '{"ids":["filters/rulelist/*"]}' -v "http://${LISTEN_ADDR}:${LISTEN_PORT}/debug/api/cache/clear"
``` ```
Request body example: Request body example:
@ -43,18 +44,74 @@ Request body example:
```json ```json
{ {
"ids": [ "ids": [
"filter_storage", "filters/hashprefix/adult_blocking",
"adult_blocking" "filters/custom"
] ]
} }
``` ```
Supported IDs: Supported IDs:
- `adult_blocking`; - `dns/ecscache_no_ecs`
- `filter_storage`; - `dns/ecscache_with_ecs`
- `newly_registered_domains`; - `filters/blocked_service/*`
- `safe_browsing`. - `filters/custom`
- `filters/hashprefix/adult_blocking`
- `filters/hashprefix/newly_registered_domains`
- `filters/hashprefix/safe_browsing`
- `filters/rulelist/*`
- `filters/safe_search/general_safe_search`
- `geoip/host`
- `geoip/ip`
Note that you can clear the cache of any individual blocked service, e.g. `filters/blocked_service/youtube`, and any filter rule list, e.g. `filters/rulelist/adguard_dns_filter`.
The special ID `*`, when used alone, causes all available caches to be purged. Use with caution.
Response body example:
```json
{
"results": {
"filters/hashprefix/adult_blocking": "ok",
"filters/custom": "ok"
}
}
```
## <a href="#api-refresh" id="api-refresh" name="api-refresh">`POST /debug/api/refresh`</a>
Run some refresh jobs manually. The `ids` is an array of path patterns to match the refreshers IDs. This refresh does not alter the time of the next automatic refresh.
Example request:
```sh
curl -d '{"ids":["filters/*"]}' -v "http://${LISTEN_ADDR}:${LISTEN_PORT}/debug/api/refresh"
```
Request body example:
```json
{
"ids": [
"filters/hashprefix/adult_blocking",
"filters/storage"
]
}
```
Supported IDs:
- `allowlist`
- `billstat`
- `filters/hashprefix/adult_blocking`
- `filters/hashprefix/newly_registered_domains`
- `filters/hashprefix/safe_browsing`
- `filters/storage`
- `geoip`
- `profiledb`
- `rulestat`
- `ticket_rotator`
The special ID `*`, when used alone, causes all available refresh tasks to be performed. Use with caution. The special ID `*`, when used alone, causes all available refresh tasks to be performed. Use with caution.
@ -63,8 +120,8 @@ Response body example:
```json ```json
{ {
"results": { "results": {
"adult_blocking": "ok", "filters/hashprefix/adult_blocking": "ok",
"filter_storage": "ok" "filters/storage": "ok"
} }
} }
``` ```

View File

@ -1,134 +1,74 @@
# AdGuard DNS Development Setup # AdGuard DNS Development Setup
## Contents ## Contents
* [Initial setup](#init) - [Initial setup](#init)
* [Common Makefile macros and targets](#makefile) - [Common Makefile macros and targets](#makefile)
* [How to run AdGuard DNS](#run) - [How to run AdGuard DNS](#run)
* [Testing](#testing) - [Testing](#testing)
## <a href="#init" id="init" name="init">Initial setup</a> ## <a href="#init" id="init" name="init">Initial setup</a>
Development is supported on Linux and macOS (aka Darwin) systems. Development is supported on Linux and macOS (aka Darwin) systems.
1. Install Go 1.22 or later. 1. Install Go 1.23 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.
2. Call `make init` to set up the Git pre-commit hook.
3. 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> ## <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 Most development tasks are done through the use of our Makefile. Please keep the Makefile POSIX-compliant and portable.
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.
### <a href="#makefile-macros" id="makefile-macros" name="makefile-macros">Macros</a> - `OUT`: The name of the binary to build. Default: `./AdGuardDNS`.
This is not an extensive list. See `../Makefile` and the scripts in the - `RACE`: Set to `1` to enable the race detector. The race detector is always enabled for `make go-test`.
`../scripts/make/` directory.
<dl> - `VERBOSE`: Set to `1` to enable verbose mode. Default: `0`.
<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>
### <a href="#makefile-targets" id="makefile-targets" name="makefile-targets">Targets</a>
This is not an extensive list. See `../Makefile`. This is not an extensive list. See `../Makefile`.
<dl> - `make init`: Set up the pre-commit hook that runs checks, linters, and tests.
<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>
<p>
Regenerate the automatically generated Go files that need to be
periodically updated. Those generated files are:
</p>
<ul>
<li>
<code>../internal/geoip/country_generate.go</code>;
</li>
<li>
<code>../internal/geoip/asntops_generate.go</code>;
</li>
<li>
<code>../internal/ecscache/ecsblockilist_generate.go</code>;
</li>
<li>
<code>../internal/profiledb/internal/filecachepb/filecache.pb.go</code>.
</li>
</ul>
<p>
You'll need to
<a href="https://protobuf.dev/getting-started/gotutorial/#compiling-protocol-buffers">install <code>protoc</code></a>
for the last one.
</p>
</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>
- `make go-build`: Build the binary. See also the `OUT` and `RACE` macros.
- `make go-gen`: Regenerate the automatically generated Go files that need to be periodically updated. Those generated files are:
- `../internal/backendpb/dns.pb.go`
- `../internal/backendpb/dns_grpc.pb.go`
- `../internal/ecscache/ecsblockilist_generate.go`
- `../internal/geoip/asntops_generate.go`
- `../internal/geoip/country_generate.go`
- `../internal/profiledb/internal/filecachepb/filecache.pb.go`
You'll need to [install `protoc`][protoc] for the last one.
- `make go-lint`: Run Go checkers and static analysis.
- `make go-test`: Run Go tests.
- `make go-bench`: Run Go benchmarks.
- `make go-tools`: Install the Go static analysis tools locally.
- `make test`: Currently does the same thing as `make go-test` but is defined both because it's a common target and also in case code in another language appears in the future.
- `make txt-lint`: Run plain text checkers.
[protoc]: https://protobuf.dev/getting-started/gotutorial/#compiling-protocol-buffers
## <a href="#run" id="run" name="run">How to run AdGuard DNS</a> ## <a href="#run" id="run" name="run">How to run AdGuard DNS</a>
This is an example on how to run AdGuard DNS locally. 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>
### <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`: Keeping the test files in the `test` directory since it's added to `.gitignore`:
@ -150,20 +90,17 @@ openssl rand 32 > ./tls_key_1
openssl rand 32 > ./tls_key_2 openssl rand 32 > ./tls_key_2
``` ```
### <a href="#run-2" id="run-2" name="run-2">Step 2: prepare the DNSCrypt configuration</a>
### <a href="#run-2" id="run-2" name="run-2">Step 2: prepare the DNSCrypt configuration</a>
Install the [`dnscrypt`][dnsc] tool: Install the [`dnscrypt`][dnsc] tool:
* On macOS, install from Brew: - On macOS, install from Brew:
```sh ```sh
brew install ameshkov/tap/dnscrypt brew install ameshkov/tap/dnscrypt
``` ```
* On other unixes, such as Linux, [download][dnscdl] and install the latest - On other unixes, such as Linux, [download][dnscdl] and install the latest release manually.
release manually.
Then, generate the configuration: Then, generate the configuration:
@ -171,18 +108,14 @@ Then, generate the configuration:
dnscrypt generate -p testdns -o ./dnscrypt.yml dnscrypt generate -p testdns -o ./dnscrypt.yml
``` ```
### <a href="#run-3" id="run-3" name="run-3">Step 3: prepare the configuration file</a>
### <a href="#run-3" id="run-3" name="run-3">Step 3: prepare the configuration file</a>
```sh ```sh
cd ../ cd ../
cp -f config.dist.yaml config.yaml cp -f config.dist.yaml config.yaml
``` ```
### <a href="#run-4" id="run-4" name="run-4">Step 4: prepare the test data</a>
### <a href="#run-4" id="run-4" name="run-4">Step 4: prepare the test data</a>
```sh ```sh
echo '<html><body>General content ahead</body></html>' > ./test/block_page_general.html echo '<html><body>General content ahead</body></html>' > ./test/block_page_general.html
@ -192,17 +125,13 @@ echo '<html><body>Error 404</body></html>' > ./test/error_404.html
echo '<html><body>Error 500</body></html>' > ./test/error_500.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>
### <a href="#run-5" id="run-5" name="run-5">Step 5: compile AdGuard DNS</a>
```sh ```sh
make build make build
``` ```
### <a href="#run-6" id="run-6" name="run-6">Step 6: prepare cache data and GeoIP</a>
### <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. We'll use the test versions of the GeoIP databases here.
@ -214,45 +143,39 @@ curl 'https://raw.githubusercontent.com/maxmind/MaxMind-DB/main/test-data/GeoIP2
curl 'https://raw.githubusercontent.com/maxmind/MaxMind-DB/main/test-data/GeoIP2-ISP-Test.mmdb' -o ./test/GeoIP2-ISP-Test.mmdb curl 'https://raw.githubusercontent.com/maxmind/MaxMind-DB/main/test-data/GeoIP2-ISP-Test.mmdb' -o ./test/GeoIP2-ISP-Test.mmdb
``` ```
### <a href="#run-7" id="run-7" name="run-7">Step 7: run AdGuard DNS</a>
### <a href="#run-7" id="run-7" name="run-7">Step 7: run AdGuard DNS</a>
You'll need to supply the following: You'll need to supply the following:
* [`ADULT_BLOCKING_URL`][env-ADULT_BLOCKING_URL] - [`ADULT_BLOCKING_URL`][env-ADULT_BLOCKING_URL]
* [`BILLSTAT_URL`][env-BILLSTAT_URL] - [`BILLSTAT_URL`][env-BILLSTAT_URL]
* [`CONSUL_ALLOWLIST_URL`][env-CONSUL_ALLOWLIST_URL] - [`CONSUL_ALLOWLIST_URL`][env-CONSUL_ALLOWLIST_URL]
* [`GENERAL_SAFE_SEARCH_URL`][env-GENERAL_SAFE_SEARCH_URL] - [`GENERAL_SAFE_SEARCH_URL`][env-GENERAL_SAFE_SEARCH_URL]
* [`LINKED_IP_TARGET_URL`][env-LINKED_IP_TARGET_URL] - [`LINKED_IP_TARGET_URL`][env-LINKED_IP_TARGET_URL]
* [`NEW_REG_DOMAINS_URL`][env-NEW_REG_DOMAINS_URL] - [`NEW_REG_DOMAINS_URL`][env-NEW_REG_DOMAINS_URL]
* [`PROFILES_URL`][env-PROFILES_URL] - [`PROFILES_URL`][env-PROFILES_URL]
* [`SAFE_BROWSING_URL`][env-SAFE_BROWSING_URL] - [`SAFE_BROWSING_URL`][env-SAFE_BROWSING_URL]
* [`YOUTUBE_SAFE_SEARCH_URL`][env-YOUTUBE_SAFE_SEARCH_URL] - [`YOUTUBE_SAFE_SEARCH_URL`][env-YOUTUBE_SAFE_SEARCH_URL]
See the [external HTTP API documentation][externalhttp]. See the [external HTTP API documentation][externalhttp].
You may use `go run ./scripts/backend` to start mock GRPC server for You may use `go run ./scripts/backend` to start mock GRPC server for `BILLSTAT_URL` and `PROFILES_URL` endpoints.
`BILLSTAT_URL` and `PROFILES_URL` endpoints.
You may need to change the listen ports in `config.yaml` which are less than You may need to change the listen ports in `config.yaml` which are less than 1024 to some other ports. Otherwise, `sudo` or `doas` is required to run `AdGuardDNS`.
1024 to some other ports. Otherwise, `sudo` or `doas` is required to run
`AdGuardDNS`.
Examples below are for the configuration with the following changes: Examples below are for the configuration with the following changes:
* Plain DNS: `53``5354` - Plain DNS: `53``5354`
* DoT: `853``8853` - DoT: `853``8853`
* DoH: `443``8443` - DoH: `443``8443`
* DoQ: `853``8853` - DoQ: `853``8853`
You may also need to remove `probe_ipv6` if your network does not support IPv6. You may also need to remove `probe_ipv6` if your network does not support IPv6.
If you're using an OS different from Linux, you also need to make these changes: If you're using an OS different from Linux, you also need to make these changes:
* Remove the `interface_listeners` section. - Remove the `interface_listeners` section.
* Remove `bind_interfaces` from the `default_dns` server configuration and - Remove `bind_interfaces` from the `default_dns` server configuration and replace it with `bind_addresses`.
replace it with `bind_addresses`.
```sh ```sh
env \ env \
@ -293,9 +216,7 @@ env \
[externalhttp]: externalhttp.md [externalhttp]: externalhttp.md
### <a href="#run-8" id="run-8" name="run-8">Step 8: test your instance</a>
### <a href="#run-8" id="run-8" name="run-8">Step 8: test your instance</a>
Plain DNS: Plain DNS:
@ -323,11 +244,10 @@ 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. 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 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>.
there to generate an SDNS stamp on <https://dnscrypt.info/stamps>.
> [!NOTE] > [!NOTE]
> The example below is for a test configuration that won't work for you. > The example below is for a test configuration that won't work for you.
```sh ```sh
dnslookup example.org sdns://AQcAAAAAAAAADjEyNy4wLjAuMTo1NDQzIAbKgP3dmXybr1DaKIFgKjsc8zSFX4rgT_hFgymSq6w1FzIuZG5zY3J5cHQtY2VydC50ZXN0ZG5z dnslookup example.org sdns://AQcAAAAAAAAADjEyNy4wLjAuMTo1NDQzIAbKgP3dmXybr1DaKIFgKjsc8zSFX4rgT_hFgymSq6w1FzIuZG5zY3J5cHQtY2VydC50ZXN0ZG5z
@ -336,53 +256,35 @@ dnslookup example.org sdns://AQcAAAAAAAAADjEyNy4wLjAuMTo1NDQzIAbKgP3dmXybr1DaKIF
[dnsc]: https://github.com/ameshkov/dnscrypt [dnsc]: https://github.com/ameshkov/dnscrypt
[dnscdl]: https://github.com/ameshkov/dnscrypt/releases [dnscdl]: https://github.com/ameshkov/dnscrypt/releases
## <a href="#testing" id="testing" name="testing">Testing</a> ## <a href="#testing" id="testing" name="testing">Testing</a>
The `go-bench` and `go-test` targets [described earlier](#makefile-targets) The `go-bench` and `go-test` targets [described earlier](#makefile-targets) should generally be enough, but there are cases where additional testing setup is required. One such case is package `bindtodevice`.
should generally be enough, but there are cases where additional testing setup
is required. One such case is package `bindtodevice`.
### <a href="#testing-bindtodevice" id="testing-bindtodevice" name="testing-bindtodevice">Testing `SO_BINDTODEVICE` features</a>
The `SO_BINDTODEVICE` features require a Linux machine with a particular IP routing set up. In order to test these features on architectures other than Linux, this repository has a Dockerfile and a convenient script to use it, see `scripts/test/bindtodevice.sh`.
### <a href="#testing-bindtodevice" id="testing-bindtodevice" name="testing-bindtodevice">Testing `SO_BINDTODEVICE` features</a>
The `SO_BINDTODEVICE` features require a Linux machine with a particular IP
routing set up. In order to test these features on architectures other than
Linux, this repository has a Dockerfile and a convenient script to use it, see
`scripts/test/bindtodevice.sh`.
A simple example: A simple example:
* If your Docker is installed in a way that doesn't require `sudo` to use it: - If your Docker is installed in a way that doesn't require `sudo` to use it:
```sh ```sh
sh ./scripts/test/bindtodevice.sh sh ./scripts/test/bindtodevice.sh
``` ```
* Otherwise: - Otherwise:
```sh ```sh
env USE_SUDO=1 sh ./scripts/test/bindtodevice.sh env USE_SUDO=1 sh ./scripts/test/bindtodevice.sh
``` ```
This will build the image and open a shell within the container. The container This will build the image and open a shell within the container. The container environment is defined by `scripts/test/bindtodevice.docker`, and has all utilities required to build the `AdGuardDNS` binary and test it. The working directory is also shared with the container through the `/test` directory inside it. The container also routes all IP connections to any address in the `172.17.0.0/16` subnet to the `eth0` network interface. So, calling `make go-test` or a similar command from within the container will actually test the `SO_BINDTODEVICE` features:
environment is defined by `scripts/test/bindtodevice.docker`, and has all
utilities required to build the `AdGuardDNS` binary and test it. The working
directory is also shared with the container through the `/test` directory inside
it. The container also routes all IP connections to any address in the
`172.17.0.0/16` subnet to the `eth0` network interface. So, calling `make
go-test` or a similar command from within the container will actually test the
`SO_BINDTODEVICE` features:
```sh ```sh
go test --cover -v ./internal/bindtodevice/ go test --cover -v ./internal/bindtodevice/
``` ```
If you want to open an additional terminal (for example to launch `AdGuardDNS` If you want to open an additional terminal (for example to launch `AdGuardDNS` in one and `dig` it in the other), use `docker exec` like this (you may need `sudo` for that):
in one and `dig` it in the other), use `docker exec` like this (you may need
`sudo` for that):
```sh ```sh
docker exec -i -t agdns_bindtodevice_test /bin/sh docker exec -i -t agdns_bindtodevice_test /bin/sh

View File

@ -4,9 +4,11 @@ AdGuard DNS uses [environment variables][wiki-env] to store some of the more sen
## Contents ## Contents
- [`ADULT_BLOCKING_ENABLED`](#ADULT_BLOCKING_ENABLED)
- [`ADULT_BLOCKING_URL`](#ADULT_BLOCKING_URL) - [`ADULT_BLOCKING_URL`](#ADULT_BLOCKING_URL)
- [`BILLSTAT_API_KEY`](#BILLSTAT_API_KEY) - [`BILLSTAT_API_KEY`](#BILLSTAT_API_KEY)
- [`BILLSTAT_URL`](#BILLSTAT_URL) - [`BILLSTAT_URL`](#BILLSTAT_URL)
- [`BLOCKED_SERVICE_ENABLED`](#BLOCKED_SERVICE_ENABLED)
- [`BLOCKED_SERVICE_INDEX_URL`](#BLOCKED_SERVICE_INDEX_URL) - [`BLOCKED_SERVICE_INDEX_URL`](#BLOCKED_SERVICE_INDEX_URL)
- [`CONFIG_PATH`](#CONFIG_PATH) - [`CONFIG_PATH`](#CONFIG_PATH)
- [`CONSUL_ALLOWLIST_URL`](#CONSUL_ALLOWLIST_URL) - [`CONSUL_ALLOWLIST_URL`](#CONSUL_ALLOWLIST_URL)
@ -14,33 +16,51 @@ AdGuard DNS uses [environment variables][wiki-env] to store some of the more sen
- [`CONSUL_DNSCHECK_SESSION_URL`](#CONSUL_DNSCHECK_SESSION_URL) - [`CONSUL_DNSCHECK_SESSION_URL`](#CONSUL_DNSCHECK_SESSION_URL)
- [`FILTER_CACHE_PATH`](#FILTER_CACHE_PATH) - [`FILTER_CACHE_PATH`](#FILTER_CACHE_PATH)
- [`FILTER_INDEX_URL`](#FILTER_INDEX_URL) - [`FILTER_INDEX_URL`](#FILTER_INDEX_URL)
- [`GENERAL_SAFE_ENABLED`](#GENERAL_SAFE_SEARCH_ENABLED)
- [`GENERAL_SAFE_SEARCH_URL`](#GENERAL_SAFE_SEARCH_URL) - [`GENERAL_SAFE_SEARCH_URL`](#GENERAL_SAFE_SEARCH_URL)
- [`GEOIP_ASN_PATH` and `GEOIP_COUNTRY_PATH`](#GEOIP_ASN_PATH) - [`GEOIP_ASN_PATH` and `GEOIP_COUNTRY_PATH`](#GEOIP_ASN_PATH)
- [`LINKED_IP_TARGET_URL`](#LINKED_IP_TARGET_URL) - [`LINKED_IP_TARGET_URL`](#LINKED_IP_TARGET_URL)
- [`LISTEN_ADDR`](#LISTEN_ADDR) - [`LISTEN_ADDR`](#LISTEN_ADDR)
- [`LISTEN_PORT`](#LISTEN_PORT) - [`LISTEN_PORT`](#LISTEN_PORT)
- [`LOG_TIMESTAMP`](#LOG_TIMESTAMP) - [`LOG_TIMESTAMP`](#LOG_TIMESTAMP)
- [`METRICS_NAMESPACE`](#METRICS_NAMESPACE)
- [`NEW_REG_DOMAINS_ENABLED`](#NEW_REG_DOMAINS_ENABLED)
- [`NEW_REG_DOMAINS_URL`](#NEW_REG_DOMAINS_URL) - [`NEW_REG_DOMAINS_URL`](#NEW_REG_DOMAINS_URL)
- [`PROFILES_API_KEY`](#PROFILES_API_KEY) - [`PROFILES_API_KEY`](#PROFILES_API_KEY)
- [`PROFILES_CACHE_PATH`](#PROFILES_CACHE_PATH) - [`PROFILES_CACHE_PATH`](#PROFILES_CACHE_PATH)
- [`PROFILES_ENABLED`](#PROFILES_ENABLED)
- [`PROFILES_URL`](#PROFILES_URL) - [`PROFILES_URL`](#PROFILES_URL)
- [`REDIS_ADDR`](#REDIS_ADDR)
- [`REDIS_KEY_PREFIX`](#REDIS_KEY_PREFIX)
- [`REDIS_MAX_ACTIVE`](#REDIS_MAX_ACTIVE)
- [`REDIS_MAX_IDLE`](#REDIS_MAX_IDLE)
- [`REDIS_IDLE_TIMEOUT`](#REDIS_IDLE_TIMEOUT)
- [`REDIS_PORT`](#REDIS_PORT)
- [`QUERYLOG_PATH`](#QUERYLOG_PATH) - [`QUERYLOG_PATH`](#QUERYLOG_PATH)
- [`RULESTAT_URL`](#RULESTAT_URL) - [`RULESTAT_URL`](#RULESTAT_URL)
- [`SAFE_BROWSING_ENABLED`](#SAFE_BROWSING_ENABLED)
- [`SAFE_BROWSING_URL`](#SAFE_BROWSING_URL) - [`SAFE_BROWSING_URL`](#SAFE_BROWSING_URL)
- [`SENTRY_DSN`](#SENTRY_DSN) - [`SENTRY_DSN`](#SENTRY_DSN)
- [`SSL_KEY_LOG_FILE`](#SSL_KEY_LOG_FILE) - [`SSL_KEY_LOG_FILE`](#SSL_KEY_LOG_FILE)
- [`VERBOSE`](#VERBOSE) - [`VERBOSE`](#VERBOSE)
- [`WEB_STATIC_DIR_ENABLED`](#WEB_STATIC_DIR_ENABLED)
- [`WEB_STATIC_DIR`](#WEB_STATIC_DIR)
- [`YOUTUBE_SAFE_SEARCH_ENABLED`](#YOUTUBE_SAFE_SEARCH_ENABLED)
- [`YOUTUBE_SAFE_SEARCH_URL`](#YOUTUBE_SAFE_SEARCH_URL) - [`YOUTUBE_SAFE_SEARCH_URL`](#YOUTUBE_SAFE_SEARCH_URL)
[conf]: configuration.md [conf]: configuration.md
[wiki-env]: https://en.wikipedia.org/wiki/Environment_variable [wiki-env]: https://en.wikipedia.org/wiki/Environment_variable
## <a href="#ADULT_BLOCKING_ENABLED" id="ADULT_BLOCKING_ENABLED" name="ADULT_BLOCKING_ENABLED">`ADULT_BLOCKING_ENABLED`</a>
When set to `1`, enable the adult-blocking hash-prefix filter. When set to `0`, disable it.
**Default:** `1`.
## <a href="#ADULT_BLOCKING_URL" id="ADULT_BLOCKING_URL" name="ADULT_BLOCKING_URL">`ADULT_BLOCKING_URL`</a> ## <a href="#ADULT_BLOCKING_URL" id="ADULT_BLOCKING_URL" name="ADULT_BLOCKING_URL">`ADULT_BLOCKING_URL`</a>
The URL of source list of rules for adult blocking filter. The HTTP(S) URL of source list of rules for adult blocking filter.
**Default:** No default value, the variable is **required.** **Default:** No default value, the variable is required if `ADULT_BLOCKING_ENABLED` is set to `1`.
## <a href="#BILLSTAT_API_KEY" id="BILLSTAT_API_KEY" name="BILLSTAT_API_KEY">`BILLSTAT_API_KEY`</a> ## <a href="#BILLSTAT_API_KEY" id="BILLSTAT_API_KEY" name="BILLSTAT_API_KEY">`BILLSTAT_API_KEY`</a>
@ -52,17 +72,24 @@ The API key to use when authenticating queries to the billing statistics API, if
## <a href="#BILLSTAT_URL" id="BILLSTAT_URL" name="BILLSTAT_URL">`BILLSTAT_URL`</a> ## <a href="#BILLSTAT_URL" id="BILLSTAT_URL" name="BILLSTAT_URL">`BILLSTAT_URL`</a>
The base backend URL for backend billing statistics uploader API. Supports GRPC (`grpc://` and`grpcs://`) URLs. See the [external HTTP API requirements section][ext-billstat]. The base backend URL for backend billing statistics uploader API. Supports gRPC(S) (`grpc://` and`grpcs://`) URLs. See the [external HTTP API requirements section][ext-billstat].
**Default:** No default value, the variable is **required.** **Default:** No default value, the variable is required if there is at least one [server group][conf-sg] with profiles enabled.
[conf-sg]: configuration.md#server_groups
[ext-billstat]: externalhttp.md#backend-billstat [ext-billstat]: externalhttp.md#backend-billstat
## <a href="#BLOCKED_SERVICE_ENABLED" id="BLOCKED_SERVICE_ENABLED" name="BLOCKED_SERVICE_ENABLED">`BLOCKED_SERVICE_ENABLED`</a>
When set to `1`, enable the blocked service filter. When set to `0`, disable it.
**Default:** `1`.
## <a href="#BLOCKED_SERVICE_INDEX_URL" id="BLOCKED_SERVICE_INDEX_URL" name="BLOCKED_SERVICE_INDEX_URL">`BLOCKED_SERVICE_INDEX_URL`</a> ## <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. The HTTP(S) 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.** **Default:** No default value, the variable is required if `BLOCKED_SERVICE_ENABLED` is set to `1`.
[ext-blocked]: externalhttp.md#filters-blocked-services [ext-blocked]: externalhttp.md#filters-blocked-services
@ -74,7 +101,7 @@ The path to the configuration file.
## <a href="#CONSUL_ALLOWLIST_URL" id="CONSUL_ALLOWLIST_URL" name="CONSUL_ALLOWLIST_URL">`CONSUL_ALLOWLIST_URL`</a> ## <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. The HTTP(S) 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.** **Default:** No default value, the variable is **required.**
@ -82,7 +109,7 @@ The URL of the Consul instance serving the dynamic part of the rate-limit allowl
## <a href="#CONSUL_DNSCHECK_KV_URL" id="CONSUL_DNSCHECK_KV_URL" name="CONSUL_DNSCHECK_KV_URL">`CONSUL_DNSCHECK_KV_URL`</a> ## <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. The HTTP(S) 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.** **Default:** **Unset.**
@ -90,7 +117,7 @@ The URL of the KV API of the Consul instance used as a key-value database for th
## <a href="#CONSUL_DNSCHECK_SESSION_URL" id="CONSUL_DNSCHECK_SESSION_URL" name="CONSUL_DNSCHECK_SESSION_URL">`CONSUL_DNSCHECK_SESSION_URL`</a> ## <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. The HTTP(S) 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.** **Default:** **Unset.**
@ -104,17 +131,23 @@ The path to the directory used to store the cached version of all filters and fi
## <a href="#FILTER_INDEX_URL" id="FILTER_INDEX_URL" name="FILTER_INDEX_URL">`FILTER_INDEX_URL`</a> ## <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. The HTTP(S) URL or a hostless file URI (e.g. `file:///tmp/filters.json`) 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.** **Default:** No default value, the variable is **required.**
[ext-lists]: externalhttp.md#filters-lists [ext-lists]: externalhttp.md#filters-lists
## <a href="#GENERAL_SAFE_SEARCH_ENABLED" id="GENERAL_SAFE_SEARCH_ENABLED" name="GENERAL_SAFE_SEARCH_ENABLED">`GENERAL_SAFE_SEARCH_ENABLED`</a>
When set to `1`, enable the general safe search filter. When set to `0`, disable it.
**Default:** `1`.
## <a href="#GENERAL_SAFE_SEARCH_URL" id="GENERAL_SAFE_SEARCH_URL" name="GENERAL_SAFE_SEARCH_URL">`GENERAL_SAFE_SEARCH_URL`</a> ## <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. The HTTP(S) 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.** **Default:** No default value, the variable is required if `GENERAL_SAFE_SEARCH_ENABLED` is set to `1`.
[ext-general]: externalhttp.md#filters-safe-search [ext-general]: externalhttp.md#filters-safe-search
@ -126,7 +159,7 @@ Paths to the files containing MaxMind GeoIP databases: for ASNs and for countrie
## <a href="#LINKED_IP_TARGET_URL" id="LINKED_IP_TARGET_URL" name="LINKED_IP_TARGET_URL">`LINKED_IP_TARGET_URL`</a> ## <a href="#LINKED_IP_TARGET_URL" id="LINKED_IP_TARGET_URL" name="LINKED_IP_TARGET_URL">`LINKED_IP_TARGET_URL`</a>
The target URL to which linked IP API requests are proxied. In case [linked IP and dynamic DNS][conf-web-linked_ip] web server is configured, the variable is required. See the [external HTTP API requirements section][ext-linked_ip]. The target HTTP(S) URL to which linked IP API requests are proxied. In case [linked IP and dynamic DNS][conf-web-linked_ip] web server is configured, the variable is required. See the [external HTTP API requirements section][ext-linked_ip].
**Default:** **Unset.** **Default:** **Unset.**
@ -153,11 +186,23 @@ If `1`, show timestamps in the plain text logs. If `0`, don't show the timestamp
**Default:** `1`. **Default:** `1`.
## <a href="#METRICS_NAMESPACE" id="METRICS_NAMESPACE" name="METRICS_NAMESPACE">`METRICS_NAMESPACE`</a>
The namespace to be used for Prometheus metrics. It must be a valid Prometheus metric label.
**Default:** `dns`.
## <a href="#NEW_REG_DOMAINS_ENABLED" id="NEW_REG_DOMAINS_ENABLED" name="NEW_REG_DOMAINS_ENABLED">`NEW_REG_DOMAINS_ENABLED`</a>
When set to `1`, enable the newly-registered domains hash-prefix filter. When set to `0`, disable it.
**Default:** `1`.
## <a href="#NEW_REG_DOMAINS_URL" id="NEW_REG_DOMAINS_URL" name="NEW_REG_DOMAINS_URL">`NEW_REG_DOMAINS_URL`</a> ## <a href="#NEW_REG_DOMAINS_URL" id="NEW_REG_DOMAINS_URL" name="NEW_REG_DOMAINS_URL">`NEW_REG_DOMAINS_URL`</a>
The URL of source list of rules for newly registered domains safe browsing filter. The HTTP(S) URL of source list of rules for newly registered domains safe browsing filter.
**Default:** No default value, the variable is **required.** **Default:** No default value, the variable is required if `NEW_REG_DOMAINS_ENABLED` is set to `1`.
## <a href="#PROFILES_API_KEY" id="PROFILES_API_KEY" name="PROFILES_API_KEY">`PROFILES_API_KEY`</a> ## <a href="#PROFILES_API_KEY" id="PROFILES_API_KEY" name="PROFILES_API_KEY">`PROFILES_API_KEY`</a>
@ -189,20 +234,58 @@ The profile cache is read on start and is later updated on every [full refresh][
[conf-backend-full_refresh_interval]: configuration.md#backend-full_refresh_interval [conf-backend-full_refresh_interval]: configuration.md#backend-full_refresh_interval
## <a href="#PROFILES_ENABLED" id="PROFILES_ENABLED" name="PROFILES_ENABLED">`PROFILES_ENABLED`</a> ## <a href="#PROFILES_MAX_RESP_SIZE" id="PROFILES_MAX_RESP_SIZE" name="PROFILES_MAX_RESP_SIZE">`PROFILES_MAX_RESP_SIZE`</a>
If `0`, disables user profiles and devices recognition and billing. The maximum size of the response from the profiles API in a human-readable format.
**Default:** `1`. **Default:** `8MB`.
## <a href="#PROFILES_URL" id="PROFILES_URL" name="PROFILES_URL">`PROFILES_URL`</a> ## <a href="#PROFILES_URL" id="PROFILES_URL" name="PROFILES_URL">`PROFILES_URL`</a>
The base backend URL for profiles API. Supports GRPC (`grpc://` and`grpcs://`) URLs. See the [external API requirements section][ext-profiles]. The base backend URL for profiles API. Supports gRPC(S) (`grpc://` and`grpcs://`) URLs. See the [external API requirements section][ext-profiles].
**Default:** No default value, the variable is **required.** **Default:** No default value, the variable is required if there is at least one [server group][conf-sg] with profiles enabled.
[ext-profiles]: externalhttp.md#backend-profiles [ext-profiles]: externalhttp.md#backend-profiles
## <a href="#REDIS_ADDR" id="REDIS_ADDR" name="REDIS_ADDR">`REDIS_ADDR`</a>
Redis server address. Can be an IP address or a hostname.
**Default:** No default value, the variable if required if the [type][conf-check-kv-type] of remote KV storage for DNS server checking is `redis` in the configuration file.
[conf-check-kv-type]: configuration.md#check-kv-type
## <a href="#REDIS_KEY_PREFIX" id="REDIS_KEY_PREFIX" name="REDIS_KEY_PREFIX">`REDIS_KEY_PREFIX`</a>
The prefix for Redis keys.
**Default:** `agdns`.
## <a href="#REDIS_MAX_ACTIVE" id="REDIS_MAX_ACTIVE" name="REDIS_MAX_ACTIVE">`REDIS_MAX_ACTIVE`</a>
The maximum number of active Redis connections.
**Default:** `10`.
## <a href="#REDIS_MAX_IDLE" id="REDIS_MAX_IDLE" name="REDIS_MAX_IDLE">`REDIS_MAX_IDLE`</a>
The maximum number of idle Redis connections.
**Default:** `3`.
## <a href="#REDIS_IDLE_TIMEOUT" id="REDIS_IDLE_TIMEOUT" name="REDIS_IDLE_TIMEOUT">`REDIS_IDLE_TIMEOUT`</a>
How long until idle Redis connections are closed, as a human-readable duration.
**Default:** `30s`.
## <a href="#REDIS_PORT" id="REDIS_PORT" name="REDIS_PORT">`REDIS_PORT`</a>
Redis server port.
**Default:** `6379`.
## <a href="#QUERYLOG_PATH" id="QUERYLOG_PATH" name="QUERYLOG_PATH">`QUERYLOG_PATH`</a> ## <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. The path to the file into which the query log is going to be written.
@ -211,7 +294,7 @@ The path to the file into which the query log is going to be written.
## <a href="#RULESTAT_URL" id="RULESTAT_URL" name="RULESTAT_URL">`RULESTAT_URL`</a> ## <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. The HTTP(S) 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.** **Default:** **Unset.**
@ -219,11 +302,17 @@ The URL to send filtering rule list statistics to. If empty or unset, the collec
[ext-rulestat]: externalhttp.md#rulestat [ext-rulestat]: externalhttp.md#rulestat
## <a href="#SAFE_BROWSING_ENABLED" id="SAFE_BROWSING_ENABLED" name="SAFE_BROWSING_ENABLED">`SAFE_BROWSING_ENABLED`</a>
When set to `1`, enable the safe-browsing hash-prefix filter. When set to `0`, disable it.
**Default:** `1`.
## <a href="#SAFE_BROWSING_URL" id="SAFE_BROWSING_URL" name="SAFE_BROWSING_URL">`SAFE_BROWSING_URL`</a> ## <a href="#SAFE_BROWSING_URL" id="SAFE_BROWSING_URL" name="SAFE_BROWSING_URL">`SAFE_BROWSING_URL`</a>
The URL of source list of rules for dangerous domains safe browsing filter. The HTTP(S) URL of source list of rules for dangerous domains safe browsing filter.
**Default:** No default value, the variable is **required.** **Default:** No default value, the variable is required if `SAFE_BROWSING_ENABLED` is set to `1`.
## <a href="#SENTRY_DSN" id="SENTRY_DSN" name="SENTRY_DSN">`SENTRY_DSN`</a> ## <a href="#SENTRY_DSN" id="SENTRY_DSN" name="SENTRY_DSN">`SENTRY_DSN`</a>
@ -239,12 +328,36 @@ If set, TLS key logs are written to this file to allow other programs (i.e. Wire
## <a href="#VERBOSE" id="VERBOSE" name="VERBOSE">`VERBOSE`</a> ## <a href="#VERBOSE" id="VERBOSE" name="VERBOSE">`VERBOSE`</a>
When set to `1`, enable verbose logging. When set to `0`, disable it. - `2`: Enables trace logging.
- `1`: Enables debug logging.
- `0`: The default level of verbosity: only info logs are printed.
**Default:** `0`. **Default:** `0`.
## <a href="#WEB_STATIC_DIR_ENABLED" id="WEB_STATIC_DIR_ENABLED" name="WEB_STATIC_DIR_ENABLED">`WEB_STATIC_DIR_ENABLED`</a>
When set to `1`, use `WEB_STATIC_DIR` as the source of the static content.
**Default:** `0`.
## <a href="#WEB_STATIC_DIR" id="WEB_STATIC_DIR" name="WEB_STATIC_DIR">`WEB_STATIC_DIR`</a>
The absolute path to the directory used to serve static content. The directory must exist.
The value of the `Content-Type` header is guessed from the files' contents. Other headers cannot be modified. If the content type of a file cannot be guessed, `text/plain` is used.
**Default:** No default value, the variable is required if `WEB_STATIC_DIR_ENABLED` is set to `1`.
## <a href="#YOUTUBE_SAFE_SEARCH_ENABLED" id="YOUTUBE_SAFE_SEARCH_ENABLED" name="YOUTUBE_SAFE_SEARCH_ENABLED">`YOUTUBE_SAFE_SEARCH_ENABLED`</a>
When set to `1`, enable the youtube safe search filter. When set to `0`, disable it.
**Default:** `1`.
## <a href="#YOUTUBE_SAFE_SEARCH_URL" id="YOUTUBE_SAFE_SEARCH_URL" name="YOUTUBE_SAFE_SEARCH_URL">`YOUTUBE_SAFE_SEARCH_URL`</a> ## <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. The HTTP(S) 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.** **Default:** No default value, the variable is required if `YOUTUBE_SAFE_SEARCH_ENABLED` is set to `1`.

View File

@ -1,65 +1,46 @@
# AdGuard DNS External HTTP API Requirements # AdGuard DNS external HTTP API requirements
AdGuard DNS uses information from external HTTP APIs for filtering and other 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.
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 TODO(a.garipov): Reinspect uses of “should” and “must” throughout this
document. document.
--> -->
## Contents ## Contents
* [Backend Billing Statistics](#backend-billstat) - [Backend billing statistics](#backend-billstat)
* [Backend Profiles Service](#backend-profiles) - [Backend profiles service](#backend-profiles)
* [Consul Key-Value Storage](#consul) - [Consul key-value storage](#consul)
* [Filtering](#filters) - [Filtering](#filters)
* [Blocked Services](#filters-blocked-services) - [Blocked services](#filters-blocked-services)
* [Filtering Rule Lists](#filters-lists) - [Filtering rule lists](#filters-lists)
* [Safe Search](#filters-safe-search) - [Safe search](#filters-safe-search)
* [Proxied Linked IP and Dynamic DNS (DDNS) Endpoints](#backend-linkip) - [Proxied linked IP and dynamic DNS (DDNS) Endpoints](#backend-linkip)
* [Rule Statistics Service](#rulestat) - [Rule statistics service](#rulestat)
## <a href="#backend-billstat" id="backend-billstat" name="backend-billstat">Backend billing statistics</a>
This is the service to which the [`BILLSTAT_URL`][env-billstat_url] environment variable points. Supports gRPC(s) URLs. The service must correspond to `./internal/backendpb/dns.proto`.
## <a href="#backend-billstat" id="backend-billstat" name="backend-billstat">Backend Billing Statistics</a> This service is disabled when all server groups have property [`profiles_enabled`][conf-srvgrp-prof] set to `false`.
This is the service to which the [`BILLSTAT_URL`][env-billstat_url] environment
variable points. Supports `grpc(s)` URLs. The service must correspond to
`./internal/backendpb/backend.proto`. This service can be disabled with the
[`PROFILES_ENABLED`][env-profiles_enabled] environment variable.
[env-billstat_url]: environment.md#BILLSTAT_URL [env-billstat_url]: environment.md#BILLSTAT_URL
[env-profiles_enabled]: environment.md#PROFILES_ENABLED [conf-srvgrp-prof]: configuration.md#sg-*-profiles_enabled
## <a href="#backend-profiles" id="backend-profiles" name="backend-profiles">Backend profiles service</a>
This is the service to which the [`PROFILES_URL`][env-profiles_url] environment variable points. Supports gRPC(s) URLs. The service must correspond to `./internal/backendpb/dns.proto`.
## <a href="#backend-profiles" id="backend-profiles" name="backend-profiles">Backend Profiles Service</a> This service is disabled when all server groups have property [`profiles_enabled`][conf-srvgrp-prof] set to `false`.
This is the service to which the [`PROFILES_URL`][env-profiles_url] environment
variable points. Supports `grpc(s)` URLs. The service must correspond to
`./internal/backendpb/backend.proto`. This service can be disabled with the
[`PROFILES_ENABLED`][env-profiles_enabled] environment variable.
[env-profiles_url]: environment.md#PROFILES_URL [env-profiles_url]: environment.md#PROFILES_URL
## <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.
## <a href="#consul" id="consul" name="consul">Consul Key-Value Storage</a> The `CONSUL_ALLOWLIST_URL` endpoint must respond with a `200 OK` response code and a JSON document in the following format:
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.
The `CONSUL_ALLOWLIST_URL` endpoint must respond with a `200 OK` response code
and a JSON document in the following format:
```json ```json
[ [
@ -76,15 +57,11 @@ and a JSON document in the following format:
[env-consul-dnscheck-kv]: environment.md#CONSUL_DNSCHECK_KV_URL [env-consul-dnscheck-kv]: environment.md#CONSUL_DNSCHECK_KV_URL
[env-consul-dnscheck-session]: environment.md#CONSUL_DNSCHECK_SESSION_URL [env-consul-dnscheck-session]: environment.md#CONSUL_DNSCHECK_SESSION_URL
## <a href="#filters" id="filters" name="filters">Filtering</a> ## <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> ### <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 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:
respond with a `200 OK` response code and a JSON document in the following
format:
```json ```json
{ {
@ -100,15 +77,11 @@ format:
} }
``` ```
All properties must be filled with valid IDs and rules. Additional fields in All properties must be filled with valid IDs and rules. Additional fields in objects are ignored.
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:
### <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 ```json
{ {
@ -121,17 +94,11 @@ This endpoint, defined by [`FILTER_INDEX_URL`][env-filters], must respond with a
} }
``` ```
All properties must be filled with valid IDs and URLs. Additional fields in All properties must be filled with valid IDs and URLs. Additional fields in objects are ignored.
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:
### <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 ```none
|m.youtube.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com |m.youtube.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
@ -149,31 +116,25 @@ code and filtering rule lists with [`$dnsrewrite`][rules-dnsrewrite] rules for
<!-- <!--
TODO(a.garipov): Replace with a link to the new KB when it is finished. 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 [rules-dnsrewrite]: https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists#dnsrewrite
## <a href="#backend-linkip" id="backend-linkip" name="backend-linkip">Proxied linked iP and dynamic DNS (DDNS) endpoints</a>
The service defined by the [`LINKED_IP_TARGET_URL`][env-linked_ip_target_url] environment variable should define the following endpoints:
## <a href="#backend-linkip" id="backend-linkip" name="backend-linkip">Proxied Linked IP and Dynamic DNS (DDNS) Endpoints</a> - `GET /linkip/{device_id}/{encrypted}/status`
- `GET /linkip/{device_id}/{encrypted}`
- `POST /ddns/{device_id}/{encrypted}/{domain}`
- `POST /linkip/{device_id}/{encrypted}`
The service defined by the [`LINKED_IP_TARGET_URL`][env-linked_ip_target_url] The AdGuard DNS proxy will add the `X-Connecting-Ip` header with the IP address of the original client as well as set the `User-Agent` header to its own value.
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 `CF-Connecting-IP` header with the IP address
of the original client as well as set the `User-Agent` header to its own value.
[env-linked_ip_target_url]: environment.md#LINKED_IP_TARGET_URL [env-linked_ip_target_url]: environment.md#LINKED_IP_TARGET_URL
## <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:
## <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 ```json
{ {

View File

@ -1,30 +1,23 @@
# AdGuard DNS HTTP API # AdGuard DNS HTTP API
The main HTTP API is served on the same port as the DNS-over-HTTP servers as 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.
well as on other addresses, if the [web configuration][conf-web] is set
appropriately.
## Contents ## Contents
* [Block Pages](#block-pages) - [Block Pages](#block-pages)
* [DNS Server Check](#dnscheck-test) - [DNS Server Check](#dnscheck-test)
* [Linked IP Proxy](#linked-ip-proxy) - [Linked IP Proxy](#linked-ip-proxy)
* [Static Content](#static-content) - [Static Content](#static-content)
[conf-web]: configuration.md#web [conf-web]: configuration.md#web
## <a href="#block-pages" id="block-pages" name="block-pages">Block Pages</a> ## <a href="#block-pages" id="block-pages" name="block-pages">Block Pages</a>
The safe-browsing, adult-blocking, and popup-blocking servers. Every request is The safe-browsing, adult-blocking, and popup-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:
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` - `GET /favicon.ico` requests are responded with a plain-text `404 Not Found` response.
response.
* `GET /robots.txt` requests are responded with: - `GET /robots.txt` requests are responded with:
```none ```none
User-agent: * User-agent: *
@ -33,14 +26,9 @@ responded with the content from the configured file, with the exception of `GET
The [static content](#static-content) is not served on these servers. 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> ## <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 `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.
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.
<!-- <!--
TODO(a.garipov): Describe the check process in details. TODO(a.garipov): Describe the check process in details.
@ -63,60 +51,39 @@ Example of the output:
"node_location": "ams", "node_location": "ams",
"node_name": "eu-1.dns.example.com", "node_name": "eu-1.dns.example.com",
"server_group_name": "adguard_dns_default", "server_group_name": "adguard_dns_default",
"server_name": "default_dns" "server_name": "default_dns",
"server_type": "private"
} }
``` ```
The `protocol` field can have one of the following values: The `protocol` field can have one of the following values:
<dl> - `"dns": Plain DNS.
<dt> - `"dnscrypt": DNSCrypt.
<code>"dns"</code> - `"doh": DNS-over-HTTP.
</dt> - `"doq": DNS-over-QUIC.
<dd> - `"dot": DNS-over-TLS.
Plain DNS.
</dd> The `server_type` field can have one of the following values:
<dt>
<code>"dnscrypt"</code> - `"private"`: A private AdGuard DNS server is used.
</dt>
<dd> > [!NOTE]
DNSCrypt. > This does not mean that the server has recognized the user's device. See the `device_id` and `profile_id` properties for that.
</dd>
<dt> - `"public"`: A public AdGuard DNS server is used.
<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 [conf-check-domains]: configuration.md#check-domains
## <a href="#linked-ip-proxy" id="linked-ip-proxy" name="linked-ip-proxy">Linked IP Proxy</a> ## <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 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 [`LINKED_IP_TARGET_URL`][env-linked_ip_target_url].
IP configuration][conf-web-linked_ip] is not empty, the following queries are
either processed or proxied to [`LINKED_IP_TARGET_URL`][env-linked_ip_target_url].
* `GET /robots.txt`: a special response is served, see below; - `GET /robots.txt`: a special response is served, see below.
* `GET /linkip/{device_id}/{encrypted}/status`: proxied; - `GET /linkip/{device_id}/{encrypted}/status`: proxied.
* `GET /linkip/{device_id}/{encrypted}`: proxied; - `GET /linkip/{device_id}/{encrypted}`: proxied.
* `POST /ddns/{device_id}/{encrypted}/{domain}`: proxied; - `POST /ddns/{device_id}/{encrypted}/{domain}`: proxied.
* `POST /linkip/{device_id}/{encrypted}`: proxied. - `POST /linkip/{device_id}/{encrypted}`: proxied.
In the case of a `GET /robots.txt` request, the following content is served: In the case of a `GET /robots.txt` request, the following content is served:
@ -130,13 +97,8 @@ The [static content](#static-content) is not served on the linked IP addresses.
[conf-web-linked_ip]: configuration.md#web-linked_ip [conf-web-linked_ip]: configuration.md#web-linked_ip
[env-linked_ip_target_url]: environment.md#LINKED_IP_TARGET_URL [env-linked_ip_target_url]: environment.md#LINKED_IP_TARGET_URL
## <a href="#static-content" id="static-content" name="static-content">Static Content</a> ## <a href="#static-content" id="static-content" name="static-content">Static Content</a>
The static content server. Enabled if the [static content 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.
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 [conf-web-static_content]: configuration.md#web-static_content

View File

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

View File

@ -1,106 +1,74 @@
# AdGuard DNS Query Log Format # AdGuard DNS query log format
The query log is written in the [JSONL][jsonl] (JSON Lines) format. The log 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:
entries are designed to be concise and easily compressable. An example of the
log output:
```jsonl ```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,"rn":1234,"f":2,"s":0,"p":8,"r":0} {"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,"rn":1234,"f":2,"s":0,"p":8,"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,"rn":56789,"f":2,"s":0,"p":8,"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,"rn":56789,"f":2,"s":0,"p":8,"r":0}
``` ```
AdGuard DNS opens and closes the log file on each write to prevent issues with AdGuard DNS opens and closes the log file on each write to prevent issues with external log rotation.
external log rotation.
[jsonl]: https://jsonlines.org/ [jsonl]: https://jsonlines.org/
## <a href="#properties" id="properties" name="properties">Properties</a> ## <a href="#properties" id="properties" name="properties">Properties</a>
Property names have been chosen to be single-letter but still have mnemonic Property names have been chosen to be single-letter but still have mnemonic rules to remember, which property means what. The properties are:
rules to remember, which property means what. The properties are:
* <a href="#properties-u" id="properties-u" name="properties-u">`u`</a>: - <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”.
The unique ID of the request. The short name `u` stands for “unique”.
> [!NOTE] > [!NOTE]
> This field is deprecated and may be removed in the future. > This field is deprecated and may be removed in the future.
**Example:** `"ABCD1234"` **Example:** `"ABCD1234"`
* <a href="#properties-b" id="properties-b" name="properties-b">`b`</a>: - <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”.
The detected profile ID (also known as DNS ID and DNS Server ID), if any.
The short name `b` stands for “buyer”.
**Example:** `"prof1234"` **Example:** `"prof1234"`
* <a href="#properties-i" id="properties-i" name="properties-i">`i`</a>: - <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”.
The detected device ID, if any. The short name `i` stands for “ID”.
**Example:** `"dev1234"` **Example:** `"dev1234"`
* <a href="#properties-c" id="properties-c" name="properties-c">`c`</a>: - <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”.
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] > [!NOTE]
> AdGuard DNS uses the common user-assigned ISO 3166-1 alpha-2 code `XK` > AdGuard DNS uses the common user-assigned ISO 3166-1 alpha-2 code `XK` for the partially-recognized state of the Republic of Kosovo.
> for the partially-recognized state of the Republic of Kosovo.
**Example:** `"AU"` **Example:** `"AU"`
* <a href="#properties-d" id="properties-d" name="properties-d">`d`</a>: - <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”.
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] > [!NOTE]
> Just like in the `c` field, `XK` is used for the partially-recognized > Just like in the `c` field, `XK` is used for the partially-recognized state of the Republic of Kosovo. In addition to that, the code `QN`, “Not Applicable”, is used when the resource-record type of the response does not contain any IP-address information (for example, responses to `TXT` requests).
> state of the Republic of Kosovo. In addition to that, the code `QN`,
> “Not Applicable”, is used when the resource-record type of the response
> does not contain any IP-address information (for example, responses to
> `TXT` requests).
**Example:** `"US"` **Example:** `"US"`
* <a href="#properties-n" id="properties-n" name="properties-n">`n`</a>: - <a href="#properties-n" id="properties-n" name="properties-n">`n`</a>: The requested resource name. The short name `n` stands for “name”.
The requested resource name. The short name `n` stands for “name”.
**Example:** `"example.com."` **Example:** `"example.com."`
* <a href="#properties-l" id="properties-l" name="properties-l">`l`</a>: - <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”.
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"` **Example:** `"adguard_dns_filter"`
The special reserved values are: The special reserved values are:
* `blocked_service`: the request was blocked by the service blocker. The - `adult_blocking`: the request was filtered by the adult content blocking filter.
property `m` contains the ID of that blocked service.
* `custom`: the request was filtered by a custom profile rule. - `blocked_service`: the request was blocked by the service blocker. The property `m` contains the ID of that blocked service.
* `adult_blocking`: the request was filtered by the adult content blocking - `custom`: the request was filtered by a custom profile rule.
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.
* `general_safe_search`: the request was modified by the general safe - `newly_registered_domains`: the request was filtered by the newly-registered domains filter.
search filter.
* `youtube_safe_search`: the request was modified by the YouTube safe - `safe_browsing`: the request was filtered by the safe browsing filter.
search filter.
* <a href="#properties-m" id="properties-m" name="properties-m">`m`</a>: - `youtube_safe_search`: the request was modified by the YouTube safe search filter.
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 - <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”.
rules matched, this property is omitted. The short name `m` stands for
“match”.
**Object examples:** **Object examples:**
@ -120,158 +88,66 @@ rules to remember, which property means what. The properties are:
} }
``` ```
* <a href="#properties-t" id="properties-t" name="properties-t">`t`</a>: - <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”.
The [Unix time][wiki-unix] at which the request was received, in
milliseconds. The short name `t` stands for “time”.
**Example:** `1629974298000` **Example:** `1629974298000`
* <a href="#properties-a" id="properties-a" name="properties-a">`a`</a>: - <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”.
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` **Example:** `1234`
* <a href="#properties-e" id="properties-e" name="properties-e">`e`</a>: - <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”.
The time passed since the beginning of the request processing, in
milliseconds. The short name `e` stands for “elapsed”.
**Example:** `3` **Example:** `3`
* <a href="#properties-q" id="properties-q" name="properties-q">`q`</a>: - <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”.
The type of the resource record of the query. The short name `q` stands for
“question”.
**Example:** `1` **Example:** `1`
See [this Wikipedia list][wiki-dnsrr] for numeric values and their meanings. See [this Wikipedia list][wiki-dnsrr] for numeric values and their meanings.
* <a href="#properties-rn" id="properties-rn" name="properties-rn">`rn`</a>: - <a href="#properties-rn" id="properties-rn" name="properties-rn">`rn`</a>: A random 16-bit unsigned integer added to an entry for easier deduplication when `"u"` is not used for that.
A random 16-bit unsigned integer added to an entry for easier deduplication
when `"u"` is not used for that.
**Example:** `12345` **Example:** `12345`
* <a href="#properties-f" id="properties-f" name="properties-f">`f`</a>: - <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:
The action taken with this request. The short name `f` stands for
“filtering”. The possible values are:
<dl> - `0`: invalid or unknown action (typically, this value is not used)
<dt> - `1`: no filtering
<code>0</code> - `2`: the request (question) is blocked
</dt> - `3`: the response (answer) is blocked
<dd> - `4`: the request (question) is allowed by an allowlist rule
Invalid or unknown action. Typically, this value is never used. - `5`: the response (answer) is allowed by an allowlist rule
</dd> - `6`: the request (question) or response (answer) was modified or rewritten by a safety filter or a DNS rewrite rule
<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` **Example:** `2`
* <a href="#properties-s" id="properties-s" name="properties-s">`s`</a>: - <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”.
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` **Example:** `1`
* <a href="#properties-p" id="properties-p" name="properties-p">`p`</a>: - <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:
The DNS protocol used to process this request. The short name `p` stands
for “protocol”. The possible values are:
<dl> - `0`: invalid or unknown protocol (typically, this value is not used)
<dt> - `3`: DNS-over-HTTPS
<code>0</code> - `4`: DNS-over-QUIC
</dt> - `5`: DNS-over-TLS
<dd> - `8`: Plain DNS
Invalid or unknown protocol. Typically, this value is never used. - `9`: DNSCrypt
</dd>
<dt>
<code>3</code>
</dt>
<dd>
DNS-over-HTTPS.
</dd>
<dt>
<code>4</code>
</dt>
<dd>
DNS-over-QUIC.
</dd>
<dt>
<code>5</code>
</dt>
<dd>
DNS-over-TLS.
</dd>
<dt>
<code>8</code>
</dt>
<dd>
Plain DNS.
</dd>
<dt>
<code>9</code>
</dt>
<dd>
DNSCrypt.
</dd>
</dl>
**Example:** `3` **Example:** `3`
* <a href="#properties-r" id="properties-r" name="properties-r">`r`</a>: - <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”.
The response code (aka `RCODE`) sent to the client. The short name `r`
stands for “response”.
**Example:** `0` **Example:** `0`
See [this IANA list][iana-rcode] for numeric values and their meanings. See [this IANA list][iana-rcode] for numeric values and their meanings.
* <a href="#properties-ip" id="properties-ip" name="properties-ip">`ip`</a>: - <a href="#properties-ip" id="properties-ip" name="properties-ip">`ip`</a>: The IP address of the client. This field is omitted in case the IP logging is turned off for the corresponding profile. The short name `ip` stands for “IP”.
The IP address of the client. This field is omitted in case the IP logging
is turned off for the corresponding profile. The short name `ip` stands for
“IP”.
**Example:** `1.2.3.4` **Example:** `1.2.3.4`
See also [file `internal/querylog/entry.go`][file-entry.go] for an explanation See also [file `internal/querylog/entry.go`][file-entry.go] for an explanation of the properties, their names, and mnemonics.
of the properties, their names, and mnemonics.
[file-entry.go]: ../internal/querylog/entry.go [file-entry.go]: ../internal/querylog/entry.go
[iana-rcode]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6 [iana-rcode]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6

50
go.mod
View File

@ -1,32 +1,33 @@
module github.com/AdguardTeam/AdGuardDNS module github.com/AdguardTeam/AdGuardDNS
go 1.22.5 go 1.23.1
require ( require (
github.com/AdguardTeam/AdGuardDNS/internal/dnsserver v0.0.0-20240607112746-5690301129fe github.com/AdguardTeam/AdGuardDNS/internal/dnsserver v0.0.0-20240607112746-5690301129fe
github.com/AdguardTeam/golibs v0.24.0 github.com/AdguardTeam/golibs v0.28.0
github.com/AdguardTeam/urlfilter v0.19.0 github.com/AdguardTeam/urlfilter v0.19.0
github.com/ameshkov/dnscrypt/v2 v2.3.0 github.com/ameshkov/dnscrypt/v2 v2.3.0
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 github.com/axiomhq/hyperloglog v0.2.0
github.com/bluele/gcache v0.0.2 github.com/bluele/gcache v0.0.2
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
github.com/caarlos0/env/v7 v7.1.0 github.com/caarlos0/env/v7 v7.1.0
github.com/getsentry/sentry-go v0.28.1 github.com/getsentry/sentry-go v0.28.1
github.com/gomodule/redigo v1.9.2
github.com/google/renameio/v2 v2.0.0 github.com/google/renameio/v2 v2.0.0
github.com/miekg/dns v1.1.61 github.com/miekg/dns v1.1.62
github.com/oschwald/maxminddb-golang v1.13.0 github.com/oschwald/maxminddb-golang v1.13.1
github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible
github.com/prometheus/client_golang v1.19.1 github.com/prometheus/client_golang v1.20.1
github.com/prometheus/client_model v0.6.1 github.com/prometheus/client_model v0.6.1
github.com/prometheus/common v0.54.0 github.com/prometheus/common v0.55.0
github.com/quic-go/quic-go v0.45.0 github.com/quic-go/quic-go v0.47.0
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.24.0 golang.org/x/crypto v0.27.0
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0
golang.org/x/net v0.26.0 golang.org/x/net v0.29.0
golang.org/x/sys v0.21.0 golang.org/x/sys v0.25.0
golang.org/x/time v0.5.0 golang.org/x/time v0.6.0
google.golang.org/grpc v1.64.0 google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.34.2 google.golang.org/protobuf v1.34.2
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )
@ -40,19 +41,24 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/pprof v0.0.0-20240618054019-d3b898a103f8 // indirect github.com/google/pprof v0.0.0-20240929191954-255acd752d31 // indirect
github.com/onsi/ginkgo/v2 v2.19.0 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.20.2 // indirect
github.com/panjf2000/ants/v2 v2.10.0 // indirect github.com/panjf2000/ants/v2 v2.10.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect
go.uber.org/mock v0.4.0 // indirect go.uber.org/mock v0.4.0 // indirect
golang.org/x/mod v0.18.0 // indirect golang.org/x/mod v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.16.0 // indirect golang.org/x/text v0.18.0 // indirect
golang.org/x/tools v0.22.0 // indirect golang.org/x/tools v0.25.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
replace github.com/AdguardTeam/AdGuardDNS/internal/dnsserver => ./internal/dnsserver replace github.com/AdguardTeam/AdGuardDNS/internal/dnsserver => ./internal/dnsserver
// TODO(a.garipov): Remove once https://github.com/quic-go/quic-go/pull/4685 is merged.
replace github.com/quic-go/quic-go => github.com/ainar-g/quic-go v0.0.0-20240930125330-446bd86056fd

101
go.sum
View File

@ -1,17 +1,19 @@
github.com/AdguardTeam/golibs v0.24.0 h1:qAnOq7BQtwSVo7Co9q703/n+nZ2Ap6smkugU9G9MomY= github.com/AdguardTeam/golibs v0.28.0 h1:SK1q8SqkkJ/61pp2abTmio90S4QpteYK9rtgROfnrb4=
github.com/AdguardTeam/golibs v0.24.0/go.mod h1:9/vJcYznW7RlmCT/Qzi8XNZGj+ZbWfHZJmEXKnRpCAU= github.com/AdguardTeam/golibs v0.28.0/go.mod h1:iWdjXPCwmK2g2FKIb/OwEPnovSXeMqRhI8FWLxF5oxE=
github.com/AdguardTeam/urlfilter v0.19.0 h1:q7eH13+yNETlpD/VD3u5rLQOripcUdEktqZFy+KiQLk= github.com/AdguardTeam/urlfilter v0.19.0 h1:q7eH13+yNETlpD/VD3u5rLQOripcUdEktqZFy+KiQLk=
github.com/AdguardTeam/urlfilter v0.19.0/go.mod h1:+N54ZvxqXYLnXuvpaUhK2exDQW+djZBRSb6F6j0rkBY= github.com/AdguardTeam/urlfilter v0.19.0/go.mod h1:+N54ZvxqXYLnXuvpaUhK2exDQW+djZBRSb6F6j0rkBY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
github.com/ainar-g/quic-go v0.0.0-20240930125330-446bd86056fd h1:mw4LqrCiv3vcKuCxBRg7kA17xfHKM+9hZgFWmyhe/AY=
github.com/ainar-g/quic-go v0.0.0-20240930125330-446bd86056fd/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/ameshkov/dnscrypt/v2 v2.3.0 h1:pDXDF7eFa6Lw+04C0hoMh8kCAQM8NwUdFEllSP2zNLs= github.com/ameshkov/dnscrypt/v2 v2.3.0 h1:pDXDF7eFa6Lw+04C0hoMh8kCAQM8NwUdFEllSP2zNLs=
github.com/ameshkov/dnscrypt/v2 v2.3.0/go.mod h1:N5hDwgx2cNb4Ay7AhvOSKst+eUiOZ/vbKRO9qMpQttE= github.com/ameshkov/dnscrypt/v2 v2.3.0/go.mod h1:N5hDwgx2cNb4Ay7AhvOSKst+eUiOZ/vbKRO9qMpQttE=
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo= github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 h1:60m4tnanN1ctzIu4V3bfCNJ39BiOPSm1gHFlFjTkRE0= github.com/axiomhq/hyperloglog v0.2.0 h1:u1XT3yyY1rjzlWuP6NQIrV4bRYHOaqZaovqjcBEvZJo=
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c= github.com/axiomhq/hyperloglog v0.2.0/go.mod h1:GcgMjz9gaDKZ3G0UMS6Fq/VkZ4l7uGgcJyxA7M+omIM=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
@ -25,39 +27,46 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0= github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k= github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k=
github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg= github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s=
github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20240618054019-d3b898a103f8 h1:ASJ/LAqdCHOyMYI+dwNxn7Rd8FscNkMyTr1KZU1JI/M= github.com/google/pprof v0.0.0-20240929191954-255acd752d31 h1:LcRdQWywSgfi5jPsYZ1r2avbbs5IQ5wtyhMBCcokyo4=
github.com/google/pprof v0.0.0-20240618054019-d3b898a103f8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/pprof v0.0.0-20240929191954-255acd752d31/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/panjf2000/ants/v2 v2.10.0 h1:zhRg1pQUtkyRiOFo2Sbqwjp0GfBNo9cUY2/Grpx1p+8= github.com/panjf2000/ants/v2 v2.10.0 h1:zhRg1pQUtkyRiOFo2Sbqwjp0GfBNo9cUY2/Grpx1p+8=
github.com/panjf2000/ants/v2 v2.10.0/go.mod h1:7ZxyxsqE4vvW0M7LSD8aI3cKwgFhBHbxnlN8mDqHa1I= github.com/panjf2000/ants/v2 v2.10.0/go.mod h1:7ZxyxsqE4vvW0M7LSD8aI3cKwgFhBHbxnlN8mDqHa1I=
github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible h1:IWzUvJ72xMjmrjR9q3H1PF+jwdN0uNQiR2t1BLNalyo= github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible h1:IWzUvJ72xMjmrjR9q3H1PF+jwdN0uNQiR2t1BLNalyo=
@ -70,18 +79,16 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.20.1 h1:IMJXHOD6eARkQpxo8KkhgEVFlBNm+nkrFUyGlIu7Na8=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_golang v1.20.1/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8= github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ= github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.45.0 h1:OHmkQGM37luZITyTSu6ff03HP/2IrwDX1ZFiNEhSFUE=
github.com/quic-go/quic-go v0.45.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
@ -104,29 +111,29 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -1,4 +1,4 @@
go 1.22.5 go 1.23.1
use ( use (
. .

View File

@ -1,3 +1,5 @@
cel.dev/expr v0.15.0 h1:O1jzfJCQBfL5BFoYktaxwIhuttaQPsVWerH9/EEKx0w=
cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
@ -41,6 +43,8 @@ cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//u
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/contactcenterinsights v1.13.0/go.mod h1:ieq5d5EtHsu8vhe2y3amtZ+BE+AQwX5qAy7cpo0POsI= cloud.google.com/go/contactcenterinsights v1.13.0/go.mod h1:ieq5d5EtHsu8vhe2y3amtZ+BE+AQwX5qAy7cpo0POsI=
cloud.google.com/go/container v1.31.0/go.mod h1:7yABn5s3Iv3lmw7oMmyGbeV6tQj86njcTijkkGuvdZA= cloud.google.com/go/container v1.31.0/go.mod h1:7yABn5s3Iv3lmw7oMmyGbeV6tQj86njcTijkkGuvdZA=
cloud.google.com/go/containeranalysis v0.11.4/go.mod h1:cVZT7rXYBS9NG1rhQbWL9pWbXCKHWJPYraE8/FTSYPE= cloud.google.com/go/containeranalysis v0.11.4/go.mod h1:cVZT7rXYBS9NG1rhQbWL9pWbXCKHWJPYraE8/FTSYPE=
@ -149,6 +153,9 @@ git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGy
github.com/AdguardTeam/golibs v0.10.7/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw= github.com/AdguardTeam/golibs v0.10.7/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
github.com/AdguardTeam/golibs v0.17.2/go.mod h1:DKhCIXHcUYtBhU8ibTLKh1paUL96n5zhQBlx763sj+U= github.com/AdguardTeam/golibs v0.17.2/go.mod h1:DKhCIXHcUYtBhU8ibTLKh1paUL96n5zhQBlx763sj+U=
github.com/AdguardTeam/golibs v0.19.0/go.mod h1:3WunclLLfrVAq7fYQRhd6f168FHOEMssnipVXCxDL/w= github.com/AdguardTeam/golibs v0.19.0/go.mod h1:3WunclLLfrVAq7fYQRhd6f168FHOEMssnipVXCxDL/w=
github.com/AdguardTeam/golibs v0.25.2/go.mod h1:HaTyS2wCbxFudjht9N/+/Qf1b5cMad2BAYSwe7DPCXI=
github.com/AdguardTeam/golibs v0.25.3 h1:A06JZGSuAhAC0uq/s7IlNsv/V8TyNJfLalB0vhkd1vA=
github.com/AdguardTeam/golibs v0.25.3/go.mod h1:HaTyS2wCbxFudjht9N/+/Qf1b5cMad2BAYSwe7DPCXI=
github.com/AdguardTeam/gomitmproxy v0.2.0 h1:rvCOf17pd1/CnMyMQW891zrEiIQBpQ8cIGjKN9pinUU= github.com/AdguardTeam/gomitmproxy v0.2.0 h1:rvCOf17pd1/CnMyMQW891zrEiIQBpQ8cIGjKN9pinUU=
github.com/AdguardTeam/gomitmproxy v0.2.1 h1:p9gr8Er1TYvf+7ic81Ax1sZ62UNCsMTZNbm7tC59S9o= github.com/AdguardTeam/gomitmproxy v0.2.1 h1:p9gr8Er1TYvf+7ic81Ax1sZ62UNCsMTZNbm7tC59S9o=
github.com/AdguardTeam/gomitmproxy v0.2.1/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU= github.com/AdguardTeam/gomitmproxy v0.2.1/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
@ -236,6 +243,8 @@ github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa h1:jQCWAUqqlij9Pgj2i/P
github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa/go.mod h1:x/1Gn8zydmfq8dk6e9PdstVsDgu9RuyIIJqAaF//0IM= github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa/go.mod h1:x/1Gn8zydmfq8dk6e9PdstVsDgu9RuyIIJqAaF//0IM=
github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 h1:DBmgJDC9dTfkVyGgipamEh2BpGYxScCH1TOF1LL1cXc= github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 h1:DBmgJDC9dTfkVyGgipamEh2BpGYxScCH1TOF1LL1cXc=
github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM= github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM=
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw=
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d h1:t5Wuyh53qYyg9eqn4BbnlIT+vmhyww0TatL+zT3uWgI= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d h1:t5Wuyh53qYyg9eqn4BbnlIT+vmhyww0TatL+zT3uWgI=
@ -293,6 +302,7 @@ github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KE
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
@ -303,6 +313,7 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
@ -325,6 +336,8 @@ github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4=
github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -378,6 +391,7 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99 h1:Ak8CrdlwwXwAZxzS66
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
@ -548,6 +562,7 @@ github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJ
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc=
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM=
github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
@ -563,6 +578,7 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
@ -588,7 +604,6 @@ github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiy
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4 h1:Fth6mevc5rX7glNLpbAMJnqKlfIkcTjZCSHEeqvKbcI= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4 h1:Fth6mevc5rX7glNLpbAMJnqKlfIkcTjZCSHEeqvKbcI=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48 h1:vabduItPAIz9px5iryD5peyx7O3Ya8TBThapgXim98o= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48 h1:vabduItPAIz9px5iryD5peyx7O3Ya8TBThapgXim98o=
@ -658,8 +673,6 @@ github.com/tdewolff/minify/v2 v2.12.4 h1:kejsHQMM17n6/gwdw53qsi6lg0TGddZADVyQOz1
github.com/tdewolff/minify/v2 v2.12.4/go.mod h1:h+SRvSIX3kwgwTFOpSckvSxgax3uy8kZTSF1Ojrr3bk= github.com/tdewolff/minify/v2 v2.12.4/go.mod h1:h+SRvSIX3kwgwTFOpSckvSxgax3uy8kZTSF1Ojrr3bk=
github.com/tdewolff/parse/v2 v2.6.4 h1:KCkDvNUMof10e3QExio9OPZJT8SbdKojLBumw8YZycQ= github.com/tdewolff/parse/v2 v2.6.4 h1:KCkDvNUMof10e3QExio9OPZJT8SbdKojLBumw8YZycQ=
github.com/tdewolff/parse/v2 v2.6.4/go.mod h1:woz0cgbLwFdtbjJu8PIKxhW05KplTFQkOdX78o+Jgrs= github.com/tdewolff/parse/v2 v2.6.4/go.mod h1:woz0cgbLwFdtbjJu8PIKxhW05KplTFQkOdX78o+Jgrs=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
@ -704,7 +717,6 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf
github.com/yuin/goldmark v1.4.1 h1:/vn0k+RBvwlxEmP5E7SZMqNxPhfMVFEJiykr15/0XKM= github.com/yuin/goldmark v1.4.1 h1:/vn0k+RBvwlxEmP5E7SZMqNxPhfMVFEJiykr15/0XKM=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M= go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto=
@ -743,6 +755,9 @@ golang.org/x/exp v0.0.0-20230807204917-050eac23e9de/go.mod h1:FXUEEKJgO7OQYeo8N0
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -791,6 +806,9 @@ golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -808,6 +826,8 @@ golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852 h1:xYq6+9AtI+xP3M4r0N1hCkHrInHDBohhquRgx9Kk6gI= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852 h1:xYq6+9AtI+xP3M4r0N1hCkHrInHDBohhquRgx9Kk6gI=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -819,6 +839,7 @@ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -852,6 +873,10 @@ golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808 h1:+Kc94D8UVEVxJnLXp/+FMfqQARZtWHfVrcRtcG8aT3g= golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808 h1:+Kc94D8UVEVxJnLXp/+FMfqQARZtWHfVrcRtcG8aT3g=
golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY=
@ -882,6 +907,11 @@ golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@ -893,8 +923,11 @@ golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -917,6 +950,7 @@ golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -940,6 +974,7 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440 h1:VOR2wHHZJgoALLvnlCN4JUaWACO1lOLXiSN2F3g/GXU=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
@ -963,6 +998,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.
google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4= google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:vh/N7795ftP0AkN1w8XKqN4w1OdUKXW5Eummda+ofv8= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:vh/N7795ftP0AkN1w8XKqN4w1OdUKXW5Eummda+ofv8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
@ -992,6 +1029,7 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=

View File

@ -10,8 +10,6 @@ import (
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
) )
// Common Context Helpers
// ctxKey is the type for all common context keys. // ctxKey is the type for all common context keys.
type ctxKey uint8 type ctxKey uint8
@ -65,20 +63,15 @@ func RequestIDFromContext(ctx context.Context) (id RequestID, ok bool) {
// RequestInfo contains information about the current request. A RequestInfo // RequestInfo contains information about the current request. A RequestInfo
// put into the context must not be modified. // put into the context must not be modified.
type RequestInfo struct { type RequestInfo struct {
// Device is the found device. It is nil for anonymous requests. If Device // DeviceResult is the result of finding the device.
// is present then Profile is also present. DeviceResult DeviceResult
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 is the GeoIP location data about the remote IP address, if any.
Location *geoip.Location Location *geoip.Location
// ECS contains the EDNS Client Subnet option information of the request, if // ECS contains the EDNS Client Subnet option information of the request, if
// any. // any.
ECS *ECS ECS *dnsmsg.ECS
// FilteringGroup is the server's default filtering group. // FilteringGroup is the server's default filtering group.
FilteringGroup *FilteringGroup FilteringGroup *FilteringGroup
@ -87,12 +80,12 @@ type RequestInfo struct {
// to this request. // to this request.
Messages *dnsmsg.Constructor Messages *dnsmsg.Constructor
// ServerGroup is the server group which handles this request.
ServerGroup *ServerGroup
// RemoteIP is the remote IP address of the client. // RemoteIP is the remote IP address of the client.
RemoteIP netip.Addr 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 is the name of the server which handles this request.
Server ServerName Server ServerName
@ -114,19 +107,14 @@ type RequestInfo struct {
Proto Protocol Proto Protocol
} }
// ECS is the content of the EDNS Client Subnet option of a DNS message. // DeviceData returns the profile and device data if any. Either both p and d
// // are nil or neither is nil.
// See https://datatracker.ietf.org/doc/html/rfc7871#section-6. func (ri *RequestInfo) DeviceData() (p *Profile, d *Device) {
type ECS struct { if r, ok := ri.DeviceResult.(*DeviceResultOK); ok {
// Location is the GeoIP location data about the IP address from the return r.Profile, r.Device
// request's ECS data, if any. }
Location *geoip.Location
// Subnet is the source subnet. return nil, nil
Subnet netip.Prefix
// Scope is the scope prefix.
Scope uint8
} }
// ContextWithRequestInfo returns a copy of the parent context with the request // ContextWithRequestInfo returns a copy of the parent context with the request

View File

@ -0,0 +1,106 @@
package agd
import (
"context"
"net/netip"
"github.com/miekg/dns"
)
// DeviceFinder finds the user data, such as the profile and a device for a
// request.
//
// TODO(a.garipov): Move device-related stuff to agddevice.
type DeviceFinder interface {
// Find returns the profile and device data in ri if it can recognize those.
// All arguments must not be empty. A nil result means that the profile and
// device data could not be found.
Find(ctx context.Context, req *dns.Msg, raddr, laddr netip.AddrPort) (r DeviceResult)
}
// EmptyDeviceFinder is an [DeviceFinder] implementation that does nothing.
type EmptyDeviceFinder struct{}
// type check
var _ DeviceFinder = EmptyDeviceFinder{}
// Find implements the [DeviceFinder] interface for EmptyDeviceFinder. It does
// nothing and returns nil.
func (EmptyDeviceFinder) Find(_ context.Context, _ *dns.Msg, _, _ netip.AddrPort) (r DeviceResult) {
return nil
}
// DeviceResult is the sum type of the results that can be returned by a
// [DeviceFinder] implementation.
//
// The implementations are:
//
// - [*DeviceResultAuthenticationFailure]
// - [*DeviceResultError]
// - [*DeviceResultOK]
// - [*DeviceResultUnknownDedicated]
//
// A nil result means that the user data was not found.
type DeviceResult interface {
isResult()
}
// DeviceResultAuthenticationFailure means that the profile and the device have
// been found, but the device should have been authenticated but could not. For
// generic errors, see [DeviceResultError].
type DeviceResultAuthenticationFailure struct {
Err error
}
// type check
var _ DeviceResult = (*DeviceResultAuthenticationFailure)(nil)
// isResult implements the [DeviceResult] interface for
// *DeviceResultAuthenticationFailure.
func (*DeviceResultAuthenticationFailure) isResult() {}
// DeviceResultError is a generic error result. For authentication errors, see
// [DeviceResultAuthenticationFailure].
type DeviceResultError struct {
Err error
}
// type check
var _ DeviceResult = (*DeviceResultError)(nil)
// isResult implements the [DeviceResult] interface for *DeviceResultError.
func (*DeviceResultError) isResult() {}
// DeviceResultOK is a successful result that contains the profile and the
// device data. If the device requires authentication, DeviceResultOK implies
// that the authentication has been successful. See also
// [DeviceResultAuthenticationFailure].
type DeviceResultOK struct {
// Device is the device that has been found. It must not be nil and it must
// belong to the profile.
Device *Device
// Profile is the profile that has been found. It must not be nil or
// deleted, and the device must belong to it.
Profile *Profile
}
// type check
var _ DeviceResult = (*DeviceResultOK)(nil)
// isResult implements the [DeviceResult] interface for *DeviceResultOK.
func (*DeviceResultOK) isResult() {}
// DeviceResultUnknownDedicated means that the request has been made for a
// dedicated IP address with no corresponding profile or device data. For
// generic errors, see [DeviceResultError].
type DeviceResultUnknownDedicated struct {
Err error
}
// type check
var _ DeviceResult = (*DeviceResultUnknownDedicated)(nil)
// isResult implements the [DeviceResult] interface for
// *DeviceResultUnknownDedicated.
func (*DeviceResultUnknownDedicated) isResult() {}

View File

@ -62,6 +62,7 @@ func DeviceTypeFromDNS(s string) (dt DeviceType, err error) {
for i, dtStr := range deviceTypeStrings[1:] { for i, dtStr := range deviceTypeStrings[1:] {
if strings.EqualFold(s, dtStr) { if strings.EqualFold(s, dtStr) {
// #nosec G115 -- i is below math.MaxUint8.
return DeviceType(i + 1), nil return DeviceType(i + 1), nil
} }
} }

View File

@ -29,6 +29,13 @@ type Profile struct {
// [internal/profiledb/internal.FileCacheVersion]. // [internal/profiledb/internal.FileCacheVersion].
Parental *ParentalProtectionSettings Parental *ParentalProtectionSettings
// Ratelimiter is the custom ratelimiter for this profile. It must not be
// nil.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
Ratelimiter Ratelimiter
// SafeBrowsing are the safe browsing settings for this profile. They are // SafeBrowsing are the safe browsing settings for this profile. They are
// ignored if FilteringEnabled is set to false. // ignored if FilteringEnabled is set to false.
// //
@ -36,19 +43,20 @@ type Profile struct {
// [internal/profiledb/internal.FileCacheVersion]. // [internal/profiledb/internal.FileCacheVersion].
SafeBrowsing *SafeBrowsingSettings SafeBrowsing *SafeBrowsingSettings
// Access is the access manager for this profile. Access is never nil. // Access is the access manager for this profile. It must not be nil.
// //
// NOTE: Do not change fields of this structure without incrementing // NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion]. // [internal/profiledb/internal.FileCacheVersion].
Access access.Profile Access access.Profile
// BlockingMode defines the way blocked responses are constructed. // BlockingMode defines the way blocked responses are constructed. It must
// not be nil.
// //
// NOTE: Do not change fields of this structure without incrementing // NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion]. // [internal/profiledb/internal.FileCacheVersion].
BlockingMode dnsmsg.BlockingMode BlockingMode dnsmsg.BlockingMode
// ID is the unique ID of this profile. // ID is the unique ID of this profile. It must not be empty.
// //
// NOTE: Do not change fields of this structure without incrementing // NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion]. // [internal/profiledb/internal.FileCacheVersion].

147
internal/agd/ratelimit.go Normal file
View File

@ -0,0 +1,147 @@
package agd
import (
"context"
"net/netip"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/ratelimit"
"github.com/AdguardTeam/golibs/netutil"
"github.com/c2h5oh/datasize"
"github.com/miekg/dns"
)
// RatelimitConfig are the rate-limiting settings of a profile.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
type RatelimitConfig struct {
// ClientSubnets are the optional subnets for which to apply the custom
// limit. If empty, the custom limit is applied to all clients.
ClientSubnets []netip.Prefix
// RPS is the rate limit for this profile.
RPS uint32
// Enabled defines whether the custom limit should be enforced.
Enabled bool
}
// Ratelimiter is the interface for profiles' custom ratelimiters.
//
// TODO(a.garipov): Refactor ratelimit packages.
type Ratelimiter interface {
// Check reports the result of checking the request against the ratelimiter.
// req must not be nil.
Check(ctx context.Context, req *dns.Msg, remoteIP netip.Addr) (res RatelimitResult)
// Config returns the configuration for this ratelimiter. conf must never
// be nil.
Config() (conf *RatelimitConfig)
// CountResponses adds the response to the counter. resp must not be nil.
CountResponses(ctx context.Context, resp *dns.Msg, remoteIP netip.Addr)
}
// GlobalRatelimiter is a [Ratelimiter] implementation that always returns
// [RatelimitResultUseGlobal] from its Check method.
type GlobalRatelimiter struct{}
// type check
var _ Ratelimiter = GlobalRatelimiter{}
// Check implements the [Ratelimiter] interface for GlobalRatelimiter. It
// always returns [RatelimitResultUseGlobal].
func (GlobalRatelimiter) Check(_ context.Context, _ *dns.Msg, _ netip.Addr) (res RatelimitResult) {
return RatelimitResultUseGlobal
}
// Config implements the [Ratelimiter] interface for GlobalRatelimiter. It
// returns an empty config.
func (GlobalRatelimiter) Config() (_ *RatelimitConfig) { return &RatelimitConfig{} }
// CountResponses implements the [Ratelimiter] interface for GlobalRatelimiter.
func (GlobalRatelimiter) CountResponses(_ context.Context, _ *dns.Msg, _ netip.Addr) {}
// DefaultRatelimiter is the default [Ratelimiter] impelentation.
//
// TODO(a.garipov): Add tests.
type DefaultRatelimiter struct {
counter *ratelimit.RequestCounter
clientSubnets netutil.SliceSubnetSet
respSzEst datasize.ByteSize
rps uint32
}
// NewDefaultRatelimiter returns a properly initialized *DefaultRatelimiter.
// conf must not be nil.
func NewDefaultRatelimiter(
conf *RatelimitConfig,
respSzEst datasize.ByteSize,
) (r *DefaultRatelimiter) {
return &DefaultRatelimiter{
counter: ratelimit.NewRequestCounter(uint(conf.RPS), time.Second),
clientSubnets: conf.ClientSubnets,
respSzEst: respSzEst,
rps: conf.RPS,
}
}
// type check
var _ Ratelimiter = (*DefaultRatelimiter)(nil)
// Check implements the [Ratelimiter] interface for *DefaultRatelimiter.
func (r *DefaultRatelimiter) Check(
ctx context.Context,
req *dns.Msg,
remoteIP netip.Addr,
) (res RatelimitResult) {
if len(r.clientSubnets) > 0 && !r.clientSubnets.Contains(remoteIP) {
return RatelimitResultUseGlobal
}
if r.counter.Add(time.Now()) {
return RatelimitResultDrop
}
return RatelimitResultPass
}
// Config implements the [Ratelimiter] interface for *DefaultRatelimiter.
func (r *DefaultRatelimiter) Config() (conf *RatelimitConfig) {
return &RatelimitConfig{
ClientSubnets: r.clientSubnets,
RPS: r.rps,
Enabled: true,
}
}
// CountResponses implements the [Ratelimiter] interface for
// *DefaultRatelimiter.
func (r *DefaultRatelimiter) CountResponses(
ctx context.Context,
resp *dns.Msg,
remoteIP netip.Addr,
) {
// #nosec G115 -- Assume that resp.Len is always non-negative.
estRespNum := datasize.ByteSize(resp.Len()) / r.respSzEst
for range estRespNum {
_ = r.Check(ctx, resp, remoteIP)
}
}
// RatelimitResult defines what to do with a request.
type RatelimitResult uint8
// RatelimitResult constants.
const (
// RatelimitResultPass means that the request should be passed.
RatelimitResultPass RatelimitResult = iota + 1
// RatelimitResultDrop means that the request should be dropped.
RatelimitResultDrop
// RatelimitResultUseGlobal means that the request should be checked with
// the global ratelimit.
RatelimitResultUseGlobal
)

View File

@ -24,6 +24,7 @@ var requestIDRand = rand.New(&rand.LockedSource{})
// InitRequestID initializes the [RequestID] generator. // InitRequestID initializes the [RequestID] generator.
func InitRequestID() { func InitRequestID() {
// #nosec G115 -- The Unix epoch time is highly unlikely to be negative.
requestIDRand.Seed(uint64(time.Now().UnixNano())) requestIDRand.Seed(uint64(time.Now().UnixNano()))
} }

View File

@ -11,7 +11,7 @@ import (
// settings. // settings.
type ServerGroup struct { type ServerGroup struct {
// DDR is the configuration for the server group's Discovery Of Designated // DDR is the configuration for the server group's Discovery Of Designated
// Resolvers (DDR) handlers. DDR is never nil. // Resolvers (DDR) handlers. DDR must not be nil.
DDR *DDR DDR *DDR
// TLS are the TLS settings for this server group. If Servers contains at // TLS are the TLS settings for this server group. If Servers contains at
@ -22,11 +22,15 @@ type ServerGroup struct {
// Name is the unique name of the server group. // Name is the unique name of the server group.
Name ServerGroupName Name ServerGroupName
// FilteringGroup is the ID of the filtering group for this server. // FilteringGroup is the ID of the filtering group for this server group.
FilteringGroup FilteringGroupID FilteringGroup FilteringGroupID
// Servers are the settings for servers. Each element must be non-nil. // Servers are the settings for servers. Each element must be non-nil.
Servers []*Server Servers []*Server
// ProfilesEnabled, if true, enables recognition of user devices and
// profiles for this server group.
ProfilesEnabled bool
} }
// ServerGroupName is the name of a server group. // ServerGroupName is the name of a server group.
@ -37,9 +41,9 @@ type TLS struct {
// Conf is the server's TLS configuration. // Conf is the server's TLS configuration.
Conf *tls.Config Conf *tls.Config
// DeviceIDWildcards are the domain wildcards used to detect device IDs from // DeviceDomains are the domain names used to detect device IDs from
// clients' server names. // clients' server names.
DeviceIDWildcards []string DeviceDomains []string
// SessionKeys are paths to files containing the TLS session keys for this // SessionKeys are paths to files containing the TLS session keys for this
// server. // server.

View File

@ -1,33 +0,0 @@
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

@ -1,10 +1,9 @@
package agdcache package agdcache
import ( import (
"maps"
"slices" "slices"
"sync" "sync"
"golang.org/x/exp/maps"
) )
// Manager is the cache manager interface. All methods must be safe for // Manager is the cache manager interface. All methods must be safe for
@ -70,10 +69,7 @@ func (m *DefaultManager) IDs() (ids []string) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
ids = maps.Keys(m.caches) return slices.Sorted(maps.Keys(m.caches))
slices.Sort(ids)
return ids
} }
// EmptyManager implements the [Manager] interface that does nothing. // EmptyManager implements the [Manager] interface that does nothing.

View File

@ -4,17 +4,14 @@
// TODO(a.garipov): Consider moving all or some of this stuff to module golibs. // TODO(a.garipov): Consider moving all or some of this stuff to module golibs.
package agdhttp package agdhttp
import ( import "github.com/AdguardTeam/AdGuardDNS/internal/version"
"fmt"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
)
// Common Constants, Functions And Types // Common Constants, Functions And Types
// HTTP header value constants. // HTTP header value constants.
const ( const (
HdrValApplicationJSON = "application/json" HdrValApplicationJSON = "application/json"
HdrValApplicationOctetStream = "application/octet-stream"
HdrValGzip = "gzip" HdrValGzip = "gzip"
HdrValTextCSV = "text/csv" HdrValTextCSV = "text/csv"
HdrValTextHTML = "text/html" HdrValTextHTML = "text/html"
@ -25,8 +22,11 @@ const (
// RobotsDisallowAll is a predefined robots disallow all content. // RobotsDisallowAll is a predefined robots disallow all content.
const RobotsDisallowAll = "User-agent: *\nDisallow: /\n" const RobotsDisallowAll = "User-agent: *\nDisallow: /\n"
// userAgent is the cached User-Agent string for AdGuardDNS.
var userAgent = version.Name() + "/" + version.Version()
// UserAgent returns the ID of the service as a User-Agent string. It can also // UserAgent returns the ID of the service as a User-Agent string. It can also
// be used as the value of the Server HTTP header. // be used as the value of the Server HTTP header.
func UserAgent() (ua string) { func UserAgent() (ua string) {
return fmt.Sprintf("AdGuardDNS/%s", agd.Version()) return userAgent
} }

View File

@ -7,7 +7,44 @@ import (
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
) )
// URL Types And Utilities // Known scheme constants.
//
// TODO(a.garipov): Move to agdurl or golibs.
//
// TODO(a.garipov): Use more.
const (
SchemeFile = "file"
SchemeGRPC = "grpc"
SchemeGRPCS = "grpcs"
SchemeHTTP = "http"
SchemeHTTPS = "https"
)
// CheckGRPCURLScheme returns true if s is a valid gRPC URL scheme. That is,
// [SchemeGRPC] or [SchemeGRPCS]
//
// TODO(a.garipov): Move to golibs?
func CheckGRPCURLScheme(s string) (ok bool) {
switch s {
case SchemeGRPC, SchemeGRPCS:
return true
default:
return false
}
}
// CheckHTTPURLScheme returns true if s is a valid HTTP URL scheme. That is,
// [SchemeHTTP] or [SchemeHTTPS]
//
// TODO(a.garipov): Move to golibs?
func CheckHTTPURLScheme(s string) (ok bool) {
switch s {
case SchemeHTTP, SchemeHTTPS:
return true
default:
return false
}
}
// ParseHTTPURL parses an absolute URL and makes sure that it is a valid HTTP(S) // 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]. // URL. All returned errors will have the underlying type [*url.Error].
@ -26,7 +63,7 @@ func ParseHTTPURL(s string) (u *url.URL, err error) {
URL: s, URL: s,
Err: errors.Error("empty host"), Err: errors.Error("empty host"),
} }
case u.Scheme != "http" && u.Scheme != "https": case !CheckHTTPURLScheme(u.Scheme):
return nil, &url.Error{ return nil, &url.Error{
Op: "parse", Op: "parse",
URL: s, URL: s,

View File

@ -63,7 +63,7 @@ func TestParseHTTPURL(t *testing.T) {
func testURL() (u *url.URL) { func testURL() (u *url.URL) {
return &url.URL{ return &url.URL{
Scheme: "http", Scheme: agdhttp.SchemeHTTP,
User: url.UserPassword("user", "pass"), User: url.UserPassword("user", "pass"),
Host: "example.com", Host: "example.com",
Path: "/a/b/c/", Path: "/a/b/c/",

View File

@ -1,15 +1,6 @@
package agdservice_test package agdservice_test
import ( import "time"
"testing"
"time"
"github.com/AdguardTeam/golibs/testutil"
)
func TestMain(m *testing.M) {
testutil.DiscardLogOutput(m)
}
// testTimeout is the timeout for common test operations. // testTimeout is the timeout for common test operations.
const testTimeout = 1 * time.Second const testTimeout = 1 * time.Second

View File

@ -3,11 +3,13 @@ package agdservice
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog"
"time" "time"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll" "github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/service" "github.com/AdguardTeam/golibs/service"
"github.com/AdguardTeam/golibs/timeutil"
"golang.org/x/exp/rand" "golang.org/x/exp/rand"
) )
@ -23,12 +25,12 @@ type Refresher interface {
// RefreshWorker is an [Interface] implementation that updates its [Refresher] // RefreshWorker is an [Interface] implementation that updates its [Refresher]
// every tick of the provided ticker. // every tick of the provided ticker.
type RefreshWorker struct { type RefreshWorker struct {
logger *slog.Logger
done chan unit done chan unit
context func() (ctx context.Context, cancel context.CancelFunc) context func() (ctx context.Context, cancel context.CancelFunc)
tick *time.Ticker tick *time.Ticker
rand *rand.Rand rand *rand.Rand
refr Refresher refr Refresher
name string
maxStartSleep time.Duration maxStartSleep time.Duration
refrOnShutdown bool refrOnShutdown bool
@ -37,17 +39,17 @@ type RefreshWorker struct {
// RefreshWorkerConfig is the configuration structure for a *RefreshWorker. // RefreshWorkerConfig is the configuration structure for a *RefreshWorker.
type RefreshWorkerConfig struct { type RefreshWorkerConfig struct {
// Context is used to provide a context for the Refresh method of Refresher. // Context is used to provide a context for the Refresh method of Refresher.
//
// NOTE: It is not used for the shutdown refresh.
//
// TODO(a.garipov): Consider ways of fixing that.
Context func() (ctx context.Context, cancel context.CancelFunc) Context func() (ctx context.Context, cancel context.CancelFunc)
// Refresher is the entity being refreshed. // Refresher is the entity being refreshed.
Refresher Refresher Refresher Refresher
// Name is the name of this worker. It is used for logging and error // Logger is used for logging the operation of the worker.
// collecting. Logger *slog.Logger
//
// TODO(a.garipov): Consider accepting a slog.Logger or removing this and
// making all Refreshers handle their own logging.
Name string
// Interval is the refresh interval. Must be greater than zero. // Interval is the refresh interval. Must be greater than zero.
// //
@ -76,16 +78,17 @@ func NewRefreshWorker(c *RefreshWorkerConfig) (w *RefreshWorker) {
var rng *rand.Rand var rng *rand.Rand
if c.RandomizeStart { if c.RandomizeStart {
maxStartSleep = c.Interval / 10 maxStartSleep = c.Interval / 10
// #nosec G115 -- The Unix epoch time is highly unlikely to be negative.
rng = rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) rng = rand.New(rand.NewSource(uint64(time.Now().UnixNano())))
} }
return &RefreshWorker{ return &RefreshWorker{
logger: c.Logger,
done: make(chan unit), done: make(chan unit),
context: c.Context, context: c.Context,
tick: time.NewTicker(c.Interval), tick: time.NewTicker(c.Interval),
rand: rng, rand: rng,
refr: c.Refresher, refr: c.Refresher,
name: c.Name,
maxStartSleep: maxStartSleep, maxStartSleep: maxStartSleep,
refrOnShutdown: c.RefreshOnShutdown, refrOnShutdown: c.RefreshOnShutdown,
} }
@ -103,20 +106,22 @@ func (w *RefreshWorker) Start(_ context.Context) (err error) {
} }
// Shutdown implements the [service.Interface] interface for *RefreshWorker. // Shutdown implements the [service.Interface] interface for *RefreshWorker.
//
// NOTE: The context provided by [RefreshWorkerConfig.Context] is not used for
// the shutdown refresh.
func (w *RefreshWorker) Shutdown(ctx context.Context) (err error) { func (w *RefreshWorker) Shutdown(ctx context.Context) (err error) {
if w.refrOnShutdown { if w.refrOnShutdown {
err = w.refr.Refresh(ctx) err = w.refr.Refresh(slogutil.ContextWithLogger(ctx, w.logger))
} }
close(w.done) close(w.done)
w.tick.Stop() w.tick.Stop()
name := w.name
if err != nil { if err != nil {
err = fmt.Errorf("refresh on shutdown: %w", err) err = fmt.Errorf("refresh on shutdown: %w", err)
} else { } else {
log.Info("worker %q: shut down successfully", name) w.logger.InfoContext(ctx, "shut down successfully")
} }
return err return err
@ -125,19 +130,19 @@ func (w *RefreshWorker) Shutdown(ctx context.Context) (err error) {
// refreshInALoop refreshes the entity every tick of w.tick until Shutdown is // refreshInALoop refreshes the entity every tick of w.tick until Shutdown is
// called. // called.
func (w *RefreshWorker) refreshInALoop() { func (w *RefreshWorker) refreshInALoop() {
name := w.name ctx := context.Background()
defer log.OnPanic(name) defer slogutil.RecoverAndLog(ctx, w.logger)
log.Info("worker %q: starting refresh loop", name) w.logger.InfoContext(ctx, "starting refresh loop")
for { for {
select { select {
case <-w.done: case <-w.done:
log.Info("worker %q: finished refresh loop", name) w.logger.InfoContext(ctx, "finished refresh loop")
return return
case <-w.tick.C: case <-w.tick.C:
if w.sleepRandom() { if w.sleepRandom(ctx) {
w.refresh() w.refresh()
} }
} }
@ -146,13 +151,17 @@ func (w *RefreshWorker) refreshInALoop() {
// sleepRandom sleeps for up to maxStartSleep unless it's zero. shouldRefresh // sleepRandom sleeps for up to maxStartSleep unless it's zero. shouldRefresh
// shows if a refresh should be performed once the sleep is finished. // shows if a refresh should be performed once the sleep is finished.
func (w *RefreshWorker) sleepRandom() (shouldRefresh bool) { func (w *RefreshWorker) sleepRandom(ctx context.Context) (shouldRefresh bool) {
if w.maxStartSleep == 0 { if w.maxStartSleep == 0 {
return true return true
} }
sleepDur := time.Duration(w.rand.Int63n(int64(w.maxStartSleep))) sleepDur := time.Duration(w.rand.Int63n(int64(w.maxStartSleep)))
log.Debug("worker %q: sleeping for %s before refresh", w.name, sleepDur) // TODO(a.garipov): Augment our JSON handler to use time.Duration.String
// automatically?
w.logger.DebugContext(ctx, "sleeping before refresh", "dur", timeutil.Duration{
Duration: sleepDur,
})
timer := time.NewTimer(sleepDur) timer := time.NewTimer(sleepDur)
defer func() { defer func() {
@ -183,14 +192,16 @@ func (w *RefreshWorker) refresh() {
ctx, cancel := w.context() ctx, cancel := w.context()
defer cancel() defer cancel()
ctx = slogutil.ContextWithLogger(ctx, w.logger)
_ = w.refr.Refresh(ctx) _ = w.refr.Refresh(ctx)
} }
// RefresherWithErrColl reports all refresh errors to errColl and logs them // RefresherWithErrColl reports all refresh errors to errColl and logs them
// using a provided logging function. // using a provided logging function.
type RefresherWithErrColl struct { type RefresherWithErrColl struct {
logger *slog.Logger
refr Refresher refr Refresher
log func(format string, args ...any)
errColl errcoll.Interface errColl errcoll.Interface
prefix string prefix string
} }
@ -199,13 +210,13 @@ type RefresherWithErrColl struct {
// logs them. // logs them.
func NewRefresherWithErrColl( func NewRefresherWithErrColl(
refr Refresher, refr Refresher,
logFunc func(format string, args ...any), logger *slog.Logger,
errColl errcoll.Interface, errColl errcoll.Interface,
prefix string, prefix string,
) (wrapped *RefresherWithErrColl) { ) (wrapped *RefresherWithErrColl) {
return &RefresherWithErrColl{ return &RefresherWithErrColl{
refr: refr, refr: refr,
log: logFunc, logger: logger,
errColl: errColl, errColl: errColl,
prefix: prefix, prefix: prefix,
} }
@ -218,9 +229,7 @@ var _ Refresher = (*RefresherWithErrColl)(nil)
func (r *RefresherWithErrColl) Refresh(ctx context.Context) (err error) { func (r *RefresherWithErrColl) Refresh(ctx context.Context) (err error) {
err = r.refr.Refresh(ctx) err = r.refr.Refresh(ctx)
if err != nil { if err != nil {
err = fmt.Errorf("%s: %w", r.prefix, err) errcoll.Collect(ctx, r.errColl, r.logger, "refreshing", err)
r.log("%s", err)
r.errColl.Collect(ctx, err)
} }
return err return err

View File

@ -8,6 +8,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/agdservice" "github.com/AdguardTeam/AdGuardDNS/internal/agdservice"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest" "github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -55,8 +56,8 @@ func newRefrConfig(
Context: func() (ctx context.Context, cancel context.CancelFunc) { Context: func() (ctx context.Context, cancel context.CancelFunc) {
return context.WithTimeout(context.Background(), testTimeout) return context.WithTimeout(context.Background(), testTimeout)
}, },
Logger: slogutil.NewDiscardLogger(),
Refresher: refr, Refresher: refr,
Name: name,
Interval: ivl, Interval: ivl,
RefreshOnShutdown: refrOnShutDown, RefreshOnShutdown: refrOnShutDown,
RandomizeStart: false, RandomizeStart: false,

View File

@ -3,9 +3,11 @@
package agdtest package agdtest
import ( import (
"testing"
"time" "time"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg" "github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/stretchr/testify/require"
) )
// FilteredResponseTTL is the common filtering response TTL for tests. It is // FilteredResponseTTL is the common filtering response TTL for tests. It is
@ -16,9 +18,34 @@ const FilteredResponseTTL = FilteredResponseTTLSec * time.Second
// number to simplify message creation. // number to simplify message creation.
const FilteredResponseTTLSec = 10 const FilteredResponseTTLSec = 10
// NewConstructor returns a standard dnsmsg.Constructor for tests. // NewConstructorWithTTL returns a standard dnsmsg.Constructor for tests, using
func NewConstructor() (c *dnsmsg.Constructor) { // ttl as the TTL for filtered responses.
return dnsmsg.NewConstructor(nil, &dnsmsg.BlockingModeNullIP{}, FilteredResponseTTL) func NewConstructorWithTTL(tb testing.TB, ttl time.Duration) (c *dnsmsg.Constructor) {
tb.Helper()
c, err := dnsmsg.NewConstructor(&dnsmsg.ConstructorConfig{
Cloner: NewCloner(),
BlockingMode: &dnsmsg.BlockingModeNullIP{},
FilteredResponseTTL: ttl,
})
require.NoError(tb, err)
return c
}
// NewConstructor returns a standard dnsmsg.Constructor for tests, using
// [FilteredResponseTTL] as the TTL for filtered responses.
func NewConstructor(tb testing.TB) (c *dnsmsg.Constructor) {
tb.Helper()
c, err := dnsmsg.NewConstructor(&dnsmsg.ConstructorConfig{
Cloner: NewCloner(),
BlockingMode: &dnsmsg.BlockingModeNullIP{},
FilteredResponseTTL: FilteredResponseTTL,
})
require.NoError(tb, err)
return c
} }
// NewCloner returns a standard dnsmsg.Cloner for tests. // NewCloner returns a standard dnsmsg.Cloner for tests.

View File

@ -2,6 +2,7 @@ package agdtest
import ( import (
"context" "context"
"fmt"
"net" "net"
"net/netip" "net/netip"
"time" "time"
@ -20,9 +21,11 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/geoip" "github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb" "github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
"github.com/AdguardTeam/AdGuardDNS/internal/querylog" "github.com/AdguardTeam/AdGuardDNS/internal/querylog"
"github.com/AdguardTeam/AdGuardDNS/internal/remotekv"
"github.com/AdguardTeam/AdGuardDNS/internal/rulestat" "github.com/AdguardTeam/AdGuardDNS/internal/rulestat"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
) )
// Interface Mocks // Interface Mocks
@ -52,6 +55,31 @@ func (a *AccessManager) IsBlockedIP(ip netip.Addr) (blocked bool) {
return a.OnIsBlockedIP(ip) return a.OnIsBlockedIP(ip)
} }
// Package agd
// type check
var _ agd.DeviceFinder = (*DeviceFinder)(nil)
// DeviceFinder is an [agd.DeviceFinder] for tests.
type DeviceFinder struct {
OnFind func(
ctx context.Context,
req *dns.Msg,
raddr netip.AddrPort,
laddr netip.AddrPort,
) (r agd.DeviceResult)
}
// Find implements the [agd.DeviceFinder] interface for *DeviceFinder.
func (f *DeviceFinder) Find(
ctx context.Context,
req *dns.Msg,
raddr netip.AddrPort,
laddr netip.AddrPort,
) (r agd.DeviceResult) {
return f.OnFind(ctx, req, raddr, laddr)
}
// Package agdpasswd // Package agdpasswd
// type check // type check
@ -176,6 +204,15 @@ func (c *ErrorCollector) Collect(ctx context.Context, err error) {
c.OnCollect(ctx, err) c.OnCollect(ctx, err)
} }
// NewErrorCollector returns a new [ErrorCollector] all methods of which panic.
func NewErrorCollector() (c *ErrorCollector) {
return &ErrorCollector{
OnCollect: func(_ context.Context, err error) {
panic(fmt.Errorf("unexpected call to ErrorCollector.Collect(%v)", err))
},
}
}
// Package filter // Package filter
// type check // type check
@ -470,7 +507,7 @@ type RateLimit struct {
ctx context.Context, ctx context.Context,
req *dns.Msg, req *dns.Msg,
ip netip.Addr, ip netip.Addr,
) (drop, allowlisted bool, err error) ) (shouldDrop, isAllowlisted bool, err error)
OnCountResponses func(ctx context.Context, resp *dns.Msg, ip netip.Addr) OnCountResponses func(ctx context.Context, resp *dns.Msg, ip netip.Addr)
} }
@ -479,7 +516,7 @@ func (l *RateLimit) IsRateLimited(
ctx context.Context, ctx context.Context,
req *dns.Msg, req *dns.Msg,
ip netip.Addr, ip netip.Addr,
) (drop, allowlisted bool, err error) { ) (shouldDrop, isAllowlisted bool, err error) {
return l.OnIsRateLimited(ctx, req, ip) return l.OnIsRateLimited(ctx, req, ip)
} }
@ -488,3 +525,63 @@ func (l *RateLimit) IsRateLimited(
func (l *RateLimit) CountResponses(ctx context.Context, req *dns.Msg, ip netip.Addr) { func (l *RateLimit) CountResponses(ctx context.Context, req *dns.Msg, ip netip.Addr) {
l.OnCountResponses(ctx, req, ip) l.OnCountResponses(ctx, req, ip)
} }
// RemoteKV is an [remotekv.Interface] implementation for tests.
type RemoteKV struct {
OnGet func(ctx context.Context, key string) (val []byte, ok bool, err error)
OnSet func(ctx context.Context, key string, val []byte) (err error)
}
// type check
var _ remotekv.Interface = (*RemoteKV)(nil)
// Get implements the [remotekv.Interface] interface for *RemoteKV.
func (kv *RemoteKV) Get(ctx context.Context, key string) (val []byte, ok bool, err error) {
return kv.OnGet(ctx, key)
}
// Set implements the [remotekv.Interface] interface for *RemoteKV.
func (kv *RemoteKV) Set(ctx context.Context, key string, val []byte) (err error) {
return kv.OnSet(ctx, key, val)
}
// Module prometheus
// PrometheusRegisterer is a [prometheus.Registerer] implementation for tests.
type PrometheusRegisterer struct {
OnRegister func(prometheus.Collector) (err error)
OnMustRegister func(...prometheus.Collector)
OnUnregister func(prometheus.Collector) (ok bool)
}
// type check
var _ prometheus.Registerer = (*PrometheusRegisterer)(nil)
// Register implements the [prometheus.Registerer] interface for
// *PrometheusRegisterer.
func (r *PrometheusRegisterer) Register(c prometheus.Collector) (err error) {
return r.OnRegister(c)
}
// MustRegister implements the [prometheus.Registerer] interface for
// *PrometheusRegisterer.
func (r *PrometheusRegisterer) MustRegister(collectors ...prometheus.Collector) {
r.OnMustRegister(collectors...)
}
// Unregister implements the [prometheus.Registerer] interface for
// *PrometheusRegisterer.
func (r *PrometheusRegisterer) Unregister(c prometheus.Collector) (ok bool) {
return r.OnUnregister(c)
}
// NewTestPrometheusRegisterer returns a [prometheus.Registerer] implementation
// that does nothing and returns nil from [prometheus.Registerer.Register] and
// true from [prometheus.Registerer.Unregister].
func NewTestPrometheusRegisterer() (r *PrometheusRegisterer) {
return &PrometheusRegisterer{
OnRegister: func(_ prometheus.Collector) (err error) { return nil },
OnMustRegister: func(_ ...prometheus.Collector) {},
OnUnregister: func(_ prometheus.Collector) (ok bool) { return true },
}
}

View File

@ -7,6 +7,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/agd" "github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/c2h5oh/datasize"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -35,3 +36,6 @@ var TestUpdTime = time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
// //
// TODO(a.garipov): Add to golibs/netutil. // TODO(a.garipov): Add to golibs/netutil.
var TestBind = netip.MustParsePrefix("0.0.0.0/0") var TestBind = netip.MustParsePrefix("0.0.0.0/0")
// TestRespSzEst is a response-size estimate for tests.
const TestRespSzEst datasize.ByteSize = 1 * datasize.KB

View File

@ -5,6 +5,8 @@ import (
"time" "time"
"github.com/AdguardTeam/AdGuardDNS/internal/backendpb" "github.com/AdguardTeam/AdGuardDNS/internal/backendpb"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
) )
// testTimeout is the common timeout for tests. // testTimeout is the common timeout for tests.
@ -25,11 +27,11 @@ type testDNSServiceServer struct {
OnGetDNSProfiles func( OnGetDNSProfiles func(
req *backendpb.DNSProfilesRequest, req *backendpb.DNSProfilesRequest,
srv backendpb.DNSService_GetDNSProfilesServer, srv grpc.ServerStreamingServer[backendpb.DNSProfile],
) (err error) ) (err error)
OnSaveDevicesBillingStat func( OnSaveDevicesBillingStat func(
srv backendpb.DNSService_SaveDevicesBillingStatServer, srv grpc.ClientStreamingServer[backendpb.DeviceBillingStat, emptypb.Empty],
) (err error) ) (err error)
} }
@ -51,7 +53,7 @@ func (s *testDNSServiceServer) CreateDeviceByHumanId(
// *testDNSServiceServer // *testDNSServiceServer
func (s *testDNSServiceServer) GetDNSProfiles( func (s *testDNSServiceServer) GetDNSProfiles(
req *backendpb.DNSProfilesRequest, req *backendpb.DNSProfilesRequest,
srv backendpb.DNSService_GetDNSProfilesServer, srv grpc.ServerStreamingServer[backendpb.DNSProfile],
) (err error) { ) (err error) {
return s.OnGetDNSProfiles(req, srv) return s.OnGetDNSProfiles(req, srv)
} }
@ -59,7 +61,7 @@ func (s *testDNSServiceServer) GetDNSProfiles(
// SaveDevicesBillingStat implements the [backendpb.DNSServiceServer] interface // SaveDevicesBillingStat implements the [backendpb.DNSServiceServer] interface
// for *testDNSServiceServer // for *testDNSServiceServer
func (s *testDNSServiceServer) SaveDevicesBillingStat( func (s *testDNSServiceServer) SaveDevicesBillingStat(
srv backendpb.DNSService_SaveDevicesBillingStatServer, srv grpc.ClientStreamingServer[backendpb.DeviceBillingStat, emptypb.Empty],
) (err error) { ) (err error) {
return s.OnSaveDevicesBillingStat(srv) return s.OnSaveDevicesBillingStat(srv)
} }

View File

@ -20,6 +20,9 @@ type BillStatConfig struct {
// non-critical errors. // non-critical errors.
ErrColl errcoll.Interface ErrColl errcoll.Interface
// Metrics is used for the collection of the protobuf errors.
Metrics Metrics
// Endpoint is the backend API URL. The scheme should be either "grpc" or // Endpoint is the backend API URL. The scheme should be either "grpc" or
// "grpcs". // "grpcs".
Endpoint *url.URL Endpoint *url.URL
@ -38,6 +41,7 @@ func NewBillStat(c *BillStatConfig) (b *BillStat, err error) {
return &BillStat{ return &BillStat{
errColl: c.ErrColl, errColl: c.ErrColl,
metrics: c.Metrics,
client: client, client: client,
apiKey: c.APIKey, apiKey: c.APIKey,
}, nil }, nil
@ -51,6 +55,7 @@ func NewBillStat(c *BillStatConfig) (b *BillStat, err error) {
// backendpb.Client. // backendpb.Client.
type BillStat struct { type BillStat struct {
errColl errcoll.Interface errColl errcoll.Interface
metrics Metrics
client DNSServiceClient client DNSServiceClient
apiKey string apiKey string
} }
@ -67,7 +72,7 @@ func (b *BillStat) Upload(ctx context.Context, records billstat.Records) (err er
ctx = ctxWithAuthentication(ctx, b.apiKey) ctx = ctxWithAuthentication(ctx, b.apiKey)
stream, err := b.client.SaveDevicesBillingStat(ctx) stream, err := b.client.SaveDevicesBillingStat(ctx)
if err != nil { if err != nil {
return fmt.Errorf("opening stream: %w", fixGRPCError(err)) return fmt.Errorf("opening stream: %w", fixGRPCError(ctx, b.metrics, err))
} }
for deviceID, record := range records { for deviceID, record := range records {
@ -79,7 +84,11 @@ func (b *BillStat) Upload(ctx context.Context, records billstat.Records) (err er
sendErr := stream.Send(recordToProtobuf(record, deviceID)) sendErr := stream.Send(recordToProtobuf(record, deviceID))
if sendErr != nil { if sendErr != nil {
return fmt.Errorf("uploading device %q record: %w", deviceID, fixGRPCError(sendErr)) return fmt.Errorf(
"uploading device %q record: %w",
deviceID,
fixGRPCError(ctx, b.metrics, sendErr),
)
} }
} }
@ -100,6 +109,7 @@ func recordToProtobuf(r *billstat.Record, devID agd.DeviceID) (s *DeviceBillingS
ClientCountry: string(r.Country), ClientCountry: string(r.Country),
Proto: uint32(r.Proto), Proto: uint32(r.Proto),
Asn: uint32(r.ASN), Asn: uint32(r.ASN),
// #nosec G115 -- r.Queries must not be negative.
Queries: uint32(r.Queries), Queries: uint32(r.Queries),
} }
} }

View File

@ -54,13 +54,13 @@ func TestBillStat_Upload(t *testing.T) {
OnGetDNSProfiles: func( OnGetDNSProfiles: func(
req *backendpb.DNSProfilesRequest, req *backendpb.DNSProfilesRequest,
srv backendpb.DNSService_GetDNSProfilesServer, srv grpc.ServerStreamingServer[backendpb.DNSProfile],
) (err error) { ) (err error) {
panic("not implemented") panic("not implemented")
}, },
OnSaveDevicesBillingStat: func( OnSaveDevicesBillingStat: func(
srv backendpb.DNSService_SaveDevicesBillingStatServer, srv grpc.ClientStreamingServer[backendpb.DeviceBillingStat, emptypb.Empty],
) (err error) { ) (err error) {
pt := &testutil.PanicT{} pt := &testutil.PanicT{}
@ -107,6 +107,7 @@ func TestBillStat_Upload(t *testing.T) {
b, err := backendpb.NewBillStat(&backendpb.BillStatConfig{ b, err := backendpb.NewBillStat(&backendpb.BillStatConfig{
ErrColl: errColl, ErrColl: errColl,
Metrics: backendpb.EmptyMetrics{},
Endpoint: &url.URL{ Endpoint: &url.URL{
Scheme: "grpc", Scheme: "grpc",
Host: l.Addr().String(), Host: l.Addr().String(),

View File

@ -9,7 +9,6 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/agdpasswd" "github.com/AdguardTeam/AdGuardDNS/internal/agdpasswd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdprotobuf" "github.com/AdguardTeam/AdGuardDNS/internal/agdprotobuf"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll" "github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
) )
@ -20,6 +19,7 @@ func devicesToInternal(
ds []*DeviceSettings, ds []*DeviceSettings,
bindSet netutil.SubnetSet, bindSet netutil.SubnetSet,
errColl errcoll.Interface, errColl errcoll.Interface,
mtrc Metrics,
) (out []*agd.Device, ids []agd.DeviceID) { ) (out []*agd.Device, ids []agd.DeviceID) {
l := len(ds) l := len(ds)
if l == 0 { if l == 0 {
@ -36,7 +36,10 @@ func devicesToInternal(
} }
reportf(ctx, errColl, "bad device settings for device with id %q: %w", id, err) reportf(ctx, errColl, "bad device settings for device with id %q: %w", id, err)
metrics.DevicesInvalidTotal.Inc()
// TODO(s.chzhen): Add a return result structure and move the
// metrics collection to the layer above.
mtrc.IncrementInvalidDevicesCount(ctx)
continue continue
} }

View File

@ -167,6 +167,7 @@ type DNSProfile struct {
IpLogEnabled bool `protobuf:"varint,17,opt,name=ip_log_enabled,json=ipLogEnabled,proto3" json:"ip_log_enabled,omitempty"` IpLogEnabled bool `protobuf:"varint,17,opt,name=ip_log_enabled,json=ipLogEnabled,proto3" json:"ip_log_enabled,omitempty"`
Access *AccessSettings `protobuf:"bytes,18,opt,name=access,proto3" json:"access,omitempty"` Access *AccessSettings `protobuf:"bytes,18,opt,name=access,proto3" json:"access,omitempty"`
AutoDevicesEnabled bool `protobuf:"varint,19,opt,name=auto_devices_enabled,json=autoDevicesEnabled,proto3" json:"auto_devices_enabled,omitempty"` AutoDevicesEnabled bool `protobuf:"varint,19,opt,name=auto_devices_enabled,json=autoDevicesEnabled,proto3" json:"auto_devices_enabled,omitempty"`
RateLimit *RateLimitSettings `protobuf:"bytes,20,opt,name=rate_limit,json=rateLimit,proto3" json:"rate_limit,omitempty"`
} }
func (x *DNSProfile) Reset() { func (x *DNSProfile) Reset() {
@ -341,6 +342,13 @@ func (x *DNSProfile) GetAutoDevicesEnabled() bool {
return false return false
} }
func (x *DNSProfile) GetRateLimit() *RateLimitSettings {
if x != nil {
return x.RateLimit
}
return nil
}
type isDNSProfile_BlockingMode interface { type isDNSProfile_BlockingMode interface {
isDNSProfile_BlockingMode() isDNSProfile_BlockingMode()
} }
@ -1655,6 +1663,69 @@ func (x *AuthenticationFailedError) GetMessage() string {
return "" return ""
} }
type RateLimitSettings struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"`
Rps uint32 `protobuf:"varint,2,opt,name=rps,proto3" json:"rps,omitempty"`
ClientCidr []*CidrRange `protobuf:"bytes,3,rep,name=client_cidr,json=clientCidr,proto3" json:"client_cidr,omitempty"`
}
func (x *RateLimitSettings) Reset() {
*x = RateLimitSettings{}
if protoimpl.UnsafeEnabled {
mi := &file_dns_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RateLimitSettings) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RateLimitSettings) ProtoMessage() {}
func (x *RateLimitSettings) ProtoReflect() protoreflect.Message {
mi := &file_dns_proto_msgTypes[23]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RateLimitSettings.ProtoReflect.Descriptor instead.
func (*RateLimitSettings) Descriptor() ([]byte, []int) {
return file_dns_proto_rawDescGZIP(), []int{23}
}
func (x *RateLimitSettings) GetEnabled() bool {
if x != nil {
return x.Enabled
}
return false
}
func (x *RateLimitSettings) GetRps() uint32 {
if x != nil {
return x.Rps
}
return 0
}
func (x *RateLimitSettings) GetClientCidr() []*CidrRange {
if x != nil {
return x.ClientCidr
}
return nil
}
var File_dns_proto protoreflect.FileDescriptor var File_dns_proto protoreflect.FileDescriptor
var file_dns_proto_rawDesc = []byte{ var file_dns_proto_rawDesc = []byte{
@ -1669,7 +1740,7 @@ var file_dns_proto_rawDesc = []byte{
0x37, 0x0a, 0x09, 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x37, 0x0a, 0x09, 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08,
0x73, 0x79, 0x6e, 0x63, 0x54, 0x69, 0x6d, 0x65, 0x22, 0xfa, 0x07, 0x0a, 0x0a, 0x44, 0x4e, 0x53, 0x73, 0x79, 0x6e, 0x63, 0x54, 0x69, 0x6d, 0x65, 0x22, 0xad, 0x08, 0x0a, 0x0a, 0x44, 0x4e, 0x53,
0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x64, 0x6e, 0x73, 0x5f, 0x69, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x64, 0x6e, 0x73, 0x5f, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x64, 0x6e, 0x73, 0x49, 0x64, 0x12, 0x2b, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x64, 0x6e, 0x73, 0x49, 0x64, 0x12, 0x2b,
0x0a, 0x11, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x0a, 0x11, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x65, 0x6e, 0x61, 0x62,
@ -1732,186 +1803,196 @@ var file_dns_proto_rawDesc = []byte{
0x73, 0x73, 0x12, 0x30, 0x0a, 0x14, 0x61, 0x75, 0x74, 0x6f, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x73, 0x73, 0x12, 0x30, 0x0a, 0x14, 0x61, 0x75, 0x74, 0x6f, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63,
0x65, 0x73, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x13, 0x20, 0x01, 0x28, 0x08, 0x65, 0x73, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x13, 0x20, 0x01, 0x28, 0x08,
0x52, 0x12, 0x61, 0x75, 0x74, 0x6f, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x45, 0x6e, 0x61, 0x52, 0x12, 0x61, 0x75, 0x74, 0x6f, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x45, 0x6e, 0x61,
0x62, 0x6c, 0x65, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x69, 0x6e, 0x67, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x31, 0x0a, 0x0a, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x6c, 0x69, 0x6d,
0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x22, 0x85, 0x01, 0x0a, 0x14, 0x53, 0x61, 0x66, 0x65, 0x42, 0x72, 0x69, 0x74, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x4c,
0x6f, 0x77, 0x73, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x18, 0x69, 0x6d, 0x69, 0x74, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x09, 0x72, 0x61,
0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x42, 0x0f, 0x0a, 0x0d, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x36, 0x0a, 0x17, 0x62, 0x6c, 0x6f, 0x63, 0x69, 0x6e, 0x67, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x22, 0x85, 0x01, 0x0a, 0x14, 0x53, 0x61, 0x66,
0x6b, 0x5f, 0x64, 0x61, 0x6e, 0x67, 0x65, 0x72, 0x6f, 0x75, 0x73, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x65, 0x42, 0x72, 0x6f, 0x77, 0x73, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67,
0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01,
0x44, 0x61, 0x6e, 0x67, 0x65, 0x72, 0x6f, 0x75, 0x73, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x36, 0x0a, 0x17, 0x62,
0x12, 0x1b, 0x0a, 0x09, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6e, 0x72, 0x64, 0x18, 0x03, 0x20, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x64, 0x61, 0x6e, 0x67, 0x65, 0x72, 0x6f, 0x75, 0x73, 0x5f, 0x64,
0x01, 0x28, 0x08, 0x52, 0x08, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4e, 0x72, 0x64, 0x22, 0x8a, 0x02, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x62, 0x6c,
0x0a, 0x0e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x6f, 0x63, 0x6b, 0x44, 0x61, 0x6e, 0x67, 0x65, 0x72, 0x6f, 0x75, 0x73, 0x44, 0x6f, 0x6d, 0x61,
0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x69, 0x6e, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6e, 0x72, 0x64,
0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4e, 0x72, 0x64,
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x69, 0x6e, 0x22, 0x8a, 0x02, 0x0a, 0x0e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69,
0x67, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x6e, 0x67, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x10, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x69, 0x6e, 0x6b, 0x65, 0x64, 0x5f, 0x69, 0x70, 0x18, 0x04, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2b, 0x0a, 0x11, 0x66, 0x69, 0x6c, 0x74, 0x65,
0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6c, 0x69, 0x6e, 0x6b, 0x65, 0x64, 0x49, 0x70, 0x12, 0x23, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01,
0x0a, 0x0d, 0x64, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x69, 0x70, 0x73, 0x18, 0x28, 0x08, 0x52, 0x10, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x45, 0x6e, 0x61,
0x05, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0c, 0x64, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x69, 0x6e, 0x6b, 0x65, 0x64, 0x5f, 0x69,
0x49, 0x70, 0x73, 0x12, 0x3f, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6c, 0x69, 0x6e, 0x6b, 0x65, 0x64, 0x49,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x41, 0x75, 0x70, 0x12, 0x23, 0x0a, 0x0d, 0x64, 0x65, 0x64, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x69,
0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x74, 0x70, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0c, 0x64, 0x65, 0x64, 0x69, 0x63, 0x61,
0x69, 0x6e, 0x67, 0x73, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x49, 0x70, 0x73, 0x12, 0x3f, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e,
0x74, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x68, 0x75, 0x6d, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17,
0x5f, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x68, 0x75, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53,
0x6d, 0x61, 0x6e, 0x49, 0x64, 0x4c, 0x6f, 0x77, 0x65, 0x72, 0x22, 0x87, 0x02, 0x0a, 0x10, 0x50, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74,
0x61, 0x72, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x68, 0x75, 0x6d, 0x61, 0x6e,
0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x5f, 0x69, 0x64, 0x5f, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52,
0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x62, 0x6c, 0x6f, 0x0c, 0x68, 0x75, 0x6d, 0x61, 0x6e, 0x49, 0x64, 0x4c, 0x6f, 0x77, 0x65, 0x72, 0x22, 0x87, 0x02,
0x63, 0x6b, 0x5f, 0x61, 0x64, 0x75, 0x6c, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x0a, 0x10, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e,
0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x41, 0x64, 0x75, 0x6c, 0x74, 0x12, 0x2e, 0x0a, 0x13, 0x67, 0x65, 0x67, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20,
0x6e, 0x65, 0x72, 0x61, 0x6c, 0x5f, 0x73, 0x61, 0x66, 0x65, 0x5f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1f, 0x0a, 0x0b,
0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x6c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x61, 0x64, 0x75, 0x6c, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
0x53, 0x61, 0x66, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x2e, 0x0a, 0x13, 0x79, 0x6f, 0x08, 0x52, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x41, 0x64, 0x75, 0x6c, 0x74, 0x12, 0x2e, 0x0a,
0x75, 0x74, 0x75, 0x62, 0x65, 0x5f, 0x73, 0x61, 0x66, 0x65, 0x5f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x13, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x6c, 0x5f, 0x73, 0x61, 0x66, 0x65, 0x5f, 0x73, 0x65,
0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x79, 0x6f, 0x75, 0x74, 0x75, 0x62, 0x65, 0x61, 0x72, 0x63, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x6e, 0x65,
0x53, 0x61, 0x66, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x29, 0x0a, 0x10, 0x62, 0x6c, 0x72, 0x61, 0x6c, 0x53, 0x61, 0x66, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x2e, 0x0a,
0x6f, 0x63, 0x6b, 0x65, 0x64, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x05, 0x13, 0x79, 0x6f, 0x75, 0x74, 0x75, 0x62, 0x65, 0x5f, 0x73, 0x61, 0x66, 0x65, 0x5f, 0x73, 0x65,
0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x53, 0x65, 0x72, 0x61, 0x72, 0x63, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x79, 0x6f, 0x75, 0x74,
0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x75, 0x62, 0x65, 0x53, 0x61, 0x66, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x29, 0x0a,
0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x10, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x6c, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x08, 0x73, 0x63, 0x68, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64,
0x64, 0x75, 0x6c, 0x65, 0x22, 0x54, 0x0a, 0x10, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65,
0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x6d, 0x7a, 0x18, 0x64, 0x75, 0x6c, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x53, 0x63, 0x68,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x6d, 0x7a, 0x12, 0x2e, 0x0a, 0x0b, 0x77, 0x65, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x08, 0x73,
0x65, 0x6b, 0x6c, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x22, 0x54, 0x0a, 0x10, 0x53, 0x63, 0x68, 0x65, 0x64,
0x0c, 0x2e, 0x57, 0x65, 0x65, 0x6b, 0x6c, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x0b, 0x77, 0x75, 0x6c, 0x65, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x74,
0x65, 0x65, 0x6b, 0x6c, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x22, 0xd8, 0x01, 0x0a, 0x0b, 0x57, 0x6d, 0x7a, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x6d, 0x7a, 0x12, 0x2e, 0x0a,
0x65, 0x65, 0x6b, 0x6c, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1b, 0x0a, 0x03, 0x6d, 0x6f, 0x0b, 0x77, 0x65, 0x65, 0x6b, 0x6c, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01,
0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x44, 0x61, 0x79, 0x52, 0x61, 0x6e, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x57, 0x65, 0x65, 0x6b, 0x6c, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65,
0x67, 0x65, 0x52, 0x03, 0x6d, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x03, 0x74, 0x75, 0x65, 0x18, 0x02, 0x52, 0x0b, 0x77, 0x65, 0x65, 0x6b, 0x6c, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x22, 0xd8, 0x01,
0x0a, 0x0b, 0x57, 0x65, 0x65, 0x6b, 0x6c, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1b, 0x0a,
0x03, 0x6d, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x44, 0x61, 0x79,
0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x03, 0x6d, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x03, 0x74, 0x75,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x44, 0x61, 0x79, 0x52, 0x61, 0x6e,
0x67, 0x65, 0x52, 0x03, 0x74, 0x75, 0x65, 0x12, 0x1b, 0x0a, 0x03, 0x77, 0x65, 0x64, 0x18, 0x03,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x44, 0x61, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x44, 0x61, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52,
0x03, 0x74, 0x75, 0x65, 0x12, 0x1b, 0x0a, 0x03, 0x77, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x77, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x03, 0x74, 0x68, 0x75, 0x18, 0x04, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x09, 0x2e, 0x44, 0x61, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x03, 0x77, 0x65, 0x0b, 0x32, 0x09, 0x2e, 0x44, 0x61, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x03, 0x74, 0x68,
0x64, 0x12, 0x1b, 0x0a, 0x03, 0x74, 0x68, 0x75, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x75, 0x12, 0x1b, 0x0a, 0x03, 0x66, 0x72, 0x69, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09,
0x2e, 0x44, 0x61, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x03, 0x74, 0x68, 0x75, 0x12, 0x1b, 0x2e, 0x44, 0x61, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x03, 0x66, 0x72, 0x69, 0x12, 0x1b,
0x0a, 0x03, 0x66, 0x72, 0x69, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x44, 0x61, 0x0a, 0x03, 0x73, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x44, 0x61,
0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x03, 0x66, 0x72, 0x69, 0x12, 0x1b, 0x0a, 0x03, 0x73, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x03, 0x73, 0x61, 0x74, 0x12, 0x1b, 0x0a, 0x03, 0x73,
0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x44, 0x61, 0x79, 0x52, 0x61, 0x75, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x44, 0x61, 0x79, 0x52, 0x61,
0x6e, 0x67, 0x65, 0x52, 0x03, 0x73, 0x61, 0x74, 0x12, 0x1b, 0x0a, 0x03, 0x73, 0x75, 0x6e, 0x18, 0x6e, 0x67, 0x65, 0x52, 0x03, 0x73, 0x75, 0x6e, 0x22, 0x68, 0x0a, 0x08, 0x44, 0x61, 0x79, 0x52,
0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x44, 0x61, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x2f, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20,
0x52, 0x03, 0x73, 0x75, 0x6e, 0x22, 0x68, 0x0a, 0x08, 0x44, 0x61, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x65, 0x12, 0x2f, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05,
0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x2b, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01,
0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x73, 0x74, 0x61, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x72, 0x74, 0x12, 0x2b, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x03, 0x65,
0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x6e, 0x64, 0x22, 0x3f, 0x0a, 0x11, 0x52, 0x75, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x53,
0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x22, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c,
0x3f, 0x0a, 0x11, 0x52, 0x75, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x53, 0x65, 0x74, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65,
0x69, 0x6e, 0x67, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03,
0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x10, 0x69, 0x64, 0x73, 0x22, 0x3e, 0x0a, 0x14, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x69, 0x6e, 0x67, 0x4d,
0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x64, 0x73, 0x6f, 0x64, 0x65, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x49, 0x50, 0x12, 0x12, 0x0a, 0x04, 0x69,
0x22, 0x3e, 0x0a, 0x14, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x70, 0x76, 0x34, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x69, 0x70, 0x76, 0x34, 0x12,
0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x49, 0x50, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x70, 0x76, 0x34, 0x12, 0x0a, 0x04, 0x69, 0x70, 0x76, 0x36, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x69,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x69, 0x70, 0x76, 0x34, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x76, 0x36, 0x22, 0x16, 0x0a, 0x14, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x69, 0x6e, 0x67, 0x4d,
0x69, 0x70, 0x76, 0x36, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x69, 0x70, 0x76, 0x36, 0x6f, 0x64, 0x65, 0x4e, 0x58, 0x44, 0x4f, 0x4d, 0x41, 0x49, 0x4e, 0x22, 0x14, 0x0a, 0x12, 0x42,
0x22, 0x16, 0x0a, 0x14, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x6f, 0x63, 0x6b, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x4e, 0x75, 0x6c, 0x6c, 0x49,
0x4e, 0x58, 0x44, 0x4f, 0x4d, 0x41, 0x49, 0x4e, 0x22, 0x14, 0x0a, 0x12, 0x42, 0x6c, 0x6f, 0x63, 0x50, 0x22, 0x15, 0x0a, 0x13, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64,
0x6b, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x4e, 0x75, 0x6c, 0x6c, 0x49, 0x50, 0x22, 0x15, 0x65, 0x52, 0x45, 0x46, 0x55, 0x53, 0x45, 0x44, 0x22, 0xe3, 0x01, 0x0a, 0x11, 0x44, 0x65, 0x76,
0x0a, 0x13, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x45, 0x69, 0x63, 0x65, 0x42, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x12, 0x48,
0x46, 0x55, 0x53, 0x45, 0x44, 0x22, 0xe3, 0x01, 0x0a, 0x11, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x0a, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x5f,
0x42, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x12, 0x48, 0x0a, 0x12, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f,
0x61, 0x73, 0x74, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x5f, 0x74, 0x69, 0x6d, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x41, 0x63, 0x74, 0x69,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x76, 0x69, 0x74, 0x79, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69,
0x61, 0x6d, 0x70, 0x52, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x41, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76,
0x79, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f,
0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63,
0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05,
0x6e, 0x74, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x70, 0x72, 0x6f,
0x6e, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x73, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52,
0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x73, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x18,
0x10, 0x0a, 0x03, 0x61, 0x73, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x61, 0x73, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x22, 0x90,
0x6e, 0x12, 0x18, 0x0a, 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x02, 0x0a, 0x0e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67,
0x28, 0x0d, 0x52, 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x22, 0x90, 0x02, 0x0a, 0x0e, 0x73, 0x12, 0x31, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x63,
0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x31, 0x69, 0x64, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x43, 0x69, 0x64, 0x72,
0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x0d, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74,
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x43, 0x69, 0x64, 0x72, 0x52, 0x61, 0x6e, 0x43, 0x69, 0x64, 0x72, 0x12, 0x31, 0x0a, 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x6c, 0x69, 0x73,
0x67, 0x65, 0x52, 0x0d, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x43, 0x69, 0x64, 0x74, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x43,
0x72, 0x12, 0x31, 0x0a, 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x0d, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x6c,
0x69, 0x64, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x43, 0x69, 0x64, 0x72, 0x69, 0x73, 0x74, 0x43, 0x69, 0x64, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x6c, 0x6c, 0x6f, 0x77,
0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x0d, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x61, 0x73, 0x6e, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x0c,
0x43, 0x69, 0x64, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x41, 0x73, 0x6e, 0x12, 0x23, 0x0a, 0x0d,
0x74, 0x5f, 0x61, 0x73, 0x6e, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x0c, 0x61, 0x6c, 0x6c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x61, 0x73, 0x6e, 0x18, 0x04, 0x20,
0x6f, 0x77, 0x6c, 0x69, 0x73, 0x74, 0x41, 0x73, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x62, 0x6c, 0x6f, 0x03, 0x28, 0x0d, 0x52, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x41, 0x73,
0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x61, 0x73, 0x6e, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0d, 0x6e, 0x12, 0x34, 0x0a, 0x16, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x64,
0x52, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x41, 0x73, 0x6e, 0x12, 0x34, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28,
0x0a, 0x16, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x09, 0x52, 0x14, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x44, 0x6f, 0x6d, 0x61,
0x69, 0x6e, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x14, 0x69, 0x6e, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c,
0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65,
0x75, 0x6c, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x64, 0x22, 0x3d, 0x0a, 0x09, 0x43, 0x69, 0x64, 0x72, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x18,
0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x3d, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52,
0x0a, 0x09, 0x43, 0x69, 0x64, 0x72, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66,
0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x61, 0x64, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78,
0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x22, 0x85, 0x01, 0x0a, 0x16, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74,
0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, 0x85, 0x01, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x64,
0x0a, 0x16, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x6f, 0x68, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01,
0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x64, 0x6f, 0x68, 0x5f, 0x28, 0x08, 0x52, 0x0b, 0x64, 0x6f, 0x68, 0x41, 0x75, 0x74, 0x68, 0x4f, 0x6e, 0x6c, 0x79, 0x12,
0x61, 0x75, 0x74, 0x68, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x32, 0x0a, 0x14, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x61, 0x73, 0x68,
0x0b, 0x64, 0x6f, 0x68, 0x41, 0x75, 0x74, 0x68, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x32, 0x0a, 0x14, 0x5f, 0x62, 0x63, 0x72, 0x79, 0x70, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52,
0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x5f, 0x62, 0x63, 0x12, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x48, 0x61, 0x73, 0x68, 0x42, 0x63, 0x72,
0x72, 0x79, 0x70, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x12, 0x70, 0x61, 0x79, 0x70, 0x74, 0x42, 0x13, 0x0a, 0x11, 0x64, 0x6f, 0x68, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77,
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x48, 0x61, 0x73, 0x68, 0x42, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x22, 0x75, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61,
0x42, 0x13, 0x0a, 0x11, 0x64, 0x6f, 0x68, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x5f, 0x68, 0x61, 0x73, 0x68, 0x22, 0x75, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x15, 0x0a, 0x06, 0x64, 0x6e, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x05, 0x64, 0x6e, 0x73, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x75, 0x6d, 0x61, 0x6e, 0x5f,
0x64, 0x6e, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x64, 0x6e, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x68, 0x75, 0x6d, 0x61, 0x6e, 0x49,
0x73, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x75, 0x6d, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x64, 0x12, 0x2c, 0x0a, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x68, 0x75, 0x6d, 0x61, 0x6e, 0x49, 0x64, 0x12, 0x2c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0b, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x54,
0x0a, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x79, 0x70, 0x65, 0x52, 0x0a, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x22,
0x01, 0x28, 0x0e, 0x32, 0x0b, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x3f, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52,
0x52, 0x0a, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3f, 0x0a, 0x14, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, 0x64, 0x65, 0x76, 0x69, 0x63,
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x06, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x22, 0x68, 0x0a, 0x10, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x64, 0x45,
0x74, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x06, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x22, 0x68, 0x0a, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18,
0x10, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x3a,
0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x0a, 0x0b, 0x72, 0x65, 0x74, 0x72, 0x79, 0x5f, 0x64, 0x65, 0x6c, 0x61, 0x79, 0x18, 0x02, 0x20,
0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x3a, 0x0a, 0x0b, 0x72, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x65, 0x74, 0x72, 0x79, 0x5f, 0x64, 0x65, 0x6c, 0x61, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a,
0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x72, 0x65, 0x74, 0x72, 0x79, 0x44, 0x65, 0x6c, 0x61, 0x79, 0x22, 0x34, 0x0a, 0x18, 0x44, 0x65,
0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x72, 0x65, 0x74, 0x76, 0x69, 0x63, 0x65, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65,
0x72, 0x79, 0x44, 0x65, 0x6c, 0x61, 0x79, 0x22, 0x34, 0x0a, 0x18, 0x44, 0x65, 0x76, 0x69, 0x63, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x45, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x22, 0x2b, 0x0a, 0x0f, 0x42, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x45, 0x72,
0x72, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x2b, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x35, 0x0a,
0x0f, 0x42, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46,
0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65,
0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x35, 0x0a, 0x19, 0x41, 0x75, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73,
0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x61, 0x69, 0x6c, 0x73, 0x61, 0x67, 0x65, 0x22, 0x6c, 0x0a, 0x11, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69,
0x65, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x74, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61,
0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62,
0x65, 0x2a, 0x87, 0x01, 0x0a, 0x0a, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x6c, 0x65, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x70, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d,
0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x52, 0x03, 0x72, 0x70, 0x73, 0x12, 0x2b, 0x0a, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f,
0x07, 0x57, 0x49, 0x4e, 0x44, 0x4f, 0x57, 0x53, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x41, 0x4e, 0x63, 0x69, 0x64, 0x72, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x43, 0x69, 0x64,
0x44, 0x52, 0x4f, 0x49, 0x44, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x4d, 0x41, 0x43, 0x10, 0x03, 0x72, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x0a, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x69,
0x12, 0x07, 0x0a, 0x03, 0x49, 0x4f, 0x53, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x4c, 0x49, 0x4e, 0x64, 0x72, 0x2a, 0x87, 0x01, 0x0a, 0x0a, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x54, 0x79, 0x70,
0x55, 0x58, 0x10, 0x05, 0x12, 0x0a, 0x0a, 0x06, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x52, 0x10, 0x06, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x0b,
0x12, 0x0c, 0x0a, 0x08, 0x53, 0x4d, 0x41, 0x52, 0x54, 0x5f, 0x54, 0x56, 0x10, 0x07, 0x12, 0x10, 0x0a, 0x07, 0x57, 0x49, 0x4e, 0x44, 0x4f, 0x57, 0x53, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x41,
0x0a, 0x0c, 0x47, 0x41, 0x4d, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x53, 0x4f, 0x4c, 0x45, 0x10, 0x08, 0x4e, 0x44, 0x52, 0x4f, 0x49, 0x44, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x4d, 0x41, 0x43, 0x10,
0x12, 0x09, 0x0a, 0x05, 0x4f, 0x54, 0x48, 0x45, 0x52, 0x10, 0x09, 0x32, 0xd0, 0x01, 0x0a, 0x0a, 0x03, 0x12, 0x07, 0x0a, 0x03, 0x49, 0x4f, 0x53, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x4c, 0x49,
0x44, 0x4e, 0x53, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x34, 0x0a, 0x0e, 0x67, 0x65, 0x4e, 0x55, 0x58, 0x10, 0x05, 0x12, 0x0a, 0x0a, 0x06, 0x52, 0x4f, 0x55, 0x54, 0x45, 0x52, 0x10,
0x74, 0x44, 0x4e, 0x53, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x13, 0x2e, 0x44, 0x06, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x4d, 0x41, 0x52, 0x54, 0x5f, 0x54, 0x56, 0x10, 0x07, 0x12,
0x4e, 0x53, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x10, 0x0a, 0x0c, 0x47, 0x41, 0x4d, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x53, 0x4f, 0x4c, 0x45, 0x10,
0x74, 0x1a, 0x0b, 0x2e, 0x44, 0x4e, 0x53, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x30, 0x01, 0x08, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x54, 0x48, 0x45, 0x52, 0x10, 0x09, 0x32, 0xd0, 0x01, 0x0a,
0x12, 0x46, 0x0a, 0x16, 0x73, 0x61, 0x76, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x42, 0x0a, 0x44, 0x4e, 0x53, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x34, 0x0a, 0x0e, 0x67,
0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x12, 0x12, 0x2e, 0x44, 0x65, 0x76, 0x65, 0x74, 0x44, 0x4e, 0x53, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x13, 0x2e,
0x69, 0x63, 0x65, 0x42, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x1a, 0x16, 0x44, 0x4e, 0x53, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x44, 0x4e, 0x53, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x30,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, 0x44, 0x0a, 0x15, 0x63, 0x72, 0x65, 0x61, 0x01, 0x12, 0x46, 0x0a, 0x16, 0x73, 0x61, 0x76, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73,
0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x42, 0x79, 0x48, 0x75, 0x6d, 0x61, 0x6e, 0x49, 0x42, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x12, 0x12, 0x2e, 0x44, 0x65,
0x64, 0x12, 0x14, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x76, 0x69, 0x63, 0x65, 0x42, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x1a,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3d, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, 0x44, 0x0a, 0x15, 0x63, 0x72, 0x65,
0x0a, 0x21, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x64, 0x67, 0x75, 0x61, 0x72, 0x64, 0x2e, 0x62, 0x61, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x42, 0x79, 0x48, 0x75, 0x6d, 0x61, 0x6e,
0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x49, 0x64, 0x12, 0x14, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63,
0x74, 0x65, 0x64, 0x42, 0x10, 0x44, 0x4e, 0x53, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74,
0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0xa2, 0x02, 0x03, 0x44, 0x4e, 0x53, 0x62, 0x06, 0x70, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42,
0x72, 0x6f, 0x74, 0x6f, 0x33, 0x3d, 0x0a, 0x21, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x64, 0x67, 0x75, 0x61, 0x72, 0x64, 0x2e, 0x62,
0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x67, 0x65, 0x6e, 0x65, 0x72,
0x61, 0x74, 0x65, 0x64, 0x42, 0x10, 0x44, 0x4e, 0x53, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65,
0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0xa2, 0x02, 0x03, 0x44, 0x4e, 0x53, 0x62, 0x06,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
@ -1927,7 +2008,7 @@ func file_dns_proto_rawDescGZIP() []byte {
} }
var file_dns_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_dns_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_dns_proto_msgTypes = make([]protoimpl.MessageInfo, 23) var file_dns_proto_msgTypes = make([]protoimpl.MessageInfo, 24)
var file_dns_proto_goTypes = []any{ var file_dns_proto_goTypes = []any{
(DeviceType)(0), // 0: DeviceType (DeviceType)(0), // 0: DeviceType
(*DNSProfilesRequest)(nil), // 1: DNSProfilesRequest (*DNSProfilesRequest)(nil), // 1: DNSProfilesRequest
@ -1953,51 +2034,54 @@ var file_dns_proto_goTypes = []any{
(*DeviceQuotaExceededError)(nil), // 21: DeviceQuotaExceededError (*DeviceQuotaExceededError)(nil), // 21: DeviceQuotaExceededError
(*BadRequestError)(nil), // 22: BadRequestError (*BadRequestError)(nil), // 22: BadRequestError
(*AuthenticationFailedError)(nil), // 23: AuthenticationFailedError (*AuthenticationFailedError)(nil), // 23: AuthenticationFailedError
(*timestamppb.Timestamp)(nil), // 24: google.protobuf.Timestamp (*RateLimitSettings)(nil), // 24: RateLimitSettings
(*durationpb.Duration)(nil), // 25: google.protobuf.Duration (*timestamppb.Timestamp)(nil), // 25: google.protobuf.Timestamp
(*emptypb.Empty)(nil), // 26: google.protobuf.Empty (*durationpb.Duration)(nil), // 26: google.protobuf.Duration
(*emptypb.Empty)(nil), // 27: google.protobuf.Empty
} }
var file_dns_proto_depIdxs = []int32{ var file_dns_proto_depIdxs = []int32{
24, // 0: DNSProfilesRequest.sync_time:type_name -> google.protobuf.Timestamp 25, // 0: DNSProfilesRequest.sync_time:type_name -> google.protobuf.Timestamp
3, // 1: DNSProfile.safe_browsing:type_name -> SafeBrowsingSettings 3, // 1: DNSProfile.safe_browsing:type_name -> SafeBrowsingSettings
5, // 2: DNSProfile.parental:type_name -> ParentalSettings 5, // 2: DNSProfile.parental:type_name -> ParentalSettings
9, // 3: DNSProfile.rule_lists:type_name -> RuleListsSettings 9, // 3: DNSProfile.rule_lists:type_name -> RuleListsSettings
4, // 4: DNSProfile.devices:type_name -> DeviceSettings 4, // 4: DNSProfile.devices:type_name -> DeviceSettings
25, // 5: DNSProfile.filtered_response_ttl:type_name -> google.protobuf.Duration 26, // 5: DNSProfile.filtered_response_ttl:type_name -> google.protobuf.Duration
10, // 6: DNSProfile.blocking_mode_custom_ip:type_name -> BlockingModeCustomIP 10, // 6: DNSProfile.blocking_mode_custom_ip:type_name -> BlockingModeCustomIP
11, // 7: DNSProfile.blocking_mode_nxdomain:type_name -> BlockingModeNXDOMAIN 11, // 7: DNSProfile.blocking_mode_nxdomain:type_name -> BlockingModeNXDOMAIN
12, // 8: DNSProfile.blocking_mode_null_ip:type_name -> BlockingModeNullIP 12, // 8: DNSProfile.blocking_mode_null_ip:type_name -> BlockingModeNullIP
13, // 9: DNSProfile.blocking_mode_refused:type_name -> BlockingModeREFUSED 13, // 9: DNSProfile.blocking_mode_refused:type_name -> BlockingModeREFUSED
15, // 10: DNSProfile.access:type_name -> AccessSettings 15, // 10: DNSProfile.access:type_name -> AccessSettings
17, // 11: DeviceSettings.authentication:type_name -> AuthenticationSettings 24, // 11: DNSProfile.rate_limit:type_name -> RateLimitSettings
6, // 12: ParentalSettings.schedule:type_name -> ScheduleSettings 17, // 12: DeviceSettings.authentication:type_name -> AuthenticationSettings
7, // 13: ScheduleSettings.weeklyRange:type_name -> WeeklyRange 6, // 13: ParentalSettings.schedule:type_name -> ScheduleSettings
8, // 14: WeeklyRange.mon:type_name -> DayRange 7, // 14: ScheduleSettings.weeklyRange:type_name -> WeeklyRange
8, // 15: WeeklyRange.tue:type_name -> DayRange 8, // 15: WeeklyRange.mon:type_name -> DayRange
8, // 16: WeeklyRange.wed:type_name -> DayRange 8, // 16: WeeklyRange.tue:type_name -> DayRange
8, // 17: WeeklyRange.thu:type_name -> DayRange 8, // 17: WeeklyRange.wed:type_name -> DayRange
8, // 18: WeeklyRange.fri:type_name -> DayRange 8, // 18: WeeklyRange.thu:type_name -> DayRange
8, // 19: WeeklyRange.sat:type_name -> DayRange 8, // 19: WeeklyRange.fri:type_name -> DayRange
8, // 20: WeeklyRange.sun:type_name -> DayRange 8, // 20: WeeklyRange.sat:type_name -> DayRange
25, // 21: DayRange.start:type_name -> google.protobuf.Duration 8, // 21: WeeklyRange.sun:type_name -> DayRange
25, // 22: DayRange.end:type_name -> google.protobuf.Duration 26, // 22: DayRange.start:type_name -> google.protobuf.Duration
24, // 23: DeviceBillingStat.last_activity_time:type_name -> google.protobuf.Timestamp 26, // 23: DayRange.end:type_name -> google.protobuf.Duration
16, // 24: AccessSettings.allowlist_cidr:type_name -> CidrRange 25, // 24: DeviceBillingStat.last_activity_time:type_name -> google.protobuf.Timestamp
16, // 25: AccessSettings.blocklist_cidr:type_name -> CidrRange 16, // 25: AccessSettings.allowlist_cidr:type_name -> CidrRange
0, // 26: CreateDeviceRequest.device_type:type_name -> DeviceType 16, // 26: AccessSettings.blocklist_cidr:type_name -> CidrRange
4, // 27: CreateDeviceResponse.device:type_name -> DeviceSettings 0, // 27: CreateDeviceRequest.device_type:type_name -> DeviceType
25, // 28: RateLimitedError.retry_delay:type_name -> google.protobuf.Duration 4, // 28: CreateDeviceResponse.device:type_name -> DeviceSettings
1, // 29: DNSService.getDNSProfiles:input_type -> DNSProfilesRequest 26, // 29: RateLimitedError.retry_delay:type_name -> google.protobuf.Duration
14, // 30: DNSService.saveDevicesBillingStat:input_type -> DeviceBillingStat 16, // 30: RateLimitSettings.client_cidr:type_name -> CidrRange
18, // 31: DNSService.createDeviceByHumanId:input_type -> CreateDeviceRequest 1, // 31: DNSService.getDNSProfiles:input_type -> DNSProfilesRequest
2, // 32: DNSService.getDNSProfiles:output_type -> DNSProfile 14, // 32: DNSService.saveDevicesBillingStat:input_type -> DeviceBillingStat
26, // 33: DNSService.saveDevicesBillingStat:output_type -> google.protobuf.Empty 18, // 33: DNSService.createDeviceByHumanId:input_type -> CreateDeviceRequest
19, // 34: DNSService.createDeviceByHumanId:output_type -> CreateDeviceResponse 2, // 34: DNSService.getDNSProfiles:output_type -> DNSProfile
32, // [32:35] is the sub-list for method output_type 27, // 35: DNSService.saveDevicesBillingStat:output_type -> google.protobuf.Empty
29, // [29:32] is the sub-list for method input_type 19, // 36: DNSService.createDeviceByHumanId:output_type -> CreateDeviceResponse
29, // [29:29] is the sub-list for extension type_name 34, // [34:37] is the sub-list for method output_type
29, // [29:29] is the sub-list for extension extendee 31, // [31:34] is the sub-list for method input_type
0, // [0:29] is the sub-list for field type_name 31, // [31:31] is the sub-list for extension type_name
31, // [31:31] is the sub-list for extension extendee
0, // [0:31] is the sub-list for field type_name
} }
func init() { file_dns_proto_init() } func init() { file_dns_proto_init() }
@ -2282,6 +2366,18 @@ func file_dns_proto_init() {
return nil return nil
} }
} }
file_dns_proto_msgTypes[23].Exporter = func(v any, i int) any {
switch v := v.(*RateLimitSettings); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
} }
file_dns_proto_msgTypes[1].OneofWrappers = []any{ file_dns_proto_msgTypes[1].OneofWrappers = []any{
(*DNSProfile_BlockingModeCustomIp)(nil), (*DNSProfile_BlockingModeCustomIp)(nil),
@ -2298,7 +2394,7 @@ func file_dns_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_dns_proto_rawDesc, RawDescriptor: file_dns_proto_rawDesc,
NumEnums: 1, NumEnums: 1,
NumMessages: 23, NumMessages: 24,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

View File

@ -71,6 +71,7 @@ message DNSProfile {
bool ip_log_enabled = 17; bool ip_log_enabled = 17;
AccessSettings access = 18; AccessSettings access = 18;
bool auto_devices_enabled = 19; bool auto_devices_enabled = 19;
RateLimitSettings rate_limit = 20;
} }
message SafeBrowsingSettings { message SafeBrowsingSettings {
@ -205,3 +206,9 @@ message BadRequestError {
message AuthenticationFailedError { message AuthenticationFailedError {
string message = 1; string message = 1;
} }
message RateLimitSettings {
bool enabled = 1;
uint32 rps = 2;
repeated CidrRange client_cidr = 3;
}

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.4.0 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.27.1 // - protoc v5.27.1
// source: dns.proto // source: dns.proto
@ -16,8 +16,8 @@ import (
// This is a compile-time assertion to ensure that this generated file // This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against. // is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.62.0 or later. // Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion8 const _ = grpc.SupportPackageIsVersion9
const ( const (
DNSService_GetDNSProfiles_FullMethodName = "/DNSService/getDNSProfiles" DNSService_GetDNSProfiles_FullMethodName = "/DNSService/getDNSProfiles"
@ -39,12 +39,12 @@ type DNSServiceClient interface {
// This method may return the following errors: // This method may return the following errors:
// - RateLimitedError: If too many "full sync" concurrent requests are made. // - RateLimitedError: If too many "full sync" concurrent requests are made.
// - AuthenticationFailedError: If the authentication failed. // - AuthenticationFailedError: If the authentication failed.
GetDNSProfiles(ctx context.Context, in *DNSProfilesRequest, opts ...grpc.CallOption) (DNSService_GetDNSProfilesClient, error) GetDNSProfiles(ctx context.Context, in *DNSProfilesRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[DNSProfile], error)
// Stores devices activity. // Stores devices activity.
// //
// This method may return the following errors: // This method may return the following errors:
// - AuthenticationFailedError: If the authentication failed. // - AuthenticationFailedError: If the authentication failed.
SaveDevicesBillingStat(ctx context.Context, opts ...grpc.CallOption) (DNSService_SaveDevicesBillingStatClient, error) SaveDevicesBillingStat(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[DeviceBillingStat, emptypb.Empty], error)
// Create device by "human_id". // Create device by "human_id".
// //
// This method may return the following errors: // This method may return the following errors:
@ -63,13 +63,13 @@ func NewDNSServiceClient(cc grpc.ClientConnInterface) DNSServiceClient {
return &dNSServiceClient{cc} return &dNSServiceClient{cc}
} }
func (c *dNSServiceClient) GetDNSProfiles(ctx context.Context, in *DNSProfilesRequest, opts ...grpc.CallOption) (DNSService_GetDNSProfilesClient, error) { func (c *dNSServiceClient) GetDNSProfiles(ctx context.Context, in *DNSProfilesRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[DNSProfile], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &DNSService_ServiceDesc.Streams[0], DNSService_GetDNSProfiles_FullMethodName, cOpts...) stream, err := c.cc.NewStream(ctx, &DNSService_ServiceDesc.Streams[0], DNSService_GetDNSProfiles_FullMethodName, cOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
x := &dNSServiceGetDNSProfilesClient{ClientStream: stream} x := &grpc.GenericClientStream[DNSProfilesRequest, DNSProfile]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil { if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err return nil, err
} }
@ -79,57 +79,21 @@ func (c *dNSServiceClient) GetDNSProfiles(ctx context.Context, in *DNSProfilesRe
return x, nil return x, nil
} }
type DNSService_GetDNSProfilesClient interface { // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
Recv() (*DNSProfile, error) type DNSService_GetDNSProfilesClient = grpc.ServerStreamingClient[DNSProfile]
grpc.ClientStream
}
type dNSServiceGetDNSProfilesClient struct { func (c *dNSServiceClient) SaveDevicesBillingStat(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[DeviceBillingStat, emptypb.Empty], error) {
grpc.ClientStream
}
func (x *dNSServiceGetDNSProfilesClient) Recv() (*DNSProfile, error) {
m := new(DNSProfile)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *dNSServiceClient) SaveDevicesBillingStat(ctx context.Context, opts ...grpc.CallOption) (DNSService_SaveDevicesBillingStatClient, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &DNSService_ServiceDesc.Streams[1], DNSService_SaveDevicesBillingStat_FullMethodName, cOpts...) stream, err := c.cc.NewStream(ctx, &DNSService_ServiceDesc.Streams[1], DNSService_SaveDevicesBillingStat_FullMethodName, cOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
x := &dNSServiceSaveDevicesBillingStatClient{ClientStream: stream} x := &grpc.GenericClientStream[DeviceBillingStat, emptypb.Empty]{ClientStream: stream}
return x, nil return x, nil
} }
type DNSService_SaveDevicesBillingStatClient interface { // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
Send(*DeviceBillingStat) error type DNSService_SaveDevicesBillingStatClient = grpc.ClientStreamingClient[DeviceBillingStat, emptypb.Empty]
CloseAndRecv() (*emptypb.Empty, error)
grpc.ClientStream
}
type dNSServiceSaveDevicesBillingStatClient struct {
grpc.ClientStream
}
func (x *dNSServiceSaveDevicesBillingStatClient) Send(m *DeviceBillingStat) error {
return x.ClientStream.SendMsg(m)
}
func (x *dNSServiceSaveDevicesBillingStatClient) CloseAndRecv() (*emptypb.Empty, error) {
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
m := new(emptypb.Empty)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *dNSServiceClient) CreateDeviceByHumanId(ctx context.Context, in *CreateDeviceRequest, opts ...grpc.CallOption) (*CreateDeviceResponse, error) { func (c *dNSServiceClient) CreateDeviceByHumanId(ctx context.Context, in *CreateDeviceRequest, opts ...grpc.CallOption) (*CreateDeviceResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
@ -143,7 +107,7 @@ func (c *dNSServiceClient) CreateDeviceByHumanId(ctx context.Context, in *Create
// DNSServiceServer is the server API for DNSService service. // DNSServiceServer is the server API for DNSService service.
// All implementations must embed UnimplementedDNSServiceServer // All implementations must embed UnimplementedDNSServiceServer
// for forward compatibility // for forward compatibility.
type DNSServiceServer interface { type DNSServiceServer interface {
// Gets DNS profiles. // Gets DNS profiles.
// //
@ -155,12 +119,12 @@ type DNSServiceServer interface {
// This method may return the following errors: // This method may return the following errors:
// - RateLimitedError: If too many "full sync" concurrent requests are made. // - RateLimitedError: If too many "full sync" concurrent requests are made.
// - AuthenticationFailedError: If the authentication failed. // - AuthenticationFailedError: If the authentication failed.
GetDNSProfiles(*DNSProfilesRequest, DNSService_GetDNSProfilesServer) error GetDNSProfiles(*DNSProfilesRequest, grpc.ServerStreamingServer[DNSProfile]) error
// Stores devices activity. // Stores devices activity.
// //
// This method may return the following errors: // This method may return the following errors:
// - AuthenticationFailedError: If the authentication failed. // - AuthenticationFailedError: If the authentication failed.
SaveDevicesBillingStat(DNSService_SaveDevicesBillingStatServer) error SaveDevicesBillingStat(grpc.ClientStreamingServer[DeviceBillingStat, emptypb.Empty]) error
// Create device by "human_id". // Create device by "human_id".
// //
// This method may return the following errors: // This method may return the following errors:
@ -172,20 +136,24 @@ type DNSServiceServer interface {
mustEmbedUnimplementedDNSServiceServer() mustEmbedUnimplementedDNSServiceServer()
} }
// UnimplementedDNSServiceServer must be embedded to have forward compatible implementations. // UnimplementedDNSServiceServer must be embedded to have
type UnimplementedDNSServiceServer struct { // forward compatible implementations.
} //
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedDNSServiceServer struct{}
func (UnimplementedDNSServiceServer) GetDNSProfiles(*DNSProfilesRequest, DNSService_GetDNSProfilesServer) error { func (UnimplementedDNSServiceServer) GetDNSProfiles(*DNSProfilesRequest, grpc.ServerStreamingServer[DNSProfile]) error {
return status.Errorf(codes.Unimplemented, "method GetDNSProfiles not implemented") return status.Errorf(codes.Unimplemented, "method GetDNSProfiles not implemented")
} }
func (UnimplementedDNSServiceServer) SaveDevicesBillingStat(DNSService_SaveDevicesBillingStatServer) error { func (UnimplementedDNSServiceServer) SaveDevicesBillingStat(grpc.ClientStreamingServer[DeviceBillingStat, emptypb.Empty]) error {
return status.Errorf(codes.Unimplemented, "method SaveDevicesBillingStat not implemented") return status.Errorf(codes.Unimplemented, "method SaveDevicesBillingStat not implemented")
} }
func (UnimplementedDNSServiceServer) CreateDeviceByHumanId(context.Context, *CreateDeviceRequest) (*CreateDeviceResponse, error) { func (UnimplementedDNSServiceServer) CreateDeviceByHumanId(context.Context, *CreateDeviceRequest) (*CreateDeviceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateDeviceByHumanId not implemented") return nil, status.Errorf(codes.Unimplemented, "method CreateDeviceByHumanId not implemented")
} }
func (UnimplementedDNSServiceServer) mustEmbedUnimplementedDNSServiceServer() {} func (UnimplementedDNSServiceServer) mustEmbedUnimplementedDNSServiceServer() {}
func (UnimplementedDNSServiceServer) testEmbeddedByValue() {}
// UnsafeDNSServiceServer may be embedded to opt out of forward compatibility for this service. // UnsafeDNSServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to DNSServiceServer will // Use of this interface is not recommended, as added methods to DNSServiceServer will
@ -195,6 +163,13 @@ type UnsafeDNSServiceServer interface {
} }
func RegisterDNSServiceServer(s grpc.ServiceRegistrar, srv DNSServiceServer) { func RegisterDNSServiceServer(s grpc.ServiceRegistrar, srv DNSServiceServer) {
// If the following call pancis, it indicates UnimplementedDNSServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&DNSService_ServiceDesc, srv) s.RegisterService(&DNSService_ServiceDesc, srv)
} }
@ -203,47 +178,18 @@ func _DNSService_GetDNSProfiles_Handler(srv interface{}, stream grpc.ServerStrea
if err := stream.RecvMsg(m); err != nil { if err := stream.RecvMsg(m); err != nil {
return err return err
} }
return srv.(DNSServiceServer).GetDNSProfiles(m, &dNSServiceGetDNSProfilesServer{ServerStream: stream}) return srv.(DNSServiceServer).GetDNSProfiles(m, &grpc.GenericServerStream[DNSProfilesRequest, DNSProfile]{ServerStream: stream})
} }
type DNSService_GetDNSProfilesServer interface { // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
Send(*DNSProfile) error type DNSService_GetDNSProfilesServer = grpc.ServerStreamingServer[DNSProfile]
grpc.ServerStream
}
type dNSServiceGetDNSProfilesServer struct {
grpc.ServerStream
}
func (x *dNSServiceGetDNSProfilesServer) Send(m *DNSProfile) error {
return x.ServerStream.SendMsg(m)
}
func _DNSService_SaveDevicesBillingStat_Handler(srv interface{}, stream grpc.ServerStream) error { func _DNSService_SaveDevicesBillingStat_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(DNSServiceServer).SaveDevicesBillingStat(&dNSServiceSaveDevicesBillingStatServer{ServerStream: stream}) return srv.(DNSServiceServer).SaveDevicesBillingStat(&grpc.GenericServerStream[DeviceBillingStat, emptypb.Empty]{ServerStream: stream})
} }
type DNSService_SaveDevicesBillingStatServer interface { // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
SendAndClose(*emptypb.Empty) error type DNSService_SaveDevicesBillingStatServer = grpc.ClientStreamingServer[DeviceBillingStat, emptypb.Empty]
Recv() (*DeviceBillingStat, error)
grpc.ServerStream
}
type dNSServiceSaveDevicesBillingStatServer struct {
grpc.ServerStream
}
func (x *dNSServiceSaveDevicesBillingStatServer) SendAndClose(m *emptypb.Empty) error {
return x.ServerStream.SendMsg(m)
}
func (x *dNSServiceSaveDevicesBillingStatServer) Recv() (*DeviceBillingStat, error) {
m := new(DeviceBillingStat)
if err := x.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func _DNSService_CreateDeviceByHumanId_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _DNSService_CreateDeviceByHumanId_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateDeviceRequest) in := new(CreateDeviceRequest)

View File

@ -4,21 +4,35 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb" "github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
codes "google.golang.org/grpc/codes" codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status" status "google.golang.org/grpc/status"
) )
// GRPCError is a type alias for string that contains the gRPC error type.
//
// TODO(s.chzhen): Rewrite as soon as the import cycle is resolved.
type GRPCError = string
// gRPC errors of [GRPCError] type.
const (
GRPCErrAuthentication GRPCError = "auth"
GRPCErrBadRequest GRPCError = "bad_req"
GRPCErrDeviceQuota GRPCError = "dev_quota"
GRPCErrOther GRPCError = "other"
GRPCErrRateLimit GRPCError = "rate_limit"
GRPCErrTimeout GRPCError = "timeout"
)
// fixGRPCError converts a gRPC error into an application error, if necessary. // fixGRPCError converts a gRPC error into an application error, if necessary.
// That includes gRPC deadlines, which do not match [context.DeadlineExceeded] // That includes gRPC deadlines, which do not match [context.DeadlineExceeded]
// correctly. // correctly.
// //
// It also updates the backend gRPC metrics depending on the type, see // It also updates the backend gRPC metrics depending on the type, see
// [metrics.IncGRPCErrorsCounter]. // [Metrics.IncrementGRPCErrorCount].
func fixGRPCError(err error) (res error) { func fixGRPCError(ctx context.Context, mtrc Metrics, err error) (res error) {
metricsType := metrics.GRPCErrorTypeOther metricsType := GRPCErrOther
defer func() { metrics.IncGRPCErrorsCounter(metricsType) }() defer func() { mtrc.IncrementGRPCErrorCount(ctx, metricsType) }()
s, ok := status.FromError(err) s, ok := status.FromError(err)
if !ok { if !ok {
@ -30,7 +44,7 @@ func fixGRPCError(err error) (res error) {
// //
// TODO(d.kolyshev): Remove after the grpc-go issue is fixed. // TODO(d.kolyshev): Remove after the grpc-go issue is fixed.
if s.Code() == codes.DeadlineExceeded { if s.Code() == codes.DeadlineExceeded {
metricsType = metrics.GRPCErrorTypeTimeout metricsType = GRPCErrTimeout
return fmt.Errorf("grpc: %w; original message: %s", context.DeadlineExceeded, err) return fmt.Errorf("grpc: %w; original message: %s", context.DeadlineExceeded, err)
} }
@ -38,25 +52,25 @@ func fixGRPCError(err error) (res error) {
for _, d := range s.Details() { for _, d := range s.Details() {
switch structErr := d.(type) { switch structErr := d.(type) {
case *AuthenticationFailedError: case *AuthenticationFailedError:
metricsType = metrics.GRPCErrorTypeAuthentication metricsType = GRPCErrAuthentication
return &profiledb.AuthenticationFailedError{ return &profiledb.AuthenticationFailedError{
Message: structErr.Message, Message: structErr.Message,
} }
case *BadRequestError: case *BadRequestError:
metricsType = metrics.GRPCErrorTypeBadRequest metricsType = GRPCErrBadRequest
return &profiledb.BadRequestError{ return &profiledb.BadRequestError{
Message: structErr.Message, Message: structErr.Message,
} }
case *DeviceQuotaExceededError: case *DeviceQuotaExceededError:
metricsType = metrics.GRPCErrorTypeDeviceQuota metricsType = GRPCErrDeviceQuota
return &profiledb.DeviceQuotaExceededError{ return &profiledb.DeviceQuotaExceededError{
Message: structErr.Message, Message: structErr.Message,
} }
case *RateLimitedError: case *RateLimitedError:
metricsType = metrics.GRPCErrorTypeRateLimit metricsType = GRPCErrRateLimit
return &profiledb.RateLimitedError{ return &profiledb.RateLimitedError{
Message: structErr.Message, Message: structErr.Message,

View File

@ -0,0 +1,36 @@
package backendpb
import (
"context"
"time"
)
// Metrics is an interface that is used for the collection of the protobuf
// errors statistics.
type Metrics interface {
// IncrementGRPCErrorCount increments the gRPC error count of errType.
// errType must be one of GRPCError values.
IncrementGRPCErrorCount(ctx context.Context, errType GRPCError)
// IncrementInvalidDevicesCount increments the number of invalid devices.
IncrementInvalidDevicesCount(ctx context.Context)
// UpdateStats updates profile receiving and decoding statistics.
UpdateStats(ctx context.Context, avgRecv, avgDec time.Duration)
}
// EmptyMetrics is the implementation of the [Metrics] interface that does
// nothing.
type EmptyMetrics struct{}
// type check
var _ Metrics = EmptyMetrics{}
// IncrementGRPCErrorCount implements the [Metrics] interface for EmptyMetrics.
func (EmptyMetrics) IncrementGRPCErrorCount(_ context.Context, errType GRPCError) {}
// IncrementInvalidDevicesCount implements the [Metrics] interface for EmptyMetrics.
func (EmptyMetrics) IncrementInvalidDevicesCount(_ context.Context) {}
// UpdateStats implements the [Metrics] interface for EmptyMetrics.
func (EmptyMetrics) UpdateStats(_ context.Context, _, _ time.Duration) {}

View File

@ -14,15 +14,20 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/geoip" "github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/c2h5oh/datasize"
) )
// toInternal converts the protobuf-encoded data into a profile structure and // toInternal converts the protobuf-encoded data into a profile structure and
// its device structures. // its device structures.
//
// TODO(a.garipov): Refactor into a method of [*ProfileStorage]?
func (x *DNSProfile) toInternal( func (x *DNSProfile) toInternal(
ctx context.Context, ctx context.Context,
updTime time.Time, updTime time.Time,
bindSet netutil.SubnetSet, bindSet netutil.SubnetSet,
errColl errcoll.Interface, errColl errcoll.Interface,
mtrc Metrics,
respSzEst datasize.ByteSize,
) (profile *agd.Profile, devices []*agd.Device, err error) { ) (profile *agd.Profile, devices []*agd.Device, err error) {
if x == nil { if x == nil {
return nil, nil, fmt.Errorf("profile is nil") return nil, nil, fmt.Errorf("profile is nil")
@ -38,7 +43,7 @@ func (x *DNSProfile) toInternal(
return nil, nil, fmt.Errorf("blocking mode: %w", err) return nil, nil, fmt.Errorf("blocking mode: %w", err)
} }
devices, deviceIds := devicesToInternal(ctx, x.Devices, bindSet, errColl) devices, deviceIds := devicesToInternal(ctx, x.Devices, bindSet, errColl, mtrc)
listsEnabled, listIDs := x.RuleLists.toInternal(ctx, errColl) listsEnabled, listIDs := x.RuleLists.toInternal(ctx, errColl)
profID, err := agd.NewProfileID(x.DnsId) profID, err := agd.NewProfileID(x.DnsId)
@ -61,6 +66,7 @@ func (x *DNSProfile) toInternal(
CustomRules: rulesToInternal(ctx, x.CustomRules, errColl), CustomRules: rulesToInternal(ctx, x.CustomRules, errColl),
FilteredResponseTTL: fltRespTTL, FilteredResponseTTL: fltRespTTL,
FilteringEnabled: x.FilteringEnabled, FilteringEnabled: x.FilteringEnabled,
Ratelimiter: x.RateLimit.toInternal(ctx, errColl, respSzEst),
SafeBrowsing: x.SafeBrowsing.toInternal(), SafeBrowsing: x.SafeBrowsing.toInternal(),
Access: x.Access.toInternal(ctx, errColl), Access: x.Access.toInternal(ctx, errColl),
RuleListsEnabled: listsEnabled, RuleListsEnabled: listsEnabled,
@ -98,6 +104,24 @@ func (x *ParentalSettings) toInternal(
}, nil }, nil
} }
// toInternal converts protobuf rate-limiting settings to an internal structure.
// If x is nil, toInternal returns [agd.GlobalRatelimiter].
func (x *RateLimitSettings) toInternal(
ctx context.Context,
errColl errcoll.Interface,
respSzEst datasize.ByteSize,
) (r agd.Ratelimiter) {
if x == nil || !x.Enabled {
return agd.GlobalRatelimiter{}
}
return agd.NewDefaultRatelimiter(&agd.RatelimitConfig{
ClientSubnets: cidrRangeToInternal(ctx, errColl, x.ClientCidr),
RPS: x.Rps,
Enabled: x.Enabled,
}, respSzEst)
}
// toInternal converts protobuf safe-browsing settings to an internal structure. // toInternal converts protobuf safe-browsing settings to an internal structure.
// If x is nil, toInternal returns nil. // If x is nil, toInternal returns nil.
func (x *SafeBrowsingSettings) toInternal() (sb *agd.SafeBrowsingSettings) { func (x *SafeBrowsingSettings) toInternal() (sb *agd.SafeBrowsingSettings) {

View File

@ -14,6 +14,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg" "github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip" "github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/c2h5oh/datasize"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/durationpb"
@ -24,16 +25,19 @@ func TestDNSProfile_ToInternal(t *testing.T) {
ctx := context.Background() ctx := context.Background()
errColl := &agdtest.ErrorCollector{ errColl := agdtest.NewErrorCollector()
OnCollect: func(_ context.Context, err error) {
panic(err)
},
}
t.Run("success", func(t *testing.T) { t.Run("success", func(t *testing.T) {
t.Parallel() t.Parallel()
got, gotDevices, err := NewTestDNSProfile(t).toInternal(ctx, TestUpdTime, TestBind, errColl) got, gotDevices, err := NewTestDNSProfile(t).toInternal(
ctx,
TestUpdTime,
TestBind,
errColl,
EmptyMetrics{},
TestRespSzEst,
)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newProfile(t), got) assert.Equal(t, newProfile(t), got)
@ -54,6 +58,8 @@ func TestDNSProfile_ToInternal(t *testing.T) {
TestUpdTime, TestUpdTime,
TestBind, TestBind,
savingErrColl, savingErrColl,
EmptyMetrics{},
TestRespSzEst,
) )
require.NoError(t, err) require.NoError(t, err)
testutil.AssertErrorMsg( testutil.AssertErrorMsg(
@ -83,6 +89,8 @@ func TestDNSProfile_ToInternal(t *testing.T) {
TestUpdTime, TestUpdTime,
bindSet, bindSet,
savingErrColl, savingErrColl,
EmptyMetrics{},
TestRespSzEst,
) )
require.NoError(t, err) require.NoError(t, err)
testutil.AssertErrorMsg( testutil.AssertErrorMsg(
@ -101,7 +109,14 @@ func TestDNSProfile_ToInternal(t *testing.T) {
t.Parallel() t.Parallel()
var emptyDNSProfile *DNSProfile var emptyDNSProfile *DNSProfile
_, _, err := emptyDNSProfile.toInternal(ctx, TestUpdTime, TestBind, errColl) _, _, err := emptyDNSProfile.toInternal(
ctx,
TestUpdTime,
TestBind,
errColl,
EmptyMetrics{},
TestRespSzEst,
)
testutil.AssertErrorMsg(t, "profile is nil", err) testutil.AssertErrorMsg(t, "profile is nil", err)
}) })
@ -113,7 +128,14 @@ func TestDNSProfile_ToInternal(t *testing.T) {
Deleted: true, Deleted: true,
} }
got, gotDevices, err := dp.toInternal(ctx, TestUpdTime, TestBind, errColl) got, gotDevices, err := dp.toInternal(
ctx,
TestUpdTime,
TestBind,
errColl,
EmptyMetrics{},
TestRespSzEst,
)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, got) require.NotNil(t, got)
@ -128,7 +150,14 @@ func TestDNSProfile_ToInternal(t *testing.T) {
dp := NewTestDNSProfile(t) dp := NewTestDNSProfile(t)
dp.Parental.Schedule.Tmz = "invalid" dp.Parental.Schedule.Tmz = "invalid"
_, _, err := dp.toInternal(ctx, TestUpdTime, TestBind, errColl) _, _, err := dp.toInternal(
ctx,
TestUpdTime,
TestBind,
errColl,
EmptyMetrics{},
TestRespSzEst,
)
testutil.AssertErrorMsg( testutil.AssertErrorMsg(
t, t,
"parental: schedule: loading timezone: unknown time zone invalid", "parental: schedule: loading timezone: unknown time zone invalid",
@ -145,7 +174,14 @@ func TestDNSProfile_ToInternal(t *testing.T) {
End: nil, End: nil,
} }
_, _, err := dp.toInternal(ctx, TestUpdTime, TestBind, errColl) _, _, err := dp.toInternal(
ctx,
TestUpdTime,
TestBind,
errColl,
EmptyMetrics{},
TestRespSzEst,
)
testutil.AssertErrorMsg( testutil.AssertErrorMsg(
t, t,
"parental: schedule: weekday Sunday: bad day range: end 0 less than start 16", "parental: schedule: weekday Sunday: bad day range: end 0 less than start 16",
@ -160,7 +196,14 @@ func TestDNSProfile_ToInternal(t *testing.T) {
bm := dp.BlockingMode.(*DNSProfile_BlockingModeCustomIp) bm := dp.BlockingMode.(*DNSProfile_BlockingModeCustomIp)
bm.BlockingModeCustomIp.Ipv4 = []byte("1") bm.BlockingModeCustomIp.Ipv4 = []byte("1")
_, _, err := dp.toInternal(ctx, TestUpdTime, TestBind, errColl) _, _, err := dp.toInternal(
ctx,
TestUpdTime,
TestBind,
errColl,
EmptyMetrics{},
TestRespSzEst,
)
testutil.AssertErrorMsg(t, "blocking mode: bad custom ipv4: unexpected slice size", err) testutil.AssertErrorMsg(t, "blocking mode: bad custom ipv4: unexpected slice size", err)
}) })
@ -171,7 +214,14 @@ func TestDNSProfile_ToInternal(t *testing.T) {
bm := dp.BlockingMode.(*DNSProfile_BlockingModeCustomIp) bm := dp.BlockingMode.(*DNSProfile_BlockingModeCustomIp)
bm.BlockingModeCustomIp.Ipv6 = []byte("1") bm.BlockingModeCustomIp.Ipv6 = []byte("1")
_, _, err := dp.toInternal(ctx, TestUpdTime, TestBind, errColl) _, _, err := dp.toInternal(
ctx,
TestUpdTime,
TestBind,
errColl,
EmptyMetrics{},
TestRespSzEst,
)
testutil.AssertErrorMsg(t, "blocking mode: bad custom ipv6: unexpected slice size", err) testutil.AssertErrorMsg(t, "blocking mode: bad custom ipv6: unexpected slice size", err)
}) })
@ -183,7 +233,14 @@ func TestDNSProfile_ToInternal(t *testing.T) {
bm.BlockingModeCustomIp.Ipv4 = nil bm.BlockingModeCustomIp.Ipv4 = nil
bm.BlockingModeCustomIp.Ipv6 = nil bm.BlockingModeCustomIp.Ipv6 = nil
_, _, err := dp.toInternal(ctx, TestUpdTime, TestBind, errColl) _, _, err := dp.toInternal(
ctx,
TestUpdTime,
TestBind,
errColl,
EmptyMetrics{},
TestRespSzEst,
)
testutil.AssertErrorMsg(t, "blocking mode: no valid custom ips found", err) testutil.AssertErrorMsg(t, "blocking mode: no valid custom ips found", err)
}) })
@ -193,7 +250,14 @@ func TestDNSProfile_ToInternal(t *testing.T) {
dp := NewTestDNSProfile(t) dp := NewTestDNSProfile(t)
dp.BlockingMode = nil dp.BlockingMode = nil
got, gotDevices, err := dp.toInternal(ctx, TestUpdTime, TestBind, errColl) got, gotDevices, err := dp.toInternal(
ctx,
TestUpdTime,
TestBind,
errColl,
EmptyMetrics{},
TestRespSzEst,
)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, got) require.NotNil(t, got)
@ -210,7 +274,14 @@ func TestDNSProfile_ToInternal(t *testing.T) {
dp := NewTestDNSProfile(t) dp := NewTestDNSProfile(t)
dp.Access = nil dp.Access = nil
got, _, err := dp.toInternal(ctx, TestUpdTime, TestBind, errColl) got, _, err := dp.toInternal(
ctx,
TestUpdTime,
TestBind,
errColl,
EmptyMetrics{},
TestRespSzEst,
)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, got) require.NotNil(t, got)
@ -226,7 +297,14 @@ func TestDNSProfile_ToInternal(t *testing.T) {
Enabled: false, Enabled: false,
} }
got, _, err := dp.toInternal(ctx, TestUpdTime, TestBind, errColl) got, _, err := dp.toInternal(
ctx,
TestUpdTime,
TestBind,
errColl,
EmptyMetrics{},
TestRespSzEst,
)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, got) require.NotNil(t, got)
@ -378,6 +456,14 @@ func NewTestDNSProfile(tb testing.TB) (dp *DNSProfile) {
BlocklistDomainRules: []string{"block.test"}, BlocklistDomainRules: []string{"block.test"},
Enabled: true, Enabled: true,
}, },
RateLimit: &RateLimitSettings{
Enabled: true,
Rps: 100,
ClientCidr: []*CidrRange{{
Address: netip.MustParseAddr("5.5.5.0").AsSlice(),
Prefix: 24,
}},
},
} }
} }
@ -442,6 +528,12 @@ func newProfile(tb testing.TB) (p *agd.Profile) {
BlocklistDomainRules: []string{"block.test"}, BlocklistDomainRules: []string{"block.test"},
}) })
wantRateLimiter := agd.NewDefaultRatelimiter(&agd.RatelimitConfig{
ClientSubnets: []netip.Prefix{netip.MustParsePrefix("5.5.5.0/24")},
RPS: 100,
Enabled: true,
}, 1*datasize.KB)
return &agd.Profile{ return &agd.Profile{
Parental: wantParental, Parental: wantParental,
BlockingMode: wantBlockingMode, BlockingMode: wantBlockingMode,
@ -456,6 +548,7 @@ func newProfile(tb testing.TB) (p *agd.Profile) {
RuleListIDs: []agd.FilterListID{"1"}, RuleListIDs: []agd.FilterListID{"1"},
CustomRules: []agd.FilterRuleText{"||example.org^"}, CustomRules: []agd.FilterRuleText{"||example.org^"},
FilteredResponseTTL: 10 * time.Second, FilteredResponseTTL: 10 * time.Second,
Ratelimiter: wantRateLimiter,
SafeBrowsing: wantSafeBrowsing, SafeBrowsing: wantSafeBrowsing,
Access: wantAccess, Access: wantAccess,
RuleListsEnabled: true, RuleListsEnabled: true,
@ -528,16 +621,19 @@ func BenchmarkDNSProfile_ToInternal(b *testing.B) {
dp := NewTestDNSProfile(b) dp := NewTestDNSProfile(b)
ctx := context.Background() ctx := context.Background()
errColl := &agdtest.ErrorCollector{ errColl := agdtest.NewErrorCollector()
OnCollect: func(_ context.Context, err error) {
panic(err)
},
}
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
for range b.N { for range b.N {
profSink, _, errSink = dp.toInternal(ctx, TestUpdTime, TestBind, errColl) profSink, _, errSink = dp.toInternal(
ctx,
TestUpdTime,
TestBind,
errColl,
EmptyMetrics{},
TestRespSzEst,
)
} }
require.NotNil(b, profSink) require.NotNil(b, profSink)
@ -548,6 +644,5 @@ func BenchmarkDNSProfile_ToInternal(b *testing.B) {
// goarch: amd64 // goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/backendpb // pkg: github.com/AdguardTeam/AdGuardDNS/internal/backendpb
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics // cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkDNSProfile_ToInternal // BenchmarkDNSProfile_ToInternal-16 67160 22130 ns/op 3048 B/op 51 allocs/op
// BenchmarkDNSProfile_ToInternal-16 93568 19203 ns/op 1976 B/op 45 allocs/op
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"log/slog"
"net/url" "net/url"
"strconv" "strconv"
"time" "time"
@ -13,6 +14,8 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb" "github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/c2h5oh/datasize"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
) )
@ -21,18 +24,35 @@ import (
// profile storage. // profile storage.
type ProfileStorageConfig struct { type ProfileStorageConfig struct {
// BindSet is the subnet set created from DNS servers listening addresses. // BindSet is the subnet set created from DNS servers listening addresses.
// It must not be nil.
BindSet netutil.SubnetSet BindSet netutil.SubnetSet
// ErrColl is the error collector that is used to collect critical and // ErrColl is the error collector that is used to collect critical and
// non-critical errors. // non-critical errors. It must not be nil.
ErrColl errcoll.Interface ErrColl errcoll.Interface
// Logger is used as the base logger for the profile storage. It must not
// be nil.
Logger *slog.Logger
// Metrics is used for the collection of the protobuf errors.
Metrics Metrics
// Endpoint is the backend API URL. The scheme should be either "grpc" or // Endpoint is the backend API URL. The scheme should be either "grpc" or
// "grpcs". // "grpcs". It must not be nil.
Endpoint *url.URL Endpoint *url.URL
// APIKey is the API key used for authentication, if any. // APIKey is the API key used for authentication, if any. If empty, no
// authentication is performed.
APIKey string APIKey string
// ResponseSizeEstimate is the estimate of the size of one DNS response for
// the purposes of custom ratelimiting. Responses over this estimate are
// counted as several responses.
ResponseSizeEstimate datasize.ByteSize
// MaxProfilesSize is the maximum response size for the profiles endpoint.
MaxProfilesSize datasize.ByteSize
} }
// ProfileStorage is the implementation of the [profiledb.Storage] interface // ProfileStorage is the implementation of the [profiledb.Storage] interface
@ -42,7 +62,11 @@ type ProfileStorage struct {
bindSet netutil.SubnetSet bindSet netutil.SubnetSet
errColl errcoll.Interface errColl errcoll.Interface
client DNSServiceClient client DNSServiceClient
logger *slog.Logger
metrics Metrics
apiKey string apiKey string
respSzEst datasize.ByteSize
maxProfSize datasize.ByteSize
} }
// NewProfileStorage returns a new [ProfileStorage] that retrieves information // NewProfileStorage returns a new [ProfileStorage] that retrieves information
@ -58,7 +82,11 @@ func NewProfileStorage(c *ProfileStorageConfig) (s *ProfileStorage, err error) {
bindSet: c.BindSet, bindSet: c.BindSet,
errColl: c.ErrColl, errColl: c.ErrColl,
client: client, client: client,
logger: c.Logger,
metrics: c.Metrics,
apiKey: c.APIKey, apiKey: c.APIKey,
respSzEst: c.ResponseSizeEstimate,
maxProfSize: c.MaxProfilesSize,
}, nil }, nil
} }
@ -87,7 +115,7 @@ func (s *ProfileStorage) CreateAutoDevice(
DeviceType: DeviceType(req.DeviceType), DeviceType: DeviceType(req.DeviceType),
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("calling backend: %w", fixGRPCError(err)) return nil, fmt.Errorf("calling backend: %w", fixGRPCError(ctx, s.metrics, err))
} }
d, err := backendResp.Device.toInternal(s.bindSet) d, err := backendResp.Device.toInternal(s.bindSet)
@ -106,9 +134,13 @@ func (s *ProfileStorage) Profiles(
req *profiledb.StorageProfilesRequest, req *profiledb.StorageProfilesRequest,
) (resp *profiledb.StorageProfilesResponse, err error) { ) (resp *profiledb.StorageProfilesResponse, err error) {
ctx = ctxWithAuthentication(ctx, s.apiKey) ctx = ctxWithAuthentication(ctx, s.apiKey)
stream, err := s.client.GetDNSProfiles(ctx, toProtobuf(req))
// #nosec G115 -- The value of limit comes from validated environment
// variables.
respSzOpt := grpc.MaxCallRecvMsgSize(int(s.maxProfSize.Bytes()))
stream, err := s.client.GetDNSProfiles(ctx, toProtobuf(req), respSzOpt)
if err != nil { if err != nil {
return nil, fmt.Errorf("loading profiles: %w", fixGRPCError(err)) return nil, fmt.Errorf("loading profiles: %w", fixGRPCError(ctx, s.metrics, err))
} }
defer func() { err = errors.WithDeferred(err, stream.CloseSend()) }() defer func() { err = errors.WithDeferred(err, stream.CloseSend()) }()
@ -118,6 +150,7 @@ func (s *ProfileStorage) Profiles(
} }
stats := &profilesCallStats{ stats := &profilesCallStats{
logger: s.logger,
isFullSync: req.SyncTime.IsZero(), isFullSync: req.SyncTime.IsZero(),
} }
@ -129,12 +162,23 @@ func (s *ProfileStorage) Profiles(
break break
} }
return nil, fmt.Errorf("receiving profile #%d: %w", n, fixGRPCError(profErr)) return nil, fmt.Errorf(
"receiving profile #%d: %w",
n,
fixGRPCError(ctx, s.metrics, profErr),
)
} }
stats.endRecv() stats.endRecv()
stats.startDec() stats.startDec()
prof, devices, profErr := profile.toInternal(ctx, time.Now(), s.bindSet, s.errColl) prof, devices, profErr := profile.toInternal(
ctx,
time.Now(),
s.bindSet,
s.errColl,
s.metrics,
s.respSzEst,
)
if profErr != nil { if profErr != nil {
reportf(ctx, s.errColl, "loading profile: %w", profErr) reportf(ctx, s.errColl, "loading profile: %w", profErr)
@ -146,7 +190,7 @@ func (s *ProfileStorage) Profiles(
resp.Devices = append(resp.Devices, devices...) resp.Devices = append(resp.Devices, devices...)
} }
stats.report() stats.report(ctx, s.metrics)
trailer := stream.Trailer() trailer := stream.Trailer()
resp.SyncTime, err = syncTimeFromTrailer(trailer) resp.SyncTime, err = syncTimeFromTrailer(trailer)

View File

@ -13,12 +13,15 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest" "github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/backendpb" "github.com/AdguardTeam/AdGuardDNS/internal/backendpb"
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb" "github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/c2h5oh/datasize"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/emptypb"
) )
func TestProfileStorage_CreateAutoDevice(t *testing.T) { func TestProfileStorage_CreateAutoDevice(t *testing.T) {
@ -48,30 +51,26 @@ func TestProfileStorage_CreateAutoDevice(t *testing.T) {
OnGetDNSProfiles: func( OnGetDNSProfiles: func(
req *backendpb.DNSProfilesRequest, req *backendpb.DNSProfilesRequest,
srv backendpb.DNSService_GetDNSProfilesServer, srv grpc.ServerStreamingServer[backendpb.DNSProfile],
) (err error) { ) (err error) {
panic("not implemented") panic("not implemented")
}, },
OnSaveDevicesBillingStat: func( OnSaveDevicesBillingStat: func(
srv backendpb.DNSService_SaveDevicesBillingStatServer, srv grpc.ClientStreamingServer[backendpb.DeviceBillingStat, emptypb.Empty],
) (err error) { ) (err error) {
panic("not implemented") panic("not implemented")
}, },
} }
errColl := &agdtest.ErrorCollector{
OnCollect: func(_ context.Context, err error) {
panic(err)
},
}
l, err := net.Listen("tcp", "localhost:0") l, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err) require.NoError(t, err)
s, err := backendpb.NewProfileStorage(&backendpb.ProfileStorageConfig{ s, err := backendpb.NewProfileStorage(&backendpb.ProfileStorageConfig{
BindSet: backendpb.TestBind, BindSet: backendpb.TestBind,
ErrColl: errColl, ErrColl: agdtest.NewErrorCollector(),
Logger: slogutil.NewDiscardLogger(),
Metrics: backendpb.EmptyMetrics{},
Endpoint: &url.URL{ Endpoint: &url.URL{
Scheme: "grpc", Scheme: "grpc",
Host: l.Addr().String(), Host: l.Addr().String(),
@ -139,7 +138,7 @@ func BenchmarkProfileStorage_Profiles(b *testing.B) {
OnGetDNSProfiles: func( OnGetDNSProfiles: func(
req *backendpb.DNSProfilesRequest, req *backendpb.DNSProfilesRequest,
srv backendpb.DNSService_GetDNSProfilesServer, srv grpc.ServerStreamingServer[backendpb.DNSProfile],
) (err error) { ) (err error) {
sendErr := srv.Send(srvProf) sendErr := srv.Send(srvProf)
srv.SetTrailer(trailerMD) srv.SetTrailer(trailerMD)
@ -148,28 +147,25 @@ func BenchmarkProfileStorage_Profiles(b *testing.B) {
}, },
OnSaveDevicesBillingStat: func( OnSaveDevicesBillingStat: func(
srv backendpb.DNSService_SaveDevicesBillingStatServer, srv grpc.ClientStreamingServer[backendpb.DeviceBillingStat, emptypb.Empty],
) (err error) { ) (err error) {
panic("not implemented") panic("not implemented")
}, },
} }
errColl := &agdtest.ErrorCollector{
OnCollect: func(_ context.Context, err error) {
panic(err)
},
}
l, err := net.Listen("tcp", "localhost:0") l, err := net.Listen("tcp", "localhost:0")
require.NoError(b, err) require.NoError(b, err)
s, err := backendpb.NewProfileStorage(&backendpb.ProfileStorageConfig{ s, err := backendpb.NewProfileStorage(&backendpb.ProfileStorageConfig{
BindSet: netip.MustParsePrefix("0.0.0.0/0"), BindSet: netip.MustParsePrefix("0.0.0.0/0"),
ErrColl: errColl, ErrColl: agdtest.NewErrorCollector(),
Logger: slogutil.NewDiscardLogger(),
Metrics: backendpb.EmptyMetrics{},
Endpoint: &url.URL{ Endpoint: &url.URL{
Scheme: "grpc", Scheme: "grpc",
Host: l.Addr().String(), Host: l.Addr().String(),
}, },
MaxProfilesSize: 1 * datasize.MB,
}) })
require.NoError(b, err) require.NoError(b, err)
@ -199,11 +195,8 @@ func BenchmarkProfileStorage_Profiles(b *testing.B) {
require.NoError(b, errSink) require.NoError(b, errSink)
require.NotNil(b, respSink) require.NotNil(b, respSink)
// Most recent result, on a ThinkPad X13: // goos: darwin
// goos: linux // goarch: arm64
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/backendpb // pkg: github.com/AdguardTeam/AdGuardDNS/internal/backendpb
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics // BenchmarkProfileStorage_Profiles-8 11599 104725 ns/op 18281 B/op 328 allocs/op
// BenchmarkProfileStorage_Profiles
// BenchmarkProfileStorage_Profiles-16 4128 291646 ns/op 18578 B/op 341 allocs/op
} }

View File

@ -1,15 +1,16 @@
package backendpb package backendpb
import ( import (
"context"
"log/slog"
"time" "time"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/golibs/log"
) )
// profilesCallStats is a stateful structure that collects and reports // profilesCallStats is a stateful structure that collects and reports
// statistics about a [ProfileStorage.Profiles] call. // statistics about a [ProfileStorage.Profiles] call.
type profilesCallStats struct { type profilesCallStats struct {
logger *slog.Logger
recvStart time.Time recvStart time.Time
decStart time.Time decStart time.Time
@ -53,14 +54,14 @@ func (s *profilesCallStats) endDec() {
} }
// report writes the statistics to the log and the metrics. // report writes the statistics to the log and the metrics.
func (s *profilesCallStats) report() { func (s *profilesCallStats) report(ctx context.Context, mtrc Metrics) {
logFunc := log.Debug lvl := slog.LevelDebug
if s.isFullSync { if s.isFullSync {
logFunc = log.Info lvl = slog.LevelInfo
} }
if s.numRecv == 0 { if s.numRecv == 0 {
logFunc("backendpb: no recv") s.logger.Log(ctx, lvl, "no recv")
return return
} }
@ -69,14 +70,8 @@ func (s *profilesCallStats) report() {
avgRecv := s.totalRecv / n avgRecv := s.totalRecv / n
avgDec := s.totalDec / n avgDec := s.totalDec / n
logFunc( s.logger.Log(ctx, lvl, "recv stats", "total", s.totalRecv, "avg", avgRecv, "init", s.initRecv)
"backendpb: total recv: %s; agv recv: %s; init recv: %s", s.logger.Log(ctx, lvl, "decode stats", "total", s.totalDec, "avg", avgDec)
s.totalRecv,
avgRecv,
s.initRecv,
)
logFunc("backendpb: total dec: %s; agv dec: %s", s.totalDec, avgDec)
metrics.GRPCAvgProfileRecvDuration.Observe(avgRecv.Seconds()) mtrc.UpdateStats(ctx, avgRecv, avgDec)
metrics.GRPCAvgProfileDecDuration.Observe(avgDec.Seconds())
} }

View File

@ -9,8 +9,6 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/geoip" "github.com/AdguardTeam/AdGuardDNS/internal/geoip"
) )
// Common Constants, Types, And Utilities
// Recorder is the billing statistics recorder interface. // Recorder is the billing statistics recorder interface.
type Recorder interface { type Recorder interface {
Record( Record(
@ -60,6 +58,7 @@ type Record struct {
// Queries is the total number of Queries the device has performed since the // 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 // 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. // logic backend which uses this type. Change it if it is changed there.
// Queries must not be negative.
Queries int32 Queries int32
// Proto is the DNS protocol of the most recent query from the device. // Proto is the DNS protocol of the most recent query from the device.

View File

@ -1,17 +1,6 @@
package billstat_test package billstat_test
import ( import "time"
"testing"
"time"
"github.com/AdguardTeam/golibs/testutil"
)
// Common Constants And Utilities
func TestMain(m *testing.M) {
testutil.DiscardLogOutput(m)
}
// testTimeout is the timeout for common test operations. // testTimeout is the timeout for common test operations.
const testTimeout = 1 * time.Second const testTimeout = 1 * time.Second

View File

@ -0,0 +1,26 @@
package billstat
import "context"
// Metrics is an interface that is used for the collection of the billing
// statistics.
type Metrics interface {
// BufferSizeSet sets the number of stored records to n.
BufferSizeSet(ctx context.Context, n float64)
// HandleUploadDuration handles the upload duration of billing statistics.
HandleUploadDuration(ctx context.Context, dur float64, isSuccess bool)
}
// EmptyMetrics is the implementation of the [Metrics] interface that does
// nothing.
type EmptyMetrics struct{}
// type check
var _ Metrics = EmptyMetrics{}
// BufferSizeSet implements the [Metrics] interface for EmptyMetrics.
func (EmptyMetrics) BufferSizeSet(_ context.Context, _ float64) {}
// HandleUploadDuration implements the [Metrics] interface for EmptyMetrics.
func (EmptyMetrics) HandleUploadDuration(_ context.Context, _ float64, _ bool) {}

View File

@ -2,6 +2,7 @@ package billstat
import ( import (
"context" "context"
"log/slog"
"sync" "sync"
"time" "time"
@ -9,36 +10,42 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/agdservice" "github.com/AdguardTeam/AdGuardDNS/internal/agdservice"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll" "github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip" "github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/golibs/log"
) )
// Runtime Billing Statistics Recorder
// RuntimeRecorderConfig is the configuration structure for a runtime billing // RuntimeRecorderConfig is the configuration structure for a runtime billing
// statistics recorder. All fields must be non-empty. // statistics recorder. All fields must be non-empty.
type RuntimeRecorderConfig struct { type RuntimeRecorderConfig struct {
// Logger is used for logging the operation of the recorder.
Logger *slog.Logger
// ErrColl is used to collect errors during refreshes. // ErrColl is used to collect errors during refreshes.
ErrColl errcoll.Interface ErrColl errcoll.Interface
// Uploader is used to upload the billing statistics records to. // Uploader is used to upload the billing statistics records to.
Uploader Uploader Uploader Uploader
// Metrics is used for the collection of the billing statistics.
Metrics Metrics
} }
// NewRuntimeRecorder creates a new runtime billing statistics database. c must // NewRuntimeRecorder creates a new runtime billing statistics database. c must
// be non-nil. // be non-nil.
func NewRuntimeRecorder(c *RuntimeRecorderConfig) (r *RuntimeRecorder) { func NewRuntimeRecorder(c *RuntimeRecorderConfig) (r *RuntimeRecorder) {
return &RuntimeRecorder{ return &RuntimeRecorder{
logger: c.Logger,
mu: &sync.Mutex{}, mu: &sync.Mutex{},
records: Records{}, records: Records{},
uploader: c.Uploader, uploader: c.Uploader,
errColl: c.ErrColl, errColl: c.ErrColl,
metrics: c.Metrics,
} }
} }
// RuntimeRecorder is the runtime billing statistics recorder. The records kept // RuntimeRecorder is the runtime billing statistics recorder. The records kept
// here are not persistent. // here are not persistent.
type RuntimeRecorder struct { type RuntimeRecorder struct {
logger *slog.Logger
// mu protects records and syncTime. // mu protects records and syncTime.
mu *sync.Mutex mu *sync.Mutex
@ -51,6 +58,9 @@ type RuntimeRecorder struct {
// errColl is used to collect errors during refreshes. // errColl is used to collect errors during refreshes.
errColl errcoll.Interface errColl errcoll.Interface
// metrics is used for the collection of the billing statistics.
metrics Metrics
} }
// type check // type check
@ -65,10 +75,6 @@ func (r *RuntimeRecorder) Record(
start time.Time, start time.Time,
proto agd.Protocol, proto agd.Protocol,
) { ) {
// TODO(a.garipov): Use slog.
log.Debug("billstat_refresh: started")
defer log.Debug("billstat_refresh: finished")
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
@ -82,7 +88,7 @@ func (r *RuntimeRecorder) Record(
Proto: proto, Proto: proto,
} }
metrics.BillStatBufSize.Add(1) r.metrics.BufferSizeSet(ctx, float64(len(r.records)))
} else { } else {
rec.Time = start rec.Time = start
rec.Country = ctry rec.Country = ctry
@ -98,26 +104,27 @@ var _ agdservice.Refresher = (*RuntimeRecorder)(nil)
// Refresh implements the [agdserivce.Refresher] interface for *RuntimeRecorder. // Refresh implements the [agdserivce.Refresher] interface for *RuntimeRecorder.
// It uploads the currently available data and resets it. // It uploads the currently available data and resets it.
func (r *RuntimeRecorder) Refresh(ctx context.Context) (err error) { func (r *RuntimeRecorder) Refresh(ctx context.Context) (err error) {
records := r.resetRecords() r.logger.DebugContext(ctx, "refresh started")
defer r.logger.DebugContext(ctx, "refresh finished")
records := r.resetRecords(ctx)
startTime := time.Now() startTime := time.Now()
defer func() { defer func() {
dur := time.Since(startTime).Seconds() dur := time.Since(startTime).Seconds()
metrics.BillStatUploadDuration.Observe(dur)
if err != nil { isSuccess := err == nil
r.remergeRecords(records) if !isSuccess {
log.Info("billstat_refresh: failed, records remerged") r.remergeRecords(ctx, records)
} else { r.logger.WarnContext(ctx, "refresh failed, records remerged")
metrics.BillStatUploadTimestamp.SetToCurrentTime()
} }
metrics.SetStatusGauge(metrics.BillStatUploadStatus, err) r.metrics.HandleUploadDuration(ctx, dur, isSuccess)
}() }()
err = r.uploader.Upload(ctx, records) err = r.uploader.Upload(ctx, records)
if err != nil { if err != nil {
errcoll.Collectf(ctx, r.errColl, "billstat_refresh: %w", err) errcoll.Collect(ctx, r.errColl, r.logger, "uploading billstat", err)
} }
return err return err
@ -125,20 +132,20 @@ func (r *RuntimeRecorder) Refresh(ctx context.Context) (err error) {
// resetRecords returns the current data and resets the records map to an empty // resetRecords returns the current data and resets the records map to an empty
// map. // map.
func (r *RuntimeRecorder) resetRecords() (records Records) { func (r *RuntimeRecorder) resetRecords(ctx context.Context) (records Records) {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
records, r.records = r.records, Records{} records, r.records = r.records, Records{}
metrics.BillStatBufSize.Set(0) r.metrics.BufferSizeSet(ctx, 0)
return records return records
} }
// remergeRecords merges records back into the database, unless there is already // remergeRecords merges records back into the database, unless there is already
// a newer record, in which case it merges the results. // a newer record, in which case it merges the results.
func (r *RuntimeRecorder) remergeRecords(records Records) { func (r *RuntimeRecorder) remergeRecords(ctx context.Context, records Records) {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
@ -150,5 +157,5 @@ func (r *RuntimeRecorder) remergeRecords(records Records) {
} }
} }
metrics.BillStatBufSize.Set(float64(len(r.records))) r.metrics.BufferSizeSet(ctx, float64(len(r.records)))
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/billstat" "github.com/AdguardTeam/AdGuardDNS/internal/billstat"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip" "github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -30,11 +31,8 @@ const (
func TestRuntimeRecorder_success(t *testing.T) { func TestRuntimeRecorder_success(t *testing.T) {
var gotRecord *billstat.Record var gotRecord *billstat.Record
c := &billstat.RuntimeRecorderConfig{ c := &billstat.RuntimeRecorderConfig{
ErrColl: &agdtest.ErrorCollector{ Logger: slogutil.NewDiscardLogger(),
OnCollect: func(_ context.Context, _ error) { ErrColl: agdtest.NewErrorCollector(),
panic("not implemented")
},
},
Uploader: &agdtest.BillStatUploader{ Uploader: &agdtest.BillStatUploader{
OnUpload: func(_ context.Context, records billstat.Records) (err error) { OnUpload: func(_ context.Context, records billstat.Records) (err error) {
gotRecord = records[devID] gotRecord = records[devID]
@ -42,6 +40,7 @@ func TestRuntimeRecorder_success(t *testing.T) {
return nil return nil
}, },
}, },
Metrics: billstat.EmptyMetrics{},
} }
r := billstat.NewRuntimeRecorder(c) r := billstat.NewRuntimeRecorder(c)
@ -96,6 +95,7 @@ func TestRuntimeRecorder_fail(t *testing.T) {
var gotCollErr error var gotCollErr error
c := &billstat.RuntimeRecorderConfig{ c := &billstat.RuntimeRecorderConfig{
Logger: slogutil.NewDiscardLogger(),
ErrColl: &agdtest.ErrorCollector{ ErrColl: &agdtest.ErrorCollector{
OnCollect: func(_ context.Context, err error) { OnCollect: func(_ context.Context, err error) {
gotCollErr = err gotCollErr = err
@ -104,6 +104,7 @@ func TestRuntimeRecorder_fail(t *testing.T) {
Uploader: &agdtest.BillStatUploader{ Uploader: &agdtest.BillStatUploader{
OnUpload: onUpload, OnUpload: onUpload,
}, },
Metrics: billstat.EmptyMetrics{},
} }
r := billstat.NewRuntimeRecorder(c) r := billstat.NewRuntimeRecorder(c)

View File

@ -2,17 +2,11 @@ package bindtodevice_test
import ( import (
"net/netip" "net/netip"
"testing"
"time" "time"
"github.com/AdguardTeam/AdGuardDNS/internal/bindtodevice" "github.com/AdguardTeam/AdGuardDNS/internal/bindtodevice"
"github.com/AdguardTeam/golibs/testutil"
) )
func TestMain(m *testing.M) {
testutil.DiscardLogOutput(m)
}
// testTimeout is a common timeout for tests. // testTimeout is a common timeout for tests.
const testTimeout = 1 * time.Second const testTimeout = 1 * time.Second

View File

@ -5,14 +5,15 @@ package bindtodevice
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog"
"net" "net"
"time" "time"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll" "github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics" "github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/AdGuardDNS/internal/optlog" "github.com/AdguardTeam/AdGuardDNS/internal/optslog"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/syncutil" "github.com/AdguardTeam/golibs/syncutil"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
@ -20,6 +21,7 @@ import (
// interfaceListener contains information about a single interface listener. // interfaceListener contains information about a single interface listener.
type interfaceListener struct { type interfaceListener struct {
logger *slog.Logger
conns *connIndex conns *connIndex
listenConf *net.ListenConfig listenConf *net.ListenConfig
bodyPool *syncutil.Pool[[]byte] bodyPool *syncutil.Pool[[]byte]
@ -36,10 +38,10 @@ type interfaceListener struct {
// listenTCP runs the TCP listening loop. It is intended to be used as a // listenTCP runs the TCP listening loop. It is intended to be used as a
// goroutine. errCh receives nil if the listening has started successfully or // goroutine. errCh receives nil if the listening has started successfully or
// the listening error if not. // the listening error if not.
func (l *interfaceListener) listenTCP(errCh chan<- error) { func (l *interfaceListener) listenTCP(ctx context.Context, errCh chan<- error) {
defer log.OnPanic("interfaceListener.listenTCP") logger := l.logger.With("network", "tcp")
defer slogutil.RecoverAndLog(ctx, logger)
ctx := context.Background()
addrStr := netutil.JoinHostPort("0.0.0.0", l.port) addrStr := netutil.JoinHostPort("0.0.0.0", l.port)
tcpListener, err := l.listenConf.Listen(ctx, "tcp", addrStr) tcpListener, err := l.listenConf.Listen(ctx, "tcp", addrStr)
@ -48,14 +50,12 @@ func (l *interfaceListener) listenTCP(errCh chan<- error) {
return return
} }
logPrefix := fmt.Sprintf("bindtodevice: listener %s:%d: tcp", l.ifaceName, l.port) logger.InfoContext(ctx, "starting")
log.Info("%s: starting", logPrefix)
for { for {
select { select {
case <-l.done: case <-l.done:
log.Info("%s: done", logPrefix) logger.InfoContext(ctx, "done")
return return
default: default:
@ -65,23 +65,23 @@ func (l *interfaceListener) listenTCP(errCh chan<- error) {
var conn net.Conn var conn net.Conn
conn, err = tcpListener.Accept() conn, err = tcpListener.Accept()
if err != nil { if err != nil {
errcoll.Collectf(ctx, l.errColl, "%s: accepting: %w", logPrefix, err) errcoll.Collect(ctx, l.errColl, logger, "accepting", err)
continue continue
} }
l.processConn(conn, logPrefix) l.processConn(ctx, logger, conn)
} }
} }
// processConn processes a single connection. If the connection doesn't have a // processConn processes a single connection. If the connection doesn't have a
// connected channel-listener, it is closed. // connected channel-listener, it is closed.
func (l *interfaceListener) processConn(conn net.Conn, logPrefix string) { func (l *interfaceListener) processConn(ctx context.Context, logger *slog.Logger, conn net.Conn) {
laddr := netutil.NetAddrToAddrPort(conn.LocalAddr()) laddr := netutil.NetAddrToAddrPort(conn.LocalAddr())
raddr := conn.RemoteAddr() raddr := conn.RemoteAddr()
if lsnr := l.conns.listener(laddr.Addr()); lsnr != nil { if lsnr := l.conns.listener(laddr.Addr()); lsnr != nil {
if !lsnr.send(conn) { if !lsnr.send(conn) {
optlog.Debug3("%s: from raddr %s: channel for laddr %s is closed", logPrefix, raddr, laddr) optslog.Debug2(ctx, logger, "channel is closed", "raddr", raddr, "laddr", laddr)
} }
return return
@ -89,21 +89,21 @@ func (l *interfaceListener) processConn(conn net.Conn, logPrefix string) {
metrics.BindToDeviceUnknownTCPRequestsTotal.Inc() metrics.BindToDeviceUnknownTCPRequestsTotal.Inc()
optlog.Debug3("%s: from raddr %s: no stream channel for laddr %s", logPrefix, raddr, laddr) optslog.Debug2(ctx, logger, "no stream channel", "raddr", raddr, "laddr", laddr)
err := conn.Close() err := conn.Close()
if err != nil { if err != nil {
optlog.Debug3("%s: from raddr %s: closing: %s", logPrefix, raddr, err) optslog.Debug2(ctx, logger, "closing", "raddr", raddr, slogutil.KeyError, err)
} }
} }
// listenUDP runs the UDP listening loop. It is intended to be used as a // listenUDP runs the UDP listening loop. It is intended to be used as a
// goroutine. errCh receives nil if the listening has started successfully or // goroutine. errCh receives nil if the listening has started successfully or
// the listening error if not. // the listening error if not.
func (l *interfaceListener) listenUDP(errCh chan<- error) { func (l *interfaceListener) listenUDP(ctx context.Context, errCh chan<- error) {
defer log.OnPanic("interfaceListener.listenUDP") logger := l.logger.With("network", "udp")
defer slogutil.RecoverAndLog(ctx, logger)
ctx := context.Background()
addrStr := netutil.JoinHostPort("0.0.0.0", l.port) addrStr := netutil.JoinHostPort("0.0.0.0", l.port)
packetConn, err := l.listenConf.ListenPacket(ctx, "udp", addrStr) packetConn, err := l.listenConf.ListenPacket(ctx, "udp", addrStr)
if err != nil { if err != nil {
@ -116,31 +116,33 @@ func (l *interfaceListener) listenUDP(errCh chan<- error) {
errCh <- nil errCh <- nil
go l.writeUDPResponses(udpConn) go l.writeUDPResponses(ctx, logger, udpConn)
logPrefix := fmt.Sprintf("bindtodevice: listener %s:%d: udp", l.ifaceName, l.port) logger.InfoContext(ctx, "starting")
log.Info("%s: starting", logPrefix)
for { for {
select { select {
case <-l.done: case <-l.done:
log.Info("%s: done", logPrefix) logger.InfoContext(ctx, "done")
return return
default: default:
// Go on. // Go on.
} }
err = l.readUDP(udpConn, logPrefix) err = l.readUDP(ctx, logger, udpConn)
if err != nil { if err != nil {
errcoll.Collectf(ctx, l.errColl, "%s: reading session: %w", logPrefix, err) errcoll.Collect(ctx, l.errColl, logger, "reading session", err)
} }
} }
} }
// readUDP reads a UDP session from c and sends it to the appropriate channel. // readUDP reads a UDP session from c and sends it to the appropriate channel.
func (l *interfaceListener) readUDP(c *net.UDPConn, logPrefix string) (err error) { func (l *interfaceListener) readUDP(
ctx context.Context,
logger *slog.Logger,
c *net.UDPConn,
) (err error) {
bodyPtr := l.bodyPool.Get() bodyPtr := l.bodyPool.Get()
body := *bodyPtr body := *bodyPtr
@ -171,18 +173,13 @@ func (l *interfaceListener) readUDP(c *net.UDPConn, logPrefix string) (err error
if chanPacketConn == nil { if chanPacketConn == nil {
metrics.BindToDeviceUnknownUDPRequestsTotal.Inc() metrics.BindToDeviceUnknownUDPRequestsTotal.Inc()
optlog.Debug3( optslog.Debug2(ctx, logger, "no packet channel", "raddr", sess.raddr, "laddr", laddr)
"%s: from raddr %s: no packet channel for laddr %s",
logPrefix,
sess.raddr,
laddr,
)
return nil return nil
} }
if !chanPacketConn.send(sess) { if !chanPacketConn.send(sess) {
optlog.Debug2("%s: channel for laddr %s is closed", logPrefix, laddr) optslog.Debug1(ctx, logger, "channel is closed", "laddr", laddr)
} }
return nil return nil
@ -190,13 +187,17 @@ func (l *interfaceListener) readUDP(c *net.UDPConn, logPrefix string) (err error
// writeUDPResponses runs the UDP write loop. It is intended to be used as a // writeUDPResponses runs the UDP write loop. It is intended to be used as a
// goroutine. // goroutine.
func (l *interfaceListener) writeUDPResponses(c *net.UDPConn) { func (l *interfaceListener) writeUDPResponses(
defer log.OnPanic("interfaceListener.writeUDP") ctx context.Context,
logger *slog.Logger,
c *net.UDPConn,
) {
defer slogutil.RecoverAndLog(ctx, logger)
for { for {
select { select {
case <-l.done: case <-l.done:
optlog.Debug2("bindtodevice: listener %s:%d: udp write: done", l.ifaceName, l.port) logger.DebugContext(ctx, "udp write done")
return return
case req := <-l.writeRequests: case req := <-l.writeRequests:

View File

@ -1,10 +1,17 @@
package bindtodevice package bindtodevice
import "github.com/AdguardTeam/AdGuardDNS/internal/errcoll" import (
"log/slog"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
)
// ManagerConfig is the configuration structure for [NewManager]. All fields // ManagerConfig is the configuration structure for [NewManager]. All fields
// must be set. // must be set.
type ManagerConfig struct { type ManagerConfig struct {
// Logger is used to log the operation of the manager.
Logger *slog.Logger
// InterfaceStorage is used to get the information about the system's // InterfaceStorage is used to get the information about the system's
// network interfaces. Normally, this is [DefaultInterfaceStorage]. // network interfaces. Normally, this is [DefaultInterfaceStorage].
InterfaceStorage InterfaceStorage InterfaceStorage InterfaceStorage

View File

@ -5,8 +5,11 @@ package bindtodevice
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog"
"maps"
"net" "net"
"net/netip" "net/netip"
"slices"
"sync" "sync"
"github.com/AdguardTeam/AdGuardDNS/internal/agdnet" "github.com/AdguardTeam/AdGuardDNS/internal/agdnet"
@ -14,8 +17,6 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll" "github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics" "github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/mapsutil"
"github.com/AdguardTeam/golibs/service" "github.com/AdguardTeam/golibs/service"
"github.com/AdguardTeam/golibs/syncutil" "github.com/AdguardTeam/golibs/syncutil"
"github.com/miekg/dns" "github.com/miekg/dns"
@ -23,6 +24,7 @@ import (
// Manager creates individual listeners and dispatches connections to them. // Manager creates individual listeners and dispatches connections to them.
type Manager struct { type Manager struct {
logger *slog.Logger
interfaces InterfaceStorage interfaces InterfaceStorage
closeOnce *sync.Once closeOnce *sync.Once
ifaceListeners map[ID]*interfaceListener ifaceListeners map[ID]*interfaceListener
@ -34,6 +36,7 @@ type Manager struct {
// NewManager returns a new manager of interface listeners. // NewManager returns a new manager of interface listeners.
func NewManager(c *ManagerConfig) (m *Manager) { func NewManager(c *ManagerConfig) (m *Manager) {
return &Manager{ return &Manager{
logger: c.Logger,
interfaces: c.InterfaceStorage, interfaces: c.InterfaceStorage,
closeOnce: &sync.Once{}, closeOnce: &sync.Once{},
ifaceListeners: map[ID]*interfaceListener{}, ifaceListeners: map[ID]*interfaceListener{},
@ -62,7 +65,8 @@ func (m *Manager) Add(id ID, ifaceName string, port uint16, ctrlConf *ControlCon
return fmt.Errorf("looking up interface %q: %w", ifaceName, err) return fmt.Errorf("looking up interface %q: %w", ifaceName, err)
} }
validateDup := func(lsnrID ID, lsnr *interfaceListener) (lsnrErr error) { for _, lsnrID := range slices.Sorted(maps.Keys(m.ifaceListeners)) {
lsnr := m.ifaceListeners[lsnrID]
lsnrIfaceName, lsnrPort := lsnr.ifaceName, lsnr.port lsnrIfaceName, lsnrPort := lsnr.ifaceName, lsnr.port
if lsnrID == id { if lsnrID == id {
return fmt.Errorf( return fmt.Errorf(
@ -81,14 +85,6 @@ func (m *Manager) Add(id ID, ifaceName string, port uint16, ctrlConf *ControlCon
lsnrID, lsnrID,
) )
} }
return nil
}
err = mapsutil.SortedRangeError(m.ifaceListeners, validateDup)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
} }
if ctrlConf == nil { if ctrlConf == nil {
@ -110,6 +106,7 @@ func (m *Manager) newInterfaceListener(
port uint16, port uint16,
) (l *interfaceListener) { ) (l *interfaceListener) {
return &interfaceListener{ return &interfaceListener{
logger: m.logger.With("iface", ifaceName, "port", port),
conns: &connIndex{}, conns: &connIndex{},
listenConf: newListenConfig(ifaceName, ctrlConf), listenConf: newListenConfig(ifaceName, ctrlConf),
bodyPool: syncutil.NewSlicePool[byte](bodySize), bodyPool: syncutil.NewSlicePool[byte](bodySize),
@ -228,7 +225,7 @@ var _ service.Interface = (*Manager)(nil)
// TODO(a.garipov): Consider an interface solution instead of the nil exception. // TODO(a.garipov): Consider an interface solution instead of the nil exception.
// //
// TODO(a.garipov): Use the context for cancelation. // TODO(a.garipov): Use the context for cancelation.
func (m *Manager) Start(_ context.Context) (err error) { func (m *Manager) Start(ctx context.Context) (err error) {
if m == nil { if m == nil {
return nil return nil
} }
@ -236,11 +233,11 @@ func (m *Manager) Start(_ context.Context) (err error) {
numListen := 2 * len(m.ifaceListeners) numListen := 2 * len(m.ifaceListeners)
errCh := make(chan error, numListen) errCh := make(chan error, numListen)
log.Info("bindtodevice: starting %d listeners", numListen) m.logger.InfoContext(ctx, "starting listeners", "num", numListen)
for _, lsnr := range m.ifaceListeners { for _, lsnr := range m.ifaceListeners {
go lsnr.listenTCP(errCh) go lsnr.listenTCP(ctx, errCh)
go lsnr.listenUDP(errCh) go lsnr.listenUDP(ctx, errCh)
} }
errs := make([]error, numListen) errs := make([]error, numListen)
@ -253,7 +250,7 @@ func (m *Manager) Start(_ context.Context) (err error) {
return fmt.Errorf("starting bindtodevice manager: %w", err) return fmt.Errorf("starting bindtodevice manager: %w", err)
} }
log.Info("bindtodevice: started all %d listeners", numListen) m.logger.InfoContext(ctx, "started all listeners", "num", numListen)
return nil return nil
} }

View File

@ -3,13 +3,13 @@
package bindtodevice_test package bindtodevice_test
import ( import (
"context"
"net" "net"
"net/netip" "net/netip"
"testing" "testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest" "github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/bindtodevice" "github.com/AdguardTeam/AdGuardDNS/internal/bindtodevice"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -48,17 +48,14 @@ func (iface *fakeInterface) Subnets() (subnets []netip.Prefix, err error) {
} }
func TestManager_Add(t *testing.T) { func TestManager_Add(t *testing.T) {
errColl := &agdtest.ErrorCollector{
OnCollect: func(_ context.Context, _ error) { panic("not implemented") },
}
m := bindtodevice.NewManager(&bindtodevice.ManagerConfig{ m := bindtodevice.NewManager(&bindtodevice.ManagerConfig{
Logger: slogutil.NewDiscardLogger(),
InterfaceStorage: &fakeInterfaceStorage{ InterfaceStorage: &fakeInterfaceStorage{
OnInterfaceByName: func(_ string) (iface bindtodevice.NetInterface, err error) { OnInterfaceByName: func(_ string) (iface bindtodevice.NetInterface, err error) {
return nil, nil return nil, nil
}, },
}, },
ErrColl: errColl, ErrColl: agdtest.NewErrorCollector(),
ChannelBufferSize: 1, ChannelBufferSize: 1,
}) })
require.NotNil(t, m) require.NotNil(t, m)
@ -87,10 +84,6 @@ func TestManager_Add(t *testing.T) {
} }
func TestManager_ListenConfig(t *testing.T) { func TestManager_ListenConfig(t *testing.T) {
errColl := &agdtest.ErrorCollector{
OnCollect: func(_ context.Context, _ error) { panic("not implemented") },
}
subnet := testSubnetIPv4 subnet := testSubnetIPv4
ifaceWithSubnet := &fakeInterface{ ifaceWithSubnet := &fakeInterface{
OnSubnets: func() (subnets []netip.Prefix, err error) { OnSubnets: func() (subnets []netip.Prefix, err error) {
@ -99,12 +92,13 @@ func TestManager_ListenConfig(t *testing.T) {
} }
m := bindtodevice.NewManager(&bindtodevice.ManagerConfig{ m := bindtodevice.NewManager(&bindtodevice.ManagerConfig{
Logger: slogutil.NewDiscardLogger(),
InterfaceStorage: &fakeInterfaceStorage{ InterfaceStorage: &fakeInterfaceStorage{
OnInterfaceByName: func(_ string) (iface bindtodevice.NetInterface, err error) { OnInterfaceByName: func(_ string) (iface bindtodevice.NetInterface, err error) {
return ifaceWithSubnet, nil return ifaceWithSubnet, nil
}, },
}, },
ErrColl: errColl, ErrColl: agdtest.NewErrorCollector(),
ChannelBufferSize: 1, ChannelBufferSize: 1,
}) })
require.NotNil(t, m) require.NotNil(t, m)
@ -147,12 +141,13 @@ func TestManager_ListenConfig(t *testing.T) {
} }
noSubnetMgr := bindtodevice.NewManager(&bindtodevice.ManagerConfig{ noSubnetMgr := bindtodevice.NewManager(&bindtodevice.ManagerConfig{
Logger: slogutil.NewDiscardLogger(),
InterfaceStorage: &fakeInterfaceStorage{ InterfaceStorage: &fakeInterfaceStorage{
OnInterfaceByName: func(_ string) (iface bindtodevice.NetInterface, err error) { OnInterfaceByName: func(_ string) (iface bindtodevice.NetInterface, err error) {
return ifaceWithoutSubnet, nil return ifaceWithoutSubnet, nil
}, },
}, },
ErrColl: errColl, ErrColl: agdtest.NewErrorCollector(),
ChannelBufferSize: 1, ChannelBufferSize: 1,
}) })
require.NotNil(t, noSubnetMgr) require.NotNil(t, noSubnetMgr)
@ -175,12 +170,13 @@ func TestManager_ListenConfig(t *testing.T) {
} }
narrowSubnetMgr := bindtodevice.NewManager(&bindtodevice.ManagerConfig{ narrowSubnetMgr := bindtodevice.NewManager(&bindtodevice.ManagerConfig{
Logger: slogutil.NewDiscardLogger(),
InterfaceStorage: &fakeInterfaceStorage{ InterfaceStorage: &fakeInterfaceStorage{
OnInterfaceByName: func(_ string) (iface bindtodevice.NetInterface, err error) { OnInterfaceByName: func(_ string) (iface bindtodevice.NetInterface, err error) {
return ifaceWithNarrowerSubnet, nil return ifaceWithNarrowerSubnet, nil
}, },
}, },
ErrColl: errColl, ErrColl: agdtest.NewErrorCollector(),
ChannelBufferSize: 1, ChannelBufferSize: 1,
}) })
require.NotNil(t, narrowSubnetMgr) require.NotNil(t, narrowSubnetMgr)
@ -206,13 +202,10 @@ func TestManager(t *testing.T) {
ifaceName := iface.Name ifaceName := iface.Name
errColl := &agdtest.ErrorCollector{
OnCollect: func(_ context.Context, _ error) { panic("not implemented") },
}
m := bindtodevice.NewManager(&bindtodevice.ManagerConfig{ m := bindtodevice.NewManager(&bindtodevice.ManagerConfig{
Logger: slogutil.NewDiscardLogger(),
InterfaceStorage: bindtodevice.DefaultInterfaceStorage{}, InterfaceStorage: bindtodevice.DefaultInterfaceStorage{},
ErrColl: errColl, ErrColl: agdtest.NewErrorCollector(),
ChannelBufferSize: 1, ChannelBufferSize: 1,
}) })
require.NotNil(t, m) require.NotNil(t, m)

View File

@ -1,6 +1,7 @@
package cmd package cmd
import ( import (
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
) )
@ -13,10 +14,13 @@ type accessConfig struct {
BlockedClientSubnets []netutil.Prefix `yaml:"blocked_client_subnets"` BlockedClientSubnets []netutil.Prefix `yaml:"blocked_client_subnets"`
} }
// validate returns an error if the access configuration is invalid. // type check
func (a *accessConfig) validate() (err error) { var _ validator = (*accessConfig)(nil)
if a == nil {
return errNilConfig // validate implements the [validator] interface for *accessConfig.
func (c *accessConfig) validate() (err error) {
if c == nil {
return errors.ErrNoValue
} }
return nil return nil

View File

@ -2,23 +2,25 @@ package cmd
import ( import (
"fmt" "fmt"
"maps"
"slices"
"github.com/AdguardTeam/golibs/mapsutil"
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
) )
// Additional prometheus information configuration
// additionalInfo is a extra info configuration. // additionalInfo is a extra info configuration.
type additionalInfo map[string]string type additionalInfo map[string]string
// validateAdditionalInfo return an error is the section is invalid. // type check
var _ validator = additionalInfo(nil)
// validate implements the [validator] interface for additionalInfo.
func (c additionalInfo) validate() (err error) { func (c additionalInfo) validate() (err error) {
return mapsutil.SortedRangeError(c, func(k, _ string) (keyErr error) { for _, k := range slices.Sorted(maps.Keys(c)) {
if model.LabelName(k).IsValid() { if !model.LabelName(k).IsValid() {
return nil return fmt.Errorf("prometheus labels must match %s, got %q", model.LabelNameRE, k)
}
} }
return fmt.Errorf("prometheus labels must match %s, got %q", model.LabelNameRE, k) return nil
})
} }

View File

@ -3,24 +3,20 @@ package cmd
import ( import (
"context" "context"
"fmt" "fmt"
"net/netip" "log/slog"
"time" "time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd" "github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
"github.com/AdguardTeam/AdGuardDNS/internal/agdservice"
"github.com/AdguardTeam/AdGuardDNS/internal/backendpb" "github.com/AdguardTeam/AdGuardDNS/internal/backendpb"
"github.com/AdguardTeam/AdGuardDNS/internal/billstat" "github.com/AdguardTeam/AdGuardDNS/internal/billstat"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll" "github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb" "github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/service"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
) )
// Business Logic Backend Configuration
// backendConfig is the backend module configuration. // backendConfig is the backend module configuration.
// //
// TODO(a.garipov): Reorganize this object as there is no longer the only one // TODO(a.garipov): Reorganize this object as there is no longer the only one
@ -47,162 +43,48 @@ type backendConfig struct {
BillStatIvl timeutil.Duration `yaml:"bill_stat_interval"` BillStatIvl timeutil.Duration `yaml:"bill_stat_interval"`
} }
// validate returns an error if the backend configuration is invalid. // type check
var _ validator = (*backendConfig)(nil)
// validate implements the [validator] interface for *backendConfig.
func (c *backendConfig) validate() (err error) { func (c *backendConfig) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return errNilConfig return errors.ErrNoValue
case c.Timeout.Duration < 0: case c.Timeout.Duration < 0:
return newMustBeNonNegativeError("timeout", c.Timeout) return newNegativeError("timeout", c.Timeout)
case c.RefreshIvl.Duration <= 0: case c.RefreshIvl.Duration <= 0:
return newMustBePositiveError("refresh_interval", c.RefreshIvl) return newNotPositiveError("refresh_interval", c.RefreshIvl)
case c.FullRefreshIvl.Duration <= 0: case c.FullRefreshIvl.Duration <= 0:
return newMustBePositiveError("full_refresh_interval", c.FullRefreshIvl) return newNotPositiveError("full_refresh_interval", c.FullRefreshIvl)
case c.FullRefreshRetryIvl.Duration <= 0: case c.FullRefreshRetryIvl.Duration <= 0:
return newMustBePositiveError("full_refresh_retry_interval", c.FullRefreshRetryIvl) return newNotPositiveError("full_refresh_retry_interval", c.FullRefreshRetryIvl)
case c.BillStatIvl.Duration <= 0: case c.BillStatIvl.Duration <= 0:
return newMustBePositiveError("bill_stat_interval", c.BillStatIvl) return newNotPositiveError("bill_stat_interval", c.BillStatIvl)
default: default:
return nil return nil
} }
} }
// setupBackend creates and returns a profile database and a billing-statistics
// recorder as well as starts and registers their refreshers in the signal
// handler.
func setupBackend(
conf *backendConfig,
grps []*agd.ServerGroup,
envs *environments,
sigHdlr *service.SignalHandler,
errColl errcoll.Interface,
) (profDB profiledb.Interface, rec billstat.Recorder, err error) {
if !envs.ProfilesEnabled {
return &profiledb.Disabled{}, billstat.EmptyRecorder{}, nil
}
rec, err = setupBillStat(conf, envs, sigHdlr, errColl)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, nil, err
}
profDB, err = setupProfDB(conf, grps, envs, sigHdlr, errColl)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, nil, err
}
return profDB, rec, nil
}
// setupBillStat creates and returns a billing-statistics recorder as well as
// starts and registers its refresher in the signal handler.
func setupBillStat(
conf *backendConfig,
envs *environments,
sigHdlr *service.SignalHandler,
errColl errcoll.Interface,
) (rec *billstat.RuntimeRecorder, err error) {
billStatUploader, err := setupBillStatUploader(envs, errColl)
if err != nil {
return nil, fmt.Errorf("creating bill stat uploader: %w", err)
}
rec = billstat.NewRuntimeRecorder(&billstat.RuntimeRecorderConfig{
ErrColl: errColl,
Uploader: billStatUploader,
})
refrIvl := conf.RefreshIvl.Duration
timeout := conf.Timeout.Duration
billStatRefr := agdservice.NewRefreshWorker(&agdservice.RefreshWorkerConfig{
Context: func() (ctx context.Context, cancel context.CancelFunc) {
return context.WithTimeout(context.Background(), timeout)
},
Refresher: rec,
Name: "billstat",
Interval: refrIvl,
RefreshOnShutdown: true,
RandomizeStart: false,
})
err = billStatRefr.Start(context.Background())
if err != nil {
return nil, fmt.Errorf("starting bill stat recorder refresher: %w", err)
}
sigHdlr.Add(billStatRefr)
return rec, nil
}
// setupProfDB creates and returns a profile database as well as starts and
// registers its refresher in the signal handler.
func setupProfDB(
conf *backendConfig,
grps []*agd.ServerGroup,
envs *environments,
sigHdlr *service.SignalHandler,
errColl errcoll.Interface,
) (profDB *profiledb.Default, err error) {
bindSet := collectBindSubnetSet(grps)
profStrg, err := setupProfStorage(envs, bindSet, errColl)
if err != nil {
return nil, fmt.Errorf("creating profile storage: %w", err)
}
timeout := conf.Timeout.Duration
profDB, err = profiledb.New(&profiledb.Config{
Storage: profStrg,
ErrColl: errColl,
FullSyncIvl: conf.FullRefreshIvl.Duration,
FullSyncRetryIvl: conf.FullRefreshRetryIvl.Duration,
CacheFilePath: envs.ProfilesCachePath,
})
if err != nil {
return nil, fmt.Errorf("creating default profile database: %w", err)
}
err = initProfDB(profDB, timeout)
if err != nil {
return nil, fmt.Errorf("preparing default profile database: %w", err)
}
profDBRefr := agdservice.NewRefreshWorker(&agdservice.RefreshWorkerConfig{
Context: func() (ctx context.Context, cancel context.CancelFunc) {
return context.WithTimeout(context.Background(), timeout)
},
Refresher: profDB,
Name: "profiledb",
Interval: conf.RefreshIvl.Duration,
RefreshOnShutdown: false,
RandomizeStart: true,
})
err = profDBRefr.Start(context.Background())
if err != nil {
return nil, fmt.Errorf("starting default profile database refresher: %w", err)
}
sigHdlr.Add(profDBRefr)
return profDB, nil
}
// initProfDB refreshes the profile database initially. It logs an error if // initProfDB refreshes the profile database initially. It logs an error if
// it's a timeout, and returns it otherwise. // it's a timeout, and returns it otherwise.
func initProfDB(profDB *profiledb.Default, timeout time.Duration) (err error) { func initProfDB(
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx context.Context,
mainLogger *slog.Logger,
profDB *profiledb.Default,
timeout time.Duration,
) (err error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel() defer cancel()
log.Info("main: initial profiledb refresh") mainLogger.InfoContext(ctx, "initial profiledb refresh")
err = profDB.Refresh(ctx) err = profDB.Refresh(ctx)
switch { switch {
case err == nil: case err == nil:
log.Info("main: initial profiledb refresh succeeded") mainLogger.InfoContext(ctx, "initial profiledb refresh succeeded")
case errors.Is(err, context.DeadlineExceeded): case errors.Is(err, context.DeadlineExceeded):
log.Info("main: warning: initial profiledb refresh timeout: %s", err) mainLogger.WarnContext(ctx, "initial profiledb refresh timeout", slogutil.KeyError, err)
default: default:
return fmt.Errorf("initial refresh: %w", err) return fmt.Errorf("initial refresh: %w", err)
} }
@ -210,77 +92,22 @@ func initProfDB(profDB *profiledb.Default, timeout time.Duration) (err error) {
return nil return nil
} }
// collectBindSubnetSet returns a subnet set with IP addresses of servers in the // newBillStatUploader creates and returns a billstat uploader depending on the
// provided server groups grps. // provided API URL.
func collectBindSubnetSet(grps []*agd.ServerGroup) (s netutil.SubnetSet) { func newBillStatUploader(
var serverPrefixes []netip.Prefix envs *environment,
allSingleIP := true
for _, grp := range grps {
for _, srv := range grp.Servers {
for _, p := range srv.BindDataPrefixes() {
allSingleIP = allSingleIP && p.IsSingleIP()
serverPrefixes = append(serverPrefixes, p)
}
}
}
// In cases where an installation only has single-IP prefixes in bind
// interfaces, or no bind interfaces at all, only check the dedicated IPs in
// profiles for validity.
//
// TODO(a.garipov): Do not load profiles on such installations at all, as
// they don't really need them. See AGDNS-1888.
if allSingleIP {
log.Info("warning: all bind ifaces are single-ip; only checking validity of dedicated ips")
return netutil.SubnetSetFunc(netip.Addr.IsValid)
}
return netutil.SliceSubnetSet(serverPrefixes)
}
// Backend API URL schemes.
const (
schemeGRPC = "grpc"
schemeGRPCS = "grpcs"
)
// setupBillStatUploader creates and returns a billstat uploader depending on
// the provided API URL.
func setupBillStatUploader(
envs *environments,
errColl errcoll.Interface, errColl errcoll.Interface,
mtrc backendpb.Metrics,
) (s billstat.Uploader, err error) { ) (s billstat.Uploader, err error) {
apiURL := netutil.CloneURL(&envs.BillStatURL.URL) apiURL := netutil.CloneURL(&envs.BillStatURL.URL)
scheme := apiURL.Scheme if !agdhttp.CheckGRPCURLScheme(apiURL.Scheme) {
if scheme == schemeGRPC || scheme == schemeGRPCS { return nil, fmt.Errorf("invalid backend api url: %s", apiURL)
}
return backendpb.NewBillStat(&backendpb.BillStatConfig{ return backendpb.NewBillStat(&backendpb.BillStatConfig{
ErrColl: errColl, ErrColl: errColl,
Metrics: mtrc,
Endpoint: apiURL, Endpoint: apiURL,
APIKey: envs.BillStatAPIKey, APIKey: envs.BillStatAPIKey,
}) })
}
return nil, fmt.Errorf("invalid backend api url: %s", apiURL)
}
// setupProfStorage creates and returns a profile storage depending on the
// provided API URL.
func setupProfStorage(
envs *environments,
bindSet netutil.SubnetSet,
errColl errcoll.Interface,
) (s profiledb.Storage, err error) {
apiURL := netutil.CloneURL(&envs.ProfilesURL.URL)
scheme := apiURL.Scheme
if scheme == schemeGRPC || scheme == schemeGRPCS {
return backendpb.NewProfileStorage(&backendpb.ProfileStorageConfig{
BindSet: bindSet,
ErrColl: errColl,
Endpoint: apiURL,
APIKey: envs.ProfilesAPIKey,
})
}
return nil, fmt.Errorf("invalid backend api url: %s", apiURL)
} }

1212
internal/cmd/builder.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,11 +3,10 @@ package cmd
import ( import (
"fmt" "fmt"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
) )
// Cache Configuration
// cacheConfig is the configuration of the DNS cacheConfig module // cacheConfig is the configuration of the DNS cacheConfig module
// //
// TODO(a.garipov): Consider adding parameter Enabled or a new Type instead of // TODO(a.garipov): Consider adding parameter Enabled or a new Type instead of
@ -44,21 +43,25 @@ const (
cacheTypeSimple = "simple" cacheTypeSimple = "simple"
) )
// validate returns an error if the cache configuration is invalid. // type check
var _ validator = (*cacheConfig)(nil)
// validate implements the [validator] interface for *cacheConfig.
func (c *cacheConfig) validate() (err error) { func (c *cacheConfig) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return errNilConfig return errors.ErrNoValue
case c.Type != cacheTypeSimple && c.Type != cacheTypeECS: case c.Type != cacheTypeSimple && c.Type != cacheTypeECS:
return fmt.Errorf( return fmt.Errorf(
"bad cache type %q, supported: %q", "type: %w: %q, supported: %q",
errors.ErrBadEnumValue,
c.Type, c.Type,
[]string{cacheTypeSimple, cacheTypeECS}, []string{cacheTypeSimple, cacheTypeECS},
) )
case c.Size < 0: case c.Size < 0:
return newMustBeNonNegativeError("size", c.Size) return newNegativeError("size", c.Size)
case c.Type == cacheTypeECS && c.ECSSize < 0: case c.Type == cacheTypeECS && c.ECSSize < 0:
return newMustBeNonNegativeError("ecs_size", c.ECSSize) return newNegativeError("ecs_size", c.ECSSize)
default: default:
// Go on. // Go on.
} }
@ -71,13 +74,16 @@ func (c *cacheConfig) validate() (err error) {
return nil return nil
} }
// validate returns an error if the TTL override configuration is invalid. // type check
var _ validator = (*ttlOverride)(nil)
// validate implements the [validator] interface for *ttlOverride.
func (c *ttlOverride) validate() (err error) { func (c *ttlOverride) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return errNilConfig return errors.ErrNoValue
case c.Min.Duration <= 0: case c.Min.Duration <= 0:
return newMustBePositiveError("min", c.Min) return newNotPositiveError("min", c.Min)
default: default:
return nil return nil
} }

View File

@ -3,21 +3,31 @@ package cmd
import ( import (
"fmt" "fmt"
"net/netip" "net/netip"
"net/url"
"strings" "strings"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
"github.com/AdguardTeam/AdGuardDNS/internal/dnscheck" "github.com/AdguardTeam/AdGuardDNS/internal/dnscheck"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg" "github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll" "github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/AdGuardDNS/internal/remotekv"
"github.com/AdguardTeam/AdGuardDNS/internal/remotekv/consulkv"
"github.com/AdguardTeam/AdGuardDNS/internal/remotekv/rediskv"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
"github.com/c2h5oh/datasize"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/time/rate"
) )
// DNS server check configuration
// checkConfig is the DNS server checking configuration. // checkConfig is the DNS server checking configuration.
type checkConfig struct { type checkConfig struct {
// RemoteKV is remote key-value store configuration for DNS server checking.
RemoteKV *remoteKVConfig `yaml:"kv"`
// Domains are the domain names used for DNS server checking. // Domains are the domain names used for DNS server checking.
Domains []string `yaml:"domains"` Domains []string `yaml:"domains"`
@ -34,22 +44,21 @@ type checkConfig struct {
// IPv6 is the list of IPv6 addresses to respond with for AAAA queries to // IPv6 is the list of IPv6 addresses to respond with for AAAA queries to
// subdomains of Domain. // subdomains of Domain.
IPv6 []netip.Addr `yaml:"ipv6"` IPv6 []netip.Addr `yaml:"ipv6"`
// TTL defines, for how long to keep the information about a single client.
TTL timeutil.Duration `yaml:"ttl"`
} }
// toInternal converts c to the DNS server check configuration for the DNS // toInternal converts c to the DNS server check configuration for the DNS
// server. c is assumed to be valid. // server. c must be valid.
func (c *checkConfig) toInternal( func (c *checkConfig) toInternal(
envs *environments, envs *environment,
messages *dnsmsg.Constructor, messages *dnsmsg.Constructor,
errColl errcoll.Interface, errColl errcoll.Interface,
) (conf *dnscheck.ConsulConfig) { namespace string,
var kvURL, sessURL *url.URL reg prometheus.Registerer,
if envs.ConsulDNSCheckKVURL != nil && envs.ConsulDNSCheckSessionURL != nil { ) (conf *dnscheck.RemoteKVConfig, err error) {
kvURL = netutil.CloneURL(&envs.ConsulDNSCheckKVURL.URL) kv, err := newDNSCheckKV(c, envs, namespace, reg)
sessURL = netutil.CloneURL(&envs.ConsulDNSCheckSessionURL.URL) if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
} }
domains := make([]string, len(c.Domains)) domains := make([]string, len(c.Domains))
@ -57,46 +66,107 @@ func (c *checkConfig) toInternal(
domains[i] = strings.ToLower(d) domains[i] = strings.ToLower(d)
} }
return &dnscheck.ConsulConfig{ return &dnscheck.RemoteKVConfig{
Messages: messages, Messages: messages,
ConsulKVURL: kvURL, RemoteKV: kv,
ConsulSessionURL: sessURL,
ErrColl: errColl, ErrColl: errColl,
Domains: domains, Domains: domains,
NodeLocation: c.NodeLocation, NodeLocation: c.NodeLocation,
NodeName: c.NodeName, NodeName: c.NodeName,
IPv4: c.IPv4, IPv4: c.IPv4,
IPv6: c.IPv6, IPv6: c.IPv6,
TTL: c.TTL.Duration, }, nil
}
} }
// validate returns an error if the DNS server checking configuration is // maxRespSize is the maximum size of response from Consul key-value storage.
// invalid. const maxRespSize = 1 * datasize.MB
func (c *checkConfig) validate() (err error) {
if c == nil { // keyNamespaceCheck is the namespace added to the keys of DNS check. See
return errNilConfig // [remotekv.KeyNamespace].
const keyNamespaceCheck = "check"
// newDNSCheckKV returns a new properly initialized remote key-value storage.
func newDNSCheckKV(
conf *checkConfig,
envs *environment,
namespace string,
reg prometheus.Registerer,
) (kv remotekv.Interface, err error) {
if conf.RemoteKV.Type == kvModeRedis {
var redisKVMtrc rediskv.Metrics
redisKVMtrc, err = metrics.NewRedisKV(namespace, reg)
if err != nil {
return nil, fmt.Errorf("registering redis kv metrics: %w", err)
} }
notEmptyParams := []struct { kv := rediskv.NewRedisKV(&rediskv.RedisKVConfig{
name string Metrics: redisKVMtrc,
value string Addr: &netutil.HostPort{
}{{ Host: envs.RedisAddr,
name: "node_location", Port: envs.RedisPort,
value: c.NodeLocation, },
MaxActive: envs.RedisMaxActive,
MaxIdle: envs.RedisMaxIdle,
IdleTimeout: envs.RedisIdleTimeout.Duration,
TTL: conf.RemoteKV.TTL.Duration,
})
return remotekv.NewKeyNamespace(&remotekv.KeyNamespaceConfig{
KV: kv,
Prefix: fmt.Sprintf("%s:%s:", envs.RedisKeyPrefix, keyNamespaceCheck),
}), nil
}
consulKVURL := envs.ConsulDNSCheckKVURL
consulSessionURL := envs.ConsulDNSCheckSessionURL
if consulKVURL != nil && consulSessionURL != nil {
kv, err = consulkv.NewKV(&consulkv.Config{
URL: &consulKVURL.URL,
SessionURL: &consulSessionURL.URL,
Client: agdhttp.NewClient(&agdhttp.ClientConfig{
// TODO(ameshkov): Consider making configurable.
Timeout: 15 * time.Second,
}),
// TODO(ameshkov): Consider making configurable.
Limiter: rate.NewLimiter(rate.Limit(200)/60, 1),
TTL: conf.RemoteKV.TTL.Duration,
MaxRespSize: maxRespSize,
})
if err != nil {
return nil, fmt.Errorf("initializing consul dnscheck: %w", err)
}
} else {
kv = remotekv.Empty{}
}
return kv, nil
}
// type check
var _ validator = (*checkConfig)(nil)
// validate implements the [validator] interface for *checkConfig.
func (c *checkConfig) validate() (err error) {
if c == nil {
return errors.ErrNoValue
}
notEmptyParams := container.KeyValues[string, string]{{
Key: "node_location",
Value: c.NodeLocation,
}, { }, {
name: "node_name", Key: "node_name",
value: c.NodeName, Value: c.NodeName,
}} }}
for _, param := range notEmptyParams { for _, kv := range notEmptyParams {
if param.value == "" { if kv.Value == "" {
return fmt.Errorf("no %s", param.name) return fmt.Errorf("%s: %w", kv.Key, errors.ErrEmptyValue)
} }
} }
if len(c.Domains) == 0 { if len(c.Domains) == 0 {
return errors.Error("no domains") return fmt.Errorf("domains: %w", errors.ErrEmptyValue)
} }
err = validateNonNilIPs(c.IPv4, netutil.AddrFamilyIPv4) err = validateNonNilIPs(c.IPv4, netutil.AddrFamilyIPv4)
@ -111,8 +181,9 @@ func (c *checkConfig) validate() (err error) {
return err return err
} }
if c.TTL.Duration <= 0 { err = c.RemoteKV.validate()
return newMustBePositiveError("ttl", c.TTL) if err != nil {
return fmt.Errorf("kv: %w", err)
} }
return nil return nil
@ -147,3 +218,60 @@ func validateNonNilIPs(ips []netip.Addr, fam netutil.AddrFamily) (err error) {
return nil return nil
} }
// DNSCheck key-value database modes.
const (
kvModeConsul = "consul"
kvModeRedis = "redis"
)
// remoteKVConfig is remote key-value store configuration for DNS server
// checking.
type remoteKVConfig struct {
// Type defines the type of remote key-value store. Allowed values are
// [kvModeConsul] and [kvModeRedis].
Type string `yaml:"type"`
// TTL defines, for how long to keep the information about a single client.
TTL timeutil.Duration `yaml:"ttl"`
}
// type check
var _ validator = (*remoteKVConfig)(nil)
// validate implements the [validator] interface for *remoteKVConfig.
func (c *remoteKVConfig) validate() (err error) {
if c == nil {
return errors.ErrNoValue
}
ttl := c.TTL
switch c.Type {
case kvModeConsul:
if ttl.Duration < consulkv.MinTTL || ttl.Duration > consulkv.MaxTTL {
return fmt.Errorf(
"ttl: %w: must be between %s and %s; got %s",
errors.ErrOutOfRange,
consulkv.MinTTL,
consulkv.MaxTTL,
ttl,
)
}
case kvModeRedis:
if ttl.Duration < rediskv.MinTTL {
return fmt.Errorf(
"ttl: %w: must be greater than or equal to %s got %s",
errors.ErrOutOfRange,
rediskv.MinTTL,
ttl,
)
}
case "":
return fmt.Errorf("type: %w", errors.ErrEmptyValue)
default:
return fmt.Errorf("type: %q: %w", c.Type, errors.ErrBadEnumValue)
}
return nil
}

View File

@ -4,379 +4,128 @@ package cmd
import ( import (
"context" "context"
"fmt"
"os" "os"
"runtime" "runtime"
"github.com/AdguardTeam/AdGuardDNS/internal/access"
"github.com/AdguardTeam/AdGuardDNS/internal/agd" "github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache" "github.com/AdguardTeam/AdGuardDNS/internal/cmd/plugin"
"github.com/AdguardTeam/AdGuardDNS/internal/agdservice"
"github.com/AdguardTeam/AdGuardDNS/internal/debugsvc"
"github.com/AdguardTeam/AdGuardDNS/internal/dnscheck"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/forward"
"github.com/AdguardTeam/AdGuardDNS/internal/dnssvc"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/hashprefix"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics" "github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/AdGuardDNS/internal/websvc" "github.com/AdguardTeam/AdGuardDNS/internal/version"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil" "github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/service"
) )
// Main is the entry point of application. // Main is the entry point of application.
// func Main(plugins *plugin.Registry) {
// TODO(a.garipov): Split into smaller pieces.
func Main() {
// Initial Configuration
agd.InitRequestID()
// TODO(a.garipov, e.burkov): Consider adding timeouts for initialization. // TODO(a.garipov, e.burkov): Consider adding timeouts for initialization.
agd.InitRequestID()
ctx := context.Background() ctx := context.Background()
// Log only to stdout and let users decide how to process it. // Log only to stdout and let users decide how to process it.
log.SetOutput(os.Stdout) log.SetOutput(os.Stdout)
envs, err := readEnvs() envs := errors.Must(parseEnvironment())
check(err)
errors.Check(envs.validate())
// TODO(a.garipov): Use slog everywhere. // TODO(a.garipov): Use slog everywhere.
slogLogger := envs.configureLogs() logger := envs.configureLogs()
// TODO(a.garipov): Consider ways of replacing a prefix and stop passing
// the main logger everywhere.
mainLogger := logger.With(slogutil.KeyPrefix, "main")
// Signal service startup now that we have the logs set up. // Signal service startup now that we have the logs set up.
log.Info("main: starting adguard dns") branch := version.Branch()
commitTime := version.CommitTime()
buildVersion := version.Version()
revision := version.Revision()
mainLogger.InfoContext(
ctx,
"agdns starting",
"version", buildVersion,
"revision", revision,
"branch", branch,
"commit_time", commitTime,
"race", version.RaceEnabled,
)
// Error collector // Error collector
// //
// TODO(a.garipov): Consider parsing SENTRY_DSN separately to set sentry up // TODO(a.garipov): Consider parsing SENTRY_DSN separately to set sentry up
// first and collect panics from the readEnvs call above as well. // first and collect panics from the readEnvs call above as well.
errColl, err := envs.buildErrColl() errColl := errors.Must(envs.buildErrColl())
check(err)
defer collectPanics(errColl) defer reportPanics(ctx, errColl, mainLogger)
// Cache manager c := errors.Must(parseConfig(envs.ConfPath))
cacheManager := agdcache.NewDefaultManager() errors.Check(c.validate())
// Configuration file errors.Check(envs.validateFromValidConfig(c))
c, err := readConfig(envs.ConfPath)
check(err)
err = c.validate()
check(err)
// Additional metrics
metrics.SetAdditionalInfo(c.AdditionalMetricsInfo) metrics.SetAdditionalInfo(c.AdditionalMetricsInfo)
// Signal handler // Building and running the server
sigHdlr := service.NewSignalHandler(&service.SignalHandlerConfig{ b := newBuilder(&builderConfig{
Logger: slogLogger.With(slogutil.KeyPrefix, service.SignalHandlerPrefix), envs: envs,
conf: c,
baseLogger: logger,
plugins: plugins,
errColl: errColl,
}) })
// GeoIP database b.startGeoIP(ctx)
// We start GeoIP initialization early in a dedicated routine cause it takes errors.Check(os.MkdirAll(envs.FilterCachePath, agd.DefaultDirPerm))
// time, later we wait for completion and continue with GeoIP.
//
// See AGDNS-884.
geoIP, geoIPRefr := &geoip.File{}, &agdservice.RefreshWorker{} errors.Check(b.initHashPrefixFilters(ctx))
geoIPErrCh := make(chan error, 1)
go setupGeoIP(geoIP, geoIPRefr, geoIPErrCh, c.GeoIP, envs, errColl, cacheManager) errors.Check(b.initFilterStorage(ctx))
// Safe-browsing and adult-blocking filters errors.Check(b.initFilteringGroups(ctx))
err = os.MkdirAll(envs.FilterCachePath, agd.DefaultDirPerm) errors.Check(b.initAccess(ctx))
check(err)
// TODO(ameshkov): Consider making a separated max_size config for errors.Check(b.initBindToDevice(ctx))
// safe-browsing and adult-blocking filters.
maxFilterSize := c.Filters.MaxSize.Bytes()
cloner := dnsmsg.NewCloner(metrics.ClonerStat{}) errors.Check(b.initMsgConstructor(ctx))
sbConf := c.SafeBrowsing.toInternal( errors.Check(b.initServerGroups(ctx))
errColl,
cloner,
cacheManager,
agd.FilterListIDSafeBrowsing,
envs.SafeBrowsingURL,
envs.FilterCachePath,
maxFilterSize,
)
sbHashes, sbFilter, err := setupHashPrefixFilter(ctx, sbConf, sigHdlr)
check(err)
abConf := c.AdultBlocking.toInternal( errors.Check(b.startBindToDevice(ctx))
errColl,
cloner,
cacheManager,
agd.FilterListIDAdultBlocking,
envs.AdultBlockingURL,
envs.FilterCachePath,
maxFilterSize,
)
abHashes, abFilter, err := setupHashPrefixFilter(ctx, abConf, sigHdlr)
check(err)
// Reuse general safe browsing filter configuration. errors.Check(b.initTLS(ctx))
nrdConf := c.SafeBrowsing.toInternal(
errColl,
cloner,
cacheManager,
agd.FilterListIDNewRegDomains,
envs.NewRegDomainsURL,
envs.FilterCachePath,
maxFilterSize,
)
_, nrdFilter, err := setupHashPrefixFilter(ctx, nrdConf, sigHdlr)
check(err)
// Filter storage and filtering groups errors.Check(b.initBillStat(ctx))
fltStrgConf := c.Filters.toInternal( errors.Check(b.initProfileDB(ctx))
errColl,
cacheManager,
envs,
sbFilter,
abFilter,
nrdFilter,
)
fltRefrTimeout := c.Filters.RefreshTimeout.Duration errors.Check(b.initDNSCheck(ctx))
fltStrg, err := setupFilterStorage(ctx, fltStrgConf, sigHdlr, fltRefrTimeout)
check(err)
fltGroups, err := c.FilteringGroups.toInternal(fltStrg) errors.Check(b.initRuleStat(ctx))
check(err)
// Access errors.Check(b.initRateLimiter(ctx))
accessGlobal, err := access.NewGlobal( errors.Check(b.initWeb(ctx))
c.Access.BlockedQuestionDomains,
netutil.UnembedPrefixes(c.Access.BlockedClientSubnets),
)
check(err)
// Network interface listener and server groups errors.Check(b.waitGeoIP(ctx))
messages := dnsmsg.NewConstructor( errors.Check(b.initDNS(ctx))
cloner,
&dnsmsg.BlockingModeNullIP{},
c.Filters.ResponseTTL.Duration,
)
btdCtrlConf, ctrlConf := c.Network.toInternal() errors.Check(b.performConnCheck(ctx))
btdMgr, err := c.InterfaceListeners.toInternal(errColl, btdCtrlConf) errors.Check(b.initHealthCheck(ctx))
check(err)
srvGrps, err := c.ServerGroups.toInternal(messages, btdMgr, fltGroups, c.RateLimit, c.DNS) b.mustStartDNS(ctx)
check(err)
// Start the bind-to-device manager here, now that no further calls to b.mustInitDebugSvc(ctx)
// btdMgr.ListenConfig are required.
err = btdMgr.Start(ctx)
check(err)
sigHdlr.Add(btdMgr)
// TLS keys logging
if envs.SSLKeyLogFile != "" {
log.Info("IMPORTANT: TLS KEY LOGGING IS ENABLED; KEYS ARE DUMPED TO %q", envs.SSLKeyLogFile)
err = enableTLSKeyLogging(srvGrps, envs.SSLKeyLogFile)
check(err)
}
// TLS session-tickets rotation
err = setupTicketRotator(srvGrps, sigHdlr, errColl)
check(err)
// Profiles database and billing statistics
profDB, billStatRec, err := setupBackend(c.Backend, srvGrps, envs, sigHdlr, errColl)
check(err)
// DNS checker
dnsCk, err := dnscheck.NewConsul(c.Check.toInternal(envs, messages, errColl))
check(err)
// DNSDB
dnsDB := c.DNSDB.toInternal(errColl)
// Filtering-rule statistics
ruleStat, err := envs.buildRuleStat(sigHdlr, errColl)
check(err)
// Rate limiting
consulAllowlistURL := &envs.ConsulAllowlistURL.URL
rateLimiter, connLimiter, err := setupRateLimiter(
ctx,
c.RateLimit,
consulAllowlistURL,
sigHdlr,
errColl,
)
check(err)
// GeoIP database
// Wait for long-running GeoIP initialization.
check(<-geoIPErrCh)
sigHdlr.Add(geoIPRefr)
// Web service
webConf, err := c.Web.toInternal(envs, dnsCk, errColl)
check(err)
webSvc := websvc.New(webConf)
// The web service is considered critical, so its Start method panics
// instead of returning an error.
_ = webSvc.Start(ctx)
sigHdlr.Add(webSvc)
// DNS service
fwdConf := c.Upstream.toInternal()
handler := forward.NewHandler(fwdConf)
// TODO(a.garipov): Consider making these configurable via the configuration
// file.
hashStorages := map[string]*hashprefix.Storage{
filter.GeneralTXTSuffix: sbHashes,
filter.AdultBlockingTXTSuffix: abHashes,
}
dnsConf := &dnssvc.Config{
Messages: messages,
Cloner: cloner,
CacheManager: cacheManager,
ControlConf: ctrlConf,
ConnLimiter: connLimiter,
HumanIDParser: agd.NewHumanIDParser(),
AccessManager: accessGlobal,
SafeBrowsing: hashprefix.NewMatcher(hashStorages),
BillStat: billStatRec,
ProfileDB: profDB,
DNSCheck: dnsCk,
NonDNS: webSvc,
DNSDB: dnsDB,
ErrColl: errColl,
FilterStorage: fltStrg,
GeoIP: geoIP,
Handler: handler,
QueryLog: c.buildQueryLog(envs),
RuleStat: ruleStat,
RateLimit: rateLimiter,
FilteringGroups: fltGroups,
ServerGroups: srvGrps,
HandleTimeout: c.DNS.HandleTimeout.Duration,
CacheSize: c.Cache.Size,
ECSCacheSize: c.Cache.ECSSize,
CacheMinTTL: c.Cache.TTLOverride.Min.Duration,
UseCacheTTLOverride: c.Cache.TTLOverride.Enabled,
UseECSCache: c.Cache.Type == cacheTypeECS,
ProfileDBEnabled: bool(envs.ProfilesEnabled),
}
dnsSvc, err := dnssvc.New(dnsConf)
check(err)
// Connectivity check
err = connectivityCheck(dnsConf, c.ConnectivityCheck)
check(err)
upstreamHealthcheckUpd := newUpstreamHealthcheck(handler, c.Upstream, errColl)
err = upstreamHealthcheckUpd.Start(ctx)
check(err)
sigHdlr.Add(upstreamHealthcheckUpd)
// The DNS service is considered critical, so its Start method panics
// instead of returning an error.
_ = dnsSvc.Start(ctx)
sigHdlr.Add(dnsSvc)
// Debug HTTP-service
debugSvcConf := envs.debugConf(dnsDB)
debugSvcConf.Logger = slogLogger.With(slogutil.KeyPrefix, "debugsvc")
debugSvcConf.Refreshers = debugsvc.Refreshers{
"filter_storage": fltStrg,
string(agd.FilterListIDSafeBrowsing): sbFilter,
string(agd.FilterListIDAdultBlocking): abFilter,
string(agd.FilterListIDNewRegDomains): nrdFilter,
}
debugSvc := debugsvc.New(debugSvcConf)
// The debug HTTP service is considered critical, so its Start method panics
// instead of returning an error.
_ = debugSvc.Start(ctx)
sigHdlr.Add(debugSvc)
// Signal that the server is started. // Signal that the server is started.
metrics.SetUpGauge( metrics.SetUpGauge(buildVersion, commitTime, branch, revision, runtime.Version())
agd.Version(),
agd.BuildTime(),
agd.Branch(),
agd.Revision(),
runtime.Version(),
)
// TODO(s.chzhen): Remove it. os.Exit(b.handleSignals(ctx))
log.Debug("cache manager ids: %q", cacheManager.IDs())
os.Exit(sigHdlr.Handle(ctx))
}
// collectPanics reports all panics in Main. It should be called in a defer.
//
// TODO(a.garipov): Consider making into a helper in package agd and using
// everywhere.
func collectPanics(errColl errcoll.Interface) {
v := recover()
if v == nil {
return
}
err, ok := v.(error)
if ok {
err = fmt.Errorf("panic in cmd.Main: %w", err)
} else {
err = fmt.Errorf("panic in cmd.Main: %v", v)
}
errColl.Collect(context.Background(), err)
errFlushColl, ok := errColl.(errcoll.ErrorFlushCollector)
if ok {
errFlushColl.Flush()
}
panic(v)
} }

View File

@ -1,25 +1,19 @@
package cmd package cmd
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/querylog" "github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
// On-Disk Configuration File Entities // configuration represents the on-disk configuration of AdGuard DNS. The order
// // of the fields should generally not be altered.
// These entities should only be used to parse and validate the on-disk
// configuration. The order of the fields should generally not be altered.
// //
// TODO(a.garipov): Consider collecting all validation errors instead of // TODO(a.garipov): Consider collecting all validation errors instead of
// quitting after the first one. // quitting after the first one.
// configuration represents the on-disk configuration of AdGuard DNS.
type configuration struct { type configuration struct {
// RateLimit is the rate limiting configuration. // RateLimit is the rate limiting configuration.
RateLimit *rateLimitConfig `yaml:"ratelimit"` RateLimit *rateLimitConfig `yaml:"ratelimit"`
@ -88,107 +82,103 @@ type configuration struct {
ServerGroups serverGroups `yaml:"server_groups"` ServerGroups serverGroups `yaml:"server_groups"`
} }
// errNilConfig signals that config is empty // type check
const errNilConfig errors.Error = "nil config" var _ validator = (*configuration)(nil)
// buildQueryLog build an appropriate query log implementation from the // validate implements the [validator] interface for *configuration.
// configuration and environment data. c is assumed to be valid.
func (c *configuration) buildQueryLog(envs *environments) (l querylog.Interface) {
fileNeeded := c.QueryLog.File.Enabled
if !fileNeeded {
return querylog.Empty{}
}
return querylog.NewFileSystem(&querylog.FileSystemConfig{
Path: envs.QueryLogPath,
RandSeed: uint64(time.Now().UnixNano()),
})
}
// validate returns an error if the configuration is invalid.
func (c *configuration) validate() (err error) { func (c *configuration) validate() (err error) {
if c == nil { if c == nil {
return errNilConfig return errors.ErrNoValue
} }
// Keep this in the same order as the fields in the config. // Keep this in the same order as the fields in the config.
validators := []struct { validators := container.KeyValues[string, validator]{{
validate func() (err error) Key: "ratelimit",
name string Value: c.RateLimit,
}{{
validate: c.RateLimit.validate,
name: "ratelimit",
}, { }, {
validate: c.Upstream.validate, Key: "upstream",
name: "upstream", Value: c.Upstream,
}, { }, {
validate: c.Cache.validate, Key: "cache",
name: "cache", Value: c.Cache,
}, { }, {
validate: c.DNSDB.validate, Key: "dnsdb",
name: "dnsdb", Value: c.DNSDB,
}, { }, {
validate: c.DNS.validate, Key: "dns",
name: "dns", Value: c.DNS,
}, { }, {
validate: c.Backend.validate, Key: "backend",
name: "backend", Value: c.Backend,
}, { }, {
validate: c.QueryLog.validate, Key: "query_log",
name: "query_log", Value: c.QueryLog,
}, { }, {
validate: c.GeoIP.validate, Key: "geoip",
name: "geoip", Value: c.GeoIP,
}, { }, {
validate: c.Check.validate, Key: "check",
name: "check", Value: c.Check,
}, { }, {
validate: c.Web.validate, Key: "web",
name: "web", Value: c.Web,
}, { }, {
validate: c.SafeBrowsing.validate, Key: "safe_browsing",
name: "safe_browsing", Value: c.SafeBrowsing,
}, { }, {
validate: c.AdultBlocking.validate, Key: "adult_blocking",
name: "adult_blocking", Value: c.AdultBlocking,
}, { }, {
validate: c.Filters.validate, Key: "filters",
name: "filters", Value: c.Filters,
}, { }, {
validate: c.FilteringGroups.validate, Key: "filtering_groups",
name: "filtering groups", Value: c.FilteringGroups,
}, { }, {
validate: c.ServerGroups.validate, Key: "server_groups",
name: "server_groups", Value: c.ServerGroups,
}, { }, {
validate: c.ConnectivityCheck.validate, Key: "connectivity_check",
name: "connectivity_check", Value: c.ConnectivityCheck,
}, { }, {
validate: c.InterfaceListeners.validate, Key: "interface_listeners",
name: "interface_listeners", Value: c.InterfaceListeners,
}, { }, {
validate: c.Network.validate, Key: "network",
name: "network", Value: c.Network,
}, { }, {
validate: c.Access.validate, Key: "access",
name: "access", Value: c.Access,
}, { }, {
validate: c.AdditionalMetricsInfo.validate, Key: "additional_metrics_info",
name: "additional_metrics_info", Value: c.AdditionalMetricsInfo,
}} }}
for _, v := range validators { // TODO(a.garipov): Use errors.Join everywhere.
err = v.validate() for _, kv := range validators {
err = kv.Value.validate()
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", v.name, err) return fmt.Errorf("%s: %w", kv.Key, err)
} }
} }
return nil return nil
} }
// readConfig reads the configuration. // isProfilesEnabled returns true if there is at least one server group with
func readConfig(confPath string) (c *configuration, err error) { // profiles enabled. conf must be valid.
func (c *configuration) isProfilesEnabled() (ok bool) {
for _, s := range c.ServerGroups {
if s.ProfilesEnabled {
return true
}
}
return false
}
// parseConfig reads the configuration.
func parseConfig(confPath string) (c *configuration, err error) {
// #nosec G304 -- Trust the path to the configuration file that is given // #nosec G304 -- Trust the path to the configuration file that is given
// from the environment. // from the environment.
yamlFile, err := os.ReadFile(confPath) yamlFile, err := os.ReadFile(confPath)
@ -204,13 +194,3 @@ func readConfig(confPath string) (c *configuration, err error) {
return c, nil return c, nil
} }
// defaultTimeout is the timeout used for some operations where another timeout
// hasn't been defined yet.
const defaultTimeout = 30 * time.Second
// ctxWithDefaultTimeout is a helper function that returns a context with
// timeout set to defaultTimeout.
func ctxWithDefaultTimeout() (ctx context.Context, cancel context.CancelFunc) {
return context.WithTimeout(context.Background(), defaultTimeout)
}

View File

@ -6,13 +6,9 @@ import (
"net/netip" "net/netip"
"github.com/AdguardTeam/AdGuardDNS/internal/agd" "github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/dnssvc"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
) )
// Connectivity check configuration
// connCheckConfig is the connectivity check configuration. // connCheckConfig is the connectivity check configuration.
type connCheckConfig struct { type connCheckConfig struct {
// ProbeIPv4 is a probe v4 address to perform a check to. // ProbeIPv4 is a probe v4 address to perform a check to.
@ -22,13 +18,16 @@ type connCheckConfig struct {
ProbeIPv6 netip.AddrPort `yaml:"probe_ipv6"` ProbeIPv6 netip.AddrPort `yaml:"probe_ipv6"`
} }
// validate returns an error if the connectivityCheck configuration is invalid. // type check
var _ validator = (*connCheckConfig)(nil)
// validate implements the [validator] interface for *connCheckConfig.
func (c *connCheckConfig) validate() (err error) { func (c *connCheckConfig) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return errNilConfig return errors.ErrNoValue
case c.ProbeIPv4 == netip.AddrPort{}: case c.ProbeIPv4 == netip.AddrPort{}:
return errors.Error("no ipv4") return fmt.Errorf("probe_ipv4: %w", errors.ErrEmptyValue)
} }
return nil return nil
@ -39,43 +38,39 @@ func (c *connCheckConfig) validate() (err error) {
// server bind addresses looking up for IPv6 addresses. If an IPv6 address is // server bind addresses looking up for IPv6 addresses. If an IPv6 address is
// found, then additionally to a general probe to IPv4 it will perform a check // found, then additionally to a general probe to IPv4 it will perform a check
// to IPv6 probe address. // to IPv6 probe address.
func connectivityCheck(c *dnssvc.Config, connCheck *connCheckConfig) (err error) { func connectivityCheck(srvGrps []*agd.ServerGroup, connCheck *connCheckConfig) (err error) {
probeIPv4 := net.TCPAddrFromAddrPort(connCheck.ProbeIPv4) probeIPv4 := net.TCPAddrFromAddrPort(connCheck.ProbeIPv4)
// General check to IPv4 probe address. // General check to IPv4 probe address.
conn, err := net.DialTCP("tcp4", nil, probeIPv4) conn4, err := net.DialTCP("tcp4", nil, probeIPv4)
if err != nil { if err != nil {
return fmt.Errorf("connectivity check: ipv4: %w", err) return fmt.Errorf("connectivity check: ipv4: %w", err)
} }
defer func() { defer func() {
closeErr := conn.Close() errClose := errors.Annotate(conn4.Close(), "connectivity check: closing ipv4: %w")
if closeErr != nil { err = errors.WithDeferred(err, errClose)
log.Fatalf("connectivity check: ipv4: %v", closeErr)
}
}() }()
if !requireIPv6ConnCheck(c.ServerGroups) { if !requireIPv6ConnCheck(srvGrps) {
return nil return nil
} }
if (connCheck.ProbeIPv6 == netip.AddrPort{}) { if (connCheck.ProbeIPv6 == netip.AddrPort{}) {
log.Fatal("connectivity check: no ipv6 probe address in config") return errors.Error("connectivity check: no ipv6 probe address in config")
} }
probeIPv6 := net.TCPAddrFromAddrPort(connCheck.ProbeIPv6) probeIPv6 := net.TCPAddrFromAddrPort(connCheck.ProbeIPv6)
// Check to IPv6 probe address. // Check to IPv6 probe address.
connV6, err := net.DialTCP("tcp6", nil, probeIPv6) conn6, err := net.DialTCP("tcp6", nil, probeIPv6)
if err != nil { if err != nil {
return fmt.Errorf("connectivity check: ipv6: %w", err) return fmt.Errorf("connectivity check: ipv6: %w", err)
} }
defer func() { defer func() {
closeErr := connV6.Close() errClose := errors.Annotate(conn6.Close(), "connectivity check: closing ipv6: %w")
if closeErr != nil { err = errors.WithDeferred(err, errClose)
log.Fatalf("connectivity check: ipv6: %v", closeErr)
}
}() }()
return nil return nil

29
internal/cmd/context.go Normal file
View File

@ -0,0 +1,29 @@
package cmd
import (
"context"
"time"
)
// defaultTimeout is the timeout used for some operations where another timeout
// hasn't been defined yet.
const defaultTimeout = 30 * time.Second
// contextConstructor is a type alias for functions that can create a context.
type contextConstructor = func() (ctx context.Context, cancel context.CancelFunc)
// ctxWithDefaultTimeout is a helper function that returns a context with
// timeout set to defaultTimeout.
func ctxWithDefaultTimeout() (ctx context.Context, cancel context.CancelFunc) {
return context.WithTimeout(context.Background(), defaultTimeout)
}
// newCtxWithTimeoutCons returns a context constructor that creates a simple
// context with the given timeout.
func newCtxWithTimeoutCons(timeout time.Duration) (c contextConstructor) {
parent := context.Background()
return func() (ctx context.Context, cancel context.CancelFunc) {
return context.WithTimeout(parent, timeout)
}
}

View File

@ -15,8 +15,6 @@ import (
"github.com/miekg/dns" "github.com/miekg/dns"
) )
// Discovery Of Designated Resolvers (DDR) configuration
// ddrConfig is the configuration for a server group's DDR handler. // ddrConfig is the configuration for a server group's DDR handler.
type ddrConfig struct { type ddrConfig struct {
// DeviceRecords are used to respond to DDR queries from recognized devices. // DeviceRecords are used to respond to DDR queries from recognized devices.
@ -32,8 +30,8 @@ type ddrConfig struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
} }
// toInternal returns the DDR configuration. messages must not be nil. c is // toInternal returns the DDR configuration. messages must not be nil. c must
// assumed to be valid. // be valid.
func (c *ddrConfig) toInternal(msgs *dnsmsg.Constructor) (conf *agd.DDR) { func (c *ddrConfig) toInternal(msgs *dnsmsg.Constructor) (conf *agd.DDR) {
conf = &agd.DDR{ conf = &agd.DDR{
Enabled: c.Enabled, Enabled: c.Enabled,
@ -73,44 +71,40 @@ func appendDDRSVCBTmpls(
r *ddrRecord, r *ddrRecord,
target string, target string,
) (result []*dns.SVCB) { ) (result []*dns.SVCB) {
protoPorts := []struct { protoPorts := container.KeyValues[agd.Protocol, uint16]{{
proto agd.Protocol Key: agd.ProtoDoH,
port uint16 Value: r.HTTPSPort,
}{{
proto: agd.ProtoDoH,
port: r.HTTPSPort,
}, { }, {
proto: agd.ProtoDoT, Key: agd.ProtoDoT,
port: r.TLSPort, Value: r.TLSPort,
}, { }, {
proto: agd.ProtoDoQ, Key: agd.ProtoDoQ,
port: r.QUICPort, Value: r.QUICPort,
}} }}
var prio uint16 var prio uint16
for _, pp := range protoPorts { for _, kv := range protoPorts {
if pp.port != 0 { port := kv.Value
if port == 0 {
continue
}
prio++ prio++
recs = append(recs, msgs.NewDDRTemplate( rec := msgs.NewDDRTemplate(kv.Key, target, r.DoHPath, r.IPv4Hints, r.IPv6Hints, port, prio)
pp.proto, recs = append(recs, rec)
target,
r.DoHPath,
r.IPv4Hints,
r.IPv6Hints,
pp.port,
prio,
))
}
} }
return recs return recs
} }
// validate returns an error if the DDR configuration is invalid. // type check
var _ validator = (*ddrConfig)(nil)
// validate implements the [validator] interface for *ddrConfig.
func (c *ddrConfig) validate() (err error) { func (c *ddrConfig) validate() (err error) {
if c == nil { if c == nil {
return errNilConfig return errors.ErrNoValue
} }
for wildcard, r := range c.DeviceRecords { for wildcard, r := range c.DeviceRecords {
@ -163,10 +157,13 @@ type ddrRecord struct {
TLSPort uint16 `yaml:"tls_port"` TLSPort uint16 `yaml:"tls_port"`
} }
// validate returns an error if the DDR record fields are invalid. // type check
var _ validator = (*ddrRecord)(nil)
// validate implements the [validator] interface for *ddrRecord.
func (r *ddrRecord) validate() (err error) { func (r *ddrRecord) validate() (err error) {
if r == nil { if r == nil {
return errNilConfig return errors.ErrNoValue
} }
// TODO(a.garipov): Consider validating that r.DoHPath is a valid RFC 6570 // TODO(a.garipov): Consider validating that r.DoHPath is a valid RFC 6570
@ -191,8 +188,8 @@ func (r *ddrRecord) validate() (err error) {
return r.validatePorts() return r.validatePorts()
} }
// validatePorts returns an error if the DDR record has invalid ports. r is // validatePorts returns an error if the DDR record has invalid ports. r must
// assumed to be otherwise valid. // be otherwise valid.
func (r *ddrRecord) validatePorts() (err error) { func (r *ddrRecord) validatePorts() (err error) {
switch { switch {
case r.HTTPSPort != 0 && r.HTTPSPort == r.TLSPort: case r.HTTPSPort != 0 && r.HTTPSPort == r.TLSPort:

View File

@ -3,6 +3,8 @@ package cmd
import ( import (
"fmt" "fmt"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
"github.com/c2h5oh/datasize" "github.com/c2h5oh/datasize"
"github.com/miekg/dns" "github.com/miekg/dns"
@ -31,21 +33,31 @@ type dnsConfig struct {
MaxUDPResponseSize datasize.ByteSize `yaml:"max_udp_response_size"` MaxUDPResponseSize datasize.ByteSize `yaml:"max_udp_response_size"`
} }
// validate returns an error if the configuration is invalid. // type check
var _ validator = (*dnsConfig)(nil)
// validate implements the [validator] interface for *dnsConfig.
func (c *dnsConfig) validate() (err error) { func (c *dnsConfig) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return errNilConfig return errors.ErrNoValue
case c.ReadTimeout.Duration <= 0: case c.ReadTimeout.Duration <= 0:
return newMustBePositiveError("read_timeout", c.ReadTimeout) return newNotPositiveError("read_timeout", c.ReadTimeout)
case c.TCPIdleTimeout.Duration <= 0: case c.TCPIdleTimeout.Duration <= 0:
return newMustBePositiveError("tcp_idle_timeout", c.TCPIdleTimeout) return newNotPositiveError("tcp_idle_timeout", c.TCPIdleTimeout)
case c.TCPIdleTimeout.Duration > dnsserver.MaxTCPIdleTimeout:
return fmt.Errorf(
"tcp_idle_timeout: %w: must be less than or equal to %s got %s",
errors.ErrOutOfRange,
dnsserver.MaxTCPIdleTimeout,
c.TCPIdleTimeout,
)
case c.WriteTimeout.Duration <= 0: case c.WriteTimeout.Duration <= 0:
return newMustBePositiveError("write_timeout", c.WriteTimeout) return newNotPositiveError("write_timeout", c.WriteTimeout)
case c.HandleTimeout.Duration <= 0: case c.HandleTimeout.Duration <= 0:
return newMustBePositiveError("handle_timeout", c.HandleTimeout) return newNotPositiveError("handle_timeout", c.HandleTimeout)
case c.MaxUDPResponseSize.Bytes() == 0: case c.MaxUDPResponseSize.Bytes() == 0:
return newMustBePositiveError("max_udp_response_size", c.MaxUDPResponseSize) return newNotPositiveError("max_udp_response_size", c.MaxUDPResponseSize)
case c.MaxUDPResponseSize.Bytes() > dns.MaxMsgSize: case c.MaxUDPResponseSize.Bytes() > dns.MaxMsgSize:
return fmt.Errorf( return fmt.Errorf(
"max_udp_response_size must be less than %s, got %s", "max_udp_response_size must be less than %s, got %s",

View File

@ -10,8 +10,6 @@ import (
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
// DNSCrypt Configuration
// dnsCryptConfig are the DNSCrypt server settings. // dnsCryptConfig are the DNSCrypt server settings.
type dnsCryptConfig struct { type dnsCryptConfig struct {
// Inline is the inline configuration. Must be empty if ConfigPath is not // Inline is the inline configuration. Must be empty if ConfigPath is not
@ -23,8 +21,8 @@ type dnsCryptConfig struct {
ConfigPath string `yaml:"config_path"` ConfigPath string `yaml:"config_path"`
} }
// toInternal converts c to the DNSCrypt configuration for a DNS server. c is // toInternal converts c to the DNSCrypt configuration for a DNS server. c must
// assumed to be valid. // be valid.
func (c *dnsCryptConfig) toInternal() (conf *agd.DNSCryptConfig, err error) { func (c *dnsCryptConfig) toInternal() (conf *agd.DNSCryptConfig, err error) {
if c == nil { if c == nil {
return nil, nil return nil, nil

View File

@ -3,6 +3,7 @@ package cmd
import ( import (
"github.com/AdguardTeam/AdGuardDNS/internal/dnsdb" "github.com/AdguardTeam/AdGuardDNS/internal/dnsdb"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll" "github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/golibs/errors"
) )
// dnsDBConfig is the configuration of the DNSDB module. // dnsDBConfig is the configuration of the DNSDB module.
@ -14,19 +15,25 @@ type dnsDBConfig struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
} }
// validate returns an error if the configuration is invalid. // type check
var _ validator = (*dnsDBConfig)(nil)
// validate implements the [validator] interface for *dnsDBConfig.
func (c *dnsDBConfig) validate() (err error) { func (c *dnsDBConfig) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return errNilConfig return errors.ErrNoValue
case !c.Enabled:
return nil
case c.MaxSize <= 0: case c.MaxSize <= 0:
return newMustBePositiveError("size", c.MaxSize) return newNotPositiveError("size", c.MaxSize)
default: default:
return nil return nil
} }
} }
// toInternal builds and returns an anonymous statistics collector. // toInternal builds and returns an anonymous statistics collector. c must be
// valid.
func (c *dnsDBConfig) toInternal(errColl errcoll.Interface) (d dnsdb.Interface) { func (c *dnsDBConfig) toInternal(errColl errcoll.Interface) (d dnsdb.Interface) {
if !c.Enabled { if !c.Enabled {
return dnsdb.Empty{} return dnsdb.Empty{}

View File

@ -1,47 +1,45 @@
package cmd package cmd
import ( import (
"context"
"fmt" "fmt"
"log/slog" "log/slog"
"math"
"net" "net"
"net/http" "net/http"
"os" "os"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd" "github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/AdguardTeam/AdGuardDNS/internal/agdservice"
"github.com/AdguardTeam/AdGuardDNS/internal/debugsvc" "github.com/AdguardTeam/AdGuardDNS/internal/debugsvc"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsdb" "github.com/AdguardTeam/AdGuardDNS/internal/dnsdb"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll" "github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip" "github.com/AdguardTeam/AdGuardDNS/internal/version"
"github.com/AdguardTeam/AdGuardDNS/internal/rulestat" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil" "github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/netutil/urlutil" "github.com/AdguardTeam/golibs/netutil/urlutil"
"github.com/AdguardTeam/golibs/service" "github.com/AdguardTeam/golibs/timeutil"
"github.com/c2h5oh/datasize"
"github.com/caarlos0/env/v7" "github.com/caarlos0/env/v7"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
) )
// environments represents the configuration that is kept in the environment. // environment represents the configuration that is kept in the environment.
type environments struct { type environment struct {
AdultBlockingURL *urlutil.URL `env:"ADULT_BLOCKING_URL,notEmpty"` AdultBlockingURL *urlutil.URL `env:"ADULT_BLOCKING_URL"`
BillStatURL *urlutil.URL `env:"BILLSTAT_URL,notEmpty"` BillStatURL *urlutil.URL `env:"BILLSTAT_URL"`
BlockedServiceIndexURL *urlutil.URL `env:"BLOCKED_SERVICE_INDEX_URL,notEmpty"` BlockedServiceIndexURL *urlutil.URL `env:"BLOCKED_SERVICE_INDEX_URL"`
ConsulAllowlistURL *urlutil.URL `env:"CONSUL_ALLOWLIST_URL,notEmpty"` ConsulAllowlistURL *urlutil.URL `env:"CONSUL_ALLOWLIST_URL,notEmpty"`
ConsulDNSCheckKVURL *urlutil.URL `env:"CONSUL_DNSCHECK_KV_URL"` ConsulDNSCheckKVURL *urlutil.URL `env:"CONSUL_DNSCHECK_KV_URL"`
ConsulDNSCheckSessionURL *urlutil.URL `env:"CONSUL_DNSCHECK_SESSION_URL"` ConsulDNSCheckSessionURL *urlutil.URL `env:"CONSUL_DNSCHECK_SESSION_URL"`
FilterIndexURL *urlutil.URL `env:"FILTER_INDEX_URL,notEmpty"` FilterIndexURL *urlutil.URL `env:"FILTER_INDEX_URL,notEmpty"`
GeneralSafeSearchURL *urlutil.URL `env:"GENERAL_SAFE_SEARCH_URL,notEmpty"` GeneralSafeSearchURL *urlutil.URL `env:"GENERAL_SAFE_SEARCH_URL"`
LinkedIPTargetURL *urlutil.URL `env:"LINKED_IP_TARGET_URL"` LinkedIPTargetURL *urlutil.URL `env:"LINKED_IP_TARGET_URL"`
NewRegDomainsURL *urlutil.URL `env:"NEW_REG_DOMAINS_URL,notEmpty"` NewRegDomainsURL *urlutil.URL `env:"NEW_REG_DOMAINS_URL"`
ProfilesURL *urlutil.URL `env:"PROFILES_URL,notEmpty"` ProfilesURL *urlutil.URL `env:"PROFILES_URL"`
RuleStatURL *urlutil.URL `env:"RULESTAT_URL"` RuleStatURL *urlutil.URL `env:"RULESTAT_URL"`
SafeBrowsingURL *urlutil.URL `env:"SAFE_BROWSING_URL,notEmpty"` SafeBrowsingURL *urlutil.URL `env:"SAFE_BROWSING_URL"`
YoutubeSafeSearchURL *urlutil.URL `env:"YOUTUBE_SAFE_SEARCH_URL,notEmpty"` YoutubeSafeSearchURL *urlutil.URL `env:"YOUTUBE_SAFE_SEARCH_URL"`
BillStatAPIKey string `env:"BILLSTAT_API_KEY"` BillStatAPIKey string `env:"BILLSTAT_API_KEY"`
ConfPath string `env:"CONFIG_PATH" envDefault:"./config.yaml"` ConfPath string `env:"CONFIG_PATH" envDefault:"./config.yaml"`
@ -50,22 +48,40 @@ type environments struct {
GeoIPCountryPath string `env:"GEOIP_COUNTRY_PATH" envDefault:"./country.mmdb"` GeoIPCountryPath string `env:"GEOIP_COUNTRY_PATH" envDefault:"./country.mmdb"`
ProfilesAPIKey string `env:"PROFILES_API_KEY"` ProfilesAPIKey string `env:"PROFILES_API_KEY"`
ProfilesCachePath string `env:"PROFILES_CACHE_PATH" envDefault:"./profilecache.pb"` ProfilesCachePath string `env:"PROFILES_CACHE_PATH" envDefault:"./profilecache.pb"`
RedisAddr string `env:"REDIS_ADDR"`
RedisKeyPrefix string `env:"REDIS_KEY_PREFIX" envDefault:"agdns"`
QueryLogPath string `env:"QUERYLOG_PATH" envDefault:"./querylog.jsonl"` QueryLogPath string `env:"QUERYLOG_PATH" envDefault:"./querylog.jsonl"`
SSLKeyLogFile string `env:"SSL_KEY_LOG_FILE"` SSLKeyLogFile string `env:"SSL_KEY_LOG_FILE"`
SentryDSN string `env:"SENTRY_DSN" envDefault:"stderr"` SentryDSN string `env:"SENTRY_DSN" envDefault:"stderr"`
WebStaticDir string `env:"WEB_STATIC_DIR"`
ListenAddr net.IP `env:"LISTEN_ADDR" envDefault:"127.0.0.1"` ListenAddr net.IP `env:"LISTEN_ADDR" envDefault:"127.0.0.1"`
ListenPort uint16 `env:"LISTEN_PORT" envDefault:"8181"` ProfilesMaxRespSize datasize.ByteSize `env:"PROFILES_MAX_RESP_SIZE" envDefault:"8MB"`
RedisIdleTimeout timeutil.Duration `env:"REDIS_IDLE_TIMEOUT" envDefault:"30s"`
RedisMaxActive int `env:"REDIS_MAX_ACTIVE" envDefault:"10"`
RedisMaxIdle int `env:"REDIS_MAX_IDLE" envDefault:"3"`
ListenPort uint16 `env:"LISTEN_PORT" envDefault:"8181"`
RedisPort uint16 `env:"REDIS_PORT" envDefault:"6379"`
Verbosity uint8 `env:"VERBOSE" envDefault:"0"`
AdultBlockingEnabled strictBool `env:"ADULT_BLOCKING_ENABLED" envDefault:"1"`
LogTimestamp strictBool `env:"LOG_TIMESTAMP" envDefault:"1"` LogTimestamp strictBool `env:"LOG_TIMESTAMP" envDefault:"1"`
LogVerbose strictBool `env:"VERBOSE" envDefault:"0"` NewRegDomainsEnabled strictBool `env:"NEW_REG_DOMAINS_ENABLED" envDefault:"1"`
ProfilesEnabled strictBool `env:"PROFILES_ENABLED" envDefault:"1"` SafeBrowsingEnabled strictBool `env:"SAFE_BROWSING_ENABLED" envDefault:"1"`
BlockedServiceEnabled strictBool `env:"BLOCKED_SERVICE_ENABLED" envDefault:"1"`
GeneralSafeSearchEnabled strictBool `env:"GENERAL_SAFE_SEARCH_ENABLED" envDefault:"1"`
YoutubeSafeSearchEnabled strictBool `env:"YOUTUBE_SAFE_SEARCH_ENABLED" envDefault:"1"`
WebStaticDirEnabled strictBool `env:"WEB_STATIC_DIR_ENABLED" envDefault:"0"`
} }
// readEnvs reads the configuration. // parseEnvironment reads the configuration.
func readEnvs() (envs *environments, err error) { func parseEnvironment() (envs *environment, err error) {
envs = &environments{} envs = &environment{}
err = env.Parse(envs) err = env.Parse(envs)
if err != nil { if err != nil {
return nil, fmt.Errorf("parsing environments: %w", err) return nil, fmt.Errorf("parsing environments: %w", err)
@ -74,9 +90,229 @@ func readEnvs() (envs *environments, err error) {
return envs, nil return envs, nil
} }
// type check
var _ validator = (*environment)(nil)
// validate implements the [validator] interface for *environment.
func (envs *environment) validate() (err error) {
// TODO(a.garipov): Use a similar approach with errors.Join everywhere.
var errs []error
errs = envs.validateHTTPURLs(errs)
if s := envs.FilterIndexURL.Scheme; s != agdhttp.SchemeFile && !agdhttp.CheckHTTPURLScheme(s) {
errs = append(errs, fmt.Errorf(
"env %s: not a valid http(s) url or file uri",
"FILTER_INDEX_URL",
))
}
err = envs.validateWebStaticDir()
if err != nil {
errs = append(errs, fmt.Errorf("env WEB_STATIC_DIR: %w", err))
}
_, err = slogutil.VerbosityToLevel(envs.Verbosity)
if err != nil {
errs = append(errs, fmt.Errorf("env VERBOSE: %w", err))
}
return errors.Join(errs...)
}
// urlEnvData is a helper struct for validation of URLs set in environment
// variables.
type urlEnvData struct {
url *urlutil.URL
name string
isRequired bool
}
// validateHTTPURLs appends validation errors to the given errs if HTTP(S) URLs
// in environment variables are invalid. All errors are appended to errs and
// returned as res.
func (envs *environment) validateHTTPURLs(errs []error) (res []error) {
httpOnlyURLs := []*urlEnvData{{
url: envs.AdultBlockingURL,
name: "ADULT_BLOCKING_URL",
isRequired: bool(envs.AdultBlockingEnabled),
}, {
url: envs.BlockedServiceIndexURL,
name: "BLOCKED_SERVICE_INDEX_URL",
isRequired: bool(envs.BlockedServiceEnabled),
}, {
url: envs.ConsulAllowlistURL,
name: "CONSUL_ALLOWLIST_URL",
isRequired: true,
}, {
url: envs.ConsulDNSCheckKVURL,
name: "CONSUL_DNSCHECK_KV_URL",
isRequired: envs.ConsulDNSCheckKVURL != nil,
}, {
url: envs.ConsulDNSCheckSessionURL,
name: "CONSUL_DNSCHECK_SESSION_URL",
isRequired: envs.ConsulDNSCheckSessionURL != nil,
}, {
url: envs.GeneralSafeSearchURL,
name: "GENERAL_SAFE_SEARCH_URL",
isRequired: bool(envs.GeneralSafeSearchEnabled),
}, {
url: envs.LinkedIPTargetURL,
name: "LINKED_IP_TARGET_URL",
isRequired: false,
}, {
url: envs.NewRegDomainsURL,
name: "NEW_REG_DOMAINS_URL",
isRequired: bool(envs.NewRegDomainsEnabled),
}, {
url: envs.RuleStatURL,
name: "RULESTAT_URL",
isRequired: false,
}, {
url: envs.SafeBrowsingURL,
name: "SAFE_BROWSING_URL",
isRequired: bool(envs.SafeBrowsingEnabled),
}, {
url: envs.YoutubeSafeSearchURL,
name: "YOUTUBE_SAFE_SEARCH_URL",
isRequired: bool(envs.YoutubeSafeSearchEnabled),
}}
res = errs
for _, urlData := range httpOnlyURLs {
if !urlData.isRequired {
continue
}
u := urlData.url
if u == nil {
res = append(res, fmt.Errorf("env %s: %w", urlData.name, errors.ErrEmptyValue))
continue
}
if !agdhttp.CheckHTTPURLScheme(u.Scheme) {
res = append(res, fmt.Errorf("env %s: not a valid http(s) url", urlData.name))
}
}
return res
}
// validateWebStaticDir returns an error if the WEB_STATIC_DIR environment
// variable contains an invalid value.
func (envs *environment) validateWebStaticDir() (err error) {
if !envs.WebStaticDirEnabled {
return nil
}
dir := envs.WebStaticDir
if dir == "" {
return errors.ErrEmptyValue
}
// Use a best-effort check to make sure the directory exists.
fi, err := os.Stat(dir)
if err != nil {
return err
}
if !fi.IsDir() {
return errors.Error("not a directory")
}
return nil
}
// validateFromValidConfig returns an error if environment variables that depend
// on configuration properties contain errors. conf is expected to be valid.
func (envs *environment) validateFromValidConfig(conf *configuration) (err error) {
err = envs.validateRedis(conf)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
if !conf.isProfilesEnabled() {
return nil
}
if envs.ProfilesMaxRespSize > math.MaxInt {
return fmt.Errorf(
"PROFILES_MAX_RESP_SIZE: %w: must be less than or equal to %s, got %s",
errors.ErrOutOfRange,
datasize.ByteSize(math.MaxInt),
envs.ProfilesMaxRespSize,
)
}
return envs.validateProfilesURLs()
}
// validateRedis returns an error if environment variables for Redis as a remote
// key-value store for DNS server checking contain errors.
func (envs *environment) validateRedis(conf *configuration) (err error) {
if conf.Check.RemoteKV.Type != kvModeRedis {
return nil
}
var errs []error
if envs.RedisAddr == "" {
errs = append(errs, fmt.Errorf("REDIS_ADDR: %q", errors.ErrEmptyValue))
}
if envs.RedisIdleTimeout.Duration <= 0 {
errs = append(errs, newNotPositiveError("REDIS_IDLE_TIMEOUT", envs.RedisIdleTimeout))
}
if envs.RedisMaxActive < 0 {
errs = append(errs, newNegativeError("REDIS_MAX_ACTIVE", envs.RedisMaxActive))
}
if envs.RedisMaxIdle < 0 {
errs = append(errs, newNegativeError("REDIS_MAX_IDLE", envs.RedisMaxIdle))
}
return errors.Join(errs...)
}
// validateProfilesURLs appends validation errors to the given errs if profiles
// URLs in environment variables are invalid. All errors are appended to errs
// and returned as res.
func (envs *environment) validateProfilesURLs() (err error) {
grpcOnlyURLs := []*urlEnvData{{
url: envs.BillStatURL,
name: "BILLSTAT_URL",
isRequired: true,
}, {
url: envs.ProfilesURL,
name: "PROFILES_URL",
isRequired: true,
}}
var res []error
for _, urlData := range grpcOnlyURLs {
if !urlData.isRequired {
continue
}
if urlData.url == nil {
res = append(res, fmt.Errorf("env %s: %w", urlData.name, errors.ErrEmptyValue))
continue
}
if !agdhttp.CheckGRPCURLScheme(urlData.url.Scheme) {
res = append(res, fmt.Errorf("env %s: not a valid grpc(s) url", urlData.name))
}
}
return errors.Join(res...)
}
// configureLogs sets the configuration for the plain text logs. It also // configureLogs sets the configuration for the plain text logs. It also
// returns a [slog.Logger] for code that uses it. // returns a [slog.Logger] for code that uses it.
func (envs *environments) configureLogs() (slogLogger *slog.Logger) { func (envs *environment) configureLogs() (slogLogger *slog.Logger) {
var flags int var flags int
if envs.LogTimestamp { if envs.LogTimestamp {
flags = log.LstdFlags | log.Lmicroseconds flags = log.LstdFlags | log.Lmicroseconds
@ -84,20 +320,21 @@ func (envs *environments) configureLogs() (slogLogger *slog.Logger) {
log.SetFlags(flags) log.SetFlags(flags)
if envs.LogVerbose { lvl := errors.Must(slogutil.VerbosityToLevel(envs.Verbosity))
if lvl < slog.LevelInfo {
log.SetLevel(log.DEBUG) log.SetLevel(log.DEBUG)
} }
return slogutil.New(&slogutil.Config{ return slogutil.New(&slogutil.Config{
Output: os.Stdout, Output: os.Stdout,
Format: slogutil.FormatAdGuardLegacy, Format: slogutil.FormatAdGuardLegacy,
Level: lvl,
AddTimestamp: bool(envs.LogTimestamp), AddTimestamp: bool(envs.LogTimestamp),
Verbose: bool(envs.LogVerbose),
}) })
} }
// buildErrColl builds and returns an error collector from environment. // buildErrColl builds and returns an error collector from environment.
func (envs *environments) buildErrColl() (errColl errcoll.Interface, err error) { func (envs *environment) buildErrColl() (errColl errcoll.Interface, err error) {
dsn := envs.SentryDSN dsn := envs.SentryDSN
if dsn == "stderr" { if dsn == "stderr" {
return errcoll.NewWriterErrorCollector(os.Stderr), nil return errcoll.NewWriterErrorCollector(os.Stderr), nil
@ -106,7 +343,7 @@ func (envs *environments) buildErrColl() (errColl errcoll.Interface, err error)
cli, err := sentry.NewClient(sentry.ClientOptions{ cli, err := sentry.NewClient(sentry.ClientOptions{
Dsn: dsn, Dsn: dsn,
AttachStacktrace: true, AttachStacktrace: true,
Release: agd.Version(), Release: version.Version(),
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -115,34 +352,11 @@ func (envs *environments) buildErrColl() (errColl errcoll.Interface, err error)
return errcoll.NewSentryErrorCollector(cli), nil return errcoll.NewSentryErrorCollector(cli), nil
} }
// geoIP returns an GeoIP database implementation from environment.
func (envs *environments) geoIP(
ctx context.Context,
c *geoIPConfig,
cacheManager agdcache.Manager,
) (g *geoip.File, err error) {
log.Debug("using geoip files %q and %q", envs.GeoIPASNPath, envs.GeoIPCountryPath)
g = geoip.NewFile(&geoip.FileConfig{
CacheManager: cacheManager,
ASNPath: envs.GeoIPASNPath,
CountryPath: envs.GeoIPCountryPath,
HostCacheSize: c.HostCacheSize,
IPCacheSize: c.IPCacheSize,
AllTopASNs: geoip.DefaultTopASNs,
CountryTopASNs: geoip.DefaultCountryTopASNs,
})
err = g.Refresh(ctx)
if err != nil {
return nil, fmt.Errorf("creating geoip: initial refresh: %w", err)
}
return g, nil
}
// debugConf returns a debug HTTP service configuration from environment. // debugConf returns a debug HTTP service configuration from environment.
func (envs *environments) debugConf(dnsDB dnsdb.Interface) (conf *debugsvc.Config) { func (envs *environment) debugConf(
dnsDB dnsdb.Interface,
logger *slog.Logger,
) (conf *debugsvc.Config) {
// TODO(a.garipov): Simplify the config if these are guaranteed to always be // TODO(a.garipov): Simplify the config if these are guaranteed to always be
// the same. // the same.
addr := netutil.JoinHostPort(envs.ListenAddr.String(), envs.ListenPort) addr := netutil.JoinHostPort(envs.ListenAddr.String(), envs.ListenPort)
@ -160,9 +374,9 @@ func (envs *environments) debugConf(dnsDB dnsdb.Interface) (conf *debugsvc.Confi
} }
conf = &debugsvc.Config{ conf = &debugsvc.Config{
DNSDBAddr: dnsDBAddr,
DNSDBHandler: dnsDBHdlr, DNSDBHandler: dnsDBHdlr,
Logger: logger.With(slogutil.KeyPrefix, "debugsvc"),
DNSDBAddr: dnsDBAddr,
APIAddr: addr, APIAddr: addr,
PprofAddr: addr, PprofAddr: addr,
PrometheusAddr: addr, PrometheusAddr: addr,
@ -171,42 +385,6 @@ func (envs *environments) debugConf(dnsDB dnsdb.Interface) (conf *debugsvc.Confi
return conf return conf
} }
// buildRuleStat returns a filtering rule statistics collector from environment and
// registers its refresher in sigHdlr, if necessary.
func (envs *environments) buildRuleStat(
sigHdlr *service.SignalHandler,
errColl errcoll.Interface,
) (r rulestat.Interface, err error) {
if envs.RuleStatURL == nil {
log.Info("main: warning: not collecting rule stats")
return rulestat.Empty{}, nil
}
httpRuleStat := rulestat.NewHTTP(&rulestat.HTTPConfig{
ErrColl: errColl,
URL: &envs.RuleStatURL.URL,
})
refr := agdservice.NewRefreshWorker(&agdservice.RefreshWorkerConfig{
Context: ctxWithDefaultTimeout,
Refresher: httpRuleStat,
Name: "rulestat",
// TODO(ameshkov): Consider making configurable.
Interval: 10 * time.Minute,
RefreshOnShutdown: true,
RandomizeStart: false,
})
err = refr.Start(context.Background())
if err != nil {
return nil, fmt.Errorf("starting rulestat refresher: %w", err)
}
sigHdlr.Add(refr)
return httpRuleStat, nil
}
// strictBool is a type for booleans that are parsed from the environment more // strictBool is a type for booleans that are parsed from the environment more
// strictly than the usual bool. It only accepts "0" and "1" as valid values. // strictly than the usual bool. It only accepts "0" and "1" as valid values.
type strictBool bool type strictBool bool

View File

@ -1,44 +1,69 @@
package cmd package cmd
import ( import (
"context"
"fmt" "fmt"
"log/slog"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
"golang.org/x/exp/constraints" "golang.org/x/exp/constraints"
) )
// Error-handling utilities // validator is the interface for configuration entities that can validate
// themselves.
type validator interface {
// validate returns an error if the entity isn't valid.
validate() (err error)
}
// check is a simple error-checking helper. It must only be used within Main. // reportPanics reports all panics in Main using the Sentry client, logs them,
func check(err error) { // and repanics. It should be called in a defer.
if err != nil { func reportPanics(ctx context.Context, errColl errcoll.Interface, l *slog.Logger) {
panic(err) v := recover()
if v == nil {
return
} }
err := errors.FromRecovered(v)
l.ErrorContext(ctx, "recovered from panic", slogutil.KeyError, err)
slogutil.PrintStack(ctx, l, slog.LevelError)
errColl.Collect(ctx, err)
errFlushColl, ok := errColl.(errcoll.ErrorFlushCollector)
if ok {
errFlushColl.Flush()
}
panic(v)
} }
// numberOrDuration is the constraint for integer types along with // numberOrDuration is the constraint for integer types along with
// timeutil.Duration. // [timeutil.Duration].
type numberOrDuration interface { type numberOrDuration interface {
constraints.Integer | timeutil.Duration constraints.Integer | timeutil.Duration
} }
// newMustBePositiveError returns an error about the value that must be positive // newNotPositiveError returns an error about the value that must be positive
// but isn't. prop is the name of the property to mention in the error message. // but isn't. prop is the name of the property to mention in the error message.
func newMustBePositiveError[T numberOrDuration](prop string, v T) (err error) { // The returned error has underlying value of [errors.ErrNotPositive].
func newNotPositiveError[T numberOrDuration](prop string, v T) (err error) {
if s, ok := any(v).(fmt.Stringer); ok { if s, ok := any(v).(fmt.Stringer); ok {
return fmt.Errorf("%s must be positive, got %s", prop, s) return fmt.Errorf("%s: %w: got %s", prop, errors.ErrNotPositive, s)
} }
return fmt.Errorf("%s must be positive, got %d", prop, v) return fmt.Errorf("%s: %w: got %d", prop, errors.ErrNotPositive, v)
} }
// newMustBeNonNegativeError returns an error about the value that must be // newNegativeError returns an error about the value that must be non-negative
// non-negative but isn't. prop is the name of the property to mention in the // but isn't. prop is the name of the property to mention in the error message.
// error message. // The returned error has underlying value of [errors.ErrNegative].
func newMustBeNonNegativeError[T numberOrDuration](prop string, v T) (err error) { func newNegativeError[T numberOrDuration](prop string, v T) (err error) {
if s, ok := any(v).(fmt.Stringer); ok { if s, ok := any(v).(fmt.Stringer); ok {
return fmt.Errorf("%s must be non-negative, got %s", prop, s) return fmt.Errorf("%s: %w: got %s", prop, errors.ErrNegative, s)
} }
return fmt.Errorf("%s must be non-negative, got %d", prop, v) return fmt.Errorf("%s: %w: got %d", prop, errors.ErrNegative, v)
} }

View File

@ -1,17 +1,17 @@
package cmd package cmd
import ( import (
"context"
"fmt" "fmt"
"log/slog"
"net/url"
"time" "time"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache" "github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/AdguardTeam/AdGuardDNS/internal/agdservice"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll" "github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/filter" "github.com/AdguardTeam/AdGuardDNS/internal/filter"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/hashprefix" "github.com/AdguardTeam/AdGuardDNS/internal/filter/hashprefix"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/service"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
"github.com/c2h5oh/datasize" "github.com/c2h5oh/datasize"
) )
@ -61,20 +61,37 @@ type filtersConfig struct {
} }
// toInternal converts c to the filter storage configuration for the DNS server. // toInternal converts c to the filter storage configuration for the DNS server.
// cacheDir must exist. c is assumed to be valid. // cacheDir must exist. c must be valid.
func (c *filtersConfig) toInternal( func (c *filtersConfig) toInternal(
logger *slog.Logger,
errColl errcoll.Interface, errColl errcoll.Interface,
cacheManager agdcache.Manager, cacheManager agdcache.Manager,
envs *environments, envs *environment,
safeBrowsing *hashprefix.Filter, safeBrowsing *hashprefix.Filter,
adultBlocking *hashprefix.Filter, adultBlocking *hashprefix.Filter,
newRegDomains *hashprefix.Filter, newRegDomains *hashprefix.Filter,
) (conf *filter.DefaultStorageConfig) { ) (conf *filter.DefaultStorageConfig) {
var blockedServiceIndexURL *url.URL
if envs.BlockedServiceEnabled {
blockedServiceIndexURL = netutil.CloneURL(&envs.BlockedServiceIndexURL.URL)
}
var generalSafeSearchRulesURL *url.URL
if envs.GeneralSafeSearchEnabled {
generalSafeSearchRulesURL = netutil.CloneURL(&envs.GeneralSafeSearchURL.URL)
}
var youtubeSafeSearchRulesURL *url.URL
if envs.YoutubeSafeSearchEnabled {
youtubeSafeSearchRulesURL = netutil.CloneURL(&envs.YoutubeSafeSearchURL.URL)
}
return &filter.DefaultStorageConfig{ return &filter.DefaultStorageConfig{
BaseLogger: logger,
FilterIndexURL: netutil.CloneURL(&envs.FilterIndexURL.URL), FilterIndexURL: netutil.CloneURL(&envs.FilterIndexURL.URL),
BlockedServiceIndexURL: netutil.CloneURL(&envs.BlockedServiceIndexURL.URL), BlockedServiceIndexURL: blockedServiceIndexURL,
GeneralSafeSearchRulesURL: netutil.CloneURL(&envs.GeneralSafeSearchURL.URL), GeneralSafeSearchRulesURL: generalSafeSearchRulesURL,
YoutubeSafeSearchRulesURL: netutil.CloneURL(&envs.YoutubeSafeSearchURL.URL), YoutubeSafeSearchRulesURL: youtubeSafeSearchRulesURL,
SafeBrowsing: safeBrowsing, SafeBrowsing: safeBrowsing,
AdultBlocking: adultBlocking, AdultBlocking: adultBlocking,
NewRegDomains: newRegDomains, NewRegDomains: newRegDomains,
@ -91,29 +108,32 @@ func (c *filtersConfig) toInternal(
IndexRefreshTimeout: c.IndexRefreshTimeout.Duration, IndexRefreshTimeout: c.IndexRefreshTimeout.Duration,
RuleListRefreshTimeout: c.RuleListRefreshTimeout.Duration, RuleListRefreshTimeout: c.RuleListRefreshTimeout.Duration,
UseRuleListCache: c.RuleListCache.Enabled, UseRuleListCache: c.RuleListCache.Enabled,
MaxRuleListSize: c.MaxSize.Bytes(), MaxRuleListSize: c.MaxSize,
} }
} }
// validate returns an error if the filters configuration is invalid. // type check
var _ validator = (*filtersConfig)(nil)
// validate implements the [validator] interface for *filtersConfig.
func (c *filtersConfig) validate() (err error) { func (c *filtersConfig) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return errNilConfig return errors.ErrNoValue
case c.SafeSearchCacheSize <= 0: case c.SafeSearchCacheSize <= 0:
return newMustBePositiveError("safe_search_cache_size", c.SafeSearchCacheSize) return newNotPositiveError("safe_search_cache_size", c.SafeSearchCacheSize)
case c.ResponseTTL.Duration <= 0: case c.ResponseTTL.Duration <= 0:
return newMustBePositiveError("response_ttl", c.ResponseTTL) return newNotPositiveError("response_ttl", c.ResponseTTL)
case c.RefreshIvl.Duration <= 0: case c.RefreshIvl.Duration <= 0:
return newMustBePositiveError("refresh_interval", c.RefreshIvl) return newNotPositiveError("refresh_interval", c.RefreshIvl)
case c.RefreshTimeout.Duration <= 0: case c.RefreshTimeout.Duration <= 0:
return newMustBePositiveError("refresh_timeout", c.RefreshTimeout) return newNotPositiveError("refresh_timeout", c.RefreshTimeout)
case c.IndexRefreshTimeout.Duration <= 0: case c.IndexRefreshTimeout.Duration <= 0:
return newMustBePositiveError("index_refresh_timeout", c.IndexRefreshTimeout) return newNotPositiveError("index_refresh_timeout", c.IndexRefreshTimeout)
case c.RuleListRefreshTimeout.Duration <= 0: case c.RuleListRefreshTimeout.Duration <= 0:
return newMustBePositiveError("rule_list_refresh_timeout", c.RuleListRefreshTimeout) return newNotPositiveError("rule_list_refresh_timeout", c.RuleListRefreshTimeout)
case c.MaxSize <= 0: case c.MaxSize <= 0:
return newMustBePositiveError("max_size", c.MaxSize) return newNotPositiveError("max_size", c.MaxSize)
default: default:
// Go on. // Go on.
} }
@ -136,49 +156,17 @@ type fltRuleListCache struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
} }
// validate returns an error if the rule-list cache configuration is invalid. // type check
var _ validator = (*fltRuleListCache)(nil)
// validate implements the [validator] interface for *fltRuleListCache.
func (c *fltRuleListCache) validate() (err error) { func (c *fltRuleListCache) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return errNilConfig return errors.ErrNoValue
case c.Size <= 0: case c.Size <= 0:
return newMustBePositiveError("size", c.Size) return newNotPositiveError("size", c.Size)
default: default:
return nil return nil
} }
} }
// setupFilterStorage creates and returns a filter storage as well as starts and
// registers its refresher in the signal handler.
func setupFilterStorage(
ctx context.Context,
conf *filter.DefaultStorageConfig,
sigHdlr *service.SignalHandler,
refreshTimeout time.Duration,
) (strg *filter.DefaultStorage, err error) {
strg = filter.NewDefaultStorage(conf)
err = strg.RefreshInitial(ctx)
if err != nil {
return nil, fmt.Errorf("creating default filter storage: %w", err)
}
refr := agdservice.NewRefreshWorker(&agdservice.RefreshWorkerConfig{
Context: func() (ctx context.Context, cancel context.CancelFunc) {
return context.WithTimeout(context.Background(), refreshTimeout)
},
Refresher: strg,
Name: "filter_storage",
Interval: conf.RefreshIvl,
RefreshOnShutdown: false,
RandomizeStart: false,
})
err = refr.Start(ctx)
if err != nil {
return nil, fmt.Errorf("starting default filter storage update: %w", err)
}
sigHdlr.Add(refr)
return strg, nil
}

View File

@ -9,8 +9,6 @@ import (
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
) )
// Filtering Groups Configuration
// filteringGroup represents a set of filtering settings. // filteringGroup represents a set of filtering settings.
type filteringGroup struct { type filteringGroup struct {
// RuleLists are the filtering rule lists settings for this filtering group. // RuleLists are the filtering rule lists settings for this filtering group.
@ -81,17 +79,20 @@ type fltGrpSafeBrowsing struct {
BlockNewlyRegisteredDomains bool `yaml:"block_newly_registered_domains"` BlockNewlyRegisteredDomains bool `yaml:"block_newly_registered_domains"`
} }
// validate returns an error if the filtering group is invalid. // type check
var _ validator = (*filteringGroup)(nil)
// validate implements the [validator] interface for *filteringGroup.
func (g *filteringGroup) validate() (err error) { func (g *filteringGroup) validate() (err error) {
switch { switch {
case g == nil: case g == nil:
return errNilConfig return errors.ErrNoValue
case g.RuleLists == nil: case g.RuleLists == nil:
return errors.Error("no rule_lists") return fmt.Errorf("rule_lists: %w", errors.ErrNoValue)
case g.ID == "": case g.ID == "":
return errors.Error("no id") return fmt.Errorf("id: %w", errors.ErrEmptyValue)
case g.Parental == nil: case g.Parental == nil:
return errors.Error("no parental") return fmt.Errorf("parental: %w", errors.ErrNoValue)
} }
fltIDs := container.NewMapSet[string]() fltIDs := container.NewMapSet[string]()
@ -116,7 +117,7 @@ func (g *filteringGroup) validate() (err error) {
type filteringGroups []*filteringGroup type filteringGroups []*filteringGroup
// toInternal converts groups to the filtering groups for the DNS server. // toInternal converts groups to the filtering groups for the DNS server.
// groups are assumed to be valid. // groups must be valid.
func (groups filteringGroups) toInternal( func (groups filteringGroups) toInternal(
s filter.Storage, s filter.Storage,
) (fltGrps map[agd.FilteringGroupID]*agd.FilteringGroup, err error) { ) (fltGrps map[agd.FilteringGroupID]*agd.FilteringGroup, err error) {
@ -125,7 +126,7 @@ func (groups filteringGroups) toInternal(
filterIDs := make([]agd.FilterListID, len(g.RuleLists.IDs)) filterIDs := make([]agd.FilterListID, len(g.RuleLists.IDs))
for i, fltID := range g.RuleLists.IDs { for i, fltID := range g.RuleLists.IDs {
// Assume that these have already been validated in // Assume that these have already been validated in
// filteringGroup.validate. // [filteringGroup.validate].
id := agd.FilterListID(fltID) id := agd.FilterListID(fltID)
if !s.HasListID(id) { if !s.HasListID(id) {
return nil, fmt.Errorf("filter list id %q is not in the index", id) return nil, fmt.Errorf("filter list id %q is not in the index", id)
@ -154,10 +155,13 @@ func (groups filteringGroups) toInternal(
return fltGrps, nil return fltGrps, nil
} }
// validate returns an error if these filtering groups are invalid. // type check
var _ validator = filteringGroups(nil)
// validate implements the [validator] interface for filteringGroups.
func (groups filteringGroups) validate() (err error) { func (groups filteringGroups) validate() (err error) {
if len(groups) == 0 { if len(groups) == 0 {
return errors.Error("no filtering_groups") return fmt.Errorf("filtering_groups: %w", errors.ErrEmptyValue)
} }
ids := container.NewMapSet[string]() ids := container.NewMapSet[string]()

View File

@ -1,19 +1,10 @@
package cmd package cmd
import ( import (
"context" "github.com/AdguardTeam/golibs/errors"
"fmt"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/AdguardTeam/AdGuardDNS/internal/agdservice"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
) )
// GeoIP database configuration
// geoIPConfig is the GeoIP database configuration. // geoIPConfig is the GeoIP database configuration.
type geoIPConfig struct { type geoIPConfig struct {
// HostCacheSize is the size of the hostname lookup cache, in entries. // HostCacheSize is the size of the hostname lookup cache, in entries.
@ -27,70 +18,23 @@ type geoIPConfig struct {
RefreshIvl timeutil.Duration `yaml:"refresh_interval"` RefreshIvl timeutil.Duration `yaml:"refresh_interval"`
} }
// validate returns an error if the GeoIP database configuration is invalid. // type check
var _ validator = (*geoIPConfig)(nil)
// validate implements the [validator] interface for *geoIPConfig.
func (c *geoIPConfig) validate() (err error) { func (c *geoIPConfig) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return errNilConfig return errors.ErrNoValue
case c.HostCacheSize <= 0: case c.HostCacheSize <= 0:
// Note that while geoip.File can work with an empty host cache, that // Note that while geoip.File can work with an empty host cache, that
// feature is only used for tests. // feature is only used for tests.
return newMustBePositiveError("host_cache_size", c.HostCacheSize) return newNotPositiveError("host_cache_size", c.HostCacheSize)
case c.IPCacheSize <= 0: case c.IPCacheSize <= 0:
return newMustBePositiveError("ip_cache_size", c.IPCacheSize) return newNotPositiveError("ip_cache_size", c.IPCacheSize)
case c.RefreshIvl.Duration <= 0: case c.RefreshIvl.Duration <= 0:
return newMustBePositiveError("refresh_interval", c.RefreshIvl) return newNotPositiveError("refresh_interval", c.RefreshIvl)
default: default:
return nil return nil
} }
} }
// setupGeoIP creates and sets the GeoIP database as well as creates and starts
// its refresher. It is intended to be used as a goroutine. geoIPPtr and
// refrPtr must not be nil. errCh receives nil if the database and the
// refresher have been created successfully or an error if not.
func setupGeoIP(
geoIPPtr *geoip.File,
refrPtr *agdservice.RefreshWorker,
errCh chan<- error,
conf *geoIPConfig,
envs *environments,
errColl errcoll.Interface,
cacheManager agdcache.Manager,
) {
// TODO(e.burkov): Pass the context through arguments.
ctx := context.Background()
geoIP, err := envs.geoIP(ctx, conf, cacheManager)
if err != nil {
errCh <- fmt.Errorf("creating geoip: %w", err)
return
}
refr := agdservice.NewRefreshWorker(&agdservice.RefreshWorkerConfig{
Context: ctxWithDefaultTimeout,
// Do not add errColl to geoip's config, as that would create an import
// cycle.
Refresher: agdservice.NewRefresherWithErrColl(
geoIP,
log.Info,
errColl,
"geoip_refresh",
),
Name: "geoip",
Interval: conf.RefreshIvl.Duration,
RefreshOnShutdown: false,
RandomizeStart: false,
})
err = refr.Start(ctx)
if err != nil {
errCh <- fmt.Errorf("starting geoip refresher: %w", err)
return
}
*geoIPPtr, *refrPtr = *geoIP, *refr
errCh <- nil
}

View File

@ -1,14 +1,17 @@
package cmd package cmd
import ( import (
"fmt"
"log/slog"
"maps"
"slices"
"github.com/AdguardTeam/AdGuardDNS/internal/bindtodevice" "github.com/AdguardTeam/AdGuardDNS/internal/bindtodevice"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll" "github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/mapsutil" "github.com/AdguardTeam/golibs/logutil/slogutil"
) )
// Network interface listener configuration
// interfaceListenersConfig contains the optional configuration for the network // interfaceListenersConfig contains the optional configuration for the network
// interface listeners and their common parameters. // interface listeners and their common parameters.
type interfaceListenersConfig struct { type interfaceListenersConfig struct {
@ -20,8 +23,10 @@ type interfaceListenersConfig struct {
ChannelBufferSize int `yaml:"channel_buffer_size"` ChannelBufferSize int `yaml:"channel_buffer_size"`
} }
// toInternal converts c to a bindtodevice.Manager. c is assumed to be valid. // toInternal converts c to a possibly-nil bindtodevice.Manager. c must be
// valid.
func (c *interfaceListenersConfig) toInternal( func (c *interfaceListenersConfig) toInternal(
logger *slog.Logger,
errColl errcoll.Interface, errColl errcoll.Interface,
ctrlConf *bindtodevice.ControlConfig, ctrlConf *bindtodevice.ControlConfig,
) (m *bindtodevice.Manager, err error) { ) (m *bindtodevice.Manager, err error) {
@ -30,26 +35,27 @@ func (c *interfaceListenersConfig) toInternal(
} }
m = bindtodevice.NewManager(&bindtodevice.ManagerConfig{ m = bindtodevice.NewManager(&bindtodevice.ManagerConfig{
Logger: logger.With(slogutil.KeyPrefix, "bindtodevice"),
InterfaceStorage: bindtodevice.DefaultInterfaceStorage{}, InterfaceStorage: bindtodevice.DefaultInterfaceStorage{},
ErrColl: errColl, ErrColl: errColl,
ChannelBufferSize: c.ChannelBufferSize, ChannelBufferSize: c.ChannelBufferSize,
}) })
err = mapsutil.SortedRangeError( for _, id := range slices.Sorted(maps.Keys(c.List)) {
c.List, l := c.List[id]
func(id bindtodevice.ID, l *interfaceListener) (addErr error) { err = m.Add(id, l.Interface, l.Port, ctrlConf)
return errors.Annotate(m.Add(id, l.Interface, l.Port, ctrlConf), "adding listener %q: %w", id)
},
)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("adding listener %q: %w", id, err)
}
} }
return m, nil return m, nil
} }
// validate returns an error if the network interface listeners configuration is // type check
// invalid. var _ validator = (*interfaceListenersConfig)(nil)
// validate implements the [validator] interface for *interfaceListenersConfig.
func (c *interfaceListenersConfig) validate() (err error) { func (c *interfaceListenersConfig) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
@ -59,19 +65,19 @@ func (c *interfaceListenersConfig) validate() (err error) {
// values. // values.
return nil return nil
case c.ChannelBufferSize <= 0: case c.ChannelBufferSize <= 0:
return newMustBePositiveError("channel_buffer_size", c.ChannelBufferSize) return newNotPositiveError("channel_buffer_size", c.ChannelBufferSize)
case len(c.List) == 0: case len(c.List) == 0:
return errors.Error("no list") return fmt.Errorf("list: %w", errors.ErrEmptyValue)
default: default:
// Go on. // Go on.
} }
err = mapsutil.SortedRangeError( for _, id := range slices.Sorted(maps.Keys(c.List)) {
c.List, err = c.List[id].validate()
func(id bindtodevice.ID, l *interfaceListener) (lsnrErr error) { if err != nil {
return errors.Annotate(l.validate(), "interface %q: %w", id) return fmt.Errorf("interface %q: %w", id, err)
}, }
) }
return err return err
} }
@ -86,15 +92,18 @@ type interfaceListener struct {
Port uint16 `yaml:"port"` Port uint16 `yaml:"port"`
} }
// validate returns an error if the interface listener configuration is invalid. // type check
var _ validator = (*interfaceListener)(nil)
// validate implements the [validator] interface for *interfaceListener.
func (l *interfaceListener) validate() (err error) { func (l *interfaceListener) validate() (err error) {
switch { switch {
case l == nil: case l == nil:
return errNilConfig return errors.ErrNoValue
case l.Port == 0: case l.Port == 0:
return errors.Error("port must not be zero") return fmt.Errorf("port: %w", errors.ErrEmptyValue)
case l.Interface == "": case l.Interface == "":
return errors.Error("no interface") return fmt.Errorf("interface: %w", errors.ErrEmptyValue)
default: default:
return nil return nil
} }

View File

@ -1,8 +1,12 @@
package cmd package cmd
import ( import (
"fmt"
"math"
"github.com/AdguardTeam/AdGuardDNS/internal/bindtodevice" "github.com/AdguardTeam/AdGuardDNS/internal/bindtodevice"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/netext" "github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/netext"
"github.com/AdguardTeam/golibs/errors"
"github.com/c2h5oh/datasize" "github.com/c2h5oh/datasize"
) )
@ -17,24 +21,45 @@ type network struct {
RcvBufSize datasize.ByteSize `yaml:"so_rcvbuf"` RcvBufSize datasize.ByteSize `yaml:"so_rcvbuf"`
} }
// validate returns an error if the network configuration is invalid. // type check
func (n *network) validate() (err error) { var _ validator = (*interfaceListener)(nil)
if n == nil {
return errNilConfig
}
// validate implements the [validator] interface for *network.
func (n *network) validate() (err error) {
const maxBufSize datasize.ByteSize = math.MaxInt32
switch {
case n == nil:
return errors.ErrNoValue
case n.SndBufSize > maxBufSize:
return fmt.Errorf(
"so_sndbuf: %s: must be less than or equal to %s",
errors.ErrOutOfRange,
maxBufSize,
)
case n.RcvBufSize > maxBufSize:
return fmt.Errorf(
"so_rcvbuf: %s: must be less than or equal to %s",
errors.ErrOutOfRange,
maxBufSize,
)
default:
return nil return nil
}
} }
// toInternal converts n to the bindtodevice control configuration and network // toInternal converts n to the bindtodevice control configuration and network
// extension control configuration. // extension control configuration. n must be valid.
func (n *network) toInternal() (bc *bindtodevice.ControlConfig, nc *netext.ControlConfig) { func (n *network) toInternal() (bc *bindtodevice.ControlConfig, nc *netext.ControlConfig) {
bc = &bindtodevice.ControlConfig{ bc = &bindtodevice.ControlConfig{
// #nosec G115 -- Validated in [network.validate].
SndBufSize: int(n.SndBufSize.Bytes()), SndBufSize: int(n.SndBufSize.Bytes()),
// #nosec G115 -- Validated in [network.validate].
RcvBufSize: int(n.RcvBufSize.Bytes()), RcvBufSize: int(n.RcvBufSize.Bytes()),
} }
nc = &netext.ControlConfig{ nc = &netext.ControlConfig{
// #nosec G115 -- Validated in [network.validate].
SndBufSize: int(n.SndBufSize.Bytes()), SndBufSize: int(n.SndBufSize.Bytes()),
// #nosec G115 -- Validated in [network.validate].
RcvBufSize: int(n.RcvBufSize.Bytes()), RcvBufSize: int(n.RcvBufSize.Bytes()),
} }

View File

@ -0,0 +1,46 @@
// Package plugin defines types to support creating plugins for the AdGuard DNS
// server.
package plugin
import (
"github.com/AdguardTeam/AdGuardDNS/internal/dnscheck"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
)
// Registry is a plugin registry that stores custom implementations of AdGuard
// DNS entities. A nil Registry can be used safely: all its methods return zero
// values.
type Registry struct {
dnscheck dnscheck.Interface
mainMwMtrc metrics.MainMiddleware
}
// NewRegistry returns a new registry with the given custom implementations.
func NewRegistry(
dnsCk dnscheck.Interface,
mainMwMtrc metrics.MainMiddleware,
) (r *Registry) {
return &Registry{
dnscheck: dnsCk,
mainMwMtrc: mainMwMtrc,
}
}
// DNSCheck returns a custom implementation of the DNSCheck service, if any.
func (r *Registry) DNSCheck() (dnsCk dnscheck.Interface) {
if r == nil {
return nil
}
return r.dnscheck
}
// MainMiddlewareMetrics returns a custom implementation of the
// filtering-middleware metrics, if any.
func (r *Registry) MainMiddlewareMetrics() (mainMwMtrc metrics.MainMiddleware) {
if r == nil {
return nil
}
return r.mainMwMtrc
}

View File

@ -1,8 +1,10 @@
package cmd package cmd
import "fmt" import (
"fmt"
// Query log configuration "github.com/AdguardTeam/golibs/errors"
)
// queryLogConfig is the query log configuration. // queryLogConfig is the query log configuration.
type queryLogConfig struct { type queryLogConfig struct {
@ -10,13 +12,16 @@ type queryLogConfig struct {
File *queryLogFileConfig `yaml:"file"` File *queryLogFileConfig `yaml:"file"`
} }
// validate returns an error if the query log configuration is invalid. // type check
var _ validator = (*queryLogConfig)(nil)
// validate implements the [validator] interface for *queryLogConfig.
func (c *queryLogConfig) validate() (err error) { func (c *queryLogConfig) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return errNilConfig return errors.ErrNoValue
case c.File == nil: case c.File == nil:
return fmt.Errorf("file: %w", errNilConfig) return fmt.Errorf("file: %w", errors.ErrNoValue)
default: default:
return nil return nil
} }

View File

@ -2,25 +2,19 @@ package cmd
import ( import (
"cmp" "cmp"
"context"
"fmt" "fmt"
"net/url" "log/slog"
"github.com/AdguardTeam/AdGuardDNS/internal/agdservice"
"github.com/AdguardTeam/AdGuardDNS/internal/connlimiter" "github.com/AdguardTeam/AdGuardDNS/internal/connlimiter"
"github.com/AdguardTeam/AdGuardDNS/internal/consul"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/ratelimit" "github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/ratelimit"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics" "github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/service"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
"github.com/c2h5oh/datasize" "github.com/c2h5oh/datasize"
) )
// Rate Limiter Configuration
// rateLimitConfig is the configuration of the instance's rate limiting. // rateLimitConfig is the configuration of the instance's rate limiting.
type rateLimitConfig struct { type rateLimitConfig struct {
// AllowList is the allowlist of clients. // AllowList is the allowlist of clients.
@ -42,14 +36,14 @@ type rateLimitConfig struct {
// TCP is the configuration of TCP pipeline limiting. // TCP is the configuration of TCP pipeline limiting.
TCP *ratelimitTCPConfig `yaml:"tcp"` TCP *ratelimitTCPConfig `yaml:"tcp"`
// ResponseSizeEstimate is the size of the estimate of the size of one DNS // ResponseSizeEstimate is the estimate of the size of one DNS response for
// response for the purposes of rate limiting. Responses over this estimate // the purposes of rate limiting. Responses over this estimate are counted
// are counted as several responses. // as several responses.
ResponseSizeEstimate datasize.ByteSize `yaml:"response_size_estimate"` ResponseSizeEstimate datasize.ByteSize `yaml:"response_size_estimate"`
// BackoffCount helps with repeated offenders. It defines, how many times // BackoffCount helps with repeated offenders. It defines, how many times
// a client hits the rate limit before being held in the back off. // a client hits the rate limit before being held in the back off.
BackoffCount int `yaml:"backoff_count"` BackoffCount uint `yaml:"backoff_count"`
// BackoffDuration is how much a client that has hit the rate limit too // BackoffDuration is how much a client that has hit the rate limit too
// often stays in the back off. // often stays in the back off.
@ -76,17 +70,20 @@ type allowListConfig struct {
// addresses. // addresses.
type rateLimitOptions struct { type rateLimitOptions struct {
// RPS is the maximum number of requests per second. // RPS is the maximum number of requests per second.
RPS int `yaml:"rps"` RPS uint `yaml:"rps"`
// SubnetKeyLen is the length of the subnet prefix used to calculate // SubnetKeyLen is the length of the subnet prefix used to calculate
// rate limiter bucket keys. // rate limiter bucket keys.
SubnetKeyLen int `yaml:"subnet_key_len"` SubnetKeyLen int `yaml:"subnet_key_len"`
} }
// validate returns an error if rate limit options are invalid. // type check
var _ validator = (*rateLimitOptions)(nil)
// validate implements the [validator] interface for *rateLimitOptions.
func (o *rateLimitOptions) validate() (err error) { func (o *rateLimitOptions) validate() (err error) {
if o == nil { if o == nil {
return errNilConfig return errors.ErrNoValue
} }
return cmp.Or( return cmp.Or(
@ -96,11 +93,11 @@ func (o *rateLimitOptions) validate() (err error) {
} }
// toInternal converts c to the rate limiting configuration for the DNS server. // toInternal converts c to the rate limiting configuration for the DNS server.
// c is assumed to be valid. // c must be valid.
func (c *rateLimitConfig) toInternal(al ratelimit.Allowlist) (conf *ratelimit.BackoffConfig) { func (c *rateLimitConfig) toInternal(al ratelimit.Allowlist) (conf *ratelimit.BackoffConfig) {
return &ratelimit.BackoffConfig{ return &ratelimit.BackoffConfig{
Allowlist: al, Allowlist: al,
ResponseSizeEstimate: int(c.ResponseSizeEstimate.Bytes()), ResponseSizeEstimate: c.ResponseSizeEstimate,
Duration: c.BackoffDuration.Duration, Duration: c.BackoffDuration.Duration,
Period: c.BackoffPeriod.Duration, Period: c.BackoffPeriod.Duration,
IPv4RPS: c.IPv4.RPS, IPv4RPS: c.IPv4.RPS,
@ -112,13 +109,16 @@ func (c *rateLimitConfig) toInternal(al ratelimit.Allowlist) (conf *ratelimit.Ba
} }
} }
// validate returns an error if the safe rate limiting configuration is invalid. // type check
var _ validator = (*rateLimitConfig)(nil)
// validate implements the [validator] interface for *rateLimitConfig.
func (c *rateLimitConfig) validate() (err error) { func (c *rateLimitConfig) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return errNilConfig return errors.ErrNoValue
case c.Allowlist == nil: case c.Allowlist == nil:
return fmt.Errorf("allowlist: %w", errNilConfig) return fmt.Errorf("allowlist: %w", errors.ErrNoValue)
} }
return cmp.Or( return cmp.Or(
@ -135,43 +135,6 @@ func (c *rateLimitConfig) validate() (err error) {
) )
} }
// setupRateLimiter creates and returns a backoff rate limiter as well as starts
// and registers its refresher in the signal handler.
func setupRateLimiter(
ctx context.Context,
conf *rateLimitConfig,
consulAllowlist *url.URL,
sigHdlr *service.SignalHandler,
errColl errcoll.Interface,
) (rateLimiter *ratelimit.Backoff, connLimiter *connlimiter.Limiter, err error) {
allowSubnets := netutil.UnembedPrefixes(conf.Allowlist.List)
allowlist := ratelimit.NewDynamicAllowlist(allowSubnets, nil)
refresher := consul.NewAllowlistRefresher(allowlist, consulAllowlist, errColl)
err = refresher.Refresh(ctx)
if err != nil {
return nil, nil, fmt.Errorf("allowlist: initial refresh: %w", err)
}
refr := agdservice.NewRefreshWorker(&agdservice.RefreshWorkerConfig{
Context: ctxWithDefaultTimeout,
Refresher: refresher,
Name: "allowlist",
Interval: conf.Allowlist.RefreshIvl.Duration,
RefreshOnShutdown: false,
RandomizeStart: false,
})
err = refr.Start(ctx)
if err != nil {
return nil, nil, fmt.Errorf("starting allowlist refresher: %w", err)
}
sigHdlr.Add(refr)
return ratelimit.NewBackoff(conf.toInternal(allowlist)), conf.ConnectionLimit.toInternal(), nil
}
// connLimitConfig is the configuration structure for the stream-connection // connLimitConfig is the configuration structure for the stream-connection
// limiter. // limiter.
type connLimitConfig struct { type connLimitConfig struct {
@ -192,14 +155,14 @@ type connLimitConfig struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
} }
// toInternal converts c to the connection limiter to use. c is assumed to be // toInternal converts c to the connection limiter to use. c must be valid.
// valid. func (c *connLimitConfig) toInternal(logger *slog.Logger) (l *connlimiter.Limiter) {
func (c *connLimitConfig) toInternal() (l *connlimiter.Limiter) {
if !c.Enabled { if !c.Enabled {
return nil return nil
} }
l, err := connlimiter.New(&connlimiter.Config{ l, err := connlimiter.New(&connlimiter.Config{
Logger: logger.With(slogutil.KeyPrefix, "connlimiter"),
Stop: c.Stop, Stop: c.Stop,
Resume: c.Resume, Resume: c.Resume,
}) })
@ -213,15 +176,18 @@ func (c *connLimitConfig) toInternal() (l *connlimiter.Limiter) {
return l return l
} }
// validate returns an error if the connection limit configuration is invalid. // type check
var _ validator = (*connLimitConfig)(nil)
// validate implements the [validator] interface for *connLimitConfig.
func (c *connLimitConfig) validate() (err error) { func (c *connLimitConfig) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return errNilConfig return errors.ErrNoValue
case !c.Enabled: case !c.Enabled:
return nil return nil
case c.Stop == 0: case c.Stop == 0:
return newMustBePositiveError("stop", c.Stop) return newNotPositiveError("stop", c.Stop)
case c.Resume > c.Stop: case c.Resume > c.Stop:
return errors.Error("resume: must be less than or equal to stop") return errors.Error("resume: must be less than or equal to stop")
default: default:
@ -239,10 +205,13 @@ type ratelimitTCPConfig struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
} }
// validate returns an error if the config is invalid. // type check
var _ validator = (*ratelimitTCPConfig)(nil)
// validate implements the [validator] interface for *ratelimitTCPConfig.
func (c *ratelimitTCPConfig) validate() (err error) { func (c *ratelimitTCPConfig) validate() (err error) {
if c == nil { if c == nil {
return errNilConfig return errors.ErrNoValue
} }
return validatePositive("max_pipeline_count", c.MaxPipelineCount) return validatePositive("max_pipeline_count", c.MaxPipelineCount)
@ -258,10 +227,13 @@ type ratelimitQUICConfig struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
} }
// validate returns an error if the config is invalid. // type check
var _ validator = (*ratelimitQUICConfig)(nil)
// validate implements the [validator] interface for *ratelimitQUICConfig.
func (c *ratelimitQUICConfig) validate() (err error) { func (c *ratelimitQUICConfig) validate() (err error) {
if c == nil { if c == nil {
return errNilConfig return errors.ErrNoValue
} }
return validatePositive("max_streams_per_peer", c.MaxStreamsPerPeer) return validatePositive("max_streams_per_peer", c.MaxStreamsPerPeer)

View File

@ -1,25 +1,12 @@
package cmd package cmd
import ( import (
"context"
"fmt" "fmt"
"path/filepath"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/AdguardTeam/AdGuardDNS/internal/agdservice"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/hashprefix"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/netutil/urlutil"
"github.com/AdguardTeam/golibs/service"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
) )
// Safe-browsing and adult-blocking configuration
// safeBrowsingConfig is the configuration for one of the safe browsing filters. // safeBrowsingConfig is the configuration for one of the safe browsing filters.
type safeBrowsingConfig struct { type safeBrowsingConfig struct {
// BlockHost is the hostname with which to respond to any requests that // BlockHost is the hostname with which to respond to any requests that
@ -42,96 +29,25 @@ type safeBrowsingConfig struct {
RefreshTimeout timeutil.Duration `yaml:"refresh_timeout"` RefreshTimeout timeutil.Duration `yaml:"refresh_timeout"`
} }
// toInternal converts c to the safe browsing filter configuration for the // type check
// filter storage of the DNS server. c is assumed to be valid. var _ validator = (*safeBrowsingConfig)(nil)
func (c *safeBrowsingConfig) toInternal(
errColl errcoll.Interface,
cloner *dnsmsg.Cloner,
cacheManager agdcache.Manager,
id agd.FilterListID,
url *urlutil.URL,
cacheDir string,
maxSize uint64,
) (fltConf *hashprefix.FilterConfig) {
hashes, err := hashprefix.NewStorage("")
if err != nil {
// Don't expect errors here because we pass an empty string.
panic(err)
}
return &hashprefix.FilterConfig{ // validate implements the [validator] interface for *safeBrowsingConfig.
Cloner: cloner,
CacheManager: cacheManager,
Hashes: hashes,
URL: netutil.CloneURL(&url.URL),
ErrColl: errColl,
ID: id,
CachePath: filepath.Join(cacheDir, string(id)),
ReplacementHost: c.BlockHost,
Staleness: c.RefreshIvl.Duration,
RefreshTimeout: c.RefreshTimeout.Duration,
CacheTTL: c.CacheTTL.Duration,
CacheSize: c.CacheSize,
MaxSize: maxSize,
}
}
// validate returns an error if the safe browsing filter configuration is
// invalid.
func (c *safeBrowsingConfig) validate() (err error) { func (c *safeBrowsingConfig) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return errNilConfig return errors.ErrNoValue
case c.BlockHost == "": case c.BlockHost == "":
return errors.Error("no block_host") return fmt.Errorf("block_host: %w", errors.ErrEmptyValue)
case c.CacheSize <= 0: case c.CacheSize <= 0:
return newMustBePositiveError("cache_size", c.CacheSize) return newNotPositiveError("cache_size", c.CacheSize)
case c.CacheTTL.Duration <= 0: case c.CacheTTL.Duration <= 0:
return newMustBePositiveError("cache_ttl", c.CacheTTL) return newNotPositiveError("cache_ttl", c.CacheTTL)
case c.RefreshIvl.Duration <= 0: case c.RefreshIvl.Duration <= 0:
return newMustBePositiveError("refresh_interval", c.RefreshIvl) return newNotPositiveError("refresh_interval", c.RefreshIvl)
case c.RefreshTimeout.Duration <= 0: case c.RefreshTimeout.Duration <= 0:
return newMustBePositiveError("refresh_timeout", c.RefreshTimeout) return newNotPositiveError("refresh_timeout", c.RefreshTimeout)
default: default:
return nil return nil
} }
} }
// setupHashPrefixFilter creates and returns a hash-prefix filter as well as
// starts and registers its refresher in the signal handler.
func setupHashPrefixFilter(
ctx context.Context,
fltConf *hashprefix.FilterConfig,
sigHdlr *service.SignalHandler,
) (strg *hashprefix.Storage, flt *hashprefix.Filter, err error) {
flt, err = hashprefix.NewFilter(fltConf)
if err != nil {
return nil, nil, fmt.Errorf("creating hash prefix filter %s: %w", fltConf.ID, err)
}
err = flt.RefreshInitial(ctx)
if err != nil {
return nil, nil, fmt.Errorf("initializing hash prefix filter %s: %w", fltConf.ID, err)
}
refr := agdservice.NewRefreshWorker(&agdservice.RefreshWorkerConfig{
// Note that we also set the same timeout for the http.Client in
// [hashprefix.NewFilter].
Context: func() (ctx context.Context, cancel context.CancelFunc) {
return context.WithTimeout(context.Background(), fltConf.RefreshTimeout)
},
Refresher: flt,
Name: string(fltConf.ID),
Interval: fltConf.Staleness,
RefreshOnShutdown: false,
RandomizeStart: false,
})
err = refr.Start(ctx)
if err != nil {
return nil, nil, fmt.Errorf("starting hash prefix filter %s refresher: %w", fltConf.ID, err)
}
sigHdlr.Add(refr)
return fltConf.Hashes, flt, nil
}

View File

@ -12,7 +12,7 @@ import (
) )
// toInternal returns the configuration of DNS servers for a single server // toInternal returns the configuration of DNS servers for a single server
// group. srvs and other parts of the configuration are assumed to be valid. // group. srvs and other parts of the configuration must be valid.
func (srvs servers) toInternal( func (srvs servers) toInternal(
tlsConfig *agd.TLS, tlsConfig *agd.TLS,
btdMgr *bindtodevice.Manager, btdMgr *bindtodevice.Manager,
@ -46,6 +46,8 @@ func (srvs servers) toInternal(
case agd.ProtoDNS: case agd.ProtoDNS:
dnsSrv.TCPConf = tcpConf dnsSrv.TCPConf = tcpConf
dnsSrv.UDPConf = &agd.UDPConfig{ dnsSrv.UDPConf = &agd.UDPConfig{
// #nosec G115 -- The value has already been validated in
// [dnsConfig.validate].
MaxRespSize: uint16(dnsConf.MaxUDPResponseSize.Bytes()), MaxRespSize: uint16(dnsConf.MaxUDPResponseSize.Bytes()),
} }
case agd.ProtoDNSCrypt: case agd.ProtoDNSCrypt:
@ -70,7 +72,7 @@ func (srvs servers) toInternal(
tlsConf.VerifyConnection = metrics.TLSMetricsAfterHandshake( tlsConf.VerifyConnection = metrics.TLSMetricsAfterHandshake(
string(srv.Protocol), string(srv.Protocol),
srv.Name, srv.Name,
tlsConfig.DeviceIDWildcards, tlsConfig.DeviceDomains,
tlsConf.Certificates, tlsConf.Certificates,
) )
@ -156,7 +158,10 @@ func (p serverProto) toInternal() (sp agd.Protocol) {
} }
} }
// validate returns an error if the configuration is invalid. // type check
var _ validator = serverProto("")
// validate implements the [validator] interface for serverProto.
func (p serverProto) validate() (err error) { func (p serverProto) validate() (err error) {
switch p { switch p {
case srvProtoDNS, case srvProtoDNS,
@ -166,7 +171,7 @@ func (p serverProto) validate() (err error) {
srvProtoTLS: srvProtoTLS:
return nil return nil
default: default:
return fmt.Errorf("bad protocol: %q", p) return fmt.Errorf("protocol: %w: %q", errors.ErrBadEnumValue, p)
} }
} }
@ -237,13 +242,16 @@ func (s *server) bindData(
return bindData, nil return bindData, nil
} }
// validate returns an error if the configuration is invalid. // type check
var _ validator = (*server)(nil)
// validate implements the [validator] interface for *server.
func (s *server) validate() (err error) { func (s *server) validate() (err error) {
switch { switch {
case s == nil: case s == nil:
return errNilConfig return errors.ErrNoValue
case s.Name == "": case s.Name == "":
return errors.Error("no name") return fmt.Errorf("name: %w", errors.ErrEmptyValue)
} }
err = s.validateBindData() err = s.validateBindData()
@ -309,16 +317,18 @@ type serverBindInterface struct {
Subnets []netip.Prefix `yaml:"subnets"` Subnets []netip.Prefix `yaml:"subnets"`
} }
// validate returns an error if the network interface binding configuration is // type check
// invalid. var _ validator = (*serverBindInterface)(nil)
// validate implements the [validator] interface for *serverBindInterface.
func (c *serverBindInterface) validate() (err error) { func (c *serverBindInterface) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return errNilConfig return errors.ErrNoValue
case c.ID == "": case c.ID == "":
return errors.Error("no id") return fmt.Errorf("id: %w", errors.ErrEmptyValue)
case len(c.Subnets) == 0: case len(c.Subnets) == 0:
return errors.Error("no subnets") return fmt.Errorf("subnets: %w", errors.ErrEmptyValue)
default: default:
// Go on. // Go on.
} }

View File

@ -15,8 +15,7 @@ import (
type serverGroups []*serverGroup type serverGroups []*serverGroup
// toInternal returns the configuration for all server groups in the DNS // toInternal returns the configuration for all server groups in the DNS
// service. srvGrps and other parts of the configuration are assumed to be // service. srvGrps and other parts of the configuration must be valid.
// valid.
func (srvGrps serverGroups) toInternal( func (srvGrps serverGroups) toInternal(
messages *dnsmsg.Constructor, messages *dnsmsg.Constructor,
btdMgr *bindtodevice.Manager, btdMgr *bindtodevice.Manager,
@ -43,6 +42,7 @@ func (srvGrps serverGroups) toInternal(
TLS: tlsConf, TLS: tlsConf,
Name: agd.ServerGroupName(g.Name), Name: agd.ServerGroupName(g.Name),
FilteringGroup: fltGrpID, FilteringGroup: fltGrpID,
ProfilesEnabled: g.ProfilesEnabled,
} }
svcSrvGrps[i].Servers, err = g.Servers.toInternal(tlsConf, btdMgr, ratelimitConf, dnsConf) svcSrvGrps[i].Servers, err = g.Servers.toInternal(tlsConf, btdMgr, ratelimitConf, dnsConf)
@ -54,10 +54,13 @@ func (srvGrps serverGroups) toInternal(
return svcSrvGrps, nil return svcSrvGrps, nil
} }
// validate returns an error if these server groups are invalid. // type check
var _ validator = serverGroups(nil)
// validate implements the [validator] interface for serverGroups.
func (srvGrps serverGroups) validate() (err error) { func (srvGrps serverGroups) validate() (err error) {
if len(srvGrps) == 0 { if len(srvGrps) == 0 {
return errors.Error("no server groups") return errors.ErrEmptyValue
} }
names := container.NewMapSet[string]() names := container.NewMapSet[string]()
@ -98,17 +101,24 @@ type serverGroup struct {
// Servers are the settings for servers. // Servers are the settings for servers.
Servers servers `yaml:"servers"` Servers servers `yaml:"servers"`
// ProfilesEnabled, if true, enables recognition of user devices and
// profiles for this server group.
ProfilesEnabled bool `yaml:"profiles_enabled"`
} }
// validate returns an error if the configuration is invalid. // type check
var _ validator = (*serverGroup)(nil)
// validate implements the [validator] interface for *serverGroup.
func (g *serverGroup) validate() (err error) { func (g *serverGroup) validate() (err error) {
switch { switch {
case g == nil: case g == nil:
return errNilConfig return errors.ErrNoValue
case g.Name == "": case g.Name == "":
return errors.Error("no name") return fmt.Errorf("name: %w", errors.ErrEmptyValue)
case g.FilteringGroup == "": case g.FilteringGroup == "":
return errors.Error("no filtering_group") return fmt.Errorf("filtering_group: %w", errors.ErrEmptyValue)
} }
err = g.DDR.validate() err = g.DDR.validate()

119
internal/cmd/tickrot.go Normal file
View File

@ -0,0 +1,119 @@
package cmd
import (
"context"
"crypto/tls"
"fmt"
"log/slog"
"os"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdservice"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/golibs/logutil/slogutil"
)
// ticketRotator is a refresh worker that rereads and resets TLS session
// tickets. It should be initially refreshed before use.
type ticketRotator struct {
logger *slog.Logger
errColl errcoll.Interface
confs map[*tls.Config][]string
}
// newTicketRotator creates a new TLS session ticket rotator that rotates
// tickets for the TLS configurations of all servers in grps.
//
// grps must be valid.
func newTicketRotator(
logger *slog.Logger,
errColl errcoll.Interface,
grps []*agd.ServerGroup,
) (tr *ticketRotator) {
confs := map[*tls.Config][]string{}
for _, g := range grps {
t := g.TLS
if t == nil || len(t.SessionKeys) == 0 {
continue
}
for _, srv := range g.Servers {
if srv.TLS != nil {
confs[srv.TLS] = t.SessionKeys
}
}
}
return &ticketRotator{
logger: logger.With(slogutil.KeyPrefix, "tickrot"),
errColl: errColl,
confs: confs,
}
}
// sessTickLen is the length of a single TLS session ticket key in bytes.
//
// NOTE: Unlike Nginx, Go's crypto/tls doesn't use the random bytes from the
// session ticket keys as-is, but instead hashes these bytes and uses the first
// 48 bytes of the hashed data as the key name, the AES key, and the HMAC key.
const sessTickLen = 32
// type check
var _ agdservice.Refresher = (*ticketRotator)(nil)
// Refresh implements the [agdservice.Refresher] interface for *ticketRotator.
func (r *ticketRotator) Refresh(ctx context.Context) (err error) {
r.logger.DebugContext(ctx, "refresh started")
defer r.logger.DebugContext(ctx, "refresh finished")
defer func() {
if err != nil {
errcoll.Collect(ctx, r.errColl, r.logger, "ticket rotation failed", err)
}
}()
for conf, files := range r.confs {
keys := make([][sessTickLen]byte, 0, len(files))
for _, fileName := range files {
var key [sessTickLen]byte
key, err = readSessionTicketKey(fileName)
if err != nil {
metrics.TLSSessionTicketsRotateStatus.Set(0)
return fmt.Errorf("session ticket for srv %s: %w", conf.ServerName, err)
}
keys = append(keys, key)
}
if len(keys) == 0 {
return fmt.Errorf("no session tickets for srv %s in %q", conf.ServerName, files)
}
conf.SetSessionTicketKeys(keys)
}
metrics.TLSSessionTicketsRotateStatus.Set(1)
metrics.TLSSessionTicketsRotateTime.SetToCurrentTime()
return nil
}
// readSessionTicketKey reads a single TLS session ticket key from a file.
func readSessionTicketKey(fn string) (key [sessTickLen]byte, err error) {
// #nosec G304 -- Trust the file paths that are given to us in the
// configuration file.
b, err := os.ReadFile(fn)
if err != nil {
return key, fmt.Errorf("reading session ticket: %w", err)
}
if len(b) < sessTickLen {
return key, fmt.Errorf("session ticket in %s: bad len %d, want %d", fn, len(b), sessTickLen)
}
return [sessTickLen]byte(b), nil
}

View File

@ -1,29 +1,20 @@
package cmd package cmd
import ( import (
"context"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd" "github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdservice"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics" "github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/golibs/container" "github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/service"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
) )
// TLS Configuration And Utilities
// tlsConfig are the TLS settings of a DNS server, if any. // tlsConfig are the TLS settings of a DNS server, if any.
type tlsConfig struct { type tlsConfig struct {
// Certificates are TLS certificates for this server. // Certificates are TLS certificates for this server.
@ -38,11 +29,14 @@ type tlsConfig struct {
// //
// TODO(a.garipov): Validate the actual DNS Names in the certificates // TODO(a.garipov): Validate the actual DNS Names in the certificates
// against these? // against these?
//
// TODO(a.garipov): Replace with just domain names, since the "*." isn't
// really necessary at all.
DeviceIDWildcards []string `yaml:"device_id_wildcards"` DeviceIDWildcards []string `yaml:"device_id_wildcards"`
} }
// toInternal converts c to the TLS configuration for a DNS server. c is // toInternal converts c to the TLS configuration for a DNS server. c must be
// assumed to be valid. // valid.
func (c *tlsConfig) toInternal() (conf *agd.TLS, err error) { func (c *tlsConfig) toInternal() (conf *agd.TLS, err error) {
if c == nil { if c == nil {
return nil, nil return nil, nil
@ -53,11 +47,14 @@ func (c *tlsConfig) toInternal() (conf *agd.TLS, err error) {
return nil, fmt.Errorf("certificates: %w", err) return nil, fmt.Errorf("certificates: %w", err)
} }
var deviceDomains []string
for _, w := range c.DeviceIDWildcards {
deviceDomains = append(deviceDomains, strings.TrimPrefix(w, "*."))
}
return &agd.TLS{ return &agd.TLS{
Conf: tlsConf, Conf: tlsConf,
// TODO(e.burkov): Consider trimming the asterisk since the values are DeviceDomains: deviceDomains,
// only used in this way.
DeviceIDWildcards: slices.Clone(c.DeviceIDWildcards),
SessionKeys: c.SessionKeys, SessionKeys: c.SessionKeys,
}, nil }, nil
} }
@ -78,7 +75,7 @@ func (c *tlsConfig) validate(needsTLS bool) (err error) {
} }
if len(c.Certificates) == 0 { if len(c.Certificates) == 0 {
return errors.Error("no certificates") return fmt.Errorf("certificates: %w", errors.ErrEmptyValue)
} }
err = c.Certificates.validate() err = c.Certificates.validate()
@ -125,8 +122,7 @@ type tlsConfigCert struct {
// no nil items. // no nil items.
type tlsConfigCerts []*tlsConfigCert type tlsConfigCerts []*tlsConfigCert
// toInternal converts certs to a TLS configuration. certs are assumed to be // toInternal converts certs to a TLS configuration. certs must be valid.
// valid.
func (certs tlsConfigCerts) toInternal() (conf *tls.Config, err error) { func (certs tlsConfigCerts) toInternal() (conf *tls.Config, err error) {
if len(certs) == 0 { if len(certs) == 0 {
return nil, nil return nil, nil
@ -166,126 +162,25 @@ func (certs tlsConfigCerts) toInternal() (conf *tls.Config, err error) {
}, nil }, nil
} }
// validate returns an error if the certificates are invalid. // type check
var _ validator = tlsConfigCerts(nil)
// validate implements the [validator] interface for tlsConfigCerts.
func (certs tlsConfigCerts) validate() (err error) { func (certs tlsConfigCerts) validate() (err error) {
for i, c := range certs { for i, c := range certs {
switch { switch {
case c == nil: case c == nil:
return fmt.Errorf("at index %d: no certificate object", i) return fmt.Errorf("at index %d: %w", i, errors.ErrNoValue)
case c.Certificate == "": case c.Certificate == "":
return fmt.Errorf("at index %d: no certificate", i) return fmt.Errorf("at index %d: certificate: %w", i, errors.ErrEmptyValue)
case c.Key == "": case c.Key == "":
return fmt.Errorf("at index %d: no key", i) return fmt.Errorf("at index %d: key: %w", i, errors.ErrEmptyValue)
} }
} }
return nil return nil
} }
// TLS Session Ticket Key Rotator
// ticketRotator is a refresh worker that rereads and resets TLS session
// tickets. It should be initially refreshed before use.
type ticketRotator struct {
errColl errcoll.Interface
confs map[*tls.Config][]string
}
// newTicketRotator creates a new TLS session ticket rotator that rotates
// tickets for the TLS configurations of all servers in grps.
//
// grps is assumed to be valid.
func newTicketRotator(
errColl errcoll.Interface,
grps []*agd.ServerGroup,
) (tr *ticketRotator) {
confs := map[*tls.Config][]string{}
for _, g := range grps {
t := g.TLS
if t == nil || len(t.SessionKeys) == 0 {
continue
}
for _, srv := range g.Servers {
if srv.TLS != nil {
confs[srv.TLS] = t.SessionKeys
}
}
}
return &ticketRotator{
errColl: errColl,
confs: confs,
}
}
// sessTickLen is the length of a single TLS session ticket key in bytes.
//
// NOTE: Unlike Nginx, Go's crypto/tls doesn't use the random bytes from the
// session ticket keys as-is, but instead hashes these bytes and uses the first
// 48 bytes of the hashed data as the key name, the AES key, and the HMAC key.
const sessTickLen = 32
// type check
var _ agdservice.Refresher = (*ticketRotator)(nil)
// Refresh implements the [agdservice.Refresher] interface for *ticketRotator.
func (r *ticketRotator) Refresh(ctx context.Context) (err error) {
// TODO(a.garipov): Use slog.
log.Debug("tickrot_refresh: started")
defer log.Debug("tickrot_refresh: finished")
defer func() {
if err != nil {
errcoll.Collectf(ctx, r.errColl, "tickrot_refresh: %w", err)
}
}()
for conf, files := range r.confs {
keys := make([][sessTickLen]byte, 0, len(files))
for _, fileName := range files {
var key [sessTickLen]byte
key, err = readSessionTicketKey(fileName)
if err != nil {
metrics.TLSSessionTicketsRotateStatus.Set(0)
return fmt.Errorf("session ticket for srv %s: %w", conf.ServerName, err)
}
keys = append(keys, key)
}
if len(keys) == 0 {
return fmt.Errorf("no session tickets for srv %s in %q", conf.ServerName, files)
}
conf.SetSessionTicketKeys(keys)
}
metrics.TLSSessionTicketsRotateStatus.Set(1)
metrics.TLSSessionTicketsRotateTime.SetToCurrentTime()
return nil
}
// readSessionTicketKey reads a single TLS session ticket key from a file.
func readSessionTicketKey(fn string) (key [sessTickLen]byte, err error) {
// #nosec G304 -- Trust the file paths that are given to us in the
// configuration file.
b, err := os.ReadFile(fn)
if err != nil {
return key, fmt.Errorf("reading session ticket: %w", err)
}
if len(b) < sessTickLen {
return key, fmt.Errorf("session ticket in %s: bad len %d, want %d", fn, len(b), sessTickLen)
}
return [sessTickLen]byte(b), nil
}
// enableTLSKeyLogging enables TLS key logging (use for debug purposes only). // enableTLSKeyLogging enables TLS key logging (use for debug purposes only).
func enableTLSKeyLogging(grps []*agd.ServerGroup, keyLogFileName string) (err error) { func enableTLSKeyLogging(grps []*agd.ServerGroup, keyLogFileName string) (err error) {
path := filepath.Clean(keyLogFileName) path := filepath.Clean(keyLogFileName)
@ -306,36 +201,3 @@ func enableTLSKeyLogging(grps []*agd.ServerGroup, keyLogFileName string) (err er
return nil return nil
} }
// setupTicketRotator creates and returns a ticket rotator as well as starts and
// registers its refresher in the signal handler.
func setupTicketRotator(
srvGrps []*agd.ServerGroup,
sigHdlr *service.SignalHandler,
errColl errcoll.Interface,
) (err error) {
tickRot := newTicketRotator(errColl, srvGrps)
err = tickRot.Refresh(context.Background())
if err != nil {
return fmt.Errorf("setting up ticket rotator: initial session ticket refresh: %w", err)
}
refr := agdservice.NewRefreshWorker(&agdservice.RefreshWorkerConfig{
Context: ctxWithDefaultTimeout,
Refresher: tickRot,
Name: "tickrot",
// TODO(ameshkov): Consider making configurable.
Interval: 1 * time.Minute,
RefreshOnShutdown: false,
RandomizeStart: false,
})
err = refr.Start(context.Background())
if err != nil {
return fmt.Errorf("starting ticket rotator refresh: %w", err)
}
sigHdlr.Add(refr)
return nil
}

View File

@ -2,8 +2,8 @@ package cmd
import ( import (
"cmp" "cmp"
"context"
"fmt" "fmt"
"log/slog"
"net/netip" "net/netip"
"net/url" "net/url"
"strings" "strings"
@ -13,8 +13,9 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/forward" "github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/forward"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/prometheus" "github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/prometheus"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll" "github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/service" "github.com/AdguardTeam/golibs/service"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
) )
@ -33,15 +34,14 @@ type upstreamConfig struct {
} }
// toInternal converts c to the data storage configuration for the DNS server. // toInternal converts c to the data storage configuration for the DNS server.
// c is assumed to be valid. // c must be valid.
func (c *upstreamConfig) toInternal() (fwdConf *forward.HandlerConfig) { func (c *upstreamConfig) toInternal(logger *slog.Logger) (fwdConf *forward.HandlerConfig) {
upstreams := c.Servers upstreams := c.Servers
fallbacks := c.Fallback.Servers fallbacks := c.Fallback.Servers
upsConfs := toUpstreamConfigs(upstreams) upsConfs := toUpstreamConfigs(upstreams)
fallbackConfs := toUpstreamConfigs(fallbacks) fallbackConfs := toUpstreamConfigs(fallbacks)
metricsListener := prometheus.NewForwardMetricsListener(metrics.Namespace(), len(upstreams)+len(fallbacks))
metricsListener := prometheus.NewForwardMetricsListener(len(upstreams) + len(fallbacks))
var hcInit time.Duration var hcInit time.Duration
if c.Healthcheck.Enabled { if c.Healthcheck.Enabled {
@ -49,6 +49,7 @@ func (c *upstreamConfig) toInternal() (fwdConf *forward.HandlerConfig) {
} }
fwdConf = &forward.HandlerConfig{ fwdConf = &forward.HandlerConfig{
Logger: logger.With(slogutil.KeyPrefix, "forward"),
MetricsListener: metricsListener, MetricsListener: metricsListener,
HealthcheckDomainTmpl: c.Healthcheck.DomainTmpl, HealthcheckDomainTmpl: c.Healthcheck.DomainTmpl,
UpstreamsAddresses: upsConfs, UpstreamsAddresses: upsConfs,
@ -60,13 +61,16 @@ func (c *upstreamConfig) toInternal() (fwdConf *forward.HandlerConfig) {
return fwdConf return fwdConf
} }
// validate returns an error if the upstream configuration is invalid. // type check
var _ validator = (*upstreamConfig)(nil)
// validate implements the [validator] interface for *upstreamConfig.
func (c *upstreamConfig) validate() (err error) { func (c *upstreamConfig) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return errNilConfig return errors.ErrNoValue
case len(c.Servers) == 0: case len(c.Servers) == 0:
return errors.Error("no servers") return fmt.Errorf("servers: %w", errors.ErrEmptyValue)
} }
for i, s := range c.Servers { for i, s := range c.Servers {
@ -134,30 +138,33 @@ type upstreamHealthcheckConfig struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
} }
// validate returns an error if the upstream healthcheck configuration is // type check
// invalid. var _ validator = (*upstreamHealthcheckConfig)(nil)
// validate implements the [validator] interface for *upstreamHealthcheckConfig.
func (c *upstreamHealthcheckConfig) validate() (err error) { func (c *upstreamHealthcheckConfig) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return errNilConfig return errors.ErrNoValue
case !c.Enabled: case !c.Enabled:
return nil return nil
case c.DomainTmpl == "": case c.DomainTmpl == "":
return errors.Error("no domain_tmpl") return fmt.Errorf("domain_template: %w", errors.ErrEmptyValue)
case c.Interval.Duration <= 0: case c.Interval.Duration <= 0:
return newMustBePositiveError("interval", c.Interval) return newNotPositiveError("interval", c.Interval)
case c.Timeout.Duration <= 0: case c.Timeout.Duration <= 0:
return newMustBePositiveError("timeout", c.Timeout) return newNotPositiveError("timeout", c.Timeout)
case c.BackoffDuration.Duration <= 0: case c.BackoffDuration.Duration <= 0:
return newMustBePositiveError("backoff_duration", c.BackoffDuration) return newNotPositiveError("backoff_duration", c.BackoffDuration)
} }
return nil return nil
} }
// newUpstreamHealthcheck returns refresher worker service that performs // newUpstreamHealthcheck returns refresher worker service that performs
// upstream healthchecks. conf is assumed to be valid. // upstream healthchecks. conf must be valid.
func newUpstreamHealthcheck( func newUpstreamHealthcheck(
logger *slog.Logger,
handler *forward.Handler, handler *forward.Handler,
conf *upstreamConfig, conf *upstreamConfig,
errColl errcoll.Interface, errColl errcoll.Interface,
@ -166,20 +173,12 @@ func newUpstreamHealthcheck(
return service.Empty{} return service.Empty{}
} }
const prefix = "upstream_healthcheck_refresh"
refrLogger := logger.With(slogutil.KeyPrefix, prefix)
return agdservice.NewRefreshWorker(&agdservice.RefreshWorkerConfig{ return agdservice.NewRefreshWorker(&agdservice.RefreshWorkerConfig{
Context: func() (ctx context.Context, cancel context.CancelFunc) { Context: newCtxWithTimeoutCons(conf.Healthcheck.Timeout.Duration),
return context.WithTimeout( Refresher: agdservice.NewRefresherWithErrColl(handler, refrLogger, errColl, prefix),
context.Background(), Logger: refrLogger,
conf.Healthcheck.Timeout.Duration,
)
},
Refresher: agdservice.NewRefresherWithErrColl(
handler,
log.Debug,
errColl,
"upstream_healthcheck_refresh",
),
Name: "upstream healthcheck",
Interval: conf.Healthcheck.Interval.Duration, Interval: conf.Healthcheck.Interval.Duration,
RefreshOnShutdown: false, RefreshOnShutdown: false,
RandomizeStart: false, RandomizeStart: false,
@ -194,13 +193,16 @@ type upstreamFallbackConfig struct {
Servers []*upstreamServerConfig `yaml:"servers"` Servers []*upstreamServerConfig `yaml:"servers"`
} }
// validate returns an error if the upstream fallback configuration is invalid. // type check
var _ validator = (*upstreamFallbackConfig)(nil)
// validate implements the [validator] interface for *upstreamFallbackConfig.
func (c *upstreamFallbackConfig) validate() (err error) { func (c *upstreamFallbackConfig) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return errNilConfig return errors.ErrNoValue
case len(c.Servers) == 0: case len(c.Servers) == 0:
return errors.Error("no servers") return fmt.Errorf("servers: %w", errors.ErrEmptyValue)
} }
for i, s := range c.Servers { for i, s := range c.Servers {
@ -222,13 +224,16 @@ type upstreamServerConfig struct {
Timeout timeutil.Duration `yaml:"timeout"` Timeout timeutil.Duration `yaml:"timeout"`
} }
// validate returns an error if the upstream server configuration is invalid. // type check
var _ validator = (*upstreamServerConfig)(nil)
// validate implements the [validator] interface for *upstreamServerConfig.
func (c *upstreamServerConfig) validate() (err error) { func (c *upstreamServerConfig) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return errNilConfig return errors.ErrNoValue
case c.Timeout.Duration <= 0: case c.Timeout.Duration <= 0:
return newMustBePositiveError("timeout", c.Timeout) return newNotPositiveError("timeout", c.Timeout)
} }
_, _, err = splitUpstreamURL(c.Address) _, _, err = splitUpstreamURL(c.Address)

View File

@ -7,13 +7,11 @@ import (
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
) )
// Validation utilities
// validatePositive returns an error if v is not a positive number. prop is the // validatePositive returns an error if v is not a positive number. prop is the
// name of the property being checked, used for error messages. // name of the property being checked, used for error messages.
func validatePositive[T numberOrDuration](prop string, v T) (err error) { func validatePositive[T numberOrDuration](prop string, v T) (err error) {
if d, ok := any(v).(timeutil.Duration); ok && d.Duration <= 0 { if d, ok := any(v).(timeutil.Duration); ok && d.Duration <= 0 {
return newMustBePositiveError(prop, v) return newNotPositiveError(prop, v)
} }
return nil return nil

View File

@ -3,12 +3,15 @@ package cmd
import ( import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"maps"
"net/http" "net/http"
"net/netip" "net/netip"
"net/textproto" "net/textproto"
"os" "os"
"path" "path"
"slices"
"github.com/AdguardTeam/AdGuardDNS/internal/dnscheck"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll" "github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/websvc" "github.com/AdguardTeam/AdGuardDNS/internal/websvc"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
@ -18,8 +21,6 @@ import (
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
) )
// Web Service Configuration
// webConfig contains configuration for the AdGuard DNS web service. // webConfig contains configuration for the AdGuard DNS web service.
type webConfig struct { type webConfig struct {
// LinkedIP is the optional linked IP web server. // LinkedIP is the optional linked IP web server.
@ -58,11 +59,11 @@ type webConfig struct {
Timeout timeutil.Duration `yaml:"timeout"` Timeout timeutil.Duration `yaml:"timeout"`
} }
// toInternal converts c to the AdGuardDNS web service configuration. c is // toInternal converts c to the AdGuardDNS web service configuration. c must be
// assumed to be valid. // valid.
func (c *webConfig) toInternal( func (c *webConfig) toInternal(
envs *environments, envs *environment,
dnsCk http.Handler, dnsCk dnscheck.Interface,
errColl errcoll.Interface, errColl errcoll.Interface,
) (conf *websvc.Config, err error) { ) (conf *websvc.Config, err error) {
if c == nil { if c == nil {
@ -70,11 +71,14 @@ func (c *webConfig) toInternal(
} }
conf = &websvc.Config{ conf = &websvc.Config{
DNSCheck: dnsCk,
ErrColl: errColl, ErrColl: errColl,
Timeout: c.Timeout.Duration, Timeout: c.Timeout.Duration,
} }
if dnsCkHdlr, ok := dnsCk.(http.Handler); ok {
conf.DNSCheck = dnsCkHdlr
}
if c.RootRedirectURL != nil { if c.RootRedirectURL != nil {
conf.RootRedirectURL = netutil.CloneURL(&c.RootRedirectURL.URL) conf.RootRedirectURL = netutil.CloneURL(&c.RootRedirectURL.URL)
} }
@ -84,24 +88,29 @@ func (c *webConfig) toInternal(
return nil, fmt.Errorf("converting linked_ip: %w", err) return nil, fmt.Errorf("converting linked_ip: %w", err)
} }
conf.AdultBlocking, err = c.AdultBlocking.toInternal() blockPages := []struct {
if err != nil { webConfPtr **websvc.BlockPageServerConfig
return nil, fmt.Errorf("converting adult_blocking: %w", err) conf *blockPageServer
} name string
}{{
webConfPtr: &conf.AdultBlocking,
conf: c.AdultBlocking,
name: "adult_blocking",
}, {
webConfPtr: &conf.GeneralBlocking,
conf: c.GeneralBlocking,
name: "general_blocking",
}, {
webConfPtr: &conf.SafeBrowsing,
conf: c.SafeBrowsing,
name: "safe_browsing",
}}
conf.GeneralBlocking, err = c.GeneralBlocking.toInternal() for _, bp := range blockPages {
*bp.webConfPtr, err = bp.conf.toInternal()
if err != nil { if err != nil {
return nil, fmt.Errorf("converting general_blocking: %w", err) return nil, fmt.Errorf("%s: %w", bp.name, err)
} }
conf.SafeBrowsing, err = c.SafeBrowsing.toInternal()
if err != nil {
return nil, fmt.Errorf("converting safe_browsing: %w", err)
}
conf.StaticContent, err = c.StaticContent.toInternal()
if err != nil {
return nil, fmt.Errorf("converting static_content: %w", err)
} }
conf.Error404, conf.Error500, err = c.readErrorPages() conf.Error404, conf.Error500, err = c.readErrorPages()
@ -115,6 +124,12 @@ func (c *webConfig) toInternal(
return nil, fmt.Errorf("converting non_doh_bind: %w", err) return nil, fmt.Errorf("converting non_doh_bind: %w", err)
} }
err = c.setStaticContent(envs, conf)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
return conf, nil return conf, nil
} }
@ -138,13 +153,32 @@ func (c *webConfig) readErrorPages() (error404, error500 []byte, err error) {
return error404, error500, nil return error404, error500, nil
} }
// validate returns an error if the web service configuration is invalid. // setStaticContent sets the static-content handler in conf using envs.
func (c *webConfig) setStaticContent(envs *environment, conf *websvc.Config) (err error) {
if envs.WebStaticDirEnabled {
conf.StaticContent = http.FileServer(http.Dir(envs.WebStaticDir))
return nil
}
conf.StaticContent, err = c.StaticContent.toInternal()
if err != nil {
return fmt.Errorf("converting static_content: %w", err)
}
return nil
}
// type check
var _ validator = (*webConfig)(nil)
// validate implements the [validator] interface for *webConfig.
func (c *webConfig) validate() (err error) { func (c *webConfig) validate() (err error) {
switch { switch {
case c == nil: case c == nil:
return nil return nil
case c.Timeout.Duration <= 0: case c.Timeout.Duration <= 0:
return newMustBePositiveError("timeout", c.Timeout) return newNotPositiveError("timeout", c.Timeout)
default: default:
// Go on. // Go on.
} }
@ -189,8 +223,7 @@ type linkedIPServer struct {
Bind bindData `yaml:"bind"` Bind bindData `yaml:"bind"`
} }
// toInternal converts s to a linkedIP server configuration. s is assumed to be // toInternal converts s to a linkedIP server configuration. s must be valid.
// valid.
func (s *linkedIPServer) toInternal( func (s *linkedIPServer) toInternal(
targetURL *urlutil.URL, targetURL *urlutil.URL,
) (srv *websvc.LinkedIPServer, err error) { ) (srv *websvc.LinkedIPServer, err error) {
@ -213,13 +246,16 @@ func (s *linkedIPServer) toInternal(
return srv, nil return srv, nil
} }
// validate returns an error if the linked IP server configuration is invalid. // type check
var _ validator = (*linkedIPServer)(nil)
// validate implements the [validator] interface for *linkedIPServer.
func (s *linkedIPServer) validate() (err error) { func (s *linkedIPServer) validate() (err error) {
switch { switch {
case s == nil: case s == nil:
return nil return nil
case len(s.Bind) == 0: case len(s.Bind) == 0:
return errors.Error("no bind") return fmt.Errorf("bind: %w", errors.ErrEmptyValue)
default: default:
// Go on. // Go on.
} }
@ -235,7 +271,7 @@ func (s *linkedIPServer) validate() (err error) {
// blockPageServer is the safe browsing or adult blocking block page web servers // blockPageServer is the safe browsing or adult blocking block page web servers
// configuration. // configuration.
type blockPageServer struct { type blockPageServer struct {
// BlockPage is the content of the HTML block page. // BlockPage is the path to file with HTML block page content.
BlockPage string `yaml:"block_page"` BlockPage string `yaml:"block_page"`
// Bind are the bind addresses and optional TLS configuration for the block // Bind are the bind addresses and optional TLS configuration for the block
@ -243,37 +279,36 @@ type blockPageServer struct {
Bind bindData `yaml:"bind"` Bind bindData `yaml:"bind"`
} }
// toInternal converts s to a block page server configuration. s is assumed to // toInternal converts s to a block page server configuration. s must be valid.
// be valid. func (s *blockPageServer) toInternal() (conf *websvc.BlockPageServerConfig, err error) {
func (s *blockPageServer) toInternal() (srv *websvc.BlockPageServer, err error) {
if s == nil { if s == nil {
return nil, nil return nil, nil
} }
srv = &websvc.BlockPageServer{} conf = &websvc.BlockPageServerConfig{
ContentFilePath: s.BlockPage,
srv.Content, err = os.ReadFile(s.BlockPage)
if err != nil {
return nil, fmt.Errorf("reading block_page file: %w", err)
} }
srv.Bind, err = s.Bind.toInternal() conf.Bind, err = s.Bind.toInternal()
if err != nil { if err != nil {
return nil, fmt.Errorf("converting bind: %w", err) return nil, fmt.Errorf("converting bind: %w", err)
} }
return srv, nil return conf, nil
} }
// validate returns an error if the block page server configuration is invalid. // type check
var _ validator = (*blockPageServer)(nil)
// validate implements the [validator] interface for *blockPageServer.
func (s *blockPageServer) validate() (err error) { func (s *blockPageServer) validate() (err error) {
switch { switch {
case s == nil: case s == nil:
return nil return nil
case s.BlockPage == "": case s.BlockPage == "":
return errors.Error("no block_page") return fmt.Errorf("block_page: %w", errors.ErrEmptyValue)
case len(s.Bind) == 0: case len(s.Bind) == 0:
return errors.Error("no bind") return fmt.Errorf("bind: %w", errors.ErrEmptyValue)
default: default:
// Go on. // Go on.
} }
@ -289,8 +324,8 @@ func (s *blockPageServer) validate() (err error) {
// bindData are the data for binding HTTP servers to addresses. // bindData are the data for binding HTTP servers to addresses.
type bindData []*bindItem type bindData []*bindItem
// toInternal converts bd to bind data for the AdGuard DNS web service. bd is // toInternal converts bd to bind data for the AdGuard DNS web service. bd must
// assumed to be valid. // be valid.
func (bd bindData) toInternal() (data []*websvc.BindData, err error) { func (bd bindData) toInternal() (data []*websvc.BindData, err error) {
data = make([]*websvc.BindData, len(bd)) data = make([]*websvc.BindData, len(bd))
@ -304,7 +339,10 @@ func (bd bindData) toInternal() (data []*websvc.BindData, err error) {
return data, nil return data, nil
} }
// validate returns an error if the bind data are invalid. // type check
var _ validator = bindData(nil)
// validate implements the [validator] interface for bindData.
func (bd bindData) validate() (err error) { func (bd bindData) validate() (err error) {
if len(bd) == 0 { if len(bd) == 0 {
return nil return nil
@ -329,8 +367,8 @@ type bindItem struct {
Certificates tlsConfigCerts `yaml:"certificates"` Certificates tlsConfigCerts `yaml:"certificates"`
} }
// toInternal converts i to bind data for the AdGuard DNS web service. i is // toInternal converts i to bind data for the AdGuard DNS web service. i must
// assumed to be valid. // be valid.
func (i *bindItem) toInternal() (data *websvc.BindData, err error) { func (i *bindItem) toInternal() (data *websvc.BindData, err error) {
tlsConf, err := i.Certificates.toInternal() tlsConf, err := i.Certificates.toInternal()
if err != nil { if err != nil {
@ -343,13 +381,16 @@ func (i *bindItem) toInternal() (data *websvc.BindData, err error) {
}, nil }, nil
} }
// validate returns an error if the bind data are invalid. // type check
var _ validator = (*bindItem)(nil)
// validate implements the [validator] interface for *bindItem.
func (i *bindItem) validate() (err error) { func (i *bindItem) validate() (err error) {
switch { switch {
case i == nil: case i == nil:
return errors.Error("no bind data") return errors.ErrNoValue
case i.Address == netip.AddrPort{}: case i.Address == netip.AddrPort{}:
return errors.Error("no address") return fmt.Errorf("address: %w", errors.ErrEmptyValue)
default: default:
// Go on. // Go on.
} }
@ -367,7 +408,7 @@ func (i *bindItem) validate() (err error) {
type staticContent map[string]*staticFile type staticContent map[string]*staticFile
// toInternal converts sc to a static content mapping for the AdGuard DNS web // toInternal converts sc to a static content mapping for the AdGuard DNS web
// service. sc is assumed to be valid. // service. sc must be valid.
func (sc staticContent) toInternal() (fs websvc.StaticContent, err error) { func (sc staticContent) toInternal() (fs websvc.StaticContent, err error) {
if len(sc) == 0 { if len(sc) == 0 {
return nil, nil return nil, nil
@ -384,20 +425,21 @@ func (sc staticContent) toInternal() (fs websvc.StaticContent, err error) {
return fs, nil return fs, nil
} }
// validate returns an error if the static content mapping is invalid. // type check
var _ validator = staticContent(nil)
// validate implements the [validator] interface for staticContent.
func (sc staticContent) validate() (err error) { func (sc staticContent) validate() (err error) {
if len(sc) == 0 { if len(sc) == 0 {
return nil return nil
} }
// TODO(a.garipov): Sort the keys to make the order of validations for _, p := range slices.Sorted(maps.Keys(sc)) {
// predictable.
for p, f := range sc {
if !path.IsAbs(p) { if !path.IsAbs(p) {
return fmt.Errorf("path %q: not absolute", p) return fmt.Errorf("path %q: not absolute", p)
} }
err = f.validate() err = sc[p].validate()
if err != nil { if err != nil {
return fmt.Errorf("path %q: %w", p, err) return fmt.Errorf("path %q: %w", p, err)
} }
@ -415,8 +457,8 @@ type staticFile struct {
Content string `yaml:"content"` Content string `yaml:"content"`
} }
// toInternal converts f to a static file for the AdGuard DNS web service. f is // toInternal converts f to a static file for the AdGuard DNS web service. f
// assumed to be valid. // must be valid.
func (f *staticFile) toInternal() (file *websvc.StaticFile, err error) { func (f *staticFile) toInternal() (file *websvc.StaticFile, err error) {
file = &websvc.StaticFile{ file = &websvc.StaticFile{
Headers: http.Header{}, Headers: http.Header{},
@ -441,10 +483,13 @@ func (f *staticFile) toInternal() (file *websvc.StaticFile, err error) {
return file, nil return file, nil
} }
// validate returns an error if the static content file is invalid. // type check
var _ validator = (*staticFile)(nil)
// validate implements the [validator] interface for *staticFile.
func (f *staticFile) validate() (err error) { func (f *staticFile) validate() (err error) {
if f == nil { if f == nil {
return errors.Error("no file") return errors.ErrNoValue
} }
return nil return nil

View File

@ -1,14 +1,17 @@
package connlimiter package connlimiter
import ( import (
"context"
"log/slog"
"net" "net"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver" "github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics" "github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/AdGuardDNS/internal/optlog" "github.com/AdguardTeam/AdGuardDNS/internal/optslog"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/timeutil"
) )
// limitConn is a wrapper for a stream connection that decreases the counter // limitConn is a wrapper for a stream connection that decreases the counter
@ -18,6 +21,7 @@ import (
type limitConn struct { type limitConn struct {
net.Conn net.Conn
logger *slog.Logger
serverInfo *dnsserver.ServerInfo serverInfo *dnsserver.ServerInfo
decrement func() decrement func()
start time.Time start time.Time
@ -36,14 +40,22 @@ func (c *limitConn) Close() (err error) {
// metrics later. // metrics later.
err = c.Conn.Close() err = c.Conn.Close()
connLife := time.Since(c.start).Seconds() ctx := context.Background()
name := c.serverInfo.Name connLife := time.Since(c.start)
optlog.Debug3("connlimiter: %s: closed conn from %s after %fs", name, c.RemoteAddr(), connLife) optslog.Debug2(
ctx,
c.logger,
"closed conn",
"raddr", c.RemoteAddr(),
"conn_life", timeutil.Duration{
Duration: connLife,
},
)
metrics.StreamConnLifeDuration.WithLabelValues( metrics.StreamConnLifeDuration.WithLabelValues(
name, c.serverInfo.Name,
c.serverInfo.Proto.String(), c.serverInfo.Proto.String(),
c.serverInfo.Addr, c.serverInfo.Addr,
).Observe(connLife) ).Observe(connLife.Seconds())
c.decrement() c.decrement()

View File

@ -2,6 +2,7 @@ package connlimiter
import ( import (
"fmt" "fmt"
"log/slog"
"net" "net"
"sync" "sync"
@ -11,6 +12,9 @@ import (
// Config is the configuration structure for the stream-connection limiter. // Config is the configuration structure for the stream-connection limiter.
type Config struct { type Config struct {
// Logger is used to log the operation of the limiter. It must not be nil.
Logger *slog.Logger
// Stop is the point at which the limiter stops accepting new connections. // Stop is the point at which the limiter stops accepting new connections.
// Once the number of active connections reaches this limit, new connections // Once the number of active connections reaches this limit, new connections
// wait for the number to decrease to or below Resume. // wait for the number to decrease to or below Resume.
@ -27,6 +31,8 @@ type Config struct {
// Limiter is the stream-connection limiter. // Limiter is the stream-connection limiter.
type Limiter struct { type Limiter struct {
logger *slog.Logger
// counterCond is the shared condition variable that protects counter. // counterCond is the shared condition variable that protects counter.
counterCond *sync.Cond counterCond *sync.Cond
@ -41,6 +47,7 @@ func New(c *Config) (l *Limiter, err error) {
} }
return &Limiter{ return &Limiter{
logger: c.Logger,
counterCond: sync.NewCond(&sync.Mutex{}), counterCond: sync.NewCond(&sync.Mutex{}),
counter: &counter{ counter: &counter{
current: 0, current: 0,
@ -60,6 +67,8 @@ func (l *Limiter) Limit(lsnr net.Listener, srvInfo *dnsserver.ServerInfo) (limit
return &limitListener{ return &limitListener{
Listener: lsnr, Listener: lsnr,
logger: l.logger.With("name", name),
counterCond: l.counterCond, counterCond: l.counterCond,
counter: l.counter, counter: l.counter,

View File

@ -8,6 +8,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/agd" "github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/connlimiter" "github.com/AdguardTeam/AdGuardDNS/internal/connlimiter"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver" "github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/testutil/fakenet" "github.com/AdguardTeam/golibs/testutil/fakenet"
@ -15,10 +16,6 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestMain(m *testing.M) {
testutil.DiscardLogOutput(m)
}
// testTimeout is the common timeout for tests. // testTimeout is the common timeout for tests.
const testTimeout = 1 * time.Second const testTimeout = 1 * time.Second
@ -31,6 +28,7 @@ var testServerInfo = &dnsserver.ServerInfo{
func TestLimiter(t *testing.T) { func TestLimiter(t *testing.T) {
l, err := connlimiter.New(&connlimiter.Config{ l, err := connlimiter.New(&connlimiter.Config{
Logger: slogutil.NewDiscardLogger(),
Stop: 1, Stop: 1,
Resume: 1, Resume: 1,
}) })
@ -114,6 +112,7 @@ func TestLimiter(t *testing.T) {
func TestLimiter_badConf(t *testing.T) { func TestLimiter_badConf(t *testing.T) {
l, err := connlimiter.New(&connlimiter.Config{ l, err := connlimiter.New(&connlimiter.Config{
Logger: slogutil.NewDiscardLogger(),
Stop: 1, Stop: 1,
Resume: 2, Resume: 2,
}) })

View File

@ -9,6 +9,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest" "github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/connlimiter" "github.com/AdguardTeam/AdGuardDNS/internal/connlimiter"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver" "github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil/fakenet" "github.com/AdguardTeam/golibs/testutil/fakenet"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -49,6 +50,7 @@ func TestListenConfig(t *testing.T) {
} }
l, err := connlimiter.New(&connlimiter.Config{ l, err := connlimiter.New(&connlimiter.Config{
Logger: slogutil.NewDiscardLogger(),
Stop: 1, Stop: 1,
Resume: 1, Resume: 1,
}) })

View File

@ -3,12 +3,13 @@
package connlimiter package connlimiter
import ( import (
"context"
"log/slog"
"net" "net"
"sync" "sync"
"time" "time"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver" "github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/AdGuardDNS/internal/optlog"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
) )
@ -20,6 +21,8 @@ import (
type limitListener struct { type limitListener struct {
net.Listener net.Listener
logger *slog.Logger
// serverInfo is used for logging and metrics in both the listener itself // serverInfo is used for logging and metrics in both the listener itself
// and in its conns. It's never nil. // and in its conns. It's never nil.
serverInfo *dnsserver.ServerInfo serverInfo *dnsserver.ServerInfo
@ -50,7 +53,9 @@ func (l *limitListener) Accept() (conn net.Conn, err error) {
waitStart := time.Now() waitStart := time.Now()
isClosed := l.increment() // TODO(a.garipov): Find a way to use contexts with Accept.
ctx := context.Background()
isClosed := l.increment(ctx)
if isClosed { if isClosed {
return nil, net.ErrClosed return nil, net.ErrClosed
} }
@ -68,6 +73,7 @@ func (l *limitListener) Accept() (conn net.Conn, err error) {
return &limitConn{ return &limitConn{
Conn: conn, Conn: conn,
logger: l.logger,
decrement: l.decrement, decrement: l.decrement,
start: time.Now(), start: time.Now(),
serverInfo: l.serverInfo, serverInfo: l.serverInfo,
@ -77,7 +83,7 @@ func (l *limitListener) Accept() (conn net.Conn, err error) {
// increment waits until it can increase the number of active connections // increment waits until it can increase the number of active connections
// in the counter. If the listener is closed while waiting, increment exits and // in the counter. If the listener is closed while waiting, increment exits and
// returns true // returns true
func (l *limitListener) increment() (isClosed bool) { func (l *limitListener) increment(ctx context.Context) (isClosed bool) {
l.counterCond.L.Lock() l.counterCond.L.Lock()
defer l.counterCond.L.Unlock() defer l.counterCond.L.Unlock()
@ -87,7 +93,7 @@ func (l *limitListener) increment() (isClosed bool) {
waited := false waited := false
for !l.counter.increment() && !l.isClosed { for !l.counter.increment() && !l.isClosed {
if !waited { if !waited {
optlog.Debug1("connlimiter: server %s: accept waiting", l.serverInfo.Name) l.logger.DebugContext(ctx, "accept waiting")
waited = true waited = true
} }
@ -96,7 +102,7 @@ func (l *limitListener) increment() (isClosed bool) {
} }
if waited { if waited {
optlog.Debug1("connlimiter: server %s: accept stopped waiting", l.serverInfo.Name) l.logger.DebugContext(ctx, "accept stopped waiting")
} }
return l.isClosed return l.isClosed

View File

@ -5,6 +5,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"net/netip" "net/netip"
"net/url" "net/url"
@ -16,61 +17,84 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll" "github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics" "github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/netutil/urlutil"
) )
// AllowlistRefresher is a refresh wrapper that updates the allowlist. It // AllowlistUpdater is a wrapper that updates the allowlist on refresh. It
// should be initially refreshed before use. // should be initially refreshed before use.
type AllowlistRefresher struct { type AllowlistUpdater struct {
logger *slog.Logger
allowlist *ratelimit.DynamicAllowlist allowlist *ratelimit.DynamicAllowlist
http *agdhttp.Client http *agdhttp.Client
url *url.URL url *url.URL
errColl errcoll.Interface errColl errcoll.Interface
} }
// NewAllowlistRefresher returns a properly initialized *AllowlistRefresher. // AllowlistUpdaterConfig is the configuration structure for the allowlist
func NewAllowlistRefresher( // updater. All fields must not be nil.
allowlist *ratelimit.DynamicAllowlist, type AllowlistUpdaterConfig struct {
consulURL *url.URL, // Logger is used for logging the operation of the allowlist updater.
errColl errcoll.Interface, Logger *slog.Logger
) (l *AllowlistRefresher) {
return &AllowlistRefresher{ // Allowlist is the allowlist to update.
allowlist: allowlist, Allowlist *ratelimit.DynamicAllowlist
// ConsulURL is the URL from which to update Allowlist.
ConsulURL *url.URL
// ErrColl is used to collect errors during refreshes.
ErrColl errcoll.Interface
// Timeout is the timeout for Consul queries.
Timeout time.Duration
}
// NewAllowlistUpdater returns a properly initialized *AllowlistUpdater. c must
// not be nil.
func NewAllowlistUpdater(c *AllowlistUpdaterConfig) (upd *AllowlistUpdater) {
return &AllowlistUpdater{
logger: c.Logger,
allowlist: c.Allowlist,
http: agdhttp.NewClient(&agdhttp.ClientConfig{ http: agdhttp.NewClient(&agdhttp.ClientConfig{
// TODO(a.garipov): Consider making configurable. Timeout: c.Timeout,
Timeout: 15 * time.Second,
}), }),
url: consulURL, url: c.ConsulURL,
errColl: errColl, errColl: c.ErrColl,
} }
} }
// type check // type check
var _ agdservice.Refresher = (*AllowlistRefresher)(nil) var _ agdservice.Refresher = (*AllowlistUpdater)(nil)
// Refresh implements the [agdservice.Refresher] interface for // Refresh implements the [agdservice.Refresher] interface for
// *AllowlistRefresher. // *AllowlistUpdater.
func (l *AllowlistRefresher) Refresh(ctx context.Context) (err error) { func (upd *AllowlistUpdater) Refresh(ctx context.Context) (err error) {
// TODO(a.garipov): Use slog. upd.logger.InfoContext(ctx, "refresh started")
log.Info("allowlist_refresh: started") defer upd.logger.InfoContext(ctx, "refresh finished")
defer log.Info("allowlist_refresh: finished")
defer func() { defer func() {
metrics.ConsulAllowlistUpdateTime.SetToCurrentTime() metrics.ConsulAllowlistUpdateTime.SetToCurrentTime()
metrics.SetStatusGauge(metrics.ConsulAllowlistUpdateStatus, err) metrics.SetStatusGauge(metrics.ConsulAllowlistUpdateStatus, err)
}() }()
consulNets, err := l.loadConsul(ctx) consulNets, err := upd.loadConsul(ctx)
if err != nil { if err != nil {
errcoll.Collectf(ctx, l.errColl, "allowlist_refresh: %w", err) errcoll.Collect(ctx, upd.errColl, upd.logger, "loading consul allowlist", err)
// Don't wrap the error, because it's informative enough as is. // Don't wrap the error, because it's informative enough as is.
return err return err
} }
log.Info("allowlist: loaded %d records from %s", len(consulNets), l.url) upd.logger.InfoContext(
ctx,
"refresh successful",
"num_records", len(consulNets),
"url", &urlutil.URL{
URL: *upd.url,
},
)
l.allowlist.Update(consulNets) upd.allowlist.Update(consulNets)
metrics.ConsulAllowlistSize.Set(float64(len(consulNets))) metrics.ConsulAllowlistSize.Set(float64(len(consulNets)))
return nil return nil
@ -82,10 +106,10 @@ type consulRecord struct {
} }
// loadConsul fetches, decodes, and returns the list of IP networks from consul. // loadConsul fetches, decodes, and returns the list of IP networks from consul.
func (l *AllowlistRefresher) loadConsul(ctx context.Context) (nets []netip.Prefix, err error) { func (upd *AllowlistUpdater) loadConsul(ctx context.Context) (nets []netip.Prefix, err error) {
defer func() { err = errors.Annotate(err, "loading allowlist nets from %s: %w", l.url) }() defer func() { err = errors.Annotate(err, "loading allowlist nets from %s: %w", upd.url) }()
httpResp, err := l.http.Get(ctx, l.url) httpResp, err := upd.http.Get(ctx, upd.url)
if err != nil { if err != nil {
return nil, fmt.Errorf("requesting: %w", err) return nil, fmt.Errorf("requesting: %w", err)
} }

Some files were not shown because too many files have changed in this diff Show More