Sync v2.8.0

This commit is contained in:
Andrey Meshkov 2024-07-10 19:49:07 +03:00
parent 5690301129
commit 41f7e6cb22
141 changed files with 11627 additions and 7233 deletions

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,7 @@ VERBOSE.MACRO = $${VERBOSE:-0}
BRANCH = $$( git rev-parse --abbrev-ref HEAD )
GOAMD64 = v1
GOPROXY = https://proxy.golang.org|direct
GOTOOLCHAIN = go1.22.4
GOTOOLCHAIN = go1.22.5
RACE = 0
REVISION = $$( git rev-parse --short HEAD )
VERSION = 0
@ -66,8 +66,8 @@ go-check: go-tools go-lint go-test
# A quick check to make sure that all operating systems relevant to the
# development of the project can be typechecked and built successfully.
go-os-check:
env GOOS='darwin' "$(GO.MACRO)" vet ./internal/...
env GOOS='linux' "$(GO.MACRO)" vet ./internal/...
env GOOS='darwin' "$(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.
env GOOS='freebsd' "$(GO.MACRO)" vet ./internal/dnsserver/...
env GOOS='openbsd' "$(GO.MACRO)" vet ./internal/dnsserver/...

View File

@ -1,20 +1,75 @@
# AdGuard DNS Debug HTTP API
# AdGuard DNS debug HTTP API
The AdGuard DNS debug HTTP API is served on [`LISTEN_PORT`][env-listen_port] and
contains various private debugging information.
The AdGuard DNS debug HTTP API is served on [`LISTEN_PORT`][env-listen_port] and contains various private debugging information.
## Contents
* [`GET /dnsdb/csv`](#dnsdb-csv)
* [`GET /health-check`](#health-check)
* [`GET /metrics`](#metrics)
* [`GET /debug/pprof`](#pprof)
- [`GET /health-check`](#health-check)
- [`GET /metrics`](#metrics)
- [`GET /debug/pprof`](#pprof)
- [`POST /debug/api/refresh`](#api-refresh)
- [`POST /dnsdb/csv`](#dnsdb-csv)
[env-listen_port]: environment.md#LISTEN_PORT
## <a href="#health-check" id="health-check" name="health-check">`GET /health-check`</a>
A simple health check API. Always responds with a `200 OK` status and the plain-text body `OK`.
## <a href="#dnsdb-csv" id="dnsdb-csv" name="dnsdb-csv">`GET /dnsdb/csv`</a>
## <a href="#metrics" id="metrics" name="metrics">`GET /metrics`</a>
Prometheus metrics HTTP API. See the [metrics page][metrics] for more details.
[metrics]: metrics.md
## <a href="#pprof" id="pprof" name="pprof">`GET /debug/pprof`</a>
The HTTP interface of Go's [PProf HTTP API][pprof api].
[pprof api]: https://pkg.go.dev/net/http/pprof
## <a href="#api-refresh" id="api-refresh" name="api-refresh">`POST /debug/api/refresh`</a>
Run some refresh jobs manually. This refresh does not alter the time of the next automatic refresh.
Example request:
```sh
curl -d '{"ids":["*"]}' -v "http://${LISTEN_ADDR}:${LISTEN_PORT}/debug/api/refresh"
```
Request body example:
```json
{
"ids": [
"filter_storage",
"adult_blocking"
]
}
```
Supported IDs:
- `adult_blocking`;
- `filter_storage`;
- `newly_registered_domains`;
- `safe_browsing`.
The special ID `*`, when used alone, causes all available refresh tasks to be performed. Use with caution.
Response body example:
```json
{
"results": {
"adult_blocking": "ok",
"filter_storage": "ok"
}
}
```
## <a href="#dnsdb-csv" id="dnsdb-csv" name="dnsdb-csv">`POST /dnsdb/csv`</a>
The CSV dump of the current DNSDB statistics. Example of the output:
@ -23,33 +78,4 @@ example.com,A,NOERROR,93.184.216.34,42
example.com,AAAA,NOERROR,2606:2800:220:1:248:1893:25c8:1946,123
```
The response is sent with the `Transfer-Encoding` set to `chunked` and with an
HTTP trailer named `X-Error` which describes errors that might have occurred
during the database dump.
> [!NOTE]
> For legacy software reasons, despite the endpoint being a `GET` one, it
> rotates the database, and so changes the internal state.
## <a href="#health-check" id="health-check" name="health-check">`GET /health-check`</a>
A simple health check API. Always responds with a `200 OK` status and the
plain-text body `OK`.
## <a href="#metrics" id="metrics" name="metrics">`GET /metrics`</a>
Prometheus metrics HTTP API. See the [metrics page][metrics] for more details.
[metrics]: metrics.md
## <a href="#pprof" id="pprof" name="pprof">`GET /debug/pprof`</a>
The HTTP interface of Go's [PProf HTTP API][pprof api].
[pprof api]: https://pkg.go.dev/net/http/pprof
The response is sent with the `Transfer-Encoding` set to `chunked` and with an HTTP trailer named `X-Error` which describes errors that might have occurred during the database dump.

View File

@ -185,6 +185,7 @@ cp -f config.dist.yaml config.yaml
### <a href="#run-4" id="run-4" name="run-4">Step 4: prepare the test data</a>
```sh
echo '<html><body>General content ahead</body></html>' > ./test/block_page_general.html
echo '<html><body>Dangerous content ahead</body></html>' > ./test/block_page_sb.html
echo '<html><body>Adult content ahead</body></html>' > ./test/block_page_adult.html
echo '<html><body>Error 404</body></html>' > ./test/error_404.html

View File

@ -1,174 +1,138 @@
# AdGuard DNS Environment Configuration
# AdGuard DNS environment configuration
AdGuard DNS uses [environment variables][wiki-env] to store some of the more
sensitive configuration. All other configuration is stored in the
[configuration file][conf].
AdGuard DNS uses [environment variables][wiki-env] to store some of the more sensitive configuration. All other configuration is stored in the [configuration file][conf].
## Contents
* [`ADULT_BLOCKING_URL`](#ADULT_BLOCKING_URL)
* [`BILLSTAT_URL`](#BILLSTAT_URL)
* [`BLOCKED_SERVICE_INDEX_URL`](#BLOCKED_SERVICE_INDEX_URL)
* [`CONFIG_PATH`](#CONFIG_PATH)
* [`CONSUL_ALLOWLIST_URL`](#CONSUL_ALLOWLIST_URL)
* [`CONSUL_DNSCHECK_KV_URL`](#CONSUL_DNSCHECK_KV_URL)
* [`CONSUL_DNSCHECK_SESSION_URL`](#CONSUL_DNSCHECK_SESSION_URL)
* [`FILTER_CACHE_PATH`](#FILTER_CACHE_PATH)
* [`FILTER_INDEX_URL`](#FILTER_INDEX_URL)
* [`GENERAL_SAFE_SEARCH_URL`](#GENERAL_SAFE_SEARCH_URL)
* [`GEOIP_ASN_PATH` and `GEOIP_COUNTRY_PATH`](#GEOIP_ASN_PATH)
* [`LINKED_IP_TARGET_URL`](#LINKED_IP_TARGET_URL)
* [`LISTEN_ADDR`](#LISTEN_ADDR)
* [`LISTEN_PORT`](#LISTEN_PORT)
* [`LOG_TIMESTAMP`](#LOG_TIMESTAMP)
* [`NEW_REG_DOMAINS_URL`](#NEW_REG_DOMAINS_URL)
* [`PROFILES_CACHE_PATH`](#PROFILES_CACHE_PATH)
* [`PROFILES_ENABLED`](#PROFILES_ENABLED)
* [`PROFILES_URL`](#PROFILES_URL)
* [`QUERYLOG_PATH`](#QUERYLOG_PATH)
* [`RULESTAT_URL`](#RULESTAT_URL)
* [`SAFE_BROWSING_URL`](#SAFE_BROWSING_URL)
* [`SENTRY_DSN`](#SENTRY_DSN)
* [`SSL_KEY_LOG_FILE`](#SSL_KEY_LOG_FILE)
* [`VERBOSE`](#VERBOSE)
* [`YOUTUBE_SAFE_SEARCH_URL`](#YOUTUBE_SAFE_SEARCH_URL)
- [`ADULT_BLOCKING_URL`](#ADULT_BLOCKING_URL)
- [`BILLSTAT_API_KEY`](#BILLSTAT_API_KEY)
- [`BILLSTAT_URL`](#BILLSTAT_URL)
- [`BLOCKED_SERVICE_INDEX_URL`](#BLOCKED_SERVICE_INDEX_URL)
- [`CONFIG_PATH`](#CONFIG_PATH)
- [`CONSUL_ALLOWLIST_URL`](#CONSUL_ALLOWLIST_URL)
- [`CONSUL_DNSCHECK_KV_URL`](#CONSUL_DNSCHECK_KV_URL)
- [`CONSUL_DNSCHECK_SESSION_URL`](#CONSUL_DNSCHECK_SESSION_URL)
- [`FILTER_CACHE_PATH`](#FILTER_CACHE_PATH)
- [`FILTER_INDEX_URL`](#FILTER_INDEX_URL)
- [`GENERAL_SAFE_SEARCH_URL`](#GENERAL_SAFE_SEARCH_URL)
- [`GEOIP_ASN_PATH` and `GEOIP_COUNTRY_PATH`](#GEOIP_ASN_PATH)
- [`LINKED_IP_TARGET_URL`](#LINKED_IP_TARGET_URL)
- [`LISTEN_ADDR`](#LISTEN_ADDR)
- [`LISTEN_PORT`](#LISTEN_PORT)
- [`LOG_TIMESTAMP`](#LOG_TIMESTAMP)
- [`NEW_REG_DOMAINS_URL`](#NEW_REG_DOMAINS_URL)
- [`PROFILES_API_KEY`](#PROFILES_API_KEY)
- [`PROFILES_CACHE_PATH`](#PROFILES_CACHE_PATH)
- [`PROFILES_ENABLED`](#PROFILES_ENABLED)
- [`PROFILES_URL`](#PROFILES_URL)
- [`QUERYLOG_PATH`](#QUERYLOG_PATH)
- [`RULESTAT_URL`](#RULESTAT_URL)
- [`SAFE_BROWSING_URL`](#SAFE_BROWSING_URL)
- [`SENTRY_DSN`](#SENTRY_DSN)
- [`SSL_KEY_LOG_FILE`](#SSL_KEY_LOG_FILE)
- [`VERBOSE`](#VERBOSE)
- [`YOUTUBE_SAFE_SEARCH_URL`](#YOUTUBE_SAFE_SEARCH_URL)
[conf]: configuration.md
[wiki-env]: https://en.wikipedia.org/wiki/Environment_variable
## <a href="#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.
**Default:** No default value, the variable is **required.**
## <a href="#BILLSTAT_API_KEY" id="BILLSTAT_API_KEY" name="BILLSTAT_API_KEY">`BILLSTAT_API_KEY`</a>
The API key to use when authenticating queries to the billing statistics API, if any. The API key should be valid as defined by [RFC 6750].
**Default:** **Unset.**
[RFC 6750]: https://datatracker.ietf.org/doc/html/rfc6750#section-2.1
## <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 (`grpc://` and`grpcs://`) URLs. See the [external HTTP API requirements section][ext-billstat].
**Default:** No default value, the variable is **required.**
[ext-billstat]: externalhttp.md#backend-billstat
## <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 URL of the blocked service index file server. See the [external HTTP API requirements section][ext-blocked] on the expected format of the response.
**Default:** No default value, the variable is **required.**
[ext-blocked]: externalhttp.md#filters-blocked-services
## <a href="#CONFIG_PATH" id="CONFIG_PATH" name="CONFIG_PATH">`CONFIG_PATH`</a>
The path to the configuration file.
**Default:** `./config.yaml`.
## <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 URL of the Consul instance serving the dynamic part of the rate-limit allowlist. See the [external HTTP API requirements section][ext-consul] on the expected format of the response.
**Default:** No default value, the variable is **required.**
[ext-consul]: externalhttp.md#consul
## <a href="#CONSUL_DNSCHECK_KV_URL" id="CONSUL_DNSCHECK_KV_URL" name="CONSUL_DNSCHECK_KV_URL">`CONSUL_DNSCHECK_KV_URL`</a>
The URL of the KV API of the Consul instance used as a key-value database for
the DNS server checking. It must end with `/kv/<NAMESPACE>` where `<NAMESPACE>`
is any non-empty namespace. If not specified, the
[`CONSUL_DNSCHECK_SESSION_URL`](#CONSUL_DNSCHECK_SESSION_URL) is also
omitted.
The URL of the KV API of the Consul instance used as a key-value database for the DNS server checking. It must end with `/kv/<NAMESPACE>` where `<NAMESPACE>` is any non-empty namespace. If not specified, the [`CONSUL_DNSCHECK_SESSION_URL`](#CONSUL_DNSCHECK_SESSION_URL) is also omitted.
**Default:** **Unset.**
**Example:** `http://localhost:8500/v1/kv/test`
## <a href="#CONSUL_DNSCHECK_SESSION_URL" id="CONSUL_DNSCHECK_SESSION_URL" name="CONSUL_DNSCHECK_SESSION_URL">`CONSUL_DNSCHECK_SESSION_URL`</a>
The URL of the session API of the Consul instance used as a key-value database
for the DNS server checking. If not specified, the
[`CONSUL_DNSCHECK_KV_URL`](#CONSUL_DNSCHECK_KV_URL) is also omitted.
The URL of the session API of the Consul instance used as a key-value database for the DNS server checking. If not specified, the [`CONSUL_DNSCHECK_KV_URL`](#CONSUL_DNSCHECK_KV_URL) is also omitted.
**Default:** **Unset.**
**Example:** `http://localhost:8500/v1/session/create`
## <a href="#FILTER_CACHE_PATH" id="FILTER_CACHE_PATH" name="FILTER_CACHE_PATH">`FILTER_CACHE_PATH`</a>
The path to the directory used to store the cached version of all filters and
filter indexes.
The path to the directory used to store the cached version of all filters and filter indexes.
**Default:** `./filters/`.
## <a href="#FILTER_INDEX_URL" id="FILTER_INDEX_URL" name="FILTER_INDEX_URL">`FILTER_INDEX_URL`</a>
The URL of the filtering rule index file server. See the [external HTTP API
requirements section][ext-lists] on the expected format of the response.
The URL of the filtering rule index file server. See the [external HTTP API requirements section][ext-lists] on the expected format of the response.
**Default:** No default value, the variable is **required.**
[ext-lists]: externalhttp.md#filters-lists
## <a href="#GENERAL_SAFE_SEARCH_URL" id="GENERAL_SAFE_SEARCH_URL" name="GENERAL_SAFE_SEARCH_URL">`GENERAL_SAFE_SEARCH_URL`</a>
The URL of the list of general safe search rewriting rules. See the [external
HTTP API requirements section][ext-general] on the expected format of the
response.
The URL of the list of general safe search rewriting rules. See the [external HTTP API requirements section][ext-general] on the expected format of the response.
**Default:** No default value, the variable is **required.**
[ext-general]: externalhttp.md#filters-safe-search
## <a href="#GEOIP_ASN_PATH" id="GEOIP_ASN_PATH" name="GEOIP_ASN_PATH">`GEOIP_ASN_PATH` and `GEOIP_COUNTRY_PATH`</a>
Paths to the files containing MaxMind GeoIP databases: for ASNs and for
countries and continents respectively.
Paths to the files containing MaxMind GeoIP databases: for ASNs and for countries and continents respectively.
**Default:** `./asn.mmdb` and `./country.mmdb`.
## <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 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.**
[conf-web-linked_ip]: configuration.md#web-linked_ip
[ext-linked_ip]: externalhttp.md#backend-linkip
## <a href="#LISTEN_ADDR" id="LISTEN_ADDR" name="LISTEN_ADDR">`LISTEN_ADDR`</a>
The IP address on which to bind the [debug HTTP API][debughttp].
@ -177,46 +141,39 @@ The IP address on which to bind the [debug HTTP API][debughttp].
[debughttp]: debughttp.md
## <a href="#LISTEN_PORT" id="LISTEN_PORT" name="LISTEN_PORT">`LISTEN_PORT`</a>
The port on which to bind the [debug HTTP API][debughttp], which includes the
health check, Prometheus, `pprof`, and other endpoints.
The port on which to bind the [debug HTTP API][debughttp], which includes the health check, Prometheus, `pprof`, and other endpoints.
**Default:** `8181`.
## <a href="#LOG_TIMESTAMP" id="LOG_TIMESTAMP" name="LOG_TIMESTAMP">`LOG_TIMESTAMP`</a>
If `1`, show timestamps in the plain text logs. If `0`, don't show the
timestamps.
If `1`, show timestamps in the plain text logs. If `0`, don't show the timestamps.
**Default:** `1`.
## <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 URL of source list of rules for newly registered domains safe browsing filter.
**Default:** No default value, the variable is **required.**
## <a href="#PROFILES_API_KEY" id="PROFILES_API_KEY" name="PROFILES_API_KEY">`PROFILES_API_KEY`</a>
The API key to use when authenticating queries to the profiles API, if any. The API key should be valid as defined by [RFC 6750].
**Default:** **Unset.**
## <a href="#PROFILES_CACHE_PATH" id="PROFILES_CACHE_PATH" name="PROFILES_CACHE_PATH">`PROFILES_CACHE_PATH`</a>
The path to the profile cache file:
* `none` means that the profile caching is disabled.
- `none` means that the profile caching is disabled.
* A file with the extension `.pb` means that the profiles are cached in the
protobuf format.
- A file with the extension `.pb` means that the profiles are cached in the protobuf format.
Use the following command to inspect the cache, assuming that the version is
correct:
Use the following command to inspect the cache, assuming that the version is correct:
```sh
protoc\
@ -226,47 +183,35 @@ The path to the profile cache file:
< /path/to/profilecache.pb
```
The profile cache is read on start and is later updated on every
[full refresh][conf-backend-full_refresh_interval].
The profile cache is read on start and is later updated on every [full refresh][conf-backend-full_refresh_interval].
**Default:** `./profilecache.pb`.
[conf-backend-full_refresh_interval]: configuration.md#backend-full_refresh_interval
## <a href="#PROFILES_ENABLED" id="PROFILES_ENABLED" name="PROFILES_ENABLED">`PROFILES_ENABLED`</a>
If `0`, disables user profiles and devices recognition and billing.
**Default:** `1`.
## <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 (`grpc://` and`grpcs://`) URLs. See the [external API requirements section][ext-profiles].
**Default:** No default value, the variable is **required.**
[ext-profiles]: externalhttp.md#backend-profiles
## <a href="#QUERYLOG_PATH" id="QUERYLOG_PATH" name="QUERYLOG_PATH">`QUERYLOG_PATH`</a>
The path to the file into which the query log is going to be written.
**Default:** `./querylog.jsonl`.
## <a href="#RULESTAT_URL" id="RULESTAT_URL" name="RULESTAT_URL">`RULESTAT_URL`</a>
The URL to send filtering rule list statistics to. If empty or unset, the
collection of filtering rule statistics is disabled. See the [external HTTP API
requirements section][ext-rulestat] on the expected format of the response.
The URL to send filtering rule list statistics to. If empty or unset, the collection of filtering rule statistics is disabled. See the [external HTTP API requirements section][ext-rulestat] on the expected format of the response.
**Default:** **Unset.**
@ -274,46 +219,32 @@ requirements section][ext-rulestat] on the expected format of the response.
[ext-rulestat]: externalhttp.md#rulestat
## <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.
**Default:** No default value, the variable is **required.**
## <a href="#SENTRY_DSN" id="SENTRY_DSN" name="SENTRY_DSN">`SENTRY_DSN`</a>
Sentry error collector address. The special value `stderr` makes AdGuard DNS
print these errors to standard error.
Sentry error collector address. The special value `stderr` makes AdGuard DNS print these errors to standard error.
**Default:** `stderr`.
## <a href="#SSL_KEY_LOG_FILE" id="SSL_KEY_LOG_FILE" name="SSL_KEY_LOG_FILE">`SSL_KEY_LOG_FILE`</a>
If set, TLS key logs are written to this file to allow other programs (i.e.
Wireshark) to decrypt packets. **Must only be used for debug purposes**.
If set, TLS key logs are written to this file to allow other programs (i.e. Wireshark) to decrypt packets. **Must only be used for debug purposes**.
**Default:** **Unset.**
## <a href="#VERBOSE" id="VERBOSE" name="VERBOSE">`VERBOSE`</a>
When set to `1`, enable verbose logging. When set to `0`, disable it.
**Default:** `0`.
## <a href="#YOUTUBE_SAFE_SEARCH_URL" id="YOUTUBE_SAFE_SEARCH_URL" name="YOUTUBE_SAFE_SEARCH_URL">`YOUTUBE_SAFE_SEARCH_URL`</a>
The URL of the list of YouTube-specific safe search rewriting rules. See the
[external HTTP API requirements section][ext-general] on the expected format of
the response.
The URL of the list of YouTube-specific safe search rewriting rules. See the [external HTTP API requirements section][ext-general] on the expected format of the response.
**Default:** No default value, the variable is **required.**

View File

@ -114,8 +114,8 @@ This endpoint, defined by [`FILTER_INDEX_URL`][env-filters], must respond with a
{
"filters": [
{
"downloadUrl": "https://cdn.example.com/assets/my_filter.txt",
"id": "my_filter"
"filterKey": "my_filter",
"downloadUrl": "https://cdn.example.com/assets/my_filter.txt"
}
]
}

View File

@ -58,8 +58,11 @@ rules to remember, which property means what. The properties are:
“destination”.
> [!NOTE]
> AdGuard DNS uses the common user-assigned ISO 3166-1 alpha-2 code `XK`
> for the partially-recognized state of the Republic of Kosovo.
> 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).
**Example:** `"US"`

52
go.mod
View File

@ -1,33 +1,33 @@
module github.com/AdguardTeam/AdGuardDNS
go 1.22.4
go 1.22.5
require (
github.com/AdguardTeam/AdGuardDNS/internal/dnsserver v0.0.0-00010101000000-000000000000
github.com/AdguardTeam/golibs v0.23.2
github.com/AdguardTeam/urlfilter v0.18.0
github.com/AdguardTeam/AdGuardDNS/internal/dnsserver v0.0.0-20240607112746-5690301129fe
github.com/AdguardTeam/golibs v0.24.0
github.com/AdguardTeam/urlfilter v0.19.0
github.com/ameshkov/dnscrypt/v2 v2.3.0
github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27
github.com/bluele/gcache v0.0.2
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
github.com/caarlos0/env/v7 v7.1.0
github.com/getsentry/sentry-go v0.27.0
github.com/getsentry/sentry-go v0.28.1
github.com/google/renameio/v2 v2.0.0
github.com/miekg/dns v1.1.58
github.com/oschwald/maxminddb-golang v1.12.0
github.com/miekg/dns v1.1.61
github.com/oschwald/maxminddb-golang v1.13.0
github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible
github.com/prometheus/client_golang v1.19.0
github.com/prometheus/client_golang v1.19.1
github.com/prometheus/client_model v0.6.1
github.com/prometheus/common v0.52.3
github.com/quic-go/quic-go v0.42.0
github.com/prometheus/common v0.54.0
github.com/quic-go/quic-go v0.45.0
github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.22.0
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8
golang.org/x/net v0.24.0
golang.org/x/sys v0.19.0
golang.org/x/crypto v0.24.0
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
golang.org/x/net v0.26.0
golang.org/x/sys v0.21.0
golang.org/x/time v0.5.0
google.golang.org/grpc v1.63.2
google.golang.org/protobuf v1.33.0
google.golang.org/grpc v1.64.0
google.golang.org/protobuf v1.34.2
gopkg.in/yaml.v2 v2.4.0
)
@ -39,19 +39,19 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect
github.com/onsi/ginkgo/v2 v2.17.1 // indirect
github.com/panjf2000/ants/v2 v2.9.1 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/pprof v0.0.0-20240618054019-d3b898a103f8 // indirect
github.com/onsi/ginkgo/v2 v2.19.0 // indirect
github.com/panjf2000/ants/v2 v2.10.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/procfs v0.13.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
go.uber.org/mock v0.4.0 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.20.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240412170617-26222e5d3d56 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/tools v0.22.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

119
go.sum
View File

@ -1,7 +1,7 @@
github.com/AdguardTeam/golibs v0.23.2 h1:rMjYantwtQ39e8G4zBQ6ZLlm4s3XH30Bc9VxhoOHwao=
github.com/AdguardTeam/golibs v0.23.2/go.mod h1:o9i55Sx6v7qogRQeqaBfmLbC/pZqeMBWi015U5PTDY0=
github.com/AdguardTeam/urlfilter v0.18.0 h1:ZZzwODC/ADpjJSODxySrrUnt/fvOCfGFaCW6j+wsGfQ=
github.com/AdguardTeam/urlfilter v0.18.0/go.mod h1:IXxBwedLiZA2viyHkaFxY/8mjub0li2PXRg8a3d9Z1s=
github.com/AdguardTeam/golibs v0.24.0 h1:qAnOq7BQtwSVo7Co9q703/n+nZ2Ap6smkugU9G9MomY=
github.com/AdguardTeam/golibs v0.24.0/go.mod h1:9/vJcYznW7RlmCT/Qzi8XNZGj+ZbWfHZJmEXKnRpCAU=
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/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/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
@ -10,8 +10,8 @@ github.com/ameshkov/dnscrypt/v2 v2.3.0 h1:pDXDF7eFa6Lw+04C0hoMh8kCAQM8NwUdFEllSP
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/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02 h1:bXAPYSbdYbS5VTy92NIUbeDI1qyggi+JYh5op9IFlcQ=
github.com/axiomhq/hyperloglog v0.0.0-20240319100328-84253e514e02/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c=
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 h1:60m4tnanN1ctzIu4V3bfCNJ39BiOPSm1gHFlFjTkRE0=
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27/go.mod h1:k08r+Yj1PRAmuayFiRK6MYuR5Ve4IuZtTfxErMIh0+c=
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/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
@ -28,22 +28,20 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
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/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
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/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-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
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/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/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/pprof v0.0.0-20240618054019-d3b898a103f8 h1:ASJ/LAqdCHOyMYI+dwNxn7Rd8FscNkMyTr1KZU1JI/M=
github.com/google/pprof v0.0.0-20240618054019-d3b898a103f8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -52,16 +50,16 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=
github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
github.com/panjf2000/ants/v2 v2.9.1 h1:Q5vh5xohbsZXGcD6hhszzGqB7jSSc2/CRr3QKIga8Kw=
github.com/panjf2000/ants/v2 v2.9.1/go.mod h1:7ZxyxsqE4vvW0M7LSD8aI3cKwgFhBHbxnlN8mDqHa1I=
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
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/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/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
@ -72,66 +70,65 @@ 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/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/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
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 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
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/common v0.52.3 h1:5f8uj6ZwHSscOGNdIQg6OiZv/ybiK2CO2q2drVZAQSA=
github.com/prometheus/common v0.52.3/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8=
github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
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/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM=
github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
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/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4=
github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
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/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc=
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
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.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240412170617-26222e5d3d56 h1:zviK8GX4VlMstrK3JkexM5UHjH1VOkRebH9y3jhSBGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240412170617-26222e5d3d56/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
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 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

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

View File

@ -3,19 +3,138 @@ cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8=
cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
cloud.google.com/go/accessapproval v1.7.5/go.mod h1:g88i1ok5dvQ9XJsxpUInWWvUBrIZhyPDPbk4T01OoJ0=
cloud.google.com/go/accesscontextmanager v1.8.5/go.mod h1:TInEhcZ7V9jptGNqN3EzZ5XMhT6ijWxTGjzyETwmL0Q=
cloud.google.com/go/aiplatform v1.60.0/go.mod h1:eTlGuHOahHprZw3Hio5VKmtThIOak5/qy6pzdsqcQnM=
cloud.google.com/go/analytics v0.23.0/go.mod h1:YPd7Bvik3WS95KBok2gPXDqQPHy08TsCQG6CdUCb+u0=
cloud.google.com/go/apigateway v1.6.5/go.mod h1:6wCwvYRckRQogyDDltpANi3zsCDl6kWi0b4Je+w2UiI=
cloud.google.com/go/apigeeconnect v1.6.5/go.mod h1:MEKm3AiT7s11PqTfKE3KZluZA9O91FNysvd3E6SJ6Ow=
cloud.google.com/go/apigeeregistry v0.8.3/go.mod h1:aInOWnqF4yMQx8kTjDqHNXjZGh/mxeNlAf52YqtASUs=
cloud.google.com/go/appengine v1.8.5/go.mod h1:uHBgNoGLTS5di7BvU25NFDuKa82v0qQLjyMJLuPQrVo=
cloud.google.com/go/area120 v0.8.5/go.mod h1:BcoFCbDLZjsfe4EkCnEq1LKvHSK0Ew/zk5UFu6GMyA0=
cloud.google.com/go/artifactregistry v1.14.7/go.mod h1:0AUKhzWQzfmeTvT4SjfI4zjot72EMfrkvL9g9aRjnnM=
cloud.google.com/go/asset v1.17.2/go.mod h1:SVbzde67ehddSoKf5uebOD1sYw8Ab/jD/9EIeWg99q4=
cloud.google.com/go/assuredworkloads v1.11.5/go.mod h1:FKJ3g3ZvkL2D7qtqIGnDufFkHxwIpNM9vtmhvt+6wqk=
cloud.google.com/go/automl v1.13.5/go.mod h1:MDw3vLem3yh+SvmSgeYUmUKqyls6NzSumDm9OJ3xJ1Y=
cloud.google.com/go/baremetalsolution v1.2.4/go.mod h1:BHCmxgpevw9IEryE99HbYEfxXkAEA3hkMJbYYsHtIuY=
cloud.google.com/go/batch v1.8.0/go.mod h1:k8V7f6VE2Suc0zUM4WtoibNrA6D3dqBpB+++e3vSGYc=
cloud.google.com/go/beyondcorp v1.0.4/go.mod h1:Gx8/Rk2MxrvWfn4WIhHIG1NV7IBfg14pTKv1+EArVcc=
cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA=
cloud.google.com/go/bigquery v1.59.1/go.mod h1:VP1UJYgevyTwsV7desjzNzDND5p6hZB+Z8gZJN1GQUc=
cloud.google.com/go/billing v1.18.2/go.mod h1:PPIwVsOOQ7xzbADCwNe8nvK776QpfrOAUkvKjCUcpSE=
cloud.google.com/go/binaryauthorization v1.8.1/go.mod h1:1HVRyBerREA/nhI7yLang4Zn7vfNVA3okoAR9qYQJAQ=
cloud.google.com/go/certificatemanager v1.7.5/go.mod h1:uX+v7kWqy0Y3NG/ZhNvffh0kuqkKZIXdvlZRO7z0VtM=
cloud.google.com/go/channel v1.17.5/go.mod h1:FlpaOSINDAXgEext0KMaBq/vwpLMkkPAw9b2mApQeHc=
cloud.google.com/go/cloudbuild v1.15.1/go.mod h1:gIofXZSu+XD2Uy+qkOrGKEx45zd7s28u/k8f99qKals=
cloud.google.com/go/clouddms v1.7.4/go.mod h1:RdrVqoFG9RWI5AvZ81SxJ/xvxPdtcRhFotwdE79DieY=
cloud.google.com/go/cloudtasks v1.12.6/go.mod h1:b7c7fe4+TJsFZfDyzO51F7cjq7HLUlRi/KZQLQjDsaY=
cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
cloud.google.com/go/compute v1.21.0 h1:JNBsyXVoOoNJtTQcnEY5uYpZIbeCTYIeDe0Xh1bySMk=
cloud.google.com/go/compute v1.21.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg=
cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40=
cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls=
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/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
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/containeranalysis v0.11.4/go.mod h1:cVZT7rXYBS9NG1rhQbWL9pWbXCKHWJPYraE8/FTSYPE=
cloud.google.com/go/datacatalog v1.19.3/go.mod h1:ra8V3UAsciBpJKQ+z9Whkxzxv7jmQg1hfODr3N3YPJ4=
cloud.google.com/go/dataflow v0.9.5/go.mod h1:udl6oi8pfUHnL0z6UN9Lf9chGqzDMVqcYTcZ1aPnCZQ=
cloud.google.com/go/dataform v0.9.2/go.mod h1:S8cQUwPNWXo7m/g3DhWHsLBoufRNn9EgFrMgne2j7cI=
cloud.google.com/go/datafusion v1.7.5/go.mod h1:bYH53Oa5UiqahfbNK9YuYKteeD4RbQSNMx7JF7peGHc=
cloud.google.com/go/datalabeling v0.8.5/go.mod h1:IABB2lxQnkdUbMnQaOl2prCOfms20mcPxDBm36lps+s=
cloud.google.com/go/dataplex v1.14.2/go.mod h1:0oGOSFlEKef1cQeAHXy4GZPB/Ife0fz/PxBf+ZymA2U=
cloud.google.com/go/dataproc/v2 v2.4.0/go.mod h1:3B1Ht2aRB8VZIteGxQS/iNSJGzt9+CA0WGnDVMEm7Z4=
cloud.google.com/go/dataqna v0.8.5/go.mod h1:vgihg1mz6n7pb5q2YJF7KlXve6tCglInd6XO0JGOlWM=
cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ=
cloud.google.com/go/datastore v1.15.0/go.mod h1:GAeStMBIt9bPS7jMJA85kgkpsMkvseWWXiaHya9Jes8=
cloud.google.com/go/datastream v1.10.4/go.mod h1:7kRxPdxZxhPg3MFeCSulmAJnil8NJGGvSNdn4p1sRZo=
cloud.google.com/go/deploy v1.17.1/go.mod h1:SXQyfsXrk0fBmgBHRzBjQbZhMfKZ3hMQBw5ym7MN/50=
cloud.google.com/go/dialogflow v1.49.0/go.mod h1:dhVrXKETtdPlpPhE7+2/k4Z8FRNUp6kMV3EW3oz/fe0=
cloud.google.com/go/dlp v1.11.2/go.mod h1:9Czi+8Y/FegpWzgSfkRlyz+jwW6Te9Rv26P3UfU/h/w=
cloud.google.com/go/documentai v1.25.0/go.mod h1:ftLnzw5VcXkLItp6pw1mFic91tMRyfv6hHEY5br4KzY=
cloud.google.com/go/domains v0.9.5/go.mod h1:dBzlxgepazdFhvG7u23XMhmMKBjrkoUNaw0A8AQB55Y=
cloud.google.com/go/edgecontainer v1.1.5/go.mod h1:rgcjrba3DEDEQAidT4yuzaKWTbkTI5zAMu3yy6ZWS0M=
cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU=
cloud.google.com/go/essentialcontacts v1.6.6/go.mod h1:XbqHJGaiH0v2UvtuucfOzFXN+rpL/aU5BCZLn4DYl1Q=
cloud.google.com/go/eventarc v1.13.4/go.mod h1:zV5sFVoAa9orc/52Q+OuYUG9xL2IIZTbbuTHC6JSY8s=
cloud.google.com/go/filestore v1.8.1/go.mod h1:MbN9KcaM47DRTIuLfQhJEsjaocVebNtNQhSLhKCF5GM=
cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ=
cloud.google.com/go/functions v1.16.0/go.mod h1:nbNpfAG7SG7Duw/o1iZ6ohvL7mc6MapWQVpqtM29n8k=
cloud.google.com/go/gkebackup v1.3.5/go.mod h1:KJ77KkNN7Wm1LdMopOelV6OodM01pMuK2/5Zt1t4Tvc=
cloud.google.com/go/gkeconnect v0.8.5/go.mod h1:LC/rS7+CuJ5fgIbXv8tCD/mdfnlAadTaUufgOkmijuk=
cloud.google.com/go/gkehub v0.14.5/go.mod h1:6bzqxM+a+vEH/h8W8ec4OJl4r36laxTs3A/fMNHJ0wA=
cloud.google.com/go/gkemulticloud v1.1.1/go.mod h1:C+a4vcHlWeEIf45IB5FFR5XGjTeYhF83+AYIpTy4i2Q=
cloud.google.com/go/gsuiteaddons v1.6.5/go.mod h1:Lo4P2IvO8uZ9W+RaC6s1JVxo42vgy+TX5a6hfBZ0ubs=
cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI=
cloud.google.com/go/iap v1.9.4/go.mod h1:vO4mSq0xNf/Pu6E5paORLASBwEmphXEjgCFg7aeNu1w=
cloud.google.com/go/ids v1.4.5/go.mod h1:p0ZnyzjMWxww6d2DvMGnFwCsSxDJM666Iir1bK1UuBo=
cloud.google.com/go/iot v1.7.5/go.mod h1:nq3/sqTz3HGaWJi1xNiX7F41ThOzpud67vwk0YsSsqs=
cloud.google.com/go/kms v1.15.7/go.mod h1:ub54lbsa6tDkUwnu4W7Yt1aAIFLnspgh0kPGToDukeI=
cloud.google.com/go/language v1.12.3/go.mod h1:evFX9wECX6mksEva8RbRnr/4wi/vKGYnAJrTRXU8+f8=
cloud.google.com/go/lifesciences v0.9.5/go.mod h1:OdBm0n7C0Osh5yZB7j9BXyrMnTRGBJIZonUMxo5CzPw=
cloud.google.com/go/logging v1.9.0/go.mod h1:1Io0vnZv4onoUnsVUQY3HZ3Igb1nBchky0A0y7BBBhE=
cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s=
cloud.google.com/go/managedidentities v1.6.5/go.mod h1:fkFI2PwwyRQbjLxlm5bQ8SjtObFMW3ChBGNqaMcgZjI=
cloud.google.com/go/maps v1.6.4/go.mod h1:rhjqRy8NWmDJ53saCfsXQ0LKwBHfi6OSh5wkq6BaMhI=
cloud.google.com/go/mediatranslation v0.8.5/go.mod h1:y7kTHYIPCIfgyLbKncgqouXJtLsU+26hZhHEEy80fSs=
cloud.google.com/go/memcache v1.10.5/go.mod h1:/FcblbNd0FdMsx4natdj+2GWzTq+cjZvMa1I+9QsuMA=
cloud.google.com/go/metastore v1.13.4/go.mod h1:FMv9bvPInEfX9Ac1cVcRXp8EBBQnBcqH6gz3KvJ9BAE=
cloud.google.com/go/monitoring v1.18.0/go.mod h1:c92vVBCeq/OB4Ioyo+NbN2U7tlg5ZH41PZcdvfc+Lcg=
cloud.google.com/go/networkconnectivity v1.14.4/go.mod h1:PU12q++/IMnDJAB+3r+tJtuCXCfwfN+C6Niyj6ji1Po=
cloud.google.com/go/networkmanagement v1.9.4/go.mod h1:daWJAl0KTFytFL7ar33I6R/oNBH8eEOX/rBNHrC/8TA=
cloud.google.com/go/networksecurity v0.9.5/go.mod h1:KNkjH/RsylSGyyZ8wXpue8xpCEK+bTtvof8SBfIhMG8=
cloud.google.com/go/notebooks v1.11.3/go.mod h1:0wQyI2dQC3AZyQqWnRsp+yA+kY4gC7ZIVP4Qg3AQcgo=
cloud.google.com/go/optimization v1.6.3/go.mod h1:8ve3svp3W6NFcAEFr4SfJxrldzhUl4VMUJmhrqVKtYA=
cloud.google.com/go/orchestration v1.8.5/go.mod h1:C1J7HesE96Ba8/hZ71ISTV2UAat0bwN+pi85ky38Yq8=
cloud.google.com/go/orgpolicy v1.12.1/go.mod h1:aibX78RDl5pcK3jA8ysDQCFkVxLj3aOQqrbBaUL2V5I=
cloud.google.com/go/osconfig v1.12.5/go.mod h1:D9QFdxzfjgw3h/+ZaAb5NypM8bhOMqBzgmbhzWViiW8=
cloud.google.com/go/oslogin v1.13.1/go.mod h1:vS8Sr/jR7QvPWpCjNqy6LYZr5Zs1e8ZGW/KPn9gmhws=
cloud.google.com/go/phishingprotection v0.8.5/go.mod h1:g1smd68F7mF1hgQPuYn3z8HDbNre8L6Z0b7XMYFmX7I=
cloud.google.com/go/policytroubleshooter v1.10.3/go.mod h1:+ZqG3agHT7WPb4EBIRqUv4OyIwRTZvsVDHZ8GlZaoxk=
cloud.google.com/go/privatecatalog v0.9.5/go.mod h1:fVWeBOVe7uj2n3kWRGlUQqR/pOd450J9yZoOECcQqJk=
cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU=
cloud.google.com/go/pubsub v1.36.1/go.mod h1:iYjCa9EzWOoBiTdd4ps7QoMtMln5NwaZQpK1hbRfBDE=
cloud.google.com/go/pubsublite v1.8.1/go.mod h1:fOLdU4f5xldK4RGJrBMm+J7zMWNj/k4PxwEZXy39QS0=
cloud.google.com/go/recaptchaenterprise/v2 v2.9.2/go.mod h1:trwwGkfhCmp05Ll5MSJPXY7yvnO0p4v3orGANAFHAuU=
cloud.google.com/go/recommendationengine v0.8.5/go.mod h1:A38rIXHGFvoPvmy6pZLozr0g59NRNREz4cx7F58HAsQ=
cloud.google.com/go/recommender v1.12.1/go.mod h1:gf95SInWNND5aPas3yjwl0I572dtudMhMIG4ni8nr+0=
cloud.google.com/go/redis v1.14.2/go.mod h1:g0Lu7RRRz46ENdFKQ2EcQZBAJ2PtJHJLuiiRuEXwyQw=
cloud.google.com/go/resourcemanager v1.9.5/go.mod h1:hep6KjelHA+ToEjOfO3garMKi/CLYwTqeAw7YiEI9x8=
cloud.google.com/go/resourcesettings v1.6.5/go.mod h1:WBOIWZraXZOGAgoR4ukNj0o0HiSMO62H9RpFi9WjP9I=
cloud.google.com/go/retail v1.16.0/go.mod h1:LW7tllVveZo4ReWt68VnldZFWJRzsh9np+01J9dYWzE=
cloud.google.com/go/run v1.3.4/go.mod h1:FGieuZvQ3tj1e9GnzXqrMABSuir38AJg5xhiYq+SF3o=
cloud.google.com/go/scheduler v1.10.6/go.mod h1:pe2pNCtJ+R01E06XCDOJs1XvAMbv28ZsQEbqknxGOuE=
cloud.google.com/go/secretmanager v1.11.5/go.mod h1:eAGv+DaCHkeVyQi0BeXgAHOU0RdrMeZIASKc+S7VqH4=
cloud.google.com/go/security v1.15.5/go.mod h1:KS6X2eG3ynWjqcIX976fuToN5juVkF6Ra6c7MPnldtc=
cloud.google.com/go/securitycenter v1.24.4/go.mod h1:PSccin+o1EMYKcFQzz9HMMnZ2r9+7jbc+LvPjXhpwcU=
cloud.google.com/go/servicedirectory v1.11.4/go.mod h1:Bz2T9t+/Ehg6x+Y7Ycq5xiShYLD96NfEsWNHyitj1qM=
cloud.google.com/go/shell v1.7.5/go.mod h1:hL2++7F47/IfpfTO53KYf1EC+F56k3ThfNEXd4zcuiE=
cloud.google.com/go/spanner v1.56.0/go.mod h1:DndqtUKQAt3VLuV2Le+9Y3WTnq5cNKrnLb/Piqcj+h0=
cloud.google.com/go/speech v1.21.1/go.mod h1:E5GHZXYQlkqWQwY5xRSLHw2ci5NMQNG52FfMU1aZrIA=
cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA=
cloud.google.com/go/storage v1.38.0/go.mod h1:tlUADB0mAb9BgYls9lq+8MGkfzOXuLrnHXlpHmvFJoY=
cloud.google.com/go/storagetransfer v1.10.4/go.mod h1:vef30rZKu5HSEf/x1tK3WfWrL0XVoUQN/EPDRGPzjZs=
cloud.google.com/go/talent v1.6.6/go.mod h1:y/WQDKrhVz12WagoarpAIyKKMeKGKHWPoReZ0g8tseQ=
cloud.google.com/go/texttospeech v1.7.5/go.mod h1:tzpCuNWPwrNJnEa4Pu5taALuZL4QRRLcb+K9pbhXT6M=
cloud.google.com/go/tpu v1.6.5/go.mod h1:P9DFOEBIBhuEcZhXi+wPoVy/cji+0ICFi4TtTkMHSSs=
cloud.google.com/go/trace v1.10.5/go.mod h1:9hjCV1nGBCtXbAE4YK7OqJ8pmPYSxPA0I67JwRd5s3M=
cloud.google.com/go/translate v1.10.1/go.mod h1:adGZcQNom/3ogU65N9UXHOnnSvjPwA/jKQUMnsYXOyk=
cloud.google.com/go/video v1.20.4/go.mod h1:LyUVjyW+Bwj7dh3UJnUGZfyqjEto9DnrvTe1f/+QrW0=
cloud.google.com/go/videointelligence v1.11.5/go.mod h1:/PkeQjpRponmOerPeJxNPuxvi12HlW7Em0lJO14FC3I=
cloud.google.com/go/vision/v2 v2.8.0/go.mod h1:ocqDiA2j97pvgogdyhoxiQp2ZkDCyr0HWpicywGGRhU=
cloud.google.com/go/vmmigration v1.7.5/go.mod h1:pkvO6huVnVWzkFioxSghZxIGcsstDvYiVCxQ9ZH3eYI=
cloud.google.com/go/vmwareengine v1.1.1/go.mod h1:nMpdsIVkUrSaX8UvmnBhzVzG7PPvNYc5BszcvIVudYs=
cloud.google.com/go/vpcaccess v1.7.5/go.mod h1:slc5ZRvvjP78c2dnL7m4l4R9GwL3wDLcpIWz6P/ziig=
cloud.google.com/go/webrisk v1.9.5/go.mod h1:aako0Fzep1Q714cPEM5E+mtYX8/jsfegAuS8aivxy3U=
cloud.google.com/go/websecurityscanner v1.6.5/go.mod h1:QR+DWaxAz2pWooylsBF854/Ijvuoa3FCyS1zBa1rAVQ=
cloud.google.com/go/workflows v1.12.4/go.mod h1:yQ7HUqOkdJK4duVtMeBCAOPiN1ZF1E9pAMX51vpwB/w=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3 h1:hJiie5Bf3QucGRa4ymsAUOxyhYwGEz1xrsVk0P8erlw=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY=
@ -51,6 +170,8 @@ github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKd
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0=
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/alecthomas/kingpin/v2 v2.3.1/go.mod h1:oYL5vtsvEHZGHxU7DMp32Dvx+qL+ptGn6lWaot2vCNE=
github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU=
github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
@ -64,6 +185,8 @@ github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
@ -74,9 +197,11 @@ github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBT
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 h1:D21IyuvjDCshj1/qq+pCNd3VZOAEI9jy6Bi131YlXgI=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89 h1:aPflPkRFkVwbW6dmcVqfgwp1i+UWGFH6VgR1Jim5Ygc=
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw=
@ -100,6 +225,7 @@ github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk=
github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k=
@ -108,6 +234,8 @@ github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbi
github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa h1:jQCWAUqqlij9Pgj2i/PB79y4KOPYVyFYdROxgaCwdTQ=
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/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM=
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/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d h1:t5Wuyh53qYyg9eqn4BbnlIT+vmhyww0TatL+zT3uWgI=
@ -117,18 +245,23 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o=
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4 h1:rEvIZUSZ3fx39WIi3JkQqQBitGwpELBIYWeBVh6wn+E=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.11.1 h1:wSUXTlLfiAQRWs2F+p+EKOY9rUyis1MyGqJ2DIk5HpM=
github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g=
github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI=
github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0=
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA=
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw=
github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
@ -157,7 +290,10 @@ github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
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.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
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/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
@ -167,6 +303,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/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
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/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
@ -175,6 +312,8 @@ github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk=
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofiber/fiber/v2 v2.52.2 h1:b0rYH6b06Df+4NyrbdptQL8ifuxw/Tf2DgfkZkDaxEo=
github.com/gofiber/fiber/v2 v2.52.2/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
@ -187,51 +326,86 @@ github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/
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/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-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 h1:2hRPrmiwPrp3fQX967rNJIhQPtiGXdlQWAxKbKw3VHA=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-pkcs11 v0.2.1-0.20230907215043-c6f79328ddf9/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0 h1:pMen7vLs8nvgEYhywH3KDWJIJTeEr2ULsVWHWYHQyBs=
github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99 h1:Ak8CrdlwwXwAZxzS66vgPt4U8yUZX7JwLvVR58FN5jM=
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-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
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.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0 h1:WcmKMm43DR7RdtlkEXQJyo5ws8iTp98CyhCCbOHMvNI=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@ -242,7 +416,11 @@ github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab h1:BA4a7pe
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465 h1:KwWnWVWCNtNq/ewIX7HIKnELmEx2nDP42yskD/pi7QE=
github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
github.com/influxdata/influxdb v1.7.6 h1:8mQ7A/V+3noMGCt/P9pD09ISaiz9XvgCk303UYA3gcs=
github.com/iris-contrib/httpexpect/v2 v2.12.1 h1:3cTZSyBBen/kfjCtgNFoUKi1u0FVXNaAjyRJOo6AVS4=
github.com/iris-contrib/httpexpect/v2 v2.12.1/go.mod h1:7+RB6W5oNClX7PTwJgJnsQP3ZuUUYB3u61KCqeSgZ88=
github.com/iris-contrib/jade v1.1.4 h1:WoYdfyJFfZIUgqNAeOyRfTNQZOksSlZ6+FnXR3AEpX0=
github.com/iris-contrib/jade v1.1.4/go.mod h1:EDqR+ur9piDl6DUgs6qRrlfzmlx/D5UybogqrXvJTBE=
github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw=
@ -285,6 +463,8 @@ github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B
github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@ -310,6 +490,7 @@ github.com/lucas-clemente/quic-go v0.25.0/go.mod h1:YtzP8bxRVCBlO77yRanE264+fY/T
github.com/lucas-clemente/quic-go v0.27.1/go.mod h1:AzgQoPda7N+3IqMMMkywBKggIFo2KT6pfnlrQ2QieeI=
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/lyft/protoc-gen-star/v2 v2.0.3/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk=
github.com/mailgun/raymond/v2 v2.0.46 h1:aOYHhvTpF5USySJ0o7cpPno/Uh2I5qg2115K25A+Ft4=
github.com/mailgun/raymond/v2 v2.0.46/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18=
github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw=
@ -331,6 +512,10 @@ github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peK
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
@ -341,6 +526,8 @@ github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXm
github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY=
github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4=
github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -354,10 +541,13 @@ github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJE
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab h1:eFXv9Nu1lGbrNbj619aWwZfVF5HBrm9Plte8aNptuTI=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc=
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.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc=
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.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
@ -372,23 +562,33 @@ github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaF
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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 v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
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.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/quic-go/qtls-go1-20 v0.3.3/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/quic-go/quic-go v0.38.0/go.mod h1:MPCuRq7KBK2hNcfKj/1iD1BGuN3eAYMeNxp3T42LRUg=
github.com/quic-go/quic-go v0.39.4/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
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/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/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48 h1:vabduItPAIz9px5iryD5peyx7O3Ya8TBThapgXim98o=
@ -441,6 +641,7 @@ github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:Udh
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
@ -449,12 +650,16 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tdewolff/minify/v2 v2.12.4 h1:kejsHQMM17n6/gwdw53qsi6lg0TGddZADVyQOz1KMdE=
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/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/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
@ -463,10 +668,14 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.40.0 h1:CRq/00MfruPGFLTQKY8b+8SfdK60TxNztjRMnH0t1Yc=
github.com/valyala/fasthttp v1.40.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/viant/assertly v0.4.8 h1:5x1GzBaRteIwTr5RAGFVG14uNeRFxVNbXPWrK2qAgpc=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0 h1:6TteTDQ68CjgcCe8wH3D3ZhUQQOJXMTbj/D9rkk2a1k=
@ -475,17 +684,38 @@ github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xhit/go-str2duration v1.2.0/go.mod h1:3cPSlfZlUHVlneIVfePFWcJZsuwf+P1v2SRTV4cUmp4=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA=
github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0=
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
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/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.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go4.org v0.0.0-20180809161055-417644f6feb5 h1:+hE86LblG4AyDgwMCLTE6FOlM9+qjHSYS+rKqxUVdsM=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d h1:E2M5QgjZ/Jg+ObCQAudsXxuTsLj7Nl5RV/lZcQZmKSo=
@ -494,13 +724,17 @@ golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20221019170559-20944726eadf/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
@ -508,17 +742,22 @@ golang.org/x/exp v0.0.0-20230306221820-f0f767cdffd6/go.mod h1:CxIveKay+FTh1D0yPZ
golang.org/x/exp v0.0.0-20230807204917-050eac23e9de/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
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-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
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-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -526,12 +765,18 @@ golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220516155154-20f960328961/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
@ -540,8 +785,12 @@ golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
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-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -554,22 +803,35 @@ golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
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.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
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/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/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/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-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -585,13 +847,20 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/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/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-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
@ -611,17 +880,29 @@ golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
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.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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/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-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
@ -632,6 +913,11 @@ golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
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.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
@ -640,6 +926,7 @@ google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w=
google.golang.org/api v0.169.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -654,6 +941,8 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA
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-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-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 h1:PDIOdWxZ8eRizhKa1AAvY53xsvLB1cWorMjslvY3VA8=
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWofoFcihmDEMYeDR9KN/ThbPWGrh++g=
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8=
@ -661,6 +950,8 @@ google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 h1:SeZZZx0cP0fqUyA
google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97/go.mod h1:t1VqOqqvce95G3hIDCT5FeO3YUc6Q4Oe24L/+rNMxRk=
google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe h1:USL2DhxfgRchafRvt/wYyyQNzwgL7ZiURcozOE/Pkvo=
google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro=
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo=
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw=
@ -669,13 +960,38 @@ google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4=
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0=
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8=
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/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
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-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
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.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
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/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
@ -691,7 +1007,10 @@ grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJd
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs=
moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE=
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=

View File

@ -5,8 +5,6 @@ import (
"fmt"
)
// Common Constants, Types, And Utilities
// firstNonIDRune returns the first non-printable or non-ASCII rune and its
// index. If slashes is true, it also looks for slashes. If there are no such
// runes, i is -1.

View File

@ -1,11 +1,27 @@
package agd_test
import (
"strings"
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/golibs/testutil"
)
func TestMain(m *testing.M) {
testutil.DiscardLogOutput(m)
}
// Common long strings for tests.
var (
testLongStr = strings.Repeat("a", 200)
testLongStrUnicode = strings.Repeat("ы", 200)
)
// Common IDs for tests.
const (
testHumanIDStr = "My-Device-X--10"
testHumanIDLowerStr = "my-device-x--10"
testHumanID agd.HumanID = testHumanIDStr
)

View File

@ -10,8 +10,6 @@ import (
"github.com/AdguardTeam/golibs/netutil"
)
// Devices
// Device is a device of a device attached to a profile.
//
// NOTE: Do not change fields of this structure without incrementing
@ -30,6 +28,10 @@ type Device struct {
// Name is the human-readable name of the device.
Name DeviceName
// HumanIDLower is the lower-case version of the normalized [HumanID] used
// to create this device, if any.
HumanIDLower HumanIDLower
// DedicatedIPs are the destination (server) IP-addresses dedicated to this
// device, if any. A device can use one of these addresses as a DNS server
// address for AdGuard DNS to recognize it.

View File

@ -1,7 +1,6 @@
package agd_test
import (
"strings"
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
@ -12,9 +11,6 @@ import (
func TestNewDeviceName(t *testing.T) {
t.Parallel()
tooLong := strings.Repeat("a", 200)
tooLongUnicode := strings.Repeat("ы", 200)
testCases := []struct {
name string
in string
@ -33,12 +29,12 @@ func TestNewDeviceName(t *testing.T) {
wantErrMsg: "",
}, {
name: "too_long",
in: tooLong,
wantErrMsg: `bad device name "` + tooLong + `": too long: got 200 runes, max 128`,
in: testLongStr,
wantErrMsg: `bad device name "` + testLongStr + `": too long: got 200 runes, max 128`,
}, {
name: "too_long_unicode",
in: tooLongUnicode,
wantErrMsg: `bad device name "` + tooLongUnicode + `": too long: got 200 runes, max 128`,
in: testLongStrUnicode,
wantErrMsg: `bad device name "` + testLongStrUnicode + `": too long: got 200 runes, max 128`,
}}
for _, tc := range testCases {

View File

@ -0,0 +1,82 @@
package agd
import (
"fmt"
"strings"
"github.com/AdguardTeam/golibs/errors"
)
// DeviceType is a type of a device as used in the Backend API.
type DeviceType uint8
// DeviceType values.
//
// Do not change the order. Keep in sync with the Backend API.
const (
DeviceTypeNone DeviceType = 0
DeviceTypeWindows DeviceType = 1
DeviceTypeAndroid DeviceType = 2
DeviceTypeMacOS DeviceType = 3
DeviceTypeIOS DeviceType = 4
DeviceTypeLinux DeviceType = 5
DeviceTypeRouter DeviceType = 6
DeviceTypeSmartTV DeviceType = 7
DeviceTypeGameConsole DeviceType = 8
DeviceTypeOther DeviceType = 9
)
// deviceTypeStrings is a mapping between a device type and its default string
// representation. Keep in sync with the DNS API.
var deviceTypeStrings = []string{
DeviceTypeNone: "(none)",
DeviceTypeWindows: "win",
DeviceTypeAndroid: "adr",
DeviceTypeMacOS: "mac",
DeviceTypeIOS: "ios",
DeviceTypeLinux: "lnx",
DeviceTypeRouter: "rtr",
DeviceTypeSmartTV: "stv",
DeviceTypeGameConsole: "gam",
DeviceTypeOther: "otr",
}
// DeviceTypeFromDNS converts a string into a valid device type. s is assumed
// to be from a DNS FQDN or HTTP path. The matching is case-insensitive, and
// "(none)", the string representation of [DeviceTypeNone], is not recognized,
// since it's not a valid type in the DNS API.
func DeviceTypeFromDNS(s string) (dt DeviceType, err error) {
// Do not use [errors.Annotate] here, because it allocates even when the
// error is nil.
defer func() {
if err != nil {
err = fmt.Errorf("bad device type %q: %w", s, err)
}
}()
err = ValidateInclusion(len(s), 3, 3, UnitByte)
if err != nil {
// The error will be wrapped by the deferred helper.
return DeviceTypeNone, err
}
for i, dtStr := range deviceTypeStrings[1:] {
if strings.EqualFold(s, dtStr) {
return DeviceType(i + 1), nil
}
}
return DeviceTypeNone, errors.Error("unknown device type")
}
// type check
var _ fmt.Stringer = DeviceTypeNone
// String implements the [fmt.Stringer] interface for DeviceType.
func (dt DeviceType) String() (s string) {
if int(dt) < len(deviceTypeStrings) {
return deviceTypeStrings[dt]
}
return fmt.Sprintf("!bad_device_type_%d", dt)
}

View File

@ -0,0 +1,68 @@
package agd_test
import (
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
)
func TestDeviceTypeFromDNS(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
in string
wantErrMsg string
want agd.DeviceType
}{{
name: "success",
in: "adr",
wantErrMsg: "",
want: agd.DeviceTypeAndroid,
}, {
name: "success_case",
in: "Adr",
wantErrMsg: "",
want: agd.DeviceTypeAndroid,
}, {
name: "too_long",
in: "windows",
wantErrMsg: `bad device type "windows": too long: got 7 bytes, max 3`,
want: agd.DeviceTypeNone,
}, {
name: "too_small",
in: "x",
wantErrMsg: `bad device type "x": too short: got 1 bytes, min 3`,
want: agd.DeviceTypeNone,
}, {
name: "none",
in: "(none)",
wantErrMsg: `bad device type "(none)": too long: got 6 bytes, max 3`,
want: agd.DeviceTypeNone,
}, {
name: "unknown",
in: "xxx",
wantErrMsg: `bad device type "xxx": unknown device type`,
want: agd.DeviceTypeNone,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := agd.DeviceTypeFromDNS(tc.in)
assert.Equal(t, tc.want, got)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
})
}
}
func TestDeviceType_String(t *testing.T) {
t.Parallel()
assert.Equal(t, "(none)", agd.DeviceTypeNone.String())
assert.Equal(t, "adr", agd.DeviceTypeAndroid.String())
assert.Equal(t, "!bad_device_type_42", agd.DeviceType(42).String())
}

View File

@ -12,8 +12,6 @@ import (
func TestNewFilterListID(t *testing.T) {
t.Parallel()
tooLong := strings.Repeat("a", agd.MaxFilterListIDLen+1)
testCases := []struct {
name string
in string
@ -28,8 +26,8 @@ func TestNewFilterListID(t *testing.T) {
wantErrMsg: `bad filter list id "": too short: got 0 bytes, min 1`,
}, {
name: "too_long",
in: tooLong,
wantErrMsg: `bad filter list id "` + tooLong + `": too long: got 129 bytes, max 128`,
in: testLongStr,
wantErrMsg: `bad filter list id "` + testLongStr + `": too long: got 200 bytes, max 128`,
}, {
name: "bad",
in: "bad/name",

279
internal/agd/humanid.go Normal file
View File

@ -0,0 +1,279 @@
package agd
import (
"bytes"
"fmt"
"strings"
"unicode/utf8"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/syncutil"
)
// HumanID is a more human-readable identifier of a device.
type HumanID string
const (
// MaxHumanIDLen is the maximum length of a human-readable device ID.
MaxHumanIDLen = netutil.MaxDomainLabelLen
// MinHumanIDLen is the minimum length of a human-readable device ID.
MinHumanIDLen = 1
)
// NewHumanID converts a simple string into a HumanID and makes sure that it's
// valid. This should be preferred to a simple type conversion.
//
// TODO(a.garipov): Remove if it remains unused.
func NewHumanID(s string) (id HumanID, err error) {
// Do not use [errors.Annotate] here, because it allocates even when the
// error is nil.
defer func() {
if err != nil {
err = fmt.Errorf("bad human id %q: %w", s, err)
}
}()
return newHumanID(s)
}
// newHumanID converts a simple string into a HumanID and makes sure that it's
// valid. It does not wrap the error to be used in places where that could
// create additional allocations.
func newHumanID(s string) (id HumanID, err error) {
err = ValidateInclusion(len(s), MaxHumanIDLen, MinHumanIDLen, UnitByte)
if err != nil {
// Don't wrap the error, because the caller should do that.
return "", err
}
// TODO(a.garipov): Add boolean versions to netutil to avoid allocations of
// errors that aren't used.
err = netutil.ValidateHostnameLabel(s)
if err != nil {
// Don't wrap the error, because the caller should do that.
return "", err
}
if i := strings.Index(s, "---"); i >= 0 {
return "", fmt.Errorf("at index %d: max 2 consecutive hyphens are allowed", i)
}
return HumanID(s), nil
}
// HumanIDLower is the type for [HumanID] values that must be lowercase.
type HumanIDLower string
// NewHumanIDLower converts a simple string into a HumanIDLower and makes sure
// that it's valid and lowercased. This should be preferred to a simple type
// conversion.
func NewHumanIDLower(s string) (id HumanIDLower, err error) {
// Do not use [errors.Annotate] here, because it allocates even when the
// error is nil.
humanID, err := newHumanID(s)
if err != nil {
return "", fmt.Errorf("bad lowercase human id %q: %w", s, err)
}
for i, r := range humanID {
if r >= 'A' && r <= 'Z' {
return "", fmt.Errorf(
"bad lowercase human id %q: at index %d: %q is not lowercase",
s,
i,
r,
)
}
}
return HumanIDLower(s), nil
}
// HumanIDToLower returns a lowercase version of id.
func HumanIDToLower(id HumanID) (lower HumanIDLower) {
return HumanIDLower(strings.ToLower(string(id)))
}
// HumanIDParser normalizes and parses a HumanID from a string.
type HumanIDParser struct {
pool *syncutil.Pool[bytes.Buffer]
}
// NewHumanIDParser creates a new HumanIDParser.
func NewHumanIDParser() (p *HumanIDParser) {
return &HumanIDParser{
pool: syncutil.NewPool(func() (buf *bytes.Buffer) {
return bytes.NewBuffer(make([]byte, 0, netutil.MaxDomainNameLen))
}),
}
}
// ParseNormalized normalizes and parses a HumanID from a string that may have
// issues, such as extra symbols that aren't supported. The normalization is
// best-effort and may still fail, in which case id is empty and err is not nil.
func (p *HumanIDParser) ParseNormalized(s string) (id HumanID, err error) {
id, err = newHumanID(s)
if err == nil {
return id, nil
}
// Do not use [errors.Annotate] here, because it allocates even when the
// error is nil.
original := s
defer func() {
if err != nil {
err = fmt.Errorf("bad non-normalized human id %q: %w", original, err)
}
}()
// Immediately validate it against the upper DNS hostname-length limit.
err = ValidateInclusion(len(s), netutil.MaxDomainNameLen, MinHumanIDLen, UnitByte)
if err != nil {
// Don't wrap the error, because there is already a deferred wrap, and
// the error is informative enough as is.
return "", err
}
buf := p.pool.Get()
defer func() { p.pool.Put(buf) }()
buf.Reset()
n := humanIDNormalizer{
buf: buf,
}
for s != "" {
r, sz := utf8.DecodeRuneInString(s)
s = s[sz:]
n.next(r)
}
s = n.result()
if s == "" || s == "-" {
return "", errors.Error("cannot normalize")
}
id, err = newHumanID(s)
if err != nil {
return "", err
}
return id, nil
}
// humanIDNormalizer is a stateful normalizer of human-readable device
// identifiers.
type humanIDNormalizer struct {
buf *bytes.Buffer
state uint8
prevRune rune
prevPrevRune rune
}
// [humanIDNormalizer] states.
const (
humanIDNormStateInitial uint8 = iota
humanIDNormStateInvalid
humanIDNormStateValid
)
// next writes r to the buffer, if it is valid.
func (p *humanIDNormalizer) next(r rune) {
switch p.state {
case humanIDNormStateInitial:
p.nextInitial(r)
case humanIDNormStateValid:
p.nextValid(r)
case humanIDNormStateInvalid:
p.nextInvalid(r)
default:
panic(fmt.Errorf("bad humanIDNormalizer state %d", p.state))
}
}
// nextInitial processes the initial state of the normalizer.
func (p *humanIDNormalizer) nextInitial(r rune) {
if !netutil.IsValidHostOuterRune(r) {
return
}
p.state = humanIDNormStateValid
p.write(r)
}
// nextValid processes the valid state of the normalizer.
func (p *humanIDNormalizer) nextValid(r rune) {
if r == '-' {
if p.prevPrevRune == '-' && p.prevRune == '-' {
p.buf.Truncate(p.buf.Len() - 2)
p.prevPrevRune = utf8.RuneError
p.prevRune = utf8.RuneError
p.state = humanIDNormStateInvalid
return
}
p.write(r)
return
}
if !netutil.IsValidHostOuterRune(r) {
p.truncateHyphens()
p.state = humanIDNormStateInvalid
return
}
p.write(r)
}
// truncateHyphens removes the unnecessary hyphens from the buffer if necessary.
func (p *humanIDNormalizer) truncateHyphens() {
if p.prevRune != '-' {
return
}
if p.prevPrevRune == '-' {
p.buf.Truncate(p.buf.Len() - 2)
p.prevPrevRune = utf8.RuneError
} else {
p.buf.Truncate(p.buf.Len() - 1)
}
p.prevRune = utf8.RuneError
}
// nextInvalid processes the invalid state of the normalizer.
func (p *humanIDNormalizer) nextInvalid(r rune) {
if !netutil.IsValidHostOuterRune(r) {
return
}
p.state = humanIDNormStateValid
if p.prevRune != '-' {
p.write('-')
}
p.write(r)
}
// write writes r to the buffer while also updating the previous runes.
func (p *humanIDNormalizer) write(r rune) {
_, _ = p.buf.WriteRune(r)
p.prevPrevRune = p.prevRune
p.prevRune = r
}
// result returns the result of the normalization.
func (p *humanIDNormalizer) result() (s string) {
b := p.buf.Bytes()
b = b[:min(len(b), MaxHumanIDLen)]
b = bytes.TrimRight(b, "-")
return string(b)
}

View File

@ -0,0 +1,275 @@
package agd_test
import (
"strings"
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewHumanID(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
in string
wantErrMsg string
}{{
name: "success",
in: testHumanIDStr,
wantErrMsg: "",
}, {
name: "too_long",
in: testLongStr,
wantErrMsg: `bad human id "` + testLongStr + `": too long: got 200 bytes, max 63`,
}, {
name: "too_small",
in: "",
wantErrMsg: `bad human id "": too short: got 0 bytes, min 1`,
}, {
name: "bad_start",
in: "-My-Device-X--10",
wantErrMsg: `bad human id "-My-Device-X--10": bad hostname label "-My-Device-X--10": ` +
`bad hostname label rune '-'`,
}, {
name: "bad_middle",
in: "My-Device-X---10",
wantErrMsg: `bad human id "My-Device-X---10": at index 11: ` +
`max 2 consecutive hyphens are allowed`,
}, {
name: "bad_rune",
in: "My-Device-X--10!",
wantErrMsg: `bad human id "My-Device-X--10!": bad hostname label "My-Device-X--10!": ` +
`bad hostname label rune '!'`,
}, {
name: "bad_end",
in: "My-Device-X--10-",
wantErrMsg: `bad human id "My-Device-X--10-": bad hostname label "My-Device-X--10-": ` +
`bad hostname label rune '-'`,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
id, err := agd.NewHumanID(tc.in)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
if tc.wantErrMsg == "" {
assert.Equal(t, tc.in, string(id))
}
})
}
}
func TestNewHumanIDLower(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
in string
wantErrMsg string
}{{
name: "success",
in: testHumanIDLowerStr,
wantErrMsg: "",
}, {
name: "bad_case",
in: "my-device-X--10",
wantErrMsg: `bad lowercase human id "my-device-X--10": at index 10: 'X' is not lowercase`,
}, {
name: "bad_rune",
in: "My-Device-X--10!",
wantErrMsg: `bad lowercase human id "My-Device-X--10!": ` +
`bad hostname label "My-Device-X--10!": bad hostname label rune '!'`,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
id, err := agd.NewHumanIDLower(tc.in)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
if tc.wantErrMsg == "" {
assert.Equal(t, tc.in, string(id))
}
})
}
}
func TestHumanIDParser_ParseNormalized(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
in string
wantErrMsg string
wantID agd.HumanID
}{{
name: "success",
in: testHumanIDStr,
wantErrMsg: "",
wantID: testHumanID,
}, {
name: "too_long",
in: testLongStr,
wantErrMsg: "",
wantID: agd.HumanID(strings.Repeat("a", agd.MaxHumanIDLen)),
}, {
name: "too_small",
in: "",
wantErrMsg: `bad non-normalized human id "": too short: got 0 bytes, min 1`,
}, {
name: "bad_start",
in: "-My-Device-X--10",
wantErrMsg: "",
wantID: testHumanID,
}, {
name: "bad_middle",
in: "My-Device-X---10",
wantErrMsg: "",
wantID: "My-Device-X-10",
}, {
name: "bad_rune",
in: "My-Device-X--10!",
wantErrMsg: "",
wantID: testHumanID,
}, {
name: "bad_end",
in: "My-Device-X--10-",
wantErrMsg: "",
wantID: testHumanID,
}, {
name: "bad_chars_start",
in: "абв-My-Device-X--10",
wantErrMsg: "",
wantID: testHumanID,
}, {
name: "bad_chars_end",
in: "My-Device-X--10-абв",
wantErrMsg: "",
wantID: testHumanID,
}, {
name: "bad_chars_middle",
in: "My-Device-Xабв10",
wantErrMsg: "",
wantID: "My-Device-X-10",
}, {
name: "bad_chars_middle_hyphens",
in: "My-Device-X-абв-10",
wantErrMsg: "",
wantID: "My-Device-X-10",
}, {
name: "bad_chars_middle_two_hyphens",
in: "My-Device-X--абв--10",
wantErrMsg: "",
wantID: "My-Device-X-10",
}, {
name: "bad_chars_middle_two_hyphens_other",
in: "My-DeviceабвX--10",
wantErrMsg: "",
wantID: testHumanID,
}, {
name: "one_bad_char",
in: "!",
wantErrMsg: `bad non-normalized human id "!": cannot normalize`,
wantID: "",
}, {
name: "only_bad_chars",
in: "!!!",
wantErrMsg: `bad non-normalized human id "!!!": cannot normalize`,
wantID: "",
}}
p := agd.NewHumanIDParser()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
id, err := p.ParseNormalized(tc.in)
assert.Equalf(t, tc.wantID, id, "original: %q", tc.in)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
})
}
}
// Sinks for benchmarks.
var (
humanIDSink agd.HumanID
errSink error
)
func BenchmarkHumanIDParser_ParseNormalized(b *testing.B) {
benchCases := []struct {
name string
in string
wantErrPresent bool
}{{
name: "valid",
in: testHumanIDStr,
wantErrPresent: false,
}, {
name: "normalized",
in: testHumanIDStr + "-!!!",
wantErrPresent: false,
}, {
name: "normalized_long",
in: testLongStr,
wantErrPresent: false,
}, {
name: "bad",
in: "!!!",
wantErrPresent: true,
}}
for _, bc := range benchCases {
b.Run(bc.name, func(b *testing.B) {
p := agd.NewHumanIDParser()
b.ReportAllocs()
b.ResetTimer()
for range b.N {
humanIDSink, errSink = p.ParseNormalized(bc.in)
}
if bc.wantErrPresent {
require.Empty(b, humanIDSink)
require.Error(b, errSink)
} else {
require.NotEmpty(b, humanIDSink)
require.NoError(b, errSink)
}
})
}
// Most recent result, on a ThinkPad X13 with a Ryzen Pro 7 CPU:
// goos: linux
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/agd
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkHumanIDParser_ParseNormalized/valid-16 23716176 51.48 ns/op 0 B/op 0 allocs/op
// BenchmarkHumanIDParser_ParseNormalized/normalized-16 1000000 1071 ns/op 88 B/op 3 allocs/op
// BenchmarkHumanIDParser_ParseNormalized/normalized_long-16 504042 4086 ns/op 128 B/op 4 allocs/op
// BenchmarkHumanIDParser_ParseNormalized/bad-16 752247 1361 ns/op 184 B/op 5 allocs/op
}
func FuzzHumanIDParser_ParseNormalized(f *testing.F) {
p := agd.NewHumanIDParser()
f.Fuzz(func(t *testing.T, input string) {
s, err := p.ParseNormalized(input)
if err != nil {
require.Empty(t, s)
return
}
assert.NotEmpty(t, s)
assert.LessOrEqual(t, len(s), agd.MaxHumanIDLen)
})
}

View File

@ -11,8 +11,6 @@ import (
"github.com/AdguardTeam/golibs/errors"
)
// Profiles
// Profile contains information about an AdGuard DNS profile. In other parts of
// the infrastructure, a profile is also called a “DNS server”. We call it
// profile, because it's less confusing.
@ -139,6 +137,13 @@ type Profile struct {
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
IPLogEnabled bool
// AutoDevicesEnabled shows if the automatic creation of devices using
// HumanIDs should be enabled for this profile.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
AutoDevicesEnabled bool
}
// ProfileID is the ID of a profile. It is an opaque string.

View File

@ -82,25 +82,3 @@ func (c *LRU[K, T]) Len() (n int) {
return c.cache.Len(checkExpired)
}
// Manager manages caches.
type Manager struct {
caches map[string]Clearer
}
// NewManager returns a new initialized *Manager.
func NewManager() (m *Manager) {
return &Manager{
caches: map[string]Clearer{},
}
}
// Add adds cache by id.
func (m *Manager) Add(id string, cache Clearer) {
m.caches[id] = cache
}
// ClearByID clears cache by id.
func (m *Manager) ClearByID(id string) {
m.caches[id].Clear()
}

View File

@ -35,35 +35,3 @@ func TestLRU(t *testing.T) {
assert.Equal(t, 0, cache.Len())
}
func TestManager(t *testing.T) {
const (
key = "key"
val = 123
id = "cacheID"
nonExistingKey = "nonExistingKey"
)
cache := agdcache.NewLRU[string, int](&agdcache.LRUConfig{
Size: 10,
})
cache.Set(key, val)
assert.Equal(t, 1, cache.Len())
v, ok := cache.Get(key)
assert.Equal(t, val, v)
assert.True(t, ok)
v, ok = cache.Get(nonExistingKey)
assert.Equal(t, 0, v)
assert.False(t, ok)
m := agdcache.NewManager()
m.Add(id, cache)
m.ClearByID(id)
assert.Equal(t, 0, cache.Len())
}

View File

@ -0,0 +1,89 @@
package agdcache
import (
"slices"
"sync"
"golang.org/x/exp/maps"
)
// Manager is the cache manager interface. All methods must be safe for
// concurrent use.
type Manager interface {
// Add adds cache by id. cache must not be nil.
//
// TODO(s.chzhen): Add Set method that rewrites the cache associated with
// id (current Add implementation).
//
// TODO(s.chzhen): Add panic on adding cache with duplicate id.
Add(id string, cache Clearer)
// ClearByID clears cache by id.
ClearByID(id string)
}
// DefaultManager implements the [Manager] interface that stores caches and can
// clear them by id
type DefaultManager struct {
mu *sync.Mutex
caches map[string]Clearer
}
// NewDefaultManager returns a new initialized *DefaultManager.
func NewDefaultManager() (m *DefaultManager) {
return &DefaultManager{
mu: &sync.Mutex{},
caches: map[string]Clearer{},
}
}
// type check
var _ Manager = (*DefaultManager)(nil)
// Add implements the [Manager] interface for *DefaultManager. Note that it
// replaces the saved cache with the same id if there is one.
func (m *DefaultManager) Add(id string, cache Clearer) {
m.mu.Lock()
defer m.mu.Unlock()
m.caches[id] = cache
}
// ClearByID implements the [Manager] interface for *DefaultManager.
func (m *DefaultManager) ClearByID(id string) {
cache := m.findByID(id)
if cache != nil {
cache.Clear()
}
}
// findByID returns the stored cache by id or nil.
func (m *DefaultManager) findByID(id string) (cache Clearer) {
m.mu.Lock()
defer m.mu.Unlock()
return m.caches[id]
}
// IDs returns a sorted list of stored cache identifiers.
func (m *DefaultManager) IDs() (ids []string) {
m.mu.Lock()
defer m.mu.Unlock()
ids = maps.Keys(m.caches)
slices.Sort(ids)
return ids
}
// EmptyManager implements the [Manager] interface that does nothing.
type EmptyManager struct{}
// type check
var _ Manager = EmptyManager{}
// Add implements the [Manager] interface for *EmptyManager.
func (EmptyManager) Add(_ string, _ Clearer) {}
// ClearByID implements the [Manager] interface for *EmptyManager.
func (EmptyManager) ClearByID(_ string) {}

View File

@ -0,0 +1,43 @@
package agdcache_test
import (
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/stretchr/testify/assert"
)
func TestManager(t *testing.T) {
const (
cacheID = "cacheID"
cacheIDNonExisting = "non_existing_cache_id"
)
isCleared := false
mc := &mockClearer{
onClear: func() {
isCleared = true
},
}
m := agdcache.NewDefaultManager()
m.Add(cacheID, mc)
m.ClearByID(cacheID)
assert.True(t, isCleared)
assert.NotPanics(t, func() { m.ClearByID(cacheIDNonExisting) })
}
// mockClearer is the mock implementation of the [agdcache.Clearer] for tests.
type mockClearer struct {
onClear func()
}
// type check
var _ agdcache.Clearer = (*mockClearer)(nil)
// Clear implements the [agdcache.Clearer] interface for *mockClearer.
func (mc *mockClearer) Clear() {
mc.onClear()
}

View File

@ -284,20 +284,55 @@ var _ profiledb.Interface = (*ProfileDB)(nil)
// ProfileDB is a [profiledb.Interface] for tests.
type ProfileDB struct {
OnProfileByDeviceID func(
OnCreateAutoDevice func(
ctx context.Context,
id agd.DeviceID,
id agd.ProfileID,
humanID agd.HumanID,
devType agd.DeviceType,
) (p *agd.Profile, d *agd.Device, err error)
OnProfileByDedicatedIP func(
ctx context.Context,
ip netip.Addr,
) (p *agd.Profile, d *agd.Device, err error)
OnProfileByDeviceID func(
ctx context.Context,
id agd.DeviceID,
) (p *agd.Profile, d *agd.Device, err error)
OnProfileByHumanID func(
ctx context.Context,
id agd.ProfileID,
humanID agd.HumanIDLower,
) (p *agd.Profile, d *agd.Device, err error)
OnProfileByLinkedIP func(
ctx context.Context,
ip netip.Addr,
) (p *agd.Profile, d *agd.Device, err error)
}
// CreateAutoDevice implements the [profiledb.Interface] interface for
// *ProfileDB.
func (db *ProfileDB) CreateAutoDevice(
ctx context.Context,
id agd.ProfileID,
humanID agd.HumanID,
devType agd.DeviceType,
) (p *agd.Profile, d *agd.Device, err error) {
return db.OnCreateAutoDevice(ctx, id, humanID, devType)
}
// ProfileByDedicatedIP implements the [profiledb.Interface] interface for
// *ProfileDB.
func (db *ProfileDB) ProfileByDedicatedIP(
ctx context.Context,
ip netip.Addr,
) (p *agd.Profile, d *agd.Device, err error) {
return db.OnProfileByDedicatedIP(ctx, ip)
}
// ProfileByDeviceID implements the [profiledb.Interface] interface for
// *ProfileDB.
func (db *ProfileDB) ProfileByDeviceID(
@ -307,13 +342,14 @@ func (db *ProfileDB) ProfileByDeviceID(
return db.OnProfileByDeviceID(ctx, id)
}
// ProfileByDedicatedIP implements the [profiledb.Interface] interface for
// ProfileByHumanID implements the [profiledb.Interface] interface for
// *ProfileDB.
func (db *ProfileDB) ProfileByDedicatedIP(
func (db *ProfileDB) ProfileByHumanID(
ctx context.Context,
ip netip.Addr,
id agd.ProfileID,
humanID agd.HumanIDLower,
) (p *agd.Profile, d *agd.Device, err error) {
return db.OnProfileByDedicatedIP(ctx, ip)
return db.OnProfileByHumanID(ctx, id, humanID)
}
// ProfileByLinkedIP implements the [profiledb.Interface] interface for
@ -328,19 +364,33 @@ func (db *ProfileDB) ProfileByLinkedIP(
// type check
var _ profiledb.Storage = (*ProfileStorage)(nil)
// ProfileStorage is a profiledb.Storage for tests.
// ProfileStorage is a [profiledb.Storage] implementation for tests.
type ProfileStorage struct {
OnCreateAutoDevice func(
ctx context.Context,
req *profiledb.StorageCreateAutoDeviceRequest,
) (resp *profiledb.StorageCreateAutoDeviceResponse, err error)
OnProfiles func(
ctx context.Context,
req *profiledb.StorageRequest,
) (resp *profiledb.StorageResponse, err error)
req *profiledb.StorageProfilesRequest,
) (resp *profiledb.StorageProfilesResponse, err error)
}
// CreateAutoDevice implements the [profiledb.Storage] interface for
// *ProfileStorage.
func (s *ProfileStorage) CreateAutoDevice(
ctx context.Context,
req *profiledb.StorageCreateAutoDeviceRequest,
) (resp *profiledb.StorageCreateAutoDeviceResponse, err error) {
return s.OnCreateAutoDevice(ctx, req)
}
// Profiles implements the [profiledb.Storage] interface for *ProfileStorage.
func (s *ProfileStorage) Profiles(
ctx context.Context,
req *profiledb.StorageRequest,
) (resp *profiledb.StorageResponse, err error) {
req *profiledb.StorageProfilesRequest,
) (resp *profiledb.StorageProfilesResponse, err error) {
return s.OnProfiles(ctx, req)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,6 @@
// Package backendpb contains the protobuf structures for the backend API.
//
// TODO(a.garipov): Move the generated code into a separate package.
package backendpb
import (
@ -7,9 +9,11 @@ import (
"net/url"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/golibs/httphdr"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)
// newClient returns new properly initialized DNSServiceClient.
@ -25,7 +29,7 @@ func newClient(apiURL *url.URL) (client DNSServiceClient, err error) {
return nil, fmt.Errorf("bad grpc url scheme %q", s)
}
conn, err := grpc.Dial(apiURL.Host, grpc.WithTransportCredentials(creds))
conn, err := grpc.NewClient(apiURL.Host, grpc.WithTransportCredentials(creds))
if err != nil {
return nil, fmt.Errorf("dialing: %w", err)
}
@ -41,3 +45,17 @@ func newClient(apiURL *url.URL) (client DNSServiceClient, err error) {
func reportf(ctx context.Context, errColl errcoll.Interface, format string, args ...any) {
errcoll.Collectf(ctx, errColl, "backendpb: "+format, args...)
}
// ctxWithAuthentication adds the API key authentication header to the outgoing
// request context if apiKey is not empty. If it is empty, ctx is parent.
func ctxWithAuthentication(parent context.Context, apiKey string) (ctx context.Context) {
ctx = parent
if apiKey == "" {
return ctx
}
// TODO(a.garipov): Better validations for the key.
md := metadata.Pairs(httphdr.Authorization, fmt.Sprintf("Bearer %s", apiKey))
return metadata.NewOutgoingContext(ctx, md)
}

View File

@ -0,0 +1,37 @@
package backendpb
import (
"net/netip"
"testing"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/golibs/testutil"
)
func TestMain(m *testing.M) {
testutil.DiscardLogOutput(m)
}
// Common IDs for tests and their string forms.
//
// TODO(a.garipov): Move all or most tests into external and unexport these.
const (
TestDeviceIDStr = "dev1234"
TestHumanIDStr = "My-Device-X--10"
TestHumanIDLowerStr = "my-device-x--10"
TestProfileIDStr = "prof1234"
TestDeviceID agd.DeviceID = TestDeviceIDStr
TestHumanID agd.HumanID = TestHumanIDStr
TestHumanIDLower agd.HumanIDLower = TestHumanIDLowerStr
TestProfileID agd.ProfileID = TestProfileIDStr
)
// TestUpdTime is the common update time for tests.
var TestUpdTime = time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
// TestBind includes any IPv4 address.
//
// TODO(a.garipov): Add to golibs/netutil.
var TestBind = netip.MustParsePrefix("0.0.0.0/0")

View File

@ -1,6 +1,14 @@
package backendpb_test
import "github.com/AdguardTeam/AdGuardDNS/internal/backendpb"
import (
"context"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/backendpb"
)
// testTimeout is the common timeout for tests.
const testTimeout = 1 * time.Second
// testDNSServiceServer is the [backendpb.DNSServiceServer] for tests.
//
@ -8,10 +16,18 @@ import "github.com/AdguardTeam/AdGuardDNS/internal/backendpb"
// test.
type testDNSServiceServer struct {
backendpb.UnimplementedDNSServiceServer
//lint:ignore ST1003 Keep in sync with the generated code.
OnCreateDeviceByHumanId func(
ctx context.Context,
req *backendpb.CreateDeviceRequest,
) (resp *backendpb.CreateDeviceResponse, err error)
OnGetDNSProfiles func(
req *backendpb.DNSProfilesRequest,
srv backendpb.DNSService_GetDNSProfilesServer,
) (err error)
OnSaveDevicesBillingStat func(
srv backendpb.DNSService_SaveDevicesBillingStatServer,
) (err error)
@ -20,6 +36,17 @@ type testDNSServiceServer struct {
// type check
var _ backendpb.DNSServiceServer = (*testDNSServiceServer)(nil)
// CreateDeviceByHumanId implements the [backendpb.DNSServiceServer] interface
// for *testDNSServiceServer.
//
//lint:ignore ST1003 Keep in sync with the generated code.
func (s *testDNSServiceServer) CreateDeviceByHumanId(
ctx context.Context,
req *backendpb.CreateDeviceRequest,
) (resp *backendpb.CreateDeviceResponse, err error) {
return s.OnCreateDeviceByHumanId(ctx, req)
}
// GetDNSProfiles implements the [backendpb.DNSServiceServer] interface for
// *testDNSServiceServer
func (s *testDNSServiceServer) GetDNSProfiles(

View File

@ -23,21 +23,24 @@ type BillStatConfig struct {
// Endpoint is the backend API URL. The scheme should be either "grpc" or
// "grpcs".
Endpoint *url.URL
// APIKey is the API key used for authentication, if any.
APIKey string
}
// NewBillStat creates a new billing statistics uploader. c must not be nil.
func NewBillStat(c *BillStatConfig) (b *BillStat, err error) {
b = &BillStat{
errColl: c.ErrColl,
}
b.client, err = newClient(c.Endpoint)
client, err := newClient(c.Endpoint)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
return b, nil
return &BillStat{
errColl: c.ErrColl,
client: client,
apiKey: c.APIKey,
}, nil
}
// BillStat is the implementation of the [billstat.Uploader] interface that
@ -48,9 +51,8 @@ func NewBillStat(c *BillStatConfig) (b *BillStat, err error) {
// backendpb.Client.
type BillStat struct {
errColl errcoll.Interface
// client is the current GRPC client.
client DNSServiceClient
apiKey string
}
// type check
@ -62,9 +64,10 @@ func (b *BillStat) Upload(ctx context.Context, records billstat.Records) (err er
return nil
}
ctx = ctxWithAuthentication(ctx, b.apiKey)
stream, err := b.client.SaveDevicesBillingStat(ctx)
if err != nil {
return fmt.Errorf("opening stream: %w", err)
return fmt.Errorf("opening stream: %w", fixGRPCError(err))
}
for deviceID, record := range records {
@ -76,7 +79,7 @@ func (b *BillStat) Upload(ctx context.Context, records billstat.Records) (err er
sendErr := stream.Send(recordToProtobuf(record, deviceID))
if sendErr != nil {
return fmt.Errorf("uploading device %q record: %w", deviceID, sendErr)
return fmt.Errorf("uploading device %q record: %w", deviceID, fixGRPCError(sendErr))
}
}

View File

@ -24,6 +24,8 @@ import (
)
func TestBillStat_Upload(t *testing.T) {
t.Parallel()
const (
wantDeviceID = "test"
invalidDeviceID = "invalid"
@ -43,6 +45,20 @@ func TestBillStat_Upload(t *testing.T) {
}
srv := &testDNSServiceServer{
OnCreateDeviceByHumanId: func(
ctx context.Context,
req *backendpb.CreateDeviceRequest,
) (resp *backendpb.CreateDeviceResponse, err error) {
panic("not implemented")
},
OnGetDNSProfiles: func(
req *backendpb.DNSProfilesRequest,
srv backendpb.DNSService_GetDNSProfilesServer,
) (err error) {
panic("not implemented")
},
OnSaveDevicesBillingStat: func(
srv backendpb.DNSService_SaveDevicesBillingStatServer,
) (err error) {

View File

@ -0,0 +1,161 @@
package backendpb
import (
"context"
"fmt"
"net/netip"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdpasswd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdprotobuf"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/golibs/netutil"
)
// devicesToInternal is a helper that converts the devices from protobuf to
// AdGuard DNS devices.
func devicesToInternal(
ctx context.Context,
ds []*DeviceSettings,
bindSet netutil.SubnetSet,
errColl errcoll.Interface,
) (out []*agd.Device, ids []agd.DeviceID) {
l := len(ds)
if l == 0 {
return nil, nil
}
out = make([]*agd.Device, 0, l)
for _, d := range ds {
dev, err := d.toInternal(bindSet)
if err != nil {
var id string
if d != nil {
id = d.Id
}
reportf(ctx, errColl, "bad device settings for device with id %q: %w", id, err)
metrics.DevicesInvalidTotal.Inc()
continue
}
ids = append(ids, dev.ID)
out = append(out, dev)
}
return out, ids
}
// toInternal is a helper that converts device settings from backend protobuf
// response to AdGuard DNS device object.
func (ds *DeviceSettings) toInternal(bindSet netutil.SubnetSet) (dev *agd.Device, err error) {
if ds == nil {
return nil, fmt.Errorf("device is nil")
}
var linkedIP netip.Addr
err = linkedIP.UnmarshalBinary(ds.LinkedIp)
if err != nil {
return nil, fmt.Errorf("linked ip: %w", err)
}
var dedicatedIPs []netip.Addr
dedicatedIPs, err = ds.dedicatedIPsToInternal(bindSet)
if err != nil {
return nil, fmt.Errorf("dedicated ips: %w", err)
}
auth, err := ds.Authentication.toInternal()
if err != nil {
return nil, fmt.Errorf("auth: %w", err)
}
id, err := agd.NewDeviceID(ds.Id)
if err != nil {
return nil, fmt.Errorf("device id: %w", err)
}
name, err := agd.NewDeviceName(ds.Name)
if err != nil {
return nil, fmt.Errorf("device name: %w", err)
}
var humanID agd.HumanIDLower
if humanIDStr := ds.HumanIdLower; humanIDStr != "" {
humanID, err = agd.NewHumanIDLower(humanIDStr)
if err != nil {
return nil, fmt.Errorf("lowercase human id: %w", err)
}
}
return &agd.Device{
Auth: auth,
ID: id,
Name: name,
HumanIDLower: humanID,
LinkedIP: linkedIP,
DedicatedIPs: dedicatedIPs,
FilteringEnabled: ds.FilteringEnabled,
}, nil
}
// dedicatedIPsToInternal converts the dedicated IP data while also validating
// it against the given bindSet.
func (ds *DeviceSettings) dedicatedIPsToInternal(
bindSet netutil.SubnetSet,
) (dedicatedIPs []netip.Addr, err error) {
dedicatedIPs, err = agdprotobuf.ByteSlicesToIPs(ds.DedicatedIps)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
// TODO(d.kolyshev): Extract business logic validation.
for i, addr := range dedicatedIPs {
if !bindSet.Contains(addr) {
return nil, fmt.Errorf("at index %d: %q is not in bind data", i, addr)
}
}
return dedicatedIPs, nil
}
// toInternal converts a protobuf auth settings structure to an internal one.
// If x is nil, toInternal returns non-nil settings with enabled field set to
// false.
func (x *AuthenticationSettings) toInternal() (s *agd.AuthSettings, err error) {
if x == nil {
return &agd.AuthSettings{
Enabled: false,
PasswordHash: agdpasswd.AllowAuthenticator{},
}, nil
}
ph, err := dohPasswordToInternal(x.DohPasswordHash)
if err != nil {
return nil, fmt.Errorf("password hash: %w", err)
}
return &agd.AuthSettings{
PasswordHash: ph,
Enabled: true,
DoHAuthOnly: x.DohAuthOnly,
}, nil
}
// dohPasswordToInternal converts a protobuf DoH password hash sum-type to an
// internal one.
func dohPasswordToInternal(
pbp isAuthenticationSettings_DohPasswordHash,
) (p agdpasswd.Authenticator, err error) {
switch pbp := pbp.(type) {
case nil:
return agdpasswd.AllowAuthenticator{}, nil
case *AuthenticationSettings_PasswordHashBcrypt:
return agdpasswd.NewPasswordHashBcrypt(pbp.PasswordHashBcrypt), nil
default:
return nil, fmt.Errorf("bad pb auth doh password hash %T(%[1]v)", pbp)
}
}

2314
internal/backendpb/dns.pb.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -18,13 +18,31 @@ service DNSService {
The trailers headers will include a "sync_time", given in milliseconds,
that should be used for subsequent incremental DNS profile synchronization requests.
This method may return the following errors:
- RateLimitedError: If too many "full sync" concurrent requests are made.
- AuthenticationFailedError: If the authentication failed.
*/
rpc getDNSProfiles(DNSProfilesRequest) returns (stream DNSProfile);
/*
Stores devices activity.
This method may return the following errors:
- AuthenticationFailedError: If the authentication failed.
*/
rpc saveDevicesBillingStat(stream DeviceBillingStat) returns (google.protobuf.Empty);
/*
Create device by "human_id".
This method may return the following errors:
- RateLimitedError: If the request was made too frequently and the client must wait before retrying.
- DeviceQuotaExceededError: If the client has exceeded its quota for creating devices.
- BadRequestError: If the request is invalid: DNS server does not exist, creation of auto-devices is disabled or human_id validation failed.
- AuthenticationFailedError: If the authentication failed.
*/
rpc createDeviceByHumanId(CreateDeviceRequest) returns (CreateDeviceResponse);
}
message DNSProfilesRequest {
@ -52,6 +70,7 @@ message DNSProfile {
}
bool ip_log_enabled = 17;
AccessSettings access = 18;
bool auto_devices_enabled = 19;
}
message SafeBrowsingSettings {
@ -67,6 +86,8 @@ message DeviceSettings {
bytes linked_ip = 4;
repeated bytes dedicated_ips = 5;
AuthenticationSettings authentication = 6;
// Value in lower case. Will be empty for "ordinary" devices and non-empty for "automatically" created devices.
string human_id_lower = 7;
}
message ParentalSettings {
@ -144,3 +165,43 @@ message AuthenticationSettings {
bytes password_hash_bcrypt = 2;
}
}
enum DeviceType {
INVALID = 0;
WINDOWS = 1;
ANDROID = 2;
MAC = 3;
IOS = 4;
LINUX = 5;
ROUTER = 6;
SMART_TV = 7;
GAME_CONSOLE = 8;
OTHER = 9;
}
message CreateDeviceRequest {
string dns_id = 1;
string human_id = 2;
DeviceType device_type = 3;
}
message CreateDeviceResponse {
DeviceSettings device = 1;
}
message RateLimitedError {
string message = 1;
google.protobuf.Duration retry_delay = 2;
}
message DeviceQuotaExceededError {
string message = 1;
}
message BadRequestError {
string message = 1;
}
message AuthenticationFailedError {
string message = 1;
}

View File

@ -1,8 +1,8 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.3.0
// - protoc v4.25.3
// source: backend.proto
// - protoc-gen-go-grpc v1.4.0
// - protoc v5.27.1
// source: dns.proto
package backendpb
@ -16,12 +16,13 @@ import (
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// Requires gRPC-Go v1.62.0 or later.
const _ = grpc.SupportPackageIsVersion8
const (
DNSService_GetDNSProfiles_FullMethodName = "/DNSService/getDNSProfiles"
DNSService_SaveDevicesBillingStat_FullMethodName = "/DNSService/saveDevicesBillingStat"
DNSService_CreateDeviceByHumanId_FullMethodName = "/DNSService/createDeviceByHumanId"
)
// DNSServiceClient is the client API for DNSService service.
@ -34,9 +35,24 @@ type DNSServiceClient interface {
//
// The trailers headers will include a "sync_time", given in milliseconds,
// that should be used for subsequent incremental DNS profile synchronization requests.
//
// This method may return the following errors:
// - RateLimitedError: If too many "full sync" concurrent requests are made.
// - AuthenticationFailedError: If the authentication failed.
GetDNSProfiles(ctx context.Context, in *DNSProfilesRequest, opts ...grpc.CallOption) (DNSService_GetDNSProfilesClient, error)
// Stores devices activity.
//
// This method may return the following errors:
// - AuthenticationFailedError: If the authentication failed.
SaveDevicesBillingStat(ctx context.Context, opts ...grpc.CallOption) (DNSService_SaveDevicesBillingStatClient, error)
// Create device by "human_id".
//
// This method may return the following errors:
// - RateLimitedError: If the request was made too frequently and the client must wait before retrying.
// - DeviceQuotaExceededError: If the client has exceeded its quota for creating devices.
// - BadRequestError: If the request is invalid: DNS server does not exist, creation of auto-devices is disabled or human_id validation failed.
// - AuthenticationFailedError: If the authentication failed.
CreateDeviceByHumanId(ctx context.Context, in *CreateDeviceRequest, opts ...grpc.CallOption) (*CreateDeviceResponse, error)
}
type dNSServiceClient struct {
@ -48,11 +64,12 @@ func NewDNSServiceClient(cc grpc.ClientConnInterface) DNSServiceClient {
}
func (c *dNSServiceClient) GetDNSProfiles(ctx context.Context, in *DNSProfilesRequest, opts ...grpc.CallOption) (DNSService_GetDNSProfilesClient, error) {
stream, err := c.cc.NewStream(ctx, &DNSService_ServiceDesc.Streams[0], DNSService_GetDNSProfiles_FullMethodName, opts...)
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &DNSService_ServiceDesc.Streams[0], DNSService_GetDNSProfiles_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &dNSServiceGetDNSProfilesClient{stream}
x := &dNSServiceGetDNSProfilesClient{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
@ -80,11 +97,12 @@ func (x *dNSServiceGetDNSProfilesClient) Recv() (*DNSProfile, error) {
}
func (c *dNSServiceClient) SaveDevicesBillingStat(ctx context.Context, opts ...grpc.CallOption) (DNSService_SaveDevicesBillingStatClient, error) {
stream, err := c.cc.NewStream(ctx, &DNSService_ServiceDesc.Streams[1], DNSService_SaveDevicesBillingStat_FullMethodName, opts...)
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &DNSService_ServiceDesc.Streams[1], DNSService_SaveDevicesBillingStat_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &dNSServiceSaveDevicesBillingStatClient{stream}
x := &dNSServiceSaveDevicesBillingStatClient{ClientStream: stream}
return x, nil
}
@ -113,6 +131,16 @@ func (x *dNSServiceSaveDevicesBillingStatClient) CloseAndRecv() (*emptypb.Empty,
return m, nil
}
func (c *dNSServiceClient) CreateDeviceByHumanId(ctx context.Context, in *CreateDeviceRequest, opts ...grpc.CallOption) (*CreateDeviceResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreateDeviceResponse)
err := c.cc.Invoke(ctx, DNSService_CreateDeviceByHumanId_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// DNSServiceServer is the server API for DNSService service.
// All implementations must embed UnimplementedDNSServiceServer
// for forward compatibility
@ -123,9 +151,24 @@ type DNSServiceServer interface {
//
// The trailers headers will include a "sync_time", given in milliseconds,
// that should be used for subsequent incremental DNS profile synchronization requests.
//
// This method may return the following errors:
// - RateLimitedError: If too many "full sync" concurrent requests are made.
// - AuthenticationFailedError: If the authentication failed.
GetDNSProfiles(*DNSProfilesRequest, DNSService_GetDNSProfilesServer) error
// Stores devices activity.
//
// This method may return the following errors:
// - AuthenticationFailedError: If the authentication failed.
SaveDevicesBillingStat(DNSService_SaveDevicesBillingStatServer) error
// Create device by "human_id".
//
// This method may return the following errors:
// - RateLimitedError: If the request was made too frequently and the client must wait before retrying.
// - DeviceQuotaExceededError: If the client has exceeded its quota for creating devices.
// - BadRequestError: If the request is invalid: DNS server does not exist, creation of auto-devices is disabled or human_id validation failed.
// - AuthenticationFailedError: If the authentication failed.
CreateDeviceByHumanId(context.Context, *CreateDeviceRequest) (*CreateDeviceResponse, error)
mustEmbedUnimplementedDNSServiceServer()
}
@ -139,6 +182,9 @@ func (UnimplementedDNSServiceServer) GetDNSProfiles(*DNSProfilesRequest, DNSServ
func (UnimplementedDNSServiceServer) SaveDevicesBillingStat(DNSService_SaveDevicesBillingStatServer) error {
return status.Errorf(codes.Unimplemented, "method SaveDevicesBillingStat not implemented")
}
func (UnimplementedDNSServiceServer) CreateDeviceByHumanId(context.Context, *CreateDeviceRequest) (*CreateDeviceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateDeviceByHumanId not implemented")
}
func (UnimplementedDNSServiceServer) mustEmbedUnimplementedDNSServiceServer() {}
// UnsafeDNSServiceServer may be embedded to opt out of forward compatibility for this service.
@ -157,7 +203,7 @@ func _DNSService_GetDNSProfiles_Handler(srv interface{}, stream grpc.ServerStrea
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(DNSServiceServer).GetDNSProfiles(m, &dNSServiceGetDNSProfilesServer{stream})
return srv.(DNSServiceServer).GetDNSProfiles(m, &dNSServiceGetDNSProfilesServer{ServerStream: stream})
}
type DNSService_GetDNSProfilesServer interface {
@ -174,7 +220,7 @@ func (x *dNSServiceGetDNSProfilesServer) Send(m *DNSProfile) error {
}
func _DNSService_SaveDevicesBillingStat_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(DNSServiceServer).SaveDevicesBillingStat(&dNSServiceSaveDevicesBillingStatServer{stream})
return srv.(DNSServiceServer).SaveDevicesBillingStat(&dNSServiceSaveDevicesBillingStatServer{ServerStream: stream})
}
type DNSService_SaveDevicesBillingStatServer interface {
@ -199,13 +245,36 @@ func (x *dNSServiceSaveDevicesBillingStatServer) Recv() (*DeviceBillingStat, err
return m, nil
}
func _DNSService_CreateDeviceByHumanId_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateDeviceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DNSServiceServer).CreateDeviceByHumanId(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DNSService_CreateDeviceByHumanId_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DNSServiceServer).CreateDeviceByHumanId(ctx, req.(*CreateDeviceRequest))
}
return interceptor(ctx, in, info, handler)
}
// DNSService_ServiceDesc is the grpc.ServiceDesc for DNSService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var DNSService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "DNSService",
HandlerType: (*DNSServiceServer)(nil),
Methods: []grpc.MethodDesc{},
Methods: []grpc.MethodDesc{
{
MethodName: "createDeviceByHumanId",
Handler: _DNSService_CreateDeviceByHumanId_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "getDNSProfiles",
@ -218,5 +287,5 @@ var DNSService_ServiceDesc = grpc.ServiceDesc{
ClientStreams: true,
},
},
Metadata: "backend.proto",
Metadata: "dns.proto",
}

View File

@ -0,0 +1,70 @@
package backendpb
import (
"context"
"fmt"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// fixGRPCError converts a gRPC error into an application error, if necessary.
// That includes gRPC deadlines, which do not match [context.DeadlineExceeded]
// correctly.
//
// It also updates the backend gRPC metrics depending on the type, see
// [metrics.IncGRPCErrorsCounter].
func fixGRPCError(err error) (res error) {
metricsType := metrics.GRPCErrorTypeOther
defer func() { metrics.IncGRPCErrorsCounter(metricsType) }()
s, ok := status.FromError(err)
if !ok {
// Return the error as-is.
return err
}
// See https://github.com/grpc/grpc-go/issues/4822.
//
// TODO(d.kolyshev): Remove after the grpc-go issue is fixed.
if s.Code() == codes.DeadlineExceeded {
metricsType = metrics.GRPCErrorTypeTimeout
return fmt.Errorf("grpc: %w; original message: %s", context.DeadlineExceeded, err)
}
for _, d := range s.Details() {
switch structErr := d.(type) {
case *AuthenticationFailedError:
metricsType = metrics.GRPCErrorTypeAuthentication
return &profiledb.AuthenticationFailedError{
Message: structErr.Message,
}
case *BadRequestError:
metricsType = metrics.GRPCErrorTypeBadRequest
return &profiledb.BadRequestError{
Message: structErr.Message,
}
case *DeviceQuotaExceededError:
metricsType = metrics.GRPCErrorTypeDeviceQuota
return &profiledb.DeviceQuotaExceededError{
Message: structErr.Message,
}
case *RateLimitedError:
metricsType = metrics.GRPCErrorTypeRateLimit
return &profiledb.RateLimitedError{
Message: structErr.Message,
RetryDelay: structErr.RetryDelay.AsDuration(),
}
}
}
// Return the error as-is.
return err
}

View File

@ -0,0 +1,341 @@
package backendpb
import (
"context"
"fmt"
"net/netip"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/access"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtime"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
)
// toInternal converts the protobuf-encoded data into a profile structure and
// its device structures.
func (x *DNSProfile) toInternal(
ctx context.Context,
updTime time.Time,
bindSet netutil.SubnetSet,
errColl errcoll.Interface,
) (profile *agd.Profile, devices []*agd.Device, err error) {
if x == nil {
return nil, nil, fmt.Errorf("profile is nil")
}
parental, err := x.Parental.toInternal(ctx, errColl)
if err != nil {
return nil, nil, fmt.Errorf("parental: %w", err)
}
m, err := blockingModeToInternal(x.BlockingMode)
if err != nil {
return nil, nil, fmt.Errorf("blocking mode: %w", err)
}
devices, deviceIds := devicesToInternal(ctx, x.Devices, bindSet, errColl)
listsEnabled, listIDs := x.RuleLists.toInternal(ctx, errColl)
profID, err := agd.NewProfileID(x.DnsId)
if err != nil {
return nil, nil, fmt.Errorf("id: %w", err)
}
var fltRespTTL time.Duration
if respTTL := x.FilteredResponseTtl; respTTL != nil {
fltRespTTL = respTTL.AsDuration()
}
return &agd.Profile{
Parental: parental,
BlockingMode: m,
ID: profID,
UpdateTime: updTime,
DeviceIDs: deviceIds,
RuleListIDs: listIDs,
CustomRules: rulesToInternal(ctx, x.CustomRules, errColl),
FilteredResponseTTL: fltRespTTL,
FilteringEnabled: x.FilteringEnabled,
SafeBrowsing: x.SafeBrowsing.toInternal(),
Access: x.Access.toInternal(ctx, errColl),
RuleListsEnabled: listsEnabled,
QueryLogEnabled: x.QueryLogEnabled,
Deleted: x.Deleted,
BlockPrivateRelay: x.BlockPrivateRelay,
BlockFirefoxCanary: x.BlockFirefoxCanary,
IPLogEnabled: x.IpLogEnabled,
AutoDevicesEnabled: x.AutoDevicesEnabled,
}, devices, nil
}
// toInternal converts a protobuf parental-settings structure to an internal
// one. If x is nil, toInternal returns nil.
func (x *ParentalSettings) toInternal(
ctx context.Context,
errColl errcoll.Interface,
) (s *agd.ParentalProtectionSettings, err error) {
if x == nil {
return nil, nil
}
schedule, err := x.Schedule.toInternal()
if err != nil {
return nil, fmt.Errorf("schedule: %w", err)
}
return &agd.ParentalProtectionSettings{
Schedule: schedule,
BlockedServices: blockedSvcsToInternal(ctx, errColl, x.BlockedServices),
Enabled: x.Enabled,
BlockAdult: x.BlockAdult,
GeneralSafeSearch: x.GeneralSafeSearch,
YoutubeSafeSearch: x.YoutubeSafeSearch,
}, nil
}
// toInternal converts protobuf safe-browsing settings to an internal structure.
// If x is nil, toInternal returns nil.
func (x *SafeBrowsingSettings) toInternal() (sb *agd.SafeBrowsingSettings) {
if x == nil {
return nil
}
return &agd.SafeBrowsingSettings{
Enabled: x.Enabled,
BlockDangerousDomains: x.BlockDangerousDomains,
BlockNewlyRegisteredDomains: x.BlockNrd,
}
}
// toInternal converts protobuf access settings to an internal structure. If x
// is nil, toInternal returns [access.EmptyProfile].
func (x *AccessSettings) toInternal(
ctx context.Context,
errColl errcoll.Interface,
) (a access.Profile) {
if x == nil || !x.Enabled {
return access.EmptyProfile{}
}
return access.NewDefaultProfile(&access.ProfileConfig{
AllowedNets: cidrRangeToInternal(ctx, errColl, x.AllowlistCidr),
BlockedNets: cidrRangeToInternal(ctx, errColl, x.BlocklistCidr),
AllowedASN: asnToInternal(x.AllowlistAsn),
BlockedASN: asnToInternal(x.BlocklistAsn),
BlocklistDomainRules: x.BlocklistDomainRules,
})
}
// cidrRangeToInternal is a helper that converts a slice of CidrRange to the
// slice of [netip.Prefix].
func cidrRangeToInternal(
ctx context.Context,
errColl errcoll.Interface,
cidrs []*CidrRange,
) (out []netip.Prefix) {
for i, c := range cidrs {
addr, ok := netip.AddrFromSlice(c.Address)
if !ok {
reportf(ctx, errColl, "bad cidr at index %d: %v", i, c.Address)
continue
}
out = append(out, netip.PrefixFrom(addr, int(c.Prefix)))
}
return out
}
// asnToInternal is a helper that converts a slice of ASNs to the slice of
// [geoip.ASN].
func asnToInternal(asns []uint32) (out []geoip.ASN) {
for _, asn := range asns {
out = append(out, geoip.ASN(asn))
}
return out
}
// blockedSvcsToInternal is a helper that converts the blocked service IDs from
// the backend response to AdGuard DNS blocked service IDs.
func blockedSvcsToInternal(
ctx context.Context,
errColl errcoll.Interface,
respSvcs []string,
) (svcs []agd.BlockedServiceID) {
l := len(respSvcs)
if l == 0 {
return nil
}
svcs = make([]agd.BlockedServiceID, 0, l)
for i, s := range respSvcs {
id, err := agd.NewBlockedServiceID(s)
if err != nil {
reportf(ctx, errColl, "blocked service at index %d: %w", i, err)
continue
}
svcs = append(svcs, id)
}
return svcs
}
// toInternal converts a protobuf protection-schedule structure to an internal
// one. If x is nil, toInternal returns nil.
func (x *ScheduleSettings) toInternal() (sch *agd.ParentalProtectionSchedule, err error) {
if x == nil {
return nil, nil
}
sch = &agd.ParentalProtectionSchedule{}
sch.TimeZone, err = agdtime.LoadLocation(x.Tmz)
if err != nil {
return nil, fmt.Errorf("loading timezone: %w", err)
}
sch.Week = &agd.WeeklySchedule{}
w := x.WeeklyRange
days := []*DayRange{w.Sun, w.Mon, w.Tue, w.Wed, w.Thu, w.Fri, w.Sat}
for i, d := range days {
if d == nil {
sch.Week[i] = agd.ZeroLengthDayRange()
continue
}
sch.Week[i] = agd.DayRange{
Start: uint16(d.Start.AsDuration().Minutes()),
End: uint16(d.End.AsDuration().Minutes()),
}
}
for i, r := range sch.Week {
err = r.Validate()
if err != nil {
return nil, fmt.Errorf("weekday %s: %w", time.Weekday(i), err)
}
}
return sch, nil
}
// toInternal converts a protobuf custom blocking-mode to an internal one.
// Assumes that at least one IP address is specified in the result blocking-mode
// object.
func (pbm *BlockingModeCustomIP) toInternal() (m dnsmsg.BlockingMode, err error) {
custom := &dnsmsg.BlockingModeCustomIP{}
// TODO(a.garipov): Only one IPv4 address is supported on protobuf side.
var ipv4Addr netip.Addr
err = ipv4Addr.UnmarshalBinary(pbm.Ipv4)
if err != nil {
return nil, fmt.Errorf("bad custom ipv4: %w", err)
} else if ipv4Addr.IsValid() {
custom.IPv4 = []netip.Addr{ipv4Addr}
}
// TODO(a.garipov): Only one IPv6 address is supported on protobuf side.
var ipv6Addr netip.Addr
err = ipv6Addr.UnmarshalBinary(pbm.Ipv6)
if err != nil {
return nil, fmt.Errorf("bad custom ipv6: %w", err)
} else if ipv6Addr.IsValid() {
custom.IPv6 = []netip.Addr{ipv6Addr}
}
if len(custom.IPv4)+len(custom.IPv6) == 0 {
return nil, errors.Error("no valid custom ips found")
}
return custom, nil
}
// blockingModeToInternal converts a protobuf blocking-mode sum-type to an
// internal one. If pbm is nil, blockingModeToInternal returns a null-IP
// blocking mode.
func blockingModeToInternal(pbm isDNSProfile_BlockingMode) (m dnsmsg.BlockingMode, err error) {
switch pbm := pbm.(type) {
case nil:
return &dnsmsg.BlockingModeNullIP{}, nil
case *DNSProfile_BlockingModeCustomIp:
return pbm.BlockingModeCustomIp.toInternal()
case *DNSProfile_BlockingModeNxdomain:
return &dnsmsg.BlockingModeNXDOMAIN{}, nil
case *DNSProfile_BlockingModeNullIp:
return &dnsmsg.BlockingModeNullIP{}, nil
case *DNSProfile_BlockingModeRefused:
return &dnsmsg.BlockingModeREFUSED{}, nil
default:
// Consider unhandled type-switch cases programmer errors.
return nil, fmt.Errorf("bad pb blocking mode %T(%[1]v)", pbm)
}
}
// rulesToInternal is a helper that converts the filter rules from the backend
// response to AdGuard DNS filtering rules.
func rulesToInternal(
ctx context.Context,
respRules []string,
errColl errcoll.Interface,
) (rules []agd.FilterRuleText) {
l := len(respRules)
if l == 0 {
return nil
}
rules = make([]agd.FilterRuleText, 0, l)
for i, r := range respRules {
text, err := agd.NewFilterRuleText(r)
if err != nil {
reportf(ctx, errColl, "rule at index %d: %w", i, err)
continue
}
rules = append(rules, text)
}
return rules
}
// toInternal is a helper that converts the filter lists from the backend
// response to AdGuard DNS filter list ids. If x is nil, toInternal returns
// false and nil.
func (x *RuleListsSettings) toInternal(
ctx context.Context,
errColl errcoll.Interface,
) (enabled bool, filterLists []agd.FilterListID) {
if x == nil {
return false, nil
}
l := len(x.Ids)
if l == 0 {
return x.Enabled, nil
}
filterLists = make([]agd.FilterListID, 0, l)
for i, f := range x.Ids {
id, err := agd.NewFilterListID(f)
if err != nil {
reportf(ctx, errColl, "filter id: at index %d: %w", i, err)
continue
}
filterLists = append(filterLists, id)
}
return x.Enabled, filterLists
}

View File

@ -0,0 +1,553 @@
package backendpb
import (
"context"
"net/netip"
"testing"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/access"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdpasswd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtime"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
)
func TestDNSProfile_ToInternal(t *testing.T) {
t.Parallel()
ctx := context.Background()
errColl := &agdtest.ErrorCollector{
OnCollect: func(_ context.Context, err error) {
panic(err)
},
}
t.Run("success", func(t *testing.T) {
t.Parallel()
got, gotDevices, err := NewTestDNSProfile(t).toInternal(ctx, TestUpdTime, TestBind, errColl)
require.NoError(t, err)
assert.Equal(t, newProfile(t), got)
assert.Equal(t, newDevices(t), gotDevices)
})
t.Run("success_bad_data", func(t *testing.T) {
t.Parallel()
var errCollErr error
savingErrColl := &agdtest.ErrorCollector{
OnCollect: func(_ context.Context, err error) {
errCollErr = err
},
}
got, gotDevices, err := newDNSProfileWithBadData(t).toInternal(
ctx,
TestUpdTime,
TestBind,
savingErrColl,
)
require.NoError(t, err)
testutil.AssertErrorMsg(
t,
`backendpb: bad device settings for device with id "inv-d-ip":`+
" dedicated ips: ip at index 0: unexpected slice size",
errCollErr,
)
assert.Equal(t, newProfile(t), got)
assert.Equal(t, newDevices(t), gotDevices)
})
t.Run("invalid_device_ded_ip", func(t *testing.T) {
t.Parallel()
var errCollErr error
savingErrColl := &agdtest.ErrorCollector{
OnCollect: func(_ context.Context, err error) {
errCollErr = err
},
}
bindSet := netip.MustParsePrefix("2.2.2.2/32")
got, gotDevices, err := NewTestDNSProfile(t).toInternal(
ctx,
TestUpdTime,
bindSet,
savingErrColl,
)
require.NoError(t, err)
testutil.AssertErrorMsg(
t,
`backendpb: bad device settings for device with id "`+TestDeviceIDStr+`":`+
" dedicated ips: at index 0: \"1.1.1.2\" is not in bind data",
errCollErr,
)
assert.NotEqual(t, newProfile(t), got)
assert.NotEqual(t, newDevices(t), gotDevices)
assert.Len(t, gotDevices, 3)
})
t.Run("empty", func(t *testing.T) {
t.Parallel()
var emptyDNSProfile *DNSProfile
_, _, err := emptyDNSProfile.toInternal(ctx, TestUpdTime, TestBind, errColl)
testutil.AssertErrorMsg(t, "profile is nil", err)
})
t.Run("deleted", func(t *testing.T) {
t.Parallel()
dp := &DNSProfile{
DnsId: TestProfileIDStr,
Deleted: true,
}
got, gotDevices, err := dp.toInternal(ctx, TestUpdTime, TestBind, errColl)
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, got.ID, TestProfileID)
assert.True(t, got.Deleted)
assert.Empty(t, gotDevices)
})
t.Run("inv_parental_sch_tmz", func(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
dp.Parental.Schedule.Tmz = "invalid"
_, _, err := dp.toInternal(ctx, TestUpdTime, TestBind, errColl)
testutil.AssertErrorMsg(
t,
"parental: schedule: loading timezone: unknown time zone invalid",
err,
)
})
t.Run("inv_parental_sch_day_range", func(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
dp.Parental.Schedule.WeeklyRange.Sun = &DayRange{
Start: durationpb.New(1000000000000),
End: nil,
}
_, _, err := dp.toInternal(ctx, TestUpdTime, TestBind, errColl)
testutil.AssertErrorMsg(
t,
"parental: schedule: weekday Sunday: bad day range: end 0 less than start 16",
err,
)
})
t.Run("inv_blocking_mode_v4", func(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
bm := dp.BlockingMode.(*DNSProfile_BlockingModeCustomIp)
bm.BlockingModeCustomIp.Ipv4 = []byte("1")
_, _, err := dp.toInternal(ctx, TestUpdTime, TestBind, errColl)
testutil.AssertErrorMsg(t, "blocking mode: bad custom ipv4: unexpected slice size", err)
})
t.Run("inv_blocking_mode_v6", func(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
bm := dp.BlockingMode.(*DNSProfile_BlockingModeCustomIp)
bm.BlockingModeCustomIp.Ipv6 = []byte("1")
_, _, err := dp.toInternal(ctx, TestUpdTime, TestBind, errColl)
testutil.AssertErrorMsg(t, "blocking mode: bad custom ipv6: unexpected slice size", err)
})
t.Run("nil_ips_blocking_mode", func(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
bm := dp.BlockingMode.(*DNSProfile_BlockingModeCustomIp)
bm.BlockingModeCustomIp.Ipv4 = nil
bm.BlockingModeCustomIp.Ipv6 = nil
_, _, err := dp.toInternal(ctx, TestUpdTime, TestBind, errColl)
testutil.AssertErrorMsg(t, "blocking mode: no valid custom ips found", err)
})
t.Run("nil_blocking_mode", func(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
dp.BlockingMode = nil
got, gotDevices, err := dp.toInternal(ctx, TestUpdTime, TestBind, errColl)
require.NoError(t, err)
require.NotNil(t, got)
wantProf := newProfile(t)
wantProf.BlockingMode = &dnsmsg.BlockingModeNullIP{}
assert.Equal(t, wantProf, got)
assert.Equal(t, newDevices(t), gotDevices)
})
t.Run("nil_access", func(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
dp.Access = nil
got, _, err := dp.toInternal(ctx, TestUpdTime, TestBind, errColl)
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, got.ID, TestProfileID)
assert.IsType(t, access.EmptyProfile{}, got.Access)
})
t.Run("access_disabled", func(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
dp.Access = &AccessSettings{
Enabled: false,
}
got, _, err := dp.toInternal(ctx, TestUpdTime, TestBind, errColl)
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, got.ID, TestProfileID)
assert.IsType(t, access.EmptyProfile{}, got.Access)
})
}
// newDNSProfileWithBadData returns a new instance of *DNSProfile with bad
// devices data for tests.
func newDNSProfileWithBadData(tb testing.TB) (dp *DNSProfile) {
tb.Helper()
invalidDevices := []*DeviceSettings{{
Id: "invalid-too-long-device-id",
Name: "device_name",
FilteringEnabled: true,
LinkedIp: ipToBytes(tb, netip.MustParseAddr("1.1.1.1")),
DedicatedIps: nil,
}, {
Id: "dev-name",
Name: "invalid-too-long-device-name-invalid-too-long-device-name-" +
"invalid-too-long-device-name-invalid-too-long-device-name-" +
"invalid-too-long-device-name-invalid-too-long-device-name",
FilteringEnabled: true,
LinkedIp: ipToBytes(tb, netip.MustParseAddr("1.1.1.1")),
DedicatedIps: nil,
}, {
Id: "inv-ip",
Name: "test-name",
FilteringEnabled: true,
LinkedIp: []byte("1"),
DedicatedIps: nil,
}, {
Id: "inv-d-ip",
Name: "test-name",
FilteringEnabled: true,
LinkedIp: ipToBytes(tb, netip.MustParseAddr("1.1.1.1")),
DedicatedIps: [][]byte{[]byte("1")},
}}
dp = NewTestDNSProfile(tb)
dp.Devices = append(dp.Devices, invalidDevices...)
return dp
}
// NewTestDNSProfile returns a new instance of *DNSProfile for tests.
func NewTestDNSProfile(tb testing.TB) (dp *DNSProfile) {
tb.Helper()
dayRange := &DayRange{
Start: durationpb.New(0),
End: durationpb.New(59 * time.Minute),
}
devices := []*DeviceSettings{{
Id: TestDeviceIDStr,
Name: "1111aaaa-name",
FilteringEnabled: false,
LinkedIp: ipToBytes(tb, netip.MustParseAddr("1.1.1.1")),
DedicatedIps: [][]byte{ipToBytes(tb, netip.MustParseAddr("1.1.1.2"))},
}, {
Id: "2222bbbb",
Name: "2222bbbb-name",
FilteringEnabled: true,
LinkedIp: ipToBytes(tb, netip.MustParseAddr("2.2.2.2")),
DedicatedIps: nil,
Authentication: &AuthenticationSettings{
DohAuthOnly: true,
DohPasswordHash: &AuthenticationSettings_PasswordHashBcrypt{
PasswordHashBcrypt: []byte("test-hash"),
},
},
}, {
Id: "3333cccc",
Name: "3333cccc-name",
FilteringEnabled: false,
LinkedIp: ipToBytes(tb, netip.MustParseAddr("3.3.3.3")),
DedicatedIps: nil,
Authentication: &AuthenticationSettings{
DohAuthOnly: false,
DohPasswordHash: nil,
},
}, {
Id: "4444dddd",
Name: "My Auto-Device",
HumanIdLower: "my-auto--device",
FilteringEnabled: true,
}}
return &DNSProfile{
DnsId: TestProfileIDStr,
FilteringEnabled: true,
QueryLogEnabled: true,
Deleted: false,
SafeBrowsing: &SafeBrowsingSettings{
Enabled: true,
BlockDangerousDomains: true,
BlockNrd: false,
},
Parental: &ParentalSettings{
Enabled: false,
BlockAdult: false,
GeneralSafeSearch: false,
YoutubeSafeSearch: false,
BlockedServices: []string{"youtube"},
Schedule: &ScheduleSettings{
Tmz: "GMT",
WeeklyRange: &WeeklyRange{
Sun: nil,
Mon: dayRange,
Tue: dayRange,
Wed: dayRange,
Thu: dayRange,
Fri: dayRange,
Sat: nil,
},
},
},
RuleLists: &RuleListsSettings{
Enabled: true,
Ids: []string{"1"},
},
Devices: devices,
CustomRules: []string{"||example.org^"},
FilteredResponseTtl: durationpb.New(10 * time.Second),
BlockPrivateRelay: true,
BlockFirefoxCanary: true,
IpLogEnabled: true,
AutoDevicesEnabled: true,
BlockingMode: &DNSProfile_BlockingModeCustomIp{
BlockingModeCustomIp: &BlockingModeCustomIP{
Ipv4: ipToBytes(tb, netip.MustParseAddr("1.2.3.4")),
Ipv6: ipToBytes(tb, netip.MustParseAddr("1234::cdef")),
},
},
Access: &AccessSettings{
AllowlistCidr: []*CidrRange{{
Address: netip.MustParseAddr("1.1.1.0").AsSlice(),
Prefix: 24,
}},
BlocklistCidr: []*CidrRange{{
Address: netip.MustParseAddr("2.2.2.0").AsSlice(),
Prefix: 24,
}},
AllowlistAsn: []uint32{1},
BlocklistAsn: []uint32{2},
BlocklistDomainRules: []string{"block.test"},
Enabled: true,
},
}
}
// ipToBytes is a wrapper around netip.Addr.MarshalBinary.
func ipToBytes(tb testing.TB, ip netip.Addr) (b []byte) {
tb.Helper()
b, err := ip.MarshalBinary()
require.NoError(tb, err)
return b
}
// newProfile returns a new profile for tests.
func newProfile(tb testing.TB) (p *agd.Profile) {
tb.Helper()
wantLoc, err := agdtime.LoadLocation("GMT")
require.NoError(tb, err)
dayRange := agd.DayRange{
Start: 0,
End: 59,
}
wantParental := &agd.ParentalProtectionSettings{
Schedule: &agd.ParentalProtectionSchedule{
Week: &agd.WeeklySchedule{
agd.ZeroLengthDayRange(),
dayRange,
dayRange,
dayRange,
dayRange,
dayRange,
agd.ZeroLengthDayRange(),
},
TimeZone: wantLoc,
},
BlockedServices: []agd.BlockedServiceID{"youtube"},
Enabled: false,
BlockAdult: false,
GeneralSafeSearch: false,
YoutubeSafeSearch: false,
}
wantSafeBrowsing := &agd.SafeBrowsingSettings{
Enabled: true,
BlockDangerousDomains: true,
BlockNewlyRegisteredDomains: false,
}
wantBlockingMode := &dnsmsg.BlockingModeCustomIP{
IPv4: []netip.Addr{netip.MustParseAddr("1.2.3.4")},
IPv6: []netip.Addr{netip.MustParseAddr("1234::cdef")},
}
wantAccess := access.NewDefaultProfile(&access.ProfileConfig{
AllowedNets: []netip.Prefix{netip.MustParsePrefix("1.1.1.0/24")},
BlockedNets: []netip.Prefix{netip.MustParsePrefix("2.2.2.0/24")},
AllowedASN: []geoip.ASN{1},
BlockedASN: []geoip.ASN{2},
BlocklistDomainRules: []string{"block.test"},
})
return &agd.Profile{
Parental: wantParental,
BlockingMode: wantBlockingMode,
ID: TestProfileID,
UpdateTime: TestUpdTime,
DeviceIDs: []agd.DeviceID{
TestDeviceID,
"2222bbbb",
"3333cccc",
"4444dddd",
},
RuleListIDs: []agd.FilterListID{"1"},
CustomRules: []agd.FilterRuleText{"||example.org^"},
FilteredResponseTTL: 10 * time.Second,
SafeBrowsing: wantSafeBrowsing,
Access: wantAccess,
RuleListsEnabled: true,
FilteringEnabled: true,
QueryLogEnabled: true,
Deleted: false,
BlockPrivateRelay: true,
BlockFirefoxCanary: true,
IPLogEnabled: true,
AutoDevicesEnabled: true,
}
}
// newDevices returns a slice of test devices.
func newDevices(t *testing.T) (d []*agd.Device) {
t.Helper()
return []*agd.Device{{
Auth: &agd.AuthSettings{
Enabled: false,
DoHAuthOnly: false,
PasswordHash: agdpasswd.AllowAuthenticator{},
},
ID: TestDeviceID,
LinkedIP: netip.MustParseAddr("1.1.1.1"),
Name: "1111aaaa-name",
DedicatedIPs: []netip.Addr{netip.MustParseAddr("1.1.1.2")},
FilteringEnabled: false,
}, {
Auth: &agd.AuthSettings{
Enabled: true,
DoHAuthOnly: true,
PasswordHash: agdpasswd.NewPasswordHashBcrypt([]byte("test-hash")),
},
ID: "2222bbbb",
LinkedIP: netip.MustParseAddr("2.2.2.2"),
Name: "2222bbbb-name",
DedicatedIPs: nil,
FilteringEnabled: true,
}, {
Auth: &agd.AuthSettings{
Enabled: true,
DoHAuthOnly: false,
PasswordHash: agdpasswd.AllowAuthenticator{},
},
ID: "3333cccc",
LinkedIP: netip.MustParseAddr("3.3.3.3"),
Name: "3333cccc-name",
DedicatedIPs: nil,
FilteringEnabled: false,
}, {
Auth: &agd.AuthSettings{
Enabled: false,
DoHAuthOnly: false,
PasswordHash: agdpasswd.AllowAuthenticator{},
},
ID: "4444dddd",
Name: "My Auto-Device",
HumanIDLower: "my-auto--device",
FilteringEnabled: true,
}}
}
var (
errSink error
profSink *agd.Profile
)
func BenchmarkDNSProfile_ToInternal(b *testing.B) {
dp := NewTestDNSProfile(b)
ctx := context.Background()
errColl := &agdtest.ErrorCollector{
OnCollect: func(_ context.Context, err error) {
panic(err)
},
}
b.ReportAllocs()
b.ResetTimer()
for range b.N {
profSink, _, errSink = dp.toInternal(ctx, TestUpdTime, TestBind, errColl)
}
require.NotNil(b, profSink)
require.NoError(b, errSink)
// Most recent result, on a ThinkPad X13:
// goos: linux
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/backendpb
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkDNSProfile_ToInternal
// BenchmarkDNSProfile_ToInternal-16 93568 19203 ns/op 1976 B/op 45 allocs/op
}

View File

@ -4,26 +4,16 @@ import (
"context"
"fmt"
"io"
"net/netip"
"net/url"
"strconv"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/access"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdpasswd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdprotobuf"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtime"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
)
@ -40,6 +30,9 @@ type ProfileStorageConfig struct {
// Endpoint is the backend API URL. The scheme should be either "grpc" or
// "grpcs".
Endpoint *url.URL
// APIKey is the API key used for authentication, if any.
APIKey string
}
// ProfileStorage is the implementation of the [profiledb.Storage] interface
@ -48,9 +41,8 @@ type ProfileStorageConfig struct {
type ProfileStorage struct {
bindSet netutil.SubnetSet
errColl errcoll.Interface
// client is the current GRPC client.
client DNSServiceClient
apiKey string
}
// NewProfileStorage returns a new [ProfileStorage] that retrieves information
@ -64,26 +56,63 @@ func NewProfileStorage(c *ProfileStorageConfig) (s *ProfileStorage, err error) {
return &ProfileStorage{
bindSet: c.BindSet,
client: client,
errColl: c.ErrColl,
client: client,
apiKey: c.APIKey,
}, nil
}
// type check
var _ profiledb.Storage = (*ProfileStorage)(nil)
// CreateAutoDevice implements the [profile.Storage] interface for
// *ProfileStorage.
func (s *ProfileStorage) CreateAutoDevice(
ctx context.Context,
req *profiledb.StorageCreateAutoDeviceRequest,
) (resp *profiledb.StorageCreateAutoDeviceResponse, err error) {
defer func() {
err = errors.Annotate(
err,
"creating auto device for profile %q and human id %q: %w",
req.ProfileID,
req.HumanID,
)
}()
ctx = ctxWithAuthentication(ctx, s.apiKey)
backendResp, err := s.client.CreateDeviceByHumanId(ctx, &CreateDeviceRequest{
DnsId: string(req.ProfileID),
HumanId: string(req.HumanID),
DeviceType: DeviceType(req.DeviceType),
})
if err != nil {
return nil, fmt.Errorf("calling backend: %w", fixGRPCError(err))
}
d, err := backendResp.Device.toInternal(s.bindSet)
if err != nil {
return nil, fmt.Errorf("converting device: %w", err)
}
return &profiledb.StorageCreateAutoDeviceResponse{
Device: d,
}, nil
}
// Profiles implements the [profiledb.Storage] interface for *ProfileStorage.
func (s *ProfileStorage) Profiles(
ctx context.Context,
req *profiledb.StorageRequest,
) (resp *profiledb.StorageResponse, err error) {
req *profiledb.StorageProfilesRequest,
) (resp *profiledb.StorageProfilesResponse, err error) {
ctx = ctxWithAuthentication(ctx, s.apiKey)
stream, err := s.client.GetDNSProfiles(ctx, toProtobuf(req))
if err != nil {
return nil, fmt.Errorf("loading profiles: %w", fixGRPCError(err))
}
defer func() { err = errors.WithDeferred(err, stream.CloseSend()) }()
resp = &profiledb.StorageResponse{
resp = &profiledb.StorageProfilesResponse{
Profiles: []*agd.Profile{},
Devices: []*agd.Device{},
}
@ -92,7 +121,7 @@ func (s *ProfileStorage) Profiles(
isFullSync: req.SyncTime.IsZero(),
}
for {
for n := 1; ; n++ {
stats.startRecv()
profile, profErr := stream.Recv()
if profErr != nil {
@ -100,7 +129,7 @@ func (s *ProfileStorage) Profiles(
break
}
return nil, fmt.Errorf("receiving profile: %w", fixGRPCError(profErr))
return nil, fmt.Errorf("receiving profile #%d: %w", n, fixGRPCError(profErr))
}
stats.endRecv()
@ -128,465 +157,8 @@ func (s *ProfileStorage) Profiles(
return resp, nil
}
// fixGRPCError wraps GRPC error if needed. As the GRPC deadline error is not
// correctly wrapped, this helper detects it by the status code and replaces it
// with a simple DeadlineExceeded error.
//
// See https://github.com/grpc/grpc-go/issues/4822.
//
// TODO(d.kolyshev): Remove after the grpc-go issue is fixed.
func fixGRPCError(err error) (wErr error) {
st, ok := status.FromError(err)
if ok && st.Code() == codes.DeadlineExceeded {
err = fmt.Errorf("grpc: %w", context.DeadlineExceeded)
}
return err
}
// toInternal converts the protobuf-encoded data into a profile structure.
func (x *DNSProfile) toInternal(
ctx context.Context,
updTime time.Time,
bindSet netutil.SubnetSet,
errColl errcoll.Interface,
) (profile *agd.Profile, devices []*agd.Device, err error) {
if x == nil {
return nil, nil, fmt.Errorf("profile is nil")
}
parental, err := x.Parental.toInternal(ctx, errColl)
if err != nil {
return nil, nil, fmt.Errorf("parental: %w", err)
}
m, err := blockingModeToInternal(x.BlockingMode)
if err != nil {
return nil, nil, fmt.Errorf("blocking mode: %w", err)
}
devices, deviceIds := devicesToInternal(ctx, x.Devices, bindSet, errColl)
listsEnabled, listIDs := x.RuleLists.toInternal(ctx, errColl)
profID, err := agd.NewProfileID(x.DnsId)
if err != nil {
return nil, nil, fmt.Errorf("id: %w", err)
}
var fltRespTTL time.Duration
if respTTL := x.FilteredResponseTtl; respTTL != nil {
fltRespTTL = respTTL.AsDuration()
}
return &agd.Profile{
Parental: parental,
BlockingMode: m,
ID: profID,
UpdateTime: updTime,
DeviceIDs: deviceIds,
RuleListIDs: listIDs,
CustomRules: rulesToInternal(ctx, x.CustomRules, errColl),
FilteredResponseTTL: fltRespTTL,
FilteringEnabled: x.FilteringEnabled,
SafeBrowsing: x.SafeBrowsing.toInternal(),
Access: x.Access.toInternal(ctx, errColl),
RuleListsEnabled: listsEnabled,
QueryLogEnabled: x.QueryLogEnabled,
Deleted: x.Deleted,
BlockPrivateRelay: x.BlockPrivateRelay,
BlockFirefoxCanary: x.BlockFirefoxCanary,
IPLogEnabled: x.IpLogEnabled,
}, devices, nil
}
// toInternal converts a protobuf parental-settings structure to an internal
// one. If x is nil, toInternal returns nil.
func (x *ParentalSettings) toInternal(
ctx context.Context,
errColl errcoll.Interface,
) (s *agd.ParentalProtectionSettings, err error) {
if x == nil {
return nil, nil
}
schedule, err := x.Schedule.toInternal()
if err != nil {
return nil, fmt.Errorf("schedule: %w", err)
}
return &agd.ParentalProtectionSettings{
Schedule: schedule,
BlockedServices: blockedSvcsToInternal(ctx, errColl, x.BlockedServices),
Enabled: x.Enabled,
BlockAdult: x.BlockAdult,
GeneralSafeSearch: x.GeneralSafeSearch,
YoutubeSafeSearch: x.YoutubeSafeSearch,
}, nil
}
// toInternal converts protobuf safe-browsing settings to an internal structure.
// If x is nil, toInternal returns nil.
func (x *SafeBrowsingSettings) toInternal() (sb *agd.SafeBrowsingSettings) {
if x == nil {
return nil
}
return &agd.SafeBrowsingSettings{
Enabled: x.Enabled,
BlockDangerousDomains: x.BlockDangerousDomains,
BlockNewlyRegisteredDomains: x.BlockNrd,
}
}
// toInternal converts protobuf access settings to an internal structure. If x
// is nil, toInternal returns [access.EmptyProfile].
func (x *AccessSettings) toInternal(
ctx context.Context,
errColl errcoll.Interface,
) (a access.Profile) {
if x == nil || !x.Enabled {
return access.EmptyProfile{}
}
return access.NewDefaultProfile(&access.ProfileConfig{
AllowedNets: cidrRangeToInternal(ctx, errColl, x.AllowlistCidr),
BlockedNets: cidrRangeToInternal(ctx, errColl, x.BlocklistCidr),
AllowedASN: asnToInternal(x.AllowlistAsn),
BlockedASN: asnToInternal(x.BlocklistAsn),
BlocklistDomainRules: x.BlocklistDomainRules,
})
}
// cidrRangeToInternal is a helper that converts a slice of CidrRange to the
// slice of [netip.Prefix].
func cidrRangeToInternal(
ctx context.Context,
errColl errcoll.Interface,
cidrs []*CidrRange,
) (out []netip.Prefix) {
for i, c := range cidrs {
addr, ok := netip.AddrFromSlice(c.Address)
if !ok {
reportf(ctx, errColl, "invalid cidr at index %d: %w", i)
continue
}
out = append(out, netip.PrefixFrom(addr, int(c.Prefix)))
}
return out
}
// asnToInternal is a helper that converts a slice of ASNs to the slice of
// [geoip.ASN].
func asnToInternal(asns []uint32) (out []geoip.ASN) {
for _, asn := range asns {
out = append(out, geoip.ASN(asn))
}
return out
}
// blockedSvcsToInternal is a helper that converts the blocked service IDs from
// the backend response to AdGuard DNS blocked service IDs.
func blockedSvcsToInternal(
ctx context.Context,
errColl errcoll.Interface,
respSvcs []string,
) (svcs []agd.BlockedServiceID) {
l := len(respSvcs)
if l == 0 {
return nil
}
svcs = make([]agd.BlockedServiceID, 0, l)
for i, s := range respSvcs {
id, err := agd.NewBlockedServiceID(s)
if err != nil {
reportf(ctx, errColl, "blocked service at index %d: %w", i, err)
continue
}
svcs = append(svcs, id)
}
return svcs
}
// toInternal converts a protobuf protection-schedule structure to an internal
// one. If x is nil, toInternal returns nil.
func (x *ScheduleSettings) toInternal() (sch *agd.ParentalProtectionSchedule, err error) {
if x == nil {
return nil, nil
}
sch = &agd.ParentalProtectionSchedule{}
sch.TimeZone, err = agdtime.LoadLocation(x.Tmz)
if err != nil {
return nil, fmt.Errorf("loading timezone: %w", err)
}
sch.Week = &agd.WeeklySchedule{}
w := x.WeeklyRange
days := []*DayRange{w.Sun, w.Mon, w.Tue, w.Wed, w.Thu, w.Fri, w.Sat}
for i, d := range days {
if d == nil {
sch.Week[i] = agd.ZeroLengthDayRange()
continue
}
sch.Week[i] = agd.DayRange{
Start: uint16(d.Start.AsDuration().Minutes()),
End: uint16(d.End.AsDuration().Minutes()),
}
}
for i, r := range sch.Week {
err = r.Validate()
if err != nil {
return nil, fmt.Errorf("weekday %s: %w", time.Weekday(i), err)
}
}
return sch, nil
}
// toInternal converts a protobuf custom blocking-mode to an internal one.
// Assumes that at least one IP address is specified in the result blocking-mode
// object.
func (pbm *BlockingModeCustomIP) toInternal() (m dnsmsg.BlockingMode, err error) {
custom := &dnsmsg.BlockingModeCustomIP{}
// TODO(a.garipov): Only one IPv4 address is supported on protobuf side.
var ipv4Addr netip.Addr
err = ipv4Addr.UnmarshalBinary(pbm.Ipv4)
if err != nil {
return nil, fmt.Errorf("bad custom ipv4: %w", err)
} else if ipv4Addr.IsValid() {
custom.IPv4 = []netip.Addr{ipv4Addr}
}
// TODO(a.garipov): Only one IPv6 address is supported on protobuf side.
var ipv6Addr netip.Addr
err = ipv6Addr.UnmarshalBinary(pbm.Ipv6)
if err != nil {
return nil, fmt.Errorf("bad custom ipv6: %w", err)
} else if ipv6Addr.IsValid() {
custom.IPv6 = []netip.Addr{ipv6Addr}
}
if len(custom.IPv4)+len(custom.IPv6) == 0 {
return nil, errors.Error("no valid custom ips found")
}
return custom, nil
}
// blockingModeToInternal converts a protobuf blocking-mode sum-type to an
// internal one. If pbm is nil, blockingModeToInternal returns a null-IP
// blocking mode.
func blockingModeToInternal(pbm isDNSProfile_BlockingMode) (m dnsmsg.BlockingMode, err error) {
switch pbm := pbm.(type) {
case nil:
return &dnsmsg.BlockingModeNullIP{}, nil
case *DNSProfile_BlockingModeCustomIp:
return pbm.BlockingModeCustomIp.toInternal()
case *DNSProfile_BlockingModeNxdomain:
return &dnsmsg.BlockingModeNXDOMAIN{}, nil
case *DNSProfile_BlockingModeNullIp:
return &dnsmsg.BlockingModeNullIP{}, nil
case *DNSProfile_BlockingModeRefused:
return &dnsmsg.BlockingModeREFUSED{}, nil
default:
// Consider unhandled type-switch cases programmer errors.
return nil, fmt.Errorf("bad pb blocking mode %T(%[1]v)", pbm)
}
}
// devicesToInternal is a helper that converts the devices from protobuf to
// AdGuard DNS devices.
func devicesToInternal(
ctx context.Context,
ds []*DeviceSettings,
bindSet netutil.SubnetSet,
errColl errcoll.Interface,
) (out []*agd.Device, ids []agd.DeviceID) {
l := len(ds)
if l == 0 {
return nil, nil
}
out = make([]*agd.Device, 0, l)
for _, d := range ds {
dev, err := d.toInternal(bindSet)
if err != nil {
reportf(ctx, errColl, "invalid device settings: %w", err)
metrics.DevicesInvalidTotal.Inc()
continue
}
ids = append(ids, dev.ID)
out = append(out, dev)
}
return out, ids
}
// toInternal is a helper that converts device settings from backend protobuf
// response to AdGuard DNS device object.
func (ds *DeviceSettings) toInternal(bindSet netutil.SubnetSet) (dev *agd.Device, err error) {
if ds == nil {
return nil, fmt.Errorf("device is nil")
}
var linkedIP netip.Addr
err = linkedIP.UnmarshalBinary(ds.LinkedIp)
if err != nil {
return nil, fmt.Errorf("linked ip: %w", err)
}
var dedicatedIPs []netip.Addr
dedicatedIPs, err = agdprotobuf.ByteSlicesToIPs(ds.DedicatedIps)
if err != nil {
return nil, fmt.Errorf("dedicated ips: %w", err)
}
// TODO(d.kolyshev): Extract business logic validation.
for _, addr := range dedicatedIPs {
if !bindSet.Contains(addr) {
return nil, fmt.Errorf("dedicated ip %q is not in bind data", addr)
}
}
auth, err := ds.Authentication.toInternal()
if err != nil {
return nil, fmt.Errorf("auth: %s: %w", ds.Id, err)
}
id, err := agd.NewDeviceID(ds.Id)
if err != nil {
return nil, fmt.Errorf("device id: %s: %w", ds.Id, err)
}
name, err := agd.NewDeviceName(ds.Name)
if err != nil {
return nil, fmt.Errorf("device name: %s: %w", ds.Name, err)
}
return &agd.Device{
Auth: auth,
ID: id,
Name: name,
LinkedIP: linkedIP,
DedicatedIPs: dedicatedIPs,
FilteringEnabled: ds.FilteringEnabled,
}, nil
}
// toInternal converts a protobuf auth settings structure to an internal one.
// If x is nil, toInternal returns non-nil settings with enabled field set to
// false.
func (x *AuthenticationSettings) toInternal() (s *agd.AuthSettings, err error) {
if x == nil {
return &agd.AuthSettings{
Enabled: false,
PasswordHash: agdpasswd.AllowAuthenticator{},
}, nil
}
ph, err := dohPasswordToInternal(x.DohPasswordHash)
if err != nil {
return nil, fmt.Errorf("password hash: %w", err)
}
return &agd.AuthSettings{
PasswordHash: ph,
Enabled: true,
DoHAuthOnly: x.DohAuthOnly,
}, nil
}
// dohPasswordToInternal converts a protobuf DoH password hash sum-type to an
// internal one.
func dohPasswordToInternal(
pbp isAuthenticationSettings_DohPasswordHash,
) (p agdpasswd.Authenticator, err error) {
switch pbp := pbp.(type) {
case nil:
return agdpasswd.AllowAuthenticator{}, nil
case *AuthenticationSettings_PasswordHashBcrypt:
return agdpasswd.NewPasswordHashBcrypt(pbp.PasswordHashBcrypt), nil
default:
return nil, fmt.Errorf("bad pb auth doh password hash %T(%[1]v)", pbp)
}
}
// rulesToInternal is a helper that converts the filter rules from the backend
// response to AdGuard DNS filtering rules.
func rulesToInternal(
ctx context.Context,
respRules []string,
errColl errcoll.Interface,
) (rules []agd.FilterRuleText) {
l := len(respRules)
if l == 0 {
return nil
}
rules = make([]agd.FilterRuleText, 0, l)
for i, r := range respRules {
text, err := agd.NewFilterRuleText(r)
if err != nil {
reportf(ctx, errColl, "rule at index %d: %w", i, err)
continue
}
rules = append(rules, text)
}
return rules
}
// toInternal is a helper that converts the filter lists from the backend
// response to AdGuard DNS filter list ids. If x is nil, toInternal returns
// false and nil.
func (x *RuleListsSettings) toInternal(
ctx context.Context,
errColl errcoll.Interface,
) (enabled bool, filterLists []agd.FilterListID) {
if x == nil {
return false, nil
}
l := len(x.Ids)
if l == 0 {
return x.Enabled, nil
}
filterLists = make([]agd.FilterListID, 0, l)
for _, f := range x.Ids {
id, err := agd.NewFilterListID(f)
if err != nil {
reportf(ctx, errColl, "invalid filter id: %s: %w", f, err)
continue
}
filterLists = append(filterLists, id)
}
return x.Enabled, filterLists
}
// toProtobuf converts a storage request structure into the protobuf structure.
func toProtobuf(r *profiledb.StorageRequest) (req *DNSProfilesRequest) {
func toProtobuf(r *profiledb.StorageProfilesRequest) (req *DNSProfilesRequest) {
return &DNSProfilesRequest{
SyncTime: timestamppb.New(r.SyncTime),
}
@ -603,7 +175,7 @@ func syncTimeFromTrailer(trailer metadata.MD) (syncTime time.Time, err error) {
syncTimeMs, err := strconv.ParseInt(st[0], 10, 64)
if err != nil {
return syncTime, fmt.Errorf("invalid value: %w", err)
return syncTime, fmt.Errorf("bad value: %w", err)
}
return time.Unix(0, syncTimeMs*time.Millisecond.Nanoseconds()), nil

View File

@ -1,478 +1,18 @@
package backendpb
import (
"context"
"net/netip"
"strconv"
"testing"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/access"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdpasswd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtime"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/durationpb"
)
func TestMain(m *testing.M) {
testutil.DiscardLogOutput(m)
}
// testProfileID is the common profile ID for tests.
const testProfileID agd.ProfileID = "prof1234"
// TestUpdTime is the common update time for tests.
var TestUpdTime = time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
// testBind includes any IPv4 address.
var testBind = netip.MustParsePrefix("0.0.0.0/0")
func TestDNSProfile_ToInternal(t *testing.T) {
ctx := context.Background()
errColl := &agdtest.ErrorCollector{
OnCollect: func(_ context.Context, err error) {
panic(err)
},
}
t.Run("success", func(t *testing.T) {
got, gotDevices, err := NewTestDNSProfile(t).toInternal(ctx, TestUpdTime, testBind, errColl)
require.NoError(t, err)
assert.Equal(t, newProfile(t), got)
assert.Equal(t, newDevices(t), gotDevices)
})
t.Run("success_bad_data", func(t *testing.T) {
var errCollErr error
savingErrColl := &agdtest.ErrorCollector{
OnCollect: func(_ context.Context, err error) {
errCollErr = err
},
}
got, gotDevices, err := newDNSProfileWithBadData(t).toInternal(
ctx,
TestUpdTime,
testBind,
savingErrColl,
)
require.NoError(t, err)
testutil.AssertErrorMsg(t, "backendpb: invalid device settings:"+
" dedicated ips: ip at index 0: unexpected slice size", errCollErr)
assert.Equal(t, newProfile(t), got)
assert.Equal(t, newDevices(t), gotDevices)
})
t.Run("invalid_device_ded_ip", func(t *testing.T) {
var errCollErr error
savingErrColl := &agdtest.ErrorCollector{
OnCollect: func(_ context.Context, err error) {
errCollErr = err
},
}
bindSet := netip.MustParsePrefix("2.2.2.2/32")
got, gotDevices, err := NewTestDNSProfile(t).toInternal(
ctx,
TestUpdTime,
bindSet,
savingErrColl,
)
require.NoError(t, err)
testutil.AssertErrorMsg(t, "backendpb: invalid device settings:"+
" dedicated ip \"1.1.1.2\" is not in bind data", errCollErr)
assert.NotEqual(t, newProfile(t), got)
assert.NotEqual(t, newDevices(t), gotDevices)
assert.Len(t, gotDevices, 2)
})
t.Run("empty", func(t *testing.T) {
var emptyDNSProfile *DNSProfile
_, _, err := emptyDNSProfile.toInternal(ctx, TestUpdTime, testBind, errColl)
testutil.AssertErrorMsg(t, "profile is nil", err)
})
t.Run("deleted", func(t *testing.T) {
dp := &DNSProfile{
DnsId: string(testProfileID),
Deleted: true,
}
got, gotDevices, err := dp.toInternal(ctx, TestUpdTime, testBind, errColl)
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, got.ID, testProfileID)
assert.True(t, got.Deleted)
assert.Empty(t, gotDevices)
})
t.Run("inv_parental_sch_tmz", func(t *testing.T) {
dp := NewTestDNSProfile(t)
dp.Parental.Schedule.Tmz = "invalid"
_, _, err := dp.toInternal(ctx, TestUpdTime, testBind, errColl)
testutil.AssertErrorMsg(t, "parental: schedule: loading timezone: unknown time zone invalid", err)
})
t.Run("inv_parental_sch_day_range", func(t *testing.T) {
dp := NewTestDNSProfile(t)
dp.Parental.Schedule.WeeklyRange.Sun = &DayRange{
Start: durationpb.New(1000000000000),
End: nil,
}
_, _, err := dp.toInternal(ctx, TestUpdTime, testBind, errColl)
testutil.AssertErrorMsg(t, "parental: schedule: weekday Sunday: bad day range: end 0 less than start 16", err)
})
t.Run("inv_blocking_mode_v4", func(t *testing.T) {
dp := NewTestDNSProfile(t)
bm := dp.BlockingMode.(*DNSProfile_BlockingModeCustomIp)
bm.BlockingModeCustomIp.Ipv4 = []byte("1")
_, _, err := dp.toInternal(ctx, TestUpdTime, testBind, errColl)
testutil.AssertErrorMsg(t, "blocking mode: bad custom ipv4: unexpected slice size", err)
})
t.Run("inv_blocking_mode_v6", func(t *testing.T) {
dp := NewTestDNSProfile(t)
bm := dp.BlockingMode.(*DNSProfile_BlockingModeCustomIp)
bm.BlockingModeCustomIp.Ipv6 = []byte("1")
_, _, err := dp.toInternal(ctx, TestUpdTime, testBind, errColl)
testutil.AssertErrorMsg(t, "blocking mode: bad custom ipv6: unexpected slice size", err)
})
t.Run("nil_ips_blocking_mode", func(t *testing.T) {
dp := NewTestDNSProfile(t)
bm := dp.BlockingMode.(*DNSProfile_BlockingModeCustomIp)
bm.BlockingModeCustomIp.Ipv4 = nil
bm.BlockingModeCustomIp.Ipv6 = nil
_, _, err := dp.toInternal(ctx, TestUpdTime, testBind, errColl)
testutil.AssertErrorMsg(t, "blocking mode: no valid custom ips found", err)
})
t.Run("nil_blocking_mode", func(t *testing.T) {
dp := NewTestDNSProfile(t)
dp.BlockingMode = nil
got, gotDevices, err := dp.toInternal(ctx, TestUpdTime, testBind, errColl)
require.NoError(t, err)
require.NotNil(t, got)
wantProf := newProfile(t)
wantProf.BlockingMode = &dnsmsg.BlockingModeNullIP{}
assert.Equal(t, wantProf, got)
assert.Equal(t, newDevices(t), gotDevices)
})
t.Run("nil_access", func(t *testing.T) {
dp := NewTestDNSProfile(t)
dp.Access = nil
got, _, err := dp.toInternal(ctx, TestUpdTime, testBind, errColl)
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, got.ID, testProfileID)
assert.IsType(t, access.EmptyProfile{}, got.Access)
})
t.Run("access_disabled", func(t *testing.T) {
dp := NewTestDNSProfile(t)
dp.Access = &AccessSettings{
Enabled: false,
}
got, _, err := dp.toInternal(ctx, TestUpdTime, testBind, errColl)
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, got.ID, testProfileID)
assert.IsType(t, access.EmptyProfile{}, got.Access)
})
}
// newDNSProfileWithBadData returns a new instance of *DNSProfile with bad
// devices data for tests.
func newDNSProfileWithBadData(tb testing.TB) (dp *DNSProfile) {
tb.Helper()
invalidDevices := []*DeviceSettings{{
Id: "invalid-too-long-device-id",
Name: "device_name",
FilteringEnabled: true,
LinkedIp: ipToBytes(tb, netip.MustParseAddr("1.1.1.1")),
DedicatedIps: nil,
}, {
Id: "dev-name",
Name: "invalid-too-long-device-name-invalid-too-long-device-name-" +
"invalid-too-long-device-name-invalid-too-long-device-name-" +
"invalid-too-long-device-name-invalid-too-long-device-name",
FilteringEnabled: true,
LinkedIp: ipToBytes(tb, netip.MustParseAddr("1.1.1.1")),
DedicatedIps: nil,
}, {
Id: "inv-ip",
Name: "test-name",
FilteringEnabled: true,
LinkedIp: []byte("1"),
DedicatedIps: nil,
}, {
Id: "inv-d-ip",
Name: "test-name",
FilteringEnabled: true,
LinkedIp: ipToBytes(tb, netip.MustParseAddr("1.1.1.1")),
DedicatedIps: [][]byte{[]byte("1")},
}}
dp = NewTestDNSProfile(tb)
dp.Devices = append(dp.Devices, invalidDevices...)
return dp
}
// NewTestDNSProfile returns a new instance of *DNSProfile for tests.
func NewTestDNSProfile(tb testing.TB) (dp *DNSProfile) {
tb.Helper()
dayRange := &DayRange{
Start: durationpb.New(0),
End: durationpb.New(59 * time.Minute),
}
devices := []*DeviceSettings{{
Id: "1111aaaa",
Name: "1111aaaa-name",
FilteringEnabled: false,
LinkedIp: ipToBytes(tb, netip.MustParseAddr("1.1.1.1")),
DedicatedIps: [][]byte{ipToBytes(tb, netip.MustParseAddr("1.1.1.2"))},
}, {
Id: "2222bbbb",
Name: "2222bbbb-name",
FilteringEnabled: true,
LinkedIp: ipToBytes(tb, netip.MustParseAddr("2.2.2.2")),
DedicatedIps: nil,
Authentication: &AuthenticationSettings{
DohAuthOnly: true,
DohPasswordHash: &AuthenticationSettings_PasswordHashBcrypt{
PasswordHashBcrypt: []byte("test-hash"),
},
},
}, {
Id: "3333cccc",
Name: "3333cccc-name",
FilteringEnabled: false,
LinkedIp: ipToBytes(tb, netip.MustParseAddr("3.3.3.3")),
DedicatedIps: nil,
Authentication: &AuthenticationSettings{
DohAuthOnly: false,
DohPasswordHash: nil,
},
}}
return &DNSProfile{
DnsId: string(testProfileID),
FilteringEnabled: true,
QueryLogEnabled: true,
Deleted: false,
SafeBrowsing: &SafeBrowsingSettings{
Enabled: true,
BlockDangerousDomains: true,
BlockNrd: false,
},
Parental: &ParentalSettings{
Enabled: false,
BlockAdult: false,
GeneralSafeSearch: false,
YoutubeSafeSearch: false,
BlockedServices: []string{"youtube"},
Schedule: &ScheduleSettings{
Tmz: "GMT",
WeeklyRange: &WeeklyRange{
Sun: nil,
Mon: dayRange,
Tue: dayRange,
Wed: dayRange,
Thu: dayRange,
Fri: dayRange,
Sat: nil,
},
},
},
RuleLists: &RuleListsSettings{
Enabled: true,
Ids: []string{"1"},
},
Devices: devices,
CustomRules: []string{"||example.org^"},
FilteredResponseTtl: durationpb.New(10 * time.Second),
BlockPrivateRelay: true,
BlockFirefoxCanary: true,
IpLogEnabled: true,
BlockingMode: &DNSProfile_BlockingModeCustomIp{
BlockingModeCustomIp: &BlockingModeCustomIP{
Ipv4: ipToBytes(tb, netip.MustParseAddr("1.2.3.4")),
Ipv6: ipToBytes(tb, netip.MustParseAddr("1234::cdef")),
},
},
Access: &AccessSettings{
AllowlistCidr: []*CidrRange{{
Address: netip.MustParseAddr("1.1.1.0").AsSlice(),
Prefix: 24,
}},
BlocklistCidr: []*CidrRange{{
Address: netip.MustParseAddr("2.2.2.0").AsSlice(),
Prefix: 24,
}},
AllowlistAsn: []uint32{1},
BlocklistAsn: []uint32{2},
BlocklistDomainRules: []string{"block.test"},
Enabled: true,
},
}
}
// newProfile returns a new profile for tests.
func newProfile(tb testing.TB) (p *agd.Profile) {
tb.Helper()
wantLoc, err := agdtime.LoadLocation("GMT")
require.NoError(tb, err)
dayRange := agd.DayRange{
Start: 0,
End: 59,
}
wantParental := &agd.ParentalProtectionSettings{
Schedule: &agd.ParentalProtectionSchedule{
Week: &agd.WeeklySchedule{
agd.ZeroLengthDayRange(),
dayRange,
dayRange,
dayRange,
dayRange,
dayRange,
agd.ZeroLengthDayRange(),
},
TimeZone: wantLoc,
},
BlockedServices: []agd.BlockedServiceID{"youtube"},
Enabled: false,
BlockAdult: false,
GeneralSafeSearch: false,
YoutubeSafeSearch: false,
}
wantSafeBrowsing := &agd.SafeBrowsingSettings{
Enabled: true,
BlockDangerousDomains: true,
BlockNewlyRegisteredDomains: false,
}
wantBlockingMode := &dnsmsg.BlockingModeCustomIP{
IPv4: []netip.Addr{netip.MustParseAddr("1.2.3.4")},
IPv6: []netip.Addr{netip.MustParseAddr("1234::cdef")},
}
wantAccess := access.NewDefaultProfile(&access.ProfileConfig{
AllowedNets: []netip.Prefix{netip.MustParsePrefix("1.1.1.0/24")},
BlockedNets: []netip.Prefix{netip.MustParsePrefix("2.2.2.0/24")},
AllowedASN: []geoip.ASN{1},
BlockedASN: []geoip.ASN{2},
BlocklistDomainRules: []string{"block.test"},
})
return &agd.Profile{
Parental: wantParental,
BlockingMode: wantBlockingMode,
ID: testProfileID,
UpdateTime: TestUpdTime,
DeviceIDs: []agd.DeviceID{
"1111aaaa",
"2222bbbb",
"3333cccc",
},
RuleListIDs: []agd.FilterListID{"1"},
CustomRules: []agd.FilterRuleText{"||example.org^"},
FilteredResponseTTL: 10 * time.Second,
SafeBrowsing: wantSafeBrowsing,
Access: wantAccess,
RuleListsEnabled: true,
FilteringEnabled: true,
QueryLogEnabled: true,
Deleted: false,
BlockPrivateRelay: true,
BlockFirefoxCanary: true,
IPLogEnabled: true,
}
}
// newDevices returns a slice of test devices.
func newDevices(t *testing.T) (d []*agd.Device) {
t.Helper()
return []*agd.Device{{
Auth: &agd.AuthSettings{
Enabled: false,
DoHAuthOnly: false,
PasswordHash: agdpasswd.AllowAuthenticator{},
},
ID: "1111aaaa",
LinkedIP: netip.MustParseAddr("1.1.1.1"),
Name: "1111aaaa-name",
DedicatedIPs: []netip.Addr{netip.MustParseAddr("1.1.1.2")},
FilteringEnabled: false,
}, {
Auth: &agd.AuthSettings{
Enabled: true,
DoHAuthOnly: true,
PasswordHash: agdpasswd.NewPasswordHashBcrypt([]byte("test-hash")),
},
ID: "2222bbbb",
LinkedIP: netip.MustParseAddr("2.2.2.2"),
Name: "2222bbbb-name",
DedicatedIPs: nil,
FilteringEnabled: true,
}, {
Auth: &agd.AuthSettings{
Enabled: true,
DoHAuthOnly: false,
PasswordHash: agdpasswd.AllowAuthenticator{},
},
ID: "3333cccc",
LinkedIP: netip.MustParseAddr("3.3.3.3"),
Name: "3333cccc-name",
DedicatedIPs: nil,
FilteringEnabled: false,
}}
}
// ipToBytes is a wrapper around netip.Addr.MarshalBinary.
func ipToBytes(tb testing.TB, ip netip.Addr) (b []byte) {
tb.Helper()
b, err := ip.MarshalBinary()
require.NoError(tb, err)
return b
}
func TestSyncTimeFromTrailer(t *testing.T) {
t.Parallel()
milliseconds := strconv.FormatInt(TestUpdTime.UnixMilli(), 10)
testCases := []struct {
@ -492,7 +32,7 @@ func TestSyncTimeFromTrailer(t *testing.T) {
name: "empty_key",
}, {
in: metadata.MD{"sync_time": []string{""}},
wantError: `invalid value: strconv.ParseInt: parsing "": invalid syntax`,
wantError: `bad value: strconv.ParseInt: parsing "": invalid syntax`,
want: time.Time{},
name: "empty_value",
}, {
@ -510,36 +50,3 @@ func TestSyncTimeFromTrailer(t *testing.T) {
})
}
}
var (
errSink error
profSink *agd.Profile
)
func BenchmarkDNSProfile_ToInternal(b *testing.B) {
dp := NewTestDNSProfile(b)
ctx := context.Background()
errColl := &agdtest.ErrorCollector{
OnCollect: func(_ context.Context, err error) {
panic(err)
},
}
b.ReportAllocs()
b.ResetTimer()
for range b.N {
profSink, _, errSink = dp.toInternal(ctx, TestUpdTime, testBind, errColl)
}
require.NotNil(b, profSink)
require.NoError(b, errSink)
// Most recent result, on a ThinkPad X13:
// goos: linux
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/backendpb
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkDNSProfile_ToInternal
// BenchmarkDNSProfile_ToInternal-16 157513 10340 ns/op 1148 B/op 27 allocs/op
}

View File

@ -9,19 +9,117 @@ import (
"testing"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/backendpb"
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)
func TestProfileStorage_CreateAutoDevice(t *testing.T) {
t.Parallel()
const devType = agd.DeviceTypeOther
gotReqCh := make(chan *backendpb.CreateDeviceRequest, 1)
srv := &testDNSServiceServer{
OnCreateDeviceByHumanId: func(
ctx context.Context,
req *backendpb.CreateDeviceRequest,
) (resp *backendpb.CreateDeviceResponse, err error) {
defer func() {
pt := testutil.PanicT{}
testutil.RequireSend(pt, gotReqCh, req, testTimeout)
}()
return &backendpb.CreateDeviceResponse{
Device: &backendpb.DeviceSettings{
Id: backendpb.TestDeviceIDStr,
HumanIdLower: backendpb.TestHumanIDLowerStr,
},
}, nil
},
OnGetDNSProfiles: func(
req *backendpb.DNSProfilesRequest,
srv backendpb.DNSService_GetDNSProfilesServer,
) (err error) {
panic("not implemented")
},
OnSaveDevicesBillingStat: func(
srv backendpb.DNSService_SaveDevicesBillingStatServer,
) (err error) {
panic("not implemented")
},
}
errColl := &agdtest.ErrorCollector{
OnCollect: func(_ context.Context, err error) {
panic(err)
},
}
l, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
s, err := backendpb.NewProfileStorage(&backendpb.ProfileStorageConfig{
BindSet: backendpb.TestBind,
ErrColl: errColl,
Endpoint: &url.URL{
Scheme: "grpc",
Host: l.Addr().String(),
},
})
require.NoError(t, err)
grpcSrv := grpc.NewServer(
grpc.ConnectionTimeout(1*time.Second),
grpc.Creds(insecure.NewCredentials()),
)
backendpb.RegisterDNSServiceServer(grpcSrv, srv)
go func() {
pt := &testutil.PanicT{}
srvErr := grpcSrv.Serve(l)
require.NoError(pt, srvErr)
}()
t.Cleanup(grpcSrv.GracefulStop)
ctx := testutil.ContextWithTimeout(t, testTimeout)
resp, err := s.CreateAutoDevice(ctx, &profiledb.StorageCreateAutoDeviceRequest{
ProfileID: backendpb.TestProfileID,
HumanID: backendpb.TestHumanID,
DeviceType: devType,
})
assert.NoError(t, err)
gotReq, ok := testutil.RequireReceive(t, gotReqCh, testTimeout)
require.True(t, ok)
require.NotNil(t, gotReq)
assert.Equal(t, backendpb.TestProfileIDStr, gotReq.DnsId)
assert.Equal(t, backendpb.TestHumanIDStr, gotReq.HumanId)
assert.Equal(t, backendpb.DeviceType(devType), gotReq.DeviceType)
require.NotNil(t, resp)
require.NotNil(t, resp.Device)
assert.Equal(t, backendpb.TestDeviceID, resp.Device.ID)
assert.Equal(t, backendpb.TestHumanIDLower, resp.Device.HumanIDLower)
}
var (
errSink error
respSink *profiledb.StorageResponse
respSink *profiledb.StorageProfilesResponse
)
func BenchmarkProfileStorage_Profiles(b *testing.B) {
@ -32,6 +130,13 @@ func BenchmarkProfileStorage_Profiles(b *testing.B) {
}
srv := &testDNSServiceServer{
OnCreateDeviceByHumanId: func(
ctx context.Context,
req *backendpb.CreateDeviceRequest,
) (resp *backendpb.CreateDeviceResponse, err error) {
panic("not implemented")
},
OnGetDNSProfiles: func(
req *backendpb.DNSProfilesRequest,
srv backendpb.DNSService_GetDNSProfilesServer,
@ -41,6 +146,12 @@ func BenchmarkProfileStorage_Profiles(b *testing.B) {
return sendErr
},
OnSaveDevicesBillingStat: func(
srv backendpb.DNSService_SaveDevicesBillingStatServer,
) (err error) {
panic("not implemented")
},
}
errColl := &agdtest.ErrorCollector{
@ -77,7 +188,7 @@ func BenchmarkProfileStorage_Profiles(b *testing.B) {
b.Cleanup(grpcSrv.GracefulStop)
ctx := context.Background()
req := &profiledb.StorageRequest{}
req := &profiledb.StorageProfilesRequest{}
b.ReportAllocs()
b.ResetTimer()
@ -94,5 +205,5 @@ func BenchmarkProfileStorage_Profiles(b *testing.B) {
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/backendpb
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkProfileStorage_Profiles
// BenchmarkProfileStorage_Profiles-16 5347 245341 ns/op 15129 B/op 265 allocs/op
// BenchmarkProfileStorage_Profiles-16 4128 291646 ns/op 18578 B/op 341 allocs/op
}

View File

@ -4,7 +4,7 @@ import (
"context"
"fmt"
"net/netip"
"net/url"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdservice"
@ -12,6 +12,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/billstat"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/service"
@ -103,8 +104,7 @@ func setupBillStat(
sigHdlr *service.SignalHandler,
errColl errcoll.Interface,
) (rec *billstat.RuntimeRecorder, err error) {
apiURL := netutil.CloneURL(&envs.BillStatURL.URL)
billStatUploader, err := setupBillStatUploader(apiURL, errColl)
billStatUploader, err := setupBillStatUploader(envs, errColl)
if err != nil {
return nil, fmt.Errorf("creating bill stat uploader: %w", err)
}
@ -146,9 +146,8 @@ func setupProfDB(
sigHdlr *service.SignalHandler,
errColl errcoll.Interface,
) (profDB *profiledb.Default, err error) {
apiURL := netutil.CloneURL(&envs.ProfilesURL.URL)
bindSet := collectBindSubnetSet(grps)
profStrg, err := setupProfStorage(apiURL, bindSet, errColl)
profStrg, err := setupProfStorage(envs, bindSet, errColl)
if err != nil {
return nil, fmt.Errorf("creating profile storage: %w", err)
}
@ -159,14 +158,16 @@ func setupProfDB(
ErrColl: errColl,
FullSyncIvl: conf.FullRefreshIvl.Duration,
FullSyncRetryIvl: conf.FullRefreshRetryIvl.Duration,
InitialTimeout: timeout,
CacheFilePath: envs.ProfilesCachePath,
})
if err != nil {
return nil, fmt.Errorf("creating default profile database: %w", err)
}
refrIvl := conf.RefreshIvl.Duration
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) {
@ -174,7 +175,7 @@ func setupProfDB(
},
Refresher: profDB,
Name: "profiledb",
Interval: refrIvl,
Interval: conf.RefreshIvl.Duration,
RefreshOnShutdown: false,
RandomizeStart: true,
})
@ -188,6 +189,27 @@ func setupProfDB(
return profDB, nil
}
// initProfDB refreshes the profile database initially. It logs an error if
// it's a timeout, and returns it otherwise.
func initProfDB(profDB *profiledb.Default, timeout time.Duration) (err error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
log.Info("main: initial profiledb refresh")
err = profDB.Refresh(ctx)
switch {
case err == nil:
log.Info("main: initial profiledb refresh succeeded")
case errors.Is(err, context.DeadlineExceeded):
log.Info("main: warning: initial profiledb refresh timeout: %s", err)
default:
return fmt.Errorf("initial refresh: %w", err)
}
return nil
}
// collectBindSubnetSet returns a subnet set with IP addresses of servers in the
// provided server groups grps.
func collectBindSubnetSet(grps []*agd.ServerGroup) (s netutil.SubnetSet) {
@ -223,36 +245,40 @@ const (
schemeGRPCS = "grpcs"
)
// setupProfStorage creates and returns a profile storage depending on the
// provided API URL.
func setupProfStorage(
apiURL *url.URL,
bindSet netutil.SubnetSet,
errColl errcoll.Interface,
) (s profiledb.Storage, err error) {
scheme := apiURL.Scheme
if scheme == schemeGRPC || scheme == schemeGRPCS {
return backendpb.NewProfileStorage(&backendpb.ProfileStorageConfig{
BindSet: bindSet,
Endpoint: apiURL,
ErrColl: errColl,
})
}
return nil, fmt.Errorf("invalid backend api url: %s", apiURL)
}
// setupBillStatUploader creates and returns a billstat uploader depending on
// the provided API URL.
func setupBillStatUploader(
apiURL *url.URL,
envs *environments,
errColl errcoll.Interface,
) (s billstat.Uploader, err error) {
apiURL := netutil.CloneURL(&envs.BillStatURL.URL)
scheme := apiURL.Scheme
if scheme == schemeGRPC || scheme == schemeGRPCS {
return backendpb.NewBillStat(&backendpb.BillStatConfig{
ErrColl: errColl,
Endpoint: apiURL,
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,
})
}

View File

@ -10,6 +10,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/access"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/AdguardTeam/AdGuardDNS/internal/agdservice"
"github.com/AdguardTeam/AdGuardDNS/internal/debugsvc"
"github.com/AdguardTeam/AdGuardDNS/internal/dnscheck"
@ -36,6 +37,9 @@ func Main() {
agd.InitRequestID()
// TODO(a.garipov, e.burkov): Consider adding timeouts for initialization.
ctx := context.Background()
// Log only to stdout and let users decide how to process it.
log.SetOutput(os.Stdout)
@ -58,6 +62,10 @@ func Main() {
defer collectPanics(errColl)
// Cache manager
cacheManager := agdcache.NewDefaultManager()
// Configuration file
c, err := readConfig(envs.ConfPath)
@ -86,7 +94,7 @@ func Main() {
geoIP, geoIPRefr := &geoip.File{}, &agdservice.RefreshWorker{}
geoIPErrCh := make(chan error, 1)
go setupGeoIP(geoIP, geoIPRefr, geoIPErrCh, c.GeoIP, envs, errColl)
go setupGeoIP(geoIP, geoIPRefr, geoIPErrCh, c.GeoIP, envs, errColl, cacheManager)
// Safe-browsing and adult-blocking filters
@ -98,55 +106,57 @@ func Main() {
maxFilterSize := c.Filters.MaxSize.Bytes()
cloner := dnsmsg.NewCloner(metrics.ClonerStat{})
safeBrowsingHashes, safeBrowsingFilter, err := setupHashPrefixFilter(
c.SafeBrowsing,
sbConf := c.SafeBrowsing.toInternal(
errColl,
cloner,
cacheManager,
agd.FilterListIDSafeBrowsing,
envs.SafeBrowsingURL,
envs.FilterCachePath,
maxFilterSize,
sigHdlr,
errColl,
)
sbHashes, sbFilter, err := setupHashPrefixFilter(ctx, sbConf, sigHdlr)
check(err)
adultBlockingHashes, adultBlockingFilter, err := setupHashPrefixFilter(
c.AdultBlocking,
abConf := c.AdultBlocking.toInternal(
errColl,
cloner,
cacheManager,
agd.FilterListIDAdultBlocking,
envs.AdultBlockingURL,
envs.FilterCachePath,
maxFilterSize,
sigHdlr,
errColl,
)
abHashes, abFilter, err := setupHashPrefixFilter(ctx, abConf, sigHdlr)
check(err)
_, newRegDomainsFilter, err := setupHashPrefixFilter(
// Reuse general safe browsing filter configuration.
c.SafeBrowsing,
nrdConf := c.SafeBrowsing.toInternal(
errColl,
cloner,
cacheManager,
agd.FilterListIDNewRegDomains,
envs.NewRegDomainsURL,
envs.FilterCachePath,
maxFilterSize,
sigHdlr,
errColl,
)
_, nrdFilter, err := setupHashPrefixFilter(ctx, nrdConf, sigHdlr)
check(err)
// Filter storage and filtering groups
fltStrgConf := c.Filters.toInternal(
errColl,
cacheManager,
envs,
safeBrowsingFilter,
adultBlockingFilter,
newRegDomainsFilter,
sbFilter,
abFilter,
nrdFilter,
)
fltRefrTimeout := c.Filters.RefreshTimeout.Duration
fltStrg, err := setupFilterStorage(fltStrgConf, sigHdlr, fltRefrTimeout)
fltStrg, err := setupFilterStorage(ctx, fltStrgConf, sigHdlr, fltRefrTimeout)
check(err)
fltGroups, err := c.FilteringGroups.toInternal(fltStrg)
@ -176,8 +186,6 @@ func Main() {
srvGrps, err := c.ServerGroups.toInternal(messages, btdMgr, fltGroups, c.RateLimit, c.DNS)
check(err)
ctx := context.Background()
// Start the bind-to-device manager here, now that no further calls to
// btdMgr.ListenConfig are required.
err = btdMgr.Start(ctx)
@ -220,7 +228,13 @@ func Main() {
// Rate limiting
consulAllowlistURL := &envs.ConsulAllowlistURL.URL
rateLimiter, connLimiter, err := setupRateLimiter(c.RateLimit, consulAllowlistURL, sigHdlr, errColl)
rateLimiter, connLimiter, err := setupRateLimiter(
ctx,
c.RateLimit,
consulAllowlistURL,
sigHdlr,
errColl,
)
check(err)
// GeoIP database
@ -251,15 +265,17 @@ func Main() {
// TODO(a.garipov): Consider making these configurable via the configuration
// file.
hashStorages := map[string]*hashprefix.Storage{
filter.GeneralTXTSuffix: safeBrowsingHashes,
filter.AdultBlockingTXTSuffix: adultBlockingHashes,
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,
@ -307,7 +323,15 @@ func Main() {
// Debug HTTP-service
debugSvc := debugsvc.New(envs.debugConf(dnsDB))
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.
@ -324,6 +348,9 @@ func Main() {
runtime.Version(),
)
// TODO(s.chzhen): Remove it.
log.Debug("cache manager ids: %q", cacheManager.IDs())
os.Exit(sigHdlr.Handle(ctx))
}

View File

@ -10,6 +10,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/AdguardTeam/AdGuardDNS/internal/agdservice"
"github.com/AdguardTeam/AdGuardDNS/internal/debugsvc"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsdb"
@ -25,8 +26,6 @@ import (
"github.com/getsentry/sentry-go"
)
// Environment configuration
// environments represents the configuration that is kept in the environment.
type environments struct {
AdultBlockingURL *urlutil.URL `env:"ADULT_BLOCKING_URL,notEmpty"`
@ -44,14 +43,16 @@ type environments struct {
SafeBrowsingURL *urlutil.URL `env:"SAFE_BROWSING_URL,notEmpty"`
YoutubeSafeSearchURL *urlutil.URL `env:"YOUTUBE_SAFE_SEARCH_URL,notEmpty"`
BillStatAPIKey string `env:"BILLSTAT_API_KEY"`
ConfPath string `env:"CONFIG_PATH" envDefault:"./config.yaml"`
FilterCachePath string `env:"FILTER_CACHE_PATH" envDefault:"./filters/"`
ProfilesCachePath string `env:"PROFILES_CACHE_PATH" envDefault:"./profilecache.pb"`
GeoIPASNPath string `env:"GEOIP_ASN_PATH" envDefault:"./asn.mmdb"`
GeoIPCountryPath string `env:"GEOIP_COUNTRY_PATH" envDefault:"./country.mmdb"`
ProfilesAPIKey string `env:"PROFILES_API_KEY"`
ProfilesCachePath string `env:"PROFILES_CACHE_PATH" envDefault:"./profilecache.pb"`
QueryLogPath string `env:"QUERYLOG_PATH" envDefault:"./querylog.jsonl"`
SentryDSN string `env:"SENTRY_DSN" envDefault:"stderr"`
SSLKeyLogFile string `env:"SSL_KEY_LOG_FILE"`
SentryDSN string `env:"SENTRY_DSN" envDefault:"stderr"`
ListenAddr net.IP `env:"LISTEN_ADDR" envDefault:"127.0.0.1"`
@ -115,10 +116,15 @@ func (envs *environments) buildErrColl() (errColl errcoll.Interface, err error)
}
// geoIP returns an GeoIP database implementation from environment.
func (envs *environments) geoIP(c *geoIPConfig) (g *geoip.File, err error) {
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, err = geoip.NewFile(&geoip.FileConfig{
g = geoip.NewFile(&geoip.FileConfig{
CacheManager: cacheManager,
ASNPath: envs.GeoIPASNPath,
CountryPath: envs.GeoIPCountryPath,
HostCacheSize: c.HostCacheSize,
@ -126,8 +132,10 @@ func (envs *environments) geoIP(c *geoIPConfig) (g *geoip.File, err error) {
AllTopASNs: geoip.DefaultTopASNs,
CountryTopASNs: geoip.DefaultCountryTopASNs,
})
err = g.Refresh(ctx)
if err != nil {
return nil, err
return nil, fmt.Errorf("creating geoip: initial refresh: %w", err)
}
return g, nil
@ -155,7 +163,7 @@ func (envs *environments) debugConf(dnsDB dnsdb.Interface) (conf *debugsvc.Confi
DNSDBAddr: dnsDBAddr,
DNSDBHandler: dnsDBHdlr,
HealthAddr: addr,
APIAddr: addr,
PprofAddr: addr,
PrometheusAddr: addr,
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/AdguardTeam/AdGuardDNS/internal/agdservice"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
@ -63,6 +64,7 @@ type filtersConfig struct {
// cacheDir must exist. c is assumed to be valid.
func (c *filtersConfig) toInternal(
errColl errcoll.Interface,
cacheManager agdcache.Manager,
envs *environments,
safeBrowsing *hashprefix.Filter,
adultBlocking *hashprefix.Filter,
@ -78,6 +80,7 @@ func (c *filtersConfig) toInternal(
NewRegDomains: newRegDomains,
Now: time.Now,
ErrColl: errColl,
CacheManager: cacheManager,
CacheDir: envs.FilterCachePath,
CustomFilterCacheSize: c.CustomFilterCacheSize,
SafeSearchCacheSize: c.SafeSearchCacheSize,
@ -148,11 +151,14 @@ func (c *fltRuleListCache) validate() (err error) {
// 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, err = filter.NewDefaultStorage(conf)
strg = filter.NewDefaultStorage(conf)
err = strg.RefreshInitial(ctx)
if err != nil {
return nil, fmt.Errorf("creating default filter storage: %w", err)
}
@ -162,12 +168,12 @@ func setupFilterStorage(
return context.WithTimeout(context.Background(), refreshTimeout)
},
Refresher: strg,
Name: "filters",
Name: "filter_storage",
Interval: conf.RefreshIvl,
RefreshOnShutdown: false,
RandomizeStart: false,
})
err = refr.Start(context.Background())
err = refr.Start(ctx)
if err != nil {
return nil, fmt.Errorf("starting default filter storage update: %w", err)
}

View File

@ -4,6 +4,7 @@ import (
"context"
"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"
@ -55,8 +56,12 @@ func setupGeoIP(
conf *geoIPConfig,
envs *environments,
errColl errcoll.Interface,
cacheManager agdcache.Manager,
) {
geoIP, err := envs.geoIP(conf)
// 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)
@ -78,7 +83,7 @@ func setupGeoIP(
RefreshOnShutdown: false,
RandomizeStart: false,
})
err = refr.Start(context.Background())
err = refr.Start(ctx)
if err != nil {
errCh <- fmt.Errorf("starting geoip refresher: %w", err)

View File

@ -138,6 +138,7 @@ 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,
@ -145,9 +146,11 @@ func setupRateLimiter(
) (rateLimiter *ratelimit.Backoff, connLimiter *connlimiter.Limiter, err error) {
allowSubnets := netutil.UnembedPrefixes(conf.Allowlist.List)
allowlist := ratelimit.NewDynamicAllowlist(allowSubnets, nil)
refresher, err := consul.NewAllowlistRefresher(allowlist, consulAllowlist, errColl)
refresher := consul.NewAllowlistRefresher(allowlist, consulAllowlist, errColl)
err = refresher.Refresh(ctx)
if err != nil {
return nil, nil, fmt.Errorf("creating allowlist refresher: %w", err)
return nil, nil, fmt.Errorf("allowlist: initial refresh: %w", err)
}
refr := agdservice.NewRefreshWorker(&agdservice.RefreshWorkerConfig{
@ -159,7 +162,7 @@ func setupRateLimiter(
RandomizeStart: false,
})
err = refr.Start(context.Background())
err = refr.Start(ctx)
if err != nil {
return nil, nil, fmt.Errorf("starting allowlist refresher: %w", err)
}

View File

@ -6,6 +6,7 @@ import (
"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"
@ -46,18 +47,21 @@ type safeBrowsingConfig struct {
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, err error) {
) (fltConf *hashprefix.FilterConfig) {
hashes, err := hashprefix.NewStorage("")
if err != nil {
return nil, err
// Don't expect errors here because we pass an empty string.
panic(err)
}
return &hashprefix.FilterConfig{
Cloner: cloner,
CacheManager: cacheManager,
Hashes: hashes,
URL: netutil.CloneURL(&url.URL),
ErrColl: errColl,
@ -69,7 +73,7 @@ func (c *safeBrowsingConfig) toInternal(
CacheTTL: c.CacheTTL.Duration,
CacheSize: c.CacheSize,
MaxSize: maxSize,
}, nil
}
}
// validate returns an error if the safe browsing filter configuration is
@ -96,23 +100,18 @@ func (c *safeBrowsingConfig) validate() (err error) {
// setupHashPrefixFilter creates and returns a hash-prefix filter as well as
// starts and registers its refresher in the signal handler.
func setupHashPrefixFilter(
conf *safeBrowsingConfig,
cloner *dnsmsg.Cloner,
id agd.FilterListID,
url *urlutil.URL,
cachePath string,
maxSize uint64,
ctx context.Context,
fltConf *hashprefix.FilterConfig,
sigHdlr *service.SignalHandler,
errColl errcoll.Interface,
) (strg *hashprefix.Storage, flt *hashprefix.Filter, err error) {
fltConf, err := conf.toInternal(errColl, cloner, id, url, cachePath, maxSize)
if err != nil {
return nil, nil, fmt.Errorf("configuring hash prefix filter %s: %w", id, err)
}
flt, err = hashprefix.NewFilter(fltConf)
if err != nil {
return nil, nil, fmt.Errorf("creating hash prefix filter %s: %w", id, err)
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{
@ -122,14 +121,14 @@ func setupHashPrefixFilter(
return context.WithTimeout(context.Background(), fltConf.RefreshTimeout)
},
Refresher: flt,
Name: string(id),
Name: string(fltConf.ID),
Interval: fltConf.Staleness,
RefreshOnShutdown: false,
RandomizeStart: false,
})
err = refr.Start(context.Background())
err = refr.Start(ctx)
if err != nil {
return nil, nil, fmt.Errorf("starting refresher for hash prefix filter %s: %w", id, err)
return nil, nil, fmt.Errorf("starting hash prefix filter %s refresher: %w", fltConf.ID, err)
}
sigHdlr.Add(refr)

View File

@ -184,7 +184,8 @@ func (certs tlsConfigCerts) validate() (err error) {
// TLS Session Ticket Key Rotator
// ticketRotator is a refresh worker that rereads and resets TLS session tickets.
// 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
@ -197,7 +198,7 @@ type ticketRotator struct {
func newTicketRotator(
errColl errcoll.Interface,
grps []*agd.ServerGroup,
) (tr *ticketRotator, err error) {
) (tr *ticketRotator) {
confs := map[*tls.Config][]string{}
for _, g := range grps {
@ -213,17 +214,10 @@ func newTicketRotator(
}
}
tr = &ticketRotator{
return &ticketRotator{
errColl: errColl,
confs: confs,
}
err = tr.Refresh(context.Background())
if err != nil {
return nil, fmt.Errorf("initial session ticket refresh: %w", err)
}
return tr, nil
}
// sessTickLen is the length of a single TLS session ticket key in bytes.
@ -320,9 +314,11 @@ func setupTicketRotator(
sigHdlr *service.SignalHandler,
errColl errcoll.Interface,
) (err error) {
tickRot, err := newTicketRotator(errColl, srvGrps)
tickRot := newTicketRotator(errColl, srvGrps)
err = tickRot.Refresh(context.Background())
if err != nil {
return fmt.Errorf("setting up ticket rotator: %w", err)
return fmt.Errorf("setting up ticket rotator: initial session ticket refresh: %w", err)
}
refr := agdservice.NewRefreshWorker(&agdservice.RefreshWorkerConfig{

View File

@ -19,7 +19,8 @@ import (
"github.com/AdguardTeam/golibs/log"
)
// AllowlistRefresher is a refresh wrapper that updates the allowlist.
// AllowlistRefresher is a refresh wrapper that updates the allowlist. It
// should be initially refreshed before use.
type AllowlistRefresher struct {
allowlist *ratelimit.DynamicAllowlist
http *agdhttp.Client
@ -32,8 +33,8 @@ func NewAllowlistRefresher(
allowlist *ratelimit.DynamicAllowlist,
consulURL *url.URL,
errColl errcoll.Interface,
) (l *AllowlistRefresher, err error) {
l = &AllowlistRefresher{
) (l *AllowlistRefresher) {
return &AllowlistRefresher{
allowlist: allowlist,
http: agdhttp.NewClient(&agdhttp.ClientConfig{
// TODO(a.garipov): Consider making configurable.
@ -42,13 +43,6 @@ func NewAllowlistRefresher(
url: consulURL,
errColl: errColl,
}
err = l.Refresh(context.Background())
if err != nil {
return nil, fmt.Errorf("initial refresh: %w", err)
}
return l, nil
}
// type check

View File

@ -8,6 +8,7 @@ import (
"net/netip"
"net/url"
"testing"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
@ -22,6 +23,9 @@ func TestMain(m *testing.M) {
testutil.DiscardLogOutput(m)
}
// testTimeout is the common timeout for tests and contexts.
const testTimeout = 1 * time.Second
// handleWithURL starts the test server with h, finishes it on cleanup, and
// returns it's URL.
//
@ -83,12 +87,15 @@ func TestNewAllowlistRefresher(t *testing.T) {
},
}
_, err := consul.NewAllowlistRefresher(al, u, errColl)
alr := consul.NewAllowlistRefresher(al, u, errColl)
ctx := testutil.ContextWithTimeout(t, testTimeout)
err := alr.Refresh(ctx)
require.NoError(t, err)
for _, ip := range tc.wantAllow {
var ok bool
ok, err = al.IsAllowed(context.Background(), ip)
ok, err = al.IsAllowed(ctx, ip)
require.NoError(t, err)
assert.True(t, ok)
@ -96,7 +103,7 @@ func TestNewAllowlistRefresher(t *testing.T) {
for _, ip := range tc.wantNotAllow {
var ok bool
ok, err = al.IsAllowed(context.Background(), ip)
ok, err = al.IsAllowed(ctx, ip)
require.NoError(t, err)
assert.False(t, ok)
@ -119,7 +126,10 @@ func TestNewAllowlistRefresher(t *testing.T) {
},
}
_, err := consul.NewAllowlistRefresher(al, u, errColl)
alr := consul.NewAllowlistRefresher(al, u, errColl)
ctx := testutil.ContextWithTimeout(t, testTimeout)
err := alr.Refresh(ctx)
require.ErrorAs(t, err, &wantErr)
assert.Equal(t, wantErr.Got, status)
@ -143,13 +153,12 @@ func TestAllowlistRefresher_Refresh_deadline(t *testing.T) {
},
}
c, err := consul.NewAllowlistRefresher(al, u, errColl)
require.NoError(t, err)
alr := consul.NewAllowlistRefresher(al, u, errColl)
ctx, cancel := context.WithCancel(context.Background())
cancel()
err = c.Refresh(ctx)
err := alr.Refresh(ctx)
assert.ErrorIs(t, err, context.Canceled)
assert.ErrorIs(t, gotCollErr, context.Canceled)
}

View File

@ -4,11 +4,13 @@ package debugsvc
import (
"context"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/osutil"
"github.com/AdguardTeam/golibs/pprofutil"
"github.com/AdguardTeam/golibs/service"
"github.com/prometheus/client_golang/prometheus/promhttp"
@ -17,16 +19,25 @@ import (
// Service is the HTTP service of AdGuard DNS. It serves prometheus metrics,
// pprof, health check, DNSDB, and other endpoints..
type Service struct {
servers map[string]*server
log *slog.Logger
refrHdlr *refreshHandler
dnsDB http.Handler
servers map[string]*server
}
// Config is the AdGuard DNS HTTP service configuration structure.
type Config struct {
Logger *slog.Logger
DNSDBAddr string
DNSDBHandler http.Handler
HealthAddr string
Refreshers Refreshers
// TODO(a.garipov): Consider using one address and removing addServer
// logic.
APIAddr string
PprofAddr string
PrometheusAddr string
}
@ -34,6 +45,10 @@ type Config struct {
// New returns a new properly initialized *Service.
func New(c *Config) (svc *Service) {
svc = &Service{
log: c.Logger,
refrHdlr: &refreshHandler{
refrs: c.Refreshers,
},
servers: make(map[string]*server),
dnsDB: c.DNSDBHandler,
}
@ -42,9 +57,9 @@ func New(c *Config) (svc *Service) {
svc.addServer(c.PrometheusAddr, "prometheus")
svc.addServer(c.PprofAddr, "pprof")
// The health-check server causes panic if it doesn't start because the
// server is needed to check if the dns server is active.
svc.addServer(c.HealthAddr, "health-check")
// The health-check and API server causes panic if it doesn't start because
// the server is needed to check if the dns server is active.
svc.addServer(c.APIAddr, "api")
return svc
}
@ -56,10 +71,10 @@ type server struct {
}
// startServer starts one server and panics if there is an unexpected error.
func startServer(s *server) {
defer log.OnPanicAndExit("startServer", 1)
func startServer(ctx context.Context, l *slog.Logger, s *server) {
defer recoverAndExit(ctx, l)
log.Info("debugsvc: %s: listen on %s", s.name, s.http.Addr)
l.Info("listening", "name", s.name, "addr", s.http.Addr)
srv := s.http
err := srv.ListenAndServe()
@ -68,6 +83,29 @@ func startServer(s *server) {
}
}
// recoverAndExit recovers a panic, logs it using l, and then exits with
// [osutil.ExitCodeFailure].
//
// TODO(a.garipov): Move to golibs.
func recoverAndExit(ctx context.Context, l *slog.Logger) {
v := recover()
if v == nil {
return
}
var args []any
if err, ok := v.(error); ok {
args = []any{slogutil.KeyError, err}
} else {
args = []any{"value", v}
}
l.ErrorContext(ctx, "recovered from panic", args...)
slogutil.PrintStack(ctx, l, slog.LevelError)
os.Exit(osutil.ExitCodeFailure)
}
// type check
var _ service.Interface = (*Service)(nil)
@ -78,9 +116,9 @@ var _ service.Interface = (*Service)(nil)
// TODO(a.garipov): Wait for the services to go online.
//
// TODO(a.garipov): Use the context for cancelation.
func (svc *Service) Start(_ context.Context) (err error) {
func (svc *Service) Start(ctx context.Context) (err error) {
for _, srv := range svc.servers {
go startServer(srv)
go startServer(ctx, svc.log, srv)
}
return nil
@ -98,10 +136,10 @@ func (svc *Service) Shutdown(ctx context.Context) (err error) {
srvNum++
log.Info("debugsvc: %s: server is shutdown", srv.name)
svc.log.Info("server is shutdown", "name", srv.name)
}
log.Info("debugsvc: servers shutdown: %d", srvNum)
svc.log.Info("all servers shutdown", "num", srvNum)
return nil
}
@ -127,7 +165,7 @@ func (svc *Service) addServer(addr, name string) {
http: &http.Server{
Addr: addr,
Handler: mux,
ErrorLog: log.StdLog("debugsvc", log.DEBUG),
ErrorLog: slog.NewLogLogger(svc.log.Handler(), slog.LevelDebug),
},
name: name,
}
@ -143,35 +181,37 @@ func (svc *Service) addServer(addr, name string) {
// addHandler func returns the resMux that combine with the mux from args.
func (svc *Service) addHandler(serviceName string, mux *http.ServeMux) {
switch serviceName {
case "api":
svc.apiMux(mux)
case "dnsdb":
svc.dnsDBMux(mux)
case "health-check":
healthMux(mux)
case "pprof":
// TODO(a.garipov): Find ways to wrap pprof handlers.
pprofutil.RoutePprof(mux)
case "prometheus":
promMux(mux)
svc.promMux(mux)
default:
panic(fmt.Errorf("debugsvc: could not find mux for service %q", serviceName))
}
}
// apiMux adds the health-check and other debug API handlers to mux.
func (svc *Service) apiMux(mux *http.ServeMux) {
mux.Handle("GET /health-check", svc.middleware(
http.HandlerFunc(serveHealthCheck),
slog.LevelDebug,
))
mux.Handle("POST /debug/api/refresh", svc.middleware(svc.refrHdlr, slog.LevelInfo))
}
// dnsDBMux adds the DNSDB CSV dump handler to mux.
//
// TODO(a.garipov): Tests.
func (svc *Service) dnsDBMux(mux *http.ServeMux) {
mux.Handle("/dnsdb/csv", svc.dnsDB)
mux.Handle("POST /dnsdb/csv", svc.middleware(svc.dnsDB, slog.LevelInfo))
}
// healthMux adds handler func to the mux from args for the health check service.
func healthMux(mux *http.ServeMux) {
mux.HandleFunc(
"/health-check",
func(w http.ResponseWriter, _ *http.Request) {
_, _ = io.WriteString(w, "OK")
},
)
}
// promMux adds handler func to the mux from args for the prometheus service.
func promMux(mux *http.ServeMux) {
mux.Handle("/metrics", promhttp.Handler())
// promMux adds the prometheus service handler to mux.
func (svc *Service) promMux(mux *http.ServeMux) {
mux.Handle("GET /metrics", svc.middleware(promhttp.Handler(), slog.LevelDebug))
}

View File

@ -1,21 +1,23 @@
package debugsvc_test
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"testing"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/debugsvc"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
testutil.DiscardLogOutput(m)
}
// TODO(a.garipov): Improve and split tests.
// testTimeout is a common timeout for tests.
const testTimeout = 1 * time.Second
@ -31,10 +33,21 @@ func TestService_Start(t *testing.T) {
require.NoError(pt, err)
})
refreshed := false
c := &debugsvc.Config{
Logger: slogutil.NewDiscardLogger(),
DNSDBAddr: addr,
DNSDBHandler: h,
HealthAddr: addr,
Refreshers: debugsvc.Refreshers{
"test": &agdtest.Refresher{
OnRefresh: func(_ context.Context) (err error) {
refreshed = true
return nil
},
},
},
APIAddr: addr,
PprofAddr: addr,
PrometheusAddr: addr,
}
@ -58,15 +71,15 @@ func TestService_Start(t *testing.T) {
var resp *http.Response
var body []byte
// First check health-check service URL.
// As the service could not be ready yet, check for it in periodically.
// First check health-check service URL. As the service could not be ready
// yet, check for it in periodically.
require.Eventually(t, func() bool {
resp, err = client.Get(fmt.Sprintf("http://%s/health-check", addr))
return err == nil
}, 1*time.Second, 100*time.Millisecond)
body = readRespBody(t, resp)
assert.Equal(t, []byte("OK"), body)
assert.Equal(t, []byte("OK\n"), body)
assert.Equal(t, http.StatusOK, resp.StatusCode)
// Check pprof service URL.
@ -84,6 +97,18 @@ func TestService_Start(t *testing.T) {
body = readRespBody(t, resp)
assert.True(t, len(body) > 0)
assert.Equal(t, http.StatusOK, resp.StatusCode)
// Check refresh API.
reqBody := strings.NewReader(`{"ids":["test"]}`)
urlStr := fmt.Sprintf("http://%s/debug/api/refresh", addr)
resp, err = client.Post(urlStr, "application/json", reqBody)
require.NoError(t, err)
assert.True(t, refreshed)
assert.Equal(t, http.StatusOK, resp.StatusCode)
body = readRespBody(t, resp)
assert.Equal(t, []byte(`{"results":{"test":"ok"}}`+"\n"), body)
}
// readRespBody is a helper function that reads and returns

View File

@ -0,0 +1,24 @@
package debugsvc
import (
"io"
"net/http"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/logutil/slogutil"
)
// serveHealthCheck handles the GET /health-check endpoint.
//
// TODO(a.garipov): Move to golibs.
func serveHealthCheck(w http.ResponseWriter, r *http.Request) {
w.Header().Set(httphdr.ContentType, "text/plain")
w.WriteHeader(http.StatusOK)
_, err := io.WriteString(w, "OK\n")
if err != nil {
ctx := r.Context()
l := slogutil.MustLoggerFromContext(ctx)
l.DebugContext(ctx, "writing health-check response", slogutil.KeyError, err)
}
}

View File

@ -0,0 +1,62 @@
package debugsvc
import (
"log/slog"
"net/http"
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/logutil/slogutil"
)
// middleware is the base middleware for AdGuard DNS debug API that adds a
// logger and logs the queries starting and finishing at the given level.
func (svc *Service) middleware(h http.Handler, lvl slog.Level) (wrapped http.Handler) {
f := func(w http.ResponseWriter, r *http.Request) {
respHdr := w.Header()
respHdr.Add(httphdr.Server, agdhttp.UserAgent())
l := svc.log.With(
"raddr", r.RemoteAddr,
"method", r.Method,
"host", r.Host,
"request_uri", r.RequestURI,
)
ctx := slogutil.ContextWithLogger(r.Context(), l)
r = r.WithContext(ctx)
rw := &codeRecorderResponseWriter{
ResponseWriter: w,
}
l.Log(ctx, lvl, "started")
defer func() { l.Log(ctx, lvl, "finished", "code", rw.code) }()
h.ServeHTTP(rw, r)
}
return http.HandlerFunc(f)
}
// codeRecorderResponseWriter wraps an [http.ResponseWriter] allowing to save
// the response code.
//
// TODO(a.garipov): Process zero code.
//
// TODO(a.garipov): Move to golibs.
type codeRecorderResponseWriter struct {
http.ResponseWriter
code int
}
// type check
var _ http.ResponseWriter = (*codeRecorderResponseWriter)(nil)
// WriteHeader implements [http.ResponseWriter] for *codeRecorderResponseWriter.
func (w *codeRecorderResponseWriter) WriteHeader(code int) {
w.code = code
w.ResponseWriter.WriteHeader(code)
}

View File

@ -0,0 +1,128 @@
package debugsvc
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"slices"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agdservice"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"golang.org/x/exp/maps"
)
// RefresherID is a type alias for strings that represent IDs of refreshers.
//
// TODO(a.garipov): Consider a newtype with validations.
type RefresherID = string
// Refreshers is a type alias for maps of refresher IDs to Refreshers
// themselves.
type Refreshers map[RefresherID]agdservice.Refresher
// refreshHandler performs debug refreshes.
type refreshHandler struct {
refrs Refreshers
}
// refreshRequest describes the request to the POST /debug/api/refresh HTTP API.
type refreshRequest struct {
IDs []RefresherID `json:"ids"`
}
// refreshResponse describes the response to the POST /debug/api/refresh HTTP
// API.
type refreshResponse struct {
Results map[RefresherID]string `json:"results"`
}
// type check
var _ http.Handler = (*refreshHandler)(nil)
// ServeHTTP implements the [http.Handler] interface for *refreshHandler.
func (h *refreshHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
l := slogutil.MustLoggerFromContext(ctx)
req := &refreshRequest{}
err := json.NewDecoder(r.Body).Decode(req)
if err != nil {
l.ErrorContext(ctx, "decoding request", slogutil.KeyError, err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
resp := &refreshResponse{
Results: map[RefresherID]string{},
}
reqIDs, err := h.idsFromReq(req.IDs)
if err != nil {
l.ErrorContext(ctx, "validating request", slogutil.KeyError, err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
for _, id := range reqIDs {
resp.Results[id] = h.refresh(ctx, l, id)
}
w.Header().Set(httphdr.ContentType, "application/json")
err = json.NewEncoder(w).Encode(resp)
if err != nil {
l.ErrorContext(ctx, "writing response", slogutil.KeyError, err)
}
}
// idsFromReq validates the form of the request and returns the IDs of
// refreshers to refresh.
func (h *refreshHandler) idsFromReq(reqIDs []RefresherID) (ids []RefresherID, err error) {
l := len(reqIDs)
switch l {
case 0:
return nil, errors.Error("no ids")
case 1:
if reqIDs[0] != "*" {
return reqIDs, nil
}
allIDs := maps.Keys(h.refrs)
slices.Sort(allIDs)
return allIDs, nil
default:
starIdx := slices.Index(reqIDs, "*")
if starIdx == -1 {
return reqIDs, nil
}
return nil, errors.Error(`"*" cannot be used with other ids`)
}
}
// refresh performs a single refresh and returns the result as a string.
func (h *refreshHandler) refresh(ctx context.Context, l *slog.Logger, id RefresherID) (res string) {
r, ok := h.refrs[id]
if !ok {
return "error: refresher not found"
}
start := time.Now()
err := r.Refresh(ctx)
if err != nil {
l.ErrorContext(ctx, "refresher error", "id", id, slogutil.KeyError, err)
return fmt.Sprintf("error: %s", err)
}
l.InfoContext(ctx, "refresh finished", "id", id, "duration", time.Since(start))
return "ok"
}

View File

@ -175,6 +175,7 @@ var clonerTestCases = []clonerTestCase{{
&dns.SVCBLocal{KeyCode: dns.SVCBKey(1234), Data: []byte{3, 2, 1, 0}},
&dns.SVCBMandatory{Code: []dns.SVCBKey{dns.SVCB_ALPN}},
&dns.SVCBNoDefaultAlpn{},
&dns.SVCBOhttp{},
&dns.SVCBPort{Port: 443},
}),
name: "resp_https",
@ -195,6 +196,14 @@ var clonerTestCases = []clonerTestCase{{
name: "resp_https_empty_mandatory",
wantFull: assert.True,
handledByClone: true,
}, {
msg: newHTTPSResp([]dns.SVCBKeyValue{
&dns.SVCBNoDefaultAlpn{},
&dns.SVCBOhttp{},
}),
name: "resp_https_empty_values",
wantFull: assert.True,
handledByClone: true,
}, {
msg: newHTTPSResp(nil),
name: "resp_https_nil_hint",
@ -338,26 +347,27 @@ func BenchmarkClone(b *testing.B) {
// Most recent results, on a ThinkPad X13 with a Ryzen Pro 7 CPU:
//
// goos: linux
// goos: darwin
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/querylog
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkClone/req_a-16 24691725 250.1 ns/op 168 B/op 2 allocs/op
// BenchmarkClone/resp_a-16 12547648 429.8 ns/op 256 B/op 5 allocs/op
// BenchmarkClone/resp_a_many-16 10174539 602.5 ns/op 344 B/op 7 allocs/op
// BenchmarkClone/resp_a_soa-16 10228933 600.0 ns/op 368 B/op 6 allocs/op
// BenchmarkClone/req_aaaa-16 24920611 248.6 ns/op 168 B/op 2 allocs/op
// BenchmarkClone/resp_aaaa-16 13603160 474.0 ns/op 264 B/op 5 allocs/op
// BenchmarkClone/resp_cname_a-16 10398249 589.2 ns/op 320 B/op 6 allocs/op
// BenchmarkClone/resp_mx-16 15299034 414.9 ns/op 248 B/op 4 allocs/op
// BenchmarkClone/resp_ptr-16 14701116 386.7 ns/op 232 B/op 4 allocs/op
// BenchmarkClone/resp_txt-16 11148487 495.6 ns/op 296 B/op 5 allocs/op
// BenchmarkClone/resp_srv-16 16175085 398.3 ns/op 248 B/op 4 allocs/op
// BenchmarkClone/resp_not_full-16 16115785 366.2 ns/op 248 B/op 4 allocs/op
// BenchmarkClone/resp_https-16 2940937 1998 ns/op 880 B/op 24 allocs/op
// BenchmarkClone/resp_https_empty_hint-16 8712819 744.4 ns/op 424 B/op 8 allocs/op
// BenchmarkClone/resp_https_empty_mandatory-16 8605273 655.8 ns/op 384 B/op 7 allocs/op
// BenchmarkClone/resp_a_ecs-16 7337880 777.1 ns/op 384 B/op 8 allocs/op
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg
// cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
// BenchmarkClone/req_a-12 8987066 123.7 ns/op 168 B/op 2 allocs/op
// BenchmarkClone/resp_a-12 5619037 212.7 ns/op 256 B/op 5 allocs/op
// BenchmarkClone/resp_a_many-12 4385636 273.2 ns/op 344 B/op 7 allocs/op
// BenchmarkClone/resp_a_soa-12 4533913 263.1 ns/op 368 B/op 6 allocs/op
// BenchmarkClone/req_aaaa-12 10196326 116.9 ns/op 168 B/op 2 allocs/op
// BenchmarkClone/resp_aaaa-12 5666234 211.8 ns/op 264 B/op 5 allocs/op
// BenchmarkClone/resp_cname_a-12 4713193 255.4 ns/op 320 B/op 6 allocs/op
// BenchmarkClone/resp_mx-12 6539623 181.8 ns/op 248 B/op 4 allocs/op
// BenchmarkClone/resp_ptr-12 6744601 179.8 ns/op 232 B/op 4 allocs/op
// BenchmarkClone/resp_txt-12 5120538 230.8 ns/op 296 B/op 5 allocs/op
// BenchmarkClone/resp_srv-12 6549402 181.5 ns/op 248 B/op 4 allocs/op
// BenchmarkClone/resp_not_full-12 6529968 181.4 ns/op 248 B/op 4 allocs/op
// BenchmarkClone/resp_https-12 1361680 880.8 ns/op 896 B/op 24 allocs/op
// BenchmarkClone/resp_https_empty_hint-12 3548672 345.3 ns/op 424 B/op 8 allocs/op
// BenchmarkClone/resp_https_empty_mandatory-12 4055365 291.6 ns/op 384 B/op 7 allocs/op
// BenchmarkClone/resp_https_empty_values-12 4434608 269.8 ns/op 376 B/op 6 allocs/op
// BenchmarkClone/resp_a_ecs-12 3741008 318.5 ns/op 384 B/op 8 allocs/op
}
func BenchmarkCloner_Clone(b *testing.B) {
@ -388,28 +398,29 @@ func BenchmarkCloner_Clone(b *testing.B) {
// Most recent results, on a ThinkPad X13 with a Ryzen Pro 7 CPU:
//
// goos: linux
// goos: darwin
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/querylog
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkCloner_Clone/req_a-16 167307522 36.48 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_a-16 92398767 66.01 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_a_many-16 60790945 111.8 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_a_soa-16 61474227 91.50 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/req_aaaa-16 158363983 39.89 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_aaaa-16 72113028 76.83 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_cname_a-16 67518502 89.24 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_mx-16 89713944 70.96 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_ptr-16 87175648 67.42 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_txt-16 80373494 75.37 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_srv-16 85734901 70.36 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_not_full-16 28868667 211.3 ns/op 64 B/op 1 allocs/op
// BenchmarkCloner_Clone/resp_https-16 13196191 402.3 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_https_empty_hint-16 48459688 125.6 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_https_empty_mandatory-16 63759298 96.10 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_https_nil_hint-16 89683288 66.75 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_a_ecs-16 53174110 119.6 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_a_ecs_nil-16 69814755 92.42 ns/op 0 B/op 0 allocs/op
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg
// cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
// BenchmarkCloner_Clone/req_a-12 28363105 39.40 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_a-12 17288836 72.96 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_a_many-12 11043524 105.9 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_a_soa-12 14058405 84.21 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/req_aaaa-12 31732440 37.61 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_aaaa-12 19054647 65.48 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_cname_a-12 13343605 90.35 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_mx-12 19749904 59.09 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_ptr-12 20109849 60.00 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_txt-12 17270551 64.07 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_srv-12 18197905 66.67 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_not_full-12 12713991 95.55 ns/op 64 B/op 1 allocs/op
// BenchmarkCloner_Clone/resp_https-12 3037629 395.9 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_https_empty_hint-12 10258635 119.3 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_https_empty_mandatory-12 14306239 82.14 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_https_empty_values-12 15568735 80.12 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_https_nil_hint-12 20536422 59.70 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_a_ecs-12 10978351 107.8 ns/op 0 B/op 0 allocs/op
// BenchmarkCloner_Clone/resp_a_ecs_nil-12 13626586 93.56 ns/op 0 B/op 0 allocs/op
}
func FuzzCloner_Clone(f *testing.F) {

View File

@ -23,7 +23,6 @@ type httpsCloner struct {
ipv6hint *syncutil.Pool[dns.SVCBIPv6Hint]
local *syncutil.Pool[dns.SVCBLocal]
mandatory *syncutil.Pool[dns.SVCBMandatory]
noDefALPN *syncutil.Pool[dns.SVCBNoDefaultAlpn]
port *syncutil.Pool[dns.SVCBPort]
// Miscellaneous.
@ -59,9 +58,6 @@ func newHTTPSCloner() (c *httpsCloner) {
mandatory: syncutil.NewPool(func() (v *dns.SVCBMandatory) {
return &dns.SVCBMandatory{}
}),
noDefALPN: syncutil.NewPool(func() (v *dns.SVCBNoDefaultAlpn) {
return &dns.SVCBNoDefaultAlpn{}
}),
port: syncutil.NewPool(func() (v *dns.SVCBPort) {
return &dns.SVCBPort{}
}),
@ -94,8 +90,8 @@ func (c *httpsCloner) clone(rr *dns.HTTPS) (clone *dns.HTTPS, full bool) {
clone.Value = clone.Value[:0]
for _, orig := range rr.Value {
valClone, valFull := c.cloneKV(orig)
if !valFull {
valClone := c.cloneKV(orig)
if valClone == nil {
// This branch is only reached if there is a new SVCB key-value type
// in miekg/dns. Give up and just use their copy function.
return dns.Copy(rr).(*dns.HTTPS), false
@ -107,12 +103,12 @@ func (c *httpsCloner) clone(rr *dns.HTTPS) (clone *dns.HTTPS, full bool) {
return clone, true
}
// cloneKV returns a deep clone of orig. full is true if orig was recognized.
func (c *httpsCloner) cloneKV(orig dns.SVCBKeyValue) (clone dns.SVCBKeyValue, full bool) {
// cloneKV returns a deep clone of orig. clone is nil if orig wasn't
// recognized.
func (c *httpsCloner) cloneKV(orig dns.SVCBKeyValue) (clone dns.SVCBKeyValue) {
switch orig := orig.(type) {
case *dns.SVCBAlpn:
v := c.alpn.Get()
v.Alpn = appendIfNotNil(v.Alpn[:0], orig.Alpn)
clone = v
@ -123,49 +119,57 @@ func (c *httpsCloner) cloneKV(orig dns.SVCBKeyValue) (clone dns.SVCBKeyValue, fu
clone = v
case *dns.SVCBECHConfig:
v := c.echconfig.Get()
v.ECH = appendIfNotNil(v.ECH[:0], orig.ECH)
clone = v
case *dns.SVCBIPv4Hint:
v := c.ipv4hint.Get()
v.Hint = c.appendIPs(v.Hint[:0], orig.Hint)
clone = v
case *dns.SVCBIPv6Hint:
v := c.ipv6hint.Get()
v.Hint = c.appendIPs(v.Hint[:0], orig.Hint)
clone = v
case *dns.SVCBLocal:
v := c.local.Get()
v.KeyCode = orig.KeyCode
v.Data = appendIfNotNil(v.Data[:0], orig.Data)
clone = v
case *dns.SVCBMandatory:
v := c.mandatory.Get()
v.Code = appendIfNotNil(v.Code[:0], orig.Code)
clone = v
case *dns.SVCBNoDefaultAlpn:
clone = c.noDefALPN.Get()
case *dns.SVCBPort:
v := c.port.Get()
*v = *orig
clone = v
case
*dns.SVCBNoDefaultAlpn,
*dns.SVCBOhttp:
// Just use the original value since these [dns.SVCBKeyValue] types are
// pointers to empty structures, so we're only interested in the actual
// type.
clone = orig
default:
// This branch is only reached if there is a new SVCB key-value type
// in miekg/dns.
return nil, false
clone = c.cloneIfHint(orig)
}
return clone, true
// This is only nil if there is a new SVCB key-value type in miekg/dns.
return clone
}
// cloneIfHint returns a deep clone of orig if it's either an [dns.SVCBIPv4Hint]
// or [dns.SVCBIPv6Hint]. Otherwise, it returns nil.
func (c *httpsCloner) cloneIfHint(orig dns.SVCBKeyValue) (clone dns.SVCBKeyValue) {
switch orig := orig.(type) {
case *dns.SVCBIPv4Hint:
v := c.ipv4hint.Get()
v.Hint = c.appendIPs(v.Hint[:0], orig.Hint)
return v
case *dns.SVCBIPv6Hint:
v := c.ipv6hint.Get()
v.Hint = c.appendIPs(v.Hint[:0], orig.Hint)
return v
default:
return nil
}
}
// appendIPs appends the clones of IP addresses from orig to hints and returns
@ -220,10 +224,12 @@ func (c *httpsCloner) putKV(kv dns.SVCBKeyValue) {
c.local.Put(kv)
case *dns.SVCBMandatory:
c.mandatory.Put(kv)
case *dns.SVCBNoDefaultAlpn:
c.noDefALPN.Put(kv)
case *dns.SVCBPort:
c.port.Put(kv)
case
*dns.SVCBNoDefaultAlpn,
*dns.SVCBOhttp:
// Don't use pool for empty structures, see comment in [cloneKV].
default:
// This branch is only reached if there is a new SVCB key-value type
// in miekg/dns. Noting to do.
@ -234,7 +240,6 @@ func (c *httpsCloner) putKV(kv dns.SVCBKeyValue) {
func (c *httpsCloner) putIPs(ips []net.IP) {
for _, ip := range ips {
if cap(ip) >= 16 {
// nolint:looppointer // Slicing is used to get the array pointer.
c.ip.Put((*[16]byte)(ip[:16]))
}
}

View File

@ -39,8 +39,8 @@ func TestMiddleware_Wrap(t *testing.T) {
testCases := []struct {
req *dns.Msg
resp *dns.Msg
name string
minTTL *time.Duration
name string
wantNumReq int
wantTTL uint32
}{{

View File

@ -133,9 +133,9 @@ type RequestInfo struct {
// StartTime is the request's start time. It's never zero value.
StartTime time.Time
// TLSServerName is the server name field of the client's TLS hello request.
// It is set only if the protocol of the server is either DoQ, DoT or DoH.
// Note, that the original SNI is transformed to lower-case.
// TLSServerName is the original, non-lowercased server name field of the
// client's TLS hello request. It is set only if the protocol of the server
// is either DoQ, DoT or DoH.
//
// TODO(ameshkov): use r.TLS with DoH3 (see addRequestInfo).
TLSServerName string

View File

@ -1,21 +1,21 @@
module github.com/AdguardTeam/AdGuardDNS/internal/dnsserver
go 1.22.4
go 1.22.5
require (
github.com/AdguardTeam/golibs v0.23.2
github.com/AdguardTeam/golibs v0.24.0
github.com/ameshkov/dnscrypt/v2 v2.3.0
github.com/ameshkov/dnsstamps v1.0.3
github.com/bluele/gcache v0.0.2
github.com/miekg/dns v1.1.58
github.com/panjf2000/ants/v2 v2.9.1
github.com/miekg/dns v1.1.61
github.com/panjf2000/ants/v2 v2.10.0
github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible
github.com/prometheus/client_golang v1.19.0
github.com/quic-go/quic-go v0.42.0
github.com/prometheus/client_golang v1.19.1
github.com/quic-go/quic-go v0.45.0
github.com/stretchr/testify v1.9.0
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8
golang.org/x/net v0.24.0
golang.org/x/sys v0.19.0
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
golang.org/x/net v0.26.0
golang.org/x/sys v0.21.0
)
require (
@ -24,22 +24,21 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/pprof v0.0.0-20240618054019-d3b898a103f8 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/onsi/ginkgo/v2 v2.17.1 // indirect
github.com/onsi/ginkgo/v2 v2.19.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.52.3 // indirect
github.com/prometheus/procfs v0.13.0 // indirect
github.com/prometheus/common v0.54.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
go.uber.org/mock v0.4.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.20.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/tools v0.22.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -1,5 +1,5 @@
github.com/AdguardTeam/golibs v0.23.2 h1:rMjYantwtQ39e8G4zBQ6ZLlm4s3XH30Bc9VxhoOHwao=
github.com/AdguardTeam/golibs v0.23.2/go.mod h1:o9i55Sx6v7qogRQeqaBfmLbC/pZqeMBWi015U5PTDY0=
github.com/AdguardTeam/golibs v0.24.0 h1:qAnOq7BQtwSVo7Co9q703/n+nZ2Ap6smkugU9G9MomY=
github.com/AdguardTeam/golibs v0.24.0/go.mod h1:9/vJcYznW7RlmCT/Qzi8XNZGj+ZbWfHZJmEXKnRpCAU=
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/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
@ -20,48 +20,45 @@ 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/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
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/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/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/pprof v0.0.0-20240618054019-d3b898a103f8 h1:ASJ/LAqdCHOyMYI+dwNxn7Rd8FscNkMyTr1KZU1JI/M=
github.com/google/pprof v0.0.0-20240618054019-d3b898a103f8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/panjf2000/ants/v2 v2.9.1 h1:Q5vh5xohbsZXGcD6hhszzGqB7jSSc2/CRr3QKIga8Kw=
github.com/panjf2000/ants/v2 v2.9.1/go.mod h1:7ZxyxsqE4vvW0M7LSD8aI3cKwgFhBHbxnlN8mDqHa1I=
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
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/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/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
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 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
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/common v0.52.3 h1:5f8uj6ZwHSscOGNdIQg6OiZv/ybiK2CO2q2drVZAQSA=
github.com/prometheus/common v0.52.3/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8=
github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
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/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM=
github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
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/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
@ -69,27 +66,27 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc=
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
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.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@ -18,21 +18,24 @@ const ErrClosed = errors.Error("the pool is closed")
// must use the context's deadline if it's specified.
type Factory func(ctx context.Context) (conn net.Conn, err error)
// Pool is a structure that implements a net.Conn pool. Must be initialized
// using the NewPool method.
// Pool is a structure that implements a net.Conn pool.
type Pool struct {
// IdleTimeout is the maximum TTL of an idle connection in the pool.
// Connections that weren't used for more than the specified duration will
// be closed. If set to 0, connections don't expire. Default value is 0.
IdleTimeout time.Duration
// connsChanMu is used to synchronize the closing of connsChan.
connsChanMu *sync.RWMutex
// connsChan is the storage for our connections.
connsChan chan *Conn
connsChanMu sync.RWMutex
// factory is the Pool's factory method. It is called whenever there are no
// more connections in the pool.
factory Factory
// IdleTimeout is the maximum TTL of an idle connection in the pool.
// Connections that weren't used for more than the specified duration will
// be closed. If set to 0, connections don't expire.
//
// TODO(a.garipov): Put into a config.
IdleTimeout time.Duration
}
// NewPool creates a new Pool instance. maxCapacity configures the maximum
@ -41,6 +44,7 @@ type Pool struct {
func NewPool(maxCapacity int, factory Factory) (p *Pool) {
return &Pool{
connsChan: make(chan *Conn, maxCapacity),
connsChanMu: &sync.RWMutex{},
factory: factory,
}
}

View File

@ -15,12 +15,6 @@ import (
// ConfigBase contains the necessary minimum that every Server needs to
// be initialized.
type ConfigBase struct {
// Network is the network this server listens to. If empty, the server will
// listen to all networks that are supposed to be used by the server's
// protocol. Note, that it only makes sense for [ServerDNS],
// [ServerDNSCrypt], and [ServerHTTPS].
Network Network
// Handler is a handler that processes incoming DNS messages. If not set,
// the default handler, which returns error response to any query, is used.
Handler Handler
@ -41,6 +35,12 @@ type ConfigBase struct {
// DNS server. If nil, an appropriate default ListenConfig is used.
ListenConfig netext.ListenConfig
// Network is the network this server listens to. If empty, the server will
// listen to all networks that are supposed to be used by the server's
// protocol. Note, that it only makes sense for [ServerDNS],
// [ServerDNSCrypt], and [ServerHTTPS].
Network Network
// Name is used for logging, and it may be used for perf counters reporting.
Name string
@ -51,19 +51,6 @@ type ConfigBase struct {
// ServerBase implements base methods that every Server implementation uses.
type ServerBase struct {
// name is used for logging and it may be used for perf counters reporting.
name string
// addr is the address the server listens to.
addr string
// proto is the server protocol.
proto Protocol
// network is the network to listen to. It only makes sense for the
// following protocols: ProtoDNS, ProtoDNSCrypt, ProtoDoH.
network Network
// handler is a handler that processes incoming DNS messages.
handler Handler
@ -79,9 +66,6 @@ type ServerBase struct {
// listenConfig is used to set tcpListener and udpListener.
listenConfig netext.ListenConfig
// Server operation
// --
// tcpListener is used to accept new TCP connections. It is nil for servers
// that don't use TCP.
tcpListener net.Listener
@ -90,15 +74,27 @@ type ServerBase struct {
// that don't use UDP.
udpListener net.PacketConn
// Shutdown handling
// --
// mu protects started, tcpListener, and udpListener.
mu *sync.RWMutex
// lock protects started, tcpListener and udpListener.
lock sync.RWMutex
started bool
// wg tracks active workers (listeners or query processing). Shutdown
// won't finish until there's at least one active worker.
wg sync.WaitGroup
wg *sync.WaitGroup
// name is used for logging and it may be used for perf counters reporting.
name string
// addr is the address the server listens to.
addr string
// network is the network to listen to. It only makes sense for the
// following protocols: [ProtoDNS], [ProtoDNSCrypt], [ProtoDoH].
network Network
// proto is the server protocol.
proto Protocol
started bool
}
// type check
@ -108,15 +104,17 @@ var _ Server = (*ServerBase)(nil)
// some of its internal properties.
func newServerBase(proto Protocol, conf ConfigBase) (s *ServerBase) {
s = &ServerBase{
name: conf.Name,
addr: conf.Addr,
proto: proto,
network: conf.Network,
handler: conf.Handler,
reqCtx: conf.RequestContext,
metrics: conf.Metrics,
disposer: conf.Disposer,
listenConfig: conf.ListenConfig,
reqCtx: conf.RequestContext,
mu: &sync.RWMutex{},
wg: &sync.WaitGroup{},
name: conf.Name,
addr: conf.Addr,
network: conf.Network,
proto: proto,
}
if s.reqCtx == nil {
@ -473,8 +471,8 @@ func (s *ServerBase) waitShutdown(ctx context.Context) (err error) {
// isStarted returns true if the server is started.
func (s *ServerBase) isStarted() (started bool) {
s.lock.RLock()
defer s.lock.RUnlock()
s.mu.RLock()
defer s.mu.RUnlock()
return s.started
}

View File

@ -44,6 +44,15 @@ type ConfigDNS struct {
// not set it defaults to DefaultWriteTimeout.
WriteTimeout time.Duration
// TCPIdleTimeout is the timeout for waiting between multiple queries. If
// not set it defaults to [DefaultTCPIdleTimeout].
TCPIdleTimeout time.Duration
// MaxPipelineCount is the maximum number of simultaneously processing TCP
// messages per one connection. If MaxPipelineEnabled is true, it must be
// greater than zero.
MaxPipelineCount uint
// UDPSize is the size of the buffers used to read incoming UDP messages.
// If not set it defaults to [dns.MinMsgSize], 512 B.
UDPSize int
@ -55,15 +64,6 @@ type ConfigDNS struct {
// MaxUDPRespSize is the maximum size of DNS response over UDP protocol.
MaxUDPRespSize uint16
// TCPIdleTimeout is the timeout for waiting between multiple queries. If
// not set it defaults to [DefaultTCPIdleTimeout].
TCPIdleTimeout time.Duration
// MaxPipelineCount is the maximum number of simultaneously processing TCP
// messages per one connection. If MaxPipelineEnabled is true, it must be
// greater than zero.
MaxPipelineCount uint
// MaxPipelineEnabled, if true, enables TCP pipeline limiting.
MaxPipelineEnabled bool
}
@ -143,8 +143,8 @@ func newServerDNS(proto Protocol, conf ConfigDNS) (s *ServerDNS) {
func (s *ServerDNS) Start(ctx context.Context) (err error) {
defer func() { err = errors.Annotate(err, "starting dns server: %w") }()
s.lock.Lock()
defer s.lock.Unlock()
s.mu.Lock()
defer s.mu.Unlock()
if s.started {
return ErrServerAlreadyStarted
@ -243,8 +243,9 @@ func (s *ServerDNS) startServeTCP(ctx context.Context) {
// shutdown marks the server as stopped and closes active listeners.
func (s *ServerDNS) shutdown() (err error) {
s.lock.Lock()
defer s.lock.Unlock()
s.mu.Lock()
defer s.mu.Unlock()
if !s.started {
return ErrServerNotStarted
}

View File

@ -25,11 +25,11 @@ func TestServerDNS_StartShutdown(t *testing.T) {
func TestServerDNS_integration_query(t *testing.T) {
testCases := []struct {
handler dnsserver.Handler
req *dns.Msg
wantMsg func(t *testing.T, m *dns.Msg)
name string
network dnsserver.Network
req *dns.Msg
handler dnsserver.Handler
wantMsg func(t *testing.T, m *dns.Msg)
wantRecordsCount int
wantRCode int
wantTruncated bool
@ -419,10 +419,10 @@ func TestServerDNS_integration_tcpMsgIgnore(t *testing.T) {
t.Parallel()
testCases := []struct {
expectedError func(err error)
name string
buf []byte
timeout time.Duration
expectedError func(err error)
}{
{
name: "invalid_input_timeout",

View File

@ -2,6 +2,7 @@ package dnsserver
import (
"context"
"fmt"
"net"
"time"
@ -17,23 +18,20 @@ import (
type ConfigDNSCrypt struct {
ConfigBase
// DNSCryptProviderName is a DNSCrypt provider name (see DNSCrypt spec).
DNSCryptProviderName string
// DNSCryptResolverCert is a DNSCrypt server certificate.
DNSCryptResolverCert *dnscrypt.Cert
// DNSCryptProviderName is a DNSCrypt provider name (see DNSCrypt spec).
DNSCryptProviderName string
}
// ServerDNSCrypt is a DNSCrypt server implementation.
type ServerDNSCrypt struct {
*ServerBase
dnsCryptServer *dnscrypt.Server
conf ConfigDNSCrypt
// Internal server properties
// --
dnsCryptServer *dnscrypt.Server // dnscrypt server instance
}
// type check
@ -55,8 +53,8 @@ func NewServerDNSCrypt(conf ConfigDNSCrypt) (s *ServerDNSCrypt) {
func (s *ServerDNSCrypt) Start(ctx context.Context) (err error) {
defer func() { err = errors.Annotate(err, "starting dnscrypt server: %w") }()
s.lock.Lock()
defer s.lock.Unlock()
s.mu.Lock()
defer s.mu.Unlock()
// First, validate the protocol.
if s.proto != ProtoDNSCrypt {
@ -116,24 +114,33 @@ func (s *ServerDNSCrypt) Shutdown(ctx context.Context) (err error) {
// startServe creates listeners and starts serving DNSCrypt.
func (s *ServerDNSCrypt) startServe(ctx context.Context) (err error) {
var errs []error
if s.network.CanUDP() {
err = s.listenUDP(ctx)
if err != nil {
return err
// Don't wrap the error, because it's informative enough as is.
errs = append(errs, err)
}
go s.startServeUDP(ctx)
}
if s.network.CanTCP() {
err = s.listenTCP(ctx)
if err != nil {
return err
// Don't wrap the error, because it's informative enough as is.
errs = append(errs, err)
}
}
go s.startServeTCP(ctx)
if len(errs) > 0 {
s.closeListeners()
return fmt.Errorf("creating listeners: %w", errors.Join(errs...))
}
go s.startServeUDP(ctx)
go s.startServeTCP(ctx)
return nil
}
@ -176,8 +183,9 @@ func (s *ServerDNSCrypt) startServeTCP(ctx context.Context) {
// shutdown marks the server as stopped and closes active listeners.
func (s *ServerDNSCrypt) shutdown() (err error) {
s.lock.Lock()
defer s.lock.Unlock()
s.mu.Lock()
defer s.mu.Unlock()
if !s.started {
return ErrServerNotStarted
}

View File

@ -14,11 +14,10 @@ import (
func TestServerDNSCrypt_integration_query(t *testing.T) {
testCases := []struct {
handler dnsserver.Handler
req *dns.Msg
name string
network dnsserver.Network
req *dns.Msg
// if nil, use DefaultTestHandler
handler dnsserver.Handler
expectedRecordsCount int
expectedRCode int
expectedTruncated bool

View File

@ -8,7 +8,6 @@ import (
"io"
"net"
"slices"
"strings"
"sync"
"time"
@ -173,7 +172,7 @@ func (s *ServerDNS) acceptTCPMsg(
StartTime: time.Now(),
}
if cs, ok := conn.(tlsConnectionStater); ok {
ri.TLSServerName = strings.ToLower(cs.ConnectionState().ServerName)
ri.TLSServerName = cs.ConnectionState().ServerName
}
reqCtx, reqCancel := s.requestContext()

View File

@ -58,8 +58,6 @@ var nextProtoDoH3 = []string{http3.NextProtoH3, http2.NextProtoTLS, "http/1.1"}
// will listen to both HTTP/2 and HTTP/3, but if you set it to NetworkTCP, the
// server will only use HTTP/2 and NetworkUDP will mean HTTP/3 only.
type ConfigHTTPS struct {
ConfigBase
// TLSConfig is the TLS configuration for HTTPS. If not set and
// [ConfigBase.Network] is set to NetworkTCP the server will listen to
// plain HTTP.
@ -69,6 +67,8 @@ type ConfigHTTPS struct {
// If it is empty, the server will return 404 for requests like that.
NonDNSHandler http.Handler
ConfigBase
// MaxStreamsPerPeer is the maximum number of concurrent streams that a peer
// is allowed to open.
MaxStreamsPerPeer int
@ -124,8 +124,8 @@ func NewServerHTTPS(conf ConfigHTTPS) (s *ServerHTTPS) {
func (s *ServerHTTPS) Start(ctx context.Context) (err error) {
defer func() { err = errors.Annotate(err, "starting doh server: %w") }()
s.lock.Lock()
defer s.lock.Unlock()
s.mu.Lock()
defer s.mu.Unlock()
if s.started {
return ErrServerAlreadyStarted
@ -241,8 +241,9 @@ func (s *ServerHTTPS) startH3Server(ctx context.Context) (err error) {
// shutdown marks the server as stopped and closes active listeners.
func (s *ServerHTTPS) shutdown(ctx context.Context) (err error) {
s.lock.Lock()
defer s.lock.Unlock()
s.mu.Lock()
defer s.mu.Unlock()
if !s.started {
return ErrServerNotStarted
}
@ -570,7 +571,7 @@ func addRequestInfo(parent context.Context, r *http.Request) (ctx context.Contex
}
if r.TLS != nil {
ri.TLSServerName = strings.ToLower(r.TLS.ServerName)
ri.TLSServerName = r.TLS.ServerName
}
if username, pass, ok := r.BasicAuth(); ok {

View File

@ -524,7 +524,7 @@ func createDoH3Client(
) (c quic.EarlyConnection, e error) {
return quic.DialAddrEarly(ctx, httpsAddr.String(), tlsCfg, cfg)
},
QuicConfig: quicConfig,
QUICConfig: quicConfig,
TLSClientConfig: tlsConfig,
}

View File

@ -7,7 +7,6 @@ import (
"fmt"
"io"
"net"
"strings"
"sync"
"time"
@ -141,8 +140,8 @@ var _ Server = (*ServerQUIC)(nil)
func (s *ServerQUIC) Start(ctx context.Context) (err error) {
defer func() { err = errors.Annotate(err, "starting doq server: %w") }()
s.lock.Lock()
defer s.lock.Unlock()
s.mu.Lock()
defer s.mu.Unlock()
if s.conf.TLSConfig == nil {
return errors.Error("tls config is required")
@ -202,8 +201,9 @@ func (s *ServerQUIC) Shutdown(ctx context.Context) (err error) {
// shutdown marks the server as stopped and closes active listeners.
func (s *ServerQUIC) shutdown() (err error) {
s.lock.Lock()
defer s.lock.Unlock()
s.mu.Lock()
defer s.mu.Unlock()
if !s.started {
return ErrServerNotStarted
}
@ -371,7 +371,7 @@ func (s *ServerQUIC) serveQUICConn(ctx context.Context, conn quic.Connection) (e
ri := &RequestInfo{
StartTime: time.Now(),
TLSServerName: strings.ToLower(conn.ConnectionState().TLS.ServerName),
TLSServerName: conn.ConnectionState().TLS.ServerName,
}
reqCtx, reqCancel := s.requestContext()

View File

@ -11,10 +11,10 @@ import (
// ConfigTLS is a struct that needs to be passed to NewServerTLS to
// initialize a new ServerTLS instance.
type ConfigTLS struct {
ConfigDNS
// TLSConfig is the TLS configuration for TLS.
TLSConfig *tls.Config
ConfigDNS
}
// ServerTLS implements a DNS-over-TLS server.
@ -43,8 +43,8 @@ func NewServerTLS(conf ConfigTLS) (s *ServerTLS) {
func (s *ServerTLS) Start(ctx context.Context) (err error) {
defer func() { err = errors.Annotate(err, "starting dot server: %w") }()
s.lock.Lock()
defer s.lock.Unlock()
s.mu.Lock()
defer s.mu.Unlock()
if s.conf.TLSConfig == nil {
return errors.Error("tls config is required")

View File

@ -48,10 +48,10 @@ func TestServerTLS_integration_msgIgnore(t *testing.T) {
t.Parallel()
testCases := []struct {
expectedError func(t *testing.T, err error)
name string
buf []byte
timeout time.Duration
expectedError func(t *testing.T, err error)
}{
{
name: "invalid_input_timeout",

View File

@ -12,6 +12,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/access"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/AdguardTeam/AdGuardDNS/internal/billstat"
"github.com/AdguardTeam/AdGuardDNS/internal/connlimiter"
"github.com/AdguardTeam/AdGuardDNS/internal/dnscheck"
@ -48,6 +49,9 @@ type Config struct {
// of DNS responses for later reuse.
Cloner *dnsmsg.Cloner
// CacheManager is the global cache manager. CacheManager must not be nil.
CacheManager agdcache.Manager
// ControlConf is the configuration of socket options.
ControlConf *netext.ControlConfig
@ -55,6 +59,10 @@ type Config struct {
// active stream-connections.
ConnLimiter *connlimiter.Limiter
// HumanIDParser is used to normalize and parse human-readable device
// identifiers.
HumanIDParser *agd.HumanIDParser
// AccessManager is used to block requests.
AccessManager access.Interface
@ -168,6 +176,7 @@ func New(c *Config) (svc *Service, err error) {
// groups.
preUps := preupstream.New(&preupstream.Config{
Cloner: c.Cloner,
CacheManager: c.CacheManager,
DB: c.DNSDB,
GeoIP: c.GeoIP,
CacheSize: c.CacheSize,
@ -549,6 +558,7 @@ func newDeviceSetter(c *Config, g *agd.ServerGroup, s *agd.Server) (ds deviceset
return devicesetter.NewDefault(&devicesetter.Config{
ProfileDB: c.ProfileDB,
HumanIDParser: c.HumanIDParser,
Server: s,
DeviceIDWildcards: g.TLS.DeviceIDWildcards,
})

View File

@ -79,6 +79,22 @@ func newTestService(
}
db := &agdtest.ProfileDB{
OnCreateAutoDevice: func(
ctx context.Context,
id agd.ProfileID,
humanID agd.HumanID,
devType agd.DeviceType,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByDedicatedIP: func(
_ context.Context,
_ netip.Addr,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByDeviceID: func(
_ context.Context,
id agd.DeviceID,
@ -87,12 +103,15 @@ func newTestService(
return prof, dev, nil
},
OnProfileByDedicatedIP: func(
OnProfileByHumanID: func(
_ context.Context,
_ netip.Addr,
_ agd.ProfileID,
_ agd.HumanIDLower,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByLinkedIP: func(
ctx context.Context,
ip netip.Addr,

View File

@ -2,6 +2,7 @@ package devicesetter
import (
"context"
"fmt"
"net/netip"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
@ -11,27 +12,35 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/optlog"
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
)
// findDevice returns a valid, non-deleted profile and device or nils. err is
// only not nil when it's not [profiledb.ErrDeviceNotFound].
// only not nil when it's not a not-found error from the profile database.
func (ds *Default) findDevice(
ctx context.Context,
laddr netip.AddrPort,
remoteIP netip.Addr,
id agd.DeviceID,
extID *extHumanID,
) (prof *agd.Profile, dev *agd.Device, err error) {
// TODO(a.garipov): Add package optslog for optimized slog logging?
optlog.Debug3("devicesetter: got dev id %q, raddr %s, and laddr %s", id, remoteIP, laddr)
optlog.Debug4(
"devicesetter: got dev id %q, ext id present %t, raddr %s, and laddr %s",
id,
extID != nil,
remoteIP,
laddr,
)
prof, dev, byWhat, err := ds.deviceFromDB(ctx, laddr, remoteIP, id)
prof, dev, byWhat, err := ds.deviceFromDB(ctx, laddr, remoteIP, id, extID)
if err != nil {
if !errors.Is(err, profiledb.ErrDeviceNotFound) {
// Don't wrap the error, because it's likely [errUnknownDedicated].
// Don't wrap the error, because it's likely [ErrUnknownDedicated].
return nil, nil, err
}
optlog.Debug1("devicesetter: profile or device not found: %s", err)
if prof == nil || dev == nil {
log.Debug("devicesetter: profile or device not found")
return nil, nil, nil
}
@ -49,37 +58,121 @@ func (ds *Default) findDevice(
// Constants for the parameter by which a device has been found.
const (
byDeviceID = "device id"
byDedicatedIP = "dedicated ip"
byDeviceID = "device id"
byHumanID = "human id"
byLinkedIP = "linked ip"
)
// deviceFromDB queries the profile DB for the profile and device by the client
// data. It also returns the description of how it has found them.
// data. It also returns the description of how it has found them. err is only
// not nil when it's not a not-found error from the profile database.
func (ds *Default) deviceFromDB(
ctx context.Context,
laddr netip.AddrPort,
remoteIP netip.Addr,
id agd.DeviceID,
extID *extHumanID,
) (prof *agd.Profile, dev *agd.Device, byWhat string, err error) {
if id != "" {
prof, dev, err = ds.db.ProfileByDeviceID(ctx, id)
if err != nil {
return nil, nil, "", err
// Don't wrap the error, as this is a hot path, and an error other
// than a not-found one is unlikely.
return nil, nil, "", removeNotFound(err)
}
return prof, dev, byDeviceID, nil
}
if extID != nil {
prof, dev, err = ds.deviceByExtID(ctx, extID)
if err == nil {
return prof, dev, byHumanID, nil
}
// Don't wrap the error, because it's informative enough as is.
return nil, nil, "", err
}
if ds.srv.Protocol == agd.ProtoDNS {
return ds.deviceByAddrs(ctx, laddr, remoteIP)
}
return nil, nil, "", profiledb.ErrDeviceNotFound
return nil, nil, "", nil
}
// removeNotFound returns nil if err is one of the not-found errors from the
// profile database.
func removeNotFound(err error) (res error) {
if isProfileDBNotFound(err) {
return nil
}
return err
}
// deviceByExtID queries the profile DB for the profile and device by the
// extended human-readable device identifier. extID must not be nil. err is
// only not nil when it's not a not-found error from the profile database.
func (ds *Default) deviceByExtID(
ctx context.Context,
extID *extHumanID,
) (prof *agd.Profile, dev *agd.Device, err error) {
profID, humanID := extID.ProfileID, extID.HumanID
prof, dev, err = ds.db.ProfileByHumanID(ctx, profID, agd.HumanIDToLower(humanID))
switch {
case err == nil:
return prof, dev, nil
case errorIsOpt(err, profiledb.ErrProfileNotFound):
// No such profile, return immediately.
return nil, nil, nil
case errorIsOpt(err, profiledb.ErrDeviceNotFound):
// Go on and try to create.
default:
// Unlikely, so wrap.
return nil, nil, fmt.Errorf("querying profile db by human id: %w", err)
}
prof, dev, err = ds.db.CreateAutoDevice(ctx, profID, humanID, extID.DeviceType)
switch {
case err == nil:
return prof, dev, nil
case errorIsOpt(err, profiledb.ErrProfileNotFound):
// A rare case where a profile has been deleted between the check and
// the creation.
return nil, nil, nil
default:
return nil, nil, fmt.Errorf("creating autodevice: %w", err)
}
}
// isProfileDBNotFound returns true if err is one of the not-found errors from
// the profile database.
func isProfileDBNotFound(err error) (ok bool) {
return errorIsOpt(err, profiledb.ErrDeviceNotFound) ||
errorIsOpt(err, profiledb.ErrProfileNotFound)
}
// errorIsOpt is an optimized version of [errors.Is] that is faster in cases
// where the error is returned directly or with one level of [fmt.Errorf].
// target must be comparable.
func errorIsOpt(err, target error) (ok bool) {
if err == target {
return true
}
unwrapped := errors.Unwrap(err)
if unwrapped == target {
return true
}
return unwrapped != nil && errors.Is(unwrapped, target)
}
// deviceByAddrs finds the profile and the device by the remote and local
// addresses depending on the data and the server settings.
// addresses depending on the data and the server settings. err is only not nil
// when it's not a not-found error from the profile database.
func (ds *Default) deviceByAddrs(
ctx context.Context,
laddr netip.AddrPort,
@ -88,6 +181,8 @@ func (ds *Default) deviceByAddrs(
if ds.srv.BindsToInterfaces() && !ds.srv.HasAddr(laddr) {
prof, dev, err = ds.deviceByLocalAddr(ctx, laddr.Addr())
if err != nil {
// Don't wrap the error, as this is a hot path, and an error about
// an unknown dedicated address might be common here.
return nil, nil, "", err
}
@ -95,30 +190,36 @@ func (ds *Default) deviceByAddrs(
}
if !ds.srv.LinkedIPEnabled {
return nil, nil, "", profiledb.ErrDeviceNotFound
return nil, nil, "", nil
}
prof, dev, err = ds.db.ProfileByLinkedIP(ctx, remoteIP)
if err != nil {
return nil, nil, "", err
// Don't wrap the error, as this is a hot path, and an error other than
// a not-found one is unlikely.
return nil, nil, "", removeNotFound(err)
}
return prof, dev, byLinkedIP, nil
}
// deviceByLocalAddr finds the profile and the device by the local address.
// deviceByLocalAddr replaces profiledb's not-found errors with
// [ErrUnknownDedicated].
func (ds *Default) deviceByLocalAddr(
ctx context.Context,
localIP netip.Addr,
) (prof *agd.Profile, dev *agd.Device, err error) {
prof, dev, err = ds.db.ProfileByDedicatedIP(ctx, localIP)
if err != nil {
if errors.Is(err, profiledb.ErrDeviceNotFound) {
if isProfileDBNotFound(err) {
optlog.Debug1("devicesetter: unknown dedicated ip for server %s; dropping", ds.srv.Name)
err = ErrUnknownDedicated
}
// Don't wrap the error, as this is a hot path, and a not-found error is
// common here.
return nil, nil, err
}

View File

@ -3,6 +3,7 @@ package devicesetter_test
import (
"context"
"net/netip"
"strings"
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
@ -10,6 +11,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/AdGuardDNS/internal/dnssvc/internal/devicesetter"
"github.com/AdguardTeam/AdGuardDNS/internal/dnssvc/internal/dnssvctest"
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
"github.com/AdguardTeam/golibs/testutil"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
@ -44,7 +46,7 @@ func TestDefault_SetDevice_plainAddrs(t *testing.T) {
wantDev: devNormal,
wantErrMsg: "",
laddr: dnssvctest.ServerAddrPort,
raddr: linkedAddrPort,
raddr: dnssvctest.LinkedAddrPort,
name: "linked_ip",
}, {
req: reqNormal,
@ -53,7 +55,7 @@ func TestDefault_SetDevice_plainAddrs(t *testing.T) {
wantDev: nil,
wantErrMsg: "",
laddr: dnssvctest.ServerAddrPort,
raddr: linkedAddrPort,
raddr: dnssvctest.LinkedAddrPort,
name: "linked_ip_not_supported",
}, {
req: reqNormal,
@ -61,7 +63,7 @@ func TestDefault_SetDevice_plainAddrs(t *testing.T) {
wantProf: profNormal,
wantDev: devNormal,
wantErrMsg: "",
laddr: dedicatedAddrPort,
laddr: dnssvctest.DedicatedAddrPort,
raddr: dnssvctest.ClientAddrPort,
name: "dedicated",
}, {
@ -79,7 +81,7 @@ func TestDefault_SetDevice_plainAddrs(t *testing.T) {
wantProf: nil,
wantDev: nil,
wantErrMsg: "",
laddr: dedicatedAddrPort,
laddr: dnssvctest.DedicatedAddrPort,
raddr: dnssvctest.ClientAddrPort,
name: "dedicated_not_supported",
}}
@ -89,18 +91,38 @@ func TestDefault_SetDevice_plainAddrs(t *testing.T) {
t.Parallel()
profDB := &agdtest.ProfileDB{
OnProfileByDedicatedIP: newOnProfileByDedicatedIP(dedicatedAddr),
OnCreateAutoDevice: func(
ctx context.Context,
id agd.ProfileID,
humanID agd.HumanID,
devType agd.DeviceType,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByDedicatedIP: newOnProfileByDedicatedIP(dnssvctest.DedicatedAddr),
OnProfileByDeviceID: func(
_ context.Context,
_ agd.DeviceID,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByLinkedIP: newOnProfileByLinkedIP(linkedAddr),
OnProfileByHumanID: func(
_ context.Context,
_ agd.ProfileID,
_ agd.HumanIDLower,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByLinkedIP: newOnProfileByLinkedIP(dnssvctest.LinkedAddr),
}
pf := devicesetter.NewDefault(&devicesetter.Config{
ProfileDB: profDB,
HumanIDParser: agd.NewHumanIDParser(),
Server: tc.srv,
DeviceIDWildcards: nil,
})
@ -171,13 +193,32 @@ func TestDefault_SetDevice_plainEDNS(t *testing.T) {
t.Parallel()
profDB := &agdtest.ProfileDB{
OnCreateAutoDevice: func(
ctx context.Context,
id agd.ProfileID,
humanID agd.HumanID,
devType agd.DeviceType,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByDedicatedIP: func(
_ context.Context,
_ netip.Addr,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByDeviceID: newOnProfileByDeviceID(dnssvctest.DeviceID),
OnProfileByHumanID: func(
_ context.Context,
_ agd.ProfileID,
_ agd.HumanIDLower,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByLinkedIP: func(
_ context.Context,
_ netip.Addr,
@ -188,6 +229,7 @@ func TestDefault_SetDevice_plainEDNS(t *testing.T) {
pf := devicesetter.NewDefault(&devicesetter.Config{
ProfileDB: profDB,
HumanIDParser: agd.NewHumanIDParser(),
Server: tc.srv,
DeviceIDWildcards: nil,
})
@ -209,18 +251,37 @@ func TestDefault_SetDevice_deleted(t *testing.T) {
t.Parallel()
profDB := &agdtest.ProfileDB{
OnCreateAutoDevice: func(
ctx context.Context,
id agd.ProfileID,
humanID agd.HumanID,
devType agd.DeviceType,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByDedicatedIP: func(
_ context.Context,
_ netip.Addr,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByDeviceID: func(
_ context.Context,
_ agd.DeviceID,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByHumanID: func(
_ context.Context,
_ agd.ProfileID,
_ agd.HumanIDLower,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByLinkedIP: func(
_ context.Context,
_ netip.Addr,
@ -231,10 +292,11 @@ func TestDefault_SetDevice_deleted(t *testing.T) {
pf := devicesetter.NewDefault(&devicesetter.Config{
ProfileDB: profDB,
HumanIDParser: agd.NewHumanIDParser(),
Server: srvPlainWithLinkedIP,
})
raddr := linkedAddrPort
raddr := dnssvctest.LinkedAddrPort
msgCons := agdtest.NewConstructor()
ri := &agd.RequestInfo{
Messages: msgCons,
@ -249,3 +311,71 @@ func TestDefault_SetDevice_deleted(t *testing.T) {
assert.Nil(t, ri.Device)
assert.Same(t, msgCons, ri.Messages)
}
func TestDefault_SetDevice_byHumanID(t *testing.T) {
t.Parallel()
// Use uppercase versions to make sure that the device setter recognizes the
// device-type and profile data regardless of the case.
extIDStr := "OTR-" + strings.ToUpper(dnssvctest.ProfileIDStr) + "-" + dnssvctest.HumanIDStr + "-!!!"
profDB := &agdtest.ProfileDB{
OnCreateAutoDevice: func(
ctx context.Context,
id agd.ProfileID,
humanID agd.HumanID,
devType agd.DeviceType,
) (p *agd.Profile, d *agd.Device, err error) {
return profNormal, devAuto, nil
},
OnProfileByDedicatedIP: func(
_ context.Context,
_ netip.Addr,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByDeviceID: func(
_ context.Context,
devID agd.DeviceID,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByHumanID: func(
_ context.Context,
_ agd.ProfileID,
_ agd.HumanIDLower,
) (p *agd.Profile, d *agd.Device, err error) {
return nil, nil, profiledb.ErrDeviceNotFound
},
OnProfileByLinkedIP: func(
_ context.Context,
_ netip.Addr,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
}
df := devicesetter.NewDefault(&devicesetter.Config{
ProfileDB: profDB,
HumanIDParser: agd.NewHumanIDParser(),
Server: srvDoT,
DeviceIDWildcards: []string{dnssvctest.DomainForDevices},
})
ctx := dnsserver.ContextWithRequestInfo(context.Background(), &dnsserver.RequestInfo{
TLSServerName: extIDStr + "." + dnssvctest.DomainForDevices,
})
ri := &agd.RequestInfo{
RemoteIP: dnssvctest.ClientAddr,
}
err := df.SetDevice(ctx, reqNormal, ri, dnssvctest.ServerAddrPort)
require.NoError(t, err)
assert.Equal(t, profNormal, ri.Profile)
assert.Equal(t, devAuto, ri.Device)
}

View File

@ -28,93 +28,102 @@ func supportsDeviceID(p agd.Protocol) (ok bool) {
}
}
// deviceID extracts the device ID from the given parameters. If the device ID
// is not found, it returns an empty ID and nil, as the lookup could also be
// deviceData extracts the device data from the given parameters. If the device
// data are not found, all results will be empty, as the lookup could also be
// done later by remote and local addresses.
func (ds *Default) deviceID(
func (ds *Default) deviceData(
req *dns.Msg,
srvReqInfo *dnsserver.RequestInfo,
) (id agd.DeviceID, err error) {
) (id agd.DeviceID, extID *extHumanID, err error) {
if ds.srv.Protocol.IsStdEncrypted() {
return ds.deviceIDFromSrvReqInfo(srvReqInfo)
return ds.deviceDataFromSrvReqInfo(srvReqInfo)
}
return deviceIDFromEDNS(req)
id, err = deviceIDFromEDNS(req)
return id, nil, err
}
// deviceIDFromSrvReqInfo extracts device ID from the arguments. The ID is
// extracted in the following manner:
// deviceDataFromSrvReqInfo extracts device data from the arguments. The data
// are extracted in the following manner:
//
// 1. If applicable, the ID is firstly extracted from the DoH information, such
// 1. If applicable, the data is first extracted from the DoH information, such
// as the userinfo or URL path.
//
// 2. Secondly, the TLS Server Name is inspected using ds's device ID
// wildcards.
// 2. Secondly, the TLS Server Name is inspected using the device-ID wildcards
// configured for the device setter.
//
// Any returned errors will have the underlying type of [*deviceIDError].
func (ds *Default) deviceIDFromSrvReqInfo(
func (ds *Default) deviceDataFromSrvReqInfo(
srvReqInfo *dnsserver.RequestInfo,
) (id agd.DeviceID, err error) {
) (id agd.DeviceID, extID *extHumanID, err error) {
if ds.srv.Protocol == agd.ProtoDoH {
id, err = deviceIDForDoH(srvReqInfo)
if id != "" || err != nil {
id, extID, err = ds.deviceDataForDoH(srvReqInfo)
if id != "" || extID != nil || err != nil {
// Don't wrap the error, because it's informative enough as is.
return id, err
return id, extID, err
}
}
if len(ds.wildcardDomains) == 0 {
return "", nil
return "", nil, nil
}
id, err = ds.deviceIDFromCliSrvName(srvReqInfo.TLSServerName)
id, extID, err = ds.deviceDataFromCliSrvName(srvReqInfo.TLSServerName)
if err != nil {
return "", newDeviceIDError(err, "tls server name")
return "", nil, newDeviceDataError(err, "tls server name")
}
return id, nil
return id, extID, nil
}
// deviceIDForDoH extracts a device ID from the DoH request information. The ID
// is extracted firstly from the request's userinfo, if any, and then from the
// URL path. srvReqInfo must not be nil.
// deviceDataForDoH extracts the device data from the DoH request information.
// The data are extracted first from the request's userinfo, if any, and then
// from the URL path. srvReqInfo must not be nil.
//
// Any returned errors will have the underlying type of [*deviceIDError].
func deviceIDForDoH(srvReqInfo *dnsserver.RequestInfo) (id agd.DeviceID, err error) {
// Any returned errors will have the underlying type of [*deviceDataError].
func (ds *Default) deviceDataForDoH(
srvReqInfo *dnsserver.RequestInfo,
) (id agd.DeviceID, extID *extHumanID, err error) {
if userinfo := srvReqInfo.Userinfo; userinfo != nil {
// Don't scan the userinfo for human-readable IDs, since they're not
// supported there.
id, err = agd.NewDeviceID(userinfo.Username())
if err != nil {
return "", newDeviceIDError(err, "basic auth")
return "", nil, newDeviceDataError(err, "basic auth")
}
return id, nil
return id, nil, nil
}
id, err = deviceIDFromDoHURL(srvReqInfo.URL)
id, extID, err = ds.deviceDataFromDoHURL(srvReqInfo.URL)
if err != nil {
return "", newDeviceIDError(err, "http url path")
return "", nil, newDeviceDataError(err, "http url path")
}
// In case of empty device ID, we will continue the lookup process.
return id, nil
// In case of empty device data, will continue the lookup process.
return id, extID, nil
}
// deviceIDFromDoHURL extracts the device ID from the path of the DoH request.
func deviceIDFromDoHURL(u *url.URL) (id agd.DeviceID, err error) {
// deviceDataFromDoHURL extracts the device data from the path of the DoH
// request.
func (ds *Default) deviceDataFromDoHURL(
u *url.URL,
) (id agd.DeviceID, extID *extHumanID, err error) {
parts, err := pathParts(u.Path)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return "", err
return "", nil, err
}
if len(parts) == 2 {
// Don't wrap the error, because it's informative enough as is.
return agd.NewDeviceID(parts[1])
return ds.parseDeviceData(parts[1])
}
// pathParts guarantees that if there aren't two parts, there's only one,
// and it is a valid DNS path.
return "", nil
return "", nil, nil
}
// pathParts splits and validates urlPath. If err is nil, parts has either one
@ -142,35 +151,36 @@ func pathParts(urlPath string) (parts []string, err error) {
return parts, nil
}
// deviceIDFromCliSrvName extracts and validates a device ID. cliSrvName is the
// server name as sent by the client.
func (ds *Default) deviceIDFromCliSrvName(cliSrvName string) (id agd.DeviceID, err error) {
// deviceDataFromCliSrvName extracts and validates device data. cliSrvName is
// the server name as sent by the client.
func (ds *Default) deviceDataFromCliSrvName(
cliSrvName string,
) (id agd.DeviceID, extID *extHumanID, err error) {
if cliSrvName == "" {
// No server name in ClientHello, so the request is probably made on the
// IP address.
return "", nil
return "", nil, nil
}
matchedDomain := matchDomain(cliSrvName, ds.wildcardDomains)
if matchedDomain == "" {
return "", nil
return "", nil, nil
}
optlog.Debug2("devicesetter: device id: matched %q from %q", matchedDomain, ds.wildcardDomains)
idStr := cliSrvName[:len(cliSrvName)-len(matchedDomain)-1]
id, err = agd.NewDeviceID(idStr)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return "", err
}
return id, nil
// Don't wrap the error, because it's informative enough as is.
return ds.parseDeviceData(idStr)
}
// matchDomain searches among domains for one the subdomain of which is sub. If
// there is no such domain, matchDomain returns an empty string.
func matchDomain(sub string, domains []string) (matchedDomain string) {
// TODO(a.garipov): Remove once netutil learns how to match domains in a
// case-insensitive way.
sub = strings.ToLower(sub)
for _, domain := range domains {
if netutil.IsImmediateSubdomain(sub, domain) {
return domain
@ -195,7 +205,7 @@ const DnsmasqCPEIDOption uint16 = 65074
// dig @94.140.14.49 'edns-id.example' IN A\
// +ednsopt=65074:"$( printf 'abcd1234' | od -A n -t x1 | tr -d ' ' )"
//
// Any returned errors will have the underlying type of [*deviceIDError].
// Any returned errors will have the underlying type of [*deviceDataError].
func deviceIDFromEDNS(req *dns.Msg) (id agd.DeviceID, err error) {
option := req.IsEdns0()
if option == nil {
@ -227,7 +237,7 @@ func deviceIDFromENDSOPT(opt dns.EDNS0) (id agd.DeviceID, err error) {
id, err = agd.NewDeviceID(string(o.Data))
if err != nil {
return "", newDeviceIDError(err, "edns option")
return "", newDeviceDataError(err, "edns option")
}
return id, nil

View File

@ -89,12 +89,22 @@ func TestDefault_SetDevice_DoHAuth(t *testing.T) {
t.Parallel()
profDB := &agdtest.ProfileDB{
OnCreateAutoDevice: func(
ctx context.Context,
id agd.ProfileID,
humanID agd.HumanID,
devType agd.DeviceType,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByDedicatedIP: func(
_ context.Context,
_ netip.Addr,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByDeviceID: func(
_ context.Context,
devID agd.DeviceID,
@ -105,6 +115,15 @@ func TestDefault_SetDevice_DoHAuth(t *testing.T) {
return nil, nil, profiledb.ErrDeviceNotFound
},
OnProfileByHumanID: func(
_ context.Context,
_ agd.ProfileID,
_ agd.HumanIDLower,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByLinkedIP: func(
_ context.Context,
_ netip.Addr,
@ -115,6 +134,7 @@ func TestDefault_SetDevice_DoHAuth(t *testing.T) {
df := devicesetter.NewDefault(&devicesetter.Config{
ProfileDB: profDB,
HumanIDParser: agd.NewHumanIDParser(),
Server: srvDoH,
DeviceIDWildcards: []string{},
})
@ -205,18 +225,37 @@ func TestDefault_SetDevice_DoHAuthOnly(t *testing.T) {
t.Parallel()
profDB := &agdtest.ProfileDB{
OnCreateAutoDevice: func(
ctx context.Context,
id agd.ProfileID,
humanID agd.HumanID,
devType agd.DeviceType,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByDedicatedIP: func(
_ context.Context,
_ netip.Addr,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByDeviceID: func(
_ context.Context,
devID agd.DeviceID,
) (p *agd.Profile, d *agd.Device, err error) {
return profNormal, tc.profDBDev, nil
},
OnProfileByHumanID: func(
_ context.Context,
_ agd.ProfileID,
_ agd.HumanIDLower,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByLinkedIP: func(
_ context.Context,
_ netip.Addr,
@ -227,6 +266,7 @@ func TestDefault_SetDevice_DoHAuthOnly(t *testing.T) {
df := devicesetter.NewDefault(&devicesetter.Config{
ProfileDB: profDB,
HumanIDParser: agd.NewHumanIDParser(),
Server: tc.srv,
DeviceIDWildcards: []string{dnssvctest.DeviceIDWildcard},
})
@ -311,16 +351,37 @@ func TestDefault_SetDevice_DoH(t *testing.T) {
},
wantErrMsg: `http url path device id check: bad path "/other": not a dns path`,
name: "not_dns_path",
}, {
wantProf: profNormal,
wantDev: devAuto,
reqURL: &url.URL{
Path: path.Join(dnsserver.PathDoH, dnssvctest.HumanIDPath),
},
wantErrMsg: "",
name: "human_id_path_match",
}}
profDB := &agdtest.ProfileDB{
OnCreateAutoDevice: func(
ctx context.Context,
id agd.ProfileID,
humanID agd.HumanID,
devType agd.DeviceType,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByDedicatedIP: func(
_ context.Context,
_ netip.Addr,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByDeviceID: newOnProfileByDeviceID(dnssvctest.DeviceID),
OnProfileByHumanID: newOnProfileByHumanID(dnssvctest.ProfileID, dnssvctest.HumanIDLower),
OnProfileByLinkedIP: func(
_ context.Context,
_ netip.Addr,
@ -335,6 +396,7 @@ func TestDefault_SetDevice_DoH(t *testing.T) {
df := devicesetter.NewDefault(&devicesetter.Config{
ProfileDB: profDB,
HumanIDParser: agd.NewHumanIDParser(),
Server: srvDoH,
DeviceIDWildcards: []string{},
})
@ -401,16 +463,36 @@ func TestDefault_SetDevice_stdEncrypted(t *testing.T) {
cliSrvName: "!!!.d.dns.example",
name: "bad_id",
wildcards: []string{dnssvctest.DeviceIDWildcard},
}, {
wantProf: profNormal,
wantDev: devAuto,
wantErrMsg: "",
cliSrvName: dnssvctest.HumanIDSrvName,
name: "human_id_match",
wildcards: []string{dnssvctest.DeviceIDWildcard},
}}
profDB := &agdtest.ProfileDB{
OnCreateAutoDevice: func(
ctx context.Context,
id agd.ProfileID,
humanID agd.HumanID,
devType agd.DeviceType,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByDedicatedIP: func(
_ context.Context,
_ netip.Addr,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByDeviceID: newOnProfileByDeviceID(dnssvctest.DeviceID),
OnProfileByHumanID: newOnProfileByHumanID(dnssvctest.ProfileID, dnssvctest.HumanIDLower),
OnProfileByLinkedIP: func(
_ context.Context,
_ netip.Addr,
@ -446,6 +528,7 @@ func TestDefault_SetDevice_stdEncrypted(t *testing.T) {
df := devicesetter.NewDefault(&devicesetter.Config{
ProfileDB: profDB,
HumanIDParser: agd.NewHumanIDParser(),
Server: sd.srv,
DeviceIDWildcards: tc.wildcards,
})

View File

@ -44,10 +44,10 @@ var _ Interface = Empty{}
// SetDevice implements the [Interface] interface for Empty. It does nothing
// and returns nil.
func (Empty) SetDevice(
ctx context.Context,
req *dns.Msg,
ri *agd.RequestInfo,
laddr netip.AddrPort,
_ context.Context,
_ *dns.Msg,
_ *agd.RequestInfo,
_ netip.AddrPort,
) (err error) {
return nil
}
@ -57,6 +57,10 @@ type Config struct {
// ProfileDB is used to find the profiles. It must not be nil.
ProfileDB profiledb.Interface
// HumanIDParser is used to normalize and parse human-readable device
// identifiers.
HumanIDParser *agd.HumanIDParser
// Server contains the data of the server for which the profiles are found.
// It must not be nil.
Server *agd.Server
@ -69,6 +73,7 @@ type Config struct {
// Default is the default profile setter.
type Default struct {
db profiledb.Interface
humanIDParser *agd.HumanIDParser
srv *agd.Server
wildcardDomains []string
}
@ -83,6 +88,7 @@ func NewDefault(c *Config) (ds *Default) {
return &Default{
db: c.ProfileDB,
humanIDParser: c.HumanIDParser,
srv: c.Server,
wildcardDomains: wildcardDomains,
}
@ -104,13 +110,13 @@ func (ds *Default) SetDevice(
}
srvReqInfo := dnsserver.MustRequestInfoFromContext(ctx)
id, err := ds.deviceID(req, srvReqInfo)
id, extID, err := ds.deviceData(req, srvReqInfo)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
prof, dev, err := ds.findDevice(ctx, laddr, ri.RemoteIP, id)
prof, dev, err := ds.findDevice(ctx, laddr, ri.RemoteIP, id, extID)
if err != nil {
// Likely [errUnknownDedicated].
return fmt.Errorf("setting profile: %w", err)

View File

@ -107,17 +107,6 @@ var (
}
)
// Common addresses for tests.
//
// TODO(a.garipov): Move more common variables and constants to dnssvctest.
var (
linkedAddr = netip.MustParseAddr("192.0.2.1")
dedicatedAddr = netip.MustParseAddr("192.0.2.2")
linkedAddrPort = netip.AddrPortFrom(linkedAddr, 12345)
dedicatedAddrPort = netip.AddrPortFrom(dedicatedAddr, 53)
)
// Common profiles and devices for tests.
var (
profNormal = &agd.Profile{
@ -139,7 +128,15 @@ var (
Enabled: false,
},
ID: dnssvctest.DeviceID,
LinkedIP: linkedAddr,
LinkedIP: dnssvctest.LinkedAddr,
}
devAuto = &agd.Device{
Auth: &agd.AuthSettings{
Enabled: false,
},
ID: dnssvctest.DeviceID,
HumanIDLower: dnssvctest.HumanIDLower,
}
)
@ -189,6 +186,32 @@ func newOnProfileByDeviceID(
}
}
// newOnProfileByHumanID returns a function with the type of
// [agdtest.ProfileDB.OnProfileByHumanID] that returns p and d only when id and
// humanID are equal to the given one.
func newOnProfileByHumanID(
wantProfID agd.ProfileID,
wantHumanID agd.HumanIDLower,
) (
f func(
_ context.Context,
id agd.ProfileID,
humanID agd.HumanIDLower,
) (p *agd.Profile, d *agd.Device, err error),
) {
return func(
_ context.Context,
id agd.ProfileID,
humanID agd.HumanIDLower,
) (p *agd.Profile, d *agd.Device, err error) {
if id == wantProfID && humanID == wantHumanID {
return profNormal, devAuto, nil
}
return nil, nil, profiledb.ErrDeviceNotFound
}
}
// newOnProfileByLinkedIP returns a function with the type of
// [agdtest.ProfileDB.OnProfileByLinkedIP] that returns p and d only when
// remoteIP is equal to the given one.
@ -208,6 +231,7 @@ func TestDefault_SetDevice_dnscrypt(t *testing.T) {
t.Parallel()
df := devicesetter.NewDefault(&devicesetter.Config{
HumanIDParser: agd.NewHumanIDParser(),
Server: &agd.Server{
Protocol: agd.ProtoDNSCrypt,
},

View File

@ -14,38 +14,38 @@ const (
ErrUnknownDedicated errors.Error = "unknown dedicated ip"
)
// deviceIDError is an error about bad device ID or other issues found during
// device ID checking.
type deviceIDError struct {
// deviceDataError is an error about bad device data or other issues found
// during device data checking.
type deviceDataError struct {
err error
typ string
}
// type check
var _ error = (*deviceIDError)(nil)
var _ error = (*deviceDataError)(nil)
// newDeviceIDError is a helper constructor for device-ID errors.
func newDeviceIDError(orig error, typ string) (err error) {
return &deviceIDError{
// newDeviceDataError is a helper constructor for device-data errors.
func newDeviceDataError(orig error, typ string) (err error) {
return &deviceDataError{
err: orig,
typ: typ,
}
}
// Error implements the error interface for *deviceIDError.
func (err *deviceIDError) Error() (msg string) {
// Error implements the error interface for *deviceDataError.
func (err *deviceDataError) Error() (msg string) {
return fmt.Sprintf("%s device id check: %s", err.typ, err.err)
}
// type check
var _ errors.Wrapper = (*deviceIDError)(nil)
var _ errors.Wrapper = (*deviceDataError)(nil)
// Unwrap implements the [errors.Wrapper] interface for *deviceIDError.
func (err *deviceIDError) Unwrap() (unwrapped error) { return err.err }
// Unwrap implements the [errors.Wrapper] interface for *deviceDataError.
func (err *deviceDataError) Unwrap() (unwrapped error) { return err.err }
// type check
var _ errcoll.SentryReportableError = (*deviceIDError)(nil)
var _ errcoll.SentryReportableError = (*deviceDataError)(nil)
// IsSentryReportable implements the [errcoll.SentryReportableError] interface
// for *deviceIDError.
func (*deviceIDError) IsSentryReportable() (ok bool) { return false }
// for *deviceDataError.
func (*deviceDataError) IsSentryReportable() (ok bool) { return false }

View File

@ -0,0 +1,86 @@
package devicesetter
import (
"strings"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/golibs/errors"
)
// extHumanID contains the data that can be parsed from an extended
// human-readable device identifier.
//
// TODO(a.garipov): Optimize its allocation and freeing.
type extHumanID struct {
HumanID agd.HumanID
ProfileID agd.ProfileID
DeviceType agd.DeviceType
}
// parseDeviceData returns either the device ID or the extended human-readable
// ID data depending on what it can parse from the given string.
//
// TODO(a.garipov): Optimize error handling etc. based on profiles.
func (ds *Default) parseDeviceData(s string) (id agd.DeviceID, extID *extHumanID, err error) {
if isLikelyExtHumanID(s) {
extID, err = ds.parseExtHumanID(s)
// Don't wrap the error, because it's informative enough as is.
return "", extID, err
}
// TODO(a.garipov): Remove once the profile database learns how to match
// IDs in a case-insensitive way.
s = strings.ToLower(s)
id, err = agd.NewDeviceID(s)
// Don't wrap the error, because it's informative enough as is.
return id, nil, err
}
// isLikelyExtHumanID returns true if s likely contains extended human-readable
// device-ID information.
func isLikelyExtHumanID(s string) (ok bool) {
return strings.Count(s, "-") >= 2
}
// parseExtHumanID parses the data about a device that is identified by a device
// type, a profile ID, and a human-readable device ID.
func (ds *Default) parseExtHumanID(s string) (extID *extHumanID, err error) {
defer func() { err = errors.Annotate(err, "parsing %q: %w", s) }()
parts := strings.SplitN(s, "-", 3)
if len(parts) != 3 {
// Technically shouldn't happen, as this function should only be called
// when isLikelyExtHumanID(s) is true.
return nil, errors.Error("not a valid ext human id")
}
// TODO(a.garipov): Use normalization.
dt, err := agd.DeviceTypeFromDNS(parts[0])
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
// TODO(a.garipov): Remove once the profile database learns how to match
// IDs in a case-insensitive way.
profIDStr := strings.ToLower(parts[1])
profID, err := agd.NewProfileID(profIDStr)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
humanID, err := ds.humanIDParser.ParseNormalized(parts[2])
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
return &extHumanID{
HumanID: humanID,
ProfileID: profID,
DeviceType: dt,
}, nil
}

View File

@ -0,0 +1,100 @@
package devicesetter_test
import (
"context"
"net/netip"
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/AdGuardDNS/internal/dnssvc/internal/devicesetter"
"github.com/AdguardTeam/AdGuardDNS/internal/dnssvc/internal/dnssvctest"
"github.com/AdguardTeam/golibs/testutil"
)
func TestDefault_SetDevice_humanID(t *testing.T) {
testCases := []struct {
name string
in string
wantErrMsg string
}{{
name: "bad_type",
in: "!!!-abcd1234-My-Device-X--10",
wantErrMsg: `tls server name device id check: parsing "!!!-abcd1234-My-Device-X--10": ` +
`bad device type "!!!": unknown device type`,
}, {
name: "bad_profile_id",
in: "otr-\x00-My-Device-X--10",
wantErrMsg: `tls server name device id check: parsing "otr-\x00-My-Device-X--10": ` +
`bad profile id: bad char '\x00' at index 0`,
}, {
name: "bad_human_id",
in: "otr-abcd1234-!!!",
wantErrMsg: `tls server name device id check: parsing "otr-abcd1234-!!!": ` +
`bad non-normalized human id "!!!": cannot normalize`,
}}
profDB := &agdtest.ProfileDB{
OnCreateAutoDevice: func(
ctx context.Context,
id agd.ProfileID,
humanID agd.HumanID,
devType agd.DeviceType,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByDedicatedIP: func(
_ context.Context,
_ netip.Addr,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByDeviceID: func(
_ context.Context,
devID agd.DeviceID,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByHumanID: func(
_ context.Context,
_ agd.ProfileID,
_ agd.HumanIDLower,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
OnProfileByLinkedIP: func(
_ context.Context,
_ netip.Addr,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
df := devicesetter.NewDefault(&devicesetter.Config{
ProfileDB: profDB,
HumanIDParser: agd.NewHumanIDParser(),
Server: srvDoT,
DeviceIDWildcards: []string{dnssvctest.DomainForDevices},
})
ctx := dnsserver.ContextWithRequestInfo(context.Background(), &dnsserver.RequestInfo{
TLSServerName: tc.in + "." + dnssvctest.DomainForDevices,
})
ri := &agd.RequestInfo{
RemoteIP: dnssvctest.ClientAddr,
}
err := df.SetDevice(ctx, reqNormal, ri, dnssvctest.ServerAddrPort)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
})
}
}

View File

@ -5,6 +5,7 @@ package dnssvctest
import (
"crypto/tls"
"net"
"net/netip"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
@ -14,18 +15,19 @@ import (
// Timeout is the common timeout for tests.
const Timeout time.Duration = 1 * time.Second
// String representations of the common IDs for tests.
// Common IDs for tests and their string representations.
const (
DeviceIDStr = "dev1234"
ProfileIDStr = "prof1234"
HumanIDStr = "My-Device-X--10"
HumanIDLowerStr = "my-device-x--10"
DeviceID agd.DeviceID = DeviceIDStr
ProfileID agd.ProfileID = ProfileIDStr
HumanID agd.HumanID = HumanIDStr
HumanIDLower agd.HumanIDLower = HumanIDLowerStr
)
// DeviceID is the common device ID for tests.
const DeviceID agd.DeviceID = DeviceIDStr
// ProfileID is the common profile ID for tests.
const ProfileID agd.ProfileID = ProfileIDStr
// String representations for the common filtering-rule list ID for tests.
const (
FilterListID1Str = "flt_1"
@ -66,28 +68,62 @@ const (
// [agd.DeviceID] or use [DeviceIDSrvName].
DeviceIDWildcard = "*." + DomainForDevices
// DeviceIDSrvName is the common client server-name for tests.
// DeviceIDSrvName is the common client server-name with a device ID for
// tests.
DeviceIDSrvName = DeviceIDStr + "." + DomainForDevices
// HumanIDPath is the common client URL path with human-readable device-data
// for tests.
HumanIDPath = "otr-" + ProfileIDStr + "-" + HumanIDStr
// HumanIDSrvName is the common client server-name with human-readable
// device-data for tests.
HumanIDSrvName = HumanIDPath + "." + DomainForDevices
)
// Use a constant block with iota to keep track of the unique final bytes of IP
// addresses more easily.
const (
ipByteZero = iota
ipByteClient
ipByteServer
ipByteDomain
ipByteLinked
ipByteDedicated
)
// Common addresses for tests.
var (
ClientIP = net.IP{1, 2, 3, 4}
RemoteAddr = &net.TCPAddr{
ClientIP = net.IP{192, 0, 2, ipByteClient}
ClientTCPAddr = &net.TCPAddr{
IP: ClientIP,
Port: 12345,
}
ClientAddrPort = RemoteAddr.AddrPort()
ClientAddrPort = ClientTCPAddr.AddrPort()
ClientAddr = ClientAddrPort.Addr()
LocalAddr = &net.TCPAddr{
IP: net.IP{5, 6, 7, 8},
ServerTCPAddr = &net.TCPAddr{
IP: net.IP{192, 0, 2, ipByteServer},
Port: 54321,
}
ServerAddrPort = LocalAddr.AddrPort()
ServerAddrPort = ServerTCPAddr.AddrPort()
ServerAddr = ServerAddrPort.Addr()
DomainAddrIPv4 = netip.AddrFrom4([4]byte{192, 0, 2, ipByteDomain})
DomainAddrIPv6 = netip.AddrFrom16([16]byte{
0x20, 0x01, 0x0d, 0xb8,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, ipByteDomain,
})
LinkedAddr = netip.AddrFrom4([4]byte{192, 0, 2, ipByteLinked})
LinkedAddrPort = netip.AddrPortFrom(LinkedAddr, 12345)
DedicatedAddr = netip.AddrFrom4([4]byte{192, 0, 2, ipByteDedicated})
DedicatedAddrPort = netip.AddrPortFrom(DedicatedAddr, 53)
)
// NewServer is a helper that returns a new *agd.Server for tests.

View File

@ -170,7 +170,7 @@ func TestMiddleware_Wrap(t *testing.T) {
TLSServerName: srvNameForProto(tc.device, resolverName, tc.srv.Protocol),
})
rw := dnsserver.NewNonWriterResponseWriter(nil, dnssvctest.RemoteAddr)
rw := dnsserver.NewNonWriterResponseWriter(nil, dnssvctest.ClientTCPAddr)
req := &dns.Msg{
Question: []dns.Question{{
Name: tc.host,
@ -278,7 +278,7 @@ func TestMiddleware_Wrap_error(t *testing.T) {
ctx := context.Background()
ctx = dnsserver.ContextWithRequestInfo(ctx, &dnsserver.RequestInfo{})
rw := dnsserver.NewNonWriterResponseWriter(nil, dnssvctest.RemoteAddr)
rw := dnsserver.NewNonWriterResponseWriter(nil, dnssvctest.ClientTCPAddr)
req := &dns.Msg{
Question: []dns.Question{{
Name: "www.example.com.",
@ -652,7 +652,7 @@ func BenchmarkMiddleware_Wrap(b *testing.B) {
return rw.WriteMsg(ctx, req, resp)
})
rw := dnsserver.NewNonWriterResponseWriter(nil, dnssvctest.RemoteAddr)
rw := dnsserver.NewNonWriterResponseWriter(nil, dnssvctest.ClientTCPAddr)
b.Run("success", func(b *testing.B) {
ds := &dnssvctest.DeviceSetter{

View File

@ -203,7 +203,7 @@ func TestMiddleware_ServeDNS_specialDomain(t *testing.T) {
ctx := context.Background()
ctx = dnsserver.ContextWithRequestInfo(ctx, &dnsserver.RequestInfo{})
rw := dnsserver.NewNonWriterResponseWriter(nil, dnssvctest.RemoteAddr)
rw := dnsserver.NewNonWriterResponseWriter(nil, dnssvctest.ClientTCPAddr)
req := &dns.Msg{
Question: []dns.Question{{
Name: dns.Fqdn(tc.host),

View File

@ -17,17 +17,19 @@ import (
// Debug header name constants.
const (
hdrNameResType = "res-type"
hdrNameRuleListID = "rule-list-id"
hdrNameRule = "rule"
hdrNameClientIP = "client-ip"
hdrNameServerIP = "server-ip"
hdrNameDeviceID = "device-id"
hdrNameProfileID = "profile-id"
hdrNameCountry = "country"
hdrNameASN = "asn"
hdrNameClientIP = "client-ip"
hdrNameCountry = "country"
hdrNameDeviceID = "device-id"
hdrNameHumanID = "human-id"
hdrNameProfileID = "profile-id"
hdrNameResType = "res-type"
hdrNameRule = "rule"
hdrNameRuleListID = "rule-list-id"
hdrNameServerIP = "server-ip"
hdrNameSubdivision = "subdivision"
hdrNameHost = "adguard-dns.com."
hdrDomain = "adguard-dns.com."
)
// writeDebugResponse writes the debug response to rw.
@ -90,12 +92,10 @@ func (mw *Middleware) appendDebugExtraFromContext(
resp *dns.Msg,
) (err error) {
ri := agd.MustRequestInfoFromContext(ctx)
if d := ri.Device; d != nil {
setQuestionName(debugReq, "", hdrNameDeviceID)
err = mw.messages.AppendDebugExtra(debugReq, resp, string(d.ID))
err = mw.appendDebugExtraFromDevice(ri.Device, debugReq, resp)
if err != nil {
return fmt.Errorf("adding %s extra: %w", hdrNameDeviceID, err)
}
// Don't wrap the error, because it's informative enough as is.
return err
}
if p := ri.Profile; p != nil {
@ -117,8 +117,36 @@ func (mw *Middleware) appendDebugExtraFromContext(
return nil
}
// appendDebugExtraFromDevice adds debug info to response got from devices data,
// if any.
func (mw *Middleware) appendDebugExtraFromDevice(
dev *agd.Device,
debugReq *dns.Msg,
resp *dns.Msg,
) (err error) {
if dev == nil {
return nil
}
setQuestionName(debugReq, "", hdrNameDeviceID)
err = mw.messages.AppendDebugExtra(debugReq, resp, string(dev.ID))
if err != nil {
return fmt.Errorf("adding %s extra: %w", hdrNameDeviceID, err)
}
if humanID := dev.HumanIDLower; humanID != "" {
setQuestionName(debugReq, "", hdrNameHumanID)
err = mw.messages.AppendDebugExtra(debugReq, resp, string(humanID))
if err != nil {
return fmt.Errorf("adding %s extra: %w", hdrNameHumanID, err)
}
}
return nil
}
// appendDebugExtraFromLocation adds debug info to response got from request
// info location. loc should not be nil.
// info location. loc must not be nil.
func (mw *Middleware) appendDebugExtraFromLocation(
loc *geoip.Location,
debugReq *dns.Msg,
@ -221,9 +249,9 @@ func (mw *Middleware) addDebugExtraFromFiltering(
func setQuestionName(req *dns.Msg, prefix, suffix string) {
var strs []string
if prefix == "" {
strs = []string{suffix, hdrNameHost}
strs = []string{suffix, hdrDomain}
} else {
strs = []string{prefix, suffix, hdrNameHost}
strs = []string{prefix, suffix, hdrDomain}
}
req.Question[0].Name = strings.Join(strs, ".")

View File

@ -226,7 +226,7 @@ func TestMiddleware_writeDebugResponse(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
rw := dnsserver.NewNonWriterResponseWriter(dnssvctest.LocalAddr, dnssvctest.RemoteAddr)
rw := dnsserver.NewNonWriterResponseWriter(dnssvctest.ServerTCPAddr, dnssvctest.ClientTCPAddr)
ctx := agd.ContextWithRequestInfo(context.Background(), tc.reqInfo)

View File

@ -244,7 +244,7 @@ func TestMiddleware_Wrap(t *testing.T) {
h := mw.Wrap(newSimpleHandler(t, tc.req, wantResp))
ctx := newContext(tc.device, tc.profile, reqHost, reqQType, reqStart)
rw := dnsserver.NewNonWriterResponseWriter(dnssvctest.LocalAddr, dnssvctest.RemoteAddr)
rw := dnsserver.NewNonWriterResponseWriter(dnssvctest.ServerTCPAddr, dnssvctest.ClientTCPAddr)
err := h.ServeDNS(ctx, rw, tc.req)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
@ -707,7 +707,7 @@ func TestMiddleware_Wrap_filtering(t *testing.T) {
h := mw.Wrap(newSimpleHandler(t, tc.wantUpsReq, tc.upsResp))
ctx := newContext(tc.device, tc.profile, reqHost, reqQType, reqStart)
rw := dnsserver.NewNonWriterResponseWriter(dnssvctest.LocalAddr, dnssvctest.RemoteAddr)
rw := dnsserver.NewNonWriterResponseWriter(dnssvctest.ServerTCPAddr, dnssvctest.ClientTCPAddr)
err := h.ServeDNS(ctx, rw, tc.req)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)

View File

@ -2,6 +2,7 @@ package mainmw
import (
"context"
"fmt"
"net"
"net/netip"
"time"
@ -11,6 +12,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/AdGuardDNS/internal/optlog"
"github.com/AdguardTeam/AdGuardDNS/internal/querylog"
"github.com/AdguardTeam/golibs/netutil"
"github.com/miekg/dns"
@ -74,7 +76,7 @@ func (mw *Middleware) recordQueryInfo(
ProfileID: prof.ID,
DeviceID: devID,
ClientCountry: reqCtry,
ResponseCountry: mw.responseCountry(ctx, fctx, ri, respIP),
ResponseCountry: mw.responseCountry(ctx, fctx, ri.Host, respIP, rcode),
DomainFQDN: q.Name,
ClientASN: reqASN,
Elapsed: uint16(time.Since(start).Milliseconds()),
@ -93,30 +95,34 @@ func (mw *Middleware) recordQueryInfo(
}
// responseCountry returns the country of the response IP address based on the
// request and filtering data.
// request and filtering data. If rcode is not a NOERROR one or there is no
// IP-address data in the response, ctry is [geoip.CountryNotApplicable].
func (mw *Middleware) responseCountry(
ctx context.Context,
fctx *filteringContext,
ri *agd.RequestInfo,
host string,
respIP netip.Addr,
rcode dnsmsg.RCode,
) (ctry geoip.Country) {
if respIP == (netip.Addr{}) || respIP.IsUnspecified() {
return geoip.CountryNone
if rcode != dns.RcodeSuccess || respIP == (netip.Addr{}) || respIP.IsUnspecified() {
return geoip.CountryNotApplicable
}
host := ri.Host
if modReq := fctx.modifiedRequest; modReq != nil {
// If the request was modified by CNAME rule, the actual result
// belongs to the hostname from that CNAME.
host = agdnet.NormalizeDomain(modReq.Question[0].Name)
}
return mw.country(ctx, host, respIP)
ctry = mw.country(ctx, host, respIP)
optlog.Debug2("mainmw: got ctry %q for resp ip %v", ctry, respIP)
return ctry
}
// responseData is a helper that returns the response code, the first IP
// address, and the DNSSEC AD flag from the DNS query response if the answer has
// the type of A or AAAA or an empty IP address otherwise.
// the type of A, AAAA, or HTTPS or an empty IP address otherwise.
//
// If resp is nil or contains invalid data, it returns 0xff (an unassigned
// RCODE), net.Addr{}, and false. It reports all errors using
@ -129,12 +135,23 @@ func (mw *Middleware) responseData(
return 0xff, netip.Addr{}, false
}
dnssec = resp.AuthenticatedData
rcode = dnsmsg.RCode(resp.Rcode)
ip, err := ipFromAnswer(resp.Answer)
if err != nil {
mw.reportf(ctx, "getting response data: %w", err)
}
return rcode, ip, dnssec
}
// ipFromAnswer returns the first IP address from the answer resource records.
func ipFromAnswer(answer []dns.RR) (ip netip.Addr, err error) {
var rrType dns.Type
var fam netutil.AddrFamily
var netIP net.IP
dnssec = resp.AuthenticatedData
rcode = dnsmsg.RCode(resp.Rcode)
for _, rr := range resp.Answer {
for _, rr := range answer {
switch v := rr.(type) {
case *dns.A:
fam = netutil.AddrFamilyIPv4
@ -142,6 +159,8 @@ func (mw *Middleware) responseData(
case *dns.AAAA:
fam = netutil.AddrFamilyIPv6
rrType, netIP = dns.Type(v.Hdr.Rrtype), v.AAAA
case *dns.HTTPS:
return ipFromHTTPSRR(v)
default:
continue
}
@ -149,15 +168,60 @@ func (mw *Middleware) responseData(
break
}
if netIP != nil {
var err error
if netIP == nil {
return netip.Addr{}, nil
}
ip, err = netutil.IPToAddr(netIP, fam)
if err != nil {
mw.reportf(ctx, "converting %s resp data: %w", rrType, err)
return netip.Addr{}, fmt.Errorf("converting %s resp data: %w", rrType, err)
}
return ip, nil
}
// ipFromHTTPSRR returns the data for the first IP hint in an HTTPS resource
// record.
func ipFromHTTPSRR(https *dns.HTTPS) (ip netip.Addr, err error) {
var fam netutil.AddrFamily
var netIP net.IP
for _, v := range https.Value {
fam, netIP = ipFromHTTPSRRKV(v)
if fam != netutil.AddrFamilyNone {
break
}
}
return rcode, ip, dnssec
if netIP == nil {
return netip.Addr{}, nil
}
ip, err = netutil.IPToAddr(netIP, fam)
if err != nil {
return netip.Addr{}, fmt.Errorf("converting https rr %s hint data: %w", fam, err)
}
return ip, nil
}
// ipFromHTTPSRRKV returns the IP-address data from the IP hint of an HTTPS
// resource record. If the hint does not contain an IP address, fam is
// [netutil.AddrFamilyNone] and netIP is nil.
func ipFromHTTPSRRKV(kv dns.SVCBKeyValue) (fam netutil.AddrFamily, netIP net.IP) {
switch kv := kv.(type) {
case *dns.SVCBIPv4Hint:
if len(kv.Hint) > 0 {
return netutil.AddrFamilyIPv4, kv.Hint[0]
}
case *dns.SVCBIPv6Hint:
if len(kv.Hint) > 0 {
return netutil.AddrFamilyIPv6, kv.Hint[0]
}
default:
// Go on.
}
return netutil.AddrFamilyNone, nil
}
// country is a wrapper around the GeoIP call that contains the handling of

View File

@ -0,0 +1,201 @@
package mainmw
import (
"context"
"net/netip"
"testing"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/billstat"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/dnsservertest"
"github.com/AdguardTeam/AdGuardDNS/internal/dnssvc/internal/dnssvctest"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/AdGuardDNS/internal/querylog"
"github.com/AdguardTeam/AdGuardDNS/internal/rulestat"
"github.com/AdguardTeam/golibs/netutil"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMiddleware_recordQueryInfo_respCtry(t *testing.T) {
t.Parallel()
const (
fqdn = dnssvctest.DomainFQDN
class = dns.ClassINET
ttlSec = 10
testCtry = geoip.CountryAD
)
var (
reqA = dnsservertest.NewReq(fqdn, dns.TypeA, class)
reqAAAA = dnsservertest.NewReq(fqdn, dns.TypeAAAA, class)
reqTXT = dnsservertest.NewReq(fqdn, dns.TypeTXT, class)
reqHTTPS = dnsservertest.NewReq(fqdn, dns.TypeHTTPS, class)
)
testCases := []struct {
req *dns.Msg
name string
wantRespCtry geoip.Country
respAns []dns.RR
respRCode dnsmsg.RCode
wantGeoIP bool
}{{
req: reqA,
name: "empty",
wantRespCtry: geoip.CountryNotApplicable,
respAns: nil,
respRCode: dns.RcodeSuccess,
wantGeoIP: false,
}, {
req: reqA,
name: "refused",
wantRespCtry: geoip.CountryNotApplicable,
respAns: nil,
respRCode: dns.RcodeRefused,
wantGeoIP: false,
}, {
req: reqA,
name: "a",
wantRespCtry: testCtry,
respAns: []dns.RR{
dnsservertest.NewA(fqdn, ttlSec, dnssvctest.DomainAddrIPv4),
},
respRCode: dns.RcodeSuccess,
wantGeoIP: true,
}, {
req: reqA,
name: "a_unspec",
wantRespCtry: geoip.CountryNotApplicable,
respAns: []dns.RR{
dnsservertest.NewA(fqdn, ttlSec, netip.IPv4Unspecified()),
},
respRCode: dns.RcodeSuccess,
wantGeoIP: false,
}, {
req: reqAAAA,
name: "aaaa",
wantRespCtry: testCtry,
respAns: []dns.RR{
dnsservertest.NewAAAA(fqdn, ttlSec, dnssvctest.DomainAddrIPv6),
},
respRCode: dns.RcodeSuccess,
wantGeoIP: true,
}, {
req: reqTXT,
name: "txt",
wantRespCtry: geoip.CountryNotApplicable,
respAns: []dns.RR{
dnsservertest.NewTXT(fqdn, ttlSec),
},
respRCode: dns.RcodeSuccess,
wantGeoIP: false,
}, {
req: reqHTTPS,
name: "https_no_ips",
wantRespCtry: geoip.CountryNotApplicable,
respAns: []dns.RR{
dnsservertest.NewHTTPS(fqdn, ttlSec, nil, nil),
},
respRCode: dns.RcodeSuccess,
wantGeoIP: false,
}, {
req: reqHTTPS,
name: "https_ipv4",
wantRespCtry: testCtry,
respAns: []dns.RR{
dnsservertest.NewHTTPS(fqdn, ttlSec, []netip.Addr{dnssvctest.DomainAddrIPv4}, nil),
},
respRCode: dns.RcodeSuccess,
wantGeoIP: true,
}, {
req: reqHTTPS,
name: "https_ipv6",
wantRespCtry: testCtry,
respAns: []dns.RR{
dnsservertest.NewHTTPS(fqdn, ttlSec, nil, []netip.Addr{dnssvctest.DomainAddrIPv6}),
},
respRCode: dns.RcodeSuccess,
wantGeoIP: true,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
loc := &geoip.Location{
Country: testCtry,
}
geoIP := &agdtest.GeoIP{
OnSubnetByLocation: func(
_ *geoip.Location,
_ netutil.AddrFamily,
) (n netip.Prefix, err error) {
panic("not implemented")
},
OnData: func(_ string, _ netip.Addr) (l *geoip.Location, err error) {
if !tc.wantGeoIP {
t.Error("unexpected call to geoip")
}
return loc, nil
},
}
queryLogCalled := false
var gotRespCtry geoip.Country
queryLog := &agdtest.QueryLog{
OnWrite: func(_ context.Context, e *querylog.Entry) (err error) {
queryLogCalled = true
require.NotNil(t, e)
gotRespCtry = e.ResponseCountry
return nil
},
}
mw := &Middleware{
billStat: billstat.EmptyRecorder{},
geoIP: geoIP,
queryLog: queryLog,
ruleStat: rulestat.Empty{},
}
ctx := dnsserver.ContextWithRequestInfo(context.Background(), &dnsserver.RequestInfo{
StartTime: time.Now(),
})
fctx := &filteringContext{
originalRequest: tc.req,
filteredResponse: dnsservertest.NewResp(
int(tc.respRCode),
tc.req,
dnsservertest.SectionAnswer(tc.respAns),
),
}
ri := &agd.RequestInfo{
Profile: &agd.Profile{
QueryLogEnabled: true,
},
Device: &agd.Device{},
QType: tc.req.Question[0].Qtype,
QClass: class,
}
mw.recordQueryInfo(ctx, fctx, ri)
require.True(t, queryLogCalled)
assert.Equal(t, gotRespCtry, tc.wantRespCtry)
})
}
}

View File

@ -122,7 +122,7 @@ func TestPreServiceMwHandler_ServeDNS(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
rw := dnsserver.NewNonWriterResponseWriter(nil, dnssvctest.RemoteAddr)
rw := dnsserver.NewNonWriterResponseWriter(nil, dnssvctest.ClientTCPAddr)
tctx := agd.ContextWithRequestInfo(ctx, tc.ri)
dnsCk := &agdtest.DNSCheck{

View File

@ -9,6 +9,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/AdguardTeam/AdGuardDNS/internal/agdnet"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsdb"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
@ -27,6 +28,7 @@ import (
// handling as well as records anonymous DNS statistics.
type Middleware struct {
cloner *dnsmsg.Cloner
cacheManager agdcache.Manager
db dnsdb.Interface
geoIP geoip.Interface
cacheMinTTL time.Duration
@ -42,6 +44,9 @@ type Config struct {
// Cloner is used to clone messages taken from cache.
Cloner *dnsmsg.Cloner
// CacheManager is the global cache manager. CacheManager must not be nil.
CacheManager agdcache.Manager
// DB is used to update anonymous statistics about DNS queries.
DB dnsdb.Interface
@ -72,6 +77,7 @@ type Config struct {
func New(c *Config) (mw *Middleware) {
return &Middleware{
cloner: c.Cloner,
cacheManager: c.CacheManager,
db: c.DB,
geoIP: c.GeoIP,
cacheMinTTL: c.CacheMinTTL,
@ -135,6 +141,7 @@ func (mw *Middleware) wrapCacheMw(next dnsserver.Handler) (wrapped dnsserver.Han
if mw.useECSCache {
cacheMw = ecscache.NewMiddleware(&ecscache.MiddlewareConfig{
Cloner: mw.cloner,
CacheManager: mw.cacheManager,
GeoIP: mw.geoIP,
Size: mw.cacheSize,
ECSSize: mw.ecsCacheSize,

View File

@ -7,6 +7,7 @@ import (
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsdb"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
@ -122,6 +123,7 @@ func TestPreUpstreamMwHandler_ServeDNS_withECSCache(t *testing.T) {
mw := preupstream.New(&preupstream.Config{
Cloner: agdtest.NewCloner(),
CacheManager: agdcache.EmptyManager{},
DB: dnsdb.Empty{},
GeoIP: geoIP,
CacheSize: 100,
@ -233,7 +235,7 @@ func TestPreUpstreamMwHandler_ServeDNS_androidMetric(t *testing.T) {
h := mw.Wrap(handler)
rw := dnsserver.NewNonWriterResponseWriter(nil, dnssvctest.RemoteAddr)
rw := dnsserver.NewNonWriterResponseWriter(nil, dnssvctest.ClientTCPAddr)
err := h.ServeDNS(ctx, rw, tc.req)
require.NoError(t, err)

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,12 @@ import (
"github.com/miekg/dns"
)
// Constants that define cache identifiers for the cache manager.
const (
CacheIDWithECS = "ecscache_with_ecs"
CacheIDNoECS = "ecscache_no_ecs"
)
// Middleware is a dnsserver.Middleware with ECS-aware caching.
type Middleware struct {
// cloner is the memory-efficient cloner of DNS messages.
@ -50,6 +56,9 @@ type MiddlewareConfig struct {
// Cloner is used to clone messages taken from cache.
Cloner *dnsmsg.Cloner
// CacheManager is the global cache manager. CacheManager must not be nil.
CacheManager agdcache.Manager
// GeoIP is the GeoIP database used to get subnets for countries. It must
// not be nil.
GeoIP geoip.Interface
@ -69,17 +78,24 @@ type MiddlewareConfig struct {
UseTTLOverride bool
}
// NewMiddleware initializes a new ECS-aware LRU caching middleware. c must not
// be nil.
// NewMiddleware initializes a new ECS-aware LRU caching middleware. It also
// adds the caches with IDs [CacheIDNoECS] and [CacheIDWithECS] to the cache
// manager. c must not be nil.
func NewMiddleware(c *MiddlewareConfig) (m *Middleware) {
cache := agdcache.NewLRU[uint64, *cacheItem](&agdcache.LRUConfig{
Size: c.Size,
})
ecsCache := agdcache.NewLRU[uint64, *cacheItem](&agdcache.LRUConfig{
Size: c.ECSSize,
})
c.CacheManager.Add(CacheIDNoECS, cache)
c.CacheManager.Add(CacheIDWithECS, ecsCache)
return &Middleware{
cloner: c.Cloner,
cache: agdcache.NewLRU[uint64, *cacheItem](&agdcache.LRUConfig{
Size: c.Size,
}),
ecsCache: agdcache.NewLRU[uint64, *cacheItem](&agdcache.LRUConfig{
Size: c.ECSSize,
}),
cache: cache,
ecsCache: ecsCache,
geoIP: c.GeoIP,
cacheReqPool: syncutil.NewPool(func() (req *cacheRequest) {
return &cacheRequest{}

View File

@ -9,6 +9,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
@ -686,6 +687,7 @@ func newWithCache(
h,
ecscache.NewMiddleware(&ecscache.MiddlewareConfig{
Cloner: agdtest.NewCloner(),
CacheManager: agdcache.EmptyManager{},
GeoIP: geoIP,
Size: 100,
ECSSize: 100,

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