Sync v2.17.0

This commit is contained in:
Andrey Meshkov
2025-11-07 13:47:40 +03:00
parent 5da2a9fd26
commit c1ba1c877a
86 changed files with 11561 additions and 4052 deletions

View File

@@ -7,6 +7,81 @@ The format is **not** based on [Keep a Changelog][kec], since the project **does
[kec]: https://keepachangelog.com/en/1.0.0/
[sem]: https://semver.org/spec/v2.0.0.html
## AGDNS-3287 / Build 1081
- Profiles file cache version has been incremented to support custom block-page data.
## AGDNS-3133 / Build 1078
- The new `tls` have been added:
```yaml
tls:
enabled: true
certificate_groups:
example-cert:
certificate: '/path/to/cert.crt'
key: '/path/to/cert.key'
```
- The property `certificates` of the object `server_groups.tls` has been replaced with the new object, `certificate_groups`. So replace this:
```yaml
server_groups:
tls:
certificates:
- certificate: '/path/to/cert.crt'
key: '/path/to/cert.key'
# …
```
with this:
```yaml
server_groups:
tls:
certificate_groups:
- name: 'example-cert'
# …
```
- The property `certificates` of the objects `web.linked_ip.bind`, `web.adult_blocking.bind`, `web.general_blocking.bind`, `web.safe_browsing.bind`, as well as of the `web.non_doh_bind` have been replaced with the new object, `certificate_groups`. So replace this:
```yaml
web:
linked_ip:
bind:
- # …
certificates:
- certificate: '/path/to/cert.crt'
key: '/path/to/cert.key'
# …
non_doh_bind:
- # …
certificates:
- certificate: './test/cert.crt'
key: './test/cert.key'
# …
```
with this:
```yaml
web:
linked_ip:
bind:
- # …
certificate_groups:
- name: 'example-cert'
# …
non_doh_bind:
- # …
certificate_groups:
- name: 'default'
# …
```
## AGDNS-3241 / Build 1067
- The environment variables `QUERYLOG_SEMAPHORE_ENABLED` and `QUERYLOG_SEMAPHORE_LIMIT` have been added.

View File

@@ -1,20 +1,18 @@
# Keep the Makefile POSIX-compliant. We currently allow hyphens in
# target names, but that may change in the future.
# Keep the Makefile POSIX-compliant. We currently allow hyphens in target
# names, but that may change in the future.
#
# See https://pubs.opengroup.org/onlinepubs/9699919799/utilities/make.html.
.POSIX:
# This comment is used to simplify checking local copies of the
# Makefile. Bump this number every time a significant change is made to
# this Makefile.
# This comment is used to simplify checking local copies of the Makefile. Bump
# this number every time a significant change is made to this Makefile.
#
# AdGuard-Project-Version: 9
# AdGuard-Project-Version: 11
# Don't name these macros "GO" etc., because GNU Make apparently makes
# them exported environment variables with the literal value of
# "${GO:-go}" and so on, which is not what we need. Use a dot in the
# name to make sure that users don't have an environment variable with
# the same name.
# Don't name these macros "GO" etc., because GNU Make apparently makes them
# exported environment variables with the literal value of "${GO:-go}" and so
# on, which is not what we need. Use a dot in the name to make sure that users
# don't have an environment variable with the same name.
#
# See https://unix.stackexchange.com/q/646255/105635.
GO.MACRO = $${GO:-go}
@@ -24,7 +22,7 @@ BRANCH = $${BRANCH:-$$(git rev-parse --abbrev-ref HEAD)}
GOAMD64 = v1
GOPROXY = https://proxy.golang.org|direct
GOTELEMETRY = off
GOTOOLCHAIN = go1.25.1
GOTOOLCHAIN = go1.25.3
RACE = 0
REVISION = $${REVISION:-$$(git rev-parse --short HEAD)}
VERSION = 0
@@ -51,8 +49,7 @@ ENV_MISC = env \
# Keep the line above blank.
# Keep this target first, so that a naked make invocation triggers a
# full build.
# Keep this target first, so that a naked make invocation triggers a full build.
.PHONY: build
build: go-deps go-build
@@ -91,7 +88,7 @@ go-os-check:
.PHONY: txt-lint
txt-lint: ; $(ENV) "$(SHELL)" ./scripts/make/txt-lint.sh
.PHONY: md-lint
.PHONY: md-lint sh-lint
md-lint: ; $(ENV_MISC) "$(SHELL)" ./scripts/make/md-lint.sh
sh-lint: ; $(ENV_MISC) "$(SHELL)" ./scripts/make/sh-lint.sh

View File

@@ -184,6 +184,15 @@ check:
- 1234::cdee
- 1234::cdef
tls:
# If true, enable TLS.
enabled: true
# The list of certificate groups.
certificate_groups:
default:
certificate: './test/cert.crt'
key: './test/cert.key'
# Web/HTTP(S) service configuration. All non-root requests to the main service
# not matching the static_content map are shown a 404 page. In special
# case of `/robots.txt` request the special response is served.
@@ -194,9 +203,8 @@ web:
bind:
- address: '127.0.0.1:9080'
- address: '127.0.0.1:9443'
certificates:
- certificate: './test/cert.crt'
key: './test/cert.key'
certificate_groups:
- name: 'default'
# Optional adult blocking web server configuration. static_content is not
# served on these addresses. The addresses should be the same as in the
# general_blocking and safe_browsing objects.
@@ -204,9 +212,8 @@ web:
bind:
- address: '127.0.0.1:9081'
- address: '127.0.0.1:9444'
certificates:
- certificate: './test/cert.crt'
key: './test/cert.key'
certificate_groups:
- name: 'default'
block_page: './test/block_page_adult.html'
# Optional general blocking web server configuration. static_content is not
# served on these addresses. The addresses should be the same as in the
@@ -215,9 +222,8 @@ web:
bind:
- address: '127.0.0.1:9082'
- address: '127.0.0.1:9445'
certificates:
- certificate: './test/cert.crt'
key: './test/cert.key'
certificate_groups:
- name: 'default'
block_page: './test/block_page_general.html'
# Optional safe browsing web server configuration. static_content is not
# served on these addresses. The addresses should be the same as in the
@@ -226,18 +232,16 @@ web:
bind:
- address: '127.0.0.1:9083'
- address: '127.0.0.1:9446'
certificates:
- certificate: './test/cert.crt'
key: './test/cert.key'
certificate_groups:
- name: 'default'
block_page: './test/block_page_sb.html'
# Listen addresses for the web service in addition to the ones in the
# DNS-over-HTTPS handlers.
non_doh_bind:
- address: '127.0.0.1:9084'
- address: '127.0.0.1:9447'
certificates:
- certificate: './test/cert.crt'
key: './test/cert.key'
certificate_groups:
- name: 'default'
# Static content map. Not served on the linked_ip, safe_browsing and adult_blocking
# servers. Paths must not cross the ones used by the DNS-over-HTTPS server.
static_content:
@@ -410,9 +414,8 @@ server_groups:
ipv6_hints:
- '::1'
tls:
certificates:
- certificate: './test/cert.crt'
key: './test/cert.key'
certificate_groups:
- name: 'default'
session_keys:
- './test/tls_key_1'
- './test/tls_key_2'

View File

@@ -19,6 +19,7 @@ Besides the [environment][env], AdGuard DNS uses a [YAML][yaml] file to store co
- [Query log](#query_log)
- [GeoIP database](#geoip)
- [DNS-server check](#check)
- [TLS](#tls)
- [Web API](#web)
- [Safe browsing](#safe_browsing)
- [Adult-content blocking](#adult_blocking)
@@ -403,6 +404,25 @@ The `check` object has the following properties:
[http-dnscheck]: http.md#dnscheck-test
## <a href="#tls" id="tls" name="tls">TLS</a>
The `tls` object has the following properties:
- <a href="#tls-enabled" id="tls-enabled" name="tls-enabled">`enabled`</a>: If true, the TLS certificates are used. Otherwise, any object supporting TLS should be configured to not use it.
**Example:** `true`.
- <a href="#tls-certificate_groups" id="tls-certificate_groups" name="tls-certificate_groups">`certificate_groups`</a>: The object maps certificate names to the certificate's file path and its private key's file path. The name of each certificate should be unique and should contain no more than 64 characters, which should be `a-z`, `A-Z`, `0-9`, `-`, or `_`.
**Property example:**
```yaml
'certificate_groups':
'example-cert':
'certificate': '/path/to/cert.crt'
'key': '/path/to/cert.key'
```
## <a href="#web" id="web" name="web">Web API</a>
The optional `web` object has the following properties:
@@ -420,9 +440,8 @@ The optional `web` object has the following properties:
'bind':
- 'address': '127.0.0.1:80'
- 'address': '127.0.0.1:443'
'certificates':
- 'certificate': './test/cert.crt'
'key': './test/cert.key'
'certificate_groups':
- 'name': 'example-cert'
```
- <a href="#web-safe_browsing" id="web-safe_browsing" name="web-safe_browsing">`safe_browsing`</a>: The optional safe browsing block-page web server configurations. Every request is responded with the content from the file to which the `block_page` property points.
@@ -440,9 +459,8 @@ The optional `web` object has the following properties:
'bind':
- 'address': '127.0.0.1:80'
- 'address': '127.0.0.1:443'
'certificates':
- 'certificate': './test/cert.crt'
'key': './test/cert.key'
'certificate_groups':
- 'name': 'example-cert'
'block_page': '/var/www/block_page.html'
```
@@ -450,7 +468,7 @@ The optional `web` object has the following properties:
- <a href="#web-general_blocking" id="web-general_blocking" name="web-general_blocking">`general_blocking`</a>: The optional general block-page web server configuration. The format of the values is the same as in the [`safe_browsing`](#web-safe_browsing) object above.
- <a href="#web-non_doh_bind" id="web-non_doh_bind" name="web-non_doh_bind">`non_doh_bind`</a>: The optional listen addresses and optional TLS configuration for the web service in addition to the ones in the DNS-over-HTTPS handlers. The `certificates` array has the same format as the one in a server group's [TLS settings](#server_groups-*-tls). In the special case of `GET /robots.txt` requests, a special response is served; this response could be overwritten with static content.
- <a href="#web-non_doh_bind" id="web-non_doh_bind" name="web-non_doh_bind">`non_doh_bind`</a>: The optional listen addresses and optional TLS configuration for the web service in addition to the ones in the DNS-over-HTTPS handlers. The `certificate_groups` array has the same format as [`certificate_groups`](#sg-*-tls-certificate_groups) in a server group's TLS settings. In the special case of `GET /robots.txt` requests, a special response is served; this response could be overwritten with static content.
**Property example:**
@@ -458,9 +476,8 @@ The optional `web` object has the following properties:
'non_doh_bind':
- 'address': '127.0.0.1:80'
- 'address': '127.0.0.1:443'
'certificates':
- 'certificate': './test/cert.crt'
'key': './test/cert.key'
'certificate_groups':
- 'name': 'example-cert'
```
- <a href="#web-static_content" id="web-static_content" name="web-static_content">`static_content`</a>: The optional inline static content mapping. Not served on the `linked_ip`, `safe_browsing` and `adult_blocking` servers. Paths must not duplicate the ones used by the DNS-over-HTTPS server.
@@ -757,14 +774,14 @@ The DDR configuration object. Many of these data duplicate data from objects in
### <a href="#server_groups-*-tls" id="server_groups-*-tls" name="server_groups-*-tls">TLS</a>
- <a href="#sg-*-tls-certificates" id="sg-*-tls-certificates" name="sg-*-tls-certificates">`certificates`</a>: The array of objects with paths to the certificate and the private key for this server group.
- <a href="#sg-*-tls-certificate_groups" id="sg-*-tls-certificate_groups" name="sg-*-tls-certificate_groups">`certificate_groups`</a>: The array of objects with names of the certificates to use for servers with standard encrypted protocols within the group. See [`certificate_groups`](#tls-certificate_groups).
<a href="#sg-*-tls-certificates" id="sg-*-tls-certificates" name="sg-*-tls-certificates"><!-- Name of this field has changed, so keep the anchor to avoid breaking old links. --></a>
**Property example:**
```yaml
'certificates':
- 'certificate': '/etc/dns/cert.crt'
'key': '/etc/dns/cert.key'
'certificate_groups':
- 'name': 'example-cert'
```
- <a href="#sg-*-tls-session_keys" id="sg-*-tls-session_keys" name="sg-*-tls-session_keys">`session_keys`</a>: The array of file paths from which the each server's TLS session keys are updated. Session ticket key files must contain at least 32 bytes.

75
go.mod
View File

@@ -1,48 +1,46 @@
module github.com/AdguardTeam/AdGuardDNS
go 1.25.1
go 1.25.3
require (
// NOTE: Do not change the pseudoversion.
github.com/AdguardTeam/AdGuardDNS/internal/dnsserver v0.0.0-00010101000000-000000000000
github.com/AdguardTeam/golibs v0.34.1
github.com/AdguardTeam/urlfilter v0.22.0
github.com/AdguardTeam/golibs v0.35.2
github.com/AdguardTeam/urlfilter v0.22.1
github.com/ameshkov/dnscrypt/v2 v2.4.0
github.com/axiomhq/hyperloglog v0.2.5
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.35.1
github.com/gomodule/redigo v1.9.2
github.com/getsentry/sentry-go v0.36.0
github.com/gomodule/redigo v1.9.3
github.com/google/go-cmp v0.7.0
github.com/google/renameio/v2 v2.0.0
github.com/miekg/dns v1.1.68
github.com/oschwald/maxminddb-golang v1.13.1
github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible
github.com/prometheus/client_golang v1.23.1
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.66.0
github.com/quic-go/quic-go v0.54.0
github.com/prometheus/common v0.67.1
github.com/quic-go/quic-go v0.55.0
github.com/stretchr/testify v1.11.1
github.com/viktordanov/golang-lru v0.5.6
golang.org/x/crypto v0.42.0
golang.org/x/net v0.44.0
golang.org/x/sys v0.36.0
go.yaml.in/yaml/v4 v4.0.0-rc.2
golang.org/x/crypto v0.43.0
golang.org/x/net v0.46.0
golang.org/x/sys v0.37.0
golang.org/x/time v0.13.0
google.golang.org/grpc v1.75.1
google.golang.org/protobuf v1.36.9
gopkg.in/yaml.v2 v2.4.0
google.golang.org/grpc v1.76.0
google.golang.org/protobuf v1.36.10
)
require (
cloud.google.com/go v0.122.0 // indirect
cloud.google.com/go/ai v0.12.1 // indirect
cloud.google.com/go/auth v0.16.5 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.8.0 // indirect
cloud.google.com/go/longrunning v0.6.7 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/ameshkov/dnsstamps v1.0.3 // indirect
github.com/anthropics/anthropic-sdk-go v1.14.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/caarlos0/env/v11 v11.3.1 // indirect
@@ -55,50 +53,47 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golangci/misspell v0.7.0 // indirect
github.com/google/generative-ai-go v0.20.1 // indirect
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/gookit/color v1.6.0 // indirect
github.com/gordonklaus/ineffassign v0.2.0 // indirect
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/jstemmer/go-junit-report/v2 v2.1.0 // indirect
github.com/kamstrup/intmap v0.5.1 // indirect
github.com/kisielk/errcheck v1.9.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.25.1 // indirect
github.com/panjf2000/ants/v2 v2.11.3 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/securego/gosec/v2 v2.22.8 // indirect
github.com/securego/gosec/v2 v2.22.10 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/uudashr/gocognit v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opentelemetry.io/auto/sdk v1.2.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
golang.org/x/exp/typeparams v0.0.0-20250911091902-df9299821621 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/oauth2 v0.31.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b // indirect
golang.org/x/exp/typeparams v0.0.0-20251017212417-90e834f514db // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect
golang.org/x/term v0.35.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.37.0 // indirect
golang.org/x/telemetry v0.0.0-20251014153721-24f779f6aaef // indirect
golang.org/x/term v0.36.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.org/x/vuln v1.1.4 // indirect
google.golang.org/api v0.249.0 // indirect
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect
google.golang.org/genai v1.31.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251020155222-88f65dc88635 // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
honnef.co/go/tools v0.6.1 // indirect

157
go.sum
View File

@@ -1,27 +1,23 @@
cloud.google.com/go v0.122.0 h1:0JTLGrcSIs3HIGsgVPvTx3cfyFSP/k9CI8vLPHTd6Wc=
cloud.google.com/go v0.122.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/ai v0.12.1 h1:m1n/VjUuHS+pEO/2R4/VbuuEIkgk0w67fDQvFaMngM0=
cloud.google.com/go/ai v0.12.1/go.mod h1:5vIPNe1ZQsVZqCliXIPL4QnhObQQY4d9hAGHdVc4iw4=
cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA=
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
github.com/AdguardTeam/golibs v0.34.1 h1:RyBpZiXnJqlO3T+xjWldlxsEZDelmaFfKvXiJHDZZFQ=
github.com/AdguardTeam/golibs v0.34.1/go.mod h1:K4C2EbfSEM1zY5YXoti9SfbTAHN/kIX97LpDtCwORrM=
github.com/AdguardTeam/urlfilter v0.22.0 h1:ybOz3FywbpGDGC+8gFFkM1LMUOSosY7CWSBXIYXnG1U=
github.com/AdguardTeam/urlfilter v0.22.0/go.mod h1:q0lWKapXlYTA4TUWUM1YDwU6Q0PKvQEokztcvRV2OW0=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
github.com/AdguardTeam/golibs v0.35.2 h1:GVlx/CiCz5ZXQmyvFrE3JyeGsgubE8f4rJvRshYJVVs=
github.com/AdguardTeam/golibs v0.35.2/go.mod h1:p/l6tG7QCv+Hi5yVpv1oZInoatRGOWoyD1m+Ume+ZNY=
github.com/AdguardTeam/urlfilter v0.22.1 h1:nC2x0MSNwmTsXMTPfs1Gv6GZXKmK7prlzgjCdnE4fR8=
github.com/AdguardTeam/urlfilter v0.22.1/go.mod h1:+wUx7GApNWvFPALjNd5fTLix4PFvQF5Gprx6JDYwxfE=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/ameshkov/dnscrypt/v2 v2.4.0 h1:if6ZG2cuQmcP2TwSY+D0+8+xbPfoatufGlOQTMNkI9o=
github.com/ameshkov/dnscrypt/v2 v2.4.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI=
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/anthropics/anthropic-sdk-go v1.14.0 h1:EzNQvnZlaDHe2UPkoUySDz3ixRgNbwKdH8KtFpv7pi4=
github.com/anthropics/anthropic-sdk-go v1.14.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
github.com/axiomhq/hyperloglog v0.2.5 h1:Hefy3i8nAs8zAI/tDp+wE7N+Ltr8JnwiW3875pvl0N8=
github.com/axiomhq/hyperloglog v0.2.5/go.mod h1:DLUK9yIzpU5B6YFLjxTIcbHu1g4Y1WQb1m5RH3radaM=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
@@ -48,8 +44,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
github.com/getsentry/sentry-go v0.35.1 h1:iopow6UVLE2aXu46xKVIs8Z9D/YZkJrHkgozrxa+tOQ=
github.com/getsentry/sentry-go v0.35.1/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE=
github.com/getsentry/sentry-go v0.36.0 h1:UkCk0zV28PiGf+2YIONSSYiYhxwlERE5Li3JPpZqEns=
github.com/getsentry/sentry-go v0.36.0/go.mod h1:p5Im24mJBeruET8Q4bbcMfCQ+F+Iadc4L48tB1apo2c=
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.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -65,10 +61,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golangci/misspell v0.7.0 h1:4GOHr/T1lTW0hhR4tgaaV1WS/lJ+ncvYCoFKmqJsj0c=
github.com/golangci/misspell v0.7.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg=
github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s=
github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
github.com/google/generative-ai-go v0.20.1 h1:6dEIujpgN2V0PgLhr6c/M1ynRdc7ARtiIDPFzj45uNQ=
github.com/google/generative-ai-go v0.20.1/go.mod h1:TjOnZJmZKzarWbjUJgy+r3Ee7HGBRVLhOIgupnwR4Bg=
github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU=
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -94,8 +88,8 @@ github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=
github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs=
github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jstemmer/go-junit-report/v2 v2.1.0 h1:X3+hPYlSczH9IMIpSC9CQSZA0L+BipYafciZUWHEmsc=
github.com/jstemmer/go-junit-report/v2 v2.1.0/go.mod h1:mgHVr7VUo5Tn8OLVr1cKnLuEy0M92wdRntM99h7RkgQ=
github.com/kamstrup/intmap v0.5.1 h1:ENGAowczZA+PJPYYlreoqJvWgQVtAmX1l899WfYFVK0=
@@ -114,10 +108,10 @@ github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.25.1 h1:Fwp6crTREKM+oA6Cz4MsO8RhKQzs2/gOIVOUscMAfZY=
github.com/onsi/ginkgo/v2 v2.25.1/go.mod h1:ppTWQ1dh9KM/F1XgpeRqelR+zHVwV81DGRSDnFxK7Sk=
github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY=
github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o=
github.com/onsi/ginkgo/v2 v2.26.0 h1:1J4Wut1IlYZNEAWIV3ALrT9NfiaGW2cDCJQSFQMs/gE=
github.com/onsi/ginkgo/v2 v2.26.0/go.mod h1:qhEywmzWTBUY88kfO0BRvX4py7scov9yR+Az2oavUzw=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
@@ -130,38 +124,47 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.1 h1:w6gXMLQGgd0jXXlote9lRHMe0nG01EbnJT+C0EJru2Y=
github.com/prometheus/client_golang v1.23.1/go.mod h1:br8j//v2eg2K5Vvna5klK8Ku5pcU5r4ll73v6ik5dIQ=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.0 h1:K/rJPHrG3+AoQs50r2+0t7zMnMzek2Vbv31OFVsMeVY=
github.com/prometheus/common v0.66.0/go.mod h1:Ux6NtV1B4LatamKE63tJBntoxD++xmtI/lK0VtEplN4=
github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI=
github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/securego/gosec/v2 v2.22.8 h1:3NMpmfXO8wAVFZPNsd3EscOTa32Jyo6FLLlW53bexMI=
github.com/securego/gosec/v2 v2.22.8/go.mod h1:ZAw8K2ikuH9qDlfdV87JmNghnVfKB1XC7+TVzk6Utto=
github.com/securego/gosec/v2 v2.22.10 h1:ntbBqdWXnu46DUOXn+R2SvPo3PiJCDugTCgTW2g4tQg=
github.com/securego/gosec/v2 v2.22.10/go.mod h1:9UNjK3tLpv/w2b0+7r82byV43wCJDNtEDQMeS+H/g2w=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYRA=
github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU=
github.com/viktordanov/golang-lru v0.5.6 h1:wEyMgglEo5IZ7Maxeh8E2jCPskpQnt6FJAYl1/TJ6ac=
github.com/viktordanov/golang-lru v0.5.6/go.mod h1:R91CBCcMhp6TYUy8NHP/PJ09sk5BTDwb8KMO21CELes=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.opentelemetry.io/auto/sdk v1.2.0 h1:YpRtUFjvhSymycLS2T81lT6IGhcUP+LUPtv0iv1N8bM=
go.opentelemetry.io/auto/sdk v1.2.0/go.mod h1:1deq2zL7rwjwC8mR7XgY2N+tlIl6pjmEUoLDENMEzwk=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
@@ -180,32 +183,36 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/exp/typeparams v0.0.0-20250911091902-df9299821621 h1:Yl4H5w2RV7L/dvSHp2GerziT5K2CORgFINPaMFxWGWw=
golang.org/x/exp/typeparams v0.0.0-20250911091902-df9299821621/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b h1:18qgiDvlvH7kk8Ioa8Ov+K6xCi0GMvmGfGW0sgd/SYA=
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/exp/typeparams v0.0.0-20251017212417-90e834f514db h1:zIIKf9uYLvsQHFOJ0O+SZ9iFRMNkoXzBRxOGDgr4xkA=
golang.org/x/exp/typeparams v0.0.0-20251017212417-90e834f514db/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8=
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20251014153721-24f779f6aaef h1:5xFtU4tmJMJSxSeDlr1dgBff2tDXrq0laLdS1EA3LYw=
golang.org/x/telemetry v0.0.0-20251014153721-24f779f6aaef/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
@@ -214,25 +221,19 @@ golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I=
golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.249.0 h1:0VrsWAKzIZi058aeq+I86uIXbNhm9GxSHpbmZ92a38w=
google.golang.org/api v0.249.0/go.mod h1:dGk9qyI0UYPwO/cjt2q06LG/EhUpwZGdAbYF14wHHrQ=
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
google.golang.org/genproto v0.0.0-20250908214217-97024824d090/go.mod h1:zwJI9HzbJJlw2KXy0wX+lmT2JuZoaKK9JC4ppqmxxjk=
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/genai v1.31.0 h1:R7xDt/Dosz11vcXbZ4IgisGnzUGGau2PZOIOAnXsYjw=
google.golang.org/genai v1.31.0/go.mod h1:7pAilaICJlQBonjKKJNhftDFv3SREhZcTe9F6nRcjbg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251020155222-88f65dc88635 h1:3uycTxukehWrxH4HtPRtn1PDABTU331ViDjyqrUbaog=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251020155222-88f65dc88635/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=

View File

@@ -1,4 +1,4 @@
go 1.25.1
go 1.25.3
use (
.

View File

@@ -31,10 +31,16 @@ cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZ
cloud.google.com/go v0.60.0/go.mod h1:yw2G51M9IfRboUH61Us8GqCeF1PzPblB823Mn2q2eAU=
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 v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms=
cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
cloud.google.com/go v0.121.2/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw=
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
cloud.google.com/go/accessapproval v1.7.5/go.mod h1:g88i1ok5dvQ9XJsxpUInWWvUBrIZhyPDPbk4T01OoJ0=
cloud.google.com/go/accessapproval v1.8.7/go.mod h1:BFvZOW4GJjJnl6aA/YDEg0TGViFHyusa/bMdcVFmh8A=
cloud.google.com/go/accesscontextmanager v1.8.5/go.mod h1:TInEhcZ7V9jptGNqN3EzZ5XMhT6ijWxTGjzyETwmL0Q=
cloud.google.com/go/accesscontextmanager v1.9.6/go.mod h1:884XHwy1AQpCX5Cj2VqYse77gfLaq9f8emE2bYriilk=
cloud.google.com/go/ai v0.12.1/go.mod h1:5vIPNe1ZQsVZqCliXIPL4QnhObQQY4d9hAGHdVc4iw4=
cloud.google.com/go/aiplatform v1.60.0/go.mod h1:eTlGuHOahHprZw3Hio5VKmtThIOak5/qy6pzdsqcQnM=
cloud.google.com/go/aiplatform v1.100.0/go.mod h1:oZUOTz6+cMt9eVNe62CXPfIQQQ+QjR4rW3GBGD9r6Fg=
cloud.google.com/go/analytics v0.23.0/go.mod h1:YPd7Bvik3WS95KBok2gPXDqQPHy08TsCQG6CdUCb+u0=
@@ -55,6 +61,16 @@ cloud.google.com/go/asset v1.17.2/go.mod h1:SVbzde67ehddSoKf5uebOD1sYw8Ab/jD/9EI
cloud.google.com/go/asset v1.21.1/go.mod h1:7AzY1GCC+s1O73yzLM1IpHFLHz3ws2OigmCpOQHwebk=
cloud.google.com/go/assuredworkloads v1.11.5/go.mod h1:FKJ3g3ZvkL2D7qtqIGnDufFkHxwIpNM9vtmhvt+6wqk=
cloud.google.com/go/assuredworkloads v1.12.6/go.mod h1:QyZHd7nH08fmZ+G4ElihV1zoZ7H0FQCpgS0YWtwjCKo=
cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w=
cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=
cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk=
cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M=
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/automl v1.13.5/go.mod h1:MDw3vLem3yh+SvmSgeYUmUKqyls6NzSumDm9OJ3xJ1Y=
cloud.google.com/go/automl v1.14.7/go.mod h1:8a4XbIH5pdvrReOU72oB+H3pOw2JBxo9XTk39oljObE=
cloud.google.com/go/baremetalsolution v1.2.4/go.mod h1:BHCmxgpevw9IEryE99HbYEfxXkAEA3hkMJbYYsHtIuY=
@@ -107,6 +123,7 @@ cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykW
cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
cloud.google.com/go/contactcenterinsights v1.13.0/go.mod h1:ieq5d5EtHsu8vhe2y3amtZ+BE+AQwX5qAy7cpo0POsI=
cloud.google.com/go/contactcenterinsights v1.17.3/go.mod h1:7Uu2CpxS3f6XxhRdlEzYAkrChpR5P5QfcdGAFEdHOG8=
cloud.google.com/go/container v1.31.0/go.mod h1:7yABn5s3Iv3lmw7oMmyGbeV6tQj86njcTijkkGuvdZA=
@@ -191,6 +208,8 @@ cloud.google.com/go/lifesciences v0.10.6/go.mod h1:1nnZwaZcBThDujs9wXzECnd1S5d+U
cloud.google.com/go/logging v1.9.0/go.mod h1:1Io0vnZv4onoUnsVUQY3HZ3Igb1nBchky0A0y7BBBhE=
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s=
cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
cloud.google.com/go/managedidentities v1.6.5/go.mod h1:fkFI2PwwyRQbjLxlm5bQ8SjtObFMW3ChBGNqaMcgZjI=
cloud.google.com/go/managedidentities v1.7.6/go.mod h1:pYCWPaI1AvR8Q027Vtp+SFSM/VOVgbjBF4rxp1/z5p4=
cloud.google.com/go/maps v1.6.4/go.mod h1:rhjqRy8NWmDJ53saCfsXQ0LKwBHfi6OSh5wkq6BaMhI=
@@ -405,6 +424,7 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
@@ -413,6 +433,7 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1
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/anthropics/anthropic-sdk-go v1.12.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
@@ -420,6 +441,20 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5
github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.36.30/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -508,12 +543,14 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
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/eliben/go-sentencepiece v0.6.0/go.mod h1:nNYk4aMzgBoI6QFp4LUG8Eu1uO9fHD9L5ZEre93o9+c=
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=
@@ -578,6 +615,7 @@ github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI=
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -681,6 +719,7 @@ github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/certificate-transparency-go v1.1.1/go.mod h1:FDKqPvSXawb2ecErVRrD+nfy23RCzyl7eqVCEmlT1Zs=
github.com/google/generative-ai-go v0.20.1/go.mod h1:TjOnZJmZKzarWbjUJgy+r3Ee7HGBRVLhOIgupnwR4Bg=
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=
@@ -725,6 +764,7 @@ github.com/google/pprof v0.0.0-20241101162523-b92577c0c142/go.mod h1:vavhavw2zAx
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
github.com/google/trillian v1.3.11/go.mod h1:0tPraVHrSDkA3BO6vKX67zgLXs6SsOAbHEivX+9mPgw=
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -735,6 +775,8 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
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/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
github.com/googleapis/enterprise-certificate-proxy v0.3.5/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
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=
@@ -742,6 +784,9 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc=
github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
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/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
@@ -769,6 +814,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.0/go.mod h1:4EgsQoS4TOhJizV+JTFg
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -963,6 +1010,7 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwd
github.com/mozilla/scribe v0.0.0-20180711195314-fb71baf557c1/go.mod h1:FIczTrinKo8VaLxe6PWTPEXRXDIHz2QAwiaBaP5/4a8=
github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5 h1:0KqC6/sLy7fDpBdybhVkkv4Yz+PmB7c9Dz9z3dLW804=
github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5/go.mod h1:FUqVoUPHSEdDR0MnFM3Dh8AU0pZHLXUD127SAJGER/s=
github.com/mozilla/tls-observatory v0.0.0-20250923143331-eef96233227e/go.mod h1:FUqVoUPHSEdDR0MnFM3Dh8AU0pZHLXUD127SAJGER/s=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
@@ -1011,6 +1059,7 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
@@ -1029,11 +1078,14 @@ github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB8
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/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/pseudomuto/protoc-gen-doc v1.3.2/go.mod h1:y5+P6n3iGrbKG+9O04V5ld71in3v/bX88wUwgt+U8EA=
github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q=
github.com/quic-go/qtls-go1-20 v0.3.3/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
@@ -1137,15 +1189,20 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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=
@@ -1227,6 +1284,7 @@ go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/detectors/gcp v1.32.0 h1:P78qWqkLSShicHmAzfECaTgvslqHxblNE9j62Ws1NK8=
go.opentelemetry.io/contrib/detectors/gcp v1.32.0/go.mod h1:TVqo0Sda4Cv8gCIixd7LuLwW4EylumVWfhjZJjDD4DU=
go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao=
@@ -1236,11 +1294,16 @@ go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVL
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
@@ -1248,6 +1311,7 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQF
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
@@ -1255,6 +1319,7 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
@@ -1262,6 +1327,7 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 h1:jBpDk4HAUsrnVO1FsfCfCOTEc/MkInJmvfCHYLFiT80=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0/go.mod h1:H9LUIM1daaeZaz91vZcfeM0fejXPmgCYE8ZhzqfJuiU=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE=
@@ -1269,16 +1335,25 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 h1:G8Xec/SgZQricwWBJF/mHZc7A02YHedfFDENwJEdRA0=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0/go.mod h1:tx8OOlGH6R4kLV67YaYO44GFXloEjGPZuMjEkaaqIp4=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
@@ -1287,17 +1362,20 @@ go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAj
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
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=
@@ -1306,9 +1384,11 @@ golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
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-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
@@ -1323,12 +1403,17 @@ 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/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1355,7 +1440,11 @@ golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA=
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b h1:18qgiDvlvH7kk8Ioa8Ov+K6xCi0GMvmGfGW0sgd/SYA=
golang.org/x/exp/typeparams v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=
golang.org/x/exp/typeparams v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
golang.org/x/exp/typeparams v0.0.0-20251009144603-d2f985daa21b/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -1391,6 +1480,7 @@ golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
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=
@@ -1402,10 +1492,12 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -1457,6 +1549,9 @@ golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
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=
@@ -1484,6 +1579,11 @@ golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
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=
@@ -1491,6 +1591,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
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-20190412183630-56d357773e84/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1501,9 +1602,12 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1511,9 +1615,11 @@ golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1544,6 +1650,7 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/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=
@@ -1580,6 +1687,9 @@ golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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=
@@ -1588,7 +1698,10 @@ golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMe
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
golang.org/x/telemetry v0.0.0-20250603144755-9a9ac2102d0e/go.mod h1:QNvpSH4vItB4zw8JazOv6Ba3fs1TorwPx9cCU6qTIdE=
golang.org/x/telemetry v0.0.0-20250606142133-60998feb31a8/go.mod h1:mUcjA5g0luJpMYCLjhH91f4t4RAUNp+zq9ZmUoqPD7M=
golang.org/x/telemetry v0.0.0-20250829165349-50b750f55de1/go.mod h1:JIJwPkb04vX0KeIBbQ7epGtgIjA8ihHbsAtW4A/lIQ4=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
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=
@@ -1623,9 +1736,12 @@ golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.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=
@@ -1642,6 +1758,9 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1649,8 +1768,11 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -1672,6 +1794,7 @@ golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@@ -1717,6 +1840,9 @@ golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/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=
@@ -1742,7 +1868,14 @@ google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w=
google.golang.org/api v0.169.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg=
google.golang.org/api v0.177.0/go.mod h1:srbhue4MLjkjbkux5p3dw/ocYOSZTaIEvf7bCOnFQDw=
google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8=
google.golang.org/api v0.229.0/go.mod h1:wyDfmq5g1wYJWn29O22FDWN48P7Xcz0xz+LBpptYvB0=
google.golang.org/api v0.236.0/go.mod h1:X1WF9CU2oTc+Jml1tiIxGmWFK/UZezdqEu09gcxZAj4=
google.golang.org/api v0.237.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM=
google.golang.org/api v0.248.0/go.mod h1:yAFUAF56Li7IuIQbTFoLwXTCI6XCFKueOlS7S9e4F9k=
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=
@@ -1757,6 +1890,7 @@ google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genai v1.25.0/go.mod h1:OClfdf+r5aaD+sCd4aUSkPzJItmg2wD/WON9lQnRPaY=
google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@@ -1806,6 +1940,10 @@ google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUE
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo=
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE=
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
google.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1 h1:Nm5SEGIguOIBDXs5rhfz2aKwEVWlgwC58UcmEnLDc8Y=
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw=
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo=
@@ -1815,6 +1953,7 @@ google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.
google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6/go.mod h1:10yRODfgim2/T8csjQsMPgZOMvtytXKTDRzH6HRGzRw=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
@@ -1822,6 +1961,13 @@ google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.
google.golang.org/genproto/googleapis/api v0.0.0-20250122153221-138b5a5a4fd4 h1://y4MHaM7tNLqTeWKyfBIeoAMxwKwRm/nODb5IKA3BE=
google.golang.org/genproto/googleapis/api v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:AfA77qWLcidQWywD0YgqfpJzf50w2VjzBml3TybHeJU=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:vYFwMYFbmA8vl6Z/krj/h7+U/AqpHknwJX4Uqgfyc7I=
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=
google.golang.org/genproto/googleapis/api v0.0.0-20251014184007-4626949a642f/go.mod h1:kprOiu9Tr0JYyD6DORrc4Hfyk3RFXqkQ3ctHEum3ZbM=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:vh/N7795ftP0AkN1w8XKqN4w1OdUKXW5Eummda+ofv8=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:WkJpQl6Ujj3ElX4qZaNm5t6cT95ffI4K+HKQ0+1NyMw=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250414145226-207652e42e2e h1:OK8bKvRgTGs7U871RdjtCiRcQJLice8/rZkeoaZgnlc=
@@ -1835,9 +1981,21 @@ google.golang.org/genproto/googleapis/bytestream v0.0.0-20250818200422-3122310a4
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/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250227231956-55c901821b1e/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f h1:1FTH6cpXFsENbPR5Bu8NQddPSaUUE6NA2XdZdDSAJK4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
@@ -1858,8 +2016,15 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
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/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
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=
@@ -1880,6 +2045,9 @@ google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -1920,6 +2088,7 @@ honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs=
moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE=
mvdan.cc/gofumpt v0.9.0/go.mod h1:3xYtNemnKiXaTh6R4VtlqDATFwBbdXI8lJvH/4qk7mw=
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=

View File

@@ -37,10 +37,19 @@ type Profile struct {
// Access is the access manager for this profile. It must not be nil.
Access access.Profile
// AdultBlockingMode defines the way blocked responses for adult-content are
// constructed. In case of nil, the default BlockingMode is used.
AdultBlockingMode dnsmsg.BlockingMode
// BlockingMode defines the way blocked responses are constructed. It must
// not be nil.
BlockingMode dnsmsg.BlockingMode
// SafeBrowsingBlockingMode defines the way blocked responses for
// safe-browsing content are constructed. In case of nil, the default
// BlockingMode is used.
SafeBrowsingBlockingMode dnsmsg.BlockingMode
// Ratelimiter is the custom ratelimiter for this profile. It must not be
// nil.
Ratelimiter Ratelimiter

View File

@@ -8,7 +8,7 @@ import (
)
// maxCertificateNameLen is the maximum length of a [CertificateName].
const maxCertificateNameLen = 32
const maxCertificateNameLen = 64
// CertificateName is the unique name identifying the TLS certificate.
type CertificateName string

View File

@@ -28,8 +28,8 @@ func TestNewCertificateName(t *testing.T) {
wantErrMsg: "at index 3: bad symbol: '/'",
}, {
name: "too_long",
value: "this_is_a_very_long_certificate_name",
wantErrMsg: "length: out of range: must be no greater than 32, got 36",
value: "this_is_a_very_long_certificate_name_which_should_be_64_symbols_long",
wantErrMsg: "length: out of range: must be no greater than 64, got 68",
}, {
name: "ok",
value: "ok_cert_name",

View File

@@ -4,7 +4,10 @@
package agdnet
import (
"net/http/cookiejar"
"strings"
"github.com/AdguardTeam/golibs/netutil"
)
// These are suffixes of the FQDN in DNS queries for metrics that the DNS
@@ -65,3 +68,38 @@ func NormalizeQueryDomain(host string) (norm string) {
return NormalizeDomain(host)
}
// AppendSubdomains appends all subdomains to orig and limits result length with
// subDomainNum. publicList must not be nil.
func AppendSubdomains(
orig []string,
domain string,
subDomainNum int,
publicList cookiejar.PublicSuffixList,
) (sub []string) {
sub = orig
pubSuf := publicList.PublicSuffix(domain)
dotsNum := 0
i := strings.LastIndexFunc(domain, func(r rune) (ok bool) {
if r == '.' {
dotsNum++
}
return dotsNum == subDomainNum
})
if i != -1 {
domain = domain[i+1:]
}
sub = netutil.AppendSubdomains(sub, domain)
for i, s := range sub {
if s == pubSuf {
sub = sub[:i]
break
}
}
return sub
}

View File

@@ -1,9 +1,97 @@
package agdnet_test
import "net/netip"
import (
"net/netip"
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agdnet"
"github.com/stretchr/testify/assert"
"golang.org/x/net/publicsuffix"
)
// Common subnets for tests.
var (
testSubnetIPv4 = netip.MustParsePrefix("1.2.3.0/24")
testSubnetIPv6 = netip.MustParsePrefix("1234:5678::/64")
)
func TestAppendSubdomains(t *testing.T) {
testCases := []struct {
name string
domain string
want []string
subDomainNum int
}{{
name: "all_sub_domains",
domain: "example.a.b.c.org",
subDomainNum: 5,
want: []string{
"c.org",
"b.c.org",
"a.b.c.org",
"example.a.b.c.org",
},
}, {
name: "no_sub_domains",
domain: "org",
subDomainNum: 100,
want: []string{},
}, {
name: "limit_sub_domains",
domain: "example.a.b.c.org",
subDomainNum: 3,
want: []string{
"c.org",
"b.c.org",
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := agdnet.AppendSubdomains(nil, tc.domain, tc.subDomainNum, publicsuffix.List)
assert.ElementsMatch(t, tc.want, actual)
})
}
}
func BenchmarkAppendSubdomains(b *testing.B) {
benchCases := []struct {
name string
domain string
num int
}{{
name: "many_sub_domains",
domain: "example.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.org",
num: 100,
}, {
name: "no_sub_domains",
domain: "org",
num: 100,
}, {
name: "limit_sub_domains",
domain: "example.a.b.c.org",
num: 3,
}}
for _, bc := range benchCases {
b.Run(bc.name, func(b *testing.B) {
b.ReportAllocs()
// Warmup to fill the slices.
got := agdnet.AppendSubdomains(nil, bc.domain, bc.num, publicsuffix.List)
for b.Loop() {
got = agdnet.AppendSubdomains(got[:0], bc.domain, bc.num, publicsuffix.List)
}
})
}
// Most recent results:
// goos: darwin
// goarch: arm64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/agdnet
// cpu: Apple M3
// BenchmarkAppendSubdomains/many_sub_domains-8 3826170 313.4 ns/op 0 B/op 0 allocs/op
// BenchmarkAppendSubdomains/no_sub_domains-8 20443263 58.75 ns/op 0 B/op 0 allocs/op
// BenchmarkAppendSubdomains/limit_sub_domains-8 10685626 112.2 ns/op 0 B/op 0 allocs/op
}

View File

@@ -0,0 +1,32 @@
package agdprotobuf
import "unsafe"
// UnsafelyConvertStrSlice checks if []T1 can be converted to []T2 at compile
// time and, if so, converts the slice using package unsafe.
//
// Slices resulting from this conversion must not be mutated.
// TODO(f.setrakov): Generalize.
func UnsafelyConvertStrSlice[T1, T2 ~string](s []T1) (res []T2) {
if s == nil {
return nil
}
// #nosec G103 -- Conversion between two slices with the same underlying
// element type is safe.
return *(*[]T2)(unsafe.Pointer(&s))
}
// UnsafelyConvertUint32Slice checks if []T1 can be converted to []T2 at compile
// time and, if so, converts the slice using package unsafe.
//
// Slices resulting from this conversion must not be mutated.
func UnsafelyConvertUint32Slice[T1, T2 ~uint32](s []T1) (res []T2) {
if s == nil {
return nil
}
// #nosec G103 -- Conversion between two slices with the same underlying
// element type is safe.
return *(*[]T2)(unsafe.Pointer(&s))
}

View File

@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.8
// protoc-gen-go v1.36.9
// protoc v6.32.0
// source: dns.proto
@@ -298,28 +298,28 @@ func (x *DNSProfilesRequest) GetSyncTime() *timestamppb.Timestamp {
return nil
}
// *
// Message DNSProfile contains both the data of the account and the data of the
// DNS server.
//
// The fields are ordered in a way that optimizes the generated structures'
// layouts.
//
// TODO(a.garipov): Expand the field documentation.
type DNSProfile struct {
state protoimpl.MessageState `protogen:"open.v1"`
DnsId string `protobuf:"bytes,1,opt,name=dns_id,json=dnsId,proto3" json:"dns_id,omitempty"`
FilteringEnabled bool `protobuf:"varint,2,opt,name=filtering_enabled,json=filteringEnabled,proto3" json:"filtering_enabled,omitempty"`
QueryLogEnabled bool `protobuf:"varint,3,opt,name=query_log_enabled,json=queryLogEnabled,proto3" json:"query_log_enabled,omitempty"`
Deleted bool `protobuf:"varint,4,opt,name=deleted,proto3" json:"deleted,omitempty"`
SafeBrowsing *SafeBrowsingSettings `protobuf:"bytes,5,opt,name=safe_browsing,json=safeBrowsing,proto3" json:"safe_browsing,omitempty"`
Parental *ParentalSettings `protobuf:"bytes,6,opt,name=parental,proto3" json:"parental,omitempty"`
RuleLists *RuleListsSettings `protobuf:"bytes,7,opt,name=rule_lists,json=ruleLists,proto3" json:"rule_lists,omitempty"`
// Field devices contains the COMPLETE list of all devices in the profile.
// Field device_changes contains only the list of changes that have happened
// to the profile's devices since sync_time.
//
// devices and device_changes MUST NOT be set at the same time, but they MAY
// both be empty.
//
// device_changes MUST NOT contain multiple changes for the same device.
Devices []*DeviceSettings `protobuf:"bytes,8,rep,name=devices,proto3" json:"devices,omitempty"`
CustomRules []string `protobuf:"bytes,9,rep,name=custom_rules,json=customRules,proto3" json:"custom_rules,omitempty"`
FilteredResponseTtl *durationpb.Duration `protobuf:"bytes,10,opt,name=filtered_response_ttl,json=filteredResponseTtl,proto3" json:"filtered_response_ttl,omitempty"`
BlockPrivateRelay bool `protobuf:"varint,11,opt,name=block_private_relay,json=blockPrivateRelay,proto3" json:"block_private_relay,omitempty"`
BlockFirefoxCanary bool `protobuf:"varint,12,opt,name=block_firefox_canary,json=blockFirefoxCanary,proto3" json:"block_firefox_canary,omitempty"`
Access *AccessSettings `protobuf:"bytes,18,opt,name=access,proto3" json:"access,omitempty"`
RateLimit *RateLimitSettings `protobuf:"bytes,20,opt,name=rate_limit,json=rateLimit,proto3" json:"rate_limit,omitempty"`
CustomDomain *CustomDomainSettings `protobuf:"bytes,22,opt,name=custom_domain,json=customDomain,proto3" json:"custom_domain,omitempty"`
// *
// Field blocking_mode defines the blocking mode for general rule-list based
// filtering. If field deleted is false, field blocking_mode MUST be
// present.
//
// Types that are valid to be assigned to BlockingMode:
//
// *DNSProfile_BlockingModeCustomIp
@@ -327,14 +327,64 @@ type DNSProfile struct {
// *DNSProfile_BlockingModeNullIp
// *DNSProfile_BlockingModeRefused
BlockingMode isDNSProfile_BlockingMode `protobuf_oneof:"blocking_mode"`
IpLogEnabled bool `protobuf:"varint,17,opt,name=ip_log_enabled,json=ipLogEnabled,proto3" json:"ip_log_enabled,omitempty"`
Access *AccessSettings `protobuf:"bytes,18,opt,name=access,proto3" json:"access,omitempty"`
AutoDevicesEnabled bool `protobuf:"varint,19,opt,name=auto_devices_enabled,json=autoDevicesEnabled,proto3" json:"auto_devices_enabled,omitempty"`
RateLimit *RateLimitSettings `protobuf:"bytes,20,opt,name=rate_limit,json=rateLimit,proto3" json:"rate_limit,omitempty"`
BlockChromePrefetch bool `protobuf:"varint,21,opt,name=block_chrome_prefetch,json=blockChromePrefetch,proto3" json:"block_chrome_prefetch,omitempty"`
CustomDomain *CustomDomainSettings `protobuf:"bytes,22,opt,name=custom_domain,json=customDomain,proto3" json:"custom_domain,omitempty"`
// *
// Field adult_blocking_mode defines the blocking mode for the adult-content
// filter. If absent, the default is used.
//
// Types that are valid to be assigned to AdultBlockingMode:
//
// *DNSProfile_AdultBlockingModeCustomIp
// *DNSProfile_AdultBlockingModeNxdomain
// *DNSProfile_AdultBlockingModeNullIp
// *DNSProfile_AdultBlockingModeRefused
AdultBlockingMode isDNSProfile_AdultBlockingMode `protobuf_oneof:"adult_blocking_mode"`
// *
// Field safe_browsing_blocking_mode defines the blocking mode for the
// safe-browsing filter. If absent, the default is used.
//
// Types that are valid to be assigned to SafeBrowsingBlockingMode:
//
// *DNSProfile_SafeBrowsingBlockingModeCustomIp
// *DNSProfile_SafeBrowsingBlockingModeNxdomain
// *DNSProfile_SafeBrowsingBlockingModeNullIp
// *DNSProfile_SafeBrowsingBlockingModeRefused
SafeBrowsingBlockingMode isDNSProfile_SafeBrowsingBlockingMode `protobuf_oneof:"safe_browsing_blocking_mode"`
// *
// Field dns_id is the ID of the DNS server. Not to be confused with the ID
// of the account, see account_id below. dns_id MUST be present.
DnsId string `protobuf:"bytes,1,opt,name=dns_id,json=dnsId,proto3" json:"dns_id,omitempty"`
// *
// Field account_id is the ID of the account to which this DNS server
// belongs. If field deleted is false, account_id MUST be present.
AccountId string `protobuf:"bytes,23,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"`
// *
// Field devices contains the complete list of all devices in the profile, if
// any. Fields devices and device_changes MUST NOT be set at the same time,
// but they MAY both be empty. Field devices MUST NOT contain duplicates.
Devices []*DeviceSettings `protobuf:"bytes,8,rep,name=devices,proto3" json:"devices,omitempty"`
// *
// Field device_changes contains only the list of changes that have happened
// to the profile's devices since sync_time, if any. Fields devices and
// device_changes MUST NOT be set at the same time, but they MAY both be
// empty. Field device_changes MUST NOT contain multiple changes for the
// same device.
DeviceChanges []*DeviceSettingsChange `protobuf:"bytes,24,rep,name=device_changes,json=deviceChanges,proto3" json:"device_changes,omitempty"`
// *
// Field custom_rules contains custom filtering rules for this DNS server, if
// any. All items MUST contain only valid UTF-8 characters and have the size
// less than or equal to UTF-8 1024 characters (not bytes).
CustomRules []string `protobuf:"bytes,9,rep,name=custom_rules,json=customRules,proto3" json:"custom_rules,omitempty"`
FilteringEnabled bool `protobuf:"varint,2,opt,name=filtering_enabled,json=filteringEnabled,proto3" json:"filtering_enabled,omitempty"`
QueryLogEnabled bool `protobuf:"varint,3,opt,name=query_log_enabled,json=queryLogEnabled,proto3" json:"query_log_enabled,omitempty"`
// *
// Field deleted, if true, means that this DNS server has been deleted. All
// other fields except dns_id SHOULD be absent.
Deleted bool `protobuf:"varint,4,opt,name=deleted,proto3" json:"deleted,omitempty"`
BlockPrivateRelay bool `protobuf:"varint,11,opt,name=block_private_relay,json=blockPrivateRelay,proto3" json:"block_private_relay,omitempty"`
BlockFirefoxCanary bool `protobuf:"varint,12,opt,name=block_firefox_canary,json=blockFirefoxCanary,proto3" json:"block_firefox_canary,omitempty"`
IpLogEnabled bool `protobuf:"varint,17,opt,name=ip_log_enabled,json=ipLogEnabled,proto3" json:"ip_log_enabled,omitempty"`
AutoDevicesEnabled bool `protobuf:"varint,19,opt,name=auto_devices_enabled,json=autoDevicesEnabled,proto3" json:"auto_devices_enabled,omitempty"`
BlockChromePrefetch bool `protobuf:"varint,21,opt,name=block_chrome_prefetch,json=blockChromePrefetch,proto3" json:"block_chrome_prefetch,omitempty"`
StandardAccessSettingsEnabled bool `protobuf:"varint,25,opt,name=standard_access_settings_enabled,json=standardAccessSettingsEnabled,proto3" json:"standard_access_settings_enabled,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -370,34 +420,6 @@ func (*DNSProfile) Descriptor() ([]byte, []int) {
return file_dns_proto_rawDescGZIP(), []int{5}
}
func (x *DNSProfile) GetDnsId() string {
if x != nil {
return x.DnsId
}
return ""
}
func (x *DNSProfile) GetFilteringEnabled() bool {
if x != nil {
return x.FilteringEnabled
}
return false
}
func (x *DNSProfile) GetQueryLogEnabled() bool {
if x != nil {
return x.QueryLogEnabled
}
return false
}
func (x *DNSProfile) GetDeleted() bool {
if x != nil {
return x.Deleted
}
return false
}
func (x *DNSProfile) GetSafeBrowsing() *SafeBrowsingSettings {
if x != nil {
return x.SafeBrowsing
@@ -419,20 +441,6 @@ func (x *DNSProfile) GetRuleLists() *RuleListsSettings {
return nil
}
func (x *DNSProfile) GetDevices() []*DeviceSettings {
if x != nil {
return x.Devices
}
return nil
}
func (x *DNSProfile) GetCustomRules() []string {
if x != nil {
return x.CustomRules
}
return nil
}
func (x *DNSProfile) GetFilteredResponseTtl() *durationpb.Duration {
if x != nil {
return x.FilteredResponseTtl
@@ -440,18 +448,25 @@ func (x *DNSProfile) GetFilteredResponseTtl() *durationpb.Duration {
return nil
}
func (x *DNSProfile) GetBlockPrivateRelay() bool {
func (x *DNSProfile) GetAccess() *AccessSettings {
if x != nil {
return x.BlockPrivateRelay
return x.Access
}
return false
return nil
}
func (x *DNSProfile) GetBlockFirefoxCanary() bool {
func (x *DNSProfile) GetRateLimit() *RateLimitSettings {
if x != nil {
return x.BlockFirefoxCanary
return x.RateLimit
}
return false
return nil
}
func (x *DNSProfile) GetCustomDomain() *CustomDomainSettings {
if x != nil {
return x.CustomDomain
}
return nil
}
func (x *DNSProfile) GetBlockingMode() isDNSProfile_BlockingMode {
@@ -497,48 +512,99 @@ func (x *DNSProfile) GetBlockingModeRefused() *BlockingModeREFUSED {
return nil
}
func (x *DNSProfile) GetIpLogEnabled() bool {
func (x *DNSProfile) GetAdultBlockingMode() isDNSProfile_AdultBlockingMode {
if x != nil {
return x.IpLogEnabled
}
return false
}
func (x *DNSProfile) GetAccess() *AccessSettings {
if x != nil {
return x.Access
return x.AdultBlockingMode
}
return nil
}
func (x *DNSProfile) GetAutoDevicesEnabled() bool {
func (x *DNSProfile) GetAdultBlockingModeCustomIp() *BlockingModeCustomIP {
if x != nil {
return x.AutoDevicesEnabled
if x, ok := x.AdultBlockingMode.(*DNSProfile_AdultBlockingModeCustomIp); ok {
return x.AdultBlockingModeCustomIp
}
return false
}
func (x *DNSProfile) GetRateLimit() *RateLimitSettings {
if x != nil {
return x.RateLimit
}
return nil
}
func (x *DNSProfile) GetBlockChromePrefetch() bool {
func (x *DNSProfile) GetAdultBlockingModeNxdomain() *BlockingModeNXDOMAIN {
if x != nil {
return x.BlockChromePrefetch
if x, ok := x.AdultBlockingMode.(*DNSProfile_AdultBlockingModeNxdomain); ok {
return x.AdultBlockingModeNxdomain
}
return false
}
func (x *DNSProfile) GetCustomDomain() *CustomDomainSettings {
if x != nil {
return x.CustomDomain
}
return nil
}
func (x *DNSProfile) GetAdultBlockingModeNullIp() *BlockingModeNullIP {
if x != nil {
if x, ok := x.AdultBlockingMode.(*DNSProfile_AdultBlockingModeNullIp); ok {
return x.AdultBlockingModeNullIp
}
}
return nil
}
func (x *DNSProfile) GetAdultBlockingModeRefused() *BlockingModeREFUSED {
if x != nil {
if x, ok := x.AdultBlockingMode.(*DNSProfile_AdultBlockingModeRefused); ok {
return x.AdultBlockingModeRefused
}
}
return nil
}
func (x *DNSProfile) GetSafeBrowsingBlockingMode() isDNSProfile_SafeBrowsingBlockingMode {
if x != nil {
return x.SafeBrowsingBlockingMode
}
return nil
}
func (x *DNSProfile) GetSafeBrowsingBlockingModeCustomIp() *BlockingModeCustomIP {
if x != nil {
if x, ok := x.SafeBrowsingBlockingMode.(*DNSProfile_SafeBrowsingBlockingModeCustomIp); ok {
return x.SafeBrowsingBlockingModeCustomIp
}
}
return nil
}
func (x *DNSProfile) GetSafeBrowsingBlockingModeNxdomain() *BlockingModeNXDOMAIN {
if x != nil {
if x, ok := x.SafeBrowsingBlockingMode.(*DNSProfile_SafeBrowsingBlockingModeNxdomain); ok {
return x.SafeBrowsingBlockingModeNxdomain
}
}
return nil
}
func (x *DNSProfile) GetSafeBrowsingBlockingModeNullIp() *BlockingModeNullIP {
if x != nil {
if x, ok := x.SafeBrowsingBlockingMode.(*DNSProfile_SafeBrowsingBlockingModeNullIp); ok {
return x.SafeBrowsingBlockingModeNullIp
}
}
return nil
}
func (x *DNSProfile) GetSafeBrowsingBlockingModeRefused() *BlockingModeREFUSED {
if x != nil {
if x, ok := x.SafeBrowsingBlockingMode.(*DNSProfile_SafeBrowsingBlockingModeRefused); ok {
return x.SafeBrowsingBlockingModeRefused
}
}
return nil
}
func (x *DNSProfile) GetDnsId() string {
if x != nil {
return x.DnsId
}
return ""
}
func (x *DNSProfile) GetAccountId() string {
if x != nil {
return x.AccountId
@@ -546,6 +612,13 @@ func (x *DNSProfile) GetAccountId() string {
return ""
}
func (x *DNSProfile) GetDevices() []*DeviceSettings {
if x != nil {
return x.Devices
}
return nil
}
func (x *DNSProfile) GetDeviceChanges() []*DeviceSettingsChange {
if x != nil {
return x.DeviceChanges
@@ -553,6 +626,69 @@ func (x *DNSProfile) GetDeviceChanges() []*DeviceSettingsChange {
return nil
}
func (x *DNSProfile) GetCustomRules() []string {
if x != nil {
return x.CustomRules
}
return nil
}
func (x *DNSProfile) GetFilteringEnabled() bool {
if x != nil {
return x.FilteringEnabled
}
return false
}
func (x *DNSProfile) GetQueryLogEnabled() bool {
if x != nil {
return x.QueryLogEnabled
}
return false
}
func (x *DNSProfile) GetDeleted() bool {
if x != nil {
return x.Deleted
}
return false
}
func (x *DNSProfile) GetBlockPrivateRelay() bool {
if x != nil {
return x.BlockPrivateRelay
}
return false
}
func (x *DNSProfile) GetBlockFirefoxCanary() bool {
if x != nil {
return x.BlockFirefoxCanary
}
return false
}
func (x *DNSProfile) GetIpLogEnabled() bool {
if x != nil {
return x.IpLogEnabled
}
return false
}
func (x *DNSProfile) GetAutoDevicesEnabled() bool {
if x != nil {
return x.AutoDevicesEnabled
}
return false
}
func (x *DNSProfile) GetBlockChromePrefetch() bool {
if x != nil {
return x.BlockChromePrefetch
}
return false
}
func (x *DNSProfile) GetStandardAccessSettingsEnabled() bool {
if x != nil {
return x.StandardAccessSettingsEnabled
@@ -588,6 +724,62 @@ func (*DNSProfile_BlockingModeNullIp) isDNSProfile_BlockingMode() {}
func (*DNSProfile_BlockingModeRefused) isDNSProfile_BlockingMode() {}
type isDNSProfile_AdultBlockingMode interface {
isDNSProfile_AdultBlockingMode()
}
type DNSProfile_AdultBlockingModeCustomIp struct {
AdultBlockingModeCustomIp *BlockingModeCustomIP `protobuf:"bytes,26,opt,name=adult_blocking_mode_custom_ip,json=adultBlockingModeCustomIp,proto3,oneof"`
}
type DNSProfile_AdultBlockingModeNxdomain struct {
AdultBlockingModeNxdomain *BlockingModeNXDOMAIN `protobuf:"bytes,27,opt,name=adult_blocking_mode_nxdomain,json=adultBlockingModeNxdomain,proto3,oneof"`
}
type DNSProfile_AdultBlockingModeNullIp struct {
AdultBlockingModeNullIp *BlockingModeNullIP `protobuf:"bytes,28,opt,name=adult_blocking_mode_null_ip,json=adultBlockingModeNullIp,proto3,oneof"`
}
type DNSProfile_AdultBlockingModeRefused struct {
AdultBlockingModeRefused *BlockingModeREFUSED `protobuf:"bytes,29,opt,name=adult_blocking_mode_refused,json=adultBlockingModeRefused,proto3,oneof"`
}
func (*DNSProfile_AdultBlockingModeCustomIp) isDNSProfile_AdultBlockingMode() {}
func (*DNSProfile_AdultBlockingModeNxdomain) isDNSProfile_AdultBlockingMode() {}
func (*DNSProfile_AdultBlockingModeNullIp) isDNSProfile_AdultBlockingMode() {}
func (*DNSProfile_AdultBlockingModeRefused) isDNSProfile_AdultBlockingMode() {}
type isDNSProfile_SafeBrowsingBlockingMode interface {
isDNSProfile_SafeBrowsingBlockingMode()
}
type DNSProfile_SafeBrowsingBlockingModeCustomIp struct {
SafeBrowsingBlockingModeCustomIp *BlockingModeCustomIP `protobuf:"bytes,30,opt,name=safe_browsing_blocking_mode_custom_ip,json=safeBrowsingBlockingModeCustomIp,proto3,oneof"`
}
type DNSProfile_SafeBrowsingBlockingModeNxdomain struct {
SafeBrowsingBlockingModeNxdomain *BlockingModeNXDOMAIN `protobuf:"bytes,31,opt,name=safe_browsing_blocking_mode_nxdomain,json=safeBrowsingBlockingModeNxdomain,proto3,oneof"`
}
type DNSProfile_SafeBrowsingBlockingModeNullIp struct {
SafeBrowsingBlockingModeNullIp *BlockingModeNullIP `protobuf:"bytes,32,opt,name=safe_browsing_blocking_mode_null_ip,json=safeBrowsingBlockingModeNullIp,proto3,oneof"`
}
type DNSProfile_SafeBrowsingBlockingModeRefused struct {
SafeBrowsingBlockingModeRefused *BlockingModeREFUSED `protobuf:"bytes,33,opt,name=safe_browsing_blocking_mode_refused,json=safeBrowsingBlockingModeRefused,proto3,oneof"`
}
func (*DNSProfile_SafeBrowsingBlockingModeCustomIp) isDNSProfile_SafeBrowsingBlockingMode() {}
func (*DNSProfile_SafeBrowsingBlockingModeNxdomain) isDNSProfile_SafeBrowsingBlockingMode() {}
func (*DNSProfile_SafeBrowsingBlockingModeNullIp) isDNSProfile_SafeBrowsingBlockingMode() {}
func (*DNSProfile_SafeBrowsingBlockingModeRefused) isDNSProfile_SafeBrowsingBlockingMode() {}
type DeviceSettingsChange struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Change:
@@ -1301,9 +1493,20 @@ func (x *RuleListsSettings) GetIds() []string {
return nil
}
// *
// Message BlockingModeCustomIP contains custom IP addresses typically leading
// to a blocking page.
type BlockingModeCustomIP struct {
state protoimpl.MessageState `protogen:"open.v1"`
// *
// Field ipv4 defines the IPv4 address to use to respond to a blocked
// request. If absent, blocked A requests are responded with a NODATA
// response.
Ipv4 []byte `protobuf:"bytes,1,opt,name=ipv4,proto3" json:"ipv4,omitempty"`
// *
// Field ipv6 defines the IPv6 address to use to respond to a blocked
// request. If absent, blocked AAAA requests are responded with a NODATA
// response.
Ipv6 []byte `protobuf:"bytes,2,opt,name=ipv6,proto3" json:"ipv6,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -2819,40 +3022,49 @@ const file_dns_proto_rawDesc = "" +
"\x1cGlobalAccessSettingsResponse\x12+\n" +
"\bstandard\x18\x01 \x01(\v2\x0f.AccessSettingsR\bstandard\"M\n" +
"\x12DNSProfilesRequest\x127\n" +
"\tsync_time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\bsyncTime\"\xc3\n" +
"\tsync_time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\bsyncTime\"\xf7\x10\n" +
"\n" +
"\n" +
"DNSProfile\x12\x15\n" +
"\x06dns_id\x18\x01 \x01(\tR\x05dnsId\x12+\n" +
"\x11filtering_enabled\x18\x02 \x01(\bR\x10filteringEnabled\x12*\n" +
"\x11query_log_enabled\x18\x03 \x01(\bR\x0fqueryLogEnabled\x12\x18\n" +
"\adeleted\x18\x04 \x01(\bR\adeleted\x12:\n" +
"DNSProfile\x12:\n" +
"\rsafe_browsing\x18\x05 \x01(\v2\x15.SafeBrowsingSettingsR\fsafeBrowsing\x12-\n" +
"\bparental\x18\x06 \x01(\v2\x11.ParentalSettingsR\bparental\x121\n" +
"\n" +
"rule_lists\x18\a \x01(\v2\x12.RuleListsSettingsR\truleLists\x12)\n" +
"\adevices\x18\b \x03(\v2\x0f.DeviceSettingsR\adevices\x12!\n" +
"\fcustom_rules\x18\t \x03(\tR\vcustomRules\x12M\n" +
"rule_lists\x18\a \x01(\v2\x12.RuleListsSettingsR\truleLists\x12M\n" +
"\x15filtered_response_ttl\x18\n" +
" \x01(\v2\x19.google.protobuf.DurationR\x13filteredResponseTtl\x12.\n" +
"\x13block_private_relay\x18\v \x01(\bR\x11blockPrivateRelay\x120\n" +
"\x14block_firefox_canary\x18\f \x01(\bR\x12blockFirefoxCanary\x12N\n" +
" \x01(\v2\x19.google.protobuf.DurationR\x13filteredResponseTtl\x12'\n" +
"\x06access\x18\x12 \x01(\v2\x0f.AccessSettingsR\x06access\x121\n" +
"\n" +
"rate_limit\x18\x14 \x01(\v2\x12.RateLimitSettingsR\trateLimit\x12:\n" +
"\rcustom_domain\x18\x16 \x01(\v2\x15.CustomDomainSettingsR\fcustomDomain\x12N\n" +
"\x17blocking_mode_custom_ip\x18\r \x01(\v2\x15.BlockingModeCustomIPH\x00R\x14blockingModeCustomIp\x12M\n" +
"\x16blocking_mode_nxdomain\x18\x0e \x01(\v2\x15.BlockingModeNXDOMAINH\x00R\x14blockingModeNxdomain\x12H\n" +
"\x15blocking_mode_null_ip\x18\x0f \x01(\v2\x13.BlockingModeNullIPH\x00R\x12blockingModeNullIp\x12J\n" +
"\x15blocking_mode_refused\x18\x10 \x01(\v2\x14.BlockingModeREFUSEDH\x00R\x13blockingModeRefused\x12$\n" +
"\x0eip_log_enabled\x18\x11 \x01(\bR\fipLogEnabled\x12'\n" +
"\x06access\x18\x12 \x01(\v2\x0f.AccessSettingsR\x06access\x120\n" +
"\x14auto_devices_enabled\x18\x13 \x01(\bR\x12autoDevicesEnabled\x121\n" +
"\x15blocking_mode_refused\x18\x10 \x01(\v2\x14.BlockingModeREFUSEDH\x00R\x13blockingModeRefused\x12Y\n" +
"\x1dadult_blocking_mode_custom_ip\x18\x1a \x01(\v2\x15.BlockingModeCustomIPH\x01R\x19adultBlockingModeCustomIp\x12X\n" +
"\x1cadult_blocking_mode_nxdomain\x18\x1b \x01(\v2\x15.BlockingModeNXDOMAINH\x01R\x19adultBlockingModeNxdomain\x12S\n" +
"\x1badult_blocking_mode_null_ip\x18\x1c \x01(\v2\x13.BlockingModeNullIPH\x01R\x17adultBlockingModeNullIp\x12U\n" +
"\x1badult_blocking_mode_refused\x18\x1d \x01(\v2\x14.BlockingModeREFUSEDH\x01R\x18adultBlockingModeRefused\x12h\n" +
"%safe_browsing_blocking_mode_custom_ip\x18\x1e \x01(\v2\x15.BlockingModeCustomIPH\x02R safeBrowsingBlockingModeCustomIp\x12g\n" +
"$safe_browsing_blocking_mode_nxdomain\x18\x1f \x01(\v2\x15.BlockingModeNXDOMAINH\x02R safeBrowsingBlockingModeNxdomain\x12b\n" +
"#safe_browsing_blocking_mode_null_ip\x18 \x01(\v2\x13.BlockingModeNullIPH\x02R\x1esafeBrowsingBlockingModeNullIp\x12d\n" +
"#safe_browsing_blocking_mode_refused\x18! \x01(\v2\x14.BlockingModeREFUSEDH\x02R\x1fsafeBrowsingBlockingModeRefused\x12\x15\n" +
"\x06dns_id\x18\x01 \x01(\tR\x05dnsId\x12\x1d\n" +
"\n" +
"rate_limit\x18\x14 \x01(\v2\x12.RateLimitSettingsR\trateLimit\x122\n" +
"\x15block_chrome_prefetch\x18\x15 \x01(\bR\x13blockChromePrefetch\x12:\n" +
"\rcustom_domain\x18\x16 \x01(\v2\x15.CustomDomainSettingsR\fcustomDomain\x12\x1d\n" +
"\n" +
"account_id\x18\x17 \x01(\tR\taccountId\x12<\n" +
"\x0edevice_changes\x18\x18 \x03(\v2\x15.DeviceSettingsChangeR\rdeviceChanges\x12G\n" +
"account_id\x18\x17 \x01(\tR\taccountId\x12)\n" +
"\adevices\x18\b \x03(\v2\x0f.DeviceSettingsR\adevices\x12<\n" +
"\x0edevice_changes\x18\x18 \x03(\v2\x15.DeviceSettingsChangeR\rdeviceChanges\x12!\n" +
"\fcustom_rules\x18\t \x03(\tR\vcustomRules\x12+\n" +
"\x11filtering_enabled\x18\x02 \x01(\bR\x10filteringEnabled\x12*\n" +
"\x11query_log_enabled\x18\x03 \x01(\bR\x0fqueryLogEnabled\x12\x18\n" +
"\adeleted\x18\x04 \x01(\bR\adeleted\x12.\n" +
"\x13block_private_relay\x18\v \x01(\bR\x11blockPrivateRelay\x120\n" +
"\x14block_firefox_canary\x18\f \x01(\bR\x12blockFirefoxCanary\x12$\n" +
"\x0eip_log_enabled\x18\x11 \x01(\bR\fipLogEnabled\x120\n" +
"\x14auto_devices_enabled\x18\x13 \x01(\bR\x12autoDevicesEnabled\x122\n" +
"\x15block_chrome_prefetch\x18\x15 \x01(\bR\x13blockChromePrefetch\x12G\n" +
" standard_access_settings_enabled\x18\x19 \x01(\bR\x1dstandardAccessSettingsEnabledB\x0f\n" +
"\rblocking_mode\"\xf6\x01\n" +
"\rblocking_modeB\x15\n" +
"\x13adult_blocking_modeB\x1d\n" +
"\x1bsafe_browsing_blocking_mode\"\xf6\x01\n" +
"\x14DeviceSettingsChange\x129\n" +
"\adeleted\x18\x01 \x01(\v2\x1d.DeviceSettingsChange.DeletedH\x00R\adeleted\x12<\n" +
"\bupserted\x18\x02 \x01(\v2\x1e.DeviceSettingsChange.UpsertedH\x00R\bupserted\x1a&\n" +
@@ -3093,70 +3305,78 @@ var file_dns_proto_depIdxs = []int32{
10, // 3: DNSProfile.safe_browsing:type_name -> SafeBrowsingSettings
12, // 4: DNSProfile.parental:type_name -> ParentalSettings
16, // 5: DNSProfile.rule_lists:type_name -> RuleListsSettings
11, // 6: DNSProfile.devices:type_name -> DeviceSettings
47, // 7: DNSProfile.filtered_response_ttl:type_name -> google.protobuf.Duration
17, // 8: DNSProfile.blocking_mode_custom_ip:type_name -> BlockingModeCustomIP
18, // 9: DNSProfile.blocking_mode_nxdomain:type_name -> BlockingModeNXDOMAIN
19, // 10: DNSProfile.blocking_mode_null_ip:type_name -> BlockingModeNullIP
20, // 11: DNSProfile.blocking_mode_refused:type_name -> BlockingModeREFUSED
22, // 12: DNSProfile.access:type_name -> AccessSettings
32, // 13: DNSProfile.rate_limit:type_name -> RateLimitSettings
8, // 14: DNSProfile.custom_domain:type_name -> CustomDomainSettings
7, // 15: DNSProfile.device_changes:type_name -> DeviceSettingsChange
42, // 16: DeviceSettingsChange.deleted:type_name -> DeviceSettingsChange.Deleted
43, // 17: DeviceSettingsChange.upserted:type_name -> DeviceSettingsChange.Upserted
9, // 18: CustomDomainSettings.domains:type_name -> CustomDomain
44, // 19: CustomDomain.pending:type_name -> CustomDomain.Pending
45, // 20: CustomDomain.current:type_name -> CustomDomain.Current
24, // 21: DeviceSettings.authentication:type_name -> AuthenticationSettings
13, // 22: ParentalSettings.schedule:type_name -> ScheduleSettings
14, // 23: ScheduleSettings.weekly_range:type_name -> WeeklyRange
15, // 24: WeeklyRange.mon:type_name -> DayRange
15, // 25: WeeklyRange.tue:type_name -> DayRange
15, // 26: WeeklyRange.wed:type_name -> DayRange
15, // 27: WeeklyRange.thu:type_name -> DayRange
15, // 28: WeeklyRange.fri:type_name -> DayRange
15, // 29: WeeklyRange.sat:type_name -> DayRange
15, // 30: WeeklyRange.sun:type_name -> DayRange
47, // 31: DayRange.start:type_name -> google.protobuf.Duration
47, // 32: DayRange.end:type_name -> google.protobuf.Duration
46, // 33: DeviceBillingStat.last_activity_time:type_name -> google.protobuf.Timestamp
23, // 34: AccessSettings.allowlist_cidr:type_name -> CidrRange
23, // 35: AccessSettings.blocklist_cidr:type_name -> CidrRange
0, // 36: CreateDeviceRequest.device_type:type_name -> DeviceType
11, // 37: CreateDeviceResponse.device:type_name -> DeviceSettings
47, // 38: RateLimitedError.retry_delay:type_name -> google.protobuf.Duration
23, // 39: RateLimitSettings.client_cidr:type_name -> CidrRange
48, // 40: RemoteKVGetResponse.empty:type_name -> google.protobuf.Empty
47, // 41: RemoteKVSetRequest.ttl:type_name -> google.protobuf.Duration
41, // 42: SessionTicketResponse.tickets:type_name -> SessionTicket
11, // 43: DeviceSettingsChange.Upserted.device:type_name -> DeviceSettings
46, // 44: CustomDomain.Pending.expire:type_name -> google.protobuf.Timestamp
46, // 45: CustomDomain.Current.not_before:type_name -> google.protobuf.Timestamp
46, // 46: CustomDomain.Current.not_after:type_name -> google.protobuf.Timestamp
5, // 47: DNSService.getDNSProfiles:input_type -> DNSProfilesRequest
21, // 48: DNSService.saveDevicesBillingStat:input_type -> DeviceBillingStat
25, // 49: DNSService.createDeviceByHumanId:input_type -> CreateDeviceRequest
1, // 50: RateLimitService.getRateLimitSettings:input_type -> RateLimitSettingsRequest
3, // 51: RateLimitService.getGlobalAccessSettings:input_type -> GlobalAccessSettingsRequest
33, // 52: RemoteKVService.get:input_type -> RemoteKVGetRequest
35, // 53: RemoteKVService.set:input_type -> RemoteKVSetRequest
37, // 54: CustomDomainService.getCustomDomainCertificate:input_type -> CustomDomainCertificateRequest
39, // 55: SessionTicketService.getSessionTickets:input_type -> SessionTicketRequest
6, // 56: DNSService.getDNSProfiles:output_type -> DNSProfile
48, // 57: DNSService.saveDevicesBillingStat:output_type -> google.protobuf.Empty
26, // 58: DNSService.createDeviceByHumanId:output_type -> CreateDeviceResponse
2, // 59: RateLimitService.getRateLimitSettings:output_type -> RateLimitSettingsResponse
4, // 60: RateLimitService.getGlobalAccessSettings:output_type -> GlobalAccessSettingsResponse
34, // 61: RemoteKVService.get:output_type -> RemoteKVGetResponse
36, // 62: RemoteKVService.set:output_type -> RemoteKVSetResponse
38, // 63: CustomDomainService.getCustomDomainCertificate:output_type -> CustomDomainCertificateResponse
40, // 64: SessionTicketService.getSessionTickets:output_type -> SessionTicketResponse
56, // [56:65] is the sub-list for method output_type
47, // [47:56] is the sub-list for method input_type
47, // [47:47] is the sub-list for extension type_name
47, // [47:47] is the sub-list for extension extendee
0, // [0:47] is the sub-list for field type_name
47, // 6: DNSProfile.filtered_response_ttl:type_name -> google.protobuf.Duration
22, // 7: DNSProfile.access:type_name -> AccessSettings
32, // 8: DNSProfile.rate_limit:type_name -> RateLimitSettings
8, // 9: DNSProfile.custom_domain:type_name -> CustomDomainSettings
17, // 10: DNSProfile.blocking_mode_custom_ip:type_name -> BlockingModeCustomIP
18, // 11: DNSProfile.blocking_mode_nxdomain:type_name -> BlockingModeNXDOMAIN
19, // 12: DNSProfile.blocking_mode_null_ip:type_name -> BlockingModeNullIP
20, // 13: DNSProfile.blocking_mode_refused:type_name -> BlockingModeREFUSED
17, // 14: DNSProfile.adult_blocking_mode_custom_ip:type_name -> BlockingModeCustomIP
18, // 15: DNSProfile.adult_blocking_mode_nxdomain:type_name -> BlockingModeNXDOMAIN
19, // 16: DNSProfile.adult_blocking_mode_null_ip:type_name -> BlockingModeNullIP
20, // 17: DNSProfile.adult_blocking_mode_refused:type_name -> BlockingModeREFUSED
17, // 18: DNSProfile.safe_browsing_blocking_mode_custom_ip:type_name -> BlockingModeCustomIP
18, // 19: DNSProfile.safe_browsing_blocking_mode_nxdomain:type_name -> BlockingModeNXDOMAIN
19, // 20: DNSProfile.safe_browsing_blocking_mode_null_ip:type_name -> BlockingModeNullIP
20, // 21: DNSProfile.safe_browsing_blocking_mode_refused:type_name -> BlockingModeREFUSED
11, // 22: DNSProfile.devices:type_name -> DeviceSettings
7, // 23: DNSProfile.device_changes:type_name -> DeviceSettingsChange
42, // 24: DeviceSettingsChange.deleted:type_name -> DeviceSettingsChange.Deleted
43, // 25: DeviceSettingsChange.upserted:type_name -> DeviceSettingsChange.Upserted
9, // 26: CustomDomainSettings.domains:type_name -> CustomDomain
44, // 27: CustomDomain.pending:type_name -> CustomDomain.Pending
45, // 28: CustomDomain.current:type_name -> CustomDomain.Current
24, // 29: DeviceSettings.authentication:type_name -> AuthenticationSettings
13, // 30: ParentalSettings.schedule:type_name -> ScheduleSettings
14, // 31: ScheduleSettings.weekly_range:type_name -> WeeklyRange
15, // 32: WeeklyRange.mon:type_name -> DayRange
15, // 33: WeeklyRange.tue:type_name -> DayRange
15, // 34: WeeklyRange.wed:type_name -> DayRange
15, // 35: WeeklyRange.thu:type_name -> DayRange
15, // 36: WeeklyRange.fri:type_name -> DayRange
15, // 37: WeeklyRange.sat:type_name -> DayRange
15, // 38: WeeklyRange.sun:type_name -> DayRange
47, // 39: DayRange.start:type_name -> google.protobuf.Duration
47, // 40: DayRange.end:type_name -> google.protobuf.Duration
46, // 41: DeviceBillingStat.last_activity_time:type_name -> google.protobuf.Timestamp
23, // 42: AccessSettings.allowlist_cidr:type_name -> CidrRange
23, // 43: AccessSettings.blocklist_cidr:type_name -> CidrRange
0, // 44: CreateDeviceRequest.device_type:type_name -> DeviceType
11, // 45: CreateDeviceResponse.device:type_name -> DeviceSettings
47, // 46: RateLimitedError.retry_delay:type_name -> google.protobuf.Duration
23, // 47: RateLimitSettings.client_cidr:type_name -> CidrRange
48, // 48: RemoteKVGetResponse.empty:type_name -> google.protobuf.Empty
47, // 49: RemoteKVSetRequest.ttl:type_name -> google.protobuf.Duration
41, // 50: SessionTicketResponse.tickets:type_name -> SessionTicket
11, // 51: DeviceSettingsChange.Upserted.device:type_name -> DeviceSettings
46, // 52: CustomDomain.Pending.expire:type_name -> google.protobuf.Timestamp
46, // 53: CustomDomain.Current.not_before:type_name -> google.protobuf.Timestamp
46, // 54: CustomDomain.Current.not_after:type_name -> google.protobuf.Timestamp
5, // 55: DNSService.getDNSProfiles:input_type -> DNSProfilesRequest
21, // 56: DNSService.saveDevicesBillingStat:input_type -> DeviceBillingStat
25, // 57: DNSService.createDeviceByHumanId:input_type -> CreateDeviceRequest
1, // 58: RateLimitService.getRateLimitSettings:input_type -> RateLimitSettingsRequest
3, // 59: RateLimitService.getGlobalAccessSettings:input_type -> GlobalAccessSettingsRequest
33, // 60: RemoteKVService.get:input_type -> RemoteKVGetRequest
35, // 61: RemoteKVService.set:input_type -> RemoteKVSetRequest
37, // 62: CustomDomainService.getCustomDomainCertificate:input_type -> CustomDomainCertificateRequest
39, // 63: SessionTicketService.getSessionTickets:input_type -> SessionTicketRequest
6, // 64: DNSService.getDNSProfiles:output_type -> DNSProfile
48, // 65: DNSService.saveDevicesBillingStat:output_type -> google.protobuf.Empty
26, // 66: DNSService.createDeviceByHumanId:output_type -> CreateDeviceResponse
2, // 67: RateLimitService.getRateLimitSettings:output_type -> RateLimitSettingsResponse
4, // 68: RateLimitService.getGlobalAccessSettings:output_type -> GlobalAccessSettingsResponse
34, // 69: RemoteKVService.get:output_type -> RemoteKVGetResponse
36, // 70: RemoteKVService.set:output_type -> RemoteKVSetResponse
38, // 71: CustomDomainService.getCustomDomainCertificate:output_type -> CustomDomainCertificateResponse
40, // 72: SessionTicketService.getSessionTickets:output_type -> SessionTicketResponse
64, // [64:73] is the sub-list for method output_type
55, // [55:64] is the sub-list for method input_type
55, // [55:55] is the sub-list for extension type_name
55, // [55:55] is the sub-list for extension extendee
0, // [0:55] is the sub-list for field type_name
}
func init() { file_dns_proto_init() }
@@ -3169,6 +3389,14 @@ func file_dns_proto_init() {
(*DNSProfile_BlockingModeNxdomain)(nil),
(*DNSProfile_BlockingModeNullIp)(nil),
(*DNSProfile_BlockingModeRefused)(nil),
(*DNSProfile_AdultBlockingModeCustomIp)(nil),
(*DNSProfile_AdultBlockingModeNxdomain)(nil),
(*DNSProfile_AdultBlockingModeNullIp)(nil),
(*DNSProfile_AdultBlockingModeRefused)(nil),
(*DNSProfile_SafeBrowsingBlockingModeCustomIp)(nil),
(*DNSProfile_SafeBrowsingBlockingModeNxdomain)(nil),
(*DNSProfile_SafeBrowsingBlockingModeNullIp)(nil),
(*DNSProfile_SafeBrowsingBlockingModeRefused)(nil),
}
file_dns_proto_msgTypes[6].OneofWrappers = []any{
(*DeviceSettingsChange_Deleted_)(nil),

View File

@@ -111,44 +111,117 @@ message DNSProfilesRequest {
google.protobuf.Timestamp sync_time = 1;
}
/**
* Message DNSProfile contains both the data of the account and the data of the
* DNS server.
*
* The fields are ordered in a way that optimizes the generated structures'
* layouts.
*
* TODO(a.garipov): Expand the field documentation.
*/
message DNSProfile {
string dns_id = 1;
bool filtering_enabled = 2;
bool query_log_enabled = 3;
bool deleted = 4;
// Message fields
SafeBrowsingSettings safe_browsing = 5;
ParentalSettings parental = 6;
RuleListsSettings rule_lists = 7;
/*
* Field devices contains the COMPLETE list of all devices in the profile.
* Field device_changes contains only the list of changes that have happened
* to the profile's devices since sync_time.
*
* devices and device_changes MUST NOT be set at the same time, but they MAY
* both be empty.
*
* device_changes MUST NOT contain multiple changes for the same device.
*/
repeated DeviceSettings devices = 8;
repeated string custom_rules = 9;
google.protobuf.Duration filtered_response_ttl = 10;
bool block_private_relay = 11;
bool block_firefox_canary = 12;
AccessSettings access = 18;
RateLimitSettings rate_limit = 20;
CustomDomainSettings custom_domain = 22;
// One-of fields
/**
* Field blocking_mode defines the blocking mode for general rule-list based
* filtering. If field deleted is false, field blocking_mode MUST be
* present.
*/
oneof blocking_mode {
BlockingModeCustomIP blocking_mode_custom_ip = 13;
BlockingModeNXDOMAIN blocking_mode_nxdomain = 14;
BlockingModeNullIP blocking_mode_null_ip = 15;
BlockingModeREFUSED blocking_mode_refused = 16;
}
bool ip_log_enabled = 17;
AccessSettings access = 18;
bool auto_devices_enabled = 19;
RateLimitSettings rate_limit = 20;
bool block_chrome_prefetch = 21;
CustomDomainSettings custom_domain = 22;
/**
* Field adult_blocking_mode defines the blocking mode for the adult-content
* filter. If absent, the default is used.
*/
oneof adult_blocking_mode {
BlockingModeCustomIP adult_blocking_mode_custom_ip = 26;
BlockingModeNXDOMAIN adult_blocking_mode_nxdomain = 27;
BlockingModeNullIP adult_blocking_mode_null_ip = 28;
BlockingModeREFUSED adult_blocking_mode_refused = 29;
}
/**
* Field safe_browsing_blocking_mode defines the blocking mode for the
* safe-browsing filter. If absent, the default is used.
*/
oneof safe_browsing_blocking_mode {
BlockingModeCustomIP safe_browsing_blocking_mode_custom_ip = 30;
BlockingModeNXDOMAIN safe_browsing_blocking_mode_nxdomain = 31;
BlockingModeNullIP safe_browsing_blocking_mode_null_ip = 32;
BlockingModeREFUSED safe_browsing_blocking_mode_refused = 33;
}
// String fields
/**
* Field dns_id is the ID of the DNS server. Not to be confused with the ID
* of the account, see account_id below. dns_id MUST be present.
*/
string dns_id = 1;
/**
* Field account_id is the ID of the account to which this DNS server
* belongs. If field deleted is false, account_id MUST be present.
*/
string account_id = 23;
// Repeated fields
/**
* Field devices contains the complete list of all devices in the profile, if
* any. Fields devices and device_changes MUST NOT be set at the same time,
* but they MAY both be empty. Field devices MUST NOT contain duplicates.
*/
repeated DeviceSettings devices = 8;
/**
* Field device_changes contains only the list of changes that have happened
* to the profile's devices since sync_time, if any. Fields devices and
* device_changes MUST NOT be set at the same time, but they MAY both be
* empty. Field device_changes MUST NOT contain multiple changes for the
* same device.
*/
repeated DeviceSettingsChange device_changes = 24;
/**
* Field custom_rules contains custom filtering rules for this DNS server, if
* any. All items MUST contain only valid UTF-8 characters and have the size
* less than or equal to UTF-8 1024 characters (not bytes).
*/
repeated string custom_rules = 9;
// Boolean fields
bool filtering_enabled = 2;
bool query_log_enabled = 3;
/**
* Field deleted, if true, means that this DNS server has been deleted. All
* other fields except dns_id SHOULD be absent.
*/
bool deleted = 4;
bool block_private_relay = 11;
bool block_firefox_canary = 12;
bool ip_log_enabled = 17;
bool auto_devices_enabled = 19;
bool block_chrome_prefetch = 21;
bool standard_access_settings_enabled = 25;
}
@@ -248,8 +321,23 @@ message RuleListsSettings {
repeated string ids = 2;
}
/**
* Message BlockingModeCustomIP contains custom IP addresses typically leading
* to a blocking page.
*/
message BlockingModeCustomIP {
/**
* Field ipv4 defines the IPv4 address to use to respond to a blocked
* request. If absent, blocked A requests are responded with a NODATA
* response.
*/
bytes ipv4 = 1;
/**
* Field ipv6 defines the IPv6 address to use to respond to a blocked
* request. If absent, blocked AAAA requests are responded with a NODATA
* response.
*/
bytes ipv6 = 2;
}

View File

@@ -359,9 +359,82 @@ func (pbm *BlockingModeCustomIP) toInternal() (m dnsmsg.BlockingMode, err error)
return custom, nil
}
// adultBlockingModeToInternal converts a protobuf adult blocking-mode sum-type
// to an internal one. If pbm is nil, it returns nil.
func adultBlockingModeToInternal(
pbm isDNSProfile_AdultBlockingMode,
) (m dnsmsg.BlockingMode, err error) {
switch pbm := pbm.(type) {
case nil:
return nil, nil
case *DNSProfile_AdultBlockingModeCustomIp:
return pbm.AdultBlockingModeCustomIp.toInternal()
case *DNSProfile_AdultBlockingModeNxdomain:
return &dnsmsg.BlockingModeNXDOMAIN{}, nil
case *DNSProfile_AdultBlockingModeNullIp:
return &dnsmsg.BlockingModeNullIP{}, nil
case *DNSProfile_AdultBlockingModeRefused:
return &dnsmsg.BlockingModeREFUSED{}, nil
default:
// Consider unhandled type-switch cases programmer errors.
return nil, fmt.Errorf("bad pb blocking mode %T(%[1]v)", pbm)
}
}
// safeBrowsingBlockingModeToInternal converts a protobuf safe browsing
// blocking-mode sum-type to an internal one. If pbm is nil, it returns nil.
func safeBrowsingBlockingModeToInternal(
pbm isDNSProfile_SafeBrowsingBlockingMode,
) (m dnsmsg.BlockingMode, err error) {
switch pbm := pbm.(type) {
case nil:
return nil, nil
case *DNSProfile_SafeBrowsingBlockingModeCustomIp:
return pbm.SafeBrowsingBlockingModeCustomIp.toInternal()
case *DNSProfile_SafeBrowsingBlockingModeNxdomain:
return &dnsmsg.BlockingModeNXDOMAIN{}, nil
case *DNSProfile_SafeBrowsingBlockingModeNullIp:
return &dnsmsg.BlockingModeNullIP{}, nil
case *DNSProfile_SafeBrowsingBlockingModeRefused:
return &dnsmsg.BlockingModeREFUSED{}, nil
default:
// Consider unhandled type-switch cases programmer errors.
return nil, fmt.Errorf("bad pb blocking mode %T(%[1]v)", pbm)
}
}
// blockingModesToInternal converts a protobuf blocking-mode sum-types to
// internal blocking-mode objects.
func blockingModesToInternal(p *DNSProfile) (
m dnsmsg.BlockingMode,
adultBlockingMode dnsmsg.BlockingMode,
safeBrowsingBlockingMode dnsmsg.BlockingMode,
err error,
) {
m, err = blockingModeToInternal(p.BlockingMode)
if err != nil {
return nil, nil, nil, fmt.Errorf("blocking mode: %w", err)
}
adultBlockingMode, err = adultBlockingModeToInternal(p.AdultBlockingMode)
if err != nil {
return nil, nil, nil, fmt.Errorf("adult blocking mode: %w", err)
}
safeBrowsingBlockingMode, err = safeBrowsingBlockingModeToInternal(p.SafeBrowsingBlockingMode)
if err != nil {
return nil, nil, nil, fmt.Errorf("safe browsing blocking mode: %w", err)
}
return m, adultBlockingMode, safeBrowsingBlockingMode, nil
}
// blockingModeToInternal converts a protobuf blocking-mode sum-type to an
// internal one. If pbm is nil, blockingModeToInternal returns a null-IP
// blocking mode.
//
// TODO(d.kolyshev): DRY with adultBlockingModeToInternal and
// safeBrowsingBlockingModeToInternal.
func blockingModeToInternal(pbm isDNSProfile_BlockingMode) (m dnsmsg.BlockingMode, err error) {
switch pbm := pbm.(type) {
case nil:

View File

@@ -230,14 +230,10 @@ func (s *ProfileStorage) newProfile(
return nil, nil, nil, errors.ErrNoValue
}
parental, err := p.Parental.toInternal(ctx, s.errColl, s.logger)
m, adultBlockingMode, safeBrowsingBlockingMode, err := blockingModesToInternal(p)
if err != nil {
return nil, nil, nil, fmt.Errorf("parental: %w", err)
}
m, err := blockingModeToInternal(p.BlockingMode)
if err != nil {
return nil, nil, nil, fmt.Errorf("blocking mode: %w", err)
// Do not wrap the error, because it's informative enough as is.
return nil, nil, nil, err
}
devChg = &profiledb.StorageDeviceChange{}
@@ -282,12 +278,62 @@ func (s *ProfileStorage) newProfile(
return nil, nil, nil, fmt.Errorf("account id: %w", err)
}
filterConf, err := s.newFilterConfig(ctx, p, profID, s.logger, s.errColl)
if err != nil {
return nil, nil, nil, fmt.Errorf("filter config: %w", err)
}
var fltRespTTL time.Duration
if respTTL := p.FilteredResponseTtl; respTTL != nil {
fltRespTTL = respTTL.AsDuration()
}
customRules := rulesToInternal(ctx, p.CustomRules, s.errColl, s.logger)
accessProf := p.Access.toInternal(
ctx,
s.logger,
s.errColl,
s.profAccessCons,
p.StandardAccessSettingsEnabled,
)
return &agd.Profile{
CustomDomains: p.CustomDomain.toInternal(ctx, s.errColl, s.logger),
DeviceIDs: container.NewMapSet(deviceIDs...),
FilterConfig: filterConf,
Access: accessProf,
AdultBlockingMode: adultBlockingMode,
BlockingMode: m,
SafeBrowsingBlockingMode: safeBrowsingBlockingMode,
Ratelimiter: p.RateLimit.toInternal(ctx, s.errColl, s.logger, s.respSzEst),
AccountID: accID,
ID: profID,
FilteredResponseTTL: fltRespTTL,
AutoDevicesEnabled: p.AutoDevicesEnabled,
BlockChromePrefetch: p.BlockChromePrefetch,
BlockFirefoxCanary: p.BlockFirefoxCanary,
BlockPrivateRelay: p.BlockPrivateRelay,
Deleted: p.Deleted,
FilteringEnabled: p.FilteringEnabled,
IPLogEnabled: p.IpLogEnabled,
QueryLogEnabled: p.QueryLogEnabled,
}, devices, devChg, nil
}
// newFilterConfig creates a new filter configuration from the protobuf profile.
// p, logger and errColl must not be nil.
func (s *ProfileStorage) newFilterConfig(
ctx context.Context,
p *DNSProfile,
profID agd.ProfileID,
logger *slog.Logger,
errColl errcoll.Interface,
) (conf *filter.ConfigClient, err error) {
parental, err := p.Parental.toInternal(ctx, s.errColl, s.logger)
if err != nil {
return nil, fmt.Errorf("parental: %w", err)
}
customRules := rulesToInternal(ctx, p.CustomRules, errColl, logger)
customEnabled := len(customRules) > 0
var customFilter filter.Custom
@@ -304,38 +350,12 @@ func (s *ProfileStorage) newProfile(
Enabled: customEnabled,
}
accessProf := p.Access.toInternal(
ctx,
s.logger,
s.errColl,
s.profAccessCons,
p.StandardAccessSettingsEnabled,
)
return &agd.Profile{
CustomDomains: p.CustomDomain.toInternal(ctx, s.errColl, s.logger),
DeviceIDs: container.NewMapSet(deviceIDs...),
FilterConfig: &filter.ConfigClient{
return &filter.ConfigClient{
Custom: customConf,
Parental: parental,
RuleList: p.RuleLists.toInternal(ctx, s.errColl, s.logger),
RuleList: p.RuleLists.toInternal(ctx, errColl, logger),
SafeBrowsing: p.SafeBrowsing.toInternal(),
},
Access: accessProf,
BlockingMode: m,
Ratelimiter: p.RateLimit.toInternal(ctx, s.errColl, s.logger, s.respSzEst),
AccountID: accID,
ID: profID,
FilteredResponseTTL: fltRespTTL,
AutoDevicesEnabled: p.AutoDevicesEnabled,
BlockChromePrefetch: p.BlockChromePrefetch,
BlockFirefoxCanary: p.BlockFirefoxCanary,
BlockPrivateRelay: p.BlockPrivateRelay,
Deleted: p.Deleted,
FilteringEnabled: p.FilteringEnabled,
IPLogEnabled: p.IpLogEnabled,
QueryLogEnabled: p.QueryLogEnabled,
}, devices, devChg, nil
}, nil
}
// toProtobuf converts a storage request structure into the protobuf structure.

View File

@@ -184,7 +184,7 @@ func TestProfileStorage_NewProfile(t *testing.T) {
_, _, _, err := profileStorage.newProfile(ctx, dp, true)
testutil.AssertErrorMsg(
t,
"parental: pause schedule: loading timezone: unknown time zone invalid",
"filter config: parental: pause schedule: loading timezone: unknown time zone invalid",
err,
)
})
@@ -201,7 +201,7 @@ func TestProfileStorage_NewProfile(t *testing.T) {
_, _, _, err := profileStorage.newProfile(ctx, dp, true)
testutil.AssertErrorMsg(
t,
"parental: pause schedule: weekday Sunday: end: out of range: 1 is less than start 16",
"filter config: parental: pause schedule: weekday Sunday: end: out of range: 1 is less than start 16",
err,
)
})
@@ -210,7 +210,7 @@ func TestProfileStorage_NewProfile(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
bm := dp.BlockingMode.(*DNSProfile_BlockingModeCustomIp)
bm := testutil.RequireTypeAssert[*DNSProfile_BlockingModeCustomIp](t, dp.BlockingMode)
bm.BlockingModeCustomIp.Ipv4 = []byte("1")
_, _, _, err := profileStorage.newProfile(ctx, dp, true)
@@ -221,7 +221,7 @@ func TestProfileStorage_NewProfile(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
bm := dp.BlockingMode.(*DNSProfile_BlockingModeCustomIp)
bm := testutil.RequireTypeAssert[*DNSProfile_BlockingModeCustomIp](t, dp.BlockingMode)
bm.BlockingModeCustomIp.Ipv6 = []byte("1")
_, _, _, err := profileStorage.newProfile(ctx, dp, true)
@@ -232,7 +232,7 @@ func TestProfileStorage_NewProfile(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
bm := dp.BlockingMode.(*DNSProfile_BlockingModeCustomIp)
bm := testutil.RequireTypeAssert[*DNSProfile_BlockingModeCustomIp](t, dp.BlockingMode)
bm.BlockingModeCustomIp.Ipv4 = nil
bm.BlockingModeCustomIp.Ipv6 = nil
@@ -258,6 +258,144 @@ func TestProfileStorage_NewProfile(t *testing.T) {
assert.Equal(t, wantDevChg, gotDevChg)
})
t.Run("inv_adult_blocking_mode_v4", func(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
bm := testutil.RequireTypeAssert[*DNSProfile_AdultBlockingModeCustomIp](
t,
dp.AdultBlockingMode,
)
bm.AdultBlockingModeCustomIp.Ipv4 = []byte("1")
_, _, _, err := profileStorage.newProfile(ctx, dp, true)
testutil.AssertErrorMsg(
t,
"adult blocking mode: bad custom ipv4: unexpected slice size",
err,
)
})
t.Run("inv_adult_blocking_mode_v6", func(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
bm := testutil.RequireTypeAssert[*DNSProfile_AdultBlockingModeCustomIp](
t,
dp.AdultBlockingMode,
)
bm.AdultBlockingModeCustomIp.Ipv6 = []byte("1")
_, _, _, err := profileStorage.newProfile(ctx, dp, true)
testutil.AssertErrorMsg(
t,
"adult blocking mode: bad custom ipv6: unexpected slice size",
err,
)
})
t.Run("nil_ips_adult_blocking_mode", func(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
bm := testutil.RequireTypeAssert[*DNSProfile_AdultBlockingModeCustomIp](
t,
dp.AdultBlockingMode,
)
bm.AdultBlockingModeCustomIp.Ipv4 = nil
bm.AdultBlockingModeCustomIp.Ipv6 = nil
_, _, _, err := profileStorage.newProfile(ctx, dp, true)
testutil.AssertErrorMsg(t, "adult blocking mode: no valid custom ips found", err)
})
t.Run("nil_adult_blocking_mode", func(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
dp.AdultBlockingMode = nil
got, gotDevices, gotDevChg, err := profileStorage.newProfile(ctx, dp, true)
require.NoError(t, err)
require.NotNil(t, got)
wantProf := newProfile(t)
wantProf.AdultBlockingMode = nil
agdtest.AssertEqualProfile(t, wantProf, got)
assert.Equal(t, newDevices(t), gotDevices)
assert.Equal(t, wantDevChg, gotDevChg)
})
t.Run("inv_safe_browsing_blocking_mode_v4", func(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
bm := testutil.RequireTypeAssert[*DNSProfile_SafeBrowsingBlockingModeCustomIp](
t,
dp.SafeBrowsingBlockingMode,
)
bm.SafeBrowsingBlockingModeCustomIp.Ipv4 = []byte("1")
_, _, _, err := profileStorage.newProfile(ctx, dp, true)
testutil.AssertErrorMsg(
t,
"safe browsing blocking mode: bad custom ipv4: unexpected slice size",
err,
)
})
t.Run("inv_safe_browsing_blocking_mode_v6", func(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
bm := testutil.RequireTypeAssert[*DNSProfile_SafeBrowsingBlockingModeCustomIp](
t,
dp.SafeBrowsingBlockingMode,
)
bm.SafeBrowsingBlockingModeCustomIp.Ipv6 = []byte("1")
_, _, _, err := profileStorage.newProfile(ctx, dp, true)
testutil.AssertErrorMsg(
t,
"safe browsing blocking mode: bad custom ipv6: unexpected slice size",
err,
)
})
t.Run("nil_ips_safe_browsing_blocking_mode", func(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
bm := testutil.RequireTypeAssert[*DNSProfile_SafeBrowsingBlockingModeCustomIp](
t,
dp.SafeBrowsingBlockingMode,
)
bm.SafeBrowsingBlockingModeCustomIp.Ipv4 = nil
bm.SafeBrowsingBlockingModeCustomIp.Ipv6 = nil
_, _, _, err := profileStorage.newProfile(ctx, dp, true)
testutil.AssertErrorMsg(t, "safe browsing blocking mode: no valid custom ips found", err)
})
t.Run("nil_safe_browsing_blocking_mode", func(t *testing.T) {
t.Parallel()
dp := NewTestDNSProfile(t)
dp.SafeBrowsingBlockingMode = nil
got, gotDevices, gotDevChg, err := profileStorage.newProfile(ctx, dp, true)
require.NoError(t, err)
require.NotNil(t, got)
wantProf := newProfile(t)
wantProf.SafeBrowsingBlockingMode = nil
agdtest.AssertEqualProfile(t, wantProf, got)
assert.Equal(t, newDevices(t), gotDevices)
assert.Equal(t, wantDevChg, gotDevChg)
})
t.Run("nil_access", func(t *testing.T) {
t.Parallel()
@@ -457,6 +595,18 @@ func NewTestDNSProfile(tb testing.TB) (dp *DNSProfile) {
Ipv6: ipToBytes(tb, netip.MustParseAddr("1234::cdef")),
},
},
AdultBlockingMode: &DNSProfile_AdultBlockingModeCustomIp{
AdultBlockingModeCustomIp: &BlockingModeCustomIP{
Ipv4: ipToBytes(tb, netip.MustParseAddr("1.1.1.1")),
Ipv6: ipToBytes(tb, netip.MustParseAddr("1111::cdef")),
},
},
SafeBrowsingBlockingMode: &DNSProfile_SafeBrowsingBlockingModeCustomIp{
SafeBrowsingBlockingModeCustomIp: &BlockingModeCustomIP{
Ipv4: ipToBytes(tb, netip.MustParseAddr("2.2.2.2")),
Ipv6: ipToBytes(tb, netip.MustParseAddr("2222::cdef")),
},
},
Access: &AccessSettings{
AllowlistCidr: []*CidrRange{{
Address: netip.MustParseAddr("1.1.1.0").AsSlice(),
@@ -549,11 +699,21 @@ func newProfile(tb testing.TB) (p *agd.Profile) {
NewlyRegisteredDomainsEnabled: false,
}
wantAdultBlockingMode := &dnsmsg.BlockingModeCustomIP{
IPv4: []netip.Addr{netip.MustParseAddr("1.1.1.1")},
IPv6: []netip.Addr{netip.MustParseAddr("1111::cdef")},
}
wantBlockingMode := &dnsmsg.BlockingModeCustomIP{
IPv4: []netip.Addr{netip.MustParseAddr("1.2.3.4")},
IPv6: []netip.Addr{netip.MustParseAddr("1234::cdef")},
}
wantSafeBrowsingBlockingMode := &dnsmsg.BlockingModeCustomIP{
IPv4: []netip.Addr{netip.MustParseAddr("2.2.2.2")},
IPv6: []netip.Addr{netip.MustParseAddr("2222::cdef")},
}
wantAccess := TestProfileAccessConstructor.New(&access.ProfileConfig{
AllowedNets: []netip.Prefix{netip.MustParsePrefix("1.1.1.0/24")},
BlockedNets: []netip.Prefix{netip.MustParsePrefix("2.2.2.0/24")},
@@ -601,7 +761,9 @@ func newProfile(tb testing.TB) (p *agd.Profile) {
SafeBrowsing: wantSafeBrowsing,
},
Access: wantAccess,
AdultBlockingMode: wantAdultBlockingMode,
BlockingMode: wantBlockingMode,
SafeBrowsingBlockingMode: wantSafeBrowsingBlockingMode,
Ratelimiter: wantRateLimiter,
ID: TestProfileID,
DeviceIDs: container.NewMapSet(

View File

@@ -11,6 +11,7 @@ import (
"path"
"path/filepath"
"slices"
"sync"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/access"
@@ -54,6 +55,7 @@ import (
"github.com/AdguardTeam/golibs/timeutil"
"github.com/c2h5oh/datasize"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/publicsuffix"
)
// Constants that define debug identifiers for the debug HTTP service.
@@ -74,6 +76,11 @@ const (
debugIDPrefixPlugin = "plugin/"
)
// defaultSubDomainNum is default subDomainNumValue for filters.
//
// TODO(a.garipov): Make configurable and validate to fit into int.
const defaultSubDomainNum = 4
// builder contains the logic of configuring and combining together AdGuard DNS
// entities.
//
@@ -293,6 +300,15 @@ func (b *builder) initMsgCloner(ctx context.Context) (err error) {
return nil
}
// hashPrefixFilterInitFunc is a function that initializes the hash-prefix
// filter, populates the necessary [builder] fields and returns a RefreshWorker
// to update the initialized filter.
type hashPrefixFilterInitFunc func(
ctx context.Context,
maxSize datasize.ByteSize,
cacheDir string,
) (refr *service.RefreshWorker, err error)
// initHashPrefixFilters initializes the hashprefix storages and filters.
//
// [builder.initMsgCloner] must be called before this method.
@@ -301,51 +317,107 @@ func (b *builder) initHashPrefixFilters(ctx context.Context) (err error) {
maxSize := b.conf.Filters.MaxSize
cacheDir := b.env.FilterCachePath
matchers := map[string]*hashprefix.Storage{}
b.filterMtrc, err = metrics.NewFilter(b.mtrcNamespace, b.promRegisterer)
if err != nil {
return fmt.Errorf("registering filter metrics: %w", err)
}
// TODO(a.garipov): Merge the three functions below together.
err = b.initAdultBlocking(ctx, matchers, maxSize, cacheDir)
if err != nil {
return fmt.Errorf("initializing adult-blocking filter: %w", err)
initFuncs := []hashPrefixFilterInitFunc{
b.initAdultBlocking,
b.initNewRegDomains,
b.initSafeBrowsing,
}
err = b.initNewRegDomains(ctx, maxSize, cacheDir)
workers, err := b.runHashPrefixInit(ctx, initFuncs, maxSize, cacheDir)
if err != nil {
return fmt.Errorf("initializing newly-registered domain filter: %w", err)
return fmt.Errorf("initializing hash prefixes: %w", err)
}
err = b.initSafeBrowsing(ctx, matchers, maxSize, cacheDir)
if err != nil {
return fmt.Errorf("initializing safe-browsing filter: %w", err)
}
b.hashMatcher = hashprefix.NewMatcher(matchers)
b.populateHashPrefix(workers)
b.logger.DebugContext(ctx, "initialized hash prefixes")
return nil
}
// runHashPrefixInit runs the hash prefix filter initialization functions with
// the specified arguments, creating a RefreshWorker for each enabled filter.
//
// It must be called from [builder.initHashPrefixFilters].
func (b *builder) runHashPrefixInit(
ctx context.Context,
initFuncs []hashPrefixFilterInitFunc,
maxSize datasize.ByteSize,
cacheDir string,
) (workers []*service.RefreshWorker, err error) {
wg := &sync.WaitGroup{}
workers = make([]*service.RefreshWorker, len(initFuncs))
initErrs := make([]error, len(initFuncs))
for i, fn := range initFuncs {
wg.Go(func() {
defer slogutil.RecoverAndLog(ctx, b.logger)
workers[i], initErrs[i] = fn(ctx, maxSize, cacheDir)
})
}
wg.Wait()
err = errors.Join(initErrs...)
if err != nil {
return nil, fmt.Errorf("initializing hash prefixes: %w", err)
}
return workers, nil
}
// populateHashPrefix populates [builder.debugRefrs], [builder.hashMatcher] and
// [builder.sigHdlr] with necessary values for each enabled hash prefix filter.
//
// It must be called from [builder.initHashPrefixFilters].
func (b *builder) populateHashPrefix(workers []*service.RefreshWorker) {
matchers := map[string]*hashprefix.Storage{}
if b.env.AdultBlockingEnabled {
adultPrefix := path.Join(hashprefix.IDPrefix, string(filter.IDAdultBlocking))
matchers[filter.AdultBlockingTXTSuffix] = b.adultBlockingHashes
b.debugRefrs[adultPrefix] = b.adultBlocking
}
if b.env.NewRegDomainsEnabled {
newRegDomainsPrefix := path.Join(hashprefix.IDPrefix, string(filter.IDNewRegDomains))
b.debugRefrs[newRegDomainsPrefix] = b.newRegDomains
}
if b.env.SafeBrowsingEnabled {
safeBrowsingPrefix := path.Join(hashprefix.IDPrefix, string(filter.IDSafeBrowsing))
matchers[filter.GeneralTXTSuffix] = b.safeBrowsingHashes
b.debugRefrs[safeBrowsingPrefix] = b.safeBrowsing
}
b.hashMatcher = hashprefix.NewMatcher(matchers)
for _, worker := range workers {
if worker != nil {
b.sigHdlr.AddService(worker)
}
}
}
// initAdultBlocking initializes the adult-blocking filter and hash storage. It
// also adds the refresher with ID
// [hashprefix.IDPrefix]/[filter.IDAdultBlocking] to the debug refreshers.
// also adds the refresher with ID.
//
// It must be called from [builder.initHashPrefixFilters].
func (b *builder) initAdultBlocking(
ctx context.Context,
matchers map[string]*hashprefix.Storage,
maxSize datasize.ByteSize,
cacheDir string,
) (err error) {
) (refr *service.RefreshWorker, err error) {
if !b.env.AdultBlockingEnabled {
return nil
return nil, nil
}
defer func() { err = errors.Annotate(err, "initializing adult-blocking filter: %w") }()
b.adultBlockingHashes, err = hashprefix.NewStorage(nil)
if err != nil {
@@ -359,13 +431,13 @@ func (b *builder) initAdultBlocking(
const id = filter.IDAdultBlocking
hashPrefMtcs, err := metrics.NewHashPrefixFilter(
hashPrefMtrc, err := metrics.NewHashPrefixFilter(
b.mtrcNamespace,
string(id),
b.promRegisterer,
)
if err != nil {
return fmt.Errorf("registering hashprefix filter metrics: %w", err)
return nil, fmt.Errorf("registering hashprefix filter metrics: %w", err)
}
prefix := path.Join(hashprefix.IDPrefix, string(id))
@@ -377,8 +449,9 @@ func (b *builder) initAdultBlocking(
Hashes: b.adultBlockingHashes,
URL: &b.env.AdultBlockingURL.URL,
ErrColl: b.errColl,
HashPrefixMtcs: hashPrefMtcs,
HashPrefixMetrics: hashPrefMtrc,
Metrics: b.filterMtrc,
PublicSuffixList: publicsuffix.List,
ID: id,
CachePath: filepath.Join(cacheDir, string(id)),
ReplacementHost: c.BlockHost,
@@ -389,17 +462,18 @@ func (b *builder) initAdultBlocking(
// entity counts to fooCount.
CacheCount: c.CacheSize,
MaxSize: maxSize,
SubDomainNum: defaultSubDomainNum,
})
if err != nil {
return fmt.Errorf("creating filter: %w", err)
return nil, fmt.Errorf("creating filter: %w", err)
}
err = b.adultBlocking.RefreshInitial(ctx)
if err != nil {
return fmt.Errorf("initial refresh: %w", err)
return nil, fmt.Errorf("initial refresh: %w", err)
}
refr := service.NewRefreshWorker(&service.RefreshWorkerConfig{
refr = service.NewRefreshWorker(&service.RefreshWorkerConfig{
// Note that we also set the same timeout for the http.Client in
// [hashprefix.NewFilter].
ContextConstructor: contextutil.NewTimeoutConstructor(refrTimeout),
@@ -413,16 +487,10 @@ func (b *builder) initAdultBlocking(
// routines.
err = refr.Start(context.WithoutCancel(ctx))
if err != nil {
return fmt.Errorf("starting refresher: %w", err)
return nil, fmt.Errorf("starting refresher: %w", err)
}
b.sigHdlr.AddService(refr)
matchers[filter.AdultBlockingTXTSuffix] = b.adultBlockingHashes
b.debugRefrs[prefix] = b.adultBlocking
return nil
return refr, nil
}
// newSlogErrorHandler is a convenient wrapper around
@@ -436,18 +504,20 @@ func newSlogErrorHandler(baseLogger *slog.Logger, prefix string) (h *service.Slo
}
// initNewRegDomains initializes the newly-registered domain filter and hash
// storage. It also adds the refresher with ID
// [hashprefix.IDPrefix]/[filter.IDNewRegDomains] to the debug refreshers.
// storage.
//
// It must be called from [builder.initHashPrefixFilters].
func (b *builder) initNewRegDomains(
ctx context.Context,
maxSize datasize.ByteSize,
cacheDir string,
) (err error) {
) (refr *service.RefreshWorker, err error) {
if !b.env.NewRegDomainsEnabled {
return nil
return nil, nil
}
defer func() {
err = errors.Annotate(err, "initializing newly-registered domains filter: %w")
}()
b.newRegDomainsHashes, err = hashprefix.NewStorage(nil)
if err != nil {
@@ -463,13 +533,13 @@ func (b *builder) initNewRegDomains(
const id = filter.IDNewRegDomains
hashPrefMtcs, err := metrics.NewHashPrefixFilter(
hashPrefMtrc, err := metrics.NewHashPrefixFilter(
b.mtrcNamespace,
string(id),
b.promRegisterer,
)
if err != nil {
return fmt.Errorf("registering hashprefix filter metrics: %w", err)
return nil, fmt.Errorf("registering hashprefix filter metrics: %w", err)
}
prefix := path.Join(hashprefix.IDPrefix, string(id))
@@ -481,8 +551,9 @@ func (b *builder) initNewRegDomains(
Hashes: b.newRegDomainsHashes,
URL: &b.env.NewRegDomainsURL.URL,
ErrColl: b.errColl,
HashPrefixMtcs: hashPrefMtcs,
HashPrefixMetrics: hashPrefMtrc,
Metrics: b.filterMtrc,
PublicSuffixList: publicsuffix.List,
ID: id,
CachePath: filepath.Join(cacheDir, string(id)),
ReplacementHost: c.BlockHost,
@@ -491,17 +562,18 @@ func (b *builder) initNewRegDomains(
CacheTTL: time.Duration(c.CacheTTL),
CacheCount: c.CacheSize,
MaxSize: maxSize,
SubDomainNum: defaultSubDomainNum,
})
if err != nil {
return fmt.Errorf("creating filter: %w", err)
return nil, fmt.Errorf("creating filter: %w", err)
}
err = b.newRegDomains.RefreshInitial(ctx)
if err != nil {
return fmt.Errorf("initial refresh: %w", err)
return nil, fmt.Errorf("initial refresh: %w", err)
}
refr := service.NewRefreshWorker(&service.RefreshWorkerConfig{
refr = service.NewRefreshWorker(&service.RefreshWorkerConfig{
// Note that we also set the same timeout for the http.Client in
// [hashprefix.NewFilter].
ContextConstructor: contextutil.NewTimeoutConstructor(refrTimeout),
@@ -512,30 +584,24 @@ func (b *builder) initNewRegDomains(
})
err = refr.Start(context.WithoutCancel(ctx))
if err != nil {
return fmt.Errorf("starting refresher: %w", err)
return nil, fmt.Errorf("starting refresher: %w", err)
}
b.sigHdlr.AddService(refr)
b.debugRefrs[prefix] = b.newRegDomains
return nil
return refr, nil
}
// initSafeBrowsing initializes the safe-browsing filter and hash storage. It
// also adds the refresher with ID [hashprefix.IDPrefix]/[filter.IDSafeBrowsing]
// to the debug refreshers.
// initSafeBrowsing initializes the safe-browsing filter and hash storage.
//
// It must be called from [builder.initHashPrefixFilters].
func (b *builder) initSafeBrowsing(
ctx context.Context,
matchers map[string]*hashprefix.Storage,
maxSize datasize.ByteSize,
cacheDir string,
) (err error) {
) (refr *service.RefreshWorker, err error) {
if !b.env.SafeBrowsingEnabled {
return nil
return nil, nil
}
defer func() { err = errors.Annotate(err, "initializing safe-browsing filter: %w") }()
b.safeBrowsingHashes, err = hashprefix.NewStorage(nil)
if err != nil {
@@ -549,13 +615,13 @@ func (b *builder) initSafeBrowsing(
const id = filter.IDSafeBrowsing
hashPrefMtcs, err := metrics.NewHashPrefixFilter(
hashPrefMtrc, err := metrics.NewHashPrefixFilter(
b.mtrcNamespace,
string(id),
b.promRegisterer,
)
if err != nil {
return fmt.Errorf("registering hashprefix filter metrics: %w", err)
return nil, fmt.Errorf("registering hashprefix filter metrics: %w", err)
}
prefix := path.Join(hashprefix.IDPrefix, string(id))
@@ -567,8 +633,9 @@ func (b *builder) initSafeBrowsing(
Hashes: b.safeBrowsingHashes,
URL: &b.env.SafeBrowsingURL.URL,
ErrColl: b.errColl,
HashPrefixMtcs: hashPrefMtcs,
HashPrefixMetrics: hashPrefMtrc,
Metrics: b.filterMtrc,
PublicSuffixList: publicsuffix.List,
ID: id,
CachePath: filepath.Join(cacheDir, string(id)),
ReplacementHost: c.BlockHost,
@@ -577,17 +644,18 @@ func (b *builder) initSafeBrowsing(
CacheTTL: time.Duration(c.CacheTTL),
CacheCount: c.CacheSize,
MaxSize: maxSize,
SubDomainNum: defaultSubDomainNum,
})
if err != nil {
return fmt.Errorf("creating filter: %w", err)
return nil, fmt.Errorf("creating filter: %w", err)
}
err = b.safeBrowsing.RefreshInitial(ctx)
if err != nil {
return fmt.Errorf("initial refresh: %w", err)
return nil, fmt.Errorf("initial refresh: %w", err)
}
refr := service.NewRefreshWorker(&service.RefreshWorkerConfig{
refr = service.NewRefreshWorker(&service.RefreshWorkerConfig{
// Note that we also set the same timeout for the http.Client in
// [hashprefix.NewFilter].
ContextConstructor: contextutil.NewTimeoutConstructor(refrTimeout),
@@ -598,16 +666,10 @@ func (b *builder) initSafeBrowsing(
})
err = refr.Start(context.WithoutCancel(ctx))
if err != nil {
return fmt.Errorf("starting refresher: %w", err)
return nil, fmt.Errorf("starting refresher: %w", err)
}
b.sigHdlr.AddService(refr)
matchers[filter.GeneralTXTSuffix] = b.safeBrowsingHashes
b.debugRefrs[prefix] = b.safeBrowsing
return nil
return refr, nil
}
// initStandardAccess initializes the standard access settings.
@@ -1000,7 +1062,7 @@ func (b *builder) initTLSManager(ctx context.Context) (err error) {
ErrColl: b.errColl,
Metrics: mtrc,
TicketDB: tickDB,
KeyLogFilename: logFile,
KeyLogPath: logFile,
})
if err != nil {
return fmt.Errorf("initializing tls manager: %w", err)
@@ -1009,6 +1071,11 @@ func (b *builder) initTLSManager(ctx context.Context) (err error) {
b.tlsManager = mgr
b.debugRefrs[debugIDTLSConfig] = mgr
err = b.conf.TLS.store(ctx, mgr)
if err != nil {
return fmt.Errorf("storing tls certificates: %w", err)
}
b.logger.DebugContext(ctx, "initialized tls manager")
return nil
@@ -1066,7 +1133,9 @@ func (b *builder) newTicketDB(ctx context.Context) (db tlsconfig.TicketDB, err e
// initCustomDomainDB initializes the database for the custom domains.
//
// [builder.initTLSManager] must be called before this method.
// The following methods must be called before this one:
// - [builder.initTLSManager]
// - [builder.initServerGroups]
func (b *builder) initCustomDomainDB(ctx context.Context) (err error) {
if !bool(b.env.CustomDomainsEnabled) || !b.profilesEnabled {
b.logger.WarnContext(ctx, "custom domains are disabled")
@@ -1100,6 +1169,8 @@ func (b *builder) initCustomDomainDB(ctx context.Context) (err error) {
return fmt.Errorf("registering custom domain database metrics: %w", err)
}
customDomainPrefixes := b.customDomainPrefixes()
b.customDomainDB, err = tlsconfig.NewCustomDomainDB(&tlsconfig.CustomDomainDBConfig{
Logger: b.baseLogger.With(slogutil.KeyPrefix, "custom_domain_db"),
Clock: timeutil.SystemClock{},
@@ -1109,6 +1180,7 @@ func (b *builder) initCustomDomainDB(ctx context.Context) (err error) {
Storage: strg,
CacheDirPath: b.env.CustomDomainsCachePath,
InitialRetryIvl: time.Duration(b.env.CustomDomainsRefreshIvl),
BindPrefixes: customDomainPrefixes,
// TODO(a.garipov): Consider making configurable.
MaxRetryIvl: 1 * timeutil.Day,
})
@@ -1128,6 +1200,23 @@ func (b *builder) initCustomDomainDB(ctx context.Context) (err error) {
return nil
}
// customDomainPrefixes returns the IP prefixes for the custom-domain
// certificates to bind to.
func (b *builder) customDomainPrefixes() (prefixes []netip.Prefix) {
for _, g := range b.serverGroups {
if !g.ProfilesEnabled {
continue
}
for _, s := range g.Servers {
prefixes = append(prefixes, s.BindDataPrefixes()...)
}
}
return prefixes
}
// initServerGroups initializes the server groups.
//
// The following methods must be called before this one:

View File

@@ -120,10 +120,10 @@ func Main(plugins *plugin.Registry) {
errors.Check(b.initTLSManager(ctx))
errors.Check(b.initCustomDomainDB(ctx))
errors.Check(b.initServerGroups(ctx))
errors.Check(b.initCustomDomainDB(ctx))
errors.Check(b.initTicketRotator(ctx))
errors.Check(b.startBindToDevice(ctx))

View File

@@ -7,14 +7,11 @@ import (
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/validate"
"gopkg.in/yaml.v2"
yaml "go.yaml.in/yaml/v4"
)
// configuration represents the on-disk configuration of AdGuard DNS. The order
// of the fields should generally not be altered.
//
// TODO(a.garipov): Consider collecting all validation errors instead of
// quitting after the first one.
// of the fields defines their dependencies hierarchy and should not be altered.
type configuration struct {
// RateLimit is the rate limiting configuration.
RateLimit *rateLimitConfig `yaml:"ratelimit"`
@@ -46,6 +43,9 @@ type configuration struct {
// Check is the configuration for the DNS server checker.
Check *checkConfig `yaml:"check"`
// TLS is the configuration of TLS certificates.
TLS *tlsConfig `yaml:"tls"`
// Web is the configuration for the DNS-over-HTTP server.
Web *webConfig `yaml:"web"`
@@ -92,6 +92,11 @@ func (c *configuration) Validate() (err error) {
return errors.ErrNoValue
}
tlsConfValidator := &tlsConfigValidator{
state: tlsStateDisabled,
tlsConf: c.TLS,
}
// Keep this in the same order as the fields in the config.
validators := container.KeyValues[string, validate.Interface]{{
Key: "ratelimit",
@@ -120,9 +125,16 @@ func (c *configuration) Validate() (err error) {
}, {
Key: "check",
Value: c.Check,
}, {
Key: "tls",
Value: tlsConfValidator,
}, {
Key: "web",
Value: c.Web,
Value: validatorWithTLS{
validator: c.Web,
tlsState: &tlsConfValidator.state,
tlsConf: c.TLS,
},
}, {
Key: "safe_browsing",
Value: c.SafeBrowsing,
@@ -137,7 +149,11 @@ func (c *configuration) Validate() (err error) {
Value: c.FilteringGroups,
}, {
Key: "server_groups",
Value: c.ServerGroups,
Value: validatorWithTLS{
validator: c.ServerGroups,
tlsState: &tlsConfValidator.state,
tlsConf: c.TLS,
},
}, {
Key: "connectivity_check",
Value: c.ConnectivityCheck,

View File

@@ -8,7 +8,7 @@ import (
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/validate"
"github.com/ameshkov/dnscrypt/v2"
"gopkg.in/yaml.v2"
yaml "go.yaml.in/yaml/v4"
)
// dnsCryptConfig are the DNSCrypt server settings.

View File

@@ -9,7 +9,7 @@ import (
)
// setMaxThreads sets the maximum number of threads for the Go runtime, if
// necessary. l must not be nil, envs must not be negative.
// necessary. l must not be nil, n must not be negative.
func setMaxThreads(ctx context.Context, l *slog.Logger, n int) {
if n == 0 {
l.Log(ctx, slogutil.LevelTrace, "go max threads not set")

View File

@@ -1,6 +1,7 @@
package cmd
import (
"context"
"crypto/tls"
"fmt"
"net/netip"
@@ -19,69 +20,149 @@ import (
// toInternal returns the configuration of DNS servers for a single server
// group. srvs and other parts of the configuration must be valid.
func (srvs servers) toInternal(
ctx context.Context,
btdMgr *bindtodevice.Manager,
tlsMgr tlsconfig.Manager,
ratelimitConf *rateLimitConfig,
dnsConf *dnsConfig,
certNames []agd.CertificateName,
deviceDomains []string,
) (dnsSrvs []*agd.Server, err error) {
dnsSrvs = make([]*agd.Server, 0, len(srvs))
for _, srv := range srvs {
var bindData []*agd.ServerBindData
bindData, err = srv.bindData(btdMgr)
for i, srv := range srvs {
var dnsSrv *agd.Server
dnsSrv, err = srv.toInternal(
ctx,
tlsMgr,
btdMgr,
ratelimitConf,
dnsConf,
certNames,
deviceDomains,
)
if err != nil {
return nil, fmt.Errorf("server %q: %w", srv.Name, err)
return nil, fmt.Errorf("server %q: at index %d: %w", srv.Name, i, err)
}
name := agd.ServerName(srv.Name)
dnsSrv := &agd.Server{
Name: name,
ReadTimeout: time.Duration(dnsConf.ReadTimeout),
WriteTimeout: time.Duration(dnsConf.WriteTimeout),
LinkedIPEnabled: srv.LinkedIPEnabled,
Protocol: srv.Protocol.toInternal(),
}
tcpConf := &agd.TCPConfig{
IdleTimeout: time.Duration(dnsConf.TCPIdleTimeout),
MaxPipelineCount: ratelimitConf.TCP.MaxPipelineCount,
MaxPipelineEnabled: ratelimitConf.TCP.Enabled,
}
switch dnsSrv.Protocol {
case agd.ProtoDNS:
dnsSrv.TCPConf = tcpConf
dnsSrv.UDPConf = &agd.UDPConfig{
// #nosec G115 -- The value has already been validated in
// [dnsConfig.validate].
MaxRespSize: uint16(dnsConf.MaxUDPResponseSize.Bytes()),
}
case agd.ProtoDNSCrypt:
var dcConf *agd.DNSCryptConfig
dcConf, err = srv.DNSCrypt.toInternal()
if err != nil {
return nil, fmt.Errorf("server %q: dnscrypt: %w", srv.Name, err)
}
dnsSrv.DNSCrypt = dcConf
default:
dnsSrv.TCPConf = tcpConf
dnsSrv.QUICConf = &agd.QUICConfig{
MaxStreamsPerPeer: ratelimitConf.QUIC.MaxStreamsPerPeer,
QUICLimitsEnabled: ratelimitConf.QUIC.Enabled,
}
dnsSrv.TLS = newTLSConfig(dnsSrv, tlsMgr, deviceDomains, srv)
}
dnsSrv.SetBindData(bindData)
dnsSrvs = append(dnsSrvs, dnsSrv)
}
return dnsSrvs, nil
}
// toInternal returns the configuration for a single server. tlsMgr, btdMgr,
// ratelimitConf, and dnsConf must not be nil, certNames items must be valid,
// deviceDomains items must be a valid domain names.
func (s *server) toInternal(
ctx context.Context,
tlsMgr tlsconfig.Manager,
btdMgr *bindtodevice.Manager,
ratelimitConf *rateLimitConfig,
dnsConf *dnsConfig,
certNames []agd.CertificateName,
deviceDomains []string,
) (dnsSrv *agd.Server, err error) {
var bindData []*agd.ServerBindData
bindData, err = s.bindData(btdMgr)
if err != nil {
// Don't wrap the error, since it's informative enough as is.
return nil, err
}
name := agd.ServerName(s.Name)
dnsSrv = &agd.Server{
Name: name,
ReadTimeout: time.Duration(dnsConf.ReadTimeout),
WriteTimeout: time.Duration(dnsConf.WriteTimeout),
LinkedIPEnabled: s.LinkedIPEnabled,
Protocol: s.Protocol.toInternal(),
}
dnsSrv.SetBindData(bindData)
err = setProtoConfig(ctx, dnsSrv, s, dnsConf, ratelimitConf, tlsMgr, certNames, deviceDomains)
if err != nil {
return nil, fmt.Errorf("setting protocol-specific configuration: %w", err)
}
return dnsSrv, nil
}
// setProtoConfig sets the protocol-specific configuration to dnsSrv.
func setProtoConfig(
ctx context.Context,
dnsSrv *agd.Server,
s *server,
dnsConf *dnsConfig,
ratelimitConf *rateLimitConfig,
tlsMgr tlsconfig.Manager,
certNames []agd.CertificateName,
deviceDomains []string,
) (err error) {
switch dnsSrv.Protocol {
case agd.ProtoDNS:
dnsSrv.TCPConf = &agd.TCPConfig{
IdleTimeout: time.Duration(dnsConf.TCPIdleTimeout),
MaxPipelineCount: ratelimitConf.TCP.MaxPipelineCount,
MaxPipelineEnabled: ratelimitConf.TCP.Enabled,
}
dnsSrv.UDPConf = &agd.UDPConfig{
// #nosec G115 -- The value has already been validated in
// [dnsConfig.Validate].
MaxRespSize: uint16(dnsConf.MaxUDPResponseSize.Bytes()),
}
case agd.ProtoDNSCrypt:
var dcConf *agd.DNSCryptConfig
dcConf, err = s.DNSCrypt.toInternal()
if err != nil {
return fmt.Errorf("dnscrypt: %w", err)
}
dnsSrv.DNSCrypt = dcConf
default:
dnsSrv.TCPConf = &agd.TCPConfig{
IdleTimeout: time.Duration(dnsConf.TCPIdleTimeout),
MaxPipelineCount: ratelimitConf.TCP.MaxPipelineCount,
MaxPipelineEnabled: ratelimitConf.TCP.Enabled,
}
dnsSrv.QUICConf = &agd.QUICConfig{
MaxStreamsPerPeer: ratelimitConf.QUIC.MaxStreamsPerPeer,
QUICLimitsEnabled: ratelimitConf.QUIC.Enabled,
}
dnsSrv.TLS = newTLSConfig(dnsSrv, tlsMgr, deviceDomains, s)
err = bindTLSNames(ctx, dnsSrv, tlsMgr, certNames)
if err != nil {
// Don't wrap the error, since it's informative enough as is.
return err
}
}
return nil
}
// bindTLSNames binds the server to the specified certificate names in the TLS
// manager.
func bindTLSNames(
ctx context.Context,
s *agd.Server,
tlsMgr tlsconfig.Manager,
names []agd.CertificateName,
) (err error) {
var errs []error
for _, pref := range s.BindDataPrefixes() {
for _, name := range names {
err = tlsMgr.Bind(ctx, name, pref)
if err != nil {
errs = append(errs, fmt.Errorf("binding %q to %s: %w", name, pref, err))
}
}
}
return errors.Join(errs...)
}
// newTLSConfig returns the TLS configuration with metrics and ALPs set.
//
// TODO(s.chzhen): Consider moving to agd package as soon as the import cycle

View File

@@ -29,21 +29,18 @@ func (srvGrps serverGroups) toInternal(
ratelimitConf *rateLimitConfig,
dnsConf *dnsConfig,
) (svcSrvGrps []*dnssvc.ServerGroupConfig, err error) {
svcSrvGrps = make([]*dnssvc.ServerGroupConfig, len(srvGrps))
for i, g := range srvGrps {
svcSrvGrps = make([]*dnssvc.ServerGroupConfig, 0, len(srvGrps))
for _, g := range srvGrps {
// TODO(e.burkov): Validate in [serverGroupsValidator.Validate].
fltGrpID := agd.FilteringGroupID(g.FilteringGroup)
_, ok := fltGrps[fltGrpID]
if !ok {
return nil, fmt.Errorf("server group %q: unknown filtering group %q", g.Name, fltGrpID)
}
var deviceDomains []string
deviceDomains, err = g.TLS.toInternal(ctx, tlsMgr)
if err != nil {
return nil, fmt.Errorf("tls %q: %w", g.Name, err)
}
certNames, deviceDomains := g.TLS.toInternal()
svcSrvGrps[i] = &dnssvc.ServerGroupConfig{
groupConfig := &dnssvc.ServerGroupConfig{
DDR: g.DDR.toInternal(messages),
DeviceDomains: deviceDomains,
Name: agd.ServerGroupName(g.Name),
@@ -51,52 +48,25 @@ func (srvGrps serverGroups) toInternal(
ProfilesEnabled: g.ProfilesEnabled,
}
svcSrvGrps[i].Servers, err = g.Servers.toInternal(
groupConfig.Servers, err = g.Servers.toInternal(
ctx,
btdMgr,
tlsMgr,
ratelimitConf,
dnsConf,
certNames,
deviceDomains,
)
if err != nil {
return nil, fmt.Errorf("server group %q: %w", g.Name, err)
}
svcSrvGrps = append(svcSrvGrps, groupConfig)
}
return svcSrvGrps, nil
}
// type check
var _ validate.Interface = serverGroups(nil)
// Validate implements the [validate.Interface] interface for serverGroups.
func (srvGrps serverGroups) Validate() (err error) {
if len(srvGrps) == 0 {
return errors.ErrEmptyValue
}
var errs []error
names := container.NewMapSet[string]()
for i, g := range srvGrps {
err = g.Validate()
if err != nil {
errs = append(errs, fmt.Errorf("at index %d: %w", i, err))
continue
}
if names.Has(g.Name) {
errs = append(errs, fmt.Errorf("at index %d: %w: %q", i, errors.ErrDuplicated, g.Name))
continue
}
names.Add(g.Name)
}
return errors.Join(errs...)
}
// serverGroup defines a group of DNS servers all of which use the same
// filtering settings.
//
@@ -108,7 +78,7 @@ type serverGroup struct {
DDR *ddrConfig `yaml:"ddr"`
// TLS are the TLS settings for this server, if any.
TLS *tlsConfig `yaml:"tls"`
TLS *serverGroupTLSConfig `yaml:"tls"`
// Name is the unique name of the server group.
Name string `yaml:"name"`
@@ -125,10 +95,10 @@ type serverGroup struct {
}
// type check
var _ validate.Interface = (*serverGroup)(nil)
var _ tlsValidator = (*serverGroup)(nil)
// Validate implements the [validate.Interface] interface for *serverGroup.
func (g *serverGroup) Validate() (err error) {
// validate implements the [validatorWithTLS] interface for *serverGroup.
func (g *serverGroup) validate(tlsConf *tlsConfig, ts *tlsState) (err error) {
if g == nil {
return errors.ErrNoValue
}
@@ -145,7 +115,7 @@ func (g *serverGroup) Validate() (err error) {
errs = append(errs, fmt.Errorf("servers: %w", err))
}
err = g.TLS.validateIfNecessary(needsTLS)
err = g.TLS.validateIfNecessary(needsTLS, tlsConf, *ts)
if err != nil {
errs = append(errs, fmt.Errorf("tls: %w", err))
}
@@ -170,3 +140,34 @@ func (srvGrps serverGroups) collectSessTicketPaths() (paths []string) {
return set.Values()
}
// type check
var _ tlsValidator = (serverGroups)(nil)
// validate implements the [tlsValidator] interface for serverGroups.
func (srvGrps serverGroups) validate(tlsConf *tlsConfig, ts *tlsState) (err error) {
if len(srvGrps) == 0 {
return errors.ErrEmptyValue
}
var errs []error
names := container.NewMapSet[string]()
for i, g := range srvGrps {
err = g.validate(tlsConf, ts)
if err != nil {
errs = append(errs, fmt.Errorf("at index %d: %w", i, err))
continue
}
if names.Has(g.Name) {
errs = append(errs, fmt.Errorf("at index %d: %w: %q", i, errors.ErrDuplicated, g.Name))
continue
}
names.Add(g.Name)
}
return errors.Join(errs...)
}

View File

@@ -2,10 +2,13 @@ package cmd
import (
"context"
"crypto/tls"
"fmt"
"maps"
"net/netip"
"slices"
"strings"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/tlsconfig"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
@@ -18,10 +21,135 @@ const (
sessionTicketRemote = "remote"
)
// tlsConfig are the TLS settings of a DNS server, if any.
// tlsCertificateConfig is a single TLS certificate configuration.
type tlsCertificateConfig struct {
// CertificatePath is the path to the TLS certificate.
CertificatePath string `yaml:"certificate"`
// KeyPath is the path to the TLS private key.
KeyPath string `yaml:"key"`
}
// certificateGroupConfigs is a map of certificate group names to their
// configurations.
type certificateGroupConfigs map[string]*tlsCertificateConfig
// tlsConfig is the common configuration of TLS certificates.
type tlsConfig struct {
// Certificates are TLS certificates for this server.
Certificates tlsConfigCerts `yaml:"certificates"`
// CertificateGroups are the named groups of TLS certificates.
CertificateGroups certificateGroupConfigs `yaml:"certificate_groups"`
// Enabled is true if TLS is enabled.
Enabled bool `yaml:"enabled"`
}
// store stores the TLS certificates in the TLS manager. c must be valid,
// tlsMgr must not be nil.
func (c tlsConfig) store(ctx context.Context, tlsMgr tlsconfig.Manager) (err error) {
var errs []error
for name, conf := range c.CertificateGroups {
err = tlsMgr.Add(ctx, &tlsconfig.AddParams{
// The name is validated in [tlsConfig.Validate].
Name: agd.CertificateName(name),
CertPath: conf.CertificatePath,
KeyPath: conf.KeyPath,
IsCustom: false,
})
if err != nil {
errs = append(errs, fmt.Errorf("adding certificate %q: %w", name, err))
}
}
return errors.Join(errs...)
}
// type check
var _ validate.Interface = (certificateGroupConfigs)(nil)
// Validate implements the [validate.Interface] interface for
// certificateGroupConfigs.
//
// TODO(e.burkov): Consider checking the files existence with [os.Stat].
func (c certificateGroupConfigs) Validate() (err error) {
var errs []error
for _, name := range slices.Sorted(maps.Keys(c)) {
_, err = agd.NewCertificateName(name)
if err != nil {
errs = append(errs, fmt.Errorf("certificate group %q: %w", name, err))
}
cg := c[name]
if cg == nil {
errs = append(errs, fmt.Errorf("certificate group %q: %w", name, errors.ErrNoValue))
continue
}
err = validate.NotEmpty("certificate", cg.CertificatePath)
if err != nil {
errs = append(errs, fmt.Errorf("certificate group %q: %w", name, err))
}
err = validate.NotEmpty("key", cg.KeyPath)
if err != nil {
errs = append(errs, fmt.Errorf("certificate group %q: %w", name, err))
}
}
return errors.Join(errs...)
}
// tlsCertificateGroupConfig defines a group of certificates used by a server
// group.
type tlsCertificateGroupConfig struct {
Name agd.CertificateName `yaml:"name"`
}
// tlsCertificateGroupConfigs is a slice of certificate group configs.
type tlsCertificateGroupConfigs []*tlsCertificateGroupConfig
// validate returns an error if the certificate group configs are invalid.
func (c tlsCertificateGroupConfigs) validate(tlsConf *tlsConfig) (err error) {
if c == nil {
return errors.ErrNoValue
} else if len(c) == 0 {
return errors.ErrEmptyValue
}
var errs []error
for i, cg := range c {
nameStr := string(cg.Name)
if _, ok := tlsConf.CertificateGroups[nameStr]; !ok {
err = fmt.Errorf("at index %d: %q: %w", i, nameStr, errors.ErrBadEnumValue)
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
// bind binds the certificate group configs to pref in tlsMgr.
func (c tlsCertificateGroupConfigs) bind(
ctx context.Context,
tlsMgr tlsconfig.Manager,
pref netip.Prefix,
) (err error) {
var errs []error
for i, cg := range c {
err = tlsMgr.Bind(ctx, cg.Name, pref)
if err != nil {
err = fmt.Errorf("at index %d: binding to %s: %w", i, pref, err)
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
// serverGroupTLSConfig are the TLS settings of a DNS server, if any.
type serverGroupTLSConfig struct {
// CertificateGroups are TLS certificate groups for this server.
CertificateGroups tlsCertificateGroupConfigs `yaml:"certificate_groups"`
// SessionKeys are paths to files containing the TLS session keys for this
// server.
@@ -40,29 +168,29 @@ type tlsConfig struct {
// toInternal converts c to the TLS configuration for a DNS server. c must be
// valid.
func (c *tlsConfig) toInternal(
ctx context.Context,
tlsMgr tlsconfig.Manager,
) (deviceDomains []string, err error) {
func (c *serverGroupTLSConfig) toInternal() (certNames []agd.CertificateName, wcDomains []string) {
if c == nil {
return nil, nil
}
err = c.Certificates.store(ctx, tlsMgr)
if err != nil {
return nil, fmt.Errorf("certificates: %w", err)
for _, cg := range c.CertificateGroups {
certNames = append(certNames, cg.Name)
}
for _, w := range c.DeviceIDWildcards {
deviceDomains = append(deviceDomains, strings.TrimPrefix(w, "*."))
wcDomains = append(wcDomains, strings.TrimPrefix(w, "*."))
}
return deviceDomains, nil
return certNames, wcDomains
}
// validateIfNecessary returns an error if the TLS configuration is invalid
// depending on whether it is necessary or not.
func (c *tlsConfig) validateIfNecessary(needsTLS bool) (err error) {
func (c *serverGroupTLSConfig) validateIfNecessary(
needsTLS bool,
tlsConf *tlsConfig,
ts tlsState,
) (err error) {
switch {
case c == nil:
if needsTLS {
@@ -73,15 +201,18 @@ func (c *tlsConfig) validateIfNecessary(needsTLS bool) (err error) {
return nil
case !needsTLS:
return errors.Error("server group does not require tls")
case ts == tlsStateDisabled:
return errors.Error("tls is disabled")
}
var errs []error
if err = validate.NotEmptySlice("certificates", c.Certificates); err != nil {
// Don't wrap the error, because it's informative enough as is.
errs = append(errs, err)
}
errs = validate.Append(errs, "certificates", c.Certificates)
if ts == tlsStateValid {
err = c.CertificateGroups.validate(tlsConf)
if err != nil {
errs = append(errs, fmt.Errorf("certificate_groups: %w", err))
}
}
err = validateDeviceIDWildcards(c.DeviceIDWildcards)
if err != nil {
@@ -109,77 +240,85 @@ func validateDeviceIDWildcards(wildcards []string) (err error) {
return nil
}
// tlsConfigCert is a single TLS certificate.
type tlsConfigCert struct {
// Certificate is the path to the TLS certificate.
Certificate string `yaml:"certificate"`
// tlsState is the state of TLS validation.
type tlsState string
// Key is the path to the TLS private key.
Key string `yaml:"key"`
}
// Valid tlsState values.
const (
// tlsStateDisabled is the state of TLS validation when the TLS
// configuration is not specified.
tlsStateDisabled tlsState = "disabled"
// tlsConfigCerts are TLS certificates. A valid instance of tlsConfigCerts has
// no nil items.
type tlsConfigCerts []*tlsConfigCert
// tlsStateInvalid is the state of TLS validation when the result is
// negative.
tlsStateInvalid tlsState = "invalid"
// store stores the TLS certificates in the TLS manager. certs must be valid.
func (certs tlsConfigCerts) store(ctx context.Context, tlsMgr tlsconfig.Manager) (err error) {
var errs []error
for i, c := range certs {
err = tlsMgr.Add(ctx, c.Certificate, c.Key, false)
if err != nil {
errs = append(errs, fmt.Errorf("adding certificate at index %d: %w", i, err))
}
}
// tlsStateValid is the state of TLS validation when the result is positive.
tlsStateValid tlsState = "valid"
)
return errors.Join(errs...)
}
// tlsConfigValidator validates the TLS configuration and updates the result's
type tlsConfigValidator struct {
// tlsConf is the configuration to validate.
tlsConf *tlsConfig
// toInternal is like [tlsConfigCerts.store] but it also returns the TLS
// configuration. certs must be valid.
func (certs tlsConfigCerts) toInternal(
ctx context.Context,
tlsMgr tlsconfig.Manager,
) (conf *tls.Config, err error) {
if len(certs) == 0 {
return nil, nil
}
err = certs.store(ctx, tlsMgr)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
return tlsMgr.Clone(), nil
// state is the state of TLS validation. It must not be used until
// [tlsConfigValidator.Validate] returns.
state tlsState
}
// type check
var _ validate.Interface = tlsConfigCerts(nil)
var _ validate.Interface = (*tlsConfigValidator)(nil)
// Validate implements the [validate.Interface] interface for
// tlsConfigValidator. It sets the state field of v to the corresponding
// tlsState value.
func (v *tlsConfigValidator) Validate() (err error) {
if v.tlsConf == nil {
v.state = tlsStateInvalid
return errors.ErrNoValue
}
if !v.tlsConf.Enabled {
v.state = tlsStateDisabled
// Validate implements the [validate.Interface] interface for tlsConfigCerts.
// certs may be nil.
func (certs tlsConfigCerts) Validate() (err error) {
if len(certs) == 0 {
return nil
}
var errs []error
for i, c := range certs {
if c == nil {
errs = append(errs, fmt.Errorf("at index %d: %w", i, errors.ErrNoValue))
continue
err = v.tlsConf.CertificateGroups.Validate()
if err != nil {
v.state = tlsStateInvalid
} else {
v.state = tlsStateValid
}
if err = validate.NotEmpty("certificate", c.Certificate); err != nil {
errs = append(errs, fmt.Errorf("at index %d: %w", i, err))
}
if err = validate.NotEmpty("key", c.Key); err != nil {
errs = append(errs, fmt.Errorf("at index %d: %w", i, err))
}
}
return errors.Join(errs...)
return err
}
// tlsValidator is like [validate.Interface], but accepts TLS configuration
// and state to validate the web module configuration with.
type tlsValidator interface {
// validate returns error if the configuration is not valid. tlsConf must
// correspond to tlsState.
validate(tlsConf *tlsConfig, tlsState *tlsState) (err error)
}
type validatorWithTLS struct {
// validator is the entity to validate with TLS configuration.
validator tlsValidator
// tlsConf is the TLS configuration to validate with.
tlsConf *tlsConfig
// tlsState is the state of TLS validation.
tlsState *tlsState
}
// type check
var _ validate.Interface = (*validatorWithTLS)(nil)
// Validate implements the [validate.Interface] interface for validatorWithTLS.
func (v validatorWithTLS) Validate() (err error) {
return v.validator.validate(v.tlsConf, v.tlsState)
}

View File

@@ -18,6 +18,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/tlsconfig"
"github.com/AdguardTeam/AdGuardDNS/internal/websvc"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/logutil/slogutil"
@@ -145,6 +146,46 @@ func (c *webConfig) toInternal(
return conf, nil
}
// validate implements the [tlsValidator] interface for *webConfig.
func (c *webConfig) validate(tlsConf *tlsConfig, ts *tlsState) (err error) {
if c == nil {
return nil
}
errs := []error{
validate.Positive("timeout", c.Timeout),
}
errs = validate.Append(errs, "static_content", c.StaticContent)
withTLS := validatorWithTLS{
tlsState: ts,
tlsConf: tlsConf,
}
validators := container.KeyValues[string, tlsValidator]{{
Key: "linked_ip",
Value: c.LinkedIP,
}, {
Key: "adult_blocking",
Value: c.AdultBlocking,
}, {
Key: "general_blocking",
Value: c.GeneralBlocking,
}, {
Key: "safe_browsing",
Value: c.SafeBrowsing,
}, {
Key: "non_doh_bind",
Value: c.NonDoHBind,
}}
for _, v := range validators {
withTLS.validator = v.Value
errs = validate.Append(errs, v.Key, withTLS)
}
return errors.Join(errs...)
}
// readErrorPages returns the contents for the error pages in the configuration
// file and any errors encountered while reading them.
func (c *webConfig) readErrorPages() (error404, error500 []byte, err error) {
@@ -181,29 +222,6 @@ func (c *webConfig) setStaticContent(envs *environment, conf *websvc.Config) (er
return nil
}
// type check
var _ validate.Interface = (*webConfig)(nil)
// Validate implements the [validate.Interface] interface for *webConfig.
func (c *webConfig) Validate() (err error) {
if c == nil {
return nil
}
errs := []error{
validate.Positive("timeout", c.Timeout),
}
errs = validate.Append(errs, "linked_ip", c.LinkedIP)
errs = validate.Append(errs, "adult_blocking", c.AdultBlocking)
errs = validate.Append(errs, "general_blocking", c.GeneralBlocking)
errs = validate.Append(errs, "safe_browsing", c.SafeBrowsing)
errs = validate.Append(errs, "static_content", c.StaticContent)
errs = validate.Append(errs, "non_doh_bind", c.NonDoHBind)
return errors.Join(errs...)
}
// linkedIPServer is the linked IP web server configuration.
type linkedIPServer struct {
// Bind are the bind addresses and optional TLS configuration for the linked
@@ -237,10 +255,10 @@ func (s *linkedIPServer) toInternal(
}
// type check
var _ validate.Interface = (*linkedIPServer)(nil)
var _ tlsValidator = (*linkedIPServer)(nil)
// Validate implements the [validate.Interface] interface for *linkedIPServer.
func (s *linkedIPServer) Validate() (err error) {
// validate implements the [tlsValidator] interface for *linkedIPServer.
func (s *linkedIPServer) validate(tlsConf *tlsConfig, ts *tlsState) (err error) {
if s == nil {
return nil
}
@@ -249,7 +267,10 @@ func (s *linkedIPServer) Validate() (err error) {
validate.NotEmptySlice("bind", s.Bind),
}
errs = validate.Append(errs, "bind", s.Bind)
err = s.Bind.validate(tlsConf, ts)
if err != nil {
errs = append(errs, fmt.Errorf("bind: %w", err))
}
return errors.Join(errs...)
}
@@ -287,10 +308,10 @@ func (s *blockPageServer) toInternal(
}
// type check
var _ validate.Interface = (*blockPageServer)(nil)
var _ tlsValidator = (*blockPageServer)(nil)
// Validate implements the [validate.Interface] interface for *blockPageServer.
func (s *blockPageServer) Validate() (err error) {
// validate implements the [tlsValidator] interface for *blockPageServer.
func (s *blockPageServer) validate(tlsConf *tlsConfig, ts *tlsState) (err error) {
if s == nil {
return nil
}
@@ -300,7 +321,10 @@ func (s *blockPageServer) Validate() (err error) {
validate.NotEmptySlice("bind", s.Bind),
}
errs = validate.Append(errs, "bind", s.Bind)
err = s.Bind.validate(tlsConf, ts)
if err != nil {
errs = append(errs, fmt.Errorf("bind: %w", err))
}
return errors.Join(errs...)
}
@@ -314,30 +338,41 @@ func (bd bindData) toInternal(
ctx context.Context,
tlsMgr tlsconfig.Manager,
) (data []*websvc.BindData, err error) {
data = make([]*websvc.BindData, len(bd))
var errs []error
data = make([]*websvc.BindData, 0, len(bd))
for i, d := range bd {
data[i], err = d.toInternal(ctx, tlsMgr)
var datum *websvc.BindData
datum, err = d.toInternal(ctx, tlsMgr)
if err != nil {
return nil, fmt.Errorf("bind data at index %d: %w", i, err)
errs = append(errs, fmt.Errorf("bind data: at index %d: %w", i, err))
continue
}
data = append(data, datum)
}
err = errors.Join(errs...)
if err != nil {
return nil, err
}
return data, nil
}
// type check
var _ validate.Interface = bindData(nil)
var _ tlsValidator = bindData(nil)
// Validate implements the [validate.Interface] interface for bindData.
func (bd bindData) Validate() (err error) {
// validate implements the [tlsValidator] interface for bindData.
func (bd bindData) validate(tlsConf *tlsConfig, ts *tlsState) (err error) {
if len(bd) == 0 {
return nil
}
var errs []error
for i, d := range bd {
err = d.Validate()
err = d.validate(tlsConf, ts)
if err != nil {
errs = append(errs, fmt.Errorf("at index %d: %w", i, err))
}
@@ -351,8 +386,9 @@ type bindItem struct {
// Address is the binding address.
Address netip.AddrPort `yaml:"address"`
// Certificates are the optional TLS certificates for this HTTP(S) server.
Certificates tlsConfigCerts `yaml:"certificates"`
// CertificateGroups are the optional TLS certificates configuration for
// this HTTP(S) server.
CertificateGroups tlsCertificateGroupConfigs `yaml:"certificate_groups"`
}
// toInternal converts i to bind data for the AdGuard DNS web service. i must
@@ -361,22 +397,35 @@ func (i *bindItem) toInternal(
ctx context.Context,
tlsMgr tlsconfig.Manager,
) (data *websvc.BindData, err error) {
tlsConf, err := i.Certificates.toInternal(ctx, tlsMgr)
if len(i.CertificateGroups) == 0 {
return &websvc.BindData{
Address: i.Address,
}, nil
}
addr := i.Address.Addr()
pref, err := addr.Prefix(addr.BitLen())
if err != nil {
return nil, fmt.Errorf("prefix: %w", err)
}
err = i.CertificateGroups.bind(ctx, tlsMgr, pref)
if err != nil {
return nil, fmt.Errorf("certificates: %w", err)
}
return &websvc.BindData{
TLS: tlsConf,
TLS: tlsMgr.Clone(),
Address: i.Address,
}, nil
}
// type check
var _ validate.Interface = (*bindItem)(nil)
var _ tlsValidator = (*bindItem)(nil)
// Validate implements the [validate.Interface] interface for *bindItem.
func (i *bindItem) Validate() (err error) {
// validate implements the [tlsValidator] interface for *bindItem.
func (i *bindItem) validate(tlsConf *tlsConfig, ts *tlsState) (err error) {
if i == nil {
return errors.ErrNoValue
}
@@ -385,7 +434,22 @@ func (i *bindItem) Validate() (err error) {
validate.NotEmpty("address", i.Address),
}
errs = validate.Append(errs, "certificates", i.Certificates)
switch *ts {
case tlsStateValid:
if i.CertificateGroups == nil {
// No TLS.
break
}
err = i.CertificateGroups.validate(tlsConf)
if err != nil {
errs = append(errs, fmt.Errorf("certificate_groups: %w", err))
}
case tlsStateDisabled:
errs = append(errs, validate.EmptySlice("certificate_groups", i.CertificateGroups))
default:
// Ignore TLS configuration.
}
return errors.Join(errs...)
}

View File

@@ -18,10 +18,14 @@ func (c *Constructor) NewResp(req *dns.Msg) (resp *dns.Msg) {
}).SetReply(req)
}
// NewBlockedResp returns a blocked response DNS message based on the
// constructor's blocking mode.
func (c *Constructor) NewBlockedResp(req *dns.Msg) (msg *dns.Msg, err error) {
switch m := c.blockingMode.(type) {
// NewBlockedResp returns a blocked response DNS message based on the given
// blocking mode. If mode is nil, the constructor's blocking mode is used.
func (c *Constructor) NewBlockedResp(req *dns.Msg, mode BlockingMode) (msg *dns.Msg, err error) {
if mode == nil {
mode = c.blockingMode
}
switch m := mode.(type) {
case *BlockingModeCustomIP:
return c.newBlockedCustomIPResp(req, m)
case *BlockingModeNullIP:

View File

@@ -59,7 +59,7 @@ func TestConstructor_NewBlockedResp_nullIP(t *testing.T) {
req := dnsservertest.NewReq(testFQDN, tc.qt, dns.ClassINET, reqExtra)
resp, respErr := msgs.NewBlockedResp(req)
resp, respErr := msgs.NewBlockedResp(req, nil)
require.NoError(t, respErr)
require.NotNil(t, resp)
@@ -73,7 +73,7 @@ func TestConstructor_NewBlockedResp_nullIP(t *testing.T) {
func TestConstructor_NewBlockedResp_customIP(t *testing.T) {
t.Parallel()
cloner := agdtest.NewCloner()
msgs := agdtest.NewConstructor(t)
// TODO(a.garipov): Test the forged extra as well if the EDE with that code
// is used again.
@@ -139,20 +139,11 @@ func TestConstructor_NewBlockedResp_customIP(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
msgs, err := dnsmsg.NewConstructor(&dnsmsg.ConstructorConfig{
Cloner: cloner,
BlockingMode: tc.blockingMode,
StructuredErrors: agdtest.NewSDEConfig(true),
FilteredResponseTTL: agdtest.FilteredResponseTTL,
EDEEnabled: true,
})
require.NoError(t, err)
t.Run("a", func(t *testing.T) {
t.Parallel()
req := dnsservertest.NewReq(testFQDN, dns.TypeA, dns.ClassINET, reqExtra)
resp, respErr := msgs.NewBlockedResp(req)
resp, respErr := msgs.NewBlockedResp(req, tc.blockingMode)
require.NoError(t, respErr)
require.NotNil(t, resp)
@@ -165,7 +156,7 @@ func TestConstructor_NewBlockedResp_customIP(t *testing.T) {
t.Parallel()
req := dnsservertest.NewReq(testFQDN, dns.TypeAAAA, dns.ClassINET, reqExtra)
resp, respErr := msgs.NewBlockedResp(req)
resp, respErr := msgs.NewBlockedResp(req, tc.blockingMode)
require.NoError(t, respErr)
require.NotNil(t, resp)
@@ -180,10 +171,11 @@ func TestConstructor_NewBlockedResp_customIP(t *testing.T) {
func TestConstructor_NewBlockedResp_nodata(t *testing.T) {
t.Parallel()
msgs := agdtest.NewConstructor(t)
req := dnsservertest.NewReq(testFQDN, dns.TypeA, dns.ClassINET, dnsservertest.SectionExtra{
dnsservertest.NewOPT(true, dns.MaxMsgSize, &dns.EDNS0_EDE{}),
})
cloner := agdtest.NewCloner()
wantExtra := []dns.RR{dnsservertest.NewOPT(true, dns.MaxMsgSize, &dns.EDNS0_EDE{
InfoCode: dns.ExtendedErrorCodeFiltered,
@@ -208,16 +200,7 @@ func TestConstructor_NewBlockedResp_nodata(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
msgs, err := dnsmsg.NewConstructor(&dnsmsg.ConstructorConfig{
Cloner: cloner,
BlockingMode: tc.blockingMode,
StructuredErrors: agdtest.NewSDEConfig(true),
FilteredResponseTTL: agdtest.FilteredResponseTTL,
EDEEnabled: true,
})
require.NoError(t, err)
resp, err := msgs.NewBlockedResp(req)
resp, err := msgs.NewBlockedResp(req, tc.blockingMode)
require.NoError(t, err)
require.NotNil(t, resp)
@@ -312,7 +295,7 @@ func TestConstructor_NewBlockedResp_sde(t *testing.T) {
})
require.NoError(t, err)
resp, err := msgs.NewBlockedResp(tc.req)
resp, err := msgs.NewBlockedResp(tc.req, nil)
require.NoError(t, err)
require.NotNil(t, resp)

View File

@@ -232,6 +232,27 @@ func NewSRV(name string, ttl uint32, target string, prio, weight, port uint16) (
}
}
// NewSVCB constructs the new resource record of type SVCB.
func NewSVCB(
name string,
ttl uint32,
target string,
prio uint16,
values ...dns.SVCBKeyValue,
) (rr dns.RR) {
return &dns.SVCB{
Hdr: dns.RR_Header{
Name: dns.Fqdn(name),
Rrtype: dns.TypeSVCB,
Class: dns.ClassINET,
Ttl: ttl,
},
Priority: prio,
Target: target,
Value: values,
}
}
// NewTXT constructs the new resource record of type TXT. txts are put into the
// TXT record as is.
func NewTXT(name string, ttl uint32, txts ...string) (rr dns.RR) {
@@ -246,6 +267,20 @@ func NewTXT(name string, ttl uint32, txts ...string) (rr dns.RR) {
}
}
// NewMX constructs the new resource record of type MX.
func NewMX(name string, ttl uint32, preference uint16, mx string) (rr dns.RR) {
return &dns.MX{
Hdr: dns.RR_Header{
Name: dns.Fqdn(name),
Rrtype: dns.TypeMX,
Class: dns.ClassINET,
Ttl: ttl,
},
Preference: preference,
Mx: mx,
}
}
// NewSOA constructs the new resource record of type SOA.
func NewSOA(name string, ttl uint32, ns, mbox string) (rr dns.RR) {
return &dns.SOA{

View File

@@ -1,9 +1,9 @@
module github.com/AdguardTeam/AdGuardDNS/internal/dnsserver
go 1.25.1
go 1.25.3
require (
github.com/AdguardTeam/golibs v0.34.1
github.com/AdguardTeam/golibs v0.35.2
github.com/ameshkov/dnscrypt/v2 v2.4.0
github.com/ameshkov/dnsstamps v1.0.3
github.com/bluele/gcache v0.0.2
@@ -11,34 +11,33 @@ require (
github.com/miekg/dns v1.1.68
github.com/panjf2000/ants/v2 v2.11.3
github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible
github.com/prometheus/client_golang v1.23.1
github.com/quic-go/quic-go v0.54.0
github.com/prometheus/client_golang v1.23.2
github.com/quic-go/quic-go v0.55.0
github.com/stretchr/testify v1.11.1
golang.org/x/net v0.44.0
golang.org/x/sys v0.36.0
golang.org/x/net v0.46.0
golang.org/x/sys v0.37.0
)
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.2-0.20180830191138-d8f796af33cc // indirect
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
github.com/kr/text v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.0 // indirect
github.com/prometheus/common v0.67.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
golang.org/x/mod v0.28.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -1,4 +1,7 @@
github.com/AdguardTeam/golibs v0.34.1 h1:RyBpZiXnJqlO3T+xjWldlxsEZDelmaFfKvXiJHDZZFQ=
github.com/AdguardTeam/golibs v0.35.0 h1:O990+tbZ5W5yB0ybtaUJy4FUb0bXxyzeUC7t8cr1pCg=
github.com/AdguardTeam/golibs v0.35.0/go.mod h1:y552twxCtvOD8KKQ7ESjo10KZBAE+HSj24yAuAvz9IA=
github.com/AdguardTeam/golibs v0.35.2 h1:GVlx/CiCz5ZXQmyvFrE3JyeGsgubE8f4rJvRshYJVVs=
github.com/AdguardTeam/golibs v0.35.2/go.mod h1:p/l6tG7QCv+Hi5yVpv1oZInoatRGOWoyD1m+Ume+ZNY=
github.com/ameshkov/dnscrypt/v2 v2.4.0 h1:if6ZG2cuQmcP2TwSY+D0+8+xbPfoatufGlOQTMNkI9o=
github.com/ameshkov/dnscrypt/v2 v2.4.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI=
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
@@ -16,7 +19,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
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=
@@ -31,16 +33,18 @@ github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible
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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.1 h1:w6gXMLQGgd0jXXlote9lRHMe0nG01EbnJT+C0EJru2Y=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.0 h1:K/rJPHrG3+AoQs50r2+0t7zMnMzek2Vbv31OFVsMeVY=
github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI=
github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -51,18 +55,35 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -84,7 +84,9 @@ func newTestService(
SafeBrowsing: &filter.ConfigSafeBrowsing{},
},
Access: access.EmptyProfile{},
AdultBlockingMode: nil,
BlockingMode: &dnsmsg.BlockingModeNullIP{},
SafeBrowsingBlockingMode: nil,
ID: dnssvctest.ProfileID,
DeviceIDs: container.NewMapSet(dnssvctest.DeviceID),
FilteredResponseTTL: agdtest.FilteredResponseTTL,

View File

@@ -124,14 +124,18 @@ var (
// Common profiles, devices, and results for tests.
var (
profNormal = &agd.Profile{
AdultBlockingMode: &dnsmsg.BlockingModeNullIP{},
BlockingMode: &dnsmsg.BlockingModeNullIP{},
SafeBrowsingBlockingMode: &dnsmsg.BlockingModeNullIP{},
ID: dnssvctest.ProfileID,
DeviceIDs: container.NewMapSet(dnssvctest.DeviceID),
Deleted: false,
}
profDeleted = &agd.Profile{
AdultBlockingMode: &dnsmsg.BlockingModeNullIP{},
BlockingMode: &dnsmsg.BlockingModeNullIP{},
SafeBrowsingBlockingMode: &dnsmsg.BlockingModeNullIP{},
ID: dnssvctest.ProfileID,
DeviceIDs: container.NewMapSet(dnssvctest.DeviceID),
Deleted: true,

View File

@@ -232,46 +232,45 @@ func (mw *Middleware) handleBadResolverARPA(
return errors.Annotate(err, "writing nodata resp for %q: %w", ri.Host)
}
// specialDomainHandler returns a handler that can handle a special-domain
// query for Apple Private Relay or Firefox canary domain based on the request
// or profile information, as well as the handler's name for debugging.
// specialDomainHandler, based on the request or profile information, returns a
// handler for a special-domain query for the following third-party services:
// - [Apple Private Relay]
// - [Chrome private prefetch proxy]
// - [Firefox DNS-over-HTTPS disabled detector]
//
// It also returns the handler's name for debugging. ri must not be nil.
//
// [Apple Private Relay]: https://developer.apple.com/icloud/prepare-your-network-for-icloud-private-relay
// [Chrome private prefetch proxy]: https://developer.chrome.com/docs/privacy-security/private-prefetch-proxy-for-network-admins
// [Firefox DNS-over-HTTPS disabled detector]: https://support.mozilla.org/en-US/kb/configuring-networks-disable-dns-over-https
func (mw *Middleware) specialDomainHandler(
ri *agd.RequestInfo,
) (f reqInfoHandlerFunc, name string) {
qt := ri.QType
if qt != dns.TypeA && qt != dns.TypeAAAA {
return nil, ""
}
host := ri.Host
prof, _ := ri.DeviceData()
switch host {
case
ApplePrivateRelayMaskHost,
ApplePrivateRelayMaskH2Host,
ApplePrivateRelayMaskCanaryHost:
if shouldBlockPrivateRelay(ri, prof) {
switch {
case shouldBlockPrivateRelay(ri):
return mw.handlePrivateRelay, "apple_private_relay"
}
case ChromePrefetchHost:
if shouldBlockChromePrefetch(ri, prof) {
case shouldBlockChromePrefetch(ri):
return mw.handleChromePrefetch, "chrome_prefetch"
}
case FirefoxCanaryHost:
if shouldBlockFirefoxCanary(ri, prof) {
case shouldBlockFirefoxCanary(ri):
return mw.handleFirefoxCanary, "firefox"
}
default:
// Go on.
}
return nil, ""
}
}
// shouldBlockChromePrefetch returns true request information or profile
// indicate that the Chrome prefetch domain should be blocked.
func shouldBlockChromePrefetch(ri *agd.RequestInfo, prof *agd.Profile) (ok bool) {
// shouldBlockChromePrefetch returns true if ri or the associated profile
// indicates that the Chrome prefetch domain should be blocked.
func shouldBlockChromePrefetch(ri *agd.RequestInfo) (ok bool) {
qt := ri.QType
if qt != dns.TypeA && qt != dns.TypeAAAA {
return false
}
if ri.Host != ChromePrefetchHost {
return false
}
prof, _ := ri.DeviceData()
if prof != nil {
return prof.BlockChromePrefetch
}
@@ -295,9 +294,19 @@ func (mw *Middleware) handleChromePrefetch(
return errors.Annotate(err, "writing chrome prefetch resp: %w")
}
// shouldBlockFirefoxCanary returns true request information or profile indicate
// that the Firefox canary domain should be blocked.
func shouldBlockFirefoxCanary(ri *agd.RequestInfo, prof *agd.Profile) (ok bool) {
// shouldBlockFirefoxCanary returns true if ri or the associated profile
// indicates that the Firefox canary domain should be blocked.
func shouldBlockFirefoxCanary(ri *agd.RequestInfo) (ok bool) {
qt := ri.QType
if qt != dns.TypeA && qt != dns.TypeAAAA {
return false
}
if ri.Host != FirefoxCanaryHost {
return false
}
prof, _ := ri.DeviceData()
if prof != nil {
return prof.BlockFirefoxCanary
}
@@ -321,9 +330,20 @@ func (mw *Middleware) handleFirefoxCanary(
return errors.Annotate(err, "writing firefox canary resp: %w")
}
// shouldBlockPrivateRelay returns true request information or profile indicate
// that the Apple Private Relay domain should be blocked.
func shouldBlockPrivateRelay(ri *agd.RequestInfo, prof *agd.Profile) (ok bool) {
// shouldBlockPrivateRelay returns true if ri or the associated profile
// indicates that the Apple Private Relay domain should be blocked.
func shouldBlockPrivateRelay(ri *agd.RequestInfo) (ok bool) {
switch ri.Host {
case
ApplePrivateRelayMaskHost,
ApplePrivateRelayMaskH2Host,
ApplePrivateRelayMaskCanaryHost:
// Go on.
default:
return false
}
prof, _ := ri.DeviceData()
if prof != nil {
return prof.BlockPrivateRelay
}

View File

@@ -71,8 +71,16 @@ func TestMiddleware_Wrap_specialDomain(t *testing.T) {
wantRCode: dns.RcodeSuccess,
}, {
reqInfo: newSpecDomReqInfo(t, nil, fltGrpBlocked, appleHost, dns.TypeTXT),
name: "no_private_relay_qtype",
wantRCode: dns.RcodeSuccess,
name: "private_relay_blocked_txt",
wantRCode: dns.RcodeNameError,
}, {
reqInfo: newSpecDomReqInfo(t, nil, fltGrpBlocked, appleHost, dns.TypeCNAME),
name: "private_relay_blocked_cname",
wantRCode: dns.RcodeNameError,
}, {
reqInfo: newSpecDomReqInfo(t, nil, fltGrpBlocked, appleHost, dns.TypeHTTPS),
name: "private_relay_blocked_https",
wantRCode: dns.RcodeNameError,
}, {
reqInfo: newSpecDomReqInfo(t, profBlocked, fltGrpAllowed, appleHost, dns.TypeA),
name: "private_relay_blocked_by_prof",

View File

@@ -1,12 +1,14 @@
package mainmw
import (
"cmp"
"context"
"fmt"
"slices"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
"github.com/miekg/dns"
@@ -220,7 +222,8 @@ func resultData(
// setFilteredResponse sets the response in fctx if the filtering results
// require that. After calling setFilteredResponse, fctx.filteredResponse will
// not be nil. All errors are reported using [Middleware.reportf].
// not be nil. All errors are reported using [Middleware.reportf]. fctx and ri
// must not be nil.
func (mw *Middleware) setFilteredResponse(
ctx context.Context,
fctx *filteringContext,
@@ -230,21 +233,28 @@ func (mw *Middleware) setFilteredResponse(
case nil:
mw.setFilteredResponseNoReq(ctx, fctx, ri)
case *filter.ResultBlocked:
var err error
fctx.filteredResponse, err = ri.Messages.NewBlockedResp(fctx.originalRequest)
if err != nil {
errcoll.Collect(
ctx,
mw.errColl,
mw.logger,
"creating blocked resp for filtered req",
err,
)
blockingMode := resultBlockingMode(ri, reqRes)
mw.setFilteredResponseFromBlockingMode(ctx, fctx, ri, blockingMode)
case *filter.ResultAllowed:
fctx.filteredResponse = fctx.originalResponse
case *filter.ResultModifiedRequest:
blockingMode := filterBlockingMode(ri, reqRes)
if blockingMode != nil {
mw.setFilteredResponseFromBlockingMode(ctx, fctx, ri, blockingMode)
return
}
case *filter.ResultAllowed, *filter.ResultModifiedRequest:
fctx.filteredResponse = fctx.originalResponse
case *filter.ResultModifiedResponse:
blockingMode := filterBlockingMode(ri, reqRes)
if blockingMode != nil {
mw.setFilteredResponseFromBlockingMode(ctx, fctx, ri, blockingMode)
return
}
// Only use the request filtering result in case it's already a
// response. Otherwise, it's a CNAME rewrite result, which isn't
// filtered after resolving.
@@ -259,6 +269,25 @@ func (mw *Middleware) setFilteredResponse(
}
}
// setFilteredResponseFromBlockingMode sets the response in fctx if for the
// given blocking mode. After calling, fctx.filteredResponse will not be nil.
// All errors are reported using [Middleware.reportf]. fctx and ri must not be
// nil.
func (mw *Middleware) setFilteredResponseFromBlockingMode(
ctx context.Context,
fctx *filteringContext,
ri *agd.RequestInfo,
blockingMode dnsmsg.BlockingMode,
) {
var err error
fctx.filteredResponse, err = ri.Messages.NewBlockedResp(fctx.originalRequest, blockingMode)
if err != nil {
errcoll.Collect(ctx, mw.errColl, mw.logger, "creating blocked resp for filtered req", err)
fctx.filteredResponse = fctx.originalResponse
}
}
// setFilteredResponseNoReq sets the response in fctx if the response filtering
// results require that. After calling setFilteredResponseNoReq,
// fctx.filteredResponse will not be nil. All errors are reported using
@@ -273,8 +302,10 @@ func (mw *Middleware) setFilteredResponseNoReq(
case nil, *filter.ResultAllowed:
fctx.filteredResponse = fctx.originalResponse
case *filter.ResultBlocked:
blockingMode := resultBlockingMode(ri, respRes)
var err error
fctx.filteredResponse, err = ri.Messages.NewBlockedResp(fctx.originalRequest)
fctx.filteredResponse, err = ri.Messages.NewBlockedResp(fctx.originalRequest, blockingMode)
if err != nil {
errcoll.Collect(
ctx,
@@ -296,3 +327,47 @@ func (mw *Middleware) setFilteredResponseNoReq(
})
}
}
// resultBlockingMode returns the blocking mode for the given filtering result,
// returns profile's blocking mode if the result is not related to adult
// blocking or safe browsing filters. ri must not be nil.
//
// TODO(a.garipov): Remove this temp solution by improving blocking mode API.
func resultBlockingMode(ri *agd.RequestInfo, res filter.Result) (m dnsmsg.BlockingMode) {
profile, _ := ri.DeviceData()
if profile == nil {
return nil
}
fltID, _ := res.MatchedRule()
switch fltID {
case filter.IDAdultBlocking:
return cmp.Or(profile.AdultBlockingMode, profile.BlockingMode)
case filter.IDSafeBrowsing:
return cmp.Or(profile.SafeBrowsingBlockingMode, profile.BlockingMode)
}
return profile.BlockingMode
}
// filterBlockingMode returns the blocking mode for the given filtering result,
// returns nil if the result is not related to adult blocking or safe browsing
// filters. ri must not be nil.
//
// TODO(a.garipov): Remove this temp solution by improving blocking mode API.
func filterBlockingMode(ri *agd.RequestInfo, res filter.Result) (m dnsmsg.BlockingMode) {
profile, _ := ri.DeviceData()
if profile == nil {
return nil
}
fltID, _ := res.MatchedRule()
switch fltID {
case filter.IDAdultBlocking:
return profile.AdultBlockingMode
case filter.IDSafeBrowsing:
return profile.SafeBrowsingBlockingMode
}
return nil
}

View File

@@ -9,7 +9,9 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/dnsservertest"
"github.com/AdguardTeam/AdGuardDNS/internal/dnssvc/internal/dnssvctest"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
"github.com/AdguardTeam/golibs/testutil"
"github.com/miekg/dns"
@@ -19,14 +21,18 @@ import (
// TODO(a.garipov): Rewrite into cases in external tests.
func TestMiddleware_setFilteredResponse(t *testing.T) {
t.Parallel()
const (
respTTL = 60
fltRespTTL = agdtest.FilteredResponseTTLSec
)
const fltRespTTL = agdtest.FilteredResponseTTLSec
respIP := netip.MustParseAddr("1.2.3.4")
rewrIP := netip.MustParseAddr("5.6.7.8")
blockIP := netip.IPv4Unspecified()
blockIPAdult := netip.MustParseAddr("2.2.2.2")
blockIPSafeBrowsing := netip.MustParseAddr("3.3.3.3")
const domain = "example.com"
origReq := dnsservertest.NewReq(domain, dns.TypeA, dns.ClassINET)
@@ -61,6 +67,22 @@ func TestMiddleware_setFilteredResponse(t *testing.T) {
wantIP: blockIP,
name: "blocked_req",
wantTTL: fltRespTTL,
}, {
reqRes: &filter.ResultBlocked{
List: filter.IDAdultBlocking,
},
respRes: nil,
wantIP: blockIPAdult,
name: "blocked_req_adult",
wantTTL: fltRespTTL,
}, {
reqRes: &filter.ResultBlocked{
List: filter.IDSafeBrowsing,
},
respRes: nil,
wantIP: blockIPSafeBrowsing,
name: "blocked_req_safe_browsing",
wantTTL: fltRespTTL,
}, {
reqRes: &filter.ResultModifiedResponse{Msg: rewrResp},
respRes: nil,
@@ -79,14 +101,46 @@ func TestMiddleware_setFilteredResponse(t *testing.T) {
wantIP: blockIP,
name: "blocked_resp",
wantTTL: fltRespTTL,
}, {
reqRes: nil,
respRes: &filter.ResultBlocked{
List: filter.IDAdultBlocking,
},
wantIP: blockIPAdult,
name: "blocked_resp_adult",
wantTTL: fltRespTTL,
}, {
reqRes: nil,
respRes: &filter.ResultBlocked{
List: filter.IDSafeBrowsing,
},
wantIP: blockIPSafeBrowsing,
name: "blocked_resp_safe_browsing",
wantTTL: fltRespTTL,
}}
device := &agd.Device{ID: dnssvctest.DeviceID}
profile := &agd.Profile{
ID: dnssvctest.ProfileID,
AdultBlockingMode: &dnsmsg.BlockingModeCustomIP{
IPv4: []netip.Addr{blockIPAdult},
},
SafeBrowsingBlockingMode: &dnsmsg.BlockingModeCustomIP{
IPv4: []netip.Addr{blockIPSafeBrowsing},
},
}
ri := &agd.RequestInfo{
DeviceResult: &agd.DeviceResultOK{
Device: device,
Profile: profile,
},
Messages: agdtest.NewConstructor(t),
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
origResp := dnsservertest.NewResp(dns.RcodeSuccess, origReq)
origResp.Answer = append(origResp.Answer, dnsservertest.NewA(domain, respTTL, respIP))
@@ -115,6 +169,8 @@ func TestMiddleware_setFilteredResponse(t *testing.T) {
}
t.Run("modified_resp", func(t *testing.T) {
t.Parallel()
wantPanicMsg := (&agd.ArgumentError{
Name: "respRes",
Message: fmt.Sprintf("unexpected type %T", &filter.ResultModifiedResponse{}),

View File

@@ -14,14 +14,16 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/AdGuardDNS/internal/querylog"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/optslog"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/miekg/dns"
)
// recordQueryInfo extracts loggable information from request, response, and
// filtering data and writes them to the query log, billing, and filtering-rule
// statistics, handling non-critical errors.
// statistics, handling non-critical errors. fctx and ri must not be nil.
func (mw *Middleware) recordQueryInfo(
ctx context.Context,
fctx *filteringContext,
@@ -39,8 +41,8 @@ func (mw *Middleware) recordQueryInfo(
var reqCtry geoip.Country
var reqASN geoip.ASN
if g := ri.Location; g != nil {
reqCtry, reqASN = g.Country, g.ASN
if loc := ri.Location; loc != nil {
reqCtry, reqASN = loc.Country, loc.ASN
}
reqInfo := dnsserver.MustRequestInfoFromContext(ctx)
@@ -51,21 +53,6 @@ func (mw *Middleware) recordQueryInfo(
return
}
rcode, respIP, respDNSSEC := mw.responseData(ctx, fctx.filteredResponse)
if blocked {
// If the request or the response were blocked, resp may contain an
// unspecified IP address, a rewritten IP address, or none at all, while
// the original response may contain an actual IP address that should be
// used to determine the response country.
_, respIP, _ = mw.responseData(ctx, fctx.originalResponse)
}
var clientIP netip.Addr
if prof.IPLogEnabled {
clientIP = ri.RemoteIP
}
q := fctx.originalRequest.Question[0]
e := &querylog.Entry{
RequestResult: fctx.requestResult,
ResponseResult: fctx.responseResult,
@@ -74,20 +61,50 @@ func (mw *Middleware) recordQueryInfo(
ProfileID: prof.ID,
DeviceID: devID,
ClientCountry: reqCtry,
ResponseCountry: mw.responseCountry(ctx, fctx, ri.Host, respIP, rcode),
DomainFQDN: q.Name,
DomainFQDN: fctx.originalRequest.Question[0].Name,
Elapsed: time.Since(start),
ClientASN: reqASN,
RequestType: ri.QType,
ResponseCode: rcode,
Protocol: ri.ServerInfo.Protocol,
DNSSEC: respDNSSEC,
RemoteIP: clientIP,
}
mw.writeQueryLogEntry(ctx, fctx, ri, e, blocked, prof.IPLogEnabled)
}
// writeQueryLogEntry adds properties to e and writes it to the query log.
// fctx, ri, and e must not be nil.
func (mw *Middleware) writeQueryLogEntry(
ctx context.Context,
fctx *filteringContext,
ri *agd.RequestInfo,
e *querylog.Entry,
blocked bool,
logIP bool,
) {
var respIP netip.Addr
e.ResponseCode, respIP, e.DNSSEC = mw.responseData(ctx, fctx.filteredResponse)
if blocked {
// If the request or the response were blocked, resp may contain an
// unspecified IP address, a rewritten IP address, or none at all, while
// the original response may contain an actual IP address that should be
// used to determine the response country.
_, respIP, _ = mw.responseData(ctx, fctx.originalResponse)
}
if logIP {
e.RemoteIP = ri.RemoteIP
}
e.ResponseCountry = mw.responseCountry(ctx, fctx, ri.Host, respIP, e.ResponseCode)
err := mw.queryLog.Write(ctx, e)
if err != nil {
// Consider query logging errors non-critical.
// Consider query logging errors non-critical and don't collect timeouts.
switch {
case err == nil:
// Go on.
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
mw.logger.DebugContext(ctx, "writing query log", slogutil.KeyError, err)
default:
errcoll.Collect(ctx, mw.errColl, mw.logger, "writing query log", err)
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -4,13 +4,14 @@ import (
"context"
"fmt"
"log/slog"
"net/http/cookiejar"
"net/netip"
"net/url"
"path"
"strings"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/AdguardTeam/AdGuardDNS/internal/agdnet"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
@@ -18,9 +19,9 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/rulelist"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/service"
"github.com/AdguardTeam/golibs/syncutil"
"github.com/c2h5oh/datasize"
"github.com/miekg/dns"
"golang.org/x/net/publicsuffix"
)
// FilterConfig is the hash-prefix filter configuration structure.
@@ -43,12 +44,16 @@ type FilterConfig struct {
// ErrColl is used to collect non-critical and rare errors.
ErrColl errcoll.Interface
// HashPrefixMtcs are the specific metrics for the hashprefix filter.
HashPrefixMtcs Metrics
// HashPrefixMetrics are the specific metrics for the hashprefix filter.
HashPrefixMetrics Metrics
// Metrics are the metrics for the hashprefix filter.
Metrics filter.Metrics
// PublicSuffixList is used for obtaining public suffix for specified
// domain.
PublicSuffixList cookiejar.PublicSuffixList
// ID is the ID of this hash storage for logging and error reporting.
ID filter.ID
@@ -79,6 +84,10 @@ type FilterConfig struct {
// MaxSize is the maximum size of the downloadable rule-list.
MaxSize datasize.ByteSize
// SubDomainNum defines how many labels should be hashed to match against a
// hash prefix filter. It must be positive and fit into int.
SubDomainNum uint
}
// Filter is a filter that matches hosts by their hashes based on a hash-prefix
@@ -88,13 +97,16 @@ type Filter struct {
cloner *dnsmsg.Cloner
hashes *Storage
refr *refreshable.Refreshable
subDomainsPool *syncutil.Pool[[]string]
errColl errcoll.Interface
hashprefixMtcs Metrics
hashprefixMtrc Metrics
publicSuffixList cookiejar.PublicSuffixList
metrics filter.Metrics
resCache agdcache.Interface[rulelist.CacheKey, filter.Result]
id filter.ID
repIP netip.Addr
repFQDN string
subDomainNum int
}
// IDPrefix is a common prefix for cache IDs, logging, and refreshes of
@@ -119,11 +131,19 @@ func NewFilter(c *FilterConfig) (f *Filter, err error) {
logger: c.Logger,
cloner: c.Cloner,
hashes: c.Hashes,
// #nosec G115 -- Assume that c.SubDomainNum is always less then or
// equal to 63.
//
// TODO(f.setrakov): Validate c.SubDomainsNum.
subDomainsPool: syncutil.NewSlicePool[string](int(c.SubDomainNum)),
errColl: c.ErrColl,
hashprefixMtcs: c.HashPrefixMtcs,
hashprefixMtrc: c.HashPrefixMetrics,
publicSuffixList: c.PublicSuffixList,
metrics: c.Metrics,
resCache: resCache,
id: id,
// #nosec G115 -- The value is a constant less than int accommodates.
subDomainNum: int(c.SubDomainNum),
}
repHost := c.ReplacementHost
@@ -165,7 +185,7 @@ func (f *Filter) FilterRequest(
cacheKey := rulelist.NewCacheKey(host, qt, cl, false)
item, ok := f.resCache.Get(cacheKey)
f.hashprefixMtcs.IncrementLookups(ctx, ok)
f.hashprefixMtrc.IncrementLookups(ctx, ok)
if ok {
return f.clonedResult(req.DNS, item), nil
}
@@ -176,8 +196,11 @@ func (f *Filter) FilterRequest(
}
var matched string
sub := hashableSubdomains(host)
for _, s := range sub {
subPtr := f.subDomainsPool.Get()
defer f.subDomainsPool.Put(subPtr)
*subPtr = agdnet.AppendSubdomains((*subPtr)[:0], host, f.subDomainNum, f.publicSuffixList)
for _, s := range *subPtr {
if f.hashes.Matches(s) {
matched = s
@@ -199,7 +222,7 @@ func (f *Filter) FilterRequest(
f.setInCache(cacheKey, r)
f.hashprefixMtcs.UpdateCacheSize(ctx, f.resCache.Len())
f.hashprefixMtrc.UpdateCacheSize(ctx, f.resCache.Len())
return r, nil
}
@@ -232,7 +255,7 @@ func (f *Filter) clonedResult(req *dns.Msg, r filter.Result) (clone filter.Resul
}
}
// filteredResult returns a filtered request or response.
// filteredResult returns a filtered request or response. req must not be nil.
func (f *Filter) filteredResult(
req *filter.Request,
matched string,
@@ -274,7 +297,7 @@ func (f *Filter) respForFamily(
//
// TODO(ameshkov): Consider putting the resolved IP addresses into hints
// to show the blocked page here as well?
return req.Messages.NewBlockedResp(req.DNS)
return req.Messages.NewBlockedResp(req.DNS, nil)
}
ip := f.repIP
@@ -368,40 +391,3 @@ func (f *Filter) refresh(ctx context.Context, acceptStale bool) (err error) {
return nil
}
// subDomainNum defines how many labels should be hashed to match against a hash
// prefix filter.
const subDomainNum = 4
// hashableSubdomains returns all subdomains that should be checked by the hash
// prefix filter.
func hashableSubdomains(domain string) (sub []string) {
pubSuf, icann := publicsuffix.PublicSuffix(domain)
if !icann {
// Check the full private domain space.
pubSuf = ""
}
dotsNum := 0
i := strings.LastIndexFunc(domain, func(r rune) (ok bool) {
if r == '.' {
dotsNum++
}
return dotsNum == subDomainNum
})
if i != -1 {
domain = domain[i+1:]
}
sub = netutil.Subdomains(domain)
for i, s := range sub {
if s == pubSuf {
sub = sub[:i]
break
}
}
return sub
}

View File

@@ -20,12 +20,13 @@ import (
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/publicsuffix"
)
// type check
var _ composite.RequestFilter = (*hashprefix.Filter)(nil)
func TestFilter_FilterRequest_host(t *testing.T) {
func TestFilter_FilterRequest(t *testing.T) {
t.Parallel()
msgs := agdtest.NewConstructor(t)
@@ -115,7 +116,7 @@ func TestFilter_FilterRequest_host(t *testing.T) {
var wantRes filter.Result
if tc.wantResult {
if tc.replHost == filtertest.HostAdultContentRepl {
wantRes = newModReqResult(req, tc.wantRule)
wantRes = newModReqResult(t, req, tc.wantRule)
} else {
wantRes = newModRespResult(t, req, msgs, filtertest.IPv4AdultContentRepl)
}
@@ -124,9 +125,15 @@ func TestFilter_FilterRequest_host(t *testing.T) {
filtertest.AssertEqualResult(t, wantRes, r)
})
}
}
func TestFilter_FilterRequest_cache(t *testing.T) {
t.Parallel()
f := filtertest.NewHashprefixFilter(t, filter.IDAdultBlocking)
require.True(t, t.Run("cached_success", func(t *testing.T) {
f := filtertest.NewHashprefixFilter(t, filter.IDAdultBlocking)
t.Parallel()
req := filtertest.NewARequest(t, filtertest.HostAdultContent)
@@ -141,21 +148,27 @@ func TestFilter_FilterRequest_host(t *testing.T) {
}))
require.True(t, t.Run("cached_no_match", func(t *testing.T) {
f := filtertest.NewHashprefixFilter(t, filter.IDAdultBlocking)
t.Parallel()
req := filtertest.NewARequest(t, filtertest.Host)
ctx := testutil.ContextWithTimeout(t, filtertest.Timeout)
r, err := f.FilterRequest(ctx, req)
original, err := f.FilterRequest(ctx, req)
require.NoError(t, err)
cached, err := f.FilterRequest(ctx, req)
require.NoError(t, err)
filtertest.AssertEqualResult(t, cached, r)
filtertest.AssertEqualResult(t, cached, original)
}))
}
func TestFilter_FilterRequest_https(t *testing.T) {
t.Parallel()
require.True(t, t.Run("https", func(t *testing.T) {
t.Parallel()
f := filtertest.NewHashprefixFilter(t, filter.IDAdultBlocking)
req := filtertest.NewRequest(
@@ -171,11 +184,13 @@ func TestFilter_FilterRequest_host(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, r)
wantRes := newModReqResult(req.DNS, filtertest.HostAdultContent)
wantRes := newModReqResult(t, req.DNS, filtertest.HostAdultContent)
filtertest.AssertEqualResult(t, wantRes, r)
}))
require.True(t, t.Run("https_ip", func(t *testing.T) {
t.Parallel()
f := filtertest.NewHashprefixFilterWithRepl(
t,
filter.IDAdultBlocking,
@@ -204,37 +219,6 @@ func TestFilter_FilterRequest_host(t *testing.T) {
}))
}
// newModRespResult is a helper for creating modified results for tests.
func newModRespResult(
tb testing.TB,
req *dns.Msg,
messages *dnsmsg.Constructor,
replIP netip.Addr,
) (r *filter.ResultModifiedResponse) {
tb.Helper()
resp, err := messages.NewRespIP(req, replIP)
require.NoError(tb, err)
return &filter.ResultModifiedResponse{
Msg: resp,
List: filter.IDAdultBlocking,
Rule: filtertest.HostAdultContent,
}
}
// newModReqResult is a helper for creating modified results for tests.
func newModReqResult(req *dns.Msg, rule filter.RuleText) (r *filter.ResultModifiedRequest) {
req = dnsmsg.Clone(req)
req.Question[0].Name = filtertest.FQDNAdultContentRepl
return &filter.ResultModifiedRequest{
Msg: req,
List: filter.IDAdultBlocking,
Rule: rule,
}
}
func TestFilter_Refresh(t *testing.T) {
t.Parallel()
@@ -251,8 +235,9 @@ func TestFilter_Refresh(t *testing.T) {
Hashes: strg,
URL: srvURL,
ErrColl: agdtest.NewErrorCollector(),
HashPrefixMtcs: hashprefix.EmptyMetrics{},
HashPrefixMetrics: hashprefix.EmptyMetrics{},
Metrics: filter.EmptyMetrics{},
PublicSuffixList: publicsuffix.List,
ID: filter.IDAdultBlocking,
CachePath: cachePath,
ReplacementHost: filtertest.HostAdultContentRepl,
@@ -260,6 +245,7 @@ func TestFilter_Refresh(t *testing.T) {
CacheTTL: filtertest.CacheTTL,
CacheCount: filtertest.CacheCount,
MaxSize: filtertest.FilterMaxSize,
SubDomainNum: filtertest.SubDomainNum,
})
require.NoError(t, err)
@@ -304,8 +290,9 @@ func TestFilter_FilterRequest_staleCache(t *testing.T) {
Hashes: strg,
URL: srvURL,
ErrColl: agdtest.NewErrorCollector(),
HashPrefixMtcs: hashprefix.EmptyMetrics{},
HashPrefixMetrics: hashprefix.EmptyMetrics{},
Metrics: filter.EmptyMetrics{},
PublicSuffixList: publicsuffix.List,
ID: filter.IDAdultBlocking,
CachePath: cachePath,
ReplacementHost: filtertest.HostAdultContentRepl,
@@ -313,6 +300,7 @@ func TestFilter_FilterRequest_staleCache(t *testing.T) {
CacheTTL: filtertest.CacheTTL,
CacheCount: filtertest.CacheCount,
MaxSize: filtertest.FilterMaxSize,
SubDomainNum: filtertest.SubDomainNum,
}
f, err := hashprefix.NewFilter(fconf)
require.NoError(t, err)
@@ -339,7 +327,7 @@ func TestFilter_FilterRequest_staleCache(t *testing.T) {
r, err = f.FilterRequest(ctx, otherHostReq)
require.NoError(t, err)
wantRes := newModReqResult(otherHostReq.DNS, filtertest.Host)
wantRes := newModReqResult(t, otherHostReq.DNS, filtertest.Host)
filtertest.AssertEqualResult(t, wantRes, r)
}))
@@ -374,7 +362,46 @@ func TestFilter_FilterRequest_staleCache(t *testing.T) {
r, err = f.FilterRequest(ctx, hostReq)
require.NoError(t, err)
wantRes := newModReqResult(hostReq.DNS, filtertest.HostAdultContent)
wantRes := newModReqResult(t, hostReq.DNS, filtertest.HostAdultContent)
filtertest.AssertEqualResult(t, wantRes, r)
}))
}
// newModRespResult is a helper for creating modified response result for tests.
// req must not be nil.
func newModRespResult(
tb testing.TB,
req *dns.Msg,
messages *dnsmsg.Constructor,
replIP netip.Addr,
) (r *filter.ResultModifiedResponse) {
tb.Helper()
resp, err := messages.NewRespIP(req, replIP)
require.NoError(tb, err)
return &filter.ResultModifiedResponse{
Msg: resp,
List: filter.IDAdultBlocking,
Rule: filtertest.HostAdultContent,
}
}
// newModReqResult is a helper for creating modified request result for tests.
// req must not be nil.
func newModReqResult(
tb testing.TB,
req *dns.Msg,
rule filter.RuleText,
) (r *filter.ResultModifiedRequest) {
tb.Helper()
req = dnsmsg.Clone(req)
req.Question[0].Name = filtertest.FQDNAdultContentRepl
return &filter.ResultModifiedRequest{
Msg: req,
List: filter.IDAdultBlocking,
Rule: rule,
}
}

View File

@@ -67,6 +67,8 @@ func newComposite(tb testing.TB, c *composite.Config) (f *composite.Filter) {
}
func TestFilter_FilterRequest_customWithClientName(t *testing.T) {
t.Parallel()
f := newComposite(t, &composite.Config{
Custom: custom.New(&custom.Config{
Logger: slogutil.NewDiscardLogger(),
@@ -96,6 +98,8 @@ func TestFilter_FilterRequest_customWithClientName(t *testing.T) {
}
func TestFilter_FilterRequest_badfilter(t *testing.T) {
t.Parallel()
const (
blockRule = filtertest.RuleBlockStr
badFilterRule = filtertest.RuleBlockStr + "$badfilter"
@@ -127,6 +131,8 @@ func TestFilter_FilterRequest_badfilter(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
f := newComposite(t, &composite.Config{
RuleLists: tc.ruleLists,
})
@@ -149,6 +155,8 @@ func newFromStr(tb testing.TB, text string, id filter.ID) (rl *rulelist.Refresha
}
func TestFilter_FilterRequest_customAllow(t *testing.T) {
t.Parallel()
const allowRule = "@@" + filtertest.RuleBlock
blockingRL := newFromStr(t, filtertest.RuleBlockStr, filtertest.RuleListID1)
@@ -174,86 +182,53 @@ func TestFilter_FilterRequest_customAllow(t *testing.T) {
}
func TestFilter_FilterRequest_dnsrewrite(t *testing.T) {
t.Parallel()
const (
blockRule = filtertest.RuleBlockStr
dnsRewriteRuleRefused = filtertest.RuleBlockStr + "$dnsrewrite=REFUSED"
dnsRewriteRuleCname = filtertest.RuleBlockStr + "$dnsrewrite=new-cname.example"
dnsRewriteRuleCNAME = filtertest.RuleBlockStr + "$dnsrewrite=" + filtertest.HostCNAME
dnsRewrite2Rules = filtertest.RuleBlockStr + "$dnsrewrite=1.2.3.4\n" +
filtertest.RuleBlockStr + "$dnsrewrite=1.2.3.5"
dnsRewriteRuleTXT = filtertest.RuleBlockStr + "$dnsrewrite=NOERROR;TXT;abcdefg"
dnsRewriteRuleSOA = filtertest.RuleBlockStr + "$dnsrewrite=NOERROR;SOA;ns1." +
filtertest.FQDNBlocked + " hostmaster." + filtertest.FQDNBlocked +
" 1 3600 1800 604800 86400"
dnsRewriteTypedRules = dnsRewriteRuleTXT + "\n" + dnsRewriteRuleSOA
)
var (
rlNonRewrite = newFromStr(t, blockRule, filtertest.RuleListID1)
rlNonRewrite = newFromStr(t, filtertest.RuleBlockStr, filtertest.RuleListID1)
rlCustomRefused = newCustom(t, dnsRewriteRuleRefused)
rlCustomCname = newCustom(t, dnsRewriteRuleCname)
rlCustomCNAME = newCustom(t, dnsRewriteRuleCNAME)
rlCustom2Rules = newCustom(t, dnsRewrite2Rules)
rlCustomTyped = newCustom(t, dnsRewriteTypedRules)
)
req := dnsservertest.NewReq(filtertest.FQDNBlocked, dns.TypeA, dns.ClassINET)
// Create a CNAME-modified request.
modifiedReq := dnsmsg.Clone(req)
modifiedReq.Question[0].Name = "new-cname.example."
txtReq := dnsmsg.Clone(req)
txtReq.Question[0].Qtype = dns.TypeTXT
soaReq := dnsmsg.Clone(req)
soaReq.Question[0].Qtype = dns.TypeSOA
testCases := []struct {
custom filter.Custom
req *dns.Msg
wantRes filter.Result
name string
ruleLists []*rulelist.Refreshable
}{{
custom: nil,
req: req,
wantRes: &filter.ResultBlocked{
List: filtertest.RuleListID1,
Rule: blockRule,
Rule: filtertest.RuleBlockStr,
},
name: "block",
ruleLists: []*rulelist.Refreshable{rlNonRewrite},
}, {
custom: nil,
req: req,
wantRes: &filter.ResultBlocked{
List: filtertest.RuleListID1,
Rule: blockRule,
},
name: "dnsrewrite_no_effect",
ruleLists: []*rulelist.Refreshable{rlNonRewrite},
}, {
custom: rlCustomRefused,
req: req,
wantRes: &filter.ResultModifiedResponse{
Msg: dnsservertest.NewResp(dns.RcodeRefused, req),
List: filter.IDCustom,
Rule: dnsRewriteRuleRefused,
},
name: "dnsrewrite_block",
ruleLists: []*rulelist.Refreshable{rlNonRewrite},
}, {
custom: rlCustomCname,
req: req,
custom: rlCustomCNAME,
wantRes: &filter.ResultModifiedRequest{
Msg: modifiedReq,
Msg: dnsservertest.NewReq(filtertest.FQDNCname, dns.TypeA, dns.ClassINET),
List: filter.IDCustom,
Rule: dnsRewriteRuleCname,
Rule: dnsRewriteRuleCNAME,
},
name: "dnsrewrite_cname",
ruleLists: []*rulelist.Refreshable{rlNonRewrite},
}, {
custom: rlCustom2Rules,
req: req,
wantRes: &filter.ResultModifiedResponse{
Msg: dnsservertest.NewResp(dns.RcodeSuccess, req, dnsservertest.SectionAnswer{
dnsservertest.NewA(
@@ -271,9 +246,52 @@ func TestFilter_FilterRequest_dnsrewrite(t *testing.T) {
Rule: "",
},
name: "dnsrewrite_answers",
ruleLists: []*rulelist.Refreshable{rlNonRewrite},
}, {
custom: rlCustomTyped,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
f := newComposite(t, &composite.Config{
Custom: tc.custom,
RuleLists: []*rulelist.Refreshable{rlNonRewrite},
})
ctx := testutil.ContextWithTimeout(t, filtertest.Timeout)
res, err := f.FilterRequest(ctx, &filter.Request{
DNS: req,
Messages: agdtest.NewConstructor(t),
Host: filtertest.HostBlocked,
QType: req.Question[0].Qtype,
})
require.NoError(t, err)
filtertest.AssertEqualResult(t, tc.wantRes, res)
})
}
}
func TestFilter_FilterRequest_dnsrewriteQType(t *testing.T) {
t.Parallel()
const (
dnsRewriteRuleTXT = filtertest.RuleBlockStr + "$dnsrewrite=NOERROR;TXT;abcdefg"
dnsRewriteRuleSOA = filtertest.RuleBlockStr + "$dnsrewrite=NOERROR;SOA;ns1." +
filtertest.FQDNBlocked + " hostmaster." + filtertest.FQDNBlocked +
" 1 3600 1800 604800 86400"
dnsRewriteTypedRules = dnsRewriteRuleTXT + "\n" + dnsRewriteRuleSOA
)
txtReq := dnsservertest.NewReq(filtertest.FQDNBlocked, dns.TypeTXT, dns.ClassINET)
soaReq := dnsservertest.NewReq(filtertest.FQDNBlocked, dns.TypeSOA, dns.ClassINET)
testCases := []struct {
req *dns.Msg
wantRes filter.Result
name string
}{{
req: txtReq,
wantRes: &filter.ResultModifiedResponse{
Msg: dnsservertest.NewResp(dns.RcodeSuccess, txtReq, dnsservertest.SectionAnswer{
@@ -287,34 +305,30 @@ func TestFilter_FilterRequest_dnsrewrite(t *testing.T) {
Rule: "",
},
name: "dnsrewrite_txt",
ruleLists: []*rulelist.Refreshable{},
}, {
custom: rlCustomTyped,
req: soaReq,
wantRes: &filter.ResultModifiedResponse{
Msg: dnsservertest.NewResp(dns.RcodeSuccess, soaReq),
List: filter.IDCustom,
},
name: "dnsrewrite_soa",
ruleLists: []*rulelist.Refreshable{},
}}
f := newComposite(t, &composite.Config{
Custom: newCustom(t, dnsRewriteTypedRules),
RuleLists: []*rulelist.Refreshable{},
})
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
f := newComposite(t, &composite.Config{
Custom: tc.custom,
RuleLists: tc.ruleLists,
})
ctx := context.Background()
res, fltErr := f.FilterRequest(ctx, &filter.Request{
ctx := testutil.ContextWithTimeout(t, filtertest.Timeout)
res, err := f.FilterRequest(ctx, &filter.Request{
DNS: tc.req,
Messages: agdtest.NewConstructor(t),
Host: filtertest.HostBlocked,
QType: tc.req.Question[0].Qtype,
})
require.NoError(t, fltErr)
require.NoError(t, err)
filtertest.AssertEqualResult(t, tc.wantRes, res)
})
@@ -334,6 +348,8 @@ func newCustom(tb testing.TB, text string) (f *custom.Filter) {
}
func TestFilter_FilterRequest_hostsRules(t *testing.T) {
t.Parallel()
const (
reqHost4 = "www.example.com"
reqHost6 = "www.example.net"
@@ -420,6 +436,8 @@ func TestFilter_FilterRequest_hostsRules(t *testing.T) {
}
func TestFilter_FilterRequest_safeSearch(t *testing.T) {
t.Parallel()
const rewriteRule = filtertest.RuleSafeSearchGeneralIPv4Str + "\n"
cachePath, srvURL := filtertest.PrepareRefreshable(t, nil, rewriteRule, http.StatusOK)
@@ -469,6 +487,8 @@ func TestFilter_FilterRequest_safeSearch(t *testing.T) {
}
func TestFilter_FilterRequest_services(t *testing.T) {
t.Parallel()
svcRL := rulelist.NewImmutable(
[]byte(filtertest.RuleBlockStr),
filter.IDBlockedService,
@@ -492,23 +512,18 @@ func TestFilter_FilterRequest_services(t *testing.T) {
}
func TestFilter_FilterResponse(t *testing.T) {
const cnameReqFQDN = "sub." + filtertest.FQDNBlocked
t.Parallel()
const (
passedIPv4Str = "1.1.1.1"
blockedIPv4Str = "1.2.3.4"
blockedIPv6Str = "1234::cdef"
blockRules = filtertest.HostBlocked + "\n" +
blockedIPv4Str + "\n" +
blockedIPv6Str + "\n"
)
var (
passedIPv4 = netip.MustParseAddr(passedIPv4Str)
blockedIPv4 = netip.MustParseAddr(blockedIPv4Str)
blockedIPv6 = netip.MustParseAddr(blockedIPv6Str)
)
blockingRL := newFromStr(t, blockRules, filtertest.RuleListID1)
f := newComposite(t, &composite.Config{
RuleLists: []*rulelist.Refreshable{blockingRL},
@@ -517,48 +532,101 @@ func TestFilter_FilterResponse(t *testing.T) {
const ttl = agdtest.FilteredResponseTTLSec
testCases := []struct {
want filter.Result
name string
reqFQDN string
wantRule filter.RuleText
respAns dnsservertest.SectionAnswer
}{{
want: nil,
name: "pass",
reqFQDN: filtertest.FQDN,
respAns: dnsservertest.SectionAnswer{
dnsservertest.NewA(filtertest.FQDN, ttl, netip.MustParseAddr(passedIPv4Str)),
},
}, {
want: &filter.ResultBlocked{
List: filtertest.RuleListID1,
Rule: filtertest.HostBlocked,
},
name: "cname",
reqFQDN: filtertest.FQDNCname,
respAns: dnsservertest.SectionAnswer{
dnsservertest.NewCNAME(filtertest.FQDNCname, ttl, filtertest.FQDNBlocked),
dnsservertest.NewA(filtertest.FQDNBlocked, ttl, netip.MustParseAddr(blockedIPv4Str)),
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx, req := newReqDataWithFQDN(t, tc.reqFQDN)
res, err := f.FilterResponse(ctx, &filter.Response{
DNS: dnsservertest.NewResp(dns.RcodeSuccess, req.DNS, tc.respAns),
RemoteIP: filtertest.IPv4Client,
})
require.NoError(t, err)
assert.Equal(t, tc.want, res)
})
}
}
func TestFilter_FilterResponse_blocked(t *testing.T) {
t.Parallel()
const (
passedIPv4Str = "1.1.1.1"
blockedIPv4Str = "1.2.3.4"
blockedIPv6Str = "1234::cdef"
blockRules = filtertest.HostBlocked + "\n" +
blockedIPv4Str + "\n" +
blockedIPv6Str + "\n"
)
var (
blockedIPv4 = netip.MustParseAddr(blockedIPv4Str)
blockedIPv6 = netip.MustParseAddr(blockedIPv6Str)
)
resBlocked4 := &filter.ResultBlocked{
List: filtertest.RuleListID1,
Rule: blockedIPv4Str,
}
resBlocked6 := &filter.ResultBlocked{
List: filtertest.RuleListID1,
Rule: blockedIPv6Str,
}
blockingRL := newFromStr(t, blockRules, filtertest.RuleListID1)
f := newComposite(t, &composite.Config{
RuleLists: []*rulelist.Refreshable{blockingRL},
})
const ttl = agdtest.FilteredResponseTTLSec
testCases := []struct {
want filter.Result
name string
respAns dnsservertest.SectionAnswer
qType dnsmsg.RRType
}{{
name: "pass",
reqFQDN: filtertest.FQDN,
wantRule: "",
respAns: dnsservertest.SectionAnswer{
dnsservertest.NewA(filtertest.FQDN, ttl, passedIPv4),
},
qType: dns.TypeA,
}, {
name: "cname",
reqFQDN: cnameReqFQDN,
wantRule: filtertest.HostBlocked,
respAns: dnsservertest.SectionAnswer{
dnsservertest.NewCNAME(cnameReqFQDN, ttl, filtertest.FQDNBlocked),
dnsservertest.NewA(filtertest.FQDNBlocked, ttl, netip.MustParseAddr("1.2.3.4")),
},
qType: dns.TypeA,
}, {
want: resBlocked4,
name: "ipv4",
reqFQDN: filtertest.FQDNBlocked,
wantRule: blockedIPv4Str,
respAns: dnsservertest.SectionAnswer{
dnsservertest.NewA(filtertest.FQDNBlocked, ttl, blockedIPv4),
},
qType: dns.TypeA,
}, {
want: resBlocked6,
name: "ipv6",
reqFQDN: filtertest.FQDNBlocked,
wantRule: blockedIPv6Str,
respAns: dnsservertest.SectionAnswer{
dnsservertest.NewAAAA(filtertest.FQDNBlocked, ttl, blockedIPv6),
},
qType: dns.TypeAAAA,
}, {
want: resBlocked4,
name: "ipv4hint",
reqFQDN: filtertest.FQDNBlocked,
wantRule: blockedIPv4Str,
respAns: dnsservertest.SectionAnswer{dnsservertest.NewHTTPS(
filtertest.FQDNBlocked,
ttl,
@@ -567,9 +635,8 @@ func TestFilter_FilterResponse(t *testing.T) {
)},
qType: dns.TypeHTTPS,
}, {
want: resBlocked6,
name: "ipv6hint",
reqFQDN: filtertest.FQDNBlocked,
wantRule: blockedIPv6Str,
respAns: dnsservertest.SectionAnswer{dnsservertest.NewHTTPS(
filtertest.FQDNBlocked,
ttl,
@@ -578,9 +645,8 @@ func TestFilter_FilterResponse(t *testing.T) {
)},
qType: dns.TypeHTTPS,
}, {
want: resBlocked4,
name: "ipv4_ipv6_hints",
reqFQDN: filtertest.FQDNBlocked,
wantRule: blockedIPv4Str,
respAns: dnsservertest.SectionAnswer{dnsservertest.NewHTTPS(
filtertest.FQDNBlocked,
ttl,
@@ -589,13 +655,12 @@ func TestFilter_FilterResponse(t *testing.T) {
)},
qType: dns.TypeHTTPS,
}, {
want: nil,
name: "pass_hints",
reqFQDN: filtertest.FQDNBlocked,
wantRule: "",
respAns: dnsservertest.SectionAnswer{dnsservertest.NewHTTPS(
filtertest.FQDNBlocked,
ttl,
[]netip.Addr{passedIPv4},
[]netip.Addr{netip.MustParseAddr(passedIPv4Str)},
[]netip.Addr{},
)},
qType: dns.TypeHTTPS,
@@ -603,7 +668,7 @@ func TestFilter_FilterResponse(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx, req := newReqDataWithFQDN(t, tc.reqFQDN)
ctx, req := newReqDataWithFQDN(t, filtertest.FQDNBlocked)
req.DNS.Question[0].Qtype = tc.qType
res, err := f.FilterResponse(ctx, &filter.Response{
@@ -612,17 +677,7 @@ func TestFilter_FilterResponse(t *testing.T) {
})
require.NoError(t, err)
if tc.wantRule == "" {
assert.Nil(t, res)
return
}
want := &filter.ResultBlocked{
List: filtertest.RuleListID1,
Rule: tc.wantRule,
}
assert.Equal(t, want, res)
assert.Equal(t, tc.want, res)
})
}
}

View File

@@ -0,0 +1,350 @@
// Package domain implements a domain filter based on domain table.
package domain
import (
"bufio"
"bytes"
"context"
"fmt"
"log/slog"
"net/http/cookiejar"
"net/url"
"path"
"strings"
"sync/atomic"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/AdguardTeam/AdGuardDNS/internal/agdnet"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/refreshable"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/rulelist"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/service"
"github.com/AdguardTeam/golibs/syncutil"
"github.com/c2h5oh/datasize"
"github.com/miekg/dns"
)
// FilterConfig is the domain filter configuration structure.
type FilterConfig struct {
// Logger is used for logging the operation of the filter.
Logger *slog.Logger
// Cloner is used to clone messages taken from filtering-result cache.
Cloner *dnsmsg.Cloner
// CacheManager is the global cache manager. CacheManager must not be nil.
CacheManager agdcache.Manager
// URL is the URL used to update the filter.
URL *url.URL
// ErrColl is used to collect non-critical and rare errors.
ErrColl errcoll.Interface
// DomainMetrics are the specific metrics for the domain filter.
DomainMetrics Metrics
// Metrics are the metrics for the domain filter.
Metrics filter.Metrics
// PublicSuffixList is used for obtaining public suffix for specified
// domain.
PublicSuffixList cookiejar.PublicSuffixList
// ID is the ID of this storage for logging and error reporting.
ID filter.ID
// CachePath is the path to the file containing the cached filtered
// hostnames, one per line.
CachePath string
// Staleness is the time after which a file is considered stale.
Staleness time.Duration
// CacheTTL is the time-to-live value used to cache the results of the
// filter.
//
// TODO(a.garipov): Currently unused. See AGDNS-398.
CacheTTL time.Duration
// RefreshTimeout is the timeout for the filter update operation.
RefreshTimeout time.Duration
// CacheCount is the count of the elements in the filter's result cache.
CacheCount int
// MaxSize is the maximum size of the downloadable rule-list.
MaxSize datasize.ByteSize
// SubDomainNum defines how many subdomains will be checked for one domain.
// It must be positive and fit into int.
SubDomainNum uint
}
// Filter is a domain table based filter.
//
// TODO(f.setrakov): Consider DRYing it with the hasprefix filter.
type Filter struct {
logger *slog.Logger
cloner *dnsmsg.Cloner
domains *atomic.Pointer[container.MapSet[string]]
refr *refreshable.Refreshable
subDomainsPool *syncutil.Pool[[]string]
errColl errcoll.Interface
domainMtrc Metrics
publicSuffixList cookiejar.PublicSuffixList
metrics filter.Metrics
resCache agdcache.Interface[rulelist.CacheKey, filter.Result]
id filter.ID
subDomainNum int
}
// IDPrefix is a common prefix for cache IDs, logging, and refreshes of
// domain filters.
//
// TODO(a.garipov): Consider better names.
const IDPrefix = "filters/domain"
// NewFilter returns a new domain filter. It also adds the caches with IDs
// [FilterListIDAdultBlocking], [FilterListIDSafeBrowsing], and
// [FilterListIDNewRegDomains] to the cache manager. c must not be nil.
func NewFilter(c *FilterConfig) (f *Filter, err error) {
id := c.ID
resCache := agdcache.NewLRU[rulelist.CacheKey, filter.Result](&agdcache.LRUConfig{
Count: c.CacheCount,
})
c.CacheManager.Add(path.Join(IDPrefix, string(id)), resCache)
f = &Filter{
logger: c.Logger,
cloner: c.Cloner,
domains: &atomic.Pointer[container.MapSet[string]]{},
errColl: c.ErrColl,
// #nosec G115 -- Assume that c.SubDomainNum is always less then or
// equal to 63.
//
// TODO(f.setrakov): Validate c.SubDomainsNum.
subDomainsPool: syncutil.NewSlicePool[string](int(c.SubDomainNum)),
domainMtrc: c.DomainMetrics,
metrics: c.Metrics,
publicSuffixList: c.PublicSuffixList,
resCache: resCache,
id: id,
// #nosec G115 -- The value is a constant less than int accommodates.
subDomainNum: int(c.SubDomainNum),
}
f.refr, err = refreshable.New(&refreshable.Config{
Logger: f.logger,
URL: c.URL,
ID: id,
CachePath: c.CachePath,
Staleness: c.Staleness,
Timeout: c.RefreshTimeout,
MaxSize: c.MaxSize,
})
if err != nil {
return nil, fmt.Errorf("creating refreshable: %w", err)
}
return f, nil
}
// FilterRequest implements the [composite.RequestFilter] interface for *Filter.
// It modifies the request or response if host matches f.
func (f *Filter) FilterRequest(
ctx context.Context,
req *filter.Request,
) (r filter.Result, err error) {
host, qt, cl := req.Host, req.QType, req.QClass
cacheKey := rulelist.NewCacheKey(host, qt, cl, false)
item, ok := f.resCache.Get(cacheKey)
f.domainMtrc.IncrementLookups(ctx, ok)
if ok {
return f.clonedResult(req.DNS, item), nil
}
if !isFilterable(qt) {
return nil, nil
}
var matched string
subPtr := f.subDomainsPool.Get()
defer f.subDomainsPool.Put(subPtr)
*subPtr = agdnet.AppendSubdomains((*subPtr)[:0], host, f.subDomainNum, f.publicSuffixList)
domains := *f.domains.Load()
for _, s := range *subPtr {
if domains.Has(s) {
matched = s
break
}
}
if matched == "" {
f.resCache.Set(cacheKey, nil)
return nil, nil
}
r, err = f.filteredResult(req, matched)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
f.setInCache(cacheKey, r)
f.domainMtrc.UpdateCacheSize(ctx, f.resCache.Len())
return r, nil
}
// isFilterable returns true if the question type is filterable.
func isFilterable(qt dnsmsg.RRType) (ok bool) {
fam := netutil.AddrFamilyFromRRType(qt)
return qt == dns.TypeHTTPS || fam != netutil.AddrFamilyNone
}
// clonedResult returns a clone of the result based on its type. r must be nil,
// [*filter.ResultModifiedRequest], or [*filter.ResultModifiedResponse].
func (f *Filter) clonedResult(req *dns.Msg, r filter.Result) (clone filter.Result) {
switch r := r.(type) {
case nil:
return nil
case *filter.ResultModifiedRequest:
return r.Clone(f.cloner)
case *filter.ResultModifiedResponse:
return r.CloneForReq(f.cloner, req)
default:
panic(fmt.Errorf("domain: unexpected type for result: %T(%[1]v)", r))
}
}
// filteredResult returns a filtered request or response.
func (f *Filter) filteredResult(
req *filter.Request,
matched string,
) (r filter.Result, err error) {
resp, err := req.Messages.NewBlockedResp(req.DNS, nil)
if err != nil {
return nil, fmt.Errorf("filter %s: creating modified result: %w", f.id, err)
}
return &filter.ResultModifiedResponse{
Msg: resp,
List: f.id,
Rule: filter.RuleText(matched),
}, nil
}
// setInCache sets r in cache. It clones the result to make sure that
// modifications to the result message down the pipeline don't interfere with
// the cached value. r must be either [*filter.ResultModifiedRequest] or
// [*filter.ResultModifiedResponse].
//
// See AGDNS-359.
func (f *Filter) setInCache(k rulelist.CacheKey, r filter.Result) {
switch r := r.(type) {
case *filter.ResultModifiedRequest:
f.resCache.Set(k, r.Clone(f.cloner))
case *filter.ResultModifiedResponse:
f.resCache.Set(k, r.Clone(f.cloner))
default:
panic(fmt.Errorf("domain: unexpected type for result: %T(%[1]v)", r))
}
}
// type check
var _ service.Refresher = (*Filter)(nil)
// Refresh implements the [service.Refresher] interface for *Filter.
func (f *Filter) Refresh(ctx context.Context) (err error) {
f.logger.InfoContext(ctx, "refresh started")
defer f.logger.InfoContext(ctx, "refresh finished")
err = f.refresh(ctx, false)
if err != nil {
errcoll.Collect(ctx, f.errColl, f.logger, fmt.Sprintf("refreshing %q", f.id), err)
}
return err
}
// RefreshInitial loads the content of the filter, using cached files if any,
// regardless of their staleness.
func (f *Filter) RefreshInitial(ctx context.Context) (err error) {
f.logger.InfoContext(ctx, "initial refresh started")
defer f.logger.InfoContext(ctx, "initial refresh finished")
err = f.refresh(ctx, true)
if err != nil {
return fmt.Errorf("refreshing domain filter initially: %w", err)
}
return nil
}
// refresh reloads and resets domain data. If acceptStale is true, do not try
// to load the list from its URL when there is already a file in the cache
// directory, regardless of its staleness.
func (f *Filter) refresh(ctx context.Context, acceptStale bool) (err error) {
var count int
defer func() {
// TODO(a.garipov): Consider using [agdtime.Clock].
f.metrics.SetFilterStatus(ctx, string(f.id), time.Now(), count, err)
}()
b, err := f.refr.Refresh(ctx, acceptStale)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
count, err = f.resetDomains(b)
if err != nil {
return fmt.Errorf("%s: resetting: %w", f.id, err)
}
f.resCache.Clear()
f.logger.InfoContext(ctx, "reset hosts", "num", count)
return nil
}
// resetDomains populates storage with domains from domainData.
func (f *Filter) resetDomains(domainData []byte) (n int, err error) {
next := container.NewMapSet[string]()
sc := bufio.NewScanner(bytes.NewReader(domainData))
for sc.Scan() {
domain := sc.Text()
if len(domain) == 0 || domain[0] == '#' {
continue
}
next.Add(strings.ToLower(domain))
n++
}
err = sc.Err()
if err != nil {
return 0, fmt.Errorf("scanning domains: %w", err)
}
f.domains.Store(next)
return n, nil
}

View File

@@ -0,0 +1,306 @@
package domain_test
import (
"net/http"
"net/netip"
"os"
"testing"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/dnsservertest"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/composite"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/domain"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/filtertest"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/publicsuffix"
)
// type check
//
// TODO(f.setrakov): Use export via internal test to avoid loop.
var _ composite.RequestFilter = (*domain.Filter)(nil)
// testDomains is the host data for tests.
const testDomains = filtertest.HostCategory + "\n"
func TestFilter_FilterRequest(t *testing.T) {
t.Parallel()
msgs := agdtest.NewConstructor(t)
testCases := []struct {
name string
host string
qType dnsmsg.RRType
wantResult bool
}{{
name: "host_not_a_or_aaaa",
host: filtertest.HostCategory,
qType: dns.TypeTXT,
wantResult: false,
}, {
name: "host_success",
host: filtertest.HostCategory,
qType: dns.TypeA,
wantResult: true,
}, {
name: "host_success_subdomain",
host: filtertest.HostCategorySub,
qType: dns.TypeA,
wantResult: true,
}, {
name: "host_no_match",
host: filtertest.Host,
qType: dns.TypeA,
wantResult: false,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
f := filtertest.NewDomainFilter(t, testDomains)
ctx := testutil.ContextWithTimeout(t, filtertest.Timeout)
req := dnsservertest.NewReq(dns.Fqdn(tc.host), tc.qType, dns.ClassINET)
r, err := f.FilterRequest(ctx, &filter.Request{
DNS: req,
Messages: msgs,
Host: tc.host,
QType: tc.qType,
})
require.NoError(t, err)
var wantRes filter.Result
if tc.wantResult {
wantRes = newModRespResult(
t,
req,
msgs,
netip.IPv4Unspecified(),
filtertest.HostCategory,
)
}
filtertest.AssertEqualResult(t, wantRes, r)
})
}
}
func TestFilter_FilterRequest_cache(t *testing.T) {
t.Parallel()
f := filtertest.NewDomainFilter(t, testDomains)
require.True(t, t.Run("cached_success", func(t *testing.T) {
t.Parallel()
req := filtertest.NewARequest(t, filtertest.HostCategory)
ctx := testutil.ContextWithTimeout(t, filtertest.Timeout)
original, err := f.FilterRequest(ctx, req)
require.NoError(t, err)
cached, err := f.FilterRequest(ctx, req)
require.NoError(t, err)
filtertest.AssertEqualResult(t, cached, original)
}))
require.True(t, t.Run("cached_no_match", func(t *testing.T) {
t.Parallel()
req := filtertest.NewARequest(t, filtertest.Host)
ctx := testutil.ContextWithTimeout(t, filtertest.Timeout)
original, err := f.FilterRequest(ctx, req)
require.NoError(t, err)
cached, err := f.FilterRequest(ctx, req)
require.NoError(t, err)
filtertest.AssertEqualResult(t, cached, original)
}))
}
func TestFilter_Refresh(t *testing.T) {
t.Parallel()
refrCh := make(chan struct{}, 1)
cachePath, srvURL := filtertest.PrepareRefreshable(t, refrCh, testDomains, http.StatusOK)
f, err := domain.NewFilter(&domain.FilterConfig{
Logger: slogutil.NewDiscardLogger(),
Cloner: agdtest.NewCloner(),
CacheManager: agdcache.EmptyManager{},
URL: srvURL,
ErrColl: agdtest.NewErrorCollector(),
DomainMetrics: domain.EmptyMetrics{},
Metrics: filter.EmptyMetrics{},
PublicSuffixList: publicsuffix.List,
ID: filtertest.RuleListIDDomain,
CachePath: cachePath,
Staleness: filtertest.Staleness,
CacheTTL: filtertest.CacheTTL,
CacheCount: filtertest.CacheCount,
MaxSize: filtertest.FilterMaxSize,
SubDomainNum: filtertest.SubDomainNum,
})
require.NoError(t, err)
ctx := testutil.ContextWithTimeout(t, filtertest.Timeout)
require.NoError(t, f.RefreshInitial(ctx))
testutil.RequireReceive(t, refrCh, filtertest.Timeout)
ctx = testutil.ContextWithTimeout(t, filtertest.Timeout)
err = f.Refresh(ctx)
assert.NoError(t, err)
assert.Empty(t, refrCh)
}
func TestFilter_FilterRequest_staleCache(t *testing.T) {
t.Parallel()
refrCh := make(chan struct{}, 1)
cachePath, srvURL := filtertest.PrepareRefreshable(t, refrCh, testDomains, http.StatusOK)
msgs := agdtest.NewConstructor(t)
// Put some initial data into the cache to avoid the first refresh.
cf, err := os.OpenFile(cachePath, os.O_WRONLY|os.O_APPEND, os.ModeAppend)
require.NoError(t, err)
_, err = cf.WriteString(filtertest.Host + "\n")
require.NoError(t, err)
require.NoError(t, cf.Close())
// Create the filter.
cloner := agdtest.NewCloner()
fconf := &domain.FilterConfig{
Logger: slogutil.NewDiscardLogger(),
Cloner: cloner,
CacheManager: agdcache.EmptyManager{},
URL: srvURL,
ErrColl: agdtest.NewErrorCollector(),
DomainMetrics: domain.EmptyMetrics{},
Metrics: filter.EmptyMetrics{},
PublicSuffixList: publicsuffix.List,
ID: filtertest.RuleListIDDomain,
CachePath: cachePath,
Staleness: filtertest.Staleness,
CacheTTL: filtertest.CacheTTL,
CacheCount: filtertest.CacheCount,
MaxSize: filtertest.FilterMaxSize,
SubDomainNum: filtertest.SubDomainNum,
}
f, err := domain.NewFilter(fconf)
require.NoError(t, err)
ctx := testutil.ContextWithTimeout(t, filtertest.Timeout)
require.NoError(t, f.RefreshInitial(ctx))
assert.Empty(t, refrCh)
// Test the following:
//
// 1. Check that the stale rules cache is used.
// 2. Refresh the stale rules cache.
// 3. Ensure the result cache is cleared.
// 4. Ensure the stale rules aren't used.
hostReq := filtertest.NewARequest(t, filtertest.HostCategory)
otherHostReq := filtertest.NewARequest(t, filtertest.Host)
require.True(t, t.Run("hit_cached_host", func(t *testing.T) {
ctx = testutil.ContextWithTimeout(t, filtertest.Timeout)
var r filter.Result
r, err = f.FilterRequest(ctx, otherHostReq)
require.NoError(t, err)
wantRes := newModRespResult(
t,
otherHostReq.DNS,
msgs,
netip.IPv4Unspecified(),
filtertest.Host,
)
filtertest.AssertEqualResult(t, wantRes, r)
}))
require.True(t, t.Run("refresh", func(t *testing.T) {
// Make the cache stale.
now := time.Now()
err = os.Chtimes(cachePath, now, now.Add(-2*fconf.Staleness))
require.NoError(t, err)
ctx = testutil.ContextWithTimeout(t, filtertest.Timeout)
err = f.Refresh(ctx)
assert.NoError(t, err)
testutil.RequireReceive(t, refrCh, filtertest.Timeout)
}))
require.True(t, t.Run("previously_cached", func(t *testing.T) {
ctx = testutil.ContextWithTimeout(t, filtertest.Timeout)
var r filter.Result
r, err = f.FilterRequest(ctx, otherHostReq)
require.NoError(t, err)
assert.Nil(t, r)
}))
require.True(t, t.Run("new_host", func(t *testing.T) {
ctx = testutil.ContextWithTimeout(t, filtertest.Timeout)
var r filter.Result
r, err = f.FilterRequest(ctx, hostReq)
require.NoError(t, err)
wantRes := newModRespResult(
t,
hostReq.DNS,
msgs,
netip.IPv4Unspecified(),
filtertest.HostCategory,
)
filtertest.AssertEqualResult(t, wantRes, r)
}))
}
// newModRespResult is a helper for creating modified response result for tests.
// req must not be nil.
func newModRespResult(
tb testing.TB,
req *dns.Msg,
messages *dnsmsg.Constructor,
replIP netip.Addr,
rule string,
) (r *filter.ResultModifiedResponse) {
tb.Helper()
resp, err := messages.NewRespIP(req, replIP)
require.NoError(tb, err)
return &filter.ResultModifiedResponse{
Msg: resp,
List: filtertest.RuleListIDDomain,
Rule: filter.RuleText(rule),
}
}

View File

@@ -0,0 +1,28 @@
package domain
import (
"context"
)
// Metrics is an interface used for collection if the domain filter
// statistics.
type Metrics interface {
// IncrementLookups increments the number of lookups. hit is true if the
// lookup returned a value.
IncrementLookups(ctx context.Context, hit bool)
// UpdateCacheSize is called when the cache size is updated.
UpdateCacheSize(ctx context.Context, cacheLen int)
}
// EmptyMetrics is the implementation of the [Metrics] interface that does nothing.
type EmptyMetrics struct{}
// type check
var _ Metrics = EmptyMetrics{}
// IncrementLookups implements the [Metrics] interface for EmptyMetrics.
func (EmptyMetrics) IncrementLookups(_ context.Context, _ bool) {}
// UpdateCacheSize implements the [Metrics] interface for EmptyMetrics.
func (EmptyMetrics) UpdateCacheSize(_ context.Context, _ int) {}

View File

@@ -0,0 +1,46 @@
package filtertest
import (
"net/http"
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/domain"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/require"
"golang.org/x/net/publicsuffix"
)
// NewDomainFilter is a helper constructor of domain filters for tests.
func NewDomainFilter(tb testing.TB, data string) (f *domain.Filter) {
tb.Helper()
cachePath, srvURL := PrepareRefreshable(tb, nil, data, http.StatusOK)
f, err := domain.NewFilter(&domain.FilterConfig{
Logger: slogutil.NewDiscardLogger(),
Cloner: agdtest.NewCloner(),
CacheManager: agdcache.EmptyManager{},
URL: srvURL,
ErrColl: agdtest.NewErrorCollector(),
DomainMetrics: domain.EmptyMetrics{},
Metrics: filter.EmptyMetrics{},
PublicSuffixList: publicsuffix.List,
ID: RuleListIDDomain,
CachePath: cachePath,
Staleness: Staleness,
CacheTTL: CacheTTL,
CacheCount: CacheCount,
MaxSize: FilterMaxSize,
SubDomainNum: SubDomainNum,
})
require.NoError(tb, err)
ctx := testutil.ContextWithTimeout(tb, Timeout)
require.NoError(tb, f.RefreshInitial(ctx))
return f
}

View File

@@ -57,6 +57,7 @@ const (
HostBlockedForClientIP = "blocked-for-client-ip.example"
HostBlockedForClientName = "blocked-for-client-name.example"
HostBlockedService1 = "service-1.example"
HostCNAME = "new-cname.example"
HostDangerous = "dangerous-domain.example"
HostDangerousRepl = "dangerous-domain-repl.example"
HostNewlyRegistered = "newly-registered.example"
@@ -67,12 +68,15 @@ const (
HostSafeSearchGeneralRepl = "safe.search.example"
HostSafeSearchYouTube = "video.example"
HostSafeSearchYouTubeRepl = "safe.video.example"
HostCategory = "blocked.category.example"
HostCategorySub = "a.b.c." + HostCategory
FQDN = Host + "."
FQDNAdultContent = HostAdultContent + "."
FQDNAdultContentRepl = HostAdultContentRepl + "."
FQDNBlocked = HostBlocked + "."
FQDNBlockedForClientName = HostBlockedForClientName + "."
FQDNCname = HostCNAME + "."
FQDNDangerous = HostDangerous + "."
FQDNDangerousRepl = HostDangerousRepl + "."
FQDNNewlyRegistered = HostNewlyRegistered + "."
@@ -82,6 +86,7 @@ const (
FQDNSafeSearchGeneralIPv6 = HostSafeSearchGeneralIPv6 + "."
FQDNSafeSearchYouTube = HostSafeSearchYouTube + "."
FQDNSafeSearchYouTubeRepl = HostSafeSearchYouTubeRepl + "."
FQDNCategory = HostCategory + "."
)
// Common blocked-service IDs for tests.
@@ -122,9 +127,11 @@ const BlockedServiceIndex string = `{
const (
RuleListID1Str = "rule_list_1"
RuleListID2Str = "rule_list_2"
RuleListIDDomainStr = "blocked-category"
RuleListID1 filter.ID = RuleListID1Str
RuleListID2 filter.ID = RuleListID2Str
RuleListIDDomain filter.ID = RuleListIDDomainStr
)
// NewRuleListIndex returns a rule-list index containing a record for a filter

View File

@@ -13,8 +13,12 @@ import (
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/require"
"golang.org/x/net/publicsuffix"
)
// SubDomainNum is a common subDomainNum value for tests.
const SubDomainNum = 4
// NewHashprefixFilter is like [NewHashprefixFilterWithRepl], but the
// replacement host is also set in accordance with id.
func NewHashprefixFilter(tb testing.TB, id filter.ID) (f *hashprefix.Filter) {
@@ -68,8 +72,9 @@ func NewHashprefixFilterWithRepl(
Hashes: strg,
URL: srvURL,
ErrColl: agdtest.NewErrorCollector(),
HashPrefixMtcs: hashprefix.EmptyMetrics{},
HashPrefixMetrics: hashprefix.EmptyMetrics{},
Metrics: filter.EmptyMetrics{},
PublicSuffixList: publicsuffix.List,
ID: id,
CachePath: cachePath,
ReplacementHost: replHost,
@@ -77,6 +82,7 @@ func NewHashprefixFilterWithRepl(
CacheTTL: CacheTTL,
CacheCount: CacheCount,
MaxSize: FilterMaxSize,
SubDomainNum: SubDomainNum,
})
require.NoError(tb, err)

View File

@@ -0,0 +1,279 @@
package rulelist_test
import (
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/dnsservertest"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/filtertest"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/rulelist"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testDNSRequestID is the common message ID for all DNS messages.
const testDNSRequestID = 1
// testListID is common list id for all network rules.
const testListID = rules.ListID(testDNSRequestID)
// Common rewrite values for tests.
const (
exchange = "mail.com"
exchangeFQDN = exchange + "."
targetHost = "target.com"
targetFQDN = targetHost + "."
)
// newDNSRequest is a helper that returns a DNS request with the given
// data and overrides its ID with testDNSRequestID.
func newDNSRequest(tb testing.TB, host string, qt dnsmsg.RRType) (req *dns.Msg) {
tb.Helper()
req = dnsservertest.NewReq(host, qt, dns.ClassINET)
req.Id = testDNSRequestID
return req
}
// newRule is a helper, that returns a NetworkRule generated from string, and
// sets its list ID to a common value.
func newRule(tb testing.TB, rule string) (r *rules.NetworkRule) {
tb.Helper()
r, err := rules.NewNetworkRule(rule, testListID)
require.NoError(tb, err)
return r
}
// newResultModifiedResponse is a helper that returns a ResultModifiedResponse
// with the default list ID and a dns.Msg containing the given data. request
// must not be nil.
func newResultModifiedResponse(
tb testing.TB,
rcode rules.RCode,
request *dns.Msg,
ans ...dnsservertest.RRSection,
) (resp *filter.ResultModifiedResponse) {
tb.Helper()
return &filter.ResultModifiedResponse{
List: filtertest.RuleListID1,
Msg: dnsservertest.NewResp(rcode, request, ans...),
}
}
func TestProccessDNSRewrites_RRTypes(t *testing.T) {
t.Parallel()
var (
reqA = newDNSRequest(t, filtertest.HostSafeSearchGeneralIPv4, dns.TypeA)
reqAAAA = newDNSRequest(t, filtertest.HostSafeSearchGeneralIPv6, dns.TypeAAAA)
reqTXT = newDNSRequest(t, filtertest.Host, dns.TypeTXT)
reqSRV = newDNSRequest(t, filtertest.Host, dns.TypeSRV)
reqSVCB = newDNSRequest(t, filtertest.Host, dns.TypeSVCB)
reqMX = newDNSRequest(t, filtertest.Host, dns.TypeMX)
ansA = dnsservertest.SectionAnswer{
dnsservertest.NewA(
filtertest.FQDNSafeSearchGeneralIPv4,
10,
filtertest.IPv4SafeSearchRepl,
),
}
ansAAAA = dnsservertest.SectionAnswer{
dnsservertest.NewAAAA(
filtertest.FQDNSafeSearchGeneralIPv6,
10,
filtertest.IPv6SafeSearchRepl,
),
}
srv = dnsservertest.NewSRV(filtertest.Host, 10, targetFQDN, 1, 1, 29)
svcb = dnsservertest.NewSVCB(filtertest.Host, 10, targetFQDN, 1)
)
testCases := []struct {
result filter.Result
rule *rules.NetworkRule
name string
host string
qtype dnsmsg.RRType
}{{
name: "type_a",
host: filtertest.HostSafeSearchGeneralIPv4,
qtype: dns.TypeA,
rule: newRule(t, filtertest.RuleSafeSearchGeneralIPv4Str),
result: newResultModifiedResponse(t, dns.RcodeSuccess, reqA, ansA),
}, {
name: "type_aaaa",
host: filtertest.HostSafeSearchGeneralIPv6,
qtype: dns.TypeAAAA,
rule: newRule(t, filtertest.RuleSafeSearchGeneralIPv6Str),
result: newResultModifiedResponse(t, dns.RcodeSuccess, reqAAAA, ansAAAA),
}, {
name: "type_txt",
host: filtertest.Host,
qtype: dns.TypeTXT,
rule: newRule(t, "||"+filtertest.Host+"^$dnsrewrite=NOERROR;TXT;rr_value"),
result: newResultModifiedResponse(
t,
dns.RcodeSuccess,
reqTXT,
dnsservertest.SectionAnswer{dnsservertest.NewTXT(filtertest.FQDN, 10, "rr_value")},
),
}, {
name: "type_mx",
host: filtertest.Host,
qtype: dns.TypeMX,
rule: newRule(t, "||"+filtertest.Host+"^$dnsrewrite=NOERROR;MX;1 "+exchange),
result: newResultModifiedResponse(
t,
dns.RcodeSuccess,
reqMX,
dnsservertest.SectionAnswer{
dnsservertest.NewMX(filtertest.FQDN, 10, 1, exchangeFQDN),
},
),
}, {
name: "type_srv",
host: filtertest.Host,
qtype: dns.TypeSRV,
rule: newRule(t, "||"+filtertest.Host+" ^$dnsrewrite=NOERROR;SRV;1 1 29 "+targetHost),
result: newResultModifiedResponse(
t,
dns.RcodeSuccess,
reqSRV,
dnsservertest.SectionAnswer{srv},
),
}, {
name: "type_svcb",
host: filtertest.Host,
qtype: dns.TypeSVCB,
rule: newRule(t, "||"+filtertest.Host+"^$dnsrewrite=NOERROR;SVCB;1 "+targetHost),
result: newResultModifiedResponse(
t,
dns.RcodeSuccess,
reqSVCB,
dnsservertest.SectionAnswer{svcb},
),
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
req := filtertest.NewRequest(t, "", tc.host, filtertest.IPv4Client, tc.qtype)
req.DNS.Id = testDNSRequestID
dnsr := []*rules.NetworkRule{tc.rule}
res := rulelist.ProcessDNSRewrites(req, dnsr, filtertest.RuleListID1)
assert.Equal(t, tc.result, res)
})
}
}
func TestProccessDNSRewrites_Other(t *testing.T) {
t.Parallel()
var (
cnameRule = "||" + filtertest.HostDangerous + "^$dnsrewrite=" + filtertest.Host
rcodeRule = "||" + filtertest.Host + "^$dnsrewrite=REFUSED;;"
)
reqA := newDNSRequest(t, filtertest.Host, dns.TypeA)
testCases := []struct {
result filter.Result
rule *rules.NetworkRule
name string
host string
}{{
name: "cname",
host: filtertest.HostDangerous,
rule: newRule(t, cnameRule),
result: &filter.ResultModifiedRequest{
List: filtertest.RuleListID1,
Msg: newDNSRequest(t, filtertest.FQDN, dns.TypeA),
Rule: filter.RuleText(cnameRule),
},
}, {
name: "empty_rules",
host: filtertest.Host,
rule: nil,
result: nil,
}, {
name: "equal_cname",
host: filtertest.Host,
rule: newRule(t, "||"+filtertest.Host+"^$dnsrewrite="+filtertest.Host),
result: nil,
}, {
name: "rcode",
host: filtertest.Host,
rule: newRule(t, rcodeRule),
result: &filter.ResultModifiedResponse{
List: filtertest.RuleListID1,
Msg: dnsservertest.NewResp(dns.RcodeRefused, reqA),
Rule: filter.RuleText(rcodeRule),
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
req := filtertest.NewRequest(t, "", tc.host, filtertest.IPv4Client, dns.TypeA)
req.DNS.Id = testDNSRequestID
var dnsr []*rules.NetworkRule
if tc.rule != nil {
dnsr = []*rules.NetworkRule{tc.rule}
}
res := rulelist.ProcessDNSRewrites(req, dnsr, filtertest.RuleListID1)
assert.Equal(t, tc.result, res)
})
}
}
func BenchmarkProcessDNSRewrite(b *testing.B) {
benchCases := []struct {
rule *rules.NetworkRule
name string
}{{
name: "cname",
rule: newRule(b, "||"+filtertest.Host+"^$dnsrewrite="+filtertest.HostDangerous),
}, {
name: "rcode",
rule: newRule(b, "||"+filtertest.Host+"^$dnsrewrite=REFUSED;;"),
}, {
name: "type_a",
rule: newRule(b, "||"+filtertest.Host+"^$dnsrewrite="+filtertest.IPv4ClientStr),
}}
req := filtertest.NewRequest(b, "", filtertest.Host, filtertest.IPv4Client, dns.TypeA)
for _, bc := range benchCases {
b.Run(bc.name, func(b *testing.B) {
var result filter.Result
dnsr := []*rules.NetworkRule{bc.rule}
b.ReportAllocs()
for b.Loop() {
result = rulelist.ProcessDNSRewrites(req, dnsr, filtertest.RuleListID1)
}
assert.NotNil(b, result)
})
}
// Most recent results:
// goos: darwin
// goarch: arm64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/rulelist
// cpu: Apple M3
// BenchmarkProcessDNSRewrite/cname-8 9117697 111.7 ns/op 280 B/op 5 allocs/op
// BenchmarkProcessDNSRewrite/rcode-8 16215256 75.06 ns/op 264 B/op 4 allocs/op
// BenchmarkProcessDNSRewrite/type_a-8 5742392 214.6 ns/op 656 B/op 9 allocs/op
}

View File

@@ -124,64 +124,83 @@ func TestRefreshable_Refresh(t *testing.T) {
}
func BenchmarkRefreshable_SetURLFilterResult(b *testing.B) {
ctx := b.Context()
benchCases := []struct {
request *urlfilter.DNSRequest
cache rulelist.ResultCache
want require.BoolAssertionFunc
name string
}{{
name: "blocked",
cache: rulelist.EmptyResultCache{},
request: &urlfilter.DNSRequest{
ClientIP: filtertest.IPv4Client,
Hostname: filtertest.HostBlocked,
DNSType: dns.TypeA,
},
want: require.True,
}, {
name: "other",
cache: rulelist.EmptyResultCache{},
request: &urlfilter.DNSRequest{
ClientIP: filtertest.IPv4Client,
Hostname: filtertest.Host,
DNSType: dns.TypeA,
},
want: require.False,
}, {
name: "blocked_with_cache",
cache: rulelist.NewResultCache(filtertest.CacheCount, true),
request: &urlfilter.DNSRequest{
ClientIP: filtertest.IPv4Client,
Hostname: filtertest.HostBlocked,
DNSType: dns.TypeA,
},
want: require.True,
}, {
name: "other_with_cache",
cache: rulelist.NewResultCache(filtertest.CacheCount, true),
request: &urlfilter.DNSRequest{
ClientIP: filtertest.IPv4Client,
Hostname: filtertest.Host,
DNSType: dns.TypeA,
},
want: require.False,
}}
for _, bc := range benchCases {
b.Run(bc.name, func(b *testing.B) {
rl := rulelist.NewFromString(
filtertest.RuleBlockStr,
filtertest.RuleListID1,
"",
rulelist.EmptyResultCache{},
bc.cache,
)
ctx := testutil.ContextWithTimeout(b, filtertest.Timeout)
b.Run("blocked", func(b *testing.B) {
req := &urlfilter.DNSRequest{
ClientIP: filtertest.IPv4Client,
Hostname: filtertest.HostBlocked,
DNSType: dns.TypeA,
}
res := &urlfilter.DNSResult{}
// Warmup to fill the slices.
ok := rl.SetURLFilterResult(ctx, req, res)
require.True(b, ok)
ok := rl.SetURLFilterResult(ctx, bc.request, res)
bc.want(b, ok)
b.ReportAllocs()
for b.Loop() {
res.Reset()
ok = rl.SetURLFilterResult(ctx, req, res)
ok = rl.SetURLFilterResult(ctx, bc.request, res)
}
require.True(b, ok)
bc.want(b, ok)
})
b.Run("other", func(b *testing.B) {
req := &urlfilter.DNSRequest{
ClientIP: filtertest.IPv4Client,
Hostname: filtertest.Host,
DNSType: dns.TypeA,
}
res := &urlfilter.DNSResult{}
// Warmup to fill the slices.
ok := rl.SetURLFilterResult(ctx, req, res)
require.False(b, ok)
b.ReportAllocs()
for b.Loop() {
res.Reset()
ok = rl.SetURLFilterResult(ctx, req, res)
}
require.False(b, ok)
})
// Most recent results:
// goos: linux
// goarch: amd64
// goos: darwin
// goarch: arm64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/rulelist
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkRefreshable_SetURLFilterResult/blocked-16 1352236 887.3 ns/op 24 B/op 1 allocs/op
// BenchmarkRefreshable_SetURLFilterResult/other-16 2772519 432.6 ns/op 24 B/op 1 allocs/op
// cpu: Apple M3
// BenchmarkRefreshable_SetURLFilterResult/blocked-8 2762634 428.2 ns/op 24 B/op 1 allocs/op
// BenchmarkRefreshable_SetURLFilterResult/other-8 5687978 242.0 ns/op 24 B/op 1 allocs/op
// BenchmarkRefreshable_SetURLFilterResult/blocked_with_cache-8 33770551 35.38 ns/op 0 B/op 0 allocs/op
// BenchmarkRefreshable_SetURLFilterResult/other_with_cache-8 43484037 31.63 ns/op 0 B/op 0 allocs/op
}

View File

@@ -1,7 +1,6 @@
package rulelist
import (
"context"
"net/netip"
"testing"
@@ -18,6 +17,9 @@ const (
// testHostOther is the other request host for tests.
testHostOther = "other.example"
// cacheCount is the common count of cache items for filtering tests.
cacheCount = 1
)
// testRemoteIP is the client IP for tests
@@ -29,68 +31,80 @@ const testFltListID filter.ID = "fl1"
// testBlockRule is the common blocking rule for tests.
const testBlockRule = "||" + testHostBlocked + "\n"
// TODO(a.garipov): Add benchmarks with a cache.
func BenchmarkBaseFilter_SetURLFilterResult(b *testing.B) {
f := newBaseFilter(
[]byte(testBlockRule),
testFltListID,
"",
EmptyResultCache{},
)
const qt = dns.TypeA
ctx := context.Background()
ctx := b.Context()
b.Run("blocked", func(b *testing.B) {
req := &urlfilter.DNSRequest{
benchCases := []struct {
request *urlfilter.DNSRequest
cache ResultCache
want require.BoolAssertionFunc
name string
}{{
name: "blocked",
cache: EmptyResultCache{},
request: &urlfilter.DNSRequest{
ClientIP: testRemoteIP,
Hostname: testHostBlocked,
DNSType: qt,
}
res := &urlfilter.DNSResult{}
// Warmup to fill the slices.
ok := f.SetURLFilterResult(ctx, req, res)
require.True(b, ok)
b.ReportAllocs()
for b.Loop() {
res.Reset()
ok = f.SetURLFilterResult(ctx, req, res)
}
require.True(b, ok)
})
b.Run("other", func(b *testing.B) {
req := &urlfilter.DNSRequest{
},
want: require.True,
}, {
name: "other",
cache: EmptyResultCache{},
request: &urlfilter.DNSRequest{
ClientIP: testRemoteIP,
Hostname: testHostOther,
DNSType: qt,
}
},
want: require.False,
}, {
name: "blocked_with_cache",
cache: NewResultCache(cacheCount, true),
request: &urlfilter.DNSRequest{
ClientIP: testRemoteIP,
Hostname: testHostBlocked,
DNSType: qt,
},
want: require.True,
}, {
name: "other_with_cache",
cache: NewResultCache(cacheCount, true),
request: &urlfilter.DNSRequest{
ClientIP: testRemoteIP,
Hostname: testHostOther,
DNSType: qt,
},
want: require.False,
}}
for _, bc := range benchCases {
b.Run(bc.name, func(b *testing.B) {
f := newBaseFilter([]byte(testBlockRule), testFltListID, "", bc.cache)
res := &urlfilter.DNSResult{}
// Warmup to fill the slices.
ok := f.SetURLFilterResult(ctx, req, res)
require.False(b, ok)
ok := f.SetURLFilterResult(ctx, bc.request, res)
bc.want(b, ok)
b.ReportAllocs()
for b.Loop() {
res.Reset()
ok = f.SetURLFilterResult(ctx, req, res)
ok = f.SetURLFilterResult(ctx, bc.request, res)
}
require.False(b, ok)
bc.want(b, ok)
})
}
// Most recent results:
// goos: linux
// goarch: amd64
// goos: darwin
// goarch: arm64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/rulelist
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkBaseFilter_SetURLFilterResult/blocked-16 911409 1315 ns/op 24 B/op 1 allocs/op
// BenchmarkBaseFilter_SetURLFilterResult/other-16 2824462 425.0 ns/op 24 B/op 1 allocs/op
// cpu: Apple M3
// BenchmarkBaseFilter_SetURLFilterResult/blocked-8 1793678 670.9 ns/op 24 B/op 1 allocs/op
// BenchmarkBaseFilter_SetURLFilterResult/other-8 5599238 222.0 ns/op 24 B/op 1 allocs/op
// BenchmarkBaseFilter_SetURLFilterResult/blocked_with_cache-8 38971425 31.01 ns/op 0 B/op 0 allocs/op
// BenchmarkBaseFilter_SetURLFilterResult/other_with_cache-8 57606105 21.05 ns/op 0 B/op 0 allocs/op
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -70,7 +70,7 @@ func main() {
// countriesASNURL is the default URL to get the per-country top ASN statistics
// from.
const countriesASNURL = `https://static.adtidy.org/dns/countries_asn.json`
const countriesASNURL = `https://filters.adtidy.org/dns/countries_asn.json`
// tmplStr is the template of the generated Go code.
const tmplStr = `// Code generated by go run ./asntops_generate.go; DO NOT EDIT.

View File

@@ -0,0 +1,97 @@
package metrics
import (
"context"
"fmt"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
"github.com/prometheus/client_golang/prometheus"
)
// DomainFilter is the Prometheus-based implementation of the
// [domain.Metrics] interface.
type DomainFilter struct {
// cacheSize is a gauge with the total count of records in the DomainStorage
// cache.
cacheSize prometheus.Gauge
// hits is a counter of the total number of lookups to the DomainStorage
// cache that succeeded.
hits prometheus.Counter
// misses is a counter of the total number of lookups to the DomainStorage
// cache that resulted in a miss.
misses prometheus.Counter
}
// NewDomainFilter registers the filtering metrics in reg and returns a
// properly initialized *DomainFilter. filterName must be a valid label
// name.
func NewDomainFilter(
namespace string,
filterName string,
reg prometheus.Registerer,
) (m *DomainFilter, err error) {
const (
cacheLookups = "domain_filter_cache_lookups"
cacheSize = "domain_filter_cache_size"
)
labels := prometheus.Labels{"filter": filterName}
lookups := prometheus.NewCounterVec(prometheus.CounterOpts{
Name: cacheLookups,
Subsystem: subsystemFilter,
Namespace: namespace,
Help: "Total number of lookups to DomainFilter host cache lookups. " +
"Label hit is the lookup result, either 1 for hit or 0 for miss.",
ConstLabels: labels,
}, []string{"hit"})
m = &DomainFilter{
cacheSize: prometheus.NewGauge(prometheus.GaugeOpts{
Name: cacheSize,
Subsystem: subsystemFilter,
Namespace: namespace,
Help: "The total number of items in the DomainFilter cache.",
ConstLabels: labels,
}),
hits: lookups.WithLabelValues("1"),
misses: lookups.WithLabelValues("0"),
}
var errs []error
collectors := container.KeyValues[string, prometheus.Collector]{{
Key: cacheSize,
Value: m.cacheSize,
}, {
Key: cacheLookups,
Value: lookups,
}}
for _, c := range collectors {
err = reg.Register(c.Value)
if err != nil {
errs = append(errs, fmt.Errorf("registering metrics %q: %w", c.Key, err))
}
}
if err = errors.Join(errs...); err != nil {
return nil, err
}
return m, nil
}
// IncrementLookups implements the [domain.Metrics] interface for
// *DomainFilter.
func (m *DomainFilter) IncrementLookups(_ context.Context, hit bool) {
IncrementCond(hit, m.hits, m.misses)
}
// UpdateCacheSize implements the [domain.Metrics] interface for
// *DomainFilter.
func (m *DomainFilter) UpdateCacheSize(_ context.Context, size int) {
m.cacheSize.Set(float64(size))
}

View File

@@ -429,7 +429,9 @@ func TestDefault_CreateAutoDevice(t *testing.T) {
}
wantProf := &agd.Profile{
CustomDomains: &agd.AccountCustomDomains{},
AdultBlockingMode: &dnsmsg.BlockingModeNullIP{},
BlockingMode: &dnsmsg.BlockingModeNullIP{},
SafeBrowsingBlockingMode: &dnsmsg.BlockingModeNullIP{},
ID: profiledbtest.ProfileID,
DeviceIDs: nil,
AutoDevicesEnabled: true,
@@ -496,7 +498,9 @@ func TestDefault_deviceChanges(t *testing.T) {
profBefore := &agd.Profile{
CustomDomains: &agd.AccountCustomDomains{},
AdultBlockingMode: &dnsmsg.BlockingModeNullIP{},
BlockingMode: &dnsmsg.BlockingModeNullIP{},
SafeBrowsingBlockingMode: &dnsmsg.BlockingModeNullIP{},
ID: profiledbtest.ProfileID,
DeviceIDs: container.NewMapSet(
profiledbtest.DeviceID,
@@ -507,7 +511,9 @@ func TestDefault_deviceChanges(t *testing.T) {
profAfter := &agd.Profile{
CustomDomains: &agd.AccountCustomDomains{},
AdultBlockingMode: &dnsmsg.BlockingModeNullIP{},
BlockingMode: &dnsmsg.BlockingModeNullIP{},
SafeBrowsingBlockingMode: &dnsmsg.BlockingModeNullIP{},
ID: profiledbtest.ProfileID,
DeviceIDs: container.NewMapSet(
profiledbtest.DeviceID,
@@ -600,7 +606,9 @@ func TestDefault_noDeviceChanges(t *testing.T) {
profBefore := &agd.Profile{
CustomDomains: &agd.AccountCustomDomains{},
AdultBlockingMode: &dnsmsg.BlockingModeNullIP{},
BlockingMode: &dnsmsg.BlockingModeNullIP{},
SafeBrowsingBlockingMode: &dnsmsg.BlockingModeNullIP{},
ID: profiledbtest.ProfileID,
FilteringEnabled: true,
DeviceIDs: container.NewMapSet(profiledbtest.DeviceID),
@@ -608,7 +616,9 @@ func TestDefault_noDeviceChanges(t *testing.T) {
profAfter := &agd.Profile{
CustomDomains: &agd.AccountCustomDomains{},
AdultBlockingMode: &dnsmsg.BlockingModeNullIP{},
BlockingMode: &dnsmsg.BlockingModeNullIP{},
SafeBrowsingBlockingMode: &dnsmsg.BlockingModeNullIP{},
ID: profiledbtest.ProfileID,
FilteringEnabled: false,
DeviceIDs: container.NewMapSet(profiledbtest.DeviceID),

View File

@@ -0,0 +1,24 @@
// Package fcpb contains the new opaque api protobuf structures for the profile
// cache.
package fcpb
import (
"fmt"
"net/netip"
)
// CIDRRangesToPrefixes is a helper that converts a slice of CidrRange to the
// slice of [netip.Prefix].
func CIDRRangesToPrefixes(cidrs []*CidrRange) (out []netip.Prefix) {
for _, c := range cidrs {
addr, ok := netip.AddrFromSlice(c.GetAddress())
if !ok {
// Should never happen.
panic(fmt.Errorf("bad address: %v", c.GetAddress()))
}
out = append(out, netip.PrefixFrom(addr, int(c.GetPrefix())))
}
return out
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
// Package filecacheopb contains encoding and decoding logic for opaque file
// cache.
package filecacheopb

View File

@@ -0,0 +1,472 @@
package filecacheopb
import (
"fmt"
"log/slog"
"net/netip"
"slices"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/access"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdprotobuf"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtime"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/custom"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb/internal/fcpb"
"github.com/AdguardTeam/golibs/container"
"github.com/AdguardTeam/golibs/errors"
"github.com/c2h5oh/datasize"
)
// profilesToInternal converts protobuf profile structures into internal ones.
// baseCustomLogger and cons must not be nil.
//
//lint:ignore U1000 TODO(f.setrakov): Use.
func profilesToInternal(
pbProfiles []*fcpb.Profile,
baseCustomLogger *slog.Logger,
cons *access.ProfileConstructor,
respSzEst datasize.ByteSize,
) (profiles []*agd.Profile, err error) {
profiles = make([]*agd.Profile, 0, len(pbProfiles))
for i, pbProf := range pbProfiles {
var prof *agd.Profile
prof, err = profileToInternal(pbProf, baseCustomLogger, cons, respSzEst)
if err != nil {
return nil, fmt.Errorf("profile at index %d: %w", i, err)
}
profiles = append(profiles, prof)
}
return profiles, nil
}
// profileToInternal converts a protobuf profile structure to an internal one.
// baseCustomLogger, cons and pbProfile must not be nil.
func profileToInternal(
pbProfile *fcpb.Profile,
baseCustomLogger *slog.Logger,
cons *access.ProfileConstructor,
respSzEst datasize.ByteSize,
) (prof *agd.Profile, err error) {
bmAdult, bmSafeBrowsing, bm, err := blockingModesToInternal(pbProfile)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
customDomains, err := customDomainsToInternal(pbProfile)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
fltConf, err := configClientToInternal(pbProfile, baseCustomLogger)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
return &agd.Profile{
CustomDomains: customDomains,
FilterConfig: fltConf,
Access: accessToInternal(pbProfile.GetAccess(), cons),
AdultBlockingMode: bmAdult,
BlockingMode: bm,
SafeBrowsingBlockingMode: bmSafeBrowsing,
Ratelimiter: rateLimiterToInternal(pbProfile.GetRatelimiter(), respSzEst),
AccountID: agd.AccountID(pbProfile.GetAccountId()),
ID: agd.ProfileID(pbProfile.GetProfileId()),
// Consider device IDs to have been prevalidated.
DeviceIDs: container.NewMapSet(
agdprotobuf.UnsafelyConvertStrSlice[string, agd.DeviceID](pbProfile.GetDeviceIds())...,
),
// Consider rule-list IDs to have been prevalidated.
FilteredResponseTTL: pbProfile.GetFilteredResponseTtl().AsDuration(),
AutoDevicesEnabled: pbProfile.GetAutoDevicesEnabled(),
BlockChromePrefetch: pbProfile.GetBlockChromePrefetch(),
BlockFirefoxCanary: pbProfile.GetBlockFirefoxCanary(),
BlockPrivateRelay: pbProfile.GetBlockPrivateRelay(),
Deleted: pbProfile.GetDeleted(),
FilteringEnabled: pbProfile.GetFilteringEnabled(),
IPLogEnabled: pbProfile.GetIpLogEnabled(),
QueryLogEnabled: pbProfile.GetQueryLogEnabled(),
}, nil
}
// customDomainsToInternal converts profile's protobuf custom domains structures
// into internal ones. pbProfile must not be nil.
func customDomainsToInternal(
pbProfile *fcpb.Profile,
) (customDomains *agd.AccountCustomDomains, err error) {
pbConfs := pbProfile.GetCustomDomains().GetDomains()
customDomainConfs, err := customDomainConfsToInternal(pbConfs)
if err != nil {
return nil, fmt.Errorf("custom domain configs: %w", err)
}
customDomains = &agd.AccountCustomDomains{
Domains: customDomainConfs,
Enabled: pbProfile.GetCustomDomains().GetEnabled(),
}
return customDomains, nil
}
// configClientToInternal converts profile's protobuf config client structures
// into internal ones. pbProfile and baseCustomLogger must not be nil.
func configClientToInternal(
pbProfile *fcpb.Profile,
baseCustomLogger *slog.Logger,
) (fltConf *filter.ConfigClient, err error) {
pbFltConf := pbProfile.GetFilterConfig()
pbFltConfSched := pbFltConf.GetParental().GetPauseSchedule()
schedule, err := filterConfigScheduleToInternal(pbFltConfSched)
if err != nil {
return nil, fmt.Errorf("pause schedule: %w", err)
}
// Consider the rules to have been prevalidated.
pbRules := pbFltConf.GetCustom().GetRules()
rules := agdprotobuf.UnsafelyConvertStrSlice[string, filter.RuleText](pbRules)
var flt filter.Custom
if len(rules) > 0 {
flt = custom.New(&custom.Config{
Logger: baseCustomLogger.With("client_id", pbProfile.GetProfileId()),
Rules: rules,
})
}
ruleListIDs := pbFltConf.GetRuleList().GetIds()
safeBrowsing := pbFltConf.GetSafeBrowsing()
fltConf = &filter.ConfigClient{
Custom: &filter.ConfigCustom{
Filter: flt,
Enabled: pbFltConf.GetCustom().GetEnabled(),
},
Parental: configParentalToInternal(pbFltConf, schedule),
RuleList: &filter.ConfigRuleList{
// Consider rule-list IDs to have been prevalidated.
IDs: agdprotobuf.UnsafelyConvertStrSlice[string, filter.ID](ruleListIDs),
Enabled: pbFltConf.GetRuleList().GetEnabled(),
},
SafeBrowsing: &filter.ConfigSafeBrowsing{
Enabled: safeBrowsing.GetEnabled(),
DangerousDomainsEnabled: safeBrowsing.GetDangerousDomainsEnabled(),
NewlyRegisteredDomainsEnabled: safeBrowsing.GetNewlyRegisteredDomainsEnabled(),
},
}
return fltConf, nil
}
// configParentalToInternal converts filter config's protobuf parental config
// structures to internal ones. pbFltConf must not be nil.
func configParentalToInternal(
pbFltConf *fcpb.FilterConfig,
schedule *filter.ConfigSchedule,
) (c *filter.ConfigParental) {
parental := pbFltConf.GetParental()
return &filter.ConfigParental{
PauseSchedule: schedule,
// Consider blocked-service IDs to have been prevalidated.
BlockedServices: agdprotobuf.UnsafelyConvertStrSlice[string, filter.BlockedServiceID](
parental.GetBlockedServices(),
),
Enabled: parental.GetEnabled(),
AdultBlockingEnabled: parental.GetAdultBlockingEnabled(),
SafeSearchGeneralEnabled: parental.GetSafeSearchGeneralEnabled(),
SafeSearchYouTubeEnabled: parental.GetSafeSearchYoutubeEnabled(),
}
}
// blockingModesToInternal converts profile's protobuf blocking mode structures
// into internal ones.
func blockingModesToInternal(pbProfile *fcpb.Profile) (
bmAdult dnsmsg.BlockingMode,
bmSafeBrowsing dnsmsg.BlockingMode,
bm dnsmsg.BlockingMode,
err error,
) {
bmAdult, err = adultBlockingModeToInternal(pbProfile)
if err != nil {
return nil, nil, nil, fmt.Errorf("adult blocking mode: %w", err)
}
bmSafeBrowsing, err = safeBrowsingBlockingModeToInternal(pbProfile)
if err != nil {
return nil, nil, nil, fmt.Errorf("safe browsing blocking mode: %w", err)
}
bm, err = blockingModeToInternal(pbProfile)
if err != nil {
return nil, nil, nil, fmt.Errorf("blocking mode: %w", err)
}
return bmAdult, bmSafeBrowsing, bm, nil
}
// blockingModeToInternal converts a protobuf blocking-mode sum-type to an
// internal one.
// TODO(d.kolyshev): DRY with adultBlockingModeToInternal and
// safeBrowsingBlockingModeToInternal.
func blockingModeToInternal(pbProfile *fcpb.Profile) (m dnsmsg.BlockingMode, err error) {
if !pbProfile.HasBlockingMode() {
return nil, nil
}
customIP := pbProfile.GetBlockingModeCustomIp()
switch {
case customIP != nil:
var ipv4 []netip.Addr
ipv4, err = agdprotobuf.ByteSlicesToIPs(customIP.GetIpv4())
if err != nil {
return nil, fmt.Errorf("bad v4 custom ips: %w", err)
}
var ipv6 []netip.Addr
ipv6, err = agdprotobuf.ByteSlicesToIPs(customIP.GetIpv6())
if err != nil {
return nil, fmt.Errorf("bad v6 custom ips: %w", err)
}
return &dnsmsg.BlockingModeCustomIP{
IPv4: ipv4,
IPv6: ipv6,
}, nil
case pbProfile.GetBlockingModeNxdomain() != nil:
return &dnsmsg.BlockingModeNXDOMAIN{}, nil
case pbProfile.GetBlockingModeNullIp() != nil:
return &dnsmsg.BlockingModeNullIP{}, nil
case pbProfile.GetBlockingModeRefused() != nil:
return &dnsmsg.BlockingModeREFUSED{}, nil
default:
return nil, fmt.Errorf("bad pb blocking mode")
}
}
// adultBlockingModeToInternal converts a protobuf blocking-mode sum-type to an
// internal one.
func adultBlockingModeToInternal(pbProfile *fcpb.Profile) (m dnsmsg.BlockingMode, err error) {
if !pbProfile.HasAdultBlockingMode() {
return nil, nil
}
customIP := pbProfile.GetAdultBlockingModeCustomIp()
switch {
case customIP != nil:
var ipv4 []netip.Addr
ipv4, err = agdprotobuf.ByteSlicesToIPs(customIP.GetIpv4())
if err != nil {
return nil, fmt.Errorf("bad v4 custom ips: %w", err)
}
var ipv6 []netip.Addr
ipv6, err = agdprotobuf.ByteSlicesToIPs(customIP.GetIpv6())
if err != nil {
return nil, fmt.Errorf("bad v6 custom ips: %w", err)
}
return &dnsmsg.BlockingModeCustomIP{
IPv4: ipv4,
IPv6: ipv6,
}, nil
case pbProfile.GetAdultBlockingModeNxdomain() != nil:
return &dnsmsg.BlockingModeNXDOMAIN{}, nil
case pbProfile.GetAdultBlockingModeNullIp() != nil:
return &dnsmsg.BlockingModeNullIP{}, nil
case pbProfile.GetAdultBlockingModeRefused() != nil:
return &dnsmsg.BlockingModeREFUSED{}, nil
default:
return nil, fmt.Errorf("bad pb adult blocking mode")
}
}
// safeBrowsingBlockingModeToInternal converts a protobuf blocking-mode sum-type
// to an internal one.
func safeBrowsingBlockingModeToInternal(
pbProfile *fcpb.Profile,
) (m dnsmsg.BlockingMode, err error) {
if !pbProfile.HasSafeBrowsingBlockingMode() {
return nil, nil
}
customIP := pbProfile.GetSafeBrowsingBlockingModeCustomIp()
switch {
case customIP != nil:
var ipv4 []netip.Addr
ipv4, err = agdprotobuf.ByteSlicesToIPs(customIP.GetIpv4())
if err != nil {
return nil, fmt.Errorf("bad v4 custom ips: %w", err)
}
var ipv6 []netip.Addr
ipv6, err = agdprotobuf.ByteSlicesToIPs(customIP.GetIpv6())
if err != nil {
return nil, fmt.Errorf("bad v6 custom ips: %w", err)
}
return &dnsmsg.BlockingModeCustomIP{
IPv4: ipv4,
IPv6: ipv6,
}, nil
case pbProfile.GetSafeBrowsingBlockingModeNxdomain() != nil:
return &dnsmsg.BlockingModeNXDOMAIN{}, nil
case pbProfile.GetSafeBrowsingBlockingModeNullIp() != nil:
return &dnsmsg.BlockingModeNullIP{}, nil
case pbProfile.GetSafeBrowsingBlockingModeRefused() != nil:
return &dnsmsg.BlockingModeREFUSED{}, nil
default:
return nil, fmt.Errorf("bad pb safe browsing blocking mode")
}
}
// filterConfigScheduleToInternal converts a protobuf protection-schedule
// structure to an internal one.
func filterConfigScheduleToInternal(
pbSchedule *fcpb.FilterConfig_Schedule,
) (c *filter.ConfigSchedule, err error) {
if pbSchedule == nil {
return nil, nil
}
loc, err := agdtime.LoadLocation(pbSchedule.GetTimeZone())
if err != nil {
return nil, fmt.Errorf("time zone: %w", err)
}
week := pbSchedule.GetWeek()
return &filter.ConfigSchedule{
// Consider the lengths to be prevalidated.
Week: &filter.WeeklySchedule{
time.Monday: dayIntervalToInternal(week.GetMon()),
time.Tuesday: dayIntervalToInternal(week.GetTue()),
time.Wednesday: dayIntervalToInternal(week.GetWed()),
time.Thursday: dayIntervalToInternal(week.GetThu()),
time.Friday: dayIntervalToInternal(week.GetFri()),
time.Saturday: dayIntervalToInternal(week.GetSat()),
time.Sunday: dayIntervalToInternal(week.GetSun()),
},
TimeZone: loc,
}, nil
}
// dayIntervalToInternal converts a protobuf day interval to an internal one.
func dayIntervalToInternal(pbInterval *fcpb.DayInterval) (i *filter.DayInterval) {
if pbInterval == nil {
return nil
}
return &filter.DayInterval{
// #nosec G115 -- The values put in these are always from uint16s.
Start: uint16(pbInterval.GetStart()),
// #nosec G115 -- The values put in these are always from uint16s.
End: uint16(pbInterval.GetEnd()),
}
}
// customDomainConfsToInternal converts protobuf custom-domain configurations to
// internal ones.
func customDomainConfsToInternal(
pbConfs []*fcpb.CustomDomainConfig,
) (confs []*agd.CustomDomainConfig, err error) {
l := len(pbConfs)
if l == 0 {
return nil, nil
}
confs = make([]*agd.CustomDomainConfig, 0, l)
for i, pbConf := range pbConfs {
var c *agd.CustomDomainConfig
c, err = customDomainConfigToInternal(pbConf)
if err != nil {
return nil, fmt.Errorf("at index %d: %w", i, err)
}
confs = append(confs, c)
}
return confs, nil
}
// customDomainConfigToInternal converts a protobuf custom-domain config to an
// internal one. pbCustomDomainConf must not be nil.
func customDomainConfigToInternal(
pbCustomDomainConf *fcpb.CustomDomainConfig,
) (c *agd.CustomDomainConfig, err error) {
var state agd.CustomDomainState
if current := pbCustomDomainConf.GetStateCurrent(); current != nil {
state = &agd.CustomDomainStateCurrent{
NotBefore: current.GetNotBefore().AsTime(),
NotAfter: current.GetNotAfter().AsTime(),
// Consider certificate names to have been prevalidated.
CertName: agd.CertificateName(current.GetCertName()),
Enabled: current.GetEnabled(),
}
} else if pending := pbCustomDomainConf.GetStatePending(); pending != nil {
state = &agd.CustomDomainStatePending{
Expire: pending.GetExpire().AsTime(),
WellKnownPath: pending.GetWellKnownPath(),
}
} else {
return nil, fmt.Errorf("pb unknown domain state: %w", errors.ErrBadEnumValue)
}
return &agd.CustomDomainConfig{
State: state,
Domains: slices.Clone(pbCustomDomainConf.GetDomains()),
}, nil
}
// rateLimiterToInternal converts a protobuf rate-limiting settings structure to
// an internal one.
func rateLimiterToInternal(
pbRateLimiter *fcpb.Ratelimiter,
respSzEst datasize.ByteSize,
) (r agd.Ratelimiter) {
if !pbRateLimiter.GetEnabled() {
return agd.GlobalRatelimiter{}
}
return agd.NewDefaultRatelimiter(&agd.RatelimitConfig{
ClientSubnets: fcpb.CIDRRangesToPrefixes(pbRateLimiter.GetClientCidr()),
RPS: pbRateLimiter.GetRps(),
Enabled: pbRateLimiter.GetEnabled(),
}, respSzEst)
}
// accessToInternal converts protobuf access settings to an internal structure.
// If x is nil, toInternal returns [access.EmptyProfile]. If pbAccess is not
// nil cons must not be nil.
func accessToInternal(pbAccess *fcpb.Access, cons *access.ProfileConstructor) (a access.Profile) {
if pbAccess == nil {
return access.EmptyProfile{}
}
allowedASN := pbAccess.GetAllowlistAsn()
blockedASN := pbAccess.GetBlocklistAsn()
return cons.New(&access.ProfileConfig{
AllowedNets: fcpb.CIDRRangesToPrefixes(pbAccess.GetAllowlistCidr()),
BlockedNets: fcpb.CIDRRangesToPrefixes(pbAccess.GetBlocklistCidr()),
AllowedASN: agdprotobuf.UnsafelyConvertUint32Slice[uint32, geoip.ASN](allowedASN),
BlockedASN: agdprotobuf.UnsafelyConvertUint32Slice[uint32, geoip.ASN](blockedASN),
BlocklistDomainRules: pbAccess.GetBlocklistDomainRules(),
StandardEnabled: pbAccess.GetStandardEnabled(),
})
}

View File

@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.8
// protoc-gen-go v1.36.10
// protoc v6.32.0
// source: filecache.proto
@@ -116,6 +116,20 @@ type Profile struct {
FilteringEnabled bool `protobuf:"varint,16,opt,name=filtering_enabled,json=filteringEnabled,proto3" json:"filtering_enabled,omitempty"`
IpLogEnabled bool `protobuf:"varint,17,opt,name=ip_log_enabled,json=ipLogEnabled,proto3" json:"ip_log_enabled,omitempty"`
QueryLogEnabled bool `protobuf:"varint,18,opt,name=query_log_enabled,json=queryLogEnabled,proto3" json:"query_log_enabled,omitempty"`
// Types that are valid to be assigned to AdultBlockingMode:
//
// *Profile_AdultBlockingModeCustomIp
// *Profile_AdultBlockingModeNxdomain
// *Profile_AdultBlockingModeNullIp
// *Profile_AdultBlockingModeRefused
AdultBlockingMode isProfile_AdultBlockingMode `protobuf_oneof:"adult_blocking_mode"`
// Types that are valid to be assigned to SafeBrowsingBlockingMode:
//
// *Profile_SafeBrowsingBlockingModeCustomIp
// *Profile_SafeBrowsingBlockingModeNxdomain
// *Profile_SafeBrowsingBlockingModeNullIp
// *Profile_SafeBrowsingBlockingModeRefused
SafeBrowsingBlockingMode isProfile_SafeBrowsingBlockingMode `protobuf_oneof:"safe_browsing_blocking_mode"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -305,6 +319,92 @@ func (x *Profile) GetQueryLogEnabled() bool {
return false
}
func (x *Profile) GetAdultBlockingMode() isProfile_AdultBlockingMode {
if x != nil {
return x.AdultBlockingMode
}
return nil
}
func (x *Profile) GetAdultBlockingModeCustomIp() *BlockingModeCustomIP {
if x != nil {
if x, ok := x.AdultBlockingMode.(*Profile_AdultBlockingModeCustomIp); ok {
return x.AdultBlockingModeCustomIp
}
}
return nil
}
func (x *Profile) GetAdultBlockingModeNxdomain() *BlockingModeNXDOMAIN {
if x != nil {
if x, ok := x.AdultBlockingMode.(*Profile_AdultBlockingModeNxdomain); ok {
return x.AdultBlockingModeNxdomain
}
}
return nil
}
func (x *Profile) GetAdultBlockingModeNullIp() *BlockingModeNullIP {
if x != nil {
if x, ok := x.AdultBlockingMode.(*Profile_AdultBlockingModeNullIp); ok {
return x.AdultBlockingModeNullIp
}
}
return nil
}
func (x *Profile) GetAdultBlockingModeRefused() *BlockingModeREFUSED {
if x != nil {
if x, ok := x.AdultBlockingMode.(*Profile_AdultBlockingModeRefused); ok {
return x.AdultBlockingModeRefused
}
}
return nil
}
func (x *Profile) GetSafeBrowsingBlockingMode() isProfile_SafeBrowsingBlockingMode {
if x != nil {
return x.SafeBrowsingBlockingMode
}
return nil
}
func (x *Profile) GetSafeBrowsingBlockingModeCustomIp() *BlockingModeCustomIP {
if x != nil {
if x, ok := x.SafeBrowsingBlockingMode.(*Profile_SafeBrowsingBlockingModeCustomIp); ok {
return x.SafeBrowsingBlockingModeCustomIp
}
}
return nil
}
func (x *Profile) GetSafeBrowsingBlockingModeNxdomain() *BlockingModeNXDOMAIN {
if x != nil {
if x, ok := x.SafeBrowsingBlockingMode.(*Profile_SafeBrowsingBlockingModeNxdomain); ok {
return x.SafeBrowsingBlockingModeNxdomain
}
}
return nil
}
func (x *Profile) GetSafeBrowsingBlockingModeNullIp() *BlockingModeNullIP {
if x != nil {
if x, ok := x.SafeBrowsingBlockingMode.(*Profile_SafeBrowsingBlockingModeNullIp); ok {
return x.SafeBrowsingBlockingModeNullIp
}
}
return nil
}
func (x *Profile) GetSafeBrowsingBlockingModeRefused() *BlockingModeREFUSED {
if x != nil {
if x, ok := x.SafeBrowsingBlockingMode.(*Profile_SafeBrowsingBlockingModeRefused); ok {
return x.SafeBrowsingBlockingModeRefused
}
}
return nil
}
type isProfile_BlockingMode interface {
isProfile_BlockingMode()
}
@@ -333,6 +433,62 @@ func (*Profile_BlockingModeNullIp) isProfile_BlockingMode() {}
func (*Profile_BlockingModeRefused) isProfile_BlockingMode() {}
type isProfile_AdultBlockingMode interface {
isProfile_AdultBlockingMode()
}
type Profile_AdultBlockingModeCustomIp struct {
AdultBlockingModeCustomIp *BlockingModeCustomIP `protobuf:"bytes,21,opt,name=adult_blocking_mode_custom_ip,json=adultBlockingModeCustomIp,proto3,oneof"`
}
type Profile_AdultBlockingModeNxdomain struct {
AdultBlockingModeNxdomain *BlockingModeNXDOMAIN `protobuf:"bytes,22,opt,name=adult_blocking_mode_nxdomain,json=adultBlockingModeNxdomain,proto3,oneof"`
}
type Profile_AdultBlockingModeNullIp struct {
AdultBlockingModeNullIp *BlockingModeNullIP `protobuf:"bytes,23,opt,name=adult_blocking_mode_null_ip,json=adultBlockingModeNullIp,proto3,oneof"`
}
type Profile_AdultBlockingModeRefused struct {
AdultBlockingModeRefused *BlockingModeREFUSED `protobuf:"bytes,24,opt,name=adult_blocking_mode_refused,json=adultBlockingModeRefused,proto3,oneof"`
}
func (*Profile_AdultBlockingModeCustomIp) isProfile_AdultBlockingMode() {}
func (*Profile_AdultBlockingModeNxdomain) isProfile_AdultBlockingMode() {}
func (*Profile_AdultBlockingModeNullIp) isProfile_AdultBlockingMode() {}
func (*Profile_AdultBlockingModeRefused) isProfile_AdultBlockingMode() {}
type isProfile_SafeBrowsingBlockingMode interface {
isProfile_SafeBrowsingBlockingMode()
}
type Profile_SafeBrowsingBlockingModeCustomIp struct {
SafeBrowsingBlockingModeCustomIp *BlockingModeCustomIP `protobuf:"bytes,25,opt,name=safe_browsing_blocking_mode_custom_ip,json=safeBrowsingBlockingModeCustomIp,proto3,oneof"`
}
type Profile_SafeBrowsingBlockingModeNxdomain struct {
SafeBrowsingBlockingModeNxdomain *BlockingModeNXDOMAIN `protobuf:"bytes,26,opt,name=safe_browsing_blocking_mode_nxdomain,json=safeBrowsingBlockingModeNxdomain,proto3,oneof"`
}
type Profile_SafeBrowsingBlockingModeNullIp struct {
SafeBrowsingBlockingModeNullIp *BlockingModeNullIP `protobuf:"bytes,27,opt,name=safe_browsing_blocking_mode_null_ip,json=safeBrowsingBlockingModeNullIp,proto3,oneof"`
}
type Profile_SafeBrowsingBlockingModeRefused struct {
SafeBrowsingBlockingModeRefused *BlockingModeREFUSED `protobuf:"bytes,28,opt,name=safe_browsing_blocking_mode_refused,json=safeBrowsingBlockingModeRefused,proto3,oneof"`
}
func (*Profile_SafeBrowsingBlockingModeCustomIp) isProfile_SafeBrowsingBlockingMode() {}
func (*Profile_SafeBrowsingBlockingModeNxdomain) isProfile_SafeBrowsingBlockingMode() {}
func (*Profile_SafeBrowsingBlockingModeNullIp) isProfile_SafeBrowsingBlockingMode() {}
func (*Profile_SafeBrowsingBlockingModeRefused) isProfile_SafeBrowsingBlockingMode() {}
type AccountCustomDomains struct {
state protoimpl.MessageState `protogen:"open.v1"`
Domains []*CustomDomainConfig `protobuf:"bytes,1,rep,name=domains,proto3" json:"domains,omitempty"`
@@ -1638,7 +1794,7 @@ const file_filecache_proto_rawDesc = "" +
"\tsync_time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\bsyncTime\x12.\n" +
"\bprofiles\x18\x02 \x03(\v2\x12.profiledb.ProfileR\bprofiles\x12+\n" +
"\adevices\x18\x03 \x03(\v2\x11.profiledb.DeviceR\adevices\x12\x18\n" +
"\aversion\x18\x04 \x01(\x05R\aversion\"\xef\b\n" +
"\aversion\x18\x04 \x01(\x05R\aversion\"\xf3\x0f\n" +
"\aProfile\x12F\n" +
"\x0ecustom_domains\x18\x14 \x01(\v2\x1f.profiledb.AccountCustomDomainsR\rcustomDomains\x12<\n" +
"\rfilter_config\x18\x01 \x01(\v2\x17.profiledb.FilterConfigR\ffilterConfig\x12)\n" +
@@ -1663,8 +1819,18 @@ const file_filecache_proto_rawDesc = "" +
"\adeleted\x18\x0f \x01(\bR\adeleted\x12+\n" +
"\x11filtering_enabled\x18\x10 \x01(\bR\x10filteringEnabled\x12$\n" +
"\x0eip_log_enabled\x18\x11 \x01(\bR\fipLogEnabled\x12*\n" +
"\x11query_log_enabled\x18\x12 \x01(\bR\x0fqueryLogEnabledB\x0f\n" +
"\rblocking_mode\"i\n" +
"\x11query_log_enabled\x18\x12 \x01(\bR\x0fqueryLogEnabled\x12c\n" +
"\x1dadult_blocking_mode_custom_ip\x18\x15 \x01(\v2\x1f.profiledb.BlockingModeCustomIPH\x01R\x19adultBlockingModeCustomIp\x12b\n" +
"\x1cadult_blocking_mode_nxdomain\x18\x16 \x01(\v2\x1f.profiledb.BlockingModeNXDOMAINH\x01R\x19adultBlockingModeNxdomain\x12]\n" +
"\x1badult_blocking_mode_null_ip\x18\x17 \x01(\v2\x1d.profiledb.BlockingModeNullIPH\x01R\x17adultBlockingModeNullIp\x12_\n" +
"\x1badult_blocking_mode_refused\x18\x18 \x01(\v2\x1e.profiledb.BlockingModeREFUSEDH\x01R\x18adultBlockingModeRefused\x12r\n" +
"%safe_browsing_blocking_mode_custom_ip\x18\x19 \x01(\v2\x1f.profiledb.BlockingModeCustomIPH\x02R safeBrowsingBlockingModeCustomIp\x12q\n" +
"$safe_browsing_blocking_mode_nxdomain\x18\x1a \x01(\v2\x1f.profiledb.BlockingModeNXDOMAINH\x02R safeBrowsingBlockingModeNxdomain\x12l\n" +
"#safe_browsing_blocking_mode_null_ip\x18\x1b \x01(\v2\x1d.profiledb.BlockingModeNullIPH\x02R\x1esafeBrowsingBlockingModeNullIp\x12n\n" +
"#safe_browsing_blocking_mode_refused\x18\x1c \x01(\v2\x1e.profiledb.BlockingModeREFUSEDH\x02R\x1fsafeBrowsingBlockingModeRefusedB\x0f\n" +
"\rblocking_modeB\x15\n" +
"\x13adult_blocking_modeB\x1d\n" +
"\x1bsafe_browsing_blocking_mode\"i\n" +
"\x14AccountCustomDomains\x127\n" +
"\adomains\x18\x01 \x03(\v2\x1d.profiledb.CustomDomainConfigR\adomains\x12\x18\n" +
"\aenabled\x18\x02 \x01(\bR\aenabled\"\x85\x04\n" +
@@ -1807,34 +1973,42 @@ var file_filecache_proto_depIdxs = []int32{
9, // 9: profiledb.Profile.blocking_mode_refused:type_name -> profiledb.BlockingModeREFUSED
14, // 10: profiledb.Profile.ratelimiter:type_name -> profiledb.Ratelimiter
24, // 11: profiledb.Profile.filtered_response_ttl:type_name -> google.protobuf.Duration
3, // 12: profiledb.AccountCustomDomains.domains:type_name -> profiledb.CustomDomainConfig
15, // 13: profiledb.CustomDomainConfig.state_current:type_name -> profiledb.CustomDomainConfig.StateCurrent
16, // 14: profiledb.CustomDomainConfig.state_pending:type_name -> profiledb.CustomDomainConfig.StatePending
17, // 15: profiledb.FilterConfig.custom:type_name -> profiledb.FilterConfig.Custom
18, // 16: profiledb.FilterConfig.parental:type_name -> profiledb.FilterConfig.Parental
21, // 17: profiledb.FilterConfig.rule_list:type_name -> profiledb.FilterConfig.RuleList
22, // 18: profiledb.FilterConfig.safe_browsing:type_name -> profiledb.FilterConfig.SafeBrowsing
13, // 19: profiledb.Device.authentication:type_name -> profiledb.AuthenticationSettings
12, // 20: profiledb.Access.allowlist_cidr:type_name -> profiledb.CidrRange
12, // 21: profiledb.Access.blocklist_cidr:type_name -> profiledb.CidrRange
12, // 22: profiledb.Ratelimiter.client_cidr:type_name -> profiledb.CidrRange
23, // 23: profiledb.CustomDomainConfig.StateCurrent.not_before:type_name -> google.protobuf.Timestamp
23, // 24: profiledb.CustomDomainConfig.StateCurrent.not_after:type_name -> google.protobuf.Timestamp
23, // 25: profiledb.CustomDomainConfig.StatePending.expire:type_name -> google.protobuf.Timestamp
19, // 26: profiledb.FilterConfig.Parental.pause_schedule:type_name -> profiledb.FilterConfig.Schedule
20, // 27: profiledb.FilterConfig.Schedule.week:type_name -> profiledb.FilterConfig.WeeklySchedule
5, // 28: profiledb.FilterConfig.WeeklySchedule.mon:type_name -> profiledb.DayInterval
5, // 29: profiledb.FilterConfig.WeeklySchedule.tue:type_name -> profiledb.DayInterval
5, // 30: profiledb.FilterConfig.WeeklySchedule.wed:type_name -> profiledb.DayInterval
5, // 31: profiledb.FilterConfig.WeeklySchedule.thu:type_name -> profiledb.DayInterval
5, // 32: profiledb.FilterConfig.WeeklySchedule.fri:type_name -> profiledb.DayInterval
5, // 33: profiledb.FilterConfig.WeeklySchedule.sat:type_name -> profiledb.DayInterval
5, // 34: profiledb.FilterConfig.WeeklySchedule.sun:type_name -> profiledb.DayInterval
35, // [35:35] is the sub-list for method output_type
35, // [35:35] is the sub-list for method input_type
35, // [35:35] is the sub-list for extension type_name
35, // [35:35] is the sub-list for extension extendee
0, // [0:35] is the sub-list for field type_name
6, // 12: profiledb.Profile.adult_blocking_mode_custom_ip:type_name -> profiledb.BlockingModeCustomIP
7, // 13: profiledb.Profile.adult_blocking_mode_nxdomain:type_name -> profiledb.BlockingModeNXDOMAIN
8, // 14: profiledb.Profile.adult_blocking_mode_null_ip:type_name -> profiledb.BlockingModeNullIP
9, // 15: profiledb.Profile.adult_blocking_mode_refused:type_name -> profiledb.BlockingModeREFUSED
6, // 16: profiledb.Profile.safe_browsing_blocking_mode_custom_ip:type_name -> profiledb.BlockingModeCustomIP
7, // 17: profiledb.Profile.safe_browsing_blocking_mode_nxdomain:type_name -> profiledb.BlockingModeNXDOMAIN
8, // 18: profiledb.Profile.safe_browsing_blocking_mode_null_ip:type_name -> profiledb.BlockingModeNullIP
9, // 19: profiledb.Profile.safe_browsing_blocking_mode_refused:type_name -> profiledb.BlockingModeREFUSED
3, // 20: profiledb.AccountCustomDomains.domains:type_name -> profiledb.CustomDomainConfig
15, // 21: profiledb.CustomDomainConfig.state_current:type_name -> profiledb.CustomDomainConfig.StateCurrent
16, // 22: profiledb.CustomDomainConfig.state_pending:type_name -> profiledb.CustomDomainConfig.StatePending
17, // 23: profiledb.FilterConfig.custom:type_name -> profiledb.FilterConfig.Custom
18, // 24: profiledb.FilterConfig.parental:type_name -> profiledb.FilterConfig.Parental
21, // 25: profiledb.FilterConfig.rule_list:type_name -> profiledb.FilterConfig.RuleList
22, // 26: profiledb.FilterConfig.safe_browsing:type_name -> profiledb.FilterConfig.SafeBrowsing
13, // 27: profiledb.Device.authentication:type_name -> profiledb.AuthenticationSettings
12, // 28: profiledb.Access.allowlist_cidr:type_name -> profiledb.CidrRange
12, // 29: profiledb.Access.blocklist_cidr:type_name -> profiledb.CidrRange
12, // 30: profiledb.Ratelimiter.client_cidr:type_name -> profiledb.CidrRange
23, // 31: profiledb.CustomDomainConfig.StateCurrent.not_before:type_name -> google.protobuf.Timestamp
23, // 32: profiledb.CustomDomainConfig.StateCurrent.not_after:type_name -> google.protobuf.Timestamp
23, // 33: profiledb.CustomDomainConfig.StatePending.expire:type_name -> google.protobuf.Timestamp
19, // 34: profiledb.FilterConfig.Parental.pause_schedule:type_name -> profiledb.FilterConfig.Schedule
20, // 35: profiledb.FilterConfig.Schedule.week:type_name -> profiledb.FilterConfig.WeeklySchedule
5, // 36: profiledb.FilterConfig.WeeklySchedule.mon:type_name -> profiledb.DayInterval
5, // 37: profiledb.FilterConfig.WeeklySchedule.tue:type_name -> profiledb.DayInterval
5, // 38: profiledb.FilterConfig.WeeklySchedule.wed:type_name -> profiledb.DayInterval
5, // 39: profiledb.FilterConfig.WeeklySchedule.thu:type_name -> profiledb.DayInterval
5, // 40: profiledb.FilterConfig.WeeklySchedule.fri:type_name -> profiledb.DayInterval
5, // 41: profiledb.FilterConfig.WeeklySchedule.sat:type_name -> profiledb.DayInterval
5, // 42: profiledb.FilterConfig.WeeklySchedule.sun:type_name -> profiledb.DayInterval
43, // [43:43] is the sub-list for method output_type
43, // [43:43] is the sub-list for method input_type
43, // [43:43] is the sub-list for extension type_name
43, // [43:43] is the sub-list for extension extendee
0, // [0:43] is the sub-list for field type_name
}
func init() { file_filecache_proto_init() }
@@ -1847,6 +2021,14 @@ func file_filecache_proto_init() {
(*Profile_BlockingModeNxdomain)(nil),
(*Profile_BlockingModeNullIp)(nil),
(*Profile_BlockingModeRefused)(nil),
(*Profile_AdultBlockingModeCustomIp)(nil),
(*Profile_AdultBlockingModeNxdomain)(nil),
(*Profile_AdultBlockingModeNullIp)(nil),
(*Profile_AdultBlockingModeRefused)(nil),
(*Profile_SafeBrowsingBlockingModeCustomIp)(nil),
(*Profile_SafeBrowsingBlockingModeNxdomain)(nil),
(*Profile_SafeBrowsingBlockingModeNullIp)(nil),
(*Profile_SafeBrowsingBlockingModeRefused)(nil),
}
file_filecache_proto_msgTypes[3].OneofWrappers = []any{
(*CustomDomainConfig_StateCurrent_)(nil),

View File

@@ -2,8 +2,6 @@ syntax = "proto3";
package profiledb;
option go_package = "./filecachepb";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
@@ -43,6 +41,20 @@ message Profile {
bool filtering_enabled = 16;
bool ip_log_enabled = 17;
bool query_log_enabled = 18;
oneof adult_blocking_mode {
BlockingModeCustomIP adult_blocking_mode_custom_ip = 21;
BlockingModeNXDOMAIN adult_blocking_mode_nxdomain = 22;
BlockingModeNullIP adult_blocking_mode_null_ip = 23;
BlockingModeREFUSED adult_blocking_mode_refused = 24;
}
oneof safe_browsing_blocking_mode {
BlockingModeCustomIP safe_browsing_blocking_mode_custom_ip = 25;
BlockingModeNXDOMAIN safe_browsing_blocking_mode_nxdomain = 26;
BlockingModeNullIP safe_browsing_blocking_mode_null_ip = 27;
BlockingModeREFUSED safe_browsing_blocking_mode_refused = 28;
}
}
message AccountCustomDomains {

View File

@@ -90,6 +90,16 @@ func (x *Profile) toInternal(
cons *access.ProfileConstructor,
respSzEst datasize.ByteSize,
) (prof *agd.Profile, err error) {
adultBlockingMode, err := adultBlockingModeToInternal(x.AdultBlockingMode)
if err != nil {
return nil, fmt.Errorf("adult blocking mode: %w", err)
}
safeBrowsingBlockingMode, err := safeBrowsingBlockingModeToInternal(x.SafeBrowsingBlockingMode)
if err != nil {
return nil, fmt.Errorf("safe browsing blocking mode: %w", err)
}
m, err := blockingModeToInternal(x.BlockingMode)
if err != nil {
return nil, fmt.Errorf("blocking mode: %w", err)
@@ -102,7 +112,7 @@ func (x *Profile) toInternal(
}
// Consider the rules to have been prevalidated.
rules := unsafelyConvertStrSlice[string, filter.RuleText](pbFltConf.Custom.Rules)
rules := agdprotobuf.UnsafelyConvertStrSlice[string, filter.RuleText](pbFltConf.Custom.Rules)
var flt filter.Custom
if len(rules) > 0 {
@@ -130,7 +140,7 @@ func (x *Profile) toInternal(
Parental: &filter.ConfigParental{
PauseSchedule: schedule,
// Consider blocked-service IDs to have been prevalidated.
BlockedServices: unsafelyConvertStrSlice[string, filter.BlockedServiceID](
BlockedServices: agdprotobuf.UnsafelyConvertStrSlice[string, filter.BlockedServiceID](
pbFltConf.Parental.BlockedServices,
),
Enabled: pbFltConf.Parental.Enabled,
@@ -140,7 +150,7 @@ func (x *Profile) toInternal(
},
RuleList: &filter.ConfigRuleList{
// Consider rule-list IDs to have been prevalidated.
IDs: unsafelyConvertStrSlice[string, filter.ID](pbFltConf.RuleList.Ids),
IDs: agdprotobuf.UnsafelyConvertStrSlice[string, filter.ID](pbFltConf.RuleList.Ids),
Enabled: pbFltConf.RuleList.Enabled,
},
SafeBrowsing: &filter.ConfigSafeBrowsing{
@@ -155,7 +165,9 @@ func (x *Profile) toInternal(
FilterConfig: fltConf,
Access: x.Access.toInternal(cons),
AdultBlockingMode: adultBlockingMode,
BlockingMode: m,
SafeBrowsingBlockingMode: safeBrowsingBlockingMode,
Ratelimiter: x.Ratelimiter.toInternal(respSzEst),
AccountID: agd.AccountID(x.AccountId),
@@ -163,7 +175,7 @@ func (x *Profile) toInternal(
// Consider device IDs to have been prevalidated.
DeviceIDs: container.NewMapSet(
unsafelyConvertStrSlice[string, agd.DeviceID](x.DeviceIds)...,
agdprotobuf.UnsafelyConvertStrSlice[string, agd.DeviceID](x.DeviceIds)...,
),
// Consider rule-list IDs to have been prevalidated.
@@ -224,6 +236,8 @@ func (x *DayInterval) toInternal() (i *filter.DayInterval) {
// blockingModeToInternal converts a protobuf blocking-mode sum-type to an
// internal one.
// TODO(d.kolyshev): DRY with adultBlockingModeToInternal and
// safeBrowsingBlockingModeToInternal.
func blockingModeToInternal(pbm isProfile_BlockingMode) (m dnsmsg.BlockingMode, err error) {
switch pbm := pbm.(type) {
case *Profile_BlockingModeCustomIp:
@@ -255,6 +269,80 @@ func blockingModeToInternal(pbm isProfile_BlockingMode) (m dnsmsg.BlockingMode,
}
}
// adultBlockingModeToInternal converts a protobuf blocking-mode sum-type to an
// internal one.
func adultBlockingModeToInternal(
pbm isProfile_AdultBlockingMode,
) (m dnsmsg.BlockingMode, err error) {
switch pbm := pbm.(type) {
case nil:
return nil, nil
case *Profile_AdultBlockingModeCustomIp:
var ipv4 []netip.Addr
ipv4, err = agdprotobuf.ByteSlicesToIPs(pbm.AdultBlockingModeCustomIp.Ipv4)
if err != nil {
return nil, fmt.Errorf("bad v4 custom ips: %w", err)
}
var ipv6 []netip.Addr
ipv6, err = agdprotobuf.ByteSlicesToIPs(pbm.AdultBlockingModeCustomIp.Ipv6)
if err != nil {
return nil, fmt.Errorf("bad v6 custom ips: %w", err)
}
return &dnsmsg.BlockingModeCustomIP{
IPv4: ipv4,
IPv6: ipv6,
}, nil
case *Profile_AdultBlockingModeNxdomain:
return &dnsmsg.BlockingModeNXDOMAIN{}, nil
case *Profile_AdultBlockingModeNullIp:
return &dnsmsg.BlockingModeNullIP{}, nil
case *Profile_AdultBlockingModeRefused:
return &dnsmsg.BlockingModeREFUSED{}, nil
default:
// Consider unhandled type-switch cases programmer errors.
return nil, fmt.Errorf("bad pb adult blocking mode %T(%[1]v)", pbm)
}
}
// safeBrowsingBlockingModeToInternal converts a protobuf blocking-mode sum-type to an
// internal one.
func safeBrowsingBlockingModeToInternal(
pbm isProfile_SafeBrowsingBlockingMode,
) (m dnsmsg.BlockingMode, err error) {
switch pbm := pbm.(type) {
case nil:
return nil, nil
case *Profile_SafeBrowsingBlockingModeCustomIp:
var ipv4 []netip.Addr
ipv4, err = agdprotobuf.ByteSlicesToIPs(pbm.SafeBrowsingBlockingModeCustomIp.Ipv4)
if err != nil {
return nil, fmt.Errorf("bad v4 custom ips: %w", err)
}
var ipv6 []netip.Addr
ipv6, err = agdprotobuf.ByteSlicesToIPs(pbm.SafeBrowsingBlockingModeCustomIp.Ipv6)
if err != nil {
return nil, fmt.Errorf("bad v6 custom ips: %w", err)
}
return &dnsmsg.BlockingModeCustomIP{
IPv4: ipv4,
IPv6: ipv6,
}, nil
case *Profile_SafeBrowsingBlockingModeNxdomain:
return &dnsmsg.BlockingModeNXDOMAIN{}, nil
case *Profile_SafeBrowsingBlockingModeNullIp:
return &dnsmsg.BlockingModeNullIP{}, nil
case *Profile_SafeBrowsingBlockingModeRefused:
return &dnsmsg.BlockingModeREFUSED{}, nil
default:
// Consider unhandled type-switch cases programmer errors.
return nil, fmt.Errorf("bad pb safe browsing blocking mode %T(%[1]v)", pbm)
}
}
// customDomainsToInternal converts protobuf custom-domain configurations to
// internal ones.
func customDomainsToInternal(
@@ -479,11 +567,13 @@ func profileToProtobuf(p *agd.Profile) (pbProf *Profile) {
CustomDomains: customDomainsToProtobuf(p.CustomDomains),
FilterConfig: filterConfigToProtobuf(p.FilterConfig),
Access: accessToProtobuf(p.Access.Config()),
AdultBlockingMode: adultBlockingModeToProtobuf(p.AdultBlockingMode),
BlockingMode: blockingModeToProtobuf(p.BlockingMode),
SafeBrowsingBlockingMode: safeBrowsingBlockingModeToProtobuf(p.SafeBrowsingBlockingMode),
Ratelimiter: ratelimiterToProtobuf(p.Ratelimiter.Config()),
AccountId: string(p.AccountID),
ProfileId: string(p.ID),
DeviceIds: unsafelyConvertStrSlice[agd.DeviceID, string](
DeviceIds: agdprotobuf.UnsafelyConvertStrSlice[agd.DeviceID, string](
p.DeviceIDs.Values(),
),
FilteredResponseTtl: durationpb.New(p.FilteredResponseTTL),
@@ -563,7 +653,7 @@ func customDomainConfigsToProtobuf(
func filterConfigToProtobuf(c *filter.ConfigClient) (fc *FilterConfig) {
var rules []string
if c.Custom.Enabled {
rules = unsafelyConvertStrSlice[filter.RuleText, string](c.Custom.Filter.Rules())
rules = agdprotobuf.UnsafelyConvertStrSlice[filter.RuleText, string](c.Custom.Filter.Rules())
}
return &FilterConfig{
@@ -573,7 +663,7 @@ func filterConfigToProtobuf(c *filter.ConfigClient) (fc *FilterConfig) {
},
Parental: &FilterConfig_Parental{
PauseSchedule: scheduleToProtobuf(c.Parental.PauseSchedule),
BlockedServices: unsafelyConvertStrSlice[filter.BlockedServiceID, string](
BlockedServices: agdprotobuf.UnsafelyConvertStrSlice[filter.BlockedServiceID, string](
c.Parental.BlockedServices,
),
Enabled: c.Parental.Enabled,
@@ -582,7 +672,7 @@ func filterConfigToProtobuf(c *filter.ConfigClient) (fc *FilterConfig) {
SafeSearchYoutubeEnabled: c.Parental.SafeSearchYouTubeEnabled,
},
RuleList: &FilterConfig_RuleList{
Ids: unsafelyConvertStrSlice[filter.ID, string](c.RuleList.IDs),
Ids: agdprotobuf.UnsafelyConvertStrSlice[filter.ID, string](c.RuleList.IDs),
Enabled: c.RuleList.Enabled,
},
SafeBrowsing: &FilterConfig_SafeBrowsing{
@@ -669,6 +759,9 @@ func prefixesToProtobuf(nets []netip.Prefix) (cidrs []*CidrRange) {
}
// blockingModeToProtobuf converts a blocking-mode sum-type to a protobuf one.
//
// TODO(d.kolyshev): DRY with adultBlockingModeToProtobuf and
// safeBrowsingBlockingModeToProtobuf.
func blockingModeToProtobuf(m dnsmsg.BlockingMode) (pbBlockingMode isProfile_BlockingMode) {
switch m := m.(type) {
case *dnsmsg.BlockingModeCustomIP:
@@ -695,6 +788,70 @@ func blockingModeToProtobuf(m dnsmsg.BlockingMode) (pbBlockingMode isProfile_Blo
}
}
// adultBlockingModeToProtobuf converts a blocking-mode sum-type to a protobuf
// one.
func adultBlockingModeToProtobuf(
m dnsmsg.BlockingMode,
) (pbBlockingMode isProfile_AdultBlockingMode) {
switch m := m.(type) {
case nil:
return nil
case *dnsmsg.BlockingModeCustomIP:
return &Profile_AdultBlockingModeCustomIp{
AdultBlockingModeCustomIp: &BlockingModeCustomIP{
Ipv4: ipsToByteSlices(m.IPv4),
Ipv6: ipsToByteSlices(m.IPv6),
},
}
case *dnsmsg.BlockingModeNXDOMAIN:
return &Profile_AdultBlockingModeNxdomain{
AdultBlockingModeNxdomain: &BlockingModeNXDOMAIN{},
}
case *dnsmsg.BlockingModeNullIP:
return &Profile_AdultBlockingModeNullIp{
AdultBlockingModeNullIp: &BlockingModeNullIP{},
}
case *dnsmsg.BlockingModeREFUSED:
return &Profile_AdultBlockingModeRefused{
AdultBlockingModeRefused: &BlockingModeREFUSED{},
}
default:
panic(fmt.Errorf("bad adult blocking mode %T(%[1]v)", m))
}
}
// safeBrowsingBlockingModeToProtobuf converts a blocking-mode sum-type to a
// protobuf one.
func safeBrowsingBlockingModeToProtobuf(
m dnsmsg.BlockingMode,
) (pbBlockingMode isProfile_SafeBrowsingBlockingMode) {
switch m := m.(type) {
case nil:
return nil
case *dnsmsg.BlockingModeCustomIP:
return &Profile_SafeBrowsingBlockingModeCustomIp{
SafeBrowsingBlockingModeCustomIp: &BlockingModeCustomIP{
Ipv4: ipsToByteSlices(m.IPv4),
Ipv6: ipsToByteSlices(m.IPv6),
},
}
case *dnsmsg.BlockingModeNXDOMAIN:
return &Profile_SafeBrowsingBlockingModeNxdomain{
SafeBrowsingBlockingModeNxdomain: &BlockingModeNXDOMAIN{},
}
case *dnsmsg.BlockingModeNullIP:
return &Profile_SafeBrowsingBlockingModeNullIp{
SafeBrowsingBlockingModeNullIp: &BlockingModeNullIP{},
}
case *dnsmsg.BlockingModeREFUSED:
return &Profile_SafeBrowsingBlockingModeRefused{
SafeBrowsingBlockingModeRefused: &BlockingModeREFUSED{},
}
default:
panic(fmt.Errorf("bad safe browsing blocking mode %T(%[1]v)", m))
}
}
// ipsToByteSlices is a wrapper around netip.Addr.MarshalBinary that ignores the
// always-nil errors.
func ipsToByteSlices(ips []netip.Addr) (data [][]byte) {

View File

@@ -1,17 +0,0 @@
package filecachepb
import "unsafe"
// unsafelyConvertStrSlice checks if []T1 can be converted to []T2 at compile
// time and, if so, converts the slice using package unsafe.
//
// Slices resulting from this conversion must not be mutated.
func unsafelyConvertStrSlice[T1, T2 ~string](s []T1) (res []T2) {
if s == nil {
return nil
}
// #nosec G103 -- Conversion between two slices with the same underlying
// element type is safe.
return *(*[]T2)(unsafe.Pointer(&s))
}

View File

@@ -13,7 +13,9 @@ import (
// FileCacheVersion is the version of cached data structure. It must be
// manually incremented on every change in [agd.Device], [agd.Profile], and any
// file-cache structures.
const FileCacheVersion = 18
//
// Please document the changes to this constant in the changelog.
const FileCacheVersion = 19
// CacheVersionError is returned from [FileCacheStorage.Load] method if the
// stored cache version doesn't match current [FileCacheVersion].

View File

@@ -162,7 +162,9 @@ func NewProfile(tb testing.TB) (p *agd.Profile, d *agd.Device) {
BlocklistDomainRules: []string{"block.test"},
StandardEnabled: true,
}),
AdultBlockingMode: &dnsmsg.BlockingModeNullIP{},
BlockingMode: &dnsmsg.BlockingModeNullIP{},
SafeBrowsingBlockingMode: &dnsmsg.BlockingModeNullIP{},
Ratelimiter: agd.NewDefaultRatelimiter(&agd.RatelimitConfig{
ClientSubnets: []netip.Prefix{netip.MustParsePrefix("5.5.5.0/24")},
RPS: 100,

View File

@@ -84,7 +84,9 @@ func newDefaultProfileDB(tb testing.TB, devices <-chan []*agd.Device) (db *profi
return &profiledb.StorageProfilesResponse{
Profiles: []*agd.Profile{{
CustomDomains: &agd.AccountCustomDomains{},
AdultBlockingMode: &dnsmsg.BlockingModeNullIP{},
BlockingMode: &dnsmsg.BlockingModeNullIP{},
SafeBrowsingBlockingMode: &dnsmsg.BlockingModeNullIP{},
ID: profiledbtest.ProfileID,
DeviceIDs: devIDs,
}},

View File

@@ -70,11 +70,7 @@ func (db *testKV) get(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK)
// TODO(a.garipov): Consider making testutil.RequireTypeAssert accept
// testutil.PanicT.
require.IsType(pt, ([]byte)(nil), v)
val := v.([]byte)
val := testutil.RequireTypeAssert[[]byte](pt, v)
err := json.NewEncoder(rw).Encode([]*consulkv.KeyReadResponse{{
Value: val,
}})

View File

@@ -2,52 +2,88 @@ package tlsconfig
import (
"crypto/tls"
"fmt"
"net/netip"
"slices"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
)
// certPaths contains a certificate path and a key path.
type certPaths struct {
// certData is an internal representation of a certificate and its paths.
type certData struct {
cert *tls.Certificate
certPath string
keyPath string
// TODO(a.garipov, e.burkov): Think of a better approach to distinct
// TODO(a.garipov, e.burkov): Think of a better approach to distinguish
// between default and custom certificates.
isCustom bool
}
// certIndex holds TLS certificates and their associated file paths. Each entry
// in the slices corresponds to a certificate and its respective paths for the
// certificate and key files. Using this struct allows us to reduce
// allocations.
type certIndex struct {
// certs contains the list of TLS certificates. All elements must not be
// nil.
certs []*tls.Certificate
// paths contains corresponding file paths for certificate and key files.
// All elements must not be nil.
paths []*certPaths
// bindData is a helper type to map IP prefixes to certificate names.
//
// TODO(e.burkov): Implement prefix comparison and use binary search.
type bindData struct {
pref netip.Prefix
name agd.CertificateName
}
// add saves the TLS certificate and its paths. Certificate paths must only be
// added once, see [certStorage.contains]. cert and cp must not be nil.
func (s *certIndex) add(cert *tls.Certificate, cp *certPaths) {
s.certs = append(s.certs, cert)
s.paths = append(s.paths, cp)
// certIndex holds TLS certificates and their associated info.
type certIndex struct {
// certs maps certificate names to their information. Each entry
// corresponds to a certificate name and its respective paths for the
// certificate and key files.
certs *sortedMap[agd.CertificateName, *certData]
// bound are the IP prefixes for the certificates. It must only contain
// certificate names that are present in certs.
bound []*bindData
}
// newCertIndex returns a new properly initialized [certIndex].
func newCertIndex() (s *certIndex) {
return &certIndex{
certs: newSortedMap[agd.CertificateName, *certData](),
}
}
// add saves the TLS certificate's data under its name. name must be unique,
// see [certIndex.contains]. certData must not be nil.
func (s *certIndex) add(name agd.CertificateName, certData *certData) {
s.certs.set(name, certData)
}
// bind binds the certificate to the given prefix. It returns false if the
// binding already exists.
func (s *certIndex) bind(name agd.CertificateName, pref netip.Prefix) (added bool) {
if slices.ContainsFunc(s.bound, func(b *bindData) (found bool) {
return b.name == name && b.pref == pref
}) {
return false
}
s.bound = append(s.bound, &bindData{
pref: pref,
name: name,
})
return true
}
// contains returns true if the TLS certificate has already been added using the
// provided file paths. cp must not be nil.
func (s *certIndex) contains(cp *certPaths) (ok bool) {
return slices.ContainsFunc(s.paths, func(p *certPaths) (found bool) {
return *cp == *p
})
// provided name.
func (s *certIndex) contains(name agd.CertificateName) (ok bool) {
_, ok = s.certs.get(name)
return ok
}
// count returns the number of saved TLS certificates.
func (s *certIndex) count() (n int) {
return len(s.certs)
return s.certs.len()
}
// certFor returns the TLS certificate for chi. chi must not be nil. cert must
@@ -60,60 +96,75 @@ func (s *certIndex) count() (n int) {
// TODO(a.garipov): Explore the above situation and consider fixes to allow
// custom IP-only certs.
func (s *certIndex) certFor(chi *tls.ClientHelloInfo) (cert *tls.Certificate, err error) {
laddr := chi.Conn.LocalAddr()
ip := netutil.NetAddrToAddrPort(laddr).Addr()
if ip == (netip.Addr{}) {
return nil, errors.Error("no local address")
}
// TODO(e.burkov): Reuse the slice to decrease allocations.
var errs []error
for _, c := range s.certs {
err = chi.SupportsCertificate(c)
for _, b := range s.bound {
if !b.pref.Contains(ip) {
continue
}
certData, ok := s.certs.get(b.name)
if !ok {
panic(fmt.Errorf("certificate %q: %w", b.name, errors.ErrNoValue))
}
cert = certData.cert
err = chi.SupportsCertificate(cert)
if err == nil {
return c, nil
return cert, nil
}
errs = append(errs, err)
}
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
return nil, fmt.Errorf("no certificate found for %s", ip)
}
// rangeFn calls fn for each stored TLS certificate and its paths. fn must not
// be nil. Neither cert nor cp must be modified.
func (s *certIndex) rangeFn(fn func(cert *tls.Certificate, cp *certPaths) (cont bool)) {
for i, p := range s.paths {
if !fn(s.certs[i], p) {
// rangeFn calls fn for each stored TLS certificate and its data. fn must not
// be nil and must not modify certData.
func (s *certIndex) rangeFn(fn func(name agd.CertificateName, certData *certData) (cont bool)) {
for name, cd := range s.certs.rangeFn {
if !fn(name, cd) {
return
}
}
}
// remove deletes the certificate from s. cp must not be nil.
func (s *certIndex) remove(cp *certPaths) {
i := slices.IndexFunc(s.paths, func(p *certPaths) (found bool) {
return *cp == *p
// remove deletes the certificate from s by name. name must be valid.
func (s *certIndex) remove(name agd.CertificateName) {
s.certs.del(name)
s.bound = slices.DeleteFunc(s.bound, func(b *bindData) (found bool) {
return b.name == name
})
if i == -1 {
return
}
s.certs = slices.Delete(s.certs, i, i+1)
s.paths = slices.Delete(s.paths, i, i+1)
}
// stored returns the list of saved TLS certificates.
// stored returns the saved TLS certificates. certs' values must not be
// modified.
func (s *certIndex) stored() (certs []*tls.Certificate) {
return s.certs
}
// update updates the certificate corresponding to the paths. cp and c must not
// be nil.
//
// TODO(a.garipov): Think of a better way to do this that doesn't involve code
// that looks like iterator invalidation.
func (s *certIndex) update(cp *certPaths, c *tls.Certificate) (ok bool) {
for i, p := range s.paths {
if *cp == *p {
s.certs[i] = c
return true
}
for _, cd := range s.certs.rangeFn {
certs = append(certs, cd.cert)
}
return false
return certs
}
// update updates the certificate corresponding to name. It returns true if the
// certificate was updated. name must be valid, c must not be nil.
func (s *certIndex) update(name agd.CertificateName, c *tls.Certificate) (ok bool) {
certData, ok := s.certs.get(name)
if ok {
certData.cert = c
}
return ok
}

View File

@@ -3,90 +3,186 @@ package tlsconfig
import (
"crypto/tls"
"crypto/x509"
"net/netip"
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Common domain names used for testing.
const (
testDomainName = "a.test"
testDomainNameAlt = "b.test"
testDomainNameUnknown = "unknown.test"
)
// Common [agd.CertificateName]s used for testing.
const (
testCertName agd.CertificateName = "cert-a"
testCertNameAlt agd.CertificateName = "cert-b"
testCertNameUnknown agd.CertificateName = "cert-unknown"
)
// Common [tls.Certificate]s used for testing.
var (
testCert = &tls.Certificate{
Leaf: &x509.Certificate{
DNSNames: []string{testDomainName},
Version: tls.VersionTLS13,
},
}
testCertAlt = &tls.Certificate{
Leaf: &x509.Certificate{
DNSNames: []string{testDomainNameAlt},
Version: tls.VersionTLS13,
},
}
)
func TestCertIndex(t *testing.T) {
const (
domainA = "a.com"
domainB = "b.org"
)
var (
pathsDomainA = &certPaths{
certPath: domainA + "_path",
keyPath: domainA + "_path",
}
pathsDomainB = &certPaths{
certPath: domainB + "_path",
keyPath: domainB + "_path",
}
nonAddedPaths = &certPaths{
certPath: "non_added_path",
keyPath: "non_added_path",
}
)
certDomainA := &tls.Certificate{Leaf: &x509.Certificate{
DNSNames: []string{domainA},
Version: tls.VersionTLS13,
}}
certDomainB := &tls.Certificate{Leaf: &x509.Certificate{
DNSNames: []string{domainB},
Version: tls.VersionTLS13,
}}
certWithPaths := []struct {
cert *tls.Certificate
paths *certPaths
}{{
cert: certDomainA,
paths: pathsDomainA,
}, {
cert: certDomainB,
paths: pathsDomainB,
}}
idx := &certIndex{}
for _, cp := range certWithPaths {
idx.add(cp.cert, cp.paths)
certs := map[agd.CertificateName]*certData{
testCertName: {
cert: testCert,
certPath: testDomainName + "_path",
keyPath: testDomainName + "_path",
},
testCertNameAlt: {
cert: testCertAlt,
certPath: testDomainNameAlt + "_path",
keyPath: testDomainNameAlt + "_path",
},
}
assert.True(t, idx.contains(pathsDomainA))
idx := newCertIndex()
for name, cd := range certs {
idx.add(name, cd)
}
copyPathsDomainsB := *pathsDomainB
assert.True(t, idx.contains(&copyPathsDomainsB))
assert.False(t, idx.contains(nonAddedPaths))
assert.Equal(t, len(certWithPaths), idx.count())
got, err := idx.certFor(&tls.ClientHelloInfo{
ServerName: domainA,
SupportedVersions: []uint16{tls.VersionTLS13},
t.Run("contains", func(t *testing.T) {
assert.True(t, idx.contains(testCertName))
assert.True(t, idx.contains(testCertNameAlt))
assert.False(t, idx.contains(testCertNameUnknown))
})
require.NoError(t, err)
assert.Equal(t, certDomainA, got)
got, err = idx.certFor(&tls.ClientHelloInfo{
ServerName: domainB,
SupportedVersions: []uint16{tls.VersionTLS13},
t.Run("count", func(t *testing.T) {
assert.Equal(t, len(certs), idx.count())
})
require.NoError(t, err)
assert.Equal(t, certDomainB, got)
assert.Equal(t, []*tls.Certificate{certDomainA, certDomainB}, idx.stored())
t.Run("stored", func(t *testing.T) {
want := []*tls.Certificate{testCert, testCertAlt}
i := 0
idx.rangeFn(func(c *tls.Certificate, cp *certPaths) (cont bool) {
assert.Equal(t, certWithPaths[i].cert, c)
assert.Equal(t, certWithPaths[i].paths, cp)
assert.ElementsMatch(t, want, idx.stored())
})
i++
t.Run("rangeFn", func(t *testing.T) {
n := 0
idx.rangeFn(func(name agd.CertificateName, cd *certData) (cont bool) {
require.Contains(t, certs, name)
assert.Equal(t, cd, certs[name])
n++
return true
})
assert.Equal(t, len(certs), n)
})
}
func TestCertIndex_CertFor(t *testing.T) {
var (
addr = netip.MustParseAddr("192.0.2.1")
addrAlt = netip.MustParseAddr("192.0.2.2")
addrUnknown = netip.MustParseAddr("192.0.2.3")
)
certs := map[agd.CertificateName]struct {
data *certData
pref netip.Prefix
}{
testCertName: {
data: &certData{
cert: testCert,
certPath: testDomainName + "_path",
keyPath: testDomainName + "_path",
},
pref: netip.PrefixFrom(addr, 32),
},
testCertNameAlt: {
data: &certData{
cert: testCertAlt,
certPath: testDomainNameAlt + "_path",
keyPath: testDomainNameAlt + "_path",
},
pref: netip.PrefixFrom(addrAlt, 32),
},
}
idx := newCertIndex()
for name, cd := range certs {
idx.add(name, cd.data)
added := idx.bind(name, cd.pref)
require.True(t, added)
}
testCases := []struct {
chi *tls.ClientHelloInfo
wantCert *tls.Certificate
wantErrMsg string
name string
}{{
chi: &tls.ClientHelloInfo{
ServerName: testDomainName,
SupportedVersions: []uint16{tls.VersionTLS13},
Conn: NewLocalAddrConn(addr),
},
wantCert: testCert,
wantErrMsg: "",
name: "success",
}, {
chi: &tls.ClientHelloInfo{
ServerName: testDomainNameAlt,
SupportedVersions: []uint16{tls.VersionTLS13},
Conn: NewLocalAddrConn(addrAlt),
},
wantCert: testCertAlt,
wantErrMsg: "",
name: "success_alternative",
}, {
chi: &tls.ClientHelloInfo{
ServerName: testDomainNameUnknown,
SupportedVersions: []uint16{tls.VersionTLS13},
Conn: NewLocalAddrConn(addrUnknown),
},
wantCert: nil,
wantErrMsg: "no certificate found for " + addrUnknown.String(),
name: "fail_unknown",
}, {
chi: &tls.ClientHelloInfo{
ServerName: testDomainNameUnknown,
SupportedVersions: []uint16{tls.VersionTLS12},
Conn: NewLocalAddrConn(addr),
},
wantCert: nil,
wantErrMsg: "certificate is not valid for requested server name: " +
"x509: certificate is valid for " + testDomainName +
", not " + testDomainNameUnknown,
name: "fail_server_name",
}}
t.Run("certFor", func(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := idx.certFor(tc.chi)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
assert.Equal(t, tc.wantCert, got)
})
}
})
}

View File

@@ -10,6 +10,7 @@ import (
"log/slog"
"maps"
"net/http"
"net/netip"
"os"
"path/filepath"
"slices"
@@ -50,6 +51,8 @@ type CustomDomainDB struct {
cacheDir string
bindPrefixes []netip.Prefix
initRetryIvl time.Duration
maxRetryIvl time.Duration
}
@@ -83,6 +86,10 @@ type CustomDomainDBConfig struct {
// exist, it is created.
CacheDirPath string
// BindPrefixes are the IP prefixes to bound to the custom-domain
// certificates. All items must be valid.
BindPrefixes []netip.Prefix
// InitialRetryIvl is the initial interval for retrying a failed cert after
// a network or a ratelimiting error. It must be positive.
InitialRetryIvl time.Duration
@@ -115,6 +122,8 @@ func NewCustomDomainDB(c *CustomDomainDBConfig) (db *CustomDomainDB, err error)
metrics: c.Metrics,
strg: c.Storage,
bindPrefixes: c.BindPrefixes,
cacheDir: c.CacheDirPath,
initRetryIvl: c.InitialRetryIvl,
@@ -190,7 +199,7 @@ func (db *CustomDomainDB) removeCertData(
var errs []error
certPath, keyPath := db.cachePaths(certName)
err := db.manager.Remove(ctx, certPath, keyPath, true)
err := db.manager.Remove(ctx, certName)
if err != nil {
errs = append(errs, fmt.Errorf("removing from manager: %w", err))
}
@@ -228,9 +237,9 @@ const (
)
// cachePaths returns the cache paths for the given certificate name.
func (db *CustomDomainDB) cachePaths(certName agd.CertificateName) (certPath, keyPath string) {
certPath = filepath.Join(db.cacheDir, string(certName)+".crt.pem")
keyPath = filepath.Join(db.cacheDir, string(certName)+".key.pem")
func (db *CustomDomainDB) cachePaths(name agd.CertificateName) (certPath, keyPath string) {
certPath = filepath.Join(db.cacheDir, string(name+".crt.pem"))
keyPath = filepath.Join(db.cacheDir, string(name+".key.pem"))
return certPath, keyPath
}
@@ -414,11 +423,22 @@ func (db *CustomDomainDB) refreshCert(
// Add to the manager in case this is an initial refresh.
//
// TODO(a.garipov): Consider splitting away the initial refresh logic.
err = db.manager.Add(ctx, certPath, keyPath, true)
err = db.manager.Add(ctx, &AddParams{
Name: certName,
CertPath: certPath,
KeyPath: keyPath,
IsCustom: true,
})
if err != nil {
return false, fmt.Errorf("adding previous cert to manager: %w", err)
}
err = db.bind(ctx, certName)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return false, err
}
return false, nil
}
@@ -434,7 +454,7 @@ func (db *CustomDomainDB) refreshCert(
l.InfoContext(ctx, "got data", "cert_len", len(certData), "key_len", len(keyData))
err = db.saveCertData(ctx, l, certPath, certData, keyPath, keyData)
err = db.saveCertData(ctx, l, certName, certPath, certData, keyPath, keyData)
if err != nil {
return false, fmt.Errorf("saving cert %q: %w", certName, err)
}
@@ -450,6 +470,7 @@ func (db *CustomDomainDB) refreshCert(
func (db *CustomDomainDB) saveCertData(
ctx context.Context,
l *slog.Logger,
certName agd.CertificateName,
certPath string,
certData []byte,
keyPath string,
@@ -479,11 +500,22 @@ func (db *CustomDomainDB) saveCertData(
l.DebugContext(ctx, "saved key file", "path", keyPath)
err = db.manager.Add(ctx, certPath, keyPath, true)
err = db.manager.Add(ctx, &AddParams{
Name: certName,
CertPath: certPath,
KeyPath: keyPath,
IsCustom: true,
})
if err != nil {
return fmt.Errorf("adding to manager: %w", err)
}
err = db.bind(ctx, certName)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
return nil
}
@@ -627,3 +659,21 @@ func (db *CustomDomainDB) performRetries(
errcoll.Collect(ctx, db.errColl, db.logger, "retrying certs", err)
}
}
// bind binds the certificate with certName name to the configured prefixes.
func (db *CustomDomainDB) bind(ctx context.Context, certName agd.CertificateName) (err error) {
var errs []error
for _, pref := range db.bindPrefixes {
err = db.manager.Bind(ctx, certName, pref)
if err != nil {
errs = append(errs, fmt.Errorf("binding cert %q: %w", certName, err))
}
}
err = errors.Join(errs...)
if err != nil {
return fmt.Errorf("binding cert %q: %w", certName, err)
}
return nil
}

View File

@@ -8,6 +8,7 @@ import (
"encoding/pem"
"net/http"
"net/http/httptest"
"net/netip"
"os"
"path/filepath"
"testing"
@@ -26,9 +27,6 @@ import (
"github.com/stretchr/testify/require"
)
// testCertName is the common certificate name for tests.
const testCertName agd.CertificateName = "cert1234"
// testProfileID is the common profile ID for tests.
const testProfileID agd.ProfileID = "prof1234"
@@ -53,6 +51,12 @@ var testDomains = []string{
testWildcard,
}
// testBindPrefixes are the common bind prefixes for tests.
var testBindPrefixes = []netip.Prefix{
netip.MustParsePrefix("127.0.0.1/32"),
netip.MustParsePrefix("::1/128"),
}
// Time values for tests.
var (
testTimeExpired = testTimeNow.Add(-1 * timeutil.Day)
@@ -65,28 +69,28 @@ var (
testStateDisabled = &agd.CustomDomainStateCurrent{
NotBefore: testTimeNow.Add(-1 * timeutil.Day),
NotAfter: testTimeNow.Add(1 * timeutil.Day),
CertName: testCertName,
CertName: testCustomCertName,
Enabled: false,
}
testStateExpired = &agd.CustomDomainStateCurrent{
NotBefore: testTimeExpired.Add(-1 * timeutil.Day),
NotAfter: testTimeExpired,
CertName: testCertName,
CertName: testCustomCertName,
Enabled: true,
}
testStateFuture = &agd.CustomDomainStateCurrent{
NotBefore: testTimeFuture.Add(-1 * timeutil.Day),
NotAfter: testTimeFuture,
CertName: testCertName,
CertName: testCustomCertName,
Enabled: true,
}
testStateOK = &agd.CustomDomainStateCurrent{
NotBefore: testTimeNow.Add(-1 * timeutil.Day),
NotAfter: testTimeNow.Add(1 * timeutil.Day),
CertName: testCertName,
CertName: testCustomCertName,
Enabled: true,
}
)
@@ -296,6 +300,7 @@ func TestCustomDomainDB_AddCertificate(t *testing.T) {
db := newCustomDomainDB(t, &tlsconfig.CustomDomainDBConfig{
Clock: clock,
BindPrefixes: testBindPrefixes,
})
ctx := testutil.ContextWithTimeout(t, testTimeout)
@@ -488,18 +493,32 @@ func (s *testCustomDomainStorage) CertificateData(
// testManager is the [tlsconfig.Manager] for tests.
type testManager struct {
onAdd func(ctx context.Context, certPath, keyPath string, isCustom bool) (err error)
onAdd func(ctx context.Context, params *tlsconfig.AddParams) (err error)
onBind func(
ctx context.Context,
name agd.CertificateName,
pref netip.Prefix,
) (err error)
onClone func() (c *tls.Config)
onCloneWithMetrics func(proto, srvName string, deviceDomains []string) (c *tls.Config)
onRemove func(ctx context.Context, certPath, keyPath string, isCustom bool) (err error)
onRemove func(ctx context.Context, name agd.CertificateName) (err error)
}
// type check
var _ tlsconfig.Manager = (*testManager)(nil)
// Add implements the [tlsconfig.Manager] interface for *testManager.
func (m *testManager) Add(ctx context.Context, certPath, keyPath string, isCustom bool) (err error) {
return m.onAdd(ctx, certPath, keyPath, isCustom)
func (m *testManager) Add(ctx context.Context, params *tlsconfig.AddParams) (err error) {
return m.onAdd(ctx, params)
}
// Bind implements the [tlsconfig.Manager] interface for *testManager.
func (m *testManager) Bind(
ctx context.Context,
name agd.CertificateName,
pref netip.Prefix,
) (err error) {
return m.onBind(ctx, name, pref)
}
// Clone implements the [tlsconfig.Manager] interface for *testManager.
@@ -518,15 +537,18 @@ func (m *testManager) CloneWithMetrics(
}
// Remove implements the [tlsconfig.Manager] interface for *testManager.
func (m *testManager) Remove(ctx context.Context, certPath, keyPath string, isCustom bool) (err error) {
return m.onRemove(ctx, certPath, keyPath, isCustom)
func (m *testManager) Remove(ctx context.Context, name agd.CertificateName) (err error) {
return m.onRemove(ctx, name)
}
// newTestManager returns a new *testManager all methods of which panic.
func newTestManager() (m *testManager) {
return &testManager{
onAdd: func(ctx context.Context, certPath, keyPath string, isCustom bool) (err error) {
panic(testutil.UnexpectedCall(ctx, certPath, keyPath, isCustom))
onAdd: func(ctx context.Context, params *tlsconfig.AddParams) (err error) {
panic(testutil.UnexpectedCall(ctx, params))
},
onBind: func(ctx context.Context, name agd.CertificateName, pref netip.Prefix) (err error) {
panic(testutil.UnexpectedCall(ctx, name, pref))
},
onClone: func() (c *tls.Config) {
panic(testutil.UnexpectedCall())
@@ -534,8 +556,8 @@ func newTestManager() (m *testManager) {
onCloneWithMetrics: func(proto, srvName string, deviceDomains []string) (c *tls.Config) {
panic(testutil.UnexpectedCall(proto, srvName, deviceDomains))
},
onRemove: func(ctx context.Context, certPath, keyPath string, isCustom bool) (err error) {
panic(testutil.UnexpectedCall(ctx, certPath, keyPath, isCustom))
onRemove: func(ctx context.Context, name agd.CertificateName) (err error) {
panic(testutil.UnexpectedCall(ctx, name))
},
}
}
@@ -580,7 +602,7 @@ func TestCustomDomainDB_Refresh(t *testing.T) {
ctx context.Context,
certName agd.CertificateName,
) (cert, key []byte, err error) {
assert.Equal(t, testCertName, certName)
assert.Equal(t, testCustomCertName, certName)
certDER, rsaKey := newCertAndKey(t, 1)
@@ -592,19 +614,23 @@ func TestCustomDomainDB_Refresh(t *testing.T) {
wantCertPath, wantKeyPath := newCertAndKeyPaths(cacheDir)
mgrWithAdd := newTestManager()
mgrWithAdd.onAdd = func(ctx context.Context, certPath, keyPath string, isCustom bool) (err error) {
assert.Equal(t, wantCertPath, certPath)
assert.Equal(t, wantKeyPath, keyPath)
assert.True(t, isCustom)
mgrWithAdd.onAdd = func(ctx context.Context, params *tlsconfig.AddParams) (err error) {
assert.Equal(t, wantCertPath, params.CertPath)
assert.Equal(t, wantKeyPath, params.KeyPath)
assert.True(t, params.IsCustom)
return nil
}
mgrWithAdd.onBind = func(ctx context.Context, n agd.CertificateName, p netip.Prefix) (err error) {
assert.Equal(t, testCustomCertName, n)
assert.Contains(t, testBindPrefixes, p)
return nil
}
mgrWithRemove := newTestManager()
mgrWithRemove.onRemove = func(ctx context.Context, certPath, keyPath string, isCustom bool) (err error) {
assert.Equal(t, wantCertPath, certPath)
assert.Equal(t, wantKeyPath, keyPath)
assert.True(t, isCustom)
mgrWithRemove.onRemove = func(ctx context.Context, name agd.CertificateName) (err error) {
assert.Equal(t, testCustomCertName, name)
return nil
}
@@ -614,6 +640,7 @@ func TestCustomDomainDB_Refresh(t *testing.T) {
Manager: mgrWithAdd,
Storage: strg,
CacheDirPath: cacheDir,
BindPrefixes: testBindPrefixes,
})
ctx := testutil.ContextWithTimeout(t, testTimeout)
@@ -632,6 +659,7 @@ func TestCustomDomainDB_Refresh(t *testing.T) {
Manager: mgrWithAdd,
Storage: strg,
CacheDirPath: cacheDir,
BindPrefixes: testBindPrefixes,
})
ctx := testutil.ContextWithTimeout(t, testTimeout)
@@ -650,6 +678,7 @@ func TestCustomDomainDB_Refresh(t *testing.T) {
Manager: mgrWithRemove,
Storage: strg,
CacheDirPath: cacheDir,
BindPrefixes: testBindPrefixes,
})
ctx := testutil.ContextWithTimeout(t, testTimeout)
@@ -668,8 +697,8 @@ func TestCustomDomainDB_Refresh(t *testing.T) {
// newCertAndKeyPaths is a helper that returns paths for the certificate and
// the key using the test's temporary directory.
func newCertAndKeyPaths(cacheDir string) (certPath, keyPath string) {
return filepath.Join(cacheDir, string(testCertName)+tlsconfig.CustomDomainCertExt),
filepath.Join(cacheDir, string(testCertName)+tlsconfig.CustomDomainKeyExt)
return filepath.Join(cacheDir, string(testCustomCertName)+tlsconfig.CustomDomainCertExt),
filepath.Join(cacheDir, string(testCustomCertName)+tlsconfig.CustomDomainKeyExt)
}
func TestCustomDomainDB_Refresh_retry(t *testing.T) {
@@ -679,17 +708,22 @@ func TestCustomDomainDB_Refresh_retry(t *testing.T) {
wantCertPath, wantKeyPath := newCertAndKeyPaths(cacheDir)
mgr := newTestManager()
mgr.onAdd = func(ctx context.Context, certPath, keyPath string, isCustom bool) (err error) {
assert.Equal(t, wantCertPath, certPath)
assert.Equal(t, wantKeyPath, keyPath)
assert.True(t, isCustom)
mgr.onAdd = func(ctx context.Context, params *tlsconfig.AddParams) (err error) {
assert.Equal(t, testCustomCertName, params.Name)
assert.Equal(t, wantCertPath, params.CertPath)
assert.Equal(t, wantKeyPath, params.KeyPath)
assert.True(t, params.IsCustom)
return nil
}
mgr.onRemove = func(ctx context.Context, certPath, keyPath string, isCustom bool) (err error) {
assert.Equal(t, wantCertPath, certPath)
assert.Equal(t, wantKeyPath, keyPath)
assert.True(t, isCustom)
mgr.onRemove = func(ctx context.Context, name agd.CertificateName) (err error) {
assert.Equal(t, testCustomCertName, name)
return nil
}
mgr.onBind = func(ctx context.Context, name agd.CertificateName, p netip.Prefix) (err error) {
assert.Equal(t, testCustomCertName, name)
assert.Contains(t, testBindPrefixes, p)
return nil
}
@@ -709,7 +743,7 @@ func TestCustomDomainDB_Refresh_retry(t *testing.T) {
return nil, nil, strgErr
}
assert.Equal(t, testCertName, certName)
assert.Equal(t, testCustomCertName, certName)
certDER, rsaKey := newCertAndKey(t, 1)
@@ -728,6 +762,7 @@ func TestCustomDomainDB_Refresh_retry(t *testing.T) {
ErrColl: &agdtest.ErrorCollector{
OnCollect: func(_ context.Context, err error) {},
},
BindPrefixes: testBindPrefixes,
})
require.True(t, t.Run("rate_limited", func(t *testing.T) {
@@ -801,12 +836,13 @@ func TestCustomDomainDB_Refresh_present(t *testing.T) {
ctx context.Context,
certName agd.CertificateName,
) (cert, key []byte, err error) {
assert.Equal(t, testCertName, certName)
assert.Equal(t, testCustomCertName, certName)
return certDER, x509.MarshalPKCS1PrivateKey(rsaKey), nil
},
},
CacheDirPath: cacheDir,
BindPrefixes: testBindPrefixes,
}
require.True(t, t.Run("both_present", func(t *testing.T) {

View File

@@ -0,0 +1,369 @@
package tlsconfig
import (
"context"
"crypto/tls"
"fmt"
"io"
"log/slog"
"net/netip"
"os"
"path/filepath"
"sync"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/service"
)
// DefaultManagerConfig is the configuration structure for [DefaultManager].
type DefaultManagerConfig struct {
// Logger is used for logging the operation of the TLS manager. It must not
// be nil.
Logger *slog.Logger
// ErrColl is used to collect TLS-related errors. It must not be nil.
ErrColl errcoll.Interface
// Metrics is used to collect TLS-related statistics. It must not be nil.
//
// TODO(a.garipov): See if the custom-domain certificates need any metrics.
Metrics ManagerMetrics
// TicketDB stores paths to the TLS session tickets and updates them. It
// must not be nil.
TicketDB TicketDB
// KeyLogPath, if not empty, is the path to the TLS key log file. If not
// empty, KeyLogPath should be a valid file path.
KeyLogPath string
}
// DefaultManager is the default implementation of [Manager].
type DefaultManager struct {
// mu protects fields certStorage, clones, clonesWithMetrics,
// sessTicketPaths.
mu *sync.Mutex
logger *slog.Logger
errColl errcoll.Interface
metrics ManagerMetrics
tickDB TicketDB
certStorage *certIndex
original *tls.Config
clones []*tls.Config
clonesWithMetrics []*tls.Config
}
// NewDefaultManager returns a new initialized *DefaultManager. c must not be
// nil and must be valid.
func NewDefaultManager(c *DefaultManagerConfig) (m *DefaultManager, err error) {
var keyLogWriter io.Writer
if keyLogFilePath := c.KeyLogPath; keyLogFilePath != "" {
keyLogWriter, err = tlsKeyLogWriter(keyLogFilePath)
if err != nil {
return nil, fmt.Errorf("initializing tls key log writer: %w", err)
}
}
m = &DefaultManager{
mu: &sync.Mutex{},
logger: c.Logger,
errColl: c.ErrColl,
metrics: c.Metrics,
tickDB: c.TicketDB,
certStorage: newCertIndex(),
}
m.original = &tls.Config{
GetCertificate: m.getCertificate,
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS13,
KeyLogWriter: keyLogWriter,
}
return m, nil
}
// type check
var _ Manager = (*DefaultManager)(nil)
// Add implements the [Manager] interface for *DefaultManager.
func (m *DefaultManager) Add(ctx context.Context, params *AddParams) (err error) {
l := m.logger.With(
"cert", params.CertPath,
"key", params.KeyPath,
"is_custom", params.IsCustom,
)
m.mu.Lock()
defer m.mu.Unlock()
if m.certStorage.contains(params.Name) {
l.InfoContext(ctx, "skipping already added certificate")
return nil
}
cert, err := m.load(ctx, params)
if err != nil {
return fmt.Errorf("adding certificate: %w", err)
}
m.certStorage.add(params.Name, &certData{
cert: cert,
certPath: params.CertPath,
keyPath: params.KeyPath,
isCustom: params.IsCustom,
})
l.InfoContext(ctx, "added certificate")
return nil
}
// Bind implements the [Manager] interface for *DefaultManager.
func (m *DefaultManager) Bind(
ctx context.Context,
name agd.CertificateName,
pref netip.Prefix,
) (err error) {
m.mu.Lock()
defer m.mu.Unlock()
added := m.certStorage.bind(name, pref)
if !added {
m.logger.InfoContext(ctx, "skipping existing binding", "cert", name, "pref", pref)
}
return nil
}
// load returns a new TLS configuration from the provided certificate and key
// paths. m.mu must be locked. c must not be modified.
func (m *DefaultManager) load(ctx context.Context, p *AddParams) (c *tls.Certificate, err error) {
cert, err := tls.LoadX509KeyPair(p.CertPath, p.KeyPath)
if err != nil {
return nil, fmt.Errorf("loading certificate: %w", err)
}
if !p.IsCustom {
authAlgo := cert.Leaf.PublicKeyAlgorithm.String()
subj := cert.Leaf.Subject.String()
m.metrics.SetCertificateInfo(ctx, authAlgo, subj, cert.Leaf.NotAfter)
}
return &cert, nil
}
// Clone implements the [Manager] interface for *DefaultManager.
func (m *DefaultManager) Clone() (clone *tls.Config) {
m.mu.Lock()
defer m.mu.Unlock()
clone = m.original.Clone()
m.clones = append(m.clones, clone)
return clone
}
// getCertificate returns the TLS certificate for chi. See
// [tls.Config.GetCertificate]. c must not be modified.
func (m *DefaultManager) getCertificate(chi *tls.ClientHelloInfo) (c *tls.Certificate, err error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.certStorage.count() == 0 {
return nil, errors.Error("no certificates")
}
return m.certStorage.certFor(chi)
}
// CloneWithMetrics implements the [Manager] interface for *DefaultManager.
func (m *DefaultManager) CloneWithMetrics(
proto string,
srvName string,
deviceDomains []string,
) (conf *tls.Config) {
m.mu.Lock()
defer m.mu.Unlock()
clone := m.original.Clone()
clone.GetConfigForClient = m.metrics.BeforeHandshake(proto)
clone.GetCertificate = m.getCertificate
clone.VerifyConnection = m.metrics.AfterHandshake(
proto,
srvName,
deviceDomains,
m.certStorage.stored(),
)
m.clonesWithMetrics = append(m.clonesWithMetrics, clone)
return clone
}
// type check
var _ service.Refresher = (*DefaultManager)(nil)
// Refresh implements the [service.Refresher] interface for *DefaultManager.
func (m *DefaultManager) Refresh(ctx context.Context) (err error) {
m.logger.DebugContext(ctx, "refresh started")
defer m.logger.DebugContext(ctx, "refresh finished")
defer func() {
if err != nil {
errcoll.Collect(ctx, m.errColl, m.logger, "certificate refresh failed", err)
}
}()
m.mu.Lock()
defer m.mu.Unlock()
var errs []error
m.certStorage.rangeFn(func(name agd.CertificateName, cd *certData) (cont bool) {
cert, loadErr := m.load(ctx, &AddParams{
Name: name,
CertPath: cd.certPath,
KeyPath: cd.keyPath,
IsCustom: cd.isCustom,
})
if loadErr != nil {
errs = append(errs, loadErr)
return true
}
msg, lvl := "refreshed certificate", slog.LevelInfo
if !m.certStorage.update(name, cert) {
msg, lvl = "certificate did not refresh", slog.LevelWarn
}
m.logger.Log(ctx, lvl, msg, "name", name, "cert", cd.certPath, "key", cd.keyPath)
return true
})
err = errors.Join(errs...)
if err != nil {
return fmt.Errorf("refreshing tls certificates: %w", err)
}
m.logger.InfoContext(ctx, "refresh successful", "num_configs", m.certStorage.count())
return nil
}
// Remove removes a certificate from the manager. certPath and keyPath must not
// be empty.
func (m *DefaultManager) Remove(
ctx context.Context,
name agd.CertificateName,
) (err error) {
m.mu.Lock()
defer m.mu.Unlock()
m.certStorage.remove(name)
m.logger.InfoContext(
ctx,
"removed certificate",
"name", name,
)
return nil
}
// RotateTickets refreshes and resets TLS session tickets. It may be used as a
// [service.RefresherFunc].
func (m *DefaultManager) RotateTickets(ctx context.Context) (err error) {
m.logger.DebugContext(ctx, "ticket rotation started")
defer m.logger.DebugContext(ctx, "ticket rotation finished")
paths, err := m.tickDB.Paths(ctx)
if err != nil {
errcoll.Collect(ctx, m.errColl, m.logger, "rotating tickets", err)
}
if len(paths) == 0 {
return nil
}
defer func() {
m.metrics.SetSessionTicketRotationStatus(ctx, err)
if err != nil {
errcoll.Collect(ctx, m.errColl, m.logger, "ticket rotation failed", err)
}
}()
tickets := make([]SessionTicket, 0, len(paths))
for _, filePath := range paths {
var ticket SessionTicket
ticket, err = readSessionTicketFile(filePath)
if err != nil {
return fmt.Errorf("reading session ticket: %w", err)
}
tickets = append(tickets, ticket)
}
m.mu.Lock()
defer m.mu.Unlock()
for _, conf := range m.clones {
conf.SetSessionTicketKeys(tickets)
}
for _, conf := range m.clonesWithMetrics {
conf.SetSessionTicketKeys(tickets)
}
m.logger.InfoContext(
ctx,
"ticket rotation successful",
"num_configs", m.certStorage.count(),
"num_tickets", len(tickets),
"num_clones", len(m.clones),
"num_clones_with_metrics", len(m.clonesWithMetrics),
)
return nil
}
// readSessionTicketFile reads a single TLS session ticket from a file.
func readSessionTicketFile(filePath string) (ticket SessionTicket, err error) {
// #nosec G304 -- Trust the file paths that are given to us in the
// configuration file.
b, err := os.ReadFile(filePath)
if err != nil {
return SessionTicket{}, fmt.Errorf("reading session ticket: %w", err)
}
ticket, err = NewSessionTicket(b)
if err != nil {
return SessionTicket{}, fmt.Errorf("session ticket in %q: %w", filePath, err)
}
return ticket, nil
}
// tlsKeyLogWriter returns a writer for logging TLS secrets to file at
// keyLogPath.
func tlsKeyLogWriter(keyLogPath string) (kl io.Writer, err error) {
path := filepath.Clean(keyLogPath)
// TODO(a.garipov): Consider closing the file when we add SIGHUP support.
kl, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
return kl, nil
}

View File

@@ -4,10 +4,12 @@ import (
"cmp"
"crypto/rand"
"crypto/tls"
"net/netip"
"os"
"path/filepath"
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/tlsconfig"
@@ -16,6 +18,12 @@ import (
"github.com/stretchr/testify/require"
)
// testCertName is the name of the certificate used for tests.
const testCertName agd.CertificateName = "test-cert"
// testCustomCertName is the name of the custom certificate used for tests.
const testCustomCertName agd.CertificateName = "test-custom-cert"
// writeSesionKey is a helper function that writes generated session key to
// specified path.
func writeSessionKey(tb testing.TB, sessKeyPath string) {
@@ -39,11 +47,12 @@ func writeSessionKey(tb testing.TB, sessKeyPath string) {
// assertCertSerialNumber is a helper function that checks serial number of the
// TLS certificate.
func assertCertSerialNumber(tb testing.TB, conf *tls.Config, wantSN int64) {
func assertCertSerialNumber(tb testing.TB, conf *tls.Config, wantSN int64, laddr netip.Addr) {
tb.Helper()
cert, err := conf.GetCertificate(&tls.ClientHelloInfo{
SupportedVersions: []uint16{tls.VersionTLS13},
Conn: tlsconfig.NewLocalAddrConn(laddr),
})
require.NoError(tb, err)
@@ -88,14 +97,25 @@ func TestDefaultManager_Refresh(t *testing.T) {
writeCertAndKey(t, certDER, certPath, key, keyPath)
ctx := testutil.ContextWithTimeout(t, testTimeout)
err := m.Add(ctx, certPath, keyPath, false)
err := m.Add(ctx, &tlsconfig.AddParams{
Name: testCertName,
CertPath: certPath,
KeyPath: keyPath,
IsCustom: false,
})
require.NoError(t, err)
ip := netip.MustParseAddr("192.0.2.1")
subnet := netip.PrefixFrom(ip, 16)
err = m.Bind(ctx, testCertName, subnet)
require.NoError(t, err)
conf := m.Clone()
confWithMetrics := m.CloneWithMetrics("", "", nil)
assertCertSerialNumber(t, conf, snBefore)
assertCertSerialNumber(t, confWithMetrics, snBefore)
assertCertSerialNumber(t, conf, snBefore, ip)
assertCertSerialNumber(t, confWithMetrics, snBefore, ip)
certDER, key = newCertAndKey(t, snAfter)
writeCertAndKey(t, certDER, certPath, key, keyPath)
@@ -103,8 +123,8 @@ func TestDefaultManager_Refresh(t *testing.T) {
err = m.Refresh(ctx)
require.NoError(t, err)
assertCertSerialNumber(t, conf, snAfter)
assertCertSerialNumber(t, confWithMetrics, snAfter)
assertCertSerialNumber(t, conf, snAfter, ip)
assertCertSerialNumber(t, confWithMetrics, snAfter, ip)
}
func TestDefaultManager_Remove(t *testing.T) {
@@ -121,11 +141,23 @@ func TestDefaultManager_Remove(t *testing.T) {
m := newManager(t, nil)
ctx := testutil.ContextWithTimeout(t, testTimeout)
err := m.Add(ctx, certPath, keyPath, false)
err := m.Add(ctx, &tlsconfig.AddParams{
Name: testCertName,
CertPath: certPath,
KeyPath: keyPath,
IsCustom: true,
})
require.NoError(t, err)
addr := netip.MustParseAddr("192.0.2.1")
subnet := netip.PrefixFrom(addr, 16)
err = m.Bind(ctx, testCertName, subnet)
require.NoError(t, err)
chi := &tls.ClientHelloInfo{
SupportedVersions: []uint16{tls.VersionTLS13},
Conn: tlsconfig.NewLocalAddrConn(addr),
}
c := m.Clone()
@@ -133,7 +165,7 @@ func TestDefaultManager_Remove(t *testing.T) {
assert.NoError(t, err)
ctx = testutil.ContextWithTimeout(t, testTimeout)
err = m.Remove(ctx, certPath, keyPath, false)
err = m.Remove(ctx, testCertName)
require.NoError(t, err)
c = m.Clone()
@@ -162,7 +194,12 @@ func TestDefaultManager_RotateTickets(t *testing.T) {
writeCertAndKey(t, certDER, certPath, key, keyPath)
ctx := testutil.ContextWithTimeout(t, testTimeout)
err := m.Add(ctx, certPath, keyPath, false)
err := m.Add(ctx, &tlsconfig.AddParams{
Name: testCertName,
CertPath: certPath,
KeyPath: keyPath,
IsCustom: false,
})
require.NoError(t, err)
err = m.RotateTickets(ctx)

View File

@@ -3,28 +3,40 @@ package tlsconfig
import (
"context"
"crypto/tls"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"sync"
"net/netip"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/service"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
)
// AddParams are the parameters for [Manager.Add].
type AddParams struct {
// Name is the name of the certificate to add. It must be valid.
Name agd.CertificateName
// CertPath is the path to the certificate. It must be a valid existing
// filesystem path.
CertPath string
// KeyPath is the path to the key. It must be a valid existing filesystem
// path.
KeyPath string
// IsCustom defines if the certificate belongs to a custom domain. If true,
// the certificate's data should not be reported in metrics.
IsCustom bool
}
// Manager stores and updates TLS configurations.
type Manager interface {
// Add saves an initialized TLS certificate using the provided paths to a
// certificate and a key. certPath and keyPath must not be empty.
//
// If isCustom is true, the certificate's data should not be reported in
// metrics.
// Add saves an initialized TLS certificate using the provided params.
//
// Add must ignore duplicates.
Add(ctx context.Context, certPath, keyPath string, isCustom bool) (err error)
Add(ctx context.Context, params *AddParams) (err error)
// Bind binds the certificate to the given prefix.
//
// Bind must ignore duplicating name-prefix combinations.
Bind(ctx context.Context, name agd.CertificateName, prefix netip.Prefix) (err error)
// Clone returns the TLS configuration that contains saved TLS certificates.
Clone() (c *tls.Config)
@@ -32,12 +44,8 @@ type Manager interface {
// CloneWithMetrics is like [Manager.Clone] but it also sets metrics.
CloneWithMetrics(proto, srvName string, deviceDomains []string) (c *tls.Config)
// Remove deletes a certificate using the provided paths to its certificate
// and key. certPath and keyPath must not be empty.
//
// If isCustom is true, the certificate's data should not be reported in
// metrics.
Remove(ctx context.Context, certPath, keyPath string, isCustom bool) (err error)
// Remove deletes a custom certificate by its name. name must be valid.
Remove(ctx context.Context, name agd.CertificateName) (err error)
}
// EmptyManager is the implementation of the [Manager] interface that does
@@ -48,7 +56,12 @@ type EmptyManager struct{}
var _ Manager = EmptyManager{}
// Add implements the [Manager] interface for EmptyManager.
func (EmptyManager) Add(_ context.Context, _, _ string, _ bool) (err error) { return nil }
func (EmptyManager) Add(_ context.Context, _ *AddParams) (err error) { return nil }
// Bind implements the [Manager] interface for EmptyManager.
func (EmptyManager) Bind(_ context.Context, _ agd.CertificateName, _ netip.Prefix) (err error) {
return nil
}
// Clone implements the [Manager] interface for EmptyManager.
func (EmptyManager) Clone() (c *tls.Config) { return nil }
@@ -57,357 +70,4 @@ func (EmptyManager) Clone() (c *tls.Config) { return nil }
func (EmptyManager) CloneWithMetrics(_, _ string, _ []string) (c *tls.Config) { return nil }
// Remove implements the [Manager] interface for EmptyManager.
func (EmptyManager) Remove(_ context.Context, _, _ string, _ bool) (err error) { return nil }
// DefaultManagerConfig is the configuration structure for [DefaultManager].
type DefaultManagerConfig struct {
// Logger is used for logging the operation of the TLS manager. It must not
// be nil.
Logger *slog.Logger
// ErrColl is used to collect TLS-related errors. It must not be nil.
ErrColl errcoll.Interface
// Metrics is used to collect TLS-related statistics. It must not be nil.
//
// TODO(a.garipov): See if the custom-domain certificates need any metrics.
Metrics ManagerMetrics
// TicketDB stores paths to the TLS session tickets and updates them. It
// must not be nil.
TicketDB TicketDB
// KeyLogFilename, if not empty, is the name of the TLS key log file. If
// not empty, KeyLogFilename must be a valid file path.
KeyLogFilename string
}
// DefaultManager is the default implementation of [Manager].
type DefaultManager struct {
// mu protects fields certStorage, clones, clonesWithMetrics,
// sessTicketPaths.
mu *sync.Mutex
logger *slog.Logger
errColl errcoll.Interface
metrics ManagerMetrics
tickDB TicketDB
idx *certIndex
original *tls.Config
clones []*tls.Config
clonesWithMetrics []*tls.Config
}
// NewDefaultManager returns a new initialized *DefaultManager. c must not be
// nil and must be valid.
func NewDefaultManager(c *DefaultManagerConfig) (m *DefaultManager, err error) {
var kl io.Writer
fn := c.KeyLogFilename
if fn != "" {
kl, err = tlsKeyLogWriter(fn)
if err != nil {
return nil, fmt.Errorf("initializing tls key log writer: %w", err)
}
}
m = &DefaultManager{
mu: &sync.Mutex{},
logger: c.Logger,
errColl: c.ErrColl,
metrics: c.Metrics,
tickDB: c.TicketDB,
idx: &certIndex{},
}
m.original = &tls.Config{
GetCertificate: m.getCertificate,
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS13,
KeyLogWriter: kl,
}
return m, nil
}
// type check
var _ Manager = (*DefaultManager)(nil)
// Add implements the [Manager] interface for *DefaultManager.
func (m *DefaultManager) Add(
ctx context.Context,
certPath string,
keyPath string,
isCustom bool,
) (err error) {
cp := &certPaths{
certPath: certPath,
keyPath: keyPath,
isCustom: isCustom,
}
m.mu.Lock()
defer m.mu.Unlock()
if m.idx.contains(cp) {
m.logger.InfoContext(
ctx,
"skipping already added certificate",
"cert", cp.certPath,
"key", cp.keyPath,
"is_custom", cp.isCustom,
)
return nil
}
cert, err := m.load(ctx, cp)
if err != nil {
return fmt.Errorf("adding certificate: %w", err)
}
m.idx.add(cert, cp)
m.logger.InfoContext(
ctx,
"added certificate",
"cert", cp.certPath,
"key", cp.keyPath,
"is_custom", cp.isCustom,
)
return nil
}
// load returns a new TLS configuration from the provided certificate and key
// paths. m.mu must be locked. c must not be modified.
func (m *DefaultManager) load(
ctx context.Context,
cp *certPaths,
) (c *tls.Certificate, err error) {
cert, err := tls.LoadX509KeyPair(cp.certPath, cp.keyPath)
if err != nil {
return nil, fmt.Errorf("loading certificate: %w", err)
}
if !cp.isCustom {
authAlgo := cert.Leaf.PublicKeyAlgorithm.String()
subj := cert.Leaf.Subject.String()
m.metrics.SetCertificateInfo(ctx, authAlgo, subj, cert.Leaf.NotAfter)
}
return &cert, nil
}
// Clone implements the [Manager] interface for *DefaultManager.
func (m *DefaultManager) Clone() (clone *tls.Config) {
m.mu.Lock()
defer m.mu.Unlock()
clone = m.original.Clone()
m.clones = append(m.clones, clone)
return clone
}
// getCertificate returns the TLS certificate for chi. See
// [tls.Config.GetCertificate]. c must not be modified.
func (m *DefaultManager) getCertificate(chi *tls.ClientHelloInfo) (c *tls.Certificate, err error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.idx.count() == 0 {
return nil, errors.Error("no certificates")
}
return m.idx.certFor(chi)
}
// CloneWithMetrics implements the [Manager] interface for *DefaultManager.
func (m *DefaultManager) CloneWithMetrics(
proto string,
srvName string,
deviceDomains []string,
) (conf *tls.Config) {
m.mu.Lock()
defer m.mu.Unlock()
clone := m.original.Clone()
clone.GetConfigForClient = m.metrics.BeforeHandshake(proto)
clone.GetCertificate = m.getCertificate
clone.VerifyConnection = m.metrics.AfterHandshake(
proto,
srvName,
deviceDomains,
m.idx.stored(),
)
m.clonesWithMetrics = append(m.clonesWithMetrics, clone)
return clone
}
// type check
var _ service.Refresher = (*DefaultManager)(nil)
// Refresh implements the [service.Refresher] interface for *DefaultManager.
func (m *DefaultManager) Refresh(ctx context.Context) (err error) {
m.logger.DebugContext(ctx, "refresh started")
defer m.logger.DebugContext(ctx, "refresh finished")
defer func() {
if err != nil {
errcoll.Collect(ctx, m.errColl, m.logger, "certificate refresh failed", err)
}
}()
m.mu.Lock()
defer m.mu.Unlock()
var errs []error
m.idx.rangeFn(func(_ *tls.Certificate, cp *certPaths) (cont bool) {
cert, loadErr := m.load(ctx, cp)
if err != nil {
errs = append(errs, loadErr)
return true
}
msg, lvl := "refreshed certificate", slog.LevelInfo
if !m.idx.update(cp, cert) {
msg, lvl = "certificate did not refresh", slog.LevelWarn
}
m.logger.Log(ctx, lvl, msg, "cert", cp.certPath, "key", cp.keyPath)
return true
})
err = errors.Join(errs...)
if err != nil {
return fmt.Errorf("refreshing tls certificates: %w", err)
}
m.logger.InfoContext(ctx, "refresh successful", "num_configs", m.idx.count())
return nil
}
// Remove removes a certificate from the manager. certPath and keyPath must not
// be empty.
func (m *DefaultManager) Remove(
ctx context.Context,
certPath string,
keyPath string,
isCustom bool,
) (err error) {
cp := &certPaths{
certPath: certPath,
keyPath: keyPath,
isCustom: isCustom,
}
m.mu.Lock()
defer m.mu.Unlock()
m.idx.remove(cp)
m.logger.InfoContext(
ctx,
"removed certificate",
"cert", cp.certPath,
"key", cp.keyPath,
"is_custom", cp.isCustom,
)
return nil
}
// RotateTickets refreshes and resets TLS session tickets. It may be used as a
// [service.RefresherFunc].
func (m *DefaultManager) RotateTickets(ctx context.Context) (err error) {
m.logger.DebugContext(ctx, "ticket rotation started")
defer m.logger.DebugContext(ctx, "ticket rotation finished")
paths, err := m.tickDB.Paths(ctx)
if err != nil {
errcoll.Collect(ctx, m.errColl, m.logger, "rotating tickets", err)
}
if len(paths) == 0 {
return nil
}
defer func() {
m.metrics.SetSessionTicketRotationStatus(ctx, err)
if err != nil {
errcoll.Collect(ctx, m.errColl, m.logger, "ticket rotation failed", err)
}
}()
tickets := make([]SessionTicket, 0, len(paths))
for _, filePath := range paths {
var ticket SessionTicket
ticket, err = readSessionTicketFile(filePath)
if err != nil {
return fmt.Errorf("reading session ticket: %w", err)
}
tickets = append(tickets, ticket)
}
m.mu.Lock()
defer m.mu.Unlock()
for _, conf := range m.clones {
conf.SetSessionTicketKeys(tickets)
}
for _, conf := range m.clonesWithMetrics {
conf.SetSessionTicketKeys(tickets)
}
m.logger.InfoContext(
ctx,
"ticket rotation successful",
"num_configs", m.idx.count(),
"num_tickets", len(tickets),
"num_clones", len(m.clones),
"num_clones_with_metrics", len(m.clonesWithMetrics),
)
return nil
}
// readSessionTicketFile reads a single TLS session ticket from a file.
func readSessionTicketFile(fn string) (ticket SessionTicket, err error) {
// #nosec G304 -- Trust the file paths that are given to us in the
// configuration file.
b, err := os.ReadFile(fn)
if err != nil {
return SessionTicket{}, fmt.Errorf("reading session ticket: %w", err)
}
ticket, err = NewSessionTicket(b)
if err != nil {
return SessionTicket{}, fmt.Errorf("session ticket in %q: %w", fn, err)
}
return ticket, nil
}
// tlsKeyLogWriter returns a writer for logging TLS secrets to keyLogFilename.
func tlsKeyLogWriter(keyLogFilename string) (kl io.Writer, err error) {
path := filepath.Clean(keyLogFilename)
// TODO(a.garipov): Consider closing the file when we add SIGHUP support.
kl, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
return kl, nil
}
func (EmptyManager) Remove(_ context.Context, _ agd.CertificateName) (err error) { return nil }

View File

@@ -0,0 +1,97 @@
package tlsconfig
import (
"cmp"
"slices"
)
// sortedMap is a map that keeps elements in order with internal sorting
// function. It must be initialized with [newSortedMap].
//
// TODO(e.burkov): Move to golibs.
type sortedMap[K comparable, V any] struct {
vals map[K]V
cmp func(a, b K) (res int)
keys []K
}
// newSortedMap initializes a new instance of sorted map.
func newSortedMap[K cmp.Ordered, V any]() (m *sortedMap[K, V]) {
return &sortedMap[K, V]{
vals: map[K]V{},
cmp: cmp.Compare[K],
}
}
// set adds val with key to the sorted map. It panics if the m is nil.
func (m *sortedMap[K, V]) set(key K, val V) {
m.vals[key] = val
i, has := slices.BinarySearchFunc(m.keys, key, m.cmp)
if has {
m.keys[i] = key
} else {
m.keys = slices.Insert(m.keys, i, key)
}
}
// get returns val by key from the sorted map.
func (m *sortedMap[K, V]) get(key K) (val V, ok bool) {
if m == nil {
var zero V
return zero, false
}
val, ok = m.vals[key]
return val, ok
}
// del removes the value by key from the sorted map.
func (m *sortedMap[K, V]) del(key K) {
if m == nil {
return
}
if _, has := m.vals[key]; !has {
return
}
delete(m.vals, key)
i, _ := slices.BinarySearchFunc(m.keys, key, m.cmp)
m.keys = slices.Delete(m.keys, i, i+1)
}
// clear removes all elements from the sorted map.
func (m *sortedMap[K, V]) clear() {
if m == nil {
return
}
m.keys = m.keys[:0]
clear(m.vals)
}
// rangeFn calls f for each element of the map, sorted by m.cmp. If f returns
// false it stops.
func (m *sortedMap[K, V]) rangeFn(f func(K, V) (cont bool)) {
if m == nil {
return
}
for _, k := range m.keys {
if !f(k, m.vals[k]) {
return
}
}
}
// len returns the number of elements in the sorted map.
func (m *sortedMap[K, V]) len() (n int) {
if m == nil {
return 0
}
return len(m.vals)
}

View File

@@ -0,0 +1,94 @@
package tlsconfig
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewSortedMap(t *testing.T) {
var m *sortedMap[string, int]
letters := []string{}
for i := range 10 {
r := string('a' + rune(i))
letters = append(letters, r)
}
t.Run("create_and_fill", func(t *testing.T) {
m = newSortedMap[string, int]()
nums := []int{}
for i, r := range letters {
m.set(r, i)
nums = append(nums, i)
}
gotLetters := []string{}
gotNums := []int{}
m.rangeFn(func(k string, v int) bool {
gotLetters = append(gotLetters, k)
gotNums = append(gotNums, v)
return true
})
assert.Equal(t, letters, gotLetters)
assert.Equal(t, nums, gotNums)
n, ok := m.get(letters[0])
assert.True(t, ok)
assert.Equal(t, nums[0], n)
})
t.Run("clear", func(t *testing.T) {
lastLetter := letters[len(letters)-1]
m.del(lastLetter)
_, ok := m.get(lastLetter)
assert.False(t, ok)
m.clear()
gotLetters := []string{}
m.rangeFn(func(k string, _ int) bool {
gotLetters = append(gotLetters, k)
return true
})
assert.Len(t, gotLetters, 0)
})
}
func TestNewSortedMap_nil(t *testing.T) {
const (
key = "key"
val = "val"
)
var m sortedMap[string, string]
assert.Panics(t, func() {
m.set(key, val)
})
assert.NotPanics(t, func() {
_, ok := m.get(key)
assert.False(t, ok)
})
assert.NotPanics(t, func() {
m.rangeFn(func(_, _ string) (cont bool) {
return true
})
})
assert.NotPanics(t, func() {
m.del(key)
})
assert.NotPanics(t, func() {
m.clear()
})
}

View File

@@ -74,7 +74,8 @@ func (db *LocalTicketDB) Paths(_ context.Context) (paths []string, err error) {
// RemoteTicketDBConfig is the configuration structure for [RemoteTicketDB].
type RemoteTicketDBConfig struct {
// Logger is used for logging the operation of the ticket database.
// Logger is used for logging the operation of the ticket database. It must
// not be nil.
Logger *slog.Logger
// Storage is used to retrieve the session tickets. It must not be nil.
@@ -84,8 +85,8 @@ type RemoteTicketDBConfig struct {
Clock timeutil.Clock
// CacheDirPath is the directory where the session tickets are cached. It
// must be a valid non-empty path to a directory. If the directory doesn't
// exist, it is created.
// should be a valid non-empty path to a directory. If the directory
// doesn't exist, it is created.
CacheDirPath string
// IndexFileName is the base name of the index file. Stored session tickets

View File

@@ -0,0 +1,29 @@
package tlsconfig
import (
"net"
"net/netip"
"time"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/testutil/fakenet"
)
// NewLocalAddrConn returns a new [net.Conn] that has only the LocalAddr method
// implemented, which returns addr.
//
// TODO(a.garipov): Add fakenet.NewConn.
func NewLocalAddrConn(addr netip.Addr) (c *fakenet.Conn) {
return &fakenet.Conn{
OnLocalAddr: func() (a net.Addr) {
return net.TCPAddrFromAddrPort(netip.AddrPortFrom(addr, 0))
},
OnRemoteAddr: func() (a net.Addr) { panic(testutil.UnexpectedCall()) },
OnClose: func() (err error) { panic(testutil.UnexpectedCall()) },
OnRead: func(b []byte) (n int, err error) { panic(testutil.UnexpectedCall()) },
OnWrite: func(b []byte) (n int, err error) { panic(testutil.UnexpectedCall()) },
OnSetDeadline: func(t time.Time) (err error) { panic(testutil.UnexpectedCall()) },
OnSetReadDeadline: func(t time.Time) (err error) { panic(testutil.UnexpectedCall()) },
OnSetWriteDeadline: func(t time.Time) (err error) { panic(testutil.UnexpectedCall()) },
}
}

View File

@@ -0,0 +1,95 @@
package main
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"fmt"
"log/slog"
"math/big"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/backendpb"
"github.com/AdguardTeam/golibs/httphdr"
"google.golang.org/grpc/metadata"
)
// mockDNSServiceServer is the mock [backendpb.CustomDomainServiceServer].
type mockCustomDomainServiceServer struct {
backendpb.UnimplementedCustomDomainServiceServer
logger *slog.Logger
}
// newCustomDomainServiceServer creates a new instance of
// *mockCustomDomainServiceServer. logger must not be nil.
func newCustomDomainServiceServer(logger *slog.Logger) (srv *mockCustomDomainServiceServer) {
return &mockCustomDomainServiceServer{
logger: logger,
}
}
// type check
var _ backendpb.CustomDomainServiceServer = (*mockCustomDomainServiceServer)(nil)
// GetCustomDomainCertificate implements the
// [backendpb.CustomDomainServiceServer] interface
// for *mockCustomDomainServiceServer.
func (s *mockCustomDomainServiceServer) GetCustomDomainCertificate(
ctx context.Context,
req *backendpb.CustomDomainCertificateRequest,
) (resp *backendpb.CustomDomainCertificateResponse, err error) {
md, _ := metadata.FromIncomingContext(ctx)
s.logger.InfoContext(
ctx,
"getting custom domain certificate",
"auth", md.Get(httphdr.Authorization),
"req", req,
)
cert, key, err := generateCert(1)
if err != nil {
return nil, err
}
resp = &backendpb.CustomDomainCertificateResponse{
Certificate: cert,
PrivateKey: x509.MarshalPKCS1PrivateKey(key),
}
return resp, nil
}
// generateCert is a helper function that generates certificate and key.
//
// TODO(f.setrakov): DRY logic with tlsconfig_test.newCertAndKey.
func generateCert(n int64) (certDER []byte, key *rsa.PrivateKey, err error) {
serialNumber := big.NewInt(n)
notBefore := time.Now().Add(-time.Hour * 24)
notAfter := time.Now().Add(time.Hour * 24)
template := x509.Certificate{
SerialNumber: serialNumber,
NotBefore: notBefore,
NotAfter: notAfter,
DNSNames: []string{
"current-1.domain.example",
"current-2.domain.example",
"pending-1.domain.example",
"pending-2.domain.example",
},
}
key, err = rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, fmt.Errorf("generating rsa key: %w", err)
}
cer, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
return nil, nil, fmt.Errorf("creating certificate: %w", err)
}
return cer, key, nil
}

View File

@@ -258,11 +258,21 @@ func (s *mockDNSServiceServer) newDNSProfile(isFullSync bool) (dp *backendpb.DNS
BlockChromePrefetch: true,
BlockFirefoxCanary: true,
BlockPrivateRelay: true,
AdultBlockingMode: &backendpb.DNSProfile_AdultBlockingModeCustomIp{
AdultBlockingModeCustomIp: &backendpb.BlockingModeCustomIP{
Ipv4: []byte{1, 1, 1, 1},
},
},
BlockingMode: &backendpb.DNSProfile_BlockingModeCustomIp{
BlockingModeCustomIp: &backendpb.BlockingModeCustomIP{
Ipv4: []byte{1, 2, 3, 4},
},
},
SafeBrowsingBlockingMode: &backendpb.DNSProfile_SafeBrowsingBlockingModeCustomIp{
SafeBrowsingBlockingModeCustomIp: &backendpb.BlockingModeCustomIP{
Ipv4: []byte{2, 2, 2, 2},
},
},
RateLimit: &backendpb.RateLimitSettings{
ClientCidr: []*backendpb.CidrRange{{
Address: netip.MustParseAddr("3.3.3.0").AsSlice(),

View File

@@ -37,6 +37,9 @@ func main() {
sessTickSrv := newMockSessionTicketServiceServer(l.With(slogutil.KeyPrefix, "session_ticket"))
backendpb.RegisterSessionTicketServiceServer(grpcSrv, sessTickSrv)
customDomainSrv := newCustomDomainServiceServer(l.With(slogutil.KeyPrefix, "custom_domain"))
backendpb.RegisterCustomDomainServiceServer(grpcSrv, customDomainSrv)
l.Info("starting serving", "laddr", listenAddr)
err = grpcSrv.Serve(lsnr)
if err != nil {

View File

@@ -41,9 +41,25 @@ ecscache() (
gofumpt -l -w ./ecsblocklist.go
)
fcpb() (
# TODO(f.setrakov): Change directory to ./internal/profiledb/internal/, so
# we don't need to go up later.
cd ./internal/profiledb/internal/filecachepb/
protoc \
--go_opt=paths=source_relative \
--go_out=../fcpb/ \
--go_opt=default_api_level=API_OPAQUE \
--go_opt=Mfilecache.proto=github.com/AdguardTeam/AdGuardDNS/internal/profiledb/internal/fcpb \
./filecache.proto
)
filecachepb() (
cd ./internal/profiledb/internal/filecachepb/
protoc --go_opt=paths=source_relative --go_out=. ./filecache.proto
protoc \
--go_opt=paths=source_relative \
--go_out=. \
--go_opt=Mfilecache.proto=github.com/AdguardTeam/AdGuardDNS/internal/profiledb/internal/filecachepb \
./filecache.proto
)
geoip_asntops() (
@@ -59,6 +75,7 @@ geoip_country() (
if [ -z "${ONLY:-}" ]; then
backendpb
ecscache
fcpb
filecachepb
geoip_asntops
geoip_country
@@ -73,6 +90,10 @@ else
ecscache
fi
if [ "${padded##* fcpb *}" = '' ]; then
fcpb
fi
if [ "${padded##* filecachepb *}" = '' ]; then
filecachepb
fi

View File

@@ -66,7 +66,7 @@ set -f -u
#
# * internal/agdtest/profile.go: a test helper requiring the use of
# reflect.Type.
# * internal/profiledb/internal/filecachepb/unsafe.go: a “safe” unsafe helper
# * internal/agdprotobuf/unsafe.go: a “safe” unsafe helper
# to prevent excessive allocations.
blocklist_imports() {
import_or_tab="$(printf '^\\(import \\|\t\\)')"
@@ -78,7 +78,7 @@ blocklist_imports() {
-name '*.go' \
'!' -name '*.pb.go' \
'!' -path './internal/agdtest/profile.go' \
'!' -path './internal/profiledb/internal/filecachepb/unsafe.go' \
'!' -path './internal/agdprotobuf/unsafe.go' \
')' \
-exec \
'grep' \
@@ -225,7 +225,7 @@ if [ "$shadow_output" != '' ]; then
exit 1
fi
run_linter gosec --exclude-generated --quiet work
run_linter gosec --exclude-generated --quiet ./...
run_linter errcheck work

View File

@@ -10,6 +10,7 @@ initialisms = [
# Do not add "PTR" since we use "Ptr" as a suffix.
"inherit"
, "ASN"
, "CIDR"
, "DHCP"
, "DNSSEC"
# E.g. SentryDSN.