Sync v2.15.1

This commit is contained in:
Andrey Meshkov
2025-09-01 14:13:28 +03:00
parent c1ed7b15fd
commit 97dbe76804
133 changed files with 6072 additions and 3621 deletions

8
.gitignore vendored
View File

@@ -1,3 +1,8 @@
# This comment is used to simplify checking local copies of the file. Bump
# this number every time a significant change is made to this file.
#
# AdGuard-Project-Version: 3
# Please, DO NOT put your text editors' temporary files here. The more are
# added, the harder it gets to maintain and manage projects' gitignores. Put
# them into your global gitignore file instead.
@@ -6,12 +11,15 @@
#
# Only build, run, and test outputs here. Sorted. With negations at the
# bottom to make sure they take effect.
*.exe
*.out
*.test
/bin/
/filters/
/github-mirror/
/test-reports/
/test/
/tmp/
AdGuardDNS
agdns
asn.mmdb

View File

@@ -7,6 +7,10 @@ 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-2998 / Build 1042
- Profiles file cache version has been incremented. The new field `StandardEnabled` has been added to access object.
## AGDNS-3018 / Build 1033
- The environment variables `DNSCHECK_KV_TTL`, `DNSCHECK_KV_TYPE` have been added.

View File

@@ -24,7 +24,7 @@ BRANCH = $${BRANCH:-$$(git rev-parse --abbrev-ref HEAD)}
GOAMD64 = v1
GOPROXY = https://proxy.golang.org|direct
GOTELEMETRY = off
GOTOOLCHAIN = go1.24.5
GOTOOLCHAIN = go1.24.6
RACE = 0
REVISION = $${REVISION:-$$(git rev-parse --short HEAD)}
VERSION = 0

View File

@@ -71,6 +71,6 @@ You can sign up for a personal AdGuard DNS account and get access to the followi
## Software license
Copyright (C) 2022-2024 AdGuard Software Ltd.
Copyright (C) 2022-2025 AdGuard Software Ltd.
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.

View File

@@ -112,6 +112,7 @@ Supported IDs:
- `profiledb_full`
- `profiledb`
- `rulestat`
- `standard_profile_access`
- `ticket_rotator`
- `tlsconfig`
- `websvc`

View File

@@ -65,6 +65,11 @@ AdGuard DNS uses [environment variables][wiki-env] to store some of the more sen
- [`SESSION_TICKET_REFRESH_INTERVAL`](#SESSION_TICKET_REFRESH_INTERVAL)
- [`SESSION_TICKET_TYPE`](#SESSION_TICKET_TYPE)
- [`SESSION_TICKET_URL`](#SESSION_TICKET_URL)
- [`STANDARD_ACCESS_API_KEY`](#STANDARD_ACCESS_API_KEY)
- [`STANDARD_ACCESS_REFRESH_INTERVAL`](#STANDARD_ACCESS_REFRESH_INTERVAL)
- [`STANDARD_ACCESS_TIMEOUT`](#STANDARD_ACCESS_TIMEOUT)
- [`STANDARD_ACCESS_TYPE`](#STANDARD_ACCESS_TYPE)
- [`STANDARD_ACCESS_URL`](#STANDARD_ACCESS_URL)
- [`SSL_KEY_LOG_FILE`](#SSL_KEY_LOG_FILE)
- [`VERBOSE`](#VERBOSE)
- [`WEB_STATIC_DIR_ENABLED`](#WEB_STATIC_DIR_ENABLED)
@@ -535,6 +540,36 @@ The base backend URL used as a TLS session ticket storage, when [`SESSION_TICKET
**Default:** **Unset.**
## <a href="#STANDARD_ACCESS_API_KEY" id="STANDARD_ACCESS_API_KEY" name="STANDARD_ACCESS_API_KEY">`STANDARD_ACCESS_API_KEY`</a>
The API key to use when authenticating requests to the standard access settings storage API, if [`STANDARD_ACCESS_TYPE`](#STANDARD_ACCESS_TYPE) is set to `backend`. The API key should be valid as defined by [RFC 6750].
**Default:** **Unset.**
## <a href="#STANDARD_ACCESS_REFRESH_INTERVAL" id="STANDARD_ACCESS_REFRESH_INTERVAL" name="STANDARD_ACCESS_REFRESH_INTERVAL">`STANDARD_ACCESS_REFRESH_INTERVAL`</a>
The interval between standard access settings updates, when [`STANDARD_ACCESS_TYPE`](#STANDARD_ACCESS_TYPE) is set to `backend`, as a human-readable duration.
**Default:** **Unset.**
## <a href="#STANDARD_ACCESS_TIMEOUT" id="STANDARD_ACCESS_TIMEOUT" name="STANDARD_ACCESS_TIMEOUT">`STANDARD_ACCESS_TIMEOUT`</a>
The timeout for standard access settings updates, when [`STANDARD_ACCESS_TYPE`](#STANDARD_ACCESS_TYPE) is set to `backend`, as a human-readable duration.
**Default:** **Unset.**
## <a href="#STANDARD_ACCESS_TYPE" id="STANDARD_ACCESS_TYPE" name="STANDARD_ACCESS_TYPE">`STANDARD_ACCESS_TYPE`</a>
The type of standard access settings storage. Its possible values are: `off` and `backend`. When set to `backend`, the [`STANDARD_ACCESS_API_KEY`](#STANDARD_ACCESS_API_KEY), [`STANDARD_ACCESS_REFRESH_INTERVAL`](#STANDARD_ACCESS_REFRESH_INTERVAL), [`STANDARD_ACCESS_TIMEOUT`](#STANDARD_ACCESS_TIMEOUT), and [`STANDARD_ACCESS_URL`](#STANDARD_ACCESS_URL) variables are required.
**Default:** **Unset.**
## <a href="#STANDARD_ACCESS_URL" id="STANDARD_ACCESS_URL" name="STANDARD_ACCESS_URL">`STANDARD_ACCESS_URL`</a>
The base backend URL used as a standard access settings storage, when [`STANDARD_ACCESS_TYPE`](#STANDARD_ACCESS_TYPE) is set to `backend`. Supports gRPC(S) (`grpc://` and`grpcs://`) URLs. See the [external API requirements section][ext-backend-dnscheck].
**Default:** **Unset.**
## <a href="#SSL_KEY_LOG_FILE" id="SSL_KEY_LOG_FILE" name="SSL_KEY_LOG_FILE">`SSL_KEY_LOG_FILE`</a>
If set, TLS key logs are written to this file to allow other programs (i.e. Wireshark) to decrypt packets. **Must only be used for debug purposes**.

78
go.mod
View File

@@ -1,79 +1,80 @@
module github.com/AdguardTeam/AdGuardDNS
go 1.24.5
go 1.24.6
require (
// NOTE: Do not change the pseudoversion.
github.com/AdguardTeam/AdGuardDNS/internal/dnsserver v0.0.0-00010101000000-000000000000
github.com/AdguardTeam/golibs v0.32.15
github.com/AdguardTeam/urlfilter v0.20.0
github.com/AdguardTeam/golibs v0.34.0
github.com/AdguardTeam/urlfilter v0.21.0
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.34.0
github.com/getsentry/sentry-go v0.35.1
github.com/gomodule/redigo v1.9.2
github.com/google/go-cmp v0.7.0
github.com/google/renameio/v2 v2.0.0
github.com/miekg/dns v1.1.66
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.22.0
github.com/prometheus/client_golang v1.23.0
github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.64.0
github.com/quic-go/quic-go v0.52.0
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.39.0
golang.org/x/net v0.41.0
golang.org/x/sys v0.34.0
github.com/prometheus/common v0.65.0
github.com/quic-go/quic-go v0.54.0
github.com/stretchr/testify v1.11.1
github.com/viktordanov/golang-lru v0.5.6
golang.org/x/crypto v0.41.0
golang.org/x/net v0.43.0
golang.org/x/sys v0.35.0
golang.org/x/time v0.12.0
google.golang.org/grpc v1.73.0
google.golang.org/protobuf v1.36.6
google.golang.org/grpc v1.75.0
google.golang.org/protobuf v1.36.8
gopkg.in/yaml.v2 v2.4.0
)
require (
cloud.google.com/go v0.121.3 // indirect
cloud.google.com/go v0.121.6 // indirect
cloud.google.com/go/ai v0.12.1 // indirect
cloud.google.com/go/auth v0.16.2 // 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.7.0 // indirect
cloud.google.com/go/compute/metadata v0.8.0 // indirect
cloud.google.com/go/longrunning v0.6.7 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/ameshkov/dnsstamps v1.0.3 // 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
github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/golangci/misspell v0.7.0 // indirect
github.com/google/generative-ai-go v0.20.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a // 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.14.2 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/gordonklaus/ineffassign v0.1.0 // indirect
github.com/gordonklaus/ineffassign v0.2.0 // 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.23.4 // 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.0 // indirect
github.com/prometheus/procfs v0.16.1 // 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.5 // indirect
github.com/securego/gosec/v2 v2.22.8 // 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.1.0 // indirect
@@ -82,21 +83,20 @@ require (
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.5.2 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/mod v0.26.0 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
golang.org/x/exp/typeparams v0.0.0-20250819193227-8b4c13bb791b // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b // indirect
golang.org/x/term v0.33.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
golang.org/x/telemetry v0.0.0-20250822161441-f407b8c191ff // indirect
golang.org/x/term v0.34.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.org/x/vuln v1.1.4 // indirect
google.golang.org/api v0.241.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
google.golang.org/api v0.248.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // 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

185
go.sum
View File

@@ -1,27 +1,31 @@
cloud.google.com/go v0.121.3 h1:84RD+hQXNdY5Sw/MWVAx5O9Aui/rd5VQ9HEcdN19afo=
cloud.google.com/go v0.121.3/go.mod h1:6vWF3nJWRrEUv26mMB3FEIU/o1MQNVPG1iHdisa2SJc=
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
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.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=
cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
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.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
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.32.15 h1:arDRDWiZCH3g5Onr8AqMnOHhaOppNoBpgC3DNhmeDeA=
github.com/AdguardTeam/golibs v0.32.15/go.mod h1:G9CzUOzx87J+2u+eClJrrwWD7lMbROvuUnT8uvDUzIA=
github.com/AdguardTeam/urlfilter v0.20.0 h1:X32qiuVCVd8WDYCEsbdZKfXMzwdVqrdulamtUi4rmzs=
github.com/AdguardTeam/urlfilter v0.20.0/go.mod h1:gjrywLTxfJh6JOkwi9SU+frhP7kVVEZ5exFGkR99qpk=
github.com/AdguardTeam/golibs v0.34.0 h1:JQK024DkTYxE7vsPVsYsoyDHW/53Nun7OYb9qscniK8=
github.com/AdguardTeam/golibs v0.34.0/go.mod h1:K4C2EbfSEM1zY5YXoti9SfbTAHN/kIX97LpDtCwORrM=
github.com/AdguardTeam/urlfilter v0.21.0 h1:ThIxiP7yoaXt8JTEroGQeU5ftQSoFpUq+t1L+TIx2pA=
github.com/AdguardTeam/urlfilter v0.21.0/go.mod h1:xoZ3AF5qpE9ngbbeSShY9hgVeyHtm9MdH5xH1u714Wg=
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/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/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=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
@@ -36,16 +40,16 @@ github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNr
github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM=
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
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.34.0 h1:1FCHBVp8TfSc8L10zqSwXUZNiOSF+10qw4czjarTiY4=
github.com/getsentry/sentry-go v0.34.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE=
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/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=
@@ -72,8 +76,8 @@ github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/google/pprof v0.0.0-20250607225305-033d6d78b36a h1://KbezygeMJZCSHH+HgUZiTeSoiuFspbMg1ge+eFj18=
github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
@@ -84,12 +88,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s=
github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0=
github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs=
github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw=
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=
@@ -106,14 +110,14 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
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.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
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/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=
@@ -124,47 +128,46 @@ github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
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.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
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.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
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.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA=
github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
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/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.5 h1:ySws9uwOeE42DsG54v2moaJfh7r08Ev7SAYJuoMDfRA=
github.com/securego/gosec/v2 v2.22.5/go.mod h1:AWfgrFsVewk5LKobsPWlygCHt8K91boVPyL6GUZG5NY=
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/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/stretchr/objx v0.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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/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=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
@@ -187,72 +190,56 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
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.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b h1:KdrhdYPDUvJTvrDK9gdjfFd6JTk8vA1WJoldYSi0kHo=
golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/exp/typeparams v0.0.0-20250819193227-8b4c13bb791b h1:GU1ttDuJS89SePnuEsEuLj7dMMFP2JkGsDV1Z51iDXo=
golang.org/x/exp/typeparams v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b h1:DU+gwOBXU+6bO0sEyO7o/NeMlxZxCZEvI7v+J4a1zRQ=
golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20250822161441-f407b8c191ff h1:ey0rzo8V0Po6WkHMbhGVL6zNkuYDBe9iP5toIRchj9Q=
golang.org/x/telemetry v0.0.0-20250822161441-f407b8c191ff/go.mod h1:JIJwPkb04vX0KeIBbQ7epGtgIjA8ihHbsAtW4A/lIQ4=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
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=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I=
golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s=
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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.241.0 h1:QKwqWQlkc6O895LchPEDUSYr22Xp3NCxpQRiWTB6avE=
google.golang.org/api v0.241.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
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.248.0 h1:hUotakSkcwGdYUqzCRc5yGYsg4wXxpkKlW5ryVqvC1Y=
google.golang.org/api v0.248.0/go.mod h1:yAFUAF56Li7IuIQbTFoLwXTCI6XCFKueOlS7S9e4F9k=
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/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/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
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.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -1,4 +1,4 @@
go 1.24.5
go 1.24.6
use (
.

View File

@@ -11,6 +11,7 @@ cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI=
cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cel.dev/expr v0.23.0 h1:wUb94w6OYQS4uXraxo9U+wUAs9jT47Xvl4iPgAwM2ss=
cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
@@ -183,6 +184,7 @@ cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqv
cloud.google.com/go/storage v1.53.0 h1:gg0ERZwL17pJ+Cz3cD2qS60w1WMDnwcm5YPAIQBHUAw=
cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA=
cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY=
cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU=
cloud.google.com/go/storagetransfer v1.10.4/go.mod h1:vef30rZKu5HSEf/x1tK3WfWrL0XVoUQN/EPDRGPzjZs=
cloud.google.com/go/talent v1.6.6/go.mod h1:y/WQDKrhVz12WagoarpAIyKKMeKGKHWPoReZ0g8tseQ=
cloud.google.com/go/texttospeech v1.7.5/go.mod h1:tzpCuNWPwrNJnEa4Pu5taALuZL4QRRLcb+K9pbhXT6M=
@@ -232,6 +234,8 @@ github.com/AdguardTeam/golibs v0.32.11/go.mod h1:LXr0gqqZuVpt+L+bP3Nnr0/CecLmm3r
github.com/AdguardTeam/gomitmproxy v0.2.0 h1:rvCOf17pd1/CnMyMQW891zrEiIQBpQ8cIGjKN9pinUU=
github.com/AdguardTeam/gomitmproxy v0.2.1 h1:p9gr8Er1TYvf+7ic81Ax1sZ62UNCsMTZNbm7tC59S9o=
github.com/AdguardTeam/gomitmproxy v0.2.1/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
github.com/AdguardTeam/urlfilter v0.21.0 h1:ThIxiP7yoaXt8JTEroGQeU5ftQSoFpUq+t1L+TIx2pA=
github.com/AdguardTeam/urlfilter v0.21.0/go.mod h1:xoZ3AF5qpE9ngbbeSShY9hgVeyHtm9MdH5xH1u714Wg=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
@@ -251,12 +255,15 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0/go.mod h1:ZV4VOm0/eHR06JLrXWe09068dHpr3TRpY9Uo7T+anuA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk=
github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM=
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
@@ -312,6 +319,7 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
@@ -362,6 +370,7 @@ github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k=
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
@@ -380,7 +389,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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=
@@ -453,6 +461,7 @@ github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
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=
@@ -514,6 +523,7 @@ github.com/golang/glog v1.2.3 h1:oDTdz9f5VGVVNGu/Q7UXKWYsD0873HXLHdJUNBsSEKM=
github.com/golang/glog v1.2.3/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -1150,7 +1160,6 @@ go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxt
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/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
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=
@@ -1161,6 +1170,7 @@ go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9f
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
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.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=
@@ -1704,6 +1714,7 @@ google.golang.org/genproto/googleapis/bytestream v0.0.0-20250512202823-5a2f75b73
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250528174236-200df99c418a/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822 h1:zWFRixYR5QlotL+Uv3YfsPRENIrQFXiGs+iwqel6fOQ=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250818200422-3122310a409c/go.mod h1:1kGGe25NDrNJYgta9Rp2QLLXWS1FLVMMXNvihbhK0iE=
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=

View File

@@ -4,10 +4,10 @@ package access
import (
"fmt"
"net/netip"
"strings"
"github.com/AdguardTeam/AdGuardDNS/internal/agdurlflt"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/syncutil"
"github.com/AdguardTeam/urlfilter"
"github.com/AdguardTeam/urlfilter/filterlist"
)
@@ -32,25 +32,28 @@ type Interface interface {
type Global struct {
blockedHostsEng *urlfilter.DNSEngine
blockedNets netutil.SubnetSet
reqPool *syncutil.Pool[urlfilter.DNSRequest]
resPool *syncutil.Pool[urlfilter.DNSResult]
}
// NewGlobal create a new Global from provided parameters.
// NewGlobal creates a new *Global from provided parameters.
func NewGlobal(blockedDomains []string, blockedSubnets []netip.Prefix) (g *Global, err error) {
g = &Global{
blockedNets: netutil.SliceSubnetSet(blockedSubnets),
reqPool: syncutil.NewPool(func() (req *urlfilter.DNSRequest) {
return &urlfilter.DNSRequest{}
}),
resPool: syncutil.NewPool(func() (v *urlfilter.DNSResult) {
return &urlfilter.DNSResult{}
}),
}
b := &strings.Builder{}
for _, h := range blockedDomains {
stringutil.WriteToBuilder(b, strings.ToLower(h), "\n")
}
lists := []filterlist.RuleList{
&filterlist.StringRuleList{
lists := []filterlist.Interface{
filterlist.NewBytes(&filterlist.BytesConfig{
ID: blocklistFilterID,
RulesText: b.String(),
RulesText: agdurlflt.RulesToBytesLower(blockedDomains),
IgnoreCosmetic: true,
},
}),
}
rulesStrg, err := filterlist.NewRuleStorage(lists)
@@ -68,16 +71,37 @@ var _ Interface = (*Global)(nil)
// IsBlockedHost implements the [Interface] interface for *Global.
func (g *Global) IsBlockedHost(host string, qt uint16) (blocked bool) {
res, matched := g.blockedHostsEng.MatchRequest(&urlfilter.DNSRequest{
Hostname: host,
DNSType: qt,
})
return matchBlocked(host, qt, g.blockedHostsEng, g.reqPool, g.resPool)
}
if matched && res.NetworkRule != nil {
// matchBlocked is a helper function that handles matching of request using DNS
// engines and pools of requests and results. engine, reqPool, and resPool must
// not be nil.
func matchBlocked(
host string,
qt uint16,
engine *urlfilter.DNSEngine,
reqPool *syncutil.Pool[urlfilter.DNSRequest],
resPool *syncutil.Pool[urlfilter.DNSResult],
) (blocked bool) {
req := reqPool.Get()
defer reqPool.Put(req)
req.Reset()
req.Hostname = host
req.DNSType = qt
res := resPool.Get()
defer resPool.Put(res)
res.Reset()
blocked = engine.MatchRequestInto(req, res)
if blocked && res.NetworkRule != nil {
return !res.NetworkRule.Whitelist
}
return matched
return blocked
}
// IsBlockedIP implements the [Interface] interface for *Global.

View File

@@ -17,7 +17,59 @@ const testTimeout = 1 * time.Second
// testAccessMtrc is the common profile access engine metrics for tests.
var testAccessMtrc = access.EmptyProfileMetrics{}
func TestGlobal_IsBlockedHost(t *testing.T) {
// testCases is the list of test cases for the [IsBlocked] function.
var testCases = []struct {
want assert.BoolAssertionFunc
name string
host string
qt uint16
}{{
want: assert.False,
name: "pass",
host: "pass.test",
qt: dns.TypeA,
}, {
want: assert.True,
name: "blocked_domain_a",
host: "block.test",
qt: dns.TypeA,
}, {
want: assert.True,
name: "blocked_domain_https",
host: "block.test",
qt: dns.TypeHTTPS,
}, {
want: assert.True,
name: "uppercase_domain",
host: "uppercase.test",
qt: dns.TypeHTTPS,
}, {
want: assert.False,
name: "pass_qt",
host: "block_aaaa.test",
qt: dns.TypeA,
}, {
want: assert.True,
name: "block_qt",
host: "block_aaaa.test",
qt: dns.TypeAAAA,
}, {
want: assert.True,
name: "allowlist_block",
host: "block.allowlist.test",
qt: dns.TypeA,
}, {
want: assert.False,
name: "allowlist_test",
host: "allow.allowlist.test",
qt: dns.TypeA,
}}
// newTestGlobal is a test helper that returns a new [access.Global] with test
// rules.
func newTestGlobal(t testing.TB) (global *access.Global) {
t.Helper()
global, err := access.NewGlobal([]string{
"block.test",
"UPPERCASE.test",
@@ -27,52 +79,13 @@ func TestGlobal_IsBlockedHost(t *testing.T) {
}, nil)
require.NoError(t, err)
testCases := []struct {
want assert.BoolAssertionFunc
name string
host string
qt uint16
}{{
want: assert.False,
name: "pass",
host: "pass.test",
qt: dns.TypeA,
}, {
want: assert.True,
name: "blocked_domain_A",
host: "block.test",
qt: dns.TypeA,
}, {
want: assert.True,
name: "blocked_domain_HTTPS",
host: "block.test",
qt: dns.TypeHTTPS,
}, {
want: assert.True,
name: "uppercase_domain",
host: "uppercase.test",
qt: dns.TypeHTTPS,
}, {
want: assert.False,
name: "pass_qt",
host: "block_aaaa.test",
qt: dns.TypeA,
}, {
want: assert.True,
name: "block_qt",
host: "block_aaaa.test",
qt: dns.TypeAAAA,
}, {
want: assert.True,
name: "allowlist_block",
host: "block.allowlist.test",
qt: dns.TypeA,
}, {
want: assert.False,
name: "allowlist_test",
host: "allow.allowlist.test",
qt: dns.TypeA,
}}
return global
}
func TestGlobal_IsBlockedHost(t *testing.T) {
t.Parallel()
global := newTestGlobal(t)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
@@ -83,9 +96,11 @@ func TestGlobal_IsBlockedHost(t *testing.T) {
}
func TestGlobal_IsBlockedIP(t *testing.T) {
t.Parallel()
global, err := access.NewGlobal([]string{}, []netip.Prefix{
netip.MustParsePrefix("1.1.1.1/32"),
netip.MustParsePrefix("2.2.2.0/8"),
netip.MustParsePrefix("192.0.2.1/32"),
netip.MustParsePrefix("198.51.100.0/24"),
})
require.NoError(t, err)
@@ -96,19 +111,19 @@ func TestGlobal_IsBlockedIP(t *testing.T) {
}{{
want: assert.False,
name: "pass",
ip: netip.MustParseAddr("1.1.1.0"),
ip: netip.MustParseAddr("192.0.2.0"),
}, {
want: assert.True,
name: "block_ip",
ip: netip.MustParseAddr("1.1.1.1"),
ip: netip.MustParseAddr("192.0.2.1"),
}, {
want: assert.False,
name: "pass_subnet",
ip: netip.MustParseAddr("1.2.2.2"),
ip: netip.MustParseAddr("198.51.101.1"),
}, {
want: assert.True,
name: "block_subnet",
ip: netip.MustParseAddr("2.2.2.2"),
ip: netip.MustParseAddr("198.51.100.1"),
}}
for _, tc := range testCases {
@@ -118,3 +133,77 @@ func TestGlobal_IsBlockedIP(t *testing.T) {
})
}
}
func BenchmarkGlobal_IsBlockedHost(b *testing.B) {
global := newTestGlobal(b)
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
var blocked bool
b.ReportAllocs()
for b.Loop() {
blocked = global.IsBlockedHost(tc.host, tc.qt)
}
tc.want(b, blocked)
})
}
// Most recent results:
//
// goos: linux
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/access
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkGlobal_IsBlockedHost/pass-16 2313513 515.0 ns/op 16 B/op 1 allocs/op
// BenchmarkGlobal_IsBlockedHost/blocked_domain_a-16 1604049 683.4 ns/op 24 B/op 1 allocs/op
// BenchmarkGlobal_IsBlockedHost/blocked_domain_https-16 1981204 597.7 ns/op 24 B/op 1 allocs/op
// BenchmarkGlobal_IsBlockedHost/uppercase_domain-16 2093197 590.5 ns/op 24 B/op 1 allocs/op
// BenchmarkGlobal_IsBlockedHost/pass_qt-16 1961065 653.3 ns/op 24 B/op 1 allocs/op
// BenchmarkGlobal_IsBlockedHost/block_qt-16 768783 1567 ns/op 24 B/op 1 allocs/op
// BenchmarkGlobal_IsBlockedHost/allowlist_block-16 759159 1890 ns/op 32 B/op 1 allocs/op
// BenchmarkGlobal_IsBlockedHost/allowlist_test-16 371722 3170 ns/op 32 B/op 1 allocs/op
}
func BenchmarkGlobal_IsBlockedIP(b *testing.B) {
global, err := access.NewGlobal([]string{}, []netip.Prefix{
netip.MustParsePrefix("192.0.2.0/24"),
})
require.NoError(b, err)
b.Run("pass", func(b *testing.B) {
ip := netip.MustParseAddr("192.0.3.0")
var blocked bool
b.ReportAllocs()
for b.Loop() {
blocked = global.IsBlockedIP(ip)
}
assert.False(b, blocked)
})
b.Run("block", func(b *testing.B) {
ip := netip.MustParseAddr("192.0.2.0")
var blocked bool
b.ReportAllocs()
for b.Loop() {
blocked = global.IsBlockedIP(ip)
}
assert.True(b, blocked)
})
// Most recent results:
//
// goos: linux
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/access
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkGlobal_IsBlockedIP/pass-16 100000000 10.18 ns/op 0 B/op 0 allocs/op
// BenchmarkGlobal_IsBlockedIP/block-16 141876058 8.545 ns/op 0 B/op 0 allocs/op
}

View File

@@ -0,0 +1,38 @@
package access
import (
"context"
"net/netip"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/miekg/dns"
)
// Blocker is the interface to control DNS resolution access.
type Blocker interface {
// IsBlocked returns true if the req should be blocked. req must not be
// nil, and req.Question must have one item.
IsBlocked(
ctx context.Context,
req *dns.Msg,
rAddr netip.AddrPort,
l *geoip.Location,
) (isBlocked bool)
}
// EmptyBlocker is an empty [Blocker] implementation that does nothing.
type EmptyBlocker struct{}
// type check
var _ Blocker = EmptyBlocker{}
// IsBlocked implements the [Blocker] interface for EmptyBlocker. It always
// returns false.
func (EmptyBlocker) IsBlocked(
_ context.Context,
_ *dns.Msg,
_ netip.AddrPort,
_ *geoip.Location,
) (isBlocked bool) {
return false
}

View File

@@ -3,12 +3,12 @@ package access
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agdnet"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/AdGuardDNS/internal/agdurlflt"
"github.com/AdguardTeam/golibs/syncutil"
"github.com/AdguardTeam/urlfilter"
"github.com/AdguardTeam/urlfilter/filterlist"
"github.com/miekg/dns"
@@ -18,18 +18,26 @@ import (
//
// TODO(a.garipov): Replace/merge with [custom.Filter].
type blockedHostEngine struct {
metrics ProfileMetrics
lazyEngine *urlfilter.DNSEngine
initOnce *sync.Once
lazyEngine *urlfilter.DNSEngine
reqPool *syncutil.Pool[urlfilter.DNSRequest]
resPool *syncutil.Pool[urlfilter.DNSResult]
metrics ProfileMetrics
rules []string
}
// newBlockedHostEngine creates a new blockedHostEngine. mtrc must not be nil.
func newBlockedHostEngine(mtrc ProfileMetrics, rules []string) (e *blockedHostEngine) {
return &blockedHostEngine{
metrics: mtrc,
rules: rules,
initOnce: &sync.Once{},
reqPool: syncutil.NewPool(func() (req *urlfilter.DNSRequest) {
return &urlfilter.DNSRequest{}
}),
resPool: syncutil.NewPool(func() (v *urlfilter.DNSResult) {
return &urlfilter.DNSResult{}
}),
metrics: mtrc,
rules: rules,
}
}
@@ -48,31 +56,20 @@ func (e *blockedHostEngine) isBlocked(ctx context.Context, req *dns.Msg) (blocke
})
q := req.Question[0]
res, matched := e.lazyEngine.MatchRequest(&urlfilter.DNSRequest{
Hostname: agdnet.NormalizeQueryDomain(q.Name),
DNSType: q.Qtype,
})
if matched && res.NetworkRule != nil {
return !res.NetworkRule.Whitelist
}
host := agdnet.NormalizeQueryDomain(q.Name)
return matched
return matchBlocked(host, q.Qtype, e.lazyEngine, e.reqPool, e.resPool)
}
// init returns new properly initialized dns engine.
func (e *blockedHostEngine) init() (eng *urlfilter.DNSEngine) {
b := &strings.Builder{}
for _, h := range e.rules {
stringutil.WriteToBuilder(b, strings.ToLower(h), "\n")
}
lists := []filterlist.RuleList{
&filterlist.StringRuleList{
lists := []filterlist.Interface{
filterlist.NewBytes(&filterlist.BytesConfig{
ID: blocklistFilterID,
RulesText: b.String(),
RulesText: agdurlflt.RulesToBytesLower(e.rules),
IgnoreCosmetic: true,
},
}),
}
rulesStrg, err := filterlist.NewRuleStorage(lists)

View File

@@ -109,3 +109,46 @@ func TestBlockedHostEngine_IsBlocked_concurrent(t *testing.T) {
wg.Wait()
}
func BenchmarkBlockedHostEngine_IsBlocked(b *testing.B) {
engine := newBlockedHostEngine(EmptyProfileMetrics{}, []string{
"block.test",
})
ctx := testutil.ContextWithTimeout(b, testTimeout)
b.Run("pass", func(b *testing.B) {
req := dnsservertest.NewReq("pass.test", dns.TypeA, dns.ClassINET)
var blocked bool
b.ReportAllocs()
for b.Loop() {
blocked = engine.isBlocked(ctx, req)
}
assert.False(b, blocked)
})
b.Run("block", func(b *testing.B) {
req := dnsservertest.NewReq("block.test", dns.TypeA, dns.ClassINET)
var blocked bool
b.ReportAllocs()
for b.Loop() {
blocked = engine.isBlocked(ctx, req)
}
assert.True(b, blocked)
})
// Most recent results:
//
// goos: linux
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/access
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkBlockedHostEngine_IsBlocked/pass-16 3362199 369.1 ns/op 16 B/op 1 allocs/op
// BenchmarkBlockedHostEngine_IsBlocked/block-16 2299890 510.6 ns/op 24 B/op 1 allocs/op
}

View File

@@ -6,6 +6,8 @@ import (
"slices"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/golibs/syncutil"
"github.com/AdguardTeam/urlfilter"
"github.com/miekg/dns"
)
@@ -14,18 +16,13 @@ type Profile interface {
// Config returns the profile access configuration.
Config() (conf *ProfileConfig)
// IsBlocked returns true if the req should be blocked. req must not be
// nil, and req.Question must have one item.
IsBlocked(
ctx context.Context,
req *dns.Msg,
rAddr netip.AddrPort,
l *geoip.Location,
) (blocked bool)
Blocker
}
// EmptyProfile is an empty profile implementation that does nothing.
type EmptyProfile struct{}
// EmptyProfile is an empty [Profile] implementation that does nothing.
type EmptyProfile struct {
EmptyBlocker
}
// type check
var _ Profile = EmptyProfile{}
@@ -34,17 +31,6 @@ var _ Profile = EmptyProfile{}
// returns nil.
func (EmptyProfile) Config() (conf *ProfileConfig) { return nil }
// IsBlocked implements the [Profile] interface for EmptyProfile. It always
// returns false.
func (EmptyProfile) IsBlocked(
_ context.Context,
_ *dns.Msg,
_ netip.AddrPort,
_ *geoip.Location,
) (blocked bool) {
return false
}
// ProfileConfig is a profile specific access configuration.
//
// NOTE: Do not change fields of this structure without incrementing
@@ -64,14 +50,23 @@ type ProfileConfig struct {
// BlocklistDomainRules is slice of rules to match requests.
BlocklistDomainRules []string
// StandardEnabled controls whether the profile should also apply standard
// access settings.
StandardEnabled bool
}
// DefaultProfile controls profile specific IP and client blocking that take
// place before all other processing. DefaultProfile is safe for concurrent
// use.
type DefaultProfile struct {
standard Blocker
blockedHostsEng *blockedHostEngine
reqPool *syncutil.Pool[urlfilter.DNSRequest]
resPool *syncutil.Pool[urlfilter.DNSResult]
allowedNets []netip.Prefix
blockedNets []netip.Prefix
@@ -80,6 +75,8 @@ type DefaultProfile struct {
blockedASN []geoip.ASN
blocklistDomainRules []string
standardEnabled bool
}
// defaultProfileConfig is the configuration for the default access for
@@ -89,21 +86,42 @@ type defaultProfileConfig struct {
// nil and must be valid.
conf *ProfileConfig
// reqPool is the pool of URLFilter request data to use and reuse during
// filtering. It must not be nil.
reqPool *syncutil.Pool[urlfilter.DNSRequest]
// resPool is the pool of URLFilter result data to use and reuse during
// filtering. It must not be nil.
resPool *syncutil.Pool[urlfilter.DNSResult]
// metrics is used for the collection of the profile access engine
// statistics. It must not be nil.
metrics ProfileMetrics
// standard is the standard access blocker to use.
standard Blocker
}
// newDefaultProfile creates a new *DefaultProfile. conf is assumed to be
// valid. mtrc must not be nil.
func newDefaultProfile(c *defaultProfileConfig) (p *DefaultProfile) {
return &DefaultProfile{
allowedNets: c.conf.AllowedNets,
blockedNets: c.conf.BlockedNets,
allowedASN: c.conf.AllowedASN,
blockedASN: c.conf.BlockedASN,
standard: c.standard,
blockedHostsEng: newBlockedHostEngine(c.metrics, c.conf.BlocklistDomainRules),
reqPool: c.reqPool,
resPool: c.resPool,
allowedNets: c.conf.AllowedNets,
blockedNets: c.conf.BlockedNets,
allowedASN: c.conf.AllowedASN,
blockedASN: c.conf.BlockedASN,
blocklistDomainRules: c.conf.BlocklistDomainRules,
blockedHostsEng: newBlockedHostEngine(c.metrics, c.conf.BlocklistDomainRules),
standardEnabled: c.conf.StandardEnabled,
}
}
@@ -118,10 +136,14 @@ func (p *DefaultProfile) Config() (conf *ProfileConfig) {
AllowedASN: slices.Clone(p.allowedASN),
BlockedASN: slices.Clone(p.blockedASN),
BlocklistDomainRules: slices.Clone(p.blocklistDomainRules),
StandardEnabled: p.standardEnabled,
}
}
// IsBlocked implements the [Profile] interface for *DefaultProfile.
// type check
var _ Blocker = (*DefaultProfile)(nil)
// IsBlocked implements the [Blocker] interface for *DefaultProfile.
func (p *DefaultProfile) IsBlocked(
ctx context.Context,
req *dns.Msg,
@@ -130,7 +152,9 @@ func (p *DefaultProfile) IsBlocked(
) (blocked bool) {
ip := rAddr.Addr()
return p.isBlockedByNets(ip, l) || p.isBlockedByHostsEng(ctx, req)
return p.isBlockedByNets(ip, l) ||
p.isBlockedByHostsEng(ctx, req) ||
p.standard.IsBlocked(ctx, req, rAddr, l)
}
// isBlockedByNets returns true if ip or l is blocked by current profile.
@@ -163,28 +187,3 @@ func matchASNs(asns []geoip.ASN, l *geoip.Location) (ok bool) {
func (p *DefaultProfile) isBlockedByHostsEng(ctx context.Context, req *dns.Msg) (blocked bool) {
return p.blockedHostsEng.isBlocked(ctx, req)
}
// ProfileConstructor creates default access managers for profiles.
//
// TODO(a.garipov): Add global standard rules for profile access managers here
// as well.
type ProfileConstructor struct {
metrics ProfileMetrics
}
// NewProfileConstructor returns a properly initialized *ProfileConstructor.
// mtrc must not be nil.
func NewProfileConstructor(mtrc ProfileMetrics) (c *ProfileConstructor) {
return &ProfileConstructor{
metrics: mtrc,
}
}
// New creates a new access manager for a profile based on the configuration.
// conf must not be nil and must be valid.
func (c *ProfileConstructor) New(conf *ProfileConfig) (p *DefaultProfile) {
return newDefaultProfile(&defaultProfileConfig{
conf: conf,
metrics: c.metrics,
})
}

View File

@@ -13,30 +13,49 @@ import (
)
func TestDefaultProfile_Config(t *testing.T) {
t.Parallel()
conf := &access.ProfileConfig{
AllowedNets: []netip.Prefix{netip.MustParsePrefix("1.1.1.0/24")},
BlockedNets: []netip.Prefix{netip.MustParsePrefix("2.2.2.0/24")},
AllowedNets: []netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")},
BlockedNets: []netip.Prefix{netip.MustParsePrefix("192.0.2.2/32")},
AllowedASN: []geoip.ASN{1},
BlockedASN: []geoip.ASN{1, 2},
BlocklistDomainRules: []string{"block.test"},
StandardEnabled: true,
}
cons := access.NewProfileConstructor(testAccessMtrc)
cons := access.NewProfileConstructor(&access.ProfileConstructorConfig{
Metrics: testAccessMtrc,
Standard: access.EmptyBlocker{},
})
a := cons.New(conf)
got := a.Config()
assert.Equal(t, conf.AllowedNets, got.AllowedNets)
assert.Equal(t, conf.BlockedNets, got.BlockedNets)
assert.Equal(t, conf.AllowedASN, got.AllowedASN)
assert.Equal(t, conf.BlockedASN, got.BlockedASN)
assert.Equal(t, conf.BlocklistDomainRules, got.BlocklistDomainRules)
assert.Equal(t, conf, got)
}
func TestDefaultProfile_IsBlocked(t *testing.T) {
passAddrPort := netip.MustParseAddrPort("3.3.3.3:3333")
t.Parallel()
passAddrPort := netip.MustParseAddrPort("192.0.2.3:3333")
std := access.NewStandardBlocker(&access.StandardBlockerConfig{
AllowedNets: []netip.Prefix{netip.MustParsePrefix("192.0.2.10/32")},
BlockedNets: []netip.Prefix{netip.MustParsePrefix("192.0.2.20/32")},
AllowedASN: []geoip.ASN{10},
BlockedASN: []geoip.ASN{10, 20},
BlocklistDomainRules: []string{
"block.std.test",
"UPPERCASE.STD.test",
"||block_aaaa.std.test^$dnstype=AAAA",
"||allowlist.std.test^",
"@@||allow.allowlist.std.test^",
},
})
conf := &access.ProfileConfig{
AllowedNets: []netip.Prefix{netip.MustParsePrefix("1.1.1.1/32")},
BlockedNets: []netip.Prefix{netip.MustParsePrefix("1.1.1.0/24")},
AllowedNets: []netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")},
BlockedNets: []netip.Prefix{netip.MustParsePrefix("192.0.2.2/32")},
AllowedASN: []geoip.ASN{1},
BlockedASN: []geoip.ASN{1, 2},
BlocklistDomainRules: []string{
@@ -46,9 +65,13 @@ func TestDefaultProfile_IsBlocked(t *testing.T) {
"||allowlist.test^",
"@@||allow.allowlist.test^",
},
StandardEnabled: true,
}
cons := access.NewProfileConstructor(testAccessMtrc)
cons := access.NewProfileConstructor(&access.ProfileConstructorConfig{
Metrics: testAccessMtrc,
Standard: std,
})
a := cons.New(conf)
testCases := []struct {
@@ -117,21 +140,21 @@ func TestDefaultProfile_IsBlocked(t *testing.T) {
}, {
want: assert.False,
name: "pass_ip",
rAddr: netip.MustParseAddrPort("1.1.1.1:57"),
rAddr: netip.MustParseAddrPort("192.0.2.1:57"),
host: "pass.test",
qt: dns.TypeA,
loc: nil,
}, {
want: assert.True,
name: "block_subnet",
rAddr: netip.MustParseAddrPort("1.1.1.2:57"),
rAddr: netip.MustParseAddrPort("192.0.2.2:57"),
host: "pass.test",
qt: dns.TypeA,
loc: nil,
}, {
want: assert.False,
name: "pass_subnet",
rAddr: netip.MustParseAddrPort("1.2.2.2:57"),
rAddr: netip.MustParseAddrPort("192.0.2.1:57"),
host: "pass.test",
qt: dns.TypeA,
loc: nil,
@@ -156,10 +179,103 @@ func TestDefaultProfile_IsBlocked(t *testing.T) {
host: "pass.test",
qt: dns.TypeA,
loc: &geoip.Location{ASN: 2},
}, {
want: assert.True,
name: "standard_blocked_domain_A",
host: "block.std.test",
qt: dns.TypeA,
rAddr: passAddrPort,
loc: nil,
}, {
want: assert.True,
name: "standard_blocked_domain_HTTPS",
host: "block.std.test",
qt: dns.TypeHTTPS,
rAddr: passAddrPort,
loc: nil,
}, {
want: assert.True,
name: "standard_uppercase_domain",
host: "uppercase.std.test",
qt: dns.TypeHTTPS,
rAddr: passAddrPort,
loc: nil,
}, {
want: assert.False,
name: "standard_pass_qt",
host: "block_aaaa.std.test",
qt: dns.TypeA,
rAddr: passAddrPort,
loc: nil,
}, {
want: assert.True,
name: "standard_block_qt",
host: "block_aaaa.std.test",
qt: dns.TypeAAAA,
rAddr: passAddrPort,
loc: nil,
}, {
want: assert.True,
name: "standard_allowlist_block",
host: "block.allowlist.std.test",
qt: dns.TypeA,
rAddr: passAddrPort,
loc: nil,
}, {
want: assert.False,
name: "standard_allowlist_test",
host: "allow.allowlist.std.test",
qt: dns.TypeA,
rAddr: passAddrPort,
loc: nil,
}, {
want: assert.False,
name: "standard_pass_ip",
rAddr: netip.MustParseAddrPort("192.0.2.21:57"),
host: "pass.std.test",
qt: dns.TypeA,
loc: nil,
}, {
want: assert.True,
name: "standard_block_subnet",
rAddr: netip.MustParseAddrPort("192.0.2.20:57"),
host: "pass.std.test",
qt: dns.TypeA,
loc: nil,
}, {
want: assert.False,
name: "standard_pass_subnet",
rAddr: netip.MustParseAddrPort("192.0.2.11:57"),
host: "pass.std.test",
qt: dns.TypeA,
loc: nil,
}, {
want: assert.True,
name: "standard_block_host_pass_asn",
rAddr: passAddrPort,
host: "block.std.test",
qt: dns.TypeA,
loc: &geoip.Location{ASN: 10},
}, {
want: assert.False,
name: "standard_pass_asn",
rAddr: passAddrPort,
host: "pass.std.test",
qt: dns.TypeA,
loc: &geoip.Location{ASN: 10},
}, {
want: assert.True,
name: "standard_block_asn",
rAddr: passAddrPort,
host: "pass.std.test",
qt: dns.TypeA,
loc: &geoip.Location{ASN: 20},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
req := dnsservertest.NewReq(tc.host, tc.qt, dns.ClassINET)
ctx := testutil.ContextWithTimeout(t, testTimeout)
@@ -170,6 +286,8 @@ func TestDefaultProfile_IsBlocked(t *testing.T) {
}
func TestDefaultProfile_IsBlocked_prefixAllowlist(t *testing.T) {
t.Parallel()
conf := &access.ProfileConfig{
AllowedNets: []netip.Prefix{
netip.MustParsePrefix("2.2.2.0/24"),
@@ -181,7 +299,10 @@ func TestDefaultProfile_IsBlocked_prefixAllowlist(t *testing.T) {
BlocklistDomainRules: nil,
}
cons := access.NewProfileConstructor(testAccessMtrc)
cons := access.NewProfileConstructor(&access.ProfileConstructorConfig{
Metrics: testAccessMtrc,
Standard: access.EmptyBlocker{},
})
a := cons.New(conf)
testCases := []struct {
@@ -212,6 +333,8 @@ func TestDefaultProfile_IsBlocked_prefixAllowlist(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
req := dnsservertest.NewReq("pass.test", dns.TypeA, dns.ClassINET)
ctx := testutil.ContextWithTimeout(t, testTimeout)
@@ -238,7 +361,10 @@ func BenchmarkDefaultProfile_IsBlocked(b *testing.B) {
},
}
cons := access.NewProfileConstructor(testAccessMtrc)
cons := access.NewProfileConstructor(&access.ProfileConstructorConfig{
Metrics: testAccessMtrc,
Standard: access.EmptyBlocker{},
})
a := cons.New(conf)
ctx := testutil.ContextWithTimeout(b, testTimeout)
@@ -271,10 +397,10 @@ func BenchmarkDefaultProfile_IsBlocked(b *testing.B) {
// Most recent results:
//
// goos: darwin
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/access
// cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
// BenchmarkDefaultProfile_IsBlocked/pass-12 2761741 421.9 ns/op 96 B/op 2 allocs/op
// BenchmarkDefaultProfile_IsBlocked/block-12 2143516 556.1 ns/op 128 B/op 4 allocs/op
// goos: linux
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/access
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkDefaultProfile_IsBlocked/pass-16 2679700 468.8 ns/op 16 B/op 1 allocs/op
// BenchmarkDefaultProfile_IsBlocked/block-16 2081113 576.4 ns/op 24 B/op 1 allocs/op
}

View File

@@ -0,0 +1,57 @@
package access
import (
"github.com/AdguardTeam/golibs/syncutil"
"github.com/AdguardTeam/urlfilter"
)
// ProfileConstructorConfig is the configuration for the [ProfileConstructor].
type ProfileConstructorConfig struct {
// Metrics is used for the collection of the statistics of profile access
// managers. It must not be nil.
Metrics ProfileMetrics
// Standard is the standard blocker for all profiles which have enabled this
// feature. It must not be nil.
Standard Blocker
}
// ProfileConstructor creates default access managers for profiles.
type ProfileConstructor struct {
reqPool *syncutil.Pool[urlfilter.DNSRequest]
resPool *syncutil.Pool[urlfilter.DNSResult]
metrics ProfileMetrics
standard Blocker
}
// NewProfileConstructor returns a properly initialized *ProfileConstructor.
// conf must not be nil.
func NewProfileConstructor(conf *ProfileConstructorConfig) (c *ProfileConstructor) {
return &ProfileConstructor{
reqPool: syncutil.NewPool(func() (req *urlfilter.DNSRequest) {
return &urlfilter.DNSRequest{}
}),
resPool: syncutil.NewPool(func() (v *urlfilter.DNSResult) {
return &urlfilter.DNSResult{}
}),
metrics: conf.Metrics,
standard: conf.Standard,
}
}
// New creates a new access manager for a profile based on the configuration.
// conf must not be nil and must be valid.
func (c *ProfileConstructor) New(conf *ProfileConfig) (p *DefaultProfile) {
var standard Blocker = EmptyBlocker{}
if conf.StandardEnabled {
standard = c.standard
}
return newDefaultProfile(&defaultProfileConfig{
conf: conf,
reqPool: c.reqPool,
resPool: c.resPool,
metrics: c.metrics,
standard: standard,
})
}

View File

@@ -0,0 +1,183 @@
package access
import (
"context"
"net/netip"
"slices"
"sync"
"github.com/AdguardTeam/AdGuardDNS/internal/agdnet"
"github.com/AdguardTeam/AdGuardDNS/internal/agdurlflt"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/syncutil"
"github.com/AdguardTeam/urlfilter"
"github.com/AdguardTeam/urlfilter/filterlist"
"github.com/miekg/dns"
)
// StandardSetter is the interface for setting the standard access blocker
// configuration.
type StandardSetter interface {
// SetConfig sets the configuration for the standard access blocker. conf
// must not be nil. Fields of conf must not be modified after calling this
// method. It must be safe for concurrent use.
SetConfig(conf *StandardBlockerConfig)
}
// EmptyStandard is an empty [StandardSetter] implementation that does nothing.
type EmptyStandard struct{}
// type check
var _ StandardSetter = EmptyStandard{}
// SetConfig implements the [StandardSetter] interface for EmptyStandard. It
// always returns false.
func (EmptyStandard) SetConfig(_ *StandardBlockerConfig) {}
// StandardBlockerConfig is the configuration structure for the standard access
// blocker.
type StandardBlockerConfig struct {
// AllowedNets are the networks allowed for DNS resolution. If empty or
// nil, all networks are allowed, except those blocked by BlockedNets.
AllowedNets []netip.Prefix
// BlockedNets are the networks blocked for DNS resolution. If empty or
// nil, all networks are allowed, except those allowed by AllowedNets.
BlockedNets []netip.Prefix
// AllowedASN are the ASNs allowed for DNS resolution. If empty or nil, all
// ASNs are allowed, except those blocked by BlockedASN.
AllowedASN []geoip.ASN
// BlockedASN are the ASNs blocked for DNS resolution. If empty or nil, all
// ASNs are allowed, except those allowed by AllowedASN.
BlockedASN []geoip.ASN
// BlocklistDomainRules are the rules blocking the domains. If empty or
// nil, no domains are blocked.
BlocklistDomainRules []string
}
// StandardBlocker is the dynamic [Blocker] implementation with standard
// access settings.
type StandardBlocker struct {
reqPool *syncutil.Pool[urlfilter.DNSRequest]
resPool *syncutil.Pool[urlfilter.DNSResult]
// mu protects all fields below.
mu *sync.RWMutex
blockedHostsEng *urlfilter.DNSEngine
allowedNets []netip.Prefix
blockedNets []netip.Prefix
// TODO(d.kolyshev): Change to map[geoip.ASN]unit to improve performance.
allowedASN []geoip.ASN
blockedASN []geoip.ASN
}
// NewStandardBlocker creates a new StandardBlocker instance. conf must not be
// nil.
func NewStandardBlocker(conf *StandardBlockerConfig) (s *StandardBlocker) {
s = &StandardBlocker{
reqPool: syncutil.NewPool(func() (req *urlfilter.DNSRequest) {
return &urlfilter.DNSRequest{}
}),
resPool: syncutil.NewPool(func() (v *urlfilter.DNSResult) {
return &urlfilter.DNSResult{}
}),
mu: &sync.RWMutex{},
}
s.SetConfig(conf)
return s
}
// type check
var _ StandardSetter = (*StandardBlocker)(nil)
// SetConfig implements the [StandardSetter] interface for *StandardBlocker.
func (b *StandardBlocker) SetConfig(c *StandardBlockerConfig) {
lists := []filterlist.Interface{
filterlist.NewBytes(&filterlist.BytesConfig{
ID: blocklistFilterID,
RulesText: agdurlflt.RulesToBytesLower(c.BlocklistDomainRules),
IgnoreCosmetic: true,
}),
}
// Should never panic, since the storage has only one list.
rulesStrg := errors.Must(filterlist.NewRuleStorage(lists))
eng := urlfilter.NewDNSEngine(rulesStrg)
b.mu.Lock()
defer b.mu.Unlock()
b.blockedHostsEng = eng
b.allowedNets = c.AllowedNets
b.blockedNets = c.BlockedNets
b.allowedASN = c.AllowedASN
b.blockedASN = c.BlockedASN
}
// type check
var _ Blocker = (*StandardBlocker)(nil)
// IsBlocked implements the [Blocker] interface for *StandardBlocker.
func (b *StandardBlocker) IsBlocked(
_ context.Context,
req *dns.Msg,
rAddr netip.AddrPort,
l *geoip.Location,
) (blocked bool) {
b.mu.RLock()
defer b.mu.RUnlock()
ip := rAddr.Addr()
return b.isBlockedByNets(ip, l) || b.isBlockedByHostsEng(req)
}
// isBlockedByNets returns true if ip or l is blocked by current configuration.
func (b *StandardBlocker) isBlockedByNets(ip netip.Addr, l *geoip.Location) (blocked bool) {
if matchASNs(b.allowedASN, l) || matchNets(b.allowedNets, ip) {
return false
}
return matchASNs(b.blockedASN, l) || matchNets(b.blockedNets, ip)
}
// isBlockedByHostsEng returns true if the req is blocked by blocklist domain
// rules. req must have exactly one question.
func (b *StandardBlocker) isBlockedByHostsEng(req *dns.Msg) (blocked bool) {
q := req.Question[0]
host := agdnet.NormalizeQueryDomain(q.Name)
return matchBlocked(host, q.Qtype, b.blockedHostsEng, b.reqPool, b.resPool)
}
// Equal returns true if c and other are equal. nil is only equal to other nil.
func (c *StandardBlockerConfig) Equal(other *StandardBlockerConfig) (ok bool) {
if c == nil {
return other == nil
} else if other == nil {
return false
}
switch {
case
!slices.Equal(c.AllowedNets, other.AllowedNets),
!slices.Equal(c.BlockedNets, other.BlockedNets),
!slices.Equal(c.AllowedASN, other.AllowedASN),
!slices.Equal(c.BlockedASN, other.BlockedASN),
!slices.Equal(c.BlocklistDomainRules, other.BlocklistDomainRules):
return false
default:
return true
}
}

View File

@@ -0,0 +1,58 @@
package access_test
import (
"net/netip"
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/access"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/dnsservertest"
"github.com/AdguardTeam/golibs/testutil"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
)
func BenchmarkStandardBlocker_IsBlocked(b *testing.B) {
blocker := access.NewStandardBlocker(&access.StandardBlockerConfig{
BlocklistDomainRules: []string{
"block.test",
},
})
ctx := testutil.ContextWithTimeout(b, testTimeout)
remoteAddr := netip.AddrPort{}
b.Run("pass", func(b *testing.B) {
req := dnsservertest.NewReq("pass.test", dns.TypeA, dns.ClassINET)
var blocked bool
b.ReportAllocs()
for b.Loop() {
blocked = blocker.IsBlocked(ctx, req, remoteAddr, nil)
}
assert.False(b, blocked)
})
b.Run("block", func(b *testing.B) {
req := dnsservertest.NewReq("block.test", dns.TypeA, dns.ClassINET)
var blocked bool
b.ReportAllocs()
for b.Loop() {
blocked = blocker.IsBlocked(ctx, req, remoteAddr, nil)
}
assert.True(b, blocked)
})
// Most recent results:
//
// goos: linux
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/access
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkStandardBlocker_IsBlocked/pass-16 3009312 378.2 ns/op 16 B/op 1 allocs/op
// BenchmarkStandardBlocker_IsBlocked/block-16 2518006 421.9 ns/op 24 B/op 1 allocs/op
}

View File

@@ -0,0 +1,13 @@
package agdcache_test
import "time"
// Constants used in tests.
const (
key = "key"
val = 123
nonExistingKey = "nonExistingKey"
expDuration = 100 * time.Millisecond
)

View File

@@ -0,0 +1,126 @@
package agdcache
import (
"fmt"
"sync"
"time"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/viktordanov/golang-lru/simplelru"
)
// Config is a configuration structure of a cache.
type Config struct {
// Clock is used to get current time for expiration. It must not be nil.
Clock timeutil.Clock
// Count is the maximum number of elements to keep in the cache. It must be
// positive.
//
// TODO(a.garipov): Make uint64.
Count int
}
// entry is an entry of the cache with expiration.
type entry[T any] struct {
// val is the value of the entry.
val T
// expiration is the expiration unix time in nanoseconds. Zero means no
// expiration. It's an int64 in optimization purposes.
expiration int64
}
// Default is an implementation of a thread safe, fixed size LRU cache with
// expiration.
type Default[K comparable, T any] struct {
// cacheMu protects cache.
cacheMu *sync.RWMutex
cache *simplelru.LRU[K, entry[T]]
clock timeutil.Clock
}
// New returns a new initialized *Default cache and error, if any.
func New[K comparable, T any](conf *Config) (c *Default[K, T], err error) {
lru, err := simplelru.NewLRU[K, entry[T]](conf.Count, nil)
if err != nil {
return nil, fmt.Errorf("agdcache: creating lru: %w", err)
}
return &Default[K, T]{
cache: lru,
clock: conf.Clock,
cacheMu: &sync.RWMutex{},
}, nil
}
// type check
var _ Interface[any, any] = (*Default[any, any])(nil)
// Set implements the [Interface] interface for *Default.
func (c *Default[K, T]) Set(key K, val T) {
c.cacheMu.Lock()
defer c.cacheMu.Unlock()
// Not a pointer, but the value is used in optimization purposes.
e := entry[T]{
val: val,
}
c.cache.Add(key, e)
}
// SetWithExpire implements the [Interface] interface for *Default.
func (c *Default[K, T]) SetWithExpire(key K, val T, duration time.Duration) {
c.cacheMu.Lock()
defer c.cacheMu.Unlock()
e := entry[T]{
val: val,
expiration: c.clock.Now().Add(duration).UnixNano(),
}
c.cache.Add(key, e)
}
// Get implements the [Interface] interface for *Default. It returns the value
// and whether the key was found. Removes the key from the cache if it has
// expired.
func (c *Default[K, T]) Get(key K) (val T, ok bool) {
// TODO(a.garipov): Optimize, use RLock.
c.cacheMu.Lock()
defer c.cacheMu.Unlock()
e, ok := c.cache.Get(key)
if !ok {
return val, false
}
if e.expiration > 0 && c.clock.Now().UnixNano() > e.expiration {
c.cache.Remove(key)
return val, false
}
return e.val, true
}
// type check
var _ Clearer = (*Default[any, any])(nil)
// Clear implements the [Interface] interface for *Default.
func (c *Default[K, T]) Clear() {
c.cacheMu.Lock()
defer c.cacheMu.Unlock()
c.cache.Purge()
}
// Len implements the [Interface] interface for *Default.
func (c *Default[K, T]) Len() (n int) {
c.cacheMu.RLock()
defer c.cacheMu.RUnlock()
return c.cache.Len()
}

View File

@@ -0,0 +1,182 @@
package agdcache_test
import (
"testing"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/AdguardTeam/golibs/testutil/faketime"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/bluele/gcache"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDefault(t *testing.T) {
var (
testTimeNow = time.Now()
nowLater = testTimeNow.Add(2 * expDuration)
)
clock := &faketime.Clock{
OnNow: func() (now time.Time) { return testTimeNow },
}
cache, err := agdcache.New[string, int](&agdcache.Config{
Clock: clock,
Count: 10,
})
require.NoError(t, err)
cache.Set(key, val)
assert.Equal(t, 1, cache.Len())
v, ok := cache.Get(key)
assert.Equal(t, val, v)
assert.True(t, ok)
v, ok = cache.Get(nonExistingKey)
assert.Equal(t, 0, v)
assert.False(t, ok)
cache.Clear()
assert.Equal(t, 0, cache.Len())
cache.SetWithExpire(key, val, expDuration)
assert.Equal(t, 1, cache.Len())
v, ok = cache.Get(key)
assert.Equal(t, val, v)
assert.True(t, ok)
clock.OnNow = func() (now time.Time) { return nowLater }
v, ok = cache.Get(key)
assert.Equal(t, 0, v)
assert.False(t, ok)
assert.Equal(t, 0, cache.Len())
}
func BenchmarkDefault(b *testing.B) {
var ok bool
b.Run("set", func(b *testing.B) {
cache := newDefault(b)
b.ReportAllocs()
for i := 0; b.Loop(); i++ {
cache.Set(i, i)
_, ok = cache.Get(i)
}
assert.True(b, ok)
})
b.Run("set_expire", func(b *testing.B) {
cache := newDefault(b)
b.ReportAllocs()
for i := 0; b.Loop(); i++ {
cache.SetWithExpire(i, i, 2000)
_, ok = cache.Get(i)
}
assert.True(b, ok)
})
// Most recent results:
//
// goos: darwin
// goarch: arm64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/agdcache
// cpu: Apple M1 Pro
// BenchmarkDefault/set-8 7764472 138.6 ns/op 56 B/op 2 allocs/op
// BenchmarkDefault/set_expire-8 4727664 246.5 ns/op 56 B/op 2 allocs/op
}
func FuzzDefault(f *testing.F) {
const (
size = 1_000
secondsSeed = uint(1)
)
f.Add("key", 1, secondsSeed, 1)
f.Add("key", 1, secondsSeed, 2)
f.Add("key", 1, secondsSeed, 3)
now := time.Now()
f.Fuzz(func(t *testing.T, key string, val int, seconds uint, op int) {
clock := &faketime.Clock{
OnNow: func() (n time.Time) {
return now
},
}
cache, err := agdcache.New[string, int](&agdcache.Config{
Clock: clock,
Count: size,
})
require.NoError(t, err)
gCache := gcache.New(size).LRU().Clock(clock).Build()
switch {
case op%2 == 0:
cache.Set(key, val)
err = gCache.Set(key, val)
require.NoError(t, err)
case op%3 == 0:
dur := time.Duration(seconds) * time.Second
cache.SetWithExpire(key, val, dur)
err = gCache.SetWithExpire(key, val, dur)
require.NoError(t, err)
case op%5 == 0:
cache.Clear()
gCache.Purge()
}
clock.OnNow = func() (n time.Time) {
return now.Add(1 * time.Second)
}
got, ok := cache.Get(key)
gGot, err := gCache.Get(key)
gVal, gValOk := gGot.(int)
if !gValOk {
gVal = 0
}
require.Equalf(
t,
err == nil,
ok,
"key %q, val %d, dur %d, op %d: incorrect ok",
key, val, seconds, op,
)
require.Equalf(
t,
gVal,
got,
"key %q, val %d, dur %d, op %d: incorrect val",
key, val, seconds, op,
)
l := cache.Len()
goL := gCache.Len(false)
require.Equal(t, l, goL)
})
}
// newDefault returns a new cache for testing.
func newDefault(tb testing.TB) (cache *agdcache.Default[int, int]) {
cache, err := agdcache.New[int, int](&agdcache.Config{
Clock: timeutil.SystemClock{},
Count: 10_000,
})
require.NoError(tb, err)
return cache
}

View File

@@ -8,13 +8,6 @@ import (
)
func TestLRU(t *testing.T) {
const (
key = "key"
val = 123
nonExistingKey = "nonExistingKey"
)
cache := agdcache.NewLRU[string, int](&agdcache.LRUConfig{
Count: 10,
})
@@ -35,3 +28,71 @@ func TestLRU(t *testing.T) {
assert.Equal(t, 0, cache.Len())
}
func BenchmarkLRU(b *testing.B) {
cache := agdcache.NewLRU[int, int](&agdcache.LRUConfig{
Count: 10_000,
})
var ok bool
b.ReportAllocs()
for i := 0; b.Loop(); i++ {
cache.Set(i, i)
_, ok = cache.Get(i)
}
assert.True(b, ok)
// Most recent results:
//
// goos: darwin
// goarch: arm64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/agdcache
// cpu: Apple M1 Pro
// BenchmarkLRU-8 5104281 207.2 ns/op 136 B/op 5 allocs/op
}
func BenchmarkLRU_expire(b *testing.B) {
cache := agdcache.NewLRU[int, int](&agdcache.LRUConfig{
Count: 10_000,
})
var ok bool
b.Run("default_expire", func(b *testing.B) {
b.ReportAllocs()
for i := 0; b.Loop(); i++ {
cache.Set(i, i)
_, ok = cache.Get(i)
}
assert.True(b, ok)
// Most recent results:
//
// goos: darwin
// goarch: arm64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/agdcache
// cpu: Apple M1 Pro
// BenchmarkLRU_expire/default_expire-8 4883622 208.6 ns/op 136 B/op 5 allocs/op
})
b.Run("set_expire", func(b *testing.B) {
b.ReportAllocs()
for i := 0; b.Loop(); i++ {
cache.SetWithExpire(i, i, 2000)
_, ok = cache.Get(i)
}
assert.True(b, ok)
// Most recent results:
//
// goos: darwin
// goarch: arm64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/agdcache
// cpu: Apple M1 Pro
// BenchmarkLRU_expire/set_expire-8 3620727 328.7 ns/op 160 B/op 5 allocs/op
})
}

View File

@@ -2,7 +2,6 @@ package agdtest
import (
"context"
"fmt"
"net"
"net/netip"
"time"
@@ -23,6 +22,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/remotekv"
"github.com/AdguardTeam/AdGuardDNS/internal/rulestat"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
)
@@ -191,8 +191,8 @@ func (c *ErrorCollector) Collect(ctx context.Context, err error) {
// NewErrorCollector returns a new *ErrorCollector all methods of which panic.
func NewErrorCollector() (c *ErrorCollector) {
return &ErrorCollector{
OnCollect: func(_ context.Context, err error) {
panic(fmt.Errorf("unexpected call to ErrorCollector.Collect(%v)", err))
OnCollect: func(ctx context.Context, err error) {
panic(testutil.UnexpectedCall(ctx, err))
},
}
}
@@ -291,13 +291,13 @@ func (g *GeoIP) SubnetByLocation(
func NewGeoIP() (c *GeoIP) {
return &GeoIP{
OnData: func(host string, ip netip.Addr) (l *geoip.Location, err error) {
panic(fmt.Errorf("unexpected call to GeoIP.Data(%v, %v)", host, ip))
panic(testutil.UnexpectedCall(host, ip))
},
OnSubnetByLocation: func(
l *geoip.Location,
fam netutil.AddrFamily,
) (n netip.Prefix, err error) {
panic(fmt.Errorf("unexpected call to GeoIP.SubnetByLocation(%v, %v)", l, fam))
panic(testutil.UnexpectedCall(l, fam))
},
}
}
@@ -390,50 +390,41 @@ func (db *ProfileDB) ProfileByLinkedIP(
func NewProfileDB() (db *ProfileDB) {
return &ProfileDB{
OnCreateAutoDevice: func(
_ context.Context,
ctx context.Context,
id agd.ProfileID,
humanID agd.HumanID,
devType agd.DeviceType,
) (p *agd.Profile, d *agd.Device, err error) {
panic(fmt.Errorf(
"unexpected call to ProfileDB.CreateAutoDevice(%v, %v, %v)",
id,
humanID,
devType,
))
panic(testutil.UnexpectedCall(ctx, id, humanID, devType))
},
OnProfileByDedicatedIP: func(
_ context.Context,
ctx context.Context,
ip netip.Addr,
) (p *agd.Profile, d *agd.Device, err error) {
panic(fmt.Errorf("unexpected call to ProfileDB.ProfileByDedicatedIP(%v)", ip))
panic(testutil.UnexpectedCall(ctx, ip))
},
OnProfileByDeviceID: func(
_ context.Context,
ctx context.Context,
id agd.DeviceID,
) (p *agd.Profile, d *agd.Device, err error) {
panic(fmt.Errorf("unexpected call to ProfileDB.ProfileByDeviceID(%v)", id))
panic(testutil.UnexpectedCall(ctx, id))
},
OnProfileByHumanID: func(
_ context.Context,
ctx context.Context,
profID agd.ProfileID,
humanID agd.HumanIDLower,
) (p *agd.Profile, d *agd.Device, err error) {
panic(fmt.Errorf(
"unexpected call to ProfileDB.ProfileByHumanID(%v, %v)",
profID,
humanID,
))
panic(testutil.UnexpectedCall(ctx, profID, humanID))
},
OnProfileByLinkedIP: func(
_ context.Context,
ctx context.Context,
ip netip.Addr,
) (p *agd.Profile, d *agd.Device, err error) {
panic(fmt.Errorf("unexpected call to ProfileDB.ProfileByLinkedIP(%v)", ip))
panic(testutil.UnexpectedCall(ctx, ip))
},
}
}
@@ -475,16 +466,16 @@ func (s *ProfileStorage) Profiles(
func NewProfileStorage() (s *ProfileStorage) {
return &ProfileStorage{
OnCreateAutoDevice: func(
_ context.Context,
ctx context.Context,
req *profiledb.StorageCreateAutoDeviceRequest,
) (resp *profiledb.StorageCreateAutoDeviceResponse, err error) {
panic(fmt.Errorf("unexpected call to ProfileStorage.CreateAutoDevice(%v)", req))
panic(testutil.UnexpectedCall(ctx, req))
},
OnProfiles: func(
_ context.Context,
ctx context.Context,
req *profiledb.StorageProfilesRequest,
) (resp *profiledb.StorageProfilesResponse, err error) {
panic(fmt.Errorf("unexpected call to ProfileStorage.Profiles(%v)", req))
panic(testutil.UnexpectedCall(ctx, req))
},
}
}
@@ -587,14 +578,14 @@ func (l *RateLimit) CountResponses(ctx context.Context, req *dns.Msg, ip netip.A
func NewRateLimit() (c *RateLimit) {
return &RateLimit{
OnIsRateLimited: func(
_ context.Context,
ctx context.Context,
req *dns.Msg,
addr netip.Addr,
) (shouldDrop, isAllowlisted bool, err error) {
panic(fmt.Errorf("unexpected call to RateLimit.IsRateLimited(%v, %v)", req, addr))
panic(testutil.UnexpectedCall(ctx, req, addr))
},
OnCountResponses: func(_ context.Context, resp *dns.Msg, addr netip.Addr) {
panic(fmt.Errorf("unexpected call to RateLimit.CountResponses(%v, %v)", resp, addr))
OnCountResponses: func(ctx context.Context, resp *dns.Msg, addr netip.Addr) {
panic(testutil.UnexpectedCall(ctx, resp, addr))
},
}
}

View File

@@ -0,0 +1,31 @@
package agdtest
import (
"reflect"
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/access"
gocmp "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
)
// AssertEqualProfile compares two values while ignoring internal details of
// some fields of profiles, such as pools.
func AssertEqualProfile(tb testing.TB, want, got any) (ok bool) {
tb.Helper()
exportAll := gocmp.Exporter(func(_ reflect.Type) (ok bool) { return true })
defAccCmp := gocmp.Comparer(func(want, got *access.DefaultProfile) (ok bool) {
return gocmp.Equal(want.Config(), got.Config(), exportAll)
})
diff := gocmp.Diff(want, got, defAccCmp, exportAll)
if diff == "" {
return true
}
// Use assert.Failf instead of tb.Errorf to get a more consistent error
// message.
return assert.Failf(tb, "not equal", "got: %+v\nwant: %+v\ndiff: %s", got, want, diff)
}

View File

@@ -0,0 +1,68 @@
// Package agdurlflt contains utilities for the urlfilter module.
package agdurlflt
import (
"bytes"
"unicode"
)
// RulesLen returns the length of the byte buffer necessary to write ruleStrs,
// separated by a newline, to it.
func RulesLen[S ~string](ruleStrs []S) (l int) {
if len(ruleStrs) == 0 {
return 0
}
for _, s := range ruleStrs {
l += len(s) + len("\n")
}
return l
}
// RulesToBytes writes ruleStrs to a byte slice and returns it.
//
// TODO(a.garipov): Consider moving to golibs or urlfilter.
func RulesToBytes[S ~string](ruleStrs []S) (b []byte) {
l := RulesLen(ruleStrs)
if l == 0 {
return nil
}
buf := bytes.NewBuffer(make([]byte, 0, l))
for _, s := range ruleStrs {
_, _ = buf.WriteString(string(s))
_ = buf.WriteByte('\n')
}
return buf.Bytes()
}
// RulesToBytesLower writes lowercase versions of ruleStrs to a byte slice and
// returns it.
//
// NOTE: Do not use this for rules that can include dnsrewrite modifiers, since
// their DNS types are case-sensitive.
//
// TODO(a.garipov): Consider moving to golibs or urlfilter.
func RulesToBytesLower(ruleStrs []string) (b []byte) {
l := RulesLen(ruleStrs)
if l == 0 {
return nil
}
buf := bytes.NewBuffer(make([]byte, 0, l))
for _, s := range ruleStrs {
for _, c := range s {
// NOTE: Theoretically there might be cases where a lowercase
// version of a rune takes up more space or less space than an
// uppercase one, but that doesn't matter since we're using a
// bytes.Buffer and rules generally are ASCII-only.
_, _ = buf.WriteRune(unicode.ToLower(c))
}
_ = buf.WriteByte('\n')
}
return buf.Bytes()
}

View File

@@ -0,0 +1,57 @@
package agdurlflt_test
import (
"strings"
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agdurlflt"
"github.com/stretchr/testify/require"
)
// testRulesStrs are the common filtering rules for tests.
var testRulesStrs = []string{
`||blocked.example^`,
`@@||allowed.example^`,
`||dnsrewrite.example^$dnsrewrite=192.0.2.1`,
}
// testRulesData is the data of [testRulesStrs] as bytes.
var testRulesData = []byte(strings.Join(testRulesStrs, "\n") + "\n")
func BenchmarkRulesToBytes(b *testing.B) {
var got []byte
b.ReportAllocs()
for b.Loop() {
got = agdurlflt.RulesToBytes(testRulesStrs)
}
require.Equal(b, testRulesData, got)
// Most recent results:
//
// goos: linux
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/agdurlflt
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkRulesToBytes-16 7925872 145.3 ns/op 96 B/op 1 allocs/op
}
func BenchmarkRulesToBytesLower(b *testing.B) {
var got []byte
b.ReportAllocs()
for b.Loop() {
got = agdurlflt.RulesToBytesLower(testRulesStrs)
}
require.Equal(b, testRulesData, got)
// Most recent results:
//
// goos: linux
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/agdurlflt
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkRulesToBytesLower-16 1000000 1188 ns/op 96 B/op 1 allocs/op
}

View File

@@ -58,4 +58,7 @@ var TestLogger = slogutil.NewDiscardLogger()
// TestProfileAccessConstructor is the common constructor of profile access
// managers for tests
var TestProfileAccessConstructor = access.NewProfileConstructor(access.EmptyProfileMetrics{})
var TestProfileAccessConstructor = access.NewProfileConstructor(&access.ProfileConstructorConfig{
Metrics: access.EmptyProfileMetrics{},
Standard: access.EmptyBlocker{},
})

View File

@@ -47,14 +47,14 @@ func TestBillStat_Upload(t *testing.T) {
ctx context.Context,
req *backendpb.CreateDeviceRequest,
) (resp *backendpb.CreateDeviceResponse, err error) {
panic("not implemented")
panic(testutil.UnexpectedCall(ctx, req))
},
OnGetDNSProfiles: func(
req *backendpb.DNSProfilesRequest,
srv grpc.ServerStreamingServer[backendpb.DNSProfile],
) (err error) {
panic("not implemented")
panic(testutil.UnexpectedCall(req, srv))
},
OnSaveDevicesBillingStat: func(

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.6
// protoc v6.31.0
// protoc-gen-go v1.36.8
// protoc v6.32.0
// source: dns.proto
package backendpb
@@ -1056,7 +1056,7 @@ func (x *ParentalSettings) GetSchedule() *ScheduleSettings {
type ScheduleSettings struct {
state protoimpl.MessageState `protogen:"open.v1"`
Tmz string `protobuf:"bytes,1,opt,name=tmz,proto3" json:"tmz,omitempty"`
WeeklyRange *WeeklyRange `protobuf:"bytes,2,opt,name=weeklyRange,proto3" json:"weeklyRange,omitempty"`
WeeklyRange *WeeklyRange `protobuf:"bytes,2,opt,name=weekly_range,json=weeklyRange,proto3" json:"weekly_range,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -2810,7 +2810,7 @@ var File_dns_proto protoreflect.FileDescriptor
const file_dns_proto_rawDesc = "" +
"\n" +
"\tdns.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1bgoogle/protobuf/empty.proto\"\x1a\n" +
"\tdns.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x1a\n" +
"\x18RateLimitSettingsRequest\"P\n" +
"\x19RateLimitSettingsResponse\x123\n" +
"\x0fallowed_subnets\x18\x01 \x03(\v2\n" +
@@ -2897,10 +2897,10 @@ const file_dns_proto_rawDesc = "" +
"\x13general_safe_search\x18\x03 \x01(\bR\x11generalSafeSearch\x12.\n" +
"\x13youtube_safe_search\x18\x04 \x01(\bR\x11youtubeSafeSearch\x12)\n" +
"\x10blocked_services\x18\x05 \x03(\tR\x0fblockedServices\x12-\n" +
"\bschedule\x18\x06 \x01(\v2\x11.ScheduleSettingsR\bschedule\"T\n" +
"\bschedule\x18\x06 \x01(\v2\x11.ScheduleSettingsR\bschedule\"U\n" +
"\x10ScheduleSettings\x12\x10\n" +
"\x03tmz\x18\x01 \x01(\tR\x03tmz\x12.\n" +
"\vweeklyRange\x18\x02 \x01(\v2\f.WeeklyRangeR\vweeklyRange\"\xd8\x01\n" +
"\x03tmz\x18\x01 \x01(\tR\x03tmz\x12/\n" +
"\fweekly_range\x18\x02 \x01(\v2\f.WeeklyRangeR\vweeklyRange\"\xd8\x01\n" +
"\vWeeklyRange\x12\x1b\n" +
"\x03mon\x18\x01 \x01(\v2\t.DayRangeR\x03mon\x12\x1b\n" +
"\x03tue\x18\x02 \x01(\v2\t.DayRangeR\x03tue\x12\x1b\n" +
@@ -3019,8 +3019,7 @@ const file_dns_proto_rawDesc = "" +
"\x13CustomDomainService\x12_\n" +
"\x1agetCustomDomainCertificate\x12\x1f.CustomDomainCertificateRequest\x1a .CustomDomainCertificateResponse2Z\n" +
"\x14SessionTicketService\x12B\n" +
"\x11getSessionTickets\x12\x15.SessionTicketRequest\x1a\x16.SessionTicketResponseB=\n" +
"!com.adguard.backend.dns.generatedB\x10DNSProfilesProtoP\x01\xa2\x02\x03DNSb\x06proto3"
"\x11getSessionTickets\x12\x15.SessionTicketRequest\x1a\x16.SessionTicketResponseb\x06proto3"
var (
file_dns_proto_rawDescOnce sync.Once
@@ -3111,7 +3110,7 @@ var file_dns_proto_depIdxs = []int32{
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.weeklyRange:type_name -> WeeklyRange
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

View File

@@ -1,118 +1,107 @@
syntax = "proto3";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
option java_multiple_files = true;
option java_package = "com.adguard.backend.dns.generated";
option java_outer_classname = "DNSProfilesProto";
option objc_class_prefix = "DNS";
// TODO(a.garipov): Expand the documentation and make it consistent.
service DNSService {
/*
Gets DNS profiles.
Gets DNS profiles.
Field "sync_time" in DNSProfilesRequest - pass to return the latest updates after this time moment.
Field "sync_time" in DNSProfilesRequest - pass to return the latest updates after this time moment.
The trailers headers will include a "sync_time", given in milliseconds,
that should be used for subsequent incremental DNS profile synchronization requests.
The trailers headers will include a "sync_time", given in milliseconds,
that should be used for subsequent incremental DNS profile synchronization requests.
This method may return the following errors:
- RateLimitedError: If too many "full sync" concurrent requests are made.
- AuthenticationFailedError: If the authentication failed.
This method may return the following errors:
- RateLimitedError: If too many "full sync" concurrent requests are made.
- AuthenticationFailedError: If the authentication failed.
*/
rpc getDNSProfiles(DNSProfilesRequest) returns (stream DNSProfile);
/*
Stores devices activity.
Stores devices activity.
This method may return the following errors:
- AuthenticationFailedError: If the authentication failed.
This method may return the following errors:
- AuthenticationFailedError: If the authentication failed.
*/
rpc saveDevicesBillingStat(stream DeviceBillingStat) returns (google.protobuf.Empty);
/*
Create device by "human_id".
Create device by "human_id".
This method may return the following errors:
- RateLimitedError: If the request was made too frequently and the client must wait before retrying.
- DeviceQuotaExceededError: If the client has exceeded its quota for creating devices.
- BadRequestError: If the request is invalid: DNS server does not exist, creation of auto-devices is disabled or human_id validation failed.
- AuthenticationFailedError: If the authentication failed.
This method may return the following errors:
- RateLimitedError: If the request was made too frequently and the client must wait before retrying.
- DeviceQuotaExceededError: If the client has exceeded its quota for creating devices.
- BadRequestError: If the request is invalid: DNS server does not exist, creation of auto-devices is disabled or human_id validation failed.
- AuthenticationFailedError: If the authentication failed.
*/
rpc createDeviceByHumanId(CreateDeviceRequest) returns (CreateDeviceResponse);
}
service RateLimitService {
/*
Gets rate limit settings.
Gets rate limit settings.
*/
rpc getRateLimitSettings(RateLimitSettingsRequest) returns (RateLimitSettingsResponse);
/*
Gets global access settings.
Gets global access settings.
*/
rpc getGlobalAccessSettings(GlobalAccessSettingsRequest) returns (GlobalAccessSettingsResponse);
}
service RemoteKVService {
/**
Get the value for the specified key.
Get the value for the specified key.
This method may return the following errors:
- AuthenticationFailedError: If the authentication failed.
*/
This method may return the following errors:
- AuthenticationFailedError: If the authentication failed.
*/
rpc get(RemoteKVGetRequest) returns (RemoteKVGetResponse);
/**
Set the value for the specified key.
Set the value for the specified key.
This method may return the following errors:
- AuthenticationFailedError: If the authentication failed.
- BadRequestError: If the request is invalid: value size exceeds the 512kb.
*/
This method may return the following errors:
- AuthenticationFailedError: If the authentication failed.
- BadRequestError: If the request is invalid: value size exceeds the 512kb.
*/
rpc set(RemoteKVSetRequest) returns (RemoteKVSetResponse);
}
service CustomDomainService {
/*
Get certificate for custom domain.
Get certificate for custom domain.
This method may return the following errors:
- AuthenticationFailedError: If the authentication failed.
- BadRequestError: If the request is invalid: cert_name is empty.
- NotFoundError: If the certificate could not be found.
This method may return the following errors:
- AuthenticationFailedError: If the authentication failed.
- BadRequestError: If the request is invalid: cert_name is empty or no certificate found.
- NotFoundError: If the certificate was not found.
- RateLimitedError: If the request was made too frequently and the client must wait before retrying.
*/
rpc getCustomDomainCertificate(CustomDomainCertificateRequest) returns (CustomDomainCertificateResponse);
}
service SessionTicketService {
/*
Gets session ticket for the current date
/*
Gets session ticket for the current date
This method may return the following errors:
- AuthenticationFailedError: If the authentication failed.
*/
rpc getSessionTickets(SessionTicketRequest) returns (SessionTicketResponse);
This method may return the following errors:
- AuthenticationFailedError: If the authentication failed.
*/
rpc getSessionTickets(SessionTicketRequest) returns (SessionTicketResponse);
}
message RateLimitSettingsRequest {
}
message RateLimitSettingsRequest {}
message RateLimitSettingsResponse {
repeated CidrRange allowed_subnets = 1;
}
message GlobalAccessSettingsRequest {
}
message GlobalAccessSettingsRequest {}
message GlobalAccessSettingsResponse {
AccessSettings standard = 1;
@@ -164,18 +153,17 @@ message DNSProfile {
}
message DeviceSettingsChange {
message Deleted {
string device_id = 1;
string device_id = 1;
}
message Upserted {
DeviceSettings device = 1;
DeviceSettings device = 1;
}
oneof change {
Deleted deleted = 1;
Upserted upserted = 2;
Deleted deleted = 1;
Upserted upserted = 2;
}
}
@@ -237,7 +225,7 @@ message ParentalSettings {
message ScheduleSettings {
string tmz = 1;
WeeklyRange weeklyRange = 2;
WeeklyRange weekly_range = 2;
}
message WeeklyRange {
@@ -369,9 +357,7 @@ message RemoteKVSetRequest {
google.protobuf.Duration ttl = 3;
}
message RemoteKVSetResponse {
}
message RemoteKVSetResponse {}
message CustomDomainCertificateRequest {
string cert_name = 1;
@@ -385,10 +371,10 @@ message CustomDomainCertificateResponse {
message SessionTicketRequest {}
message SessionTicketResponse {
repeated SessionTicket tickets = 1;
repeated SessionTicket tickets = 1;
}
message SessionTicket {
string name = 1;
bytes data = 2;
string name = 1;
bytes data = 2;
}

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v6.31.0
// - protoc v6.32.0
// source: dns.proto
package backendpb
@@ -554,8 +554,9 @@ type CustomDomainServiceClient interface {
//
// This method may return the following errors:
// - AuthenticationFailedError: If the authentication failed.
// - BadRequestError: If the request is invalid: cert_name is empty.
// - NotFoundError: If the certificate could not be found.
// - BadRequestError: If the request is invalid: cert_name is empty or no certificate found.
// - NotFoundError: If the certificate was not found.
// - RateLimitedError: If the request was made too frequently and the client must wait before retrying.
GetCustomDomainCertificate(ctx context.Context, in *CustomDomainCertificateRequest, opts ...grpc.CallOption) (*CustomDomainCertificateResponse, error)
}
@@ -585,8 +586,9 @@ type CustomDomainServiceServer interface {
//
// This method may return the following errors:
// - AuthenticationFailedError: If the authentication failed.
// - BadRequestError: If the request is invalid: cert_name is empty.
// - NotFoundError: If the certificate could not be found.
// - BadRequestError: If the request is invalid: cert_name is empty or no certificate found.
// - NotFoundError: If the certificate was not found.
// - RateLimitedError: If the request was made too frequently and the client must wait before retrying.
GetCustomDomainCertificate(context.Context, *CustomDomainCertificateRequest) (*CustomDomainCertificateResponse, error)
mustEmbedUnimplementedCustomDomainServiceServer()
}

View File

@@ -182,3 +182,22 @@ func (EmptyTicketStorageMetrics) SetTicketsState(_ context.Context, _ float64) {
// ObserveUpdate implements the [TicketStorageMetrics] interface for
// EmptyTicketStorageMetrics.
func (EmptyTicketStorageMetrics) ObserveUpdate(_ context.Context, _ time.Duration, _ error) {}
// StandardAccessMetrics is an interface that is used for the collection of
// standard access statistics.
type StandardAccessMetrics interface {
// ObserveUpdate sets the duration of the standard access settings update
// operation.
ObserveUpdate(ctx context.Context, dur time.Duration, err error)
}
// EmptyStandardAccessMetrics is the implementation of the
// [StandardAccessMetrics] interface that does nothing.
type EmptyStandardAccessMetrics struct{}
// type check
var _ StandardAccessMetrics = EmptyStandardAccessMetrics{}
// ObserveUpdate implements the [StandardAccessMetrics] interface for
// EmptyStandardAccessMetrics.
func (EmptyStandardAccessMetrics) ObserveUpdate(_ context.Context, _ time.Duration, _ error) {}

View File

@@ -88,6 +88,7 @@ func (x *AccessSettings) toInternal(
logger *slog.Logger,
errColl errcoll.Interface,
cons *access.ProfileConstructor,
standardEnabled bool,
) (a access.Profile) {
if x == nil || !x.Enabled {
return access.EmptyProfile{}
@@ -99,9 +100,32 @@ func (x *AccessSettings) toInternal(
AllowedASN: asnToInternal(x.AllowlistAsn),
BlockedASN: asnToInternal(x.BlocklistAsn),
BlocklistDomainRules: x.BlocklistDomainRules,
StandardEnabled: standardEnabled,
})
}
// toStandardConfig converts protobuf access settings to an internal structure.
// If x is nil, toStandardConfig returns nil.
func (x *AccessSettings) toStandardConfig(
ctx context.Context,
logger *slog.Logger,
errColl errcoll.Interface,
) (a *access.StandardBlockerConfig) {
if x == nil || !x.Enabled {
logger.WarnContext(ctx, "received disabled standard access settings")
return nil
}
return &access.StandardBlockerConfig{
AllowedNets: cidrRangeToInternal(ctx, errColl, logger, x.AllowlistCidr),
BlockedNets: cidrRangeToInternal(ctx, errColl, logger, x.BlocklistCidr),
AllowedASN: asnToInternal(x.AllowlistAsn),
BlockedASN: asnToInternal(x.BlocklistAsn),
BlocklistDomainRules: x.BlocklistDomainRules,
}
}
// cidrRangeToInternal is a helper that converts a slice of CidrRange to the
// slice of [netip.Prefix].
func cidrRangeToInternal(

View File

@@ -304,6 +304,14 @@ 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...),
@@ -313,7 +321,7 @@ func (s *ProfileStorage) newProfile(
RuleList: p.RuleLists.toInternal(ctx, s.errColl, s.logger),
SafeBrowsing: p.SafeBrowsing.toInternal(),
},
Access: p.Access.toInternal(ctx, s.logger, s.errColl, s.profAccessCons),
Access: accessProf,
BlockingMode: m,
Ratelimiter: p.RateLimit.toInternal(ctx, s.errColl, s.logger, s.respSzEst),
AccountID: accID,

View File

@@ -56,7 +56,7 @@ func TestProfileStorage_NewProfile(t *testing.T) {
)
require.NoError(t, err)
assert.Equal(t, newProfile(t), got)
agdtest.AssertEqualProfile(t, newProfile(t), got)
assert.Equal(t, newDevices(t), gotDevices)
assert.Equal(t, wantDevChg, gotDevChg)
})
@@ -95,7 +95,7 @@ func TestProfileStorage_NewProfile(t *testing.T) {
errCollErr,
)
assert.Equal(t, newProfile(t), got)
agdtest.AssertEqualProfile(t, newProfile(t), got)
assert.Equal(t, newDevices(t), gotDevices)
assert.Equal(t, wantDevChg, gotDevChg)
})
@@ -135,7 +135,10 @@ func TestProfileStorage_NewProfile(t *testing.T) {
errCollErr,
)
assert.NotEqual(t, newProfile(t), got)
wantProf := newProfile(t)
wantProf.DeviceIDs.Delete(TestDeviceID)
agdtest.AssertEqualProfile(t, wantProf, got)
assert.NotEqual(t, newDevices(t), gotDevices)
assert.Len(t, gotDevices, 3)
assert.Equal(t, wantDevChg, gotDevChg)
@@ -250,7 +253,7 @@ func TestProfileStorage_NewProfile(t *testing.T) {
wantProf := newProfile(t)
wantProf.BlockingMode = &dnsmsg.BlockingModeNullIP{}
assert.Equal(t, wantProf, got)
agdtest.AssertEqualProfile(t, wantProf, got)
assert.Equal(t, newDevices(t), gotDevices)
assert.Equal(t, wantDevChg, gotDevChg)
})

View File

@@ -52,13 +52,13 @@ func TestProfileStorage_CreateAutoDevice(t *testing.T) {
req *backendpb.DNSProfilesRequest,
srv grpc.ServerStreamingServer[backendpb.DNSProfile],
) (err error) {
panic("not implemented")
panic(testutil.UnexpectedCall(req, srv))
},
OnSaveDevicesBillingStat: func(
srv grpc.ClientStreamingServer[backendpb.DeviceBillingStat, emptypb.Empty],
) (err error) {
panic("not implemented")
panic(testutil.UnexpectedCall(srv))
},
}
@@ -114,10 +114,10 @@ func BenchmarkProfileStorage_Profiles(b *testing.B) {
srv := &testDNSServiceServer{
OnCreateDeviceByHumanId: func(
_ context.Context,
_ *backendpb.CreateDeviceRequest,
) (_ *backendpb.CreateDeviceResponse, _ error) {
panic("not implemented")
ctx context.Context,
req *backendpb.CreateDeviceRequest,
) (resp *backendpb.CreateDeviceResponse, err error) {
panic(testutil.UnexpectedCall(ctx, req))
},
OnGetDNSProfiles: func(
@@ -131,9 +131,9 @@ func BenchmarkProfileStorage_Profiles(b *testing.B) {
},
OnSaveDevicesBillingStat: func(
_ grpc.ClientStreamingServer[backendpb.DeviceBillingStat, emptypb.Empty],
) (_ error) {
panic("not implemented")
srv grpc.ClientStreamingServer[backendpb.DeviceBillingStat, emptypb.Empty],
) (err error) {
panic(testutil.UnexpectedCall(srv))
},
}

View File

@@ -2,7 +2,6 @@ package backendpb_test
import (
"context"
"fmt"
"net/netip"
"testing"
@@ -38,10 +37,10 @@ func TestRateLimiter_Refresh(t *testing.T) {
},
// TODO(e.burkov): Use and test.
OnGetGlobalAccessSettings: func(
_ context.Context,
_ *backendpb.GlobalAccessSettingsRequest,
) (_ *backendpb.GlobalAccessSettingsResponse, _ error) {
panic(fmt.Errorf("unexpected call to GetGlobalAccessSettings"))
ctx context.Context,
req *backendpb.GlobalAccessSettingsRequest,
) (resp *backendpb.GlobalAccessSettingsResponse, err error) {
panic(testutil.UnexpectedCall(ctx, req))
},
}

View File

@@ -0,0 +1,96 @@
package backendpb
import (
"context"
"fmt"
"log/slog"
"net/url"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/access"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/filterstorage"
)
// StandardAccessConfig is the configuration structure for the business logic
// backend standard profile access service.
type StandardAccessConfig struct {
// Logger is used for logging the operation of the standard access service.
// It must not be nil.
Logger *slog.Logger
// GRPCMetrics is used for the collection of the protobuf communication
// statistics.
GRPCMetrics GRPCMetrics
// Metrics is used to collect standard access service statistics.
Metrics StandardAccessMetrics
// ErrColl is used to collect errors during procedure calls.
ErrColl errcoll.Interface
// Endpoint is the backend API URL. The scheme should be either "grpc" or
// "grpcs". It must not be nil.
Endpoint *url.URL
// APIKey is the API key used for authentication, if any. If empty, no
// authentication is performed.
APIKey string
}
// StandardAccess is the implementation of the [service.Refresher] interface
// that retrieves the standard access settings from the business logic backend.
type StandardAccess struct {
logger *slog.Logger
grpcMetrics GRPCMetrics
metrics StandardAccessMetrics
errColl errcoll.Interface
client RateLimitServiceClient
apiKey string
}
// NewStandardAccess creates a new properly initialized standard access service.
// c must not be nil.
func NewStandardAccess(c *StandardAccessConfig) (a *StandardAccess, err error) {
client, err := newClient(c.Endpoint)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
return &StandardAccess{
logger: c.Logger,
grpcMetrics: c.GRPCMetrics,
metrics: c.Metrics,
errColl: c.ErrColl,
client: NewRateLimitServiceClient(client),
apiKey: c.APIKey,
}, nil
}
// type check
var _ filterstorage.StandardAccessStorage = (*StandardAccess)(nil)
// Config retrieves the standard access settings from the business logic
// backend.
func (a *StandardAccess) Config(ctx context.Context) (c *access.StandardBlockerConfig, err error) {
ctx = ctxWithAuthentication(ctx, a.apiKey)
req := &GlobalAccessSettingsRequest{}
start := time.Now()
defer func() {
// TODO(e.burkov): Consider separating metrics for networking and
// decoding.
a.metrics.ObserveUpdate(ctx, time.Since(start), err)
}()
resp, err := a.client.GetGlobalAccessSettings(ctx, req)
if err != nil {
return nil, fmt.Errorf(
"loading global access settings: %w",
fixGRPCError(ctx, a.grpcMetrics, err),
)
}
return resp.GetStandard().toStandardConfig(ctx, a.logger, a.errColl), nil
}

View File

@@ -225,7 +225,7 @@ var _ service.Interface = (*Manager)(nil)
//
// TODO(a.garipov): Consider an interface solution instead of the nil exception.
//
// TODO(a.garipov): Use the context for cancelation.
// TODO(a.garipov): Use the context for cancellation.
func (m *Manager) Start(ctx context.Context) (err error) {
if m == nil {
return nil
@@ -262,7 +262,7 @@ func (m *Manager) Start(ctx context.Context) (err error) {
//
// TODO(a.garipov): Consider waiting for all sockets to close.
//
// TODO(a.garipov): Use the context for cancelation.
// TODO(a.garipov): Use the context for cancellation.
//
// TODO(a.garipov): Consider an interface solution instead of the nil exception.
func (m *Manager) Shutdown(_ context.Context) (err error) {

View File

@@ -406,6 +406,7 @@ func TestListenControlWithSO(t *testing.T) {
)
require.NotNil(t, lc)
// TODO(a.garipov): Move to golibs.
type syscallConner interface {
SyscallConn() (c syscall.RawConn, err error)
}
@@ -414,9 +415,10 @@ func TestListenControlWithSO(t *testing.T) {
c, err := lc.ListenPacket(context.Background(), "udp", "0.0.0.0:0")
require.NoError(t, err)
require.NotNil(t, c)
require.Implements(t, (*syscallConner)(nil), c)
sc, err := c.(syscallConner).SyscallConn()
scConner := testutil.RequireTypeAssert[syscallConner](t, c)
sc, err := scConner.SyscallConn()
require.NoError(t, err)
err = sc.Control(func(fd uintptr) {
@@ -440,9 +442,10 @@ func TestListenControlWithSO(t *testing.T) {
c, err := lc.Listen(context.Background(), "tcp", "0.0.0.0:0")
require.NoError(t, err)
require.NotNil(t, c)
require.Implements(t, (*syscallConner)(nil), c)
sc, err := c.(syscallConner).SyscallConn()
scConner := testutil.RequireTypeAssert[syscallConner](t, c)
sc, err := scConner.SyscallConn()
require.NoError(t, err)
err = sc.Control(func(fd uintptr) {

View File

@@ -6,6 +6,12 @@ import (
"github.com/AdguardTeam/golibs/validate"
)
// Possible values of the STANDARD_ACCESS_TYPE environment variable.
const (
standardAccessOff = "off"
standardAccessBackend = "backend"
)
// accessConfig is the configuration that controls IP and hosts blocking.
type accessConfig struct {
// BlockedQuestionDomains is a list of AdBlock rules used to block access.

View File

@@ -57,16 +57,17 @@ import (
// Constants that define debug identifiers for the debug HTTP service.
const (
debugIDAllowlist = "allowlist"
debugIDBillStat = "billstat"
debugIDCustomDomainDB = "custom_domain_db"
debugIDGeoIP = "geoip"
debugIDProfileDB = "profiledb"
debugIDProfileDBFull = "profiledb_full"
debugIDRuleStat = "rulestat"
debugIDTLSConfig = "tlsconfig"
debugIDTicketRotator = "ticket_rotator"
debugIDWebSvc = "websvc"
debugIDAllowlist = "allowlist"
debugIDBillStat = "billstat"
debugIDCustomDomainDB = "custom_domain_db"
debugIDGeoIP = "geoip"
debugIDProfileDB = "profiledb"
debugIDProfileDBFull = "profiledb_full"
debugIDRuleStat = "rulestat"
debugIDStandardProfileAccess = "standard_profile_access"
debugIDTLSConfig = "tlsconfig"
debugIDTicketRotator = "ticket_rotator"
debugIDWebSvc = "websvc"
// debugIDPrefixPlugin is the prefix for plugin debug identifiers.
debugIDPrefixPlugin = "plugin/"
@@ -97,6 +98,7 @@ type builder struct {
promRegisterer prometheus.Registerer
rand *rand.Rand
sigHdlr *service.SignalHandler
standardAccess access.Blocker
// The fields below are initialized later by calling the builder's methods.
// Keep them sorted.
@@ -318,9 +320,9 @@ func (b *builder) initAdultBlocking(
return nil
}
b.adultBlockingHashes, err = hashprefix.NewStorage("")
b.adultBlockingHashes, err = hashprefix.NewStorage(nil)
if err != nil {
// Don't expect errors here because we pass an empty string.
// Expect no errors here because we pass a nil.
panic(err)
}
@@ -420,7 +422,7 @@ func (b *builder) initNewRegDomains(
return nil
}
b.newRegDomainsHashes, err = hashprefix.NewStorage("")
b.newRegDomainsHashes, err = hashprefix.NewStorage(nil)
if err != nil {
// Don't expect errors here because we pass an empty string.
panic(err)
@@ -508,7 +510,7 @@ func (b *builder) initSafeBrowsing(
return nil
}
b.safeBrowsingHashes, err = hashprefix.NewStorage("")
b.safeBrowsingHashes, err = hashprefix.NewStorage(nil)
if err != nil {
// Don't expect errors here because we pass an empty string.
panic(err)
@@ -581,6 +583,83 @@ func (b *builder) initSafeBrowsing(
return nil
}
// initStandardAccess initializes the standard access settings.
//
// The following methods must be called before this one:
// - [builder.initGRPCMetrics]
func (b *builder) initStandardAccess(ctx context.Context) (err error) {
switch typ := b.env.StandardAccessType; typ {
case standardAccessOff:
b.standardAccess = access.EmptyBlocker{}
return nil
case standardAccessBackend:
// Go on.
//
// TODO(e.burkov): Extract the initialization logic to a separate
// function.
default:
panic(fmt.Errorf("env STANDARD_ACCESS_TYPE: %w: %q", errors.ErrBadEnumValue, typ))
}
stdAcc := access.NewStandardBlocker(&access.StandardBlockerConfig{})
b.standardAccess = stdAcc
mtrc, err := metrics.NewBackendStandardAccess(b.mtrcNamespace, b.promRegisterer)
if err != nil {
return fmt.Errorf("initializing standard access metrics: %w", err)
}
strg, err := backendpb.NewStandardAccess(&backendpb.StandardAccessConfig{
Endpoint: &b.env.StandardAccessURL.URL,
GRPCMetrics: b.backendGRPCMtrc,
Metrics: mtrc,
Logger: b.baseLogger.With(slogutil.KeyPrefix, "standard_access_storage"),
ErrColl: b.errColl,
APIKey: b.env.StandardAccessAPIKey,
})
if err != nil {
return fmt.Errorf("initializing standard access storage: %w", err)
}
updater, err := filterstorage.NewStandardAccess(ctx, &filterstorage.StandardAccessConfig{
BaseLogger: b.baseLogger,
Logger: b.baseLogger.With(slogutil.KeyPrefix, "standard_access_updater"),
Getter: strg,
Setter: stdAcc,
CacheDir: b.env.FilterCachePath,
})
if err != nil {
return fmt.Errorf("initializing standard access updater: %w", err)
}
err = updater.Refresh(ctx)
if err != nil {
return fmt.Errorf("initializing standard access updater: %w", err)
}
refrWorker := service.NewRefreshWorker(&service.RefreshWorkerConfig{
Clock: timeutil.SystemClock{},
ContextConstructor: contextutil.NewTimeoutConstructor(
time.Duration(b.env.StandardAccessTimeout),
),
ErrorHandler: newSlogErrorHandler(b.baseLogger, "standard_access_refresh"),
Refresher: updater,
Schedule: timeutil.NewConstSchedule(time.Duration(b.env.StandardAccessRefreshIvl)),
RefreshOnShutdown: false,
})
err = refrWorker.Start(context.WithoutCancel(ctx))
if err != nil {
return fmt.Errorf("starting standard access refresher: %w", err)
}
b.sigHdlr.AddService(refrWorker)
b.debugRefrs[debugIDStandardProfileAccess] = updater
return nil
}
// initFilterStorage initializes and refreshes the filter storage. It also adds
// the refresher with ID [filter.StoragePrefix] to the debug refreshers.
//
@@ -598,7 +677,7 @@ func (b *builder) initFilterStorage(ctx context.Context) (err error) {
b.filterStorage, err = filterstorage.New(&filterstorage.Config{
BaseLogger: b.baseLogger,
Logger: b.baseLogger.With(slogutil.KeyPrefix, filter.StoragePrefix),
BlockedServices: &filterstorage.ConfigBlockedServices{
BlockedServices: &filterstorage.BlockedServicesConfig{
IndexURL: blockedSvcIdxURL,
// TODO(a.garipov): Consider adding a separate parameter here.
IndexMaxSize: c.MaxSize,
@@ -612,15 +691,15 @@ func (b *builder) initFilterStorage(ctx context.Context) (err error) {
ResultCacheEnabled: c.RuleListCache.Enabled,
Enabled: bool(b.env.BlockedServiceEnabled),
},
Custom: &filterstorage.ConfigCustom{
Custom: &filterstorage.CustomConfig{
CacheCount: c.CustomFilterCacheSize,
},
HashPrefix: &filterstorage.ConfigHashPrefix{
HashPrefix: &filterstorage.HashPrefixConfig{
Adult: b.adultBlocking,
Dangerous: b.safeBrowsing,
NewlyRegistered: b.newRegDomains,
},
RuleLists: &filterstorage.ConfigRuleLists{
RuleLists: &filterstorage.RuleListsConfig{
IndexURL: &b.env.FilterIndexURL.URL,
// TODO(a.garipov): Consider adding a separate parameter here.
IndexMaxSize: c.MaxSize,
@@ -686,14 +765,14 @@ func (b *builder) newSafeSearchConfig(
u *urlutil.URL,
id filter.ID,
enabled bool,
) (c *filterstorage.ConfigSafeSearch) {
) (c *filterstorage.SafeSearchConfig) {
if !enabled {
return &filterstorage.ConfigSafeSearch{}
return &filterstorage.SafeSearchConfig{}
}
fltConf := b.conf.Filters
return &filterstorage.ConfigSafeSearch{
return &filterstorage.SafeSearchConfig{
URL: &u.URL,
ID: id,
// TODO(a.garipov): Consider adding a separate parameter here.
@@ -1148,6 +1227,7 @@ func (b *builder) initGRPCMetrics(ctx context.Context) (err error) {
case
b.profilesEnabled,
b.env.SessionTicketType == sessionTicketRemote,
b.env.StandardAccessType == standardAccessBackend,
b.env.DNSCheckKVType == kvModeBackend,
b.env.RateLimitAllowlistType == rlAllowlistTypeBackend:
// Go on.
@@ -1262,7 +1342,10 @@ func (b *builder) initProfileDB(ctx context.Context) (err error) {
return fmt.Errorf("registering profile access engine metrics: %w", err)
}
profAccessCons := access.NewProfileConstructor(profileMtrc)
profAccessCons := access.NewProfileConstructor(&access.ProfileConstructorConfig{
Metrics: profileMtrc,
Standard: b.standardAccess,
})
backendProfileDBMtrc, err := metrics.NewBackendProfileDB(b.mtrcNamespace, b.promRegisterer)
if err != nil {

View File

@@ -112,6 +112,8 @@ func Main(plugins *plugin.Registry) {
errors.Check(b.initGRPCMetrics(ctx))
errors.Check(b.initStandardAccess(ctx))
errors.Check(b.initTLSManager(ctx))
errors.Check(b.initCustomDomainDB(ctx))

View File

@@ -49,6 +49,7 @@ type environment struct {
RuleStatURL *urlutil.URL `env:"RULESTAT_URL"`
SafeBrowsingURL *urlutil.URL `env:"SAFE_BROWSING_URL"`
SessionTicketURL *urlutil.URL `env:"SESSION_TICKET_URL"`
StandardAccessURL *urlutil.URL `env:"STANDARD_ACCESS_URL"`
YoutubeSafeSearchURL *urlutil.URL `env:"YOUTUBE_SAFE_SEARCH_URL"`
BackendRateLimitAPIKey string `env:"BACKEND_RATELIMIT_API_KEY"`
@@ -74,6 +75,8 @@ type environment struct {
SessionTicketCachePath string `env:"SESSION_TICKET_CACHE_PATH"`
SessionTicketIndexName string `env:"SESSION_TICKET_INDEX_NAME"`
SessionTicketType string `env:"SESSION_TICKET_TYPE"`
StandardAccessAPIKey string `env:"STANDARD_ACCESS_API_KEY"`
StandardAccessType string `env:"STANDARD_ACCESS_TYPE"`
// TODO(a.garipov): Consider renaming to "WEB_STATIC_PATH" or something
// similar.
@@ -83,9 +86,11 @@ type environment struct {
ProfilesMaxRespSize datasize.ByteSize `env:"PROFILES_MAX_RESP_SIZE" envDefault:"64MB"`
CustomDomainsRefreshIvl timeutil.Duration `env:"CUSTOM_DOMAINS_REFRESH_INTERVAL"`
DNSCheckKVTTL timeutil.Duration `env:"DNSCHECK_KV_TTL"`
SessionTicketRefreshIvl timeutil.Duration `env:"SESSION_TICKET_REFRESH_INTERVAL"`
CustomDomainsRefreshIvl timeutil.Duration `env:"CUSTOM_DOMAINS_REFRESH_INTERVAL"`
DNSCheckKVTTL timeutil.Duration `env:"DNSCHECK_KV_TTL"`
SessionTicketRefreshIvl timeutil.Duration `env:"SESSION_TICKET_REFRESH_INTERVAL"`
StandardAccessRefreshIvl timeutil.Duration `env:"STANDARD_ACCESS_REFRESH_INTERVAL"`
StandardAccessTimeout timeutil.Duration `env:"STANDARD_ACCESS_TIMEOUT"`
// TODO(a.garipov): Rename to DNSCHECK_CACHE_KV_COUNT?
DNSCheckCacheKVSize int `env:"DNSCHECK_CACHE_KV_SIZE"`
@@ -152,6 +157,7 @@ func (envs *environment) Validate() (err error) {
errs = envs.validateDNSCheck(errs)
errs = envs.validateRateLimit(errs)
errs = envs.validateSessionTickets(errs)
errs = envs.validateStandardAccess(errs)
errs = envs.validateRateLimitURLs(errs)
return errors.Join(errs...)
@@ -328,12 +334,12 @@ func (envs *environment) validateRateLimit(errs []error) (res []error) {
func (envs *environment) validateSessionTickets(errs []error) (res []error) {
res = errs
err := validate.Positive("env SESSION_TICKET_REFRESH_INTERVAL", envs.SessionTicketRefreshIvl)
err := validate.NotEmpty("env SESSION_TICKET_TYPE", envs.SessionTicketType)
if err != nil {
res = append(res, err)
return append(res, err)
}
err = validate.NotEmpty("env SESSION_TICKET_TYPE", envs.SessionTicketType)
err = validate.Positive("env SESSION_TICKET_REFRESH_INTERVAL", envs.SessionTicketRefreshIvl)
if err != nil {
return append(res, err)
}
@@ -342,24 +348,52 @@ func (envs *environment) validateSessionTickets(errs []error) (res []error) {
case sessionTicketLocal:
return res
case sessionTicketRemote:
// Go on.
res = append(
res,
validate.NotEmpty("env SESSION_TICKET_API_KEY", envs.SessionTicketAPIKey),
validate.NotEmpty("env SESSION_TICKET_CACHE_PATH", envs.SessionTicketCachePath),
validate.NotEmpty("env SESSION_TICKET_INDEX_NAME", envs.SessionTicketIndexName),
)
if err = validate.NotNil("env SESSION_TICKET_URL", envs.SessionTicketURL); err != nil {
res = append(res, err)
} else if err = urlutil.ValidateGRPCURL(&envs.SessionTicketURL.URL); err != nil {
res = append(res, fmt.Errorf("env SESSION_TICKET_URL: %w", err))
}
default:
err = fmt.Errorf("env SESSION_TICKET_TYPE: %w: %q", errors.ErrBadEnumValue, typ)
return append(res, err)
}
res = append(
res,
validate.NotEmpty("env SESSION_TICKET_API_KEY", envs.SessionTicketAPIKey),
validate.NotEmpty("env SESSION_TICKET_CACHE_PATH", envs.SessionTicketCachePath),
validate.NotEmpty("env SESSION_TICKET_INDEX_NAME", envs.SessionTicketIndexName),
)
return res
}
if err = validate.NotNil("env SESSION_TICKET_URL", envs.SessionTicketURL); err != nil {
res = append(res, err)
} else if err = urlutil.ValidateGRPCURL(&envs.SessionTicketURL.URL); err != nil {
res = append(res, fmt.Errorf("env SESSION_TICKET_URL: %w", err))
// validateStandardAccess appends validation errors to the given errs if
// environment variables for standard access contain errors.
func (envs *environment) validateStandardAccess(errs []error) (res []error) {
res = errs
switch typ := envs.StandardAccessType; typ {
case standardAccessOff:
return res
case standardAccessBackend:
res = append(
res,
validate.NotEmpty("env STANDARD_ACCESS_API_KEY", envs.StandardAccessAPIKey),
validate.Positive("env STANDARD_ACCESS_REFRESH_INTERVAL", envs.StandardAccessRefreshIvl),
validate.Positive("env STANDARD_ACCESS_TIMEOUT", envs.StandardAccessTimeout),
)
if err := validate.NotNil("env STANDARD_ACCESS_URL", envs.StandardAccessURL); err != nil {
res = append(res, err)
} else if err = urlutil.ValidateGRPCURL(&envs.StandardAccessURL.URL); err != nil {
res = append(res, fmt.Errorf("env STANDARD_ACCESS_URL: %w", err))
}
default:
err := fmt.Errorf("env STANDARD_ACCESS_TYPE: %w: %q", errors.ErrBadEnumValue, typ)
return append(res, err)
}
return res

View File

@@ -34,20 +34,21 @@ func TestLimiter(t *testing.T) {
Resume: 1,
})
// TODO(a.garipov): Add fakenet.NewConn to golibs.
conn := &fakenet.Conn{
OnClose: func() (err error) { return nil },
OnLocalAddr: func() (laddr net.Addr) { panic("not implemented") },
OnRead: func(b []byte) (n int, err error) { panic("not implemented") },
OnLocalAddr: func() (laddr net.Addr) { panic(testutil.UnexpectedCall()) },
OnRead: func(b []byte) (n int, err error) { panic(testutil.UnexpectedCall(b)) },
OnRemoteAddr: func() (addr net.Addr) {
return &net.TCPAddr{
IP: netutil.IPv4Localhost().AsSlice(),
Port: 1234,
}
},
OnSetDeadline: func(t time.Time) (err error) { panic("not implemented") },
OnSetReadDeadline: func(t time.Time) (err error) { panic("not implemented") },
OnSetWriteDeadline: func(t time.Time) (err error) { panic("not implemented") },
OnWrite: func(b []byte) (n int, err error) { panic("not implemented") },
OnSetDeadline: func(t time.Time) (err error) { panic(testutil.UnexpectedCall(t)) },
OnSetReadDeadline: func(t time.Time) (err error) { panic(testutil.UnexpectedCall(t)) },
OnSetWriteDeadline: func(t time.Time) (err error) { panic(testutil.UnexpectedCall(t)) },
OnWrite: func(b []byte) (n int, err error) { panic(testutil.UnexpectedCall(b)) },
}
lsnr := &fakenet.Listener{

View File

@@ -10,30 +10,32 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/connlimiter"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/testutil/fakenet"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestListenConfig(t *testing.T) {
// TODO(a.garipov): Add fakenet.NewPacketConn to golibs.
pc := &fakenet.PacketConn{
OnClose: func() (_ error) { panic("not implemented") },
OnLocalAddr: func() (_ net.Addr) { panic("not implemented") },
OnReadFrom: func(_ []byte) (_ int, _ net.Addr, _ error) {
panic("not implemented")
OnClose: func() (err error) { panic(testutil.UnexpectedCall()) },
OnLocalAddr: func() (laddr net.Addr) { panic(testutil.UnexpectedCall()) },
OnReadFrom: func(b []byte) (n int, addr net.Addr, err error) {
panic(testutil.UnexpectedCall(b))
},
OnSetDeadline: func(_ time.Time) (_ error) { panic("not implemented") },
OnSetReadDeadline: func(_ time.Time) (_ error) { panic("not implemented") },
OnSetWriteDeadline: func(_ time.Time) (_ error) { panic("not implemented") },
OnWriteTo: func(_ []byte, _ net.Addr) (_ int, _ error) {
panic("not implemented")
OnSetDeadline: func(t time.Time) (err error) { panic(testutil.UnexpectedCall(t)) },
OnSetReadDeadline: func(t time.Time) (err error) { panic(testutil.UnexpectedCall(t)) },
OnSetWriteDeadline: func(t time.Time) (err error) { panic(testutil.UnexpectedCall(t)) },
OnWriteTo: func(b []byte, addr net.Addr) (n int, err error) {
panic(testutil.UnexpectedCall(b, addr))
},
}
lsnr := &fakenet.Listener{
OnAccept: func() (_ net.Conn, _ error) { panic("not implemented") },
OnAddr: func() (_ net.Addr) { panic("not implemented") },
OnClose: func() (_ error) { return nil },
OnAccept: func() (c net.Conn, err error) { panic(testutil.UnexpectedCall()) },
OnAddr: func() (addr net.Addr) { panic(testutil.UnexpectedCall()) },
OnClose: func() (err error) { return nil },
}
c := &agdtest.ListenConfig{

View File

@@ -1,4 +1,6 @@
// Package debugsvc contains the debug HTTP API of AdGuard DNS.
//
// TODO(a.garipov): Add standard or custom metrics.
package debugsvc
import (
@@ -130,7 +132,7 @@ var _ service.Interface = (*Service)(nil)
//
// TODO(a.garipov): Wait for the services to go online.
//
// TODO(a.garipov): Use the context for cancelation.
// TODO(a.garipov): Use the context for cancellation.
func (svc *Service) Start(ctx context.Context) (err error) {
for _, srv := range svc.servers {
go runServer(ctx, svc.logger, srv)

View File

@@ -99,10 +99,10 @@ func TestService_Start(t *testing.T) {
// yet, check for it in periodically.
var resp *http.Response
healthCheckURL := srvURL.JoinPath(debugsvc.PathPatternHealthCheck)
require.EventuallyWithT(t, func(ct *assert.CollectT) {
require.EventuallyWithT(t, func(c *assert.CollectT) {
var getErr error
resp, getErr = client.Get(ctx, healthCheckURL)
assert.NoError(t, getErr)
assert.NoError(c, getErr)
}, testTimeout, testTimeout/10)
body := readRespBody(t, resp)

View File

@@ -24,8 +24,9 @@ import (
//
// TODO(a.garipov): Extract cache logic to golibs.
type Middleware struct {
logger *slog.Logger
metrics MetricsListener
logger *slog.Logger
metrics MetricsListener
// TODO(d.kolyshev): Use [agdcache.Default].
cache gcache.Cache
cacheMinTTL time.Duration
overrideTTL bool

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2022-2024 AdGuard Software Ltd.
// Copyright (C) 2022-2025 AdGuard Software Ltd.
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU Affero General Public License as published by the Free

View File

@@ -2,12 +2,12 @@ package dnsservertest
import (
"context"
"fmt"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/miekg/dns"
)
@@ -59,8 +59,7 @@ func NewDefaultHandlerWithCount(recordsCount int) (h dnsserver.Handler) {
// NewPanicHandler returns a DNS handler that panics with an error.
func NewPanicHandler() (handler dnsserver.Handler) {
f := func(ctx context.Context, rw dnsserver.ResponseWriter, req *dns.Msg) (err error) {
// TODO(a.garipov): Add a helper for these kinds of errors to golibs.
panic(fmt.Errorf("unexpected call to ServeDNS(%v, %v)", rw, req))
panic(testutil.UnexpectedCall(ctx, rw, req))
}
return dnsserver.HandlerFunc(f)

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2022-2024 AdGuard Software Ltd.
// Copyright (C) 2022-2025 AdGuard Software Ltd.
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU Affero General Public License as published by the Free

View File

@@ -1,46 +1,42 @@
module github.com/AdguardTeam/AdGuardDNS/internal/dnsserver
go 1.24.5
go 1.24.6
require (
github.com/AdguardTeam/golibs v0.32.15
github.com/AdguardTeam/golibs v0.34.0
github.com/ameshkov/dnscrypt/v2 v2.4.0
github.com/ameshkov/dnsstamps v1.0.3
github.com/bluele/gcache v0.0.2
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
github.com/miekg/dns v1.1.66
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.22.0
github.com/quic-go/quic-go v0.52.0
github.com/stretchr/testify v1.10.0
golang.org/x/net v0.41.0
golang.org/x/sys v0.33.0
github.com/prometheus/client_golang v1.23.0
github.com/quic-go/quic-go v0.54.0
github.com/stretchr/testify v1.11.1
golang.org/x/net v0.43.0
golang.org/x/sys v0.35.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.1 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/kr/text v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
github.com/onsi/gomega v1.37.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // 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.64.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/prometheus/common v0.65.0 // 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/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.5.2 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.36.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -1,5 +1,5 @@
github.com/AdguardTeam/golibs v0.32.15 h1:arDRDWiZCH3g5Onr8AqMnOHhaOppNoBpgC3DNhmeDeA=
github.com/AdguardTeam/golibs v0.32.15/go.mod h1:G9CzUOzx87J+2u+eClJrrwWD7lMbROvuUnT8uvDUzIA=
github.com/AdguardTeam/golibs v0.34.0 h1:JQK024DkTYxE7vsPVsYsoyDHW/53Nun7OYb9qscniK8=
github.com/AdguardTeam/golibs v0.34.0/go.mod h1:K4C2EbfSEM1zY5YXoti9SfbTAHN/kIX97LpDtCwORrM=
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=
@@ -12,76 +12,65 @@ github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 h1:6lhrsTEnloDPXye
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
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/google/pprof v0.0.0-20250607225305-033d6d78b36a h1://KbezygeMJZCSHH+HgUZiTeSoiuFspbMg1ge+eFj18=
github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
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.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible h1:IWzUvJ72xMjmrjR9q3H1PF+jwdN0uNQiR2t1BLNalyo=
github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
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.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
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.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
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.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA=
github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
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/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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -9,6 +9,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/netext"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
@@ -18,6 +19,7 @@ func TestDefaultListenConfigWithOOB(t *testing.T) {
lc := netext.DefaultListenConfigWithOOB(nil)
require.NotNil(t, lc)
// TODO(a.garipov): Move to golibs.
type syscallConner interface {
SyscallConn() (c syscall.RawConn, err error)
}
@@ -26,9 +28,10 @@ func TestDefaultListenConfigWithOOB(t *testing.T) {
c, err := lc.ListenPacket(context.Background(), "udp4", "127.0.0.1:0")
require.NoError(t, err)
require.NotNil(t, c)
require.Implements(t, (*syscallConner)(nil), c)
sc, err := c.(syscallConner).SyscallConn()
scConner := testutil.RequireTypeAssert[syscallConner](t, c)
sc, err := scConner.SyscallConn()
require.NoError(t, err)
err = sc.Control(func(fd uintptr) {
@@ -51,9 +54,10 @@ func TestDefaultListenConfigWithOOB(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, c)
require.Implements(t, (*syscallConner)(nil), c)
sc, err := c.(syscallConner).SyscallConn()
scConner := testutil.RequireTypeAssert[syscallConner](t, c)
sc, err := scConner.SyscallConn()
require.NoError(t, err)
err = sc.Control(func(fd uintptr) {
@@ -86,9 +90,10 @@ func TestDefaultListenConfigWithSO(t *testing.T) {
c, err := lc.ListenPacket(context.Background(), "udp4", "127.0.0.1:0")
require.NoError(t, err)
require.NotNil(t, c)
require.Implements(t, (*syscallConner)(nil), c)
sc, err := c.(syscallConner).SyscallConn()
scConner := testutil.RequireTypeAssert[syscallConner](t, c)
sc, err := scConner.SyscallConn()
require.NoError(t, err)
err = sc.Control(func(fd uintptr) {
@@ -119,9 +124,10 @@ func TestDefaultListenConfigWithSO(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, c)
require.Implements(t, (*syscallConner)(nil), c)
sc, err := c.(syscallConner).SyscallConn()
scConner := testutil.RequireTypeAssert[syscallConner](t, c)
sc, err := scConner.SyscallConn()
require.NoError(t, err)
err = sc.Control(func(fd uintptr) {

View File

@@ -527,7 +527,7 @@ func createDoH3Client(
_ string,
tlsCfg *tls.Config,
cfg *quic.Config,
) (c quic.EarlyConnection, e error) {
) (c *quic.Conn, e error) {
return quic.DialAddrEarly(ctx, httpsAddr.String(), tlsCfg, cfg)
},
QUICConfig: quicConfig,

View File

@@ -316,7 +316,7 @@ func (s *ServerQUIC) acceptQUICConn(
// decremented.
func (s *ServerQUIC) serveQUICConnAsync(
ctx context.Context,
conn quic.Connection,
conn *quic.Conn,
connWg *sync.WaitGroup,
) {
defer connWg.Done()
@@ -331,7 +331,7 @@ func (s *ServerQUIC) serveQUICConnAsync(
// serveQUICConn handles a new QUIC connection. It waits for new streams and
// passes them to serveQUICStream.
func (s *ServerQUIC) serveQUICConn(ctx context.Context, conn quic.Connection) (err error) {
func (s *ServerQUIC) serveQUICConn(ctx context.Context, conn *quic.Conn) (err error) {
streamWg := &sync.WaitGroup{}
defer func() {
// Wait until all streams are processed.
@@ -347,7 +347,7 @@ func (s *ServerQUIC) serveQUICConn(ctx context.Context, conn quic.Connection) (e
// design specifies that for each subsequent query on a QUIC connection
// the client MUST select the next available client-initiated
// bidirectional stream.
var stream quic.Stream
var stream *quic.Stream
acceptCtx, cancel := context.WithDeadline(ctx, time.Now().Add(maxQUICIdleTimeout))
// For some reason AcceptStream below seems to get stuck even when
@@ -403,8 +403,8 @@ func (s *ServerQUIC) serveQUICConn(ctx context.Context, conn quic.Connection) (e
// be decremented.
func (s *ServerQUIC) serveQUICStreamAsync(
ctx context.Context,
stream quic.Stream,
conn quic.Connection,
stream *quic.Stream,
conn *quic.Conn,
wg *sync.WaitGroup,
) {
defer wg.Done()
@@ -421,8 +421,8 @@ func (s *ServerQUIC) serveQUICStreamAsync(
// and writes back the responses.
func (s *ServerQUIC) serveQUICStream(
ctx context.Context,
stream quic.Stream,
conn quic.Connection,
stream *quic.Stream,
conn *quic.Conn,
) (err error) {
// The server MUST send the response on the same stream, and MUST indicate
// through the STREAM FIN mechanism that no further data will be sent on
@@ -486,7 +486,7 @@ func (s *ServerQUIC) serveQUICStream(
// if anything went wrong.
func (s *ServerQUIC) readQUICMsg(
ctx context.Context,
stream quic.Stream,
stream *quic.Stream,
) (m *dns.Msg, err error) {
bufPtr := s.reqPool.Get()
defer s.reqPool.Put(bufPtr)
@@ -607,7 +607,7 @@ func isExpectedQUICErr(err error) (ok bool) {
}
// Catch quic-go's IdleTimeoutError. This error is returned from
// quic.Connection.AcceptStream calls and this is an expected outcome,
// *quic.Conn.AcceptStream calls and this is an expected outcome,
// happens all the time with different QUIC clients.
var qErr *quic.IdleTimeoutError
if errors.As(err, &qErr) {
@@ -690,7 +690,7 @@ func validQUICMsg(req *dns.Msg) (ok bool) {
// code and logs if it fails to close the connection.
func (s *ServerQUIC) closeQUICConn(
ctx context.Context,
conn quic.Connection,
conn *quic.Conn,
code quic.ApplicationErrorCode,
) {
err := conn.CloseWithError(code, "")
@@ -725,6 +725,7 @@ func newServerQUICConfig(
// quicAddrValidator is a helper struct that holds a small LRU cache of
// addresses for which we do not require address validation.
type quicAddrValidator struct {
// TODO(d.kolyshev): Use [agdcache.Default].
cache gcache.Cache
metrics MetricsListener
ttl time.Duration

View File

@@ -86,7 +86,7 @@ func TestServerQUIC_integration_ENDS0Padding(t *testing.T) {
conn, err := quic.DialAddr(context.Background(), addr.String(), tlsConfig, nil)
require.NoError(t, err)
defer func(conn quic.Connection, code quic.ApplicationErrorCode, s string) {
defer func(conn *quic.Conn, code quic.ApplicationErrorCode, s string) {
_ = conn.CloseWithError(code, s)
}(conn, 0, "")
@@ -196,7 +196,7 @@ func testQUICExchange(
return conn.CloseWithError(0, "")
})
defer func(conn quic.Connection, code quic.ApplicationErrorCode, s string) {
defer func(conn *quic.Conn, code quic.ApplicationErrorCode, s string) {
_ = conn.CloseWithError(code, s)
}(conn, 0, "")
@@ -210,7 +210,7 @@ func testQUICExchange(
// sendQUICMessage is a test helper that sends a test QUIC message.
func sendQUICMessage(
conn quic.Connection,
conn *quic.Conn,
req *dns.Msg,
) (resp *dns.Msg, err error) {
stream, err := conn.OpenStreamSync(context.Background())
@@ -266,7 +266,7 @@ func sendQUICMessage(
// test's t.
func requireSendQUICMessage(
t testing.TB,
conn quic.Connection,
conn *quic.Conn,
req *dns.Msg,
) (resp *dns.Msg) {
t.Helper()
@@ -279,7 +279,7 @@ func requireSendQUICMessage(
// writeQUICStream writes buf to the specified QUIC stream in chunks. This way
// it is possible to test how the server deals with chunked DNS messages.
func writeQUICStream(buf []byte, stream quic.Stream) (err error) {
func writeQUICStream(buf []byte, stream *quic.Stream) (err error) {
// Send the DNS query to the stream and split it into chunks of up
// to 400 bytes. 400 is an arbitrary chosen value.
chunkSize := 400

View File

@@ -26,7 +26,7 @@ var _ contextutil.Constructor = (*contextConstructor)(nil)
// New implements the [contextutil.Constructor] interface for
// *contextConstructor. It returns a context with a new [agd.RequestID] as well
// as its timeout and the corresponding cancelation function.
// as its timeout and the corresponding cancellation function.
func (c *contextConstructor) New(
parent context.Context,
) (ctx context.Context, cancel context.CancelFunc) {

View File

@@ -80,17 +80,21 @@ func (l *testListener) LocalUDPAddr() (addr net.Addr) {
}
// newTestListener returns a *testListener all of methods of which panic with
// a "not implemented" message.
// an unexpected call message.
func newTestListener() (tl *testListener) {
return &testListener{
onName: func() (_ string) { panic("not implemented") },
onProto: func() (_ dnsserver.Protocol) { panic("not implemented") },
onNetwork: func() (_ dnsserver.Network) { panic("not implemented") },
onAddr: func() (_ string) { panic("not implemented") },
onStart: func(_ context.Context) (err error) { panic("not implemented") },
onShutdown: func(_ context.Context) (err error) { panic("not implemented") },
onLocalUDPAddr: func() (_ net.Addr) { panic("not implemented") },
onLocalTCPAddr: func() (_ net.Addr) { panic("not implemented") },
onName: func() (name string) { panic(testutil.UnexpectedCall()) },
onProto: func() (proto dnsserver.Protocol) { panic(testutil.UnexpectedCall()) },
onNetwork: func() (n dnsserver.Network) { panic(testutil.UnexpectedCall()) },
onAddr: func() (addr string) { panic(testutil.UnexpectedCall()) },
onStart: func(ctx context.Context) (err error) {
panic(testutil.UnexpectedCall(ctx))
},
onShutdown: func(ctx context.Context) (err error) {
panic(testutil.UnexpectedCall(ctx))
},
onLocalUDPAddr: func() (addr net.Addr) { panic(testutil.UnexpectedCall()) },
onLocalTCPAddr: func() (addr net.Addr) { panic(testutil.UnexpectedCall()) },
}
}

View File

@@ -17,6 +17,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/ecscache"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/timeutil"
// TODO(e.burkov): Move registering of the metrics to another package to
// avoid dependency on the metrics package.
@@ -123,6 +124,7 @@ func wrapPreUpstreamMw(
cacheMw := ecscache.NewMiddleware(&ecscache.MiddlewareConfig{
Metrics: mtrc,
Clock: timeutil.SystemClock{},
Cloner: c.Cloner,
Logger: c.BaseLogger.With(slogutil.KeyPrefix, "ecscache"),
CacheManager: c.CacheManager,

View File

@@ -26,36 +26,38 @@ func TestNewHandlers(t *testing.T) {
t.Parallel()
accessMgr := &agdtest.AccessManager{
OnIsBlockedHost: func(host string, qt uint16) (blocked bool) { panic("not implemented") },
OnIsBlockedIP: func(ip netip.Addr) (blocked bool) { panic("not implemented") },
OnIsBlockedHost: func(host string, qt uint16) (blocked bool) {
panic(testutil.UnexpectedCall(host, qt))
},
OnIsBlockedIP: func(ip netip.Addr) (blocked bool) { panic(testutil.UnexpectedCall(ip)) },
}
billStat := &agdtest.BillStatRecorder{
OnRecord: func(
_ context.Context,
_ agd.DeviceID,
_ geoip.Country,
_ geoip.ASN,
_ time.Time,
_ agd.Protocol,
ctx context.Context,
id agd.DeviceID,
ctry geoip.Country,
asn geoip.ASN,
start time.Time,
proto agd.Protocol,
) {
panic("not implemented")
panic(testutil.UnexpectedCall(ctx, id, ctry, asn, start, proto))
},
}
dnsCk := &agdtest.DNSCheck{
OnCheck: func(
_ context.Context,
_ *dns.Msg,
_ *agd.RequestInfo,
ctx context.Context,
req *dns.Msg,
ri *agd.RequestInfo,
) (resp *dns.Msg, err error) {
panic("not implemented")
panic(testutil.UnexpectedCall(ctx, req, ri))
},
}
dnsDB := &agdtest.DNSDB{
OnRecord: func(_ context.Context, _ *dns.Msg, _ *agd.RequestInfo) {
panic("not implemented")
OnRecord: func(ctx context.Context, resp *dns.Msg, ri *agd.RequestInfo) {
panic(testutil.UnexpectedCall(ctx, resp, ri))
},
}
@@ -76,30 +78,30 @@ func TestNewHandlers(t *testing.T) {
}
fltStrg := &agdtest.FilterStorage{
OnForConfig: func(_ context.Context, _ filter.Config) (f filter.Interface) {
panic("not implemented")
OnForConfig: func(ctx context.Context, c filter.Config) (f filter.Interface) {
panic(testutil.UnexpectedCall(ctx, c))
},
OnHasListID: func(_ filter.ID) (ok bool) { panic("not implemented") },
OnHasListID: func(id filter.ID) (ok bool) { panic(testutil.UnexpectedCall(id)) },
}
hashMatcher := &agdtest.HashMatcher{
OnMatchByPrefix: func(
_ context.Context,
_ string,
ctx context.Context,
host string,
) (hashes []string, matched bool, err error) {
panic("not implemented")
panic(testutil.UnexpectedCall(ctx, host))
},
}
queryLog := &agdtest.QueryLog{
OnWrite: func(_ context.Context, _ *querylog.Entry) (err error) {
panic("not implemented")
OnWrite: func(ctx context.Context, e *querylog.Entry) (err error) {
panic(testutil.UnexpectedCall(ctx, e))
},
}
ruleStat := &agdtest.RuleStat{
OnCollect: func(_ context.Context, _ filter.ID, _ filter.RuleText) {
panic("not implemented")
OnCollect: func(ctx context.Context, id filter.ID, text filter.RuleText) {
panic(testutil.UnexpectedCall(ctx, id, text))
},
}

View File

@@ -136,7 +136,7 @@ func newTestService(
OnForConfig: func(_ context.Context, _ filter.Config) (f filter.Interface) {
return flt
},
OnHasListID: func(_ filter.ID) (ok bool) { panic("not implemented") },
OnHasListID: func(id filter.ID) (ok bool) { panic(testutil.UnexpectedCall(id)) },
}
var ql querylog.Interface = &agdtest.QueryLog{
@@ -397,8 +397,8 @@ func TestService_Wrap(t *testing.T) {
Rule: cnameRule,
}, nil
},
OnFilterResponse: func(_ context.Context, _ *filter.Response) (filter.Result, error) {
panic("not implemented")
OnFilterResponse: func(ctx context.Context, resp *filter.Response) (filter.Result, error) {
panic(testutil.UnexpectedCall(ctx, resp))
},
}

View File

@@ -265,9 +265,9 @@ func assertEqualResult(tb testing.TB, want, got agd.DeviceResult) {
}
}
// newDefault is is a helper for creating the device finders for tests. c may
// be nil, and all zero-value fields in c are replaced with defaults for tests.
// The default server is [srvDoH].
// newDefault is a helper for creating device finders for tests. c may be nil,
// and all zero-value fields in c are replaced with defaults for tests. The
// default server is [srvDoH].
func newDefault(tb testing.TB, c *devicefinder.Config) (f *devicefinder.Default) {
tb.Helper()
@@ -303,12 +303,12 @@ func TestDefault_Find_dnscrypt(t *testing.T) {
func BenchmarkDefault(b *testing.B) {
profDB := &agdtest.ProfileDB{
OnCreateAutoDevice: func(
_ context.Context,
_ agd.ProfileID,
_ agd.HumanID,
_ agd.DeviceType,
ctx context.Context,
profID agd.ProfileID,
humanID agd.HumanID,
typ agd.DeviceType,
) (p *agd.Profile, d *agd.Device, err error) {
panic("not implemented")
panic(testutil.UnexpectedCall(ctx, profID, humanID, typ))
},
OnProfileByDedicatedIP: func(

View File

@@ -69,14 +69,14 @@ func TestMiddleware_Wrap(t *testing.T) {
var (
billStatNotImp = &agdtest.BillStatRecorder{
OnRecord: func(
_ context.Context,
_ agd.DeviceID,
_ geoip.Country,
_ geoip.ASN,
_ time.Time,
_ agd.Protocol,
ctx context.Context,
id agd.DeviceID,
ctry geoip.Country,
asn geoip.ASN,
start time.Time,
proto agd.Protocol,
) {
panic("not implemented")
panic(testutil.UnexpectedCall(ctx, id, ctry, asn, start, proto))
},
}
@@ -114,7 +114,7 @@ func TestMiddleware_Wrap(t *testing.T) {
OnForConfig: func(_ context.Context, _ filter.Config) (f filter.Interface) {
return flt
},
OnHasListID: func(_ filter.ID) (ok bool) { panic("not implemented") },
OnHasListID: func(id filter.ID) (ok bool) { panic(testutil.UnexpectedCall(id)) },
}
geoIP := agdtest.NewGeoIP()
@@ -392,14 +392,14 @@ func TestMiddleware_Wrap_filtering(t *testing.T) {
var (
billStatNotImp = &agdtest.BillStatRecorder{
OnRecord: func(
_ context.Context,
_ agd.DeviceID,
_ geoip.Country,
_ geoip.ASN,
_ time.Time,
_ agd.Protocol,
ctx context.Context,
id agd.DeviceID,
ctry geoip.Country,
asn geoip.ASN,
start time.Time,
proto agd.Protocol,
) {
panic("not implemented")
panic(testutil.UnexpectedCall(ctx, id, ctry, asn, start, proto))
},
}
@@ -672,7 +672,7 @@ func TestMiddleware_Wrap_filtering(t *testing.T) {
OnForConfig: func(_ context.Context, _ filter.Config) (f filter.Interface) {
return flt
},
OnHasListID: func(_ filter.ID) (ok bool) { panic("not implemented") },
OnHasListID: func(id filter.ID) (ok bool) { panic(testutil.UnexpectedCall(id)) },
}
q := tc.req.Question[0]

View File

@@ -2,14 +2,10 @@ package ecscache
import (
"context"
"encoding/binary"
"hash/maphash"
"net/netip"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/golibs/mathutil"
"github.com/miekg/dns"
)
@@ -44,12 +40,12 @@ type cacheRequest struct {
// support ECS, isECSDependent is true. cr, cr.req, and cr.subnet must not be
// nil.
func (mw *Middleware) get(
ctx context.Context,
_ context.Context,
req *dns.Msg,
cr *cacheRequest,
) (resp *dns.Msg, isECSDependent bool) {
key := mw.toCacheKey(cr, false)
item, ok := mw.itemFromCache(ctx, mw.cache, key, cr)
key := newCacheKey(cr, false)
item, ok := mw.cache.Get(key)
if ok {
return fromCacheItem(item, mw.cloner, req, cr.reqDO), false
} else if cr.isECSDeclined {
@@ -57,8 +53,8 @@ func (mw *Middleware) get(
}
// Try ECS-aware cache.
key = mw.toCacheKey(cr, true)
item, ok = mw.itemFromCache(ctx, mw.ecsCache, key, cr)
key = newCacheKey(cr, true)
item, ok = mw.ecsCache.Get(key)
if ok {
return fromCacheItem(item, mw.cloner, req, cr.reqDO), true
}
@@ -66,67 +62,6 @@ func (mw *Middleware) get(
return nil, false
}
// itemFromCache retrieves a DNS message for the given key. cr.host is used to
// detect key collisions. If there is a key collision, it returns nil and
// false.
func (mw *Middleware) itemFromCache(
ctx context.Context,
cache agdcache.Interface[uint64, *cacheItem],
key uint64,
cr *cacheRequest,
) (item *cacheItem, ok bool) {
item, ok = cache.Get(key)
if !ok {
return nil, false
}
// Check for cache key collisions.
if item.host != cr.host {
mw.logger.WarnContext(ctx, "cache collision", "item", item, "host", cr.host)
return nil, false
}
return item, true
}
// hashSeed is the seed used by all hashes to create hash keys.
var hashSeed = maphash.MakeSeed()
// toCacheKey returns the appropriate cache key for msg. msg must have one
// question record. subnet must not be nil.
func (mw *Middleware) toCacheKey(cr *cacheRequest, respIsECSDependent bool) (key uint64) {
// Use maphash explicitly instead of using a key structure to reduce
// allocations and optimize interface conversion up the stack.
//
// TODO(a.garipov, e.burkov): Consider just using struct as a key.
h := &maphash.Hash{}
h.SetSeed(hashSeed)
_, _ = h.WriteString(cr.host)
// Save on allocations by reusing a buffer.
var buf [6]byte
binary.LittleEndian.PutUint16(buf[:2], cr.qType)
binary.LittleEndian.PutUint16(buf[2:4], cr.qClass)
buf[4] = mathutil.BoolToNumber[byte](cr.reqDO)
addr := cr.subnet.Addr()
buf[5] = mathutil.BoolToNumber[byte](addr.Is6())
_, _ = h.Write(buf[:])
if respIsECSDependent {
_, _ = h.Write(addr.AsSlice())
_ = h.WriteByte(byte(cr.subnet.Bits()))
} else {
_ = h.WriteByte(mathutil.BoolToNumber[byte](cr.isECSDeclined))
}
return h.Sum64()
}
// set saves resp to the cache if it's cacheable. If msg cannot be cached, it
// is ignored.
func (mw *Middleware) set(resp *dns.Msg, cr *cacheRequest, respIsECSDependent bool) {
@@ -146,11 +81,55 @@ func (mw *Middleware) set(resp *dns.Msg, cr *cacheRequest, respIsECSDependent bo
dnsmsg.SetMinTTL(resp, uint32(exp.Seconds()))
}
key := mw.toCacheKey(cr, respIsECSDependent)
key := newCacheKey(cr, respIsECSDependent)
cache.SetWithExpire(key, &cacheItem{
msg: mw.cloner.Clone(resp),
when: mw.clock.Now(),
}, exp)
}
cachedResp := mw.cloner.Clone(resp)
// cacheKey represents a key used in the cache.
type cacheKey struct {
// host is a non-FQDN version of a cached hostname.
host string
cache.SetWithExpire(key, toCacheItem(cachedResp, cr.host), exp)
// subnet is the network of the country the DNS request came from determined
// with GeoIP.
subnet netip.Prefix
// qType is the question type of the DNS request.
qType uint16
// qClass is the class of the DNS request.
qClass uint16
// reqDO is the state of DNSSEC OK bit from the DNS request.
reqDO bool
// isECSDeclined reflects if the client explicitly restricts using its
// information in EDNS client subnet option as per RFC 7871.
//
// See https://datatracker.ietf.org/doc/html/rfc7871#section-7.1.2.
isECSDeclined bool
}
// newCacheKey returns the appropriate cache key for msg. msg must have one
// question record. cr must not be nil.
func newCacheKey(cr *cacheRequest, respIsECSDependent bool) (key cacheKey) {
key = cacheKey{
host: cr.host,
qType: cr.qType,
qClass: cr.qClass,
reqDO: cr.reqDO,
}
if respIsECSDependent {
key.subnet = cr.subnet
} else {
key.isECSDeclined = cr.isECSDeclined
}
return key
}
// cacheItem represents an item that we will store in the cache.
@@ -160,19 +139,6 @@ type cacheItem struct {
// msg is the cached DNS message.
msg *dns.Msg
// host is the cached normalized hostname for later cache key collision
// checks.
host string
}
// toCacheItem creates a *cacheItem from a DNS message.
func toCacheItem(resp *dns.Msg, host string) (item *cacheItem) {
return &cacheItem{
msg: resp,
when: time.Now(),
host: host,
}
}
// fromCacheItem creates a response from the cached item. item, cloner, and req

View File

@@ -6,26 +6,35 @@ import (
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agdcache"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/dnsservertest"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
)
func BenchmarkMiddleware_Get(b *testing.B) {
mw := &Middleware{
cache: agdcache.NewLRU[uint64, *cacheItem](&agdcache.LRUConfig{
Count: 10,
}),
ecsCache: agdcache.NewLRU[uint64, *cacheItem](&agdcache.LRUConfig{
Count: 10,
}),
}
func BenchmarkMiddleware(b *testing.B) {
mw := NewMiddleware(&MiddlewareConfig{
Metrics: EmptyMetrics{},
Clock: timeutil.SystemClock{},
Cloner: agdtest.NewCloner(),
Logger: slogutil.NewDiscardLogger(),
CacheManager: agdcache.EmptyManager{},
GeoIP: agdtest.NewGeoIP(),
NoECSCount: 100,
ECSCount: 100,
})
const (
host = "benchmark.example"
fqdn = host + "."
defaultTTL uint32 = 3600
)
reqAddr := netip.MustParseAddr("1.2.3.4")
req := dnsservertest.NewReq(fqdn, dns.TypeA, dns.ClassINET)
cr := &cacheRequest{
host: host,
@@ -34,6 +43,9 @@ func BenchmarkMiddleware_Get(b *testing.B) {
qClass: dns.ClassINET,
reqDO: true,
}
resp := dnsservertest.NewResp(dns.RcodeSuccess, req, dnsservertest.SectionAnswer{
dnsservertest.NewA(host, defaultTTL, reqAddr),
})
ctx := context.Background()
@@ -41,16 +53,18 @@ func BenchmarkMiddleware_Get(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
mw.set(resp, cr, true)
msg, _ = mw.get(ctx, req, cr)
}
assert.Nil(b, msg)
assert.NotNil(b, msg)
// Most recent results:
//
// goos: darwin
// goarch: amd64
// goarch: arm64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/ecscache
// cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
// BenchmarkMiddleware_Get-12 5855624 195.1 ns/op 16 B/op 2 allocs/op
// cpu: Apple M1 Pro
// BenchmarkMiddleware_Get-8 1647064 726.8 ns/op 568 B/op 12 allocs/op
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@ import (
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/syncutil"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/miekg/dns"
)
@@ -27,6 +28,9 @@ type MiddlewareConfig struct {
// statistics. It must not be nil.
Metrics Metrics
// Clock is used for getting current time. It must not be nil.
Clock timeutil.Clock
// Cloner is used to clone messages taken from cache. It must not be nil.
Cloner *dnsmsg.Cloner
@@ -58,6 +62,9 @@ type MiddlewareConfig struct {
// Middleware is a dnsserver.Middleware with ECS-aware caching.
type Middleware struct {
// clock is used to get current time for cache expiration.
clock timeutil.Clock
// metrics is used for the collection of the ECS cache statistics.
metrics Metrics
@@ -71,10 +78,10 @@ type Middleware struct {
logger *slog.Logger
// cache is the LRU cache for results indicating no support for ECS.
cache agdcache.Interface[uint64, *cacheItem]
cache agdcache.Interface[cacheKey, *cacheItem]
// ecsCache is the LRU cache for results indicating ECS support.
ecsCache agdcache.Interface[uint64, *cacheItem]
ecsCache agdcache.Interface[cacheKey, *cacheItem]
// geoIP is used to get subnets for countries.
geoIP geoip.Interface
@@ -97,17 +104,20 @@ const (
// adds the caches with IDs [CacheIDNoECS] and [CacheIDWithECS] to the cache
// manager. c must not be nil.
func NewMiddleware(c *MiddlewareConfig) (m *Middleware) {
cache := agdcache.NewLRU[uint64, *cacheItem](&agdcache.LRUConfig{
cache := errors.Must(agdcache.New[cacheKey, *cacheItem](&agdcache.Config{
Clock: c.Clock,
Count: c.NoECSCount,
})
ecsCache := agdcache.NewLRU[uint64, *cacheItem](&agdcache.LRUConfig{
}))
ecsCache := errors.Must(agdcache.New[cacheKey, *cacheItem](&agdcache.Config{
Clock: c.Clock,
Count: c.ECSCount,
})
}))
c.CacheManager.Add(cacheIDNoECS, cache)
c.CacheManager.Add(cacheIDWithECS, ecsCache)
return &Middleware{
clock: c.Clock,
metrics: c.Metrics,
cloner: c.Cloner,
logger: c.Logger,
@@ -235,7 +245,7 @@ func (mw *Middleware) writeUpstreamResponse(
respIsECS := respIsECSDependent(scope, req.Question[0].Name)
var cache agdcache.Interface[uint64, *cacheItem]
var cache agdcache.Interface[cacheKey, *cacheItem]
if respIsECS {
cache = mw.ecsCache
} else {

View File

@@ -40,5 +40,3 @@ func TestRoundDiv(t *testing.T) {
MaxCount: 100_000,
}))
}
// TODO(a.garipov): Add benchmarks for the new ECS cache key packing.

View File

@@ -19,6 +19,7 @@ import (
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -680,7 +681,9 @@ func newWithCache(
return dnsserver.WithMiddlewares(
h,
ecscache.NewMiddleware(&ecscache.MiddlewareConfig{
Metrics: ecscache.EmptyMetrics{},
Metrics: ecscache.EmptyMetrics{},
// TODO(d.kolyshev): Use fake clock and test expiration.
Clock: timeutil.SystemClock{},
Cloner: agdtest.NewCloner(),
Logger: slogutil.NewDiscardLogger(),
CacheManager: agdcache.EmptyManager{},

View File

@@ -2,9 +2,7 @@ package filter
import (
"context"
"net/netip"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/urlfilter"
)
@@ -56,20 +54,18 @@ type ConfigCustom struct {
// Custom is a custom filter for a client.
type Custom interface {
// DNSResult returns the result of applying the urlfilter DNS filtering
// engine. If the request is not filtered, DNSResult returns nil.
DNSResult(
ctx context.Context,
clientIP netip.Addr,
clientName string,
host string,
rrType dnsmsg.RRType,
isAns bool,
) (res *urlfilter.DNSResult)
// Rules returns the rules used to create the filter. rules must not be
// modified.
Rules() (rules []RuleText)
// SetURLFilterResult applies the DNS filtering engine and sets the values
// in res if any have matched. ok must be true if there is a match. req
// and res must not be nil.
SetURLFilterResult(
ctx context.Context,
req *urlfilter.DNSRequest,
res *urlfilter.DNSResult,
) (ok bool)
}
// ConfigParental is the configuration for parental-control filtering.

View File

@@ -4,14 +4,11 @@ package custom
import (
"context"
"log/slog"
"net/netip"
"strings"
"sync"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/agdurlflt"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/rulelist"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/urlfilter"
)
@@ -45,46 +42,33 @@ func New(c *Config) (f *Filter) {
// init initializes f.immutable.
func (f *Filter) init(ctx context.Context) {
// TODO(a.garipov): Consider making a copy of [strings.Join] for
// [filter.RuleText].
textLen := 0
for _, r := range f.rules {
textLen += len(r) + len("\n")
}
b := &strings.Builder{}
b.Grow(textLen)
for _, r := range f.rules {
stringutil.WriteToBuilder(b, string(r), "\n")
}
// Don't use cache for users' custom filters, because [rulelist.ResultCache]
// doesn't take $client rules into account.
//
// TODO(a.garipov): Consider adding client names to the result-cache keys.
cache := rulelist.EmptyResultCache{}
f.immutable = rulelist.NewImmutable(b.String(), filter.IDCustom, "", cache)
f.immutable = rulelist.NewImmutable(
agdurlflt.RulesToBytes(f.rules),
filter.IDCustom,
"",
rulelist.EmptyResultCache{},
)
f.logger.DebugContext(ctx, "engine compiled", "num_rules", f.immutable.RulesCount())
}
// DNSResult returns the result of applying the custom filter to the query with
// the given parameters.
func (f *Filter) DNSResult(
// SetURLFilterResult applies the DNS filtering engine and sets the values in
// res if any have matched. ok is true if there is a match. req and res must
// not be nil.
func (f *Filter) SetURLFilterResult(
ctx context.Context,
clientIP netip.Addr,
clientName string,
host string,
rrType dnsmsg.RRType,
isAns bool,
) (r *urlfilter.DNSResult) {
req *urlfilter.DNSRequest,
res *urlfilter.DNSResult,
) (ok bool) {
f.initOnce.Do(func() {
f.init(ctx)
})
return f.immutable.DNSResult(clientIP, clientName, host, rrType, isAns)
return f.immutable.SetURLFilterResult(ctx, req, res)
}
// Rules implements the [filter.Custom] interface for *Filter.

View File

@@ -8,6 +8,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/filter/custom"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/filtertest"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/urlfilter"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -29,7 +30,7 @@ func TestFilter(t *testing.T) {
require.NotNil(t, f)
require.Equal(t, rules, f.Rules())
ip := filtertest.IPv4Client
ctx := context.Background()
testCases := []struct {
name string
@@ -57,12 +58,20 @@ func TestFilter(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
dr := f.DNSResult(context.Background(), ip, tc.cliName, tc.host, dns.TypeA, false)
req := &urlfilter.DNSRequest{
ClientIP: filtertest.IPv4Client,
ClientName: tc.cliName,
Hostname: tc.host,
DNSType: dns.TypeA,
}
res := &urlfilter.DNSResult{}
require.NotNil(t, dr)
require.NotNil(t, dr.NetworkRule)
ok := f.SetURLFilterResult(ctx, req, res)
assert.Equal(t, tc.wantRuleStr, dr.NetworkRule.RuleText)
require.True(t, ok)
require.NotNil(t, res.NetworkRule)
assert.Equal(t, tc.wantRuleStr, res.NetworkRule.RuleText)
})
}
}

View File

@@ -25,27 +25,31 @@ type Config struct {
// BlockedServices is the configuration of a blocked-service filter for a
// default filter storage. It must not be nil
BlockedServices *ConfigBlockedServices
BlockedServices *BlockedServicesConfig
// Custom is the configuration of a custom filters storage for a default
// filter storage. It must not be nil
Custom *ConfigCustom
Custom *CustomConfig
// HashPrefix is the hashprefix-filter configuration for a default filter
// storage. It must not be nil
HashPrefix *ConfigHashPrefix
HashPrefix *HashPrefixConfig
// RuleLists is the rule-list configuration for a default filter storage.
// It must not be nil.
RuleLists *ConfigRuleLists
RuleLists *RuleListsConfig
// SafeSearchGeneral is the general safe-search configuration for a default
// filter storage. It must not be nil.
SafeSearchGeneral *ConfigSafeSearch
SafeSearchGeneral *SafeSearchConfig
// SafeSearchYouTube is the YouTube safe-search configuration for a default
// filter storage. It must not be nil.
SafeSearchYouTube *ConfigSafeSearch
SafeSearchYouTube *SafeSearchConfig
// StandardAccess is the standard access configuration for a default filter
// storage. It must not be nil.
StandardAccess *StandardAccessConfig
// CacheManager is the global cache manager. It must not be nil.
CacheManager agdcache.Manager
@@ -66,9 +70,9 @@ type Config struct {
CacheDir string
}
// ConfigBlockedServices is the blocked-service filter configuration for a
// BlockedServicesConfig is the blocked-service filter configuration for a
// default filter storage.
type ConfigBlockedServices struct {
type BlockedServicesConfig struct {
// IndexURL is the URL of the blocked-service filter index. It must not be
// modified after calling [New]. It must not be nil. It is ignored if
// [ConfigBlockedServices.Enabled] is false.
@@ -102,17 +106,17 @@ type ConfigBlockedServices struct {
Enabled bool
}
// ConfigCustom is the configuration of a custom filters storage for a default
// CustomConfig is the configuration of a custom filters storage for a default
// filter storage.
type ConfigCustom struct {
type CustomConfig struct {
// CacheCount is the count of items to keep in the LRU cache of custom
// filters. It must be greater than zero.
CacheCount int
}
// ConfigHashPrefix is the hashprefix-filter configuration for a default filter
// HashPrefixConfig is the hashprefix-filter configuration for a default filter
// storage.
type ConfigHashPrefix struct {
type HashPrefixConfig struct {
// Adult is the optional hashprefix filter for adult content. If nil, no
// adult-content filtering is performed.
Adult *hashprefix.Filter
@@ -126,8 +130,8 @@ type ConfigHashPrefix struct {
NewlyRegistered *hashprefix.Filter
}
// ConfigRuleLists is the rule-list configuration for a default filter storage.
type ConfigRuleLists struct {
// RuleListsConfig is the rule-list configuration for a default filter storage.
type RuleListsConfig struct {
// IndexURL is the URL of the rule-list filter index. It must not be
// modified after calling [New]. It must not be nil.
IndexURL *url.URL
@@ -164,9 +168,9 @@ type ConfigRuleLists struct {
ResultCacheEnabled bool
}
// ConfigSafeSearch is the single safe-search configuration for a default filter
// SafeSearchConfig is the single safe-search configuration for a default filter
// storage.
type ConfigSafeSearch struct {
type SafeSearchConfig struct {
// URL is the HTTP(S) URL of the safe-search rules list. It must not be
// modified after calling [New]. It must not be nil. It is ignored if
// [ConfigSafeSearch.Enabled] is false.

View File

@@ -21,6 +21,7 @@ import (
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/AdguardTeam/urlfilter"
"github.com/c2h5oh/datasize"
)
@@ -150,7 +151,7 @@ func (s *Default) init(c *Config) (err error) {
// initBlockedServices initializes the blocked-service filter in s. c must not
// be nil.
func (s *Default) initBlockedServices(c *ConfigBlockedServices) (err error) {
func (s *Default) initBlockedServices(c *BlockedServicesConfig) (err error) {
if !c.Enabled {
return nil
}
@@ -181,7 +182,7 @@ func (s *Default) initBlockedServices(c *ConfigBlockedServices) (err error) {
// initSafeSearch initializes the safe-search filters in s. gen and yt must not
// be nil.
func (s *Default) initSafeSearch(gen, yt *ConfigSafeSearch) (err error) {
func (s *Default) initSafeSearch(gen, yt *SafeSearchConfig) (err error) {
s.safeSearchGeneral, err = newSafeSearch(s.baseLogger, gen, s.cacheManager, s.cacheDir)
if err != nil {
return fmt.Errorf("general safe search: %w", err)
@@ -199,7 +200,7 @@ func (s *Default) initSafeSearch(gen, yt *ConfigSafeSearch) (err error) {
// arguments must not be empty.
func newSafeSearch(
baseLogger *slog.Logger,
c *ConfigSafeSearch,
c *SafeSearchConfig,
cacheMgr agdcache.Manager,
cacheDir string,
) (f *safesearch.Filter, err error) {
@@ -230,7 +231,7 @@ func newSafeSearch(
// initRuleListRefr initializes the rule-list refresher in s. c must not be
// nil.
func (s *Default) initRuleListRefr(c *ConfigRuleLists) (err error) {
func (s *Default) initRuleListRefr(c *RuleListsConfig) (err error) {
s.ruleListIdxRefr, err = refreshable.New(&refreshable.Config{
Logger: s.baseLogger.With(
slogutil.KeyPrefix, path.Join("filters", string(FilterIDRuleListIndex)),
@@ -269,7 +270,12 @@ func (s *Default) ForConfig(ctx context.Context, c filter.Config) (f filter.Inte
// forClient returns a new filter based on a client configuration. c must not
// be nil.
func (s *Default) forClient(ctx context.Context, c *filter.ConfigClient) (f filter.Interface) {
compConf := &composite.Config{}
compConf := &composite.Config{
// TODO(a.garipov): Find ways of reusing these. Perhaps add Close to
// [filter.Interface]?
URLFilterRequest: &urlfilter.DNSRequest{},
URLFilterResult: &urlfilter.DNSResult{},
}
s.setParental(ctx, compConf, c.Parental)
s.setRuleLists(compConf, c.RuleList)
@@ -364,7 +370,12 @@ func (s *Default) setSafeBrowsing(compConf *composite.Config, c *filter.ConfigSa
// forGroup returns a new filter based on a group configuration. c must not be
// nil.
func (s *Default) forGroup(ctx context.Context, c *filter.ConfigGroup) (f filter.Interface) {
compConf := &composite.Config{}
compConf := &composite.Config{
// TODO(a.garipov): Find ways of reusing these. Perhaps add Close to
// [filter.Interface]?
URLFilterRequest: &urlfilter.DNSRequest{},
URLFilterResult: &urlfilter.DNSResult{},
}
s.setParental(ctx, compConf, c.Parental)
s.setRuleLists(compConf, c.RuleList)

View File

@@ -26,24 +26,24 @@ func TestNew(t *testing.T) {
Host: "index.example",
}
servicesDisabled := &filterstorage.ConfigBlockedServices{
servicesDisabled := &filterstorage.BlockedServicesConfig{
Enabled: false,
}
safeSearchGeneralDisabled := &filterstorage.ConfigSafeSearch{
safeSearchGeneralDisabled := &filterstorage.SafeSearchConfig{
ID: filter.IDGeneralSafeSearch,
Enabled: false,
}
safeSearchYouTubeDisabled := &filterstorage.ConfigSafeSearch{
safeSearchYouTubeDisabled := &filterstorage.SafeSearchConfig{
ID: filter.IDYoutubeSafeSearch,
Enabled: false,
}
testCases := []struct {
services *filterstorage.ConfigBlockedServices
safeSearchGen *filterstorage.ConfigSafeSearch
safeSearchYT *filterstorage.ConfigSafeSearch
services *filterstorage.BlockedServicesConfig
safeSearchGen *filterstorage.SafeSearchConfig
safeSearchYT *filterstorage.SafeSearchConfig
name string
}{{
services: servicesDisabled,

View File

@@ -10,14 +10,16 @@ import (
//
// TODO(a.garipov): Consider using a separate type.
const (
FilterIDBlockedServiceIndex filter.ID = "blocked_service_index"
FilterIDRuleListIndex filter.ID = "rule_list_index"
FilterIDBlockedServiceIndex filter.ID = "blocked_service_index"
FilterIDRuleListIndex filter.ID = "rule_list_index"
FilterIDStandardProfileAccess filter.ID = "standard_profile_access"
)
// Filenames for filter indexes.
const (
indexFileNameBlockedServices = "services.json"
indexFileNameRuleLists = "filters.json"
indexFileNameBlockedServices = "services.json"
indexFileNameRuleLists = "filters.json"
indexFileNameStandardProfileAccess = "standard_profile_access.json"
)
// Constants that define cache identifiers for the cache manager.

View File

@@ -109,7 +109,7 @@ func newDefault(tb testing.TB) (s *filterstorage.Default) {
c := newDisabledConfig(tb, newConfigRuleLists(ruleListIdxURL))
c.BlockedServices = newConfigBlockedServices(svcIdxURL)
c.HashPrefix = &filterstorage.ConfigHashPrefix{
c.HashPrefix = &filterstorage.HashPrefixConfig{
Adult: filtertest.NewHashprefixFilter(tb, filter.IDAdultBlocking),
Dangerous: filtertest.NewHashprefixFilter(tb, filter.IDSafeBrowsing),
NewlyRegistered: filtertest.NewHashprefixFilter(tb, filter.IDNewRegDomains),
@@ -143,26 +143,26 @@ func newDefault(tb testing.TB) (s *filterstorage.Default) {
// entities.
func newDisabledConfig(
tb testing.TB,
rlConf *filterstorage.ConfigRuleLists,
rlConf *filterstorage.RuleListsConfig,
) (c *filterstorage.Config) {
tb.Helper()
return &filterstorage.Config{
BaseLogger: slogutil.NewDiscardLogger(),
Logger: slogutil.NewDiscardLogger(),
BlockedServices: &filterstorage.ConfigBlockedServices{
BlockedServices: &filterstorage.BlockedServicesConfig{
Enabled: false,
},
Custom: &filterstorage.ConfigCustom{
Custom: &filterstorage.CustomConfig{
CacheCount: filtertest.CacheCount,
},
HashPrefix: &filterstorage.ConfigHashPrefix{},
HashPrefix: &filterstorage.HashPrefixConfig{},
RuleLists: rlConf,
SafeSearchGeneral: &filterstorage.ConfigSafeSearch{
SafeSearchGeneral: &filterstorage.SafeSearchConfig{
ID: filter.IDGeneralSafeSearch,
Enabled: false,
},
SafeSearchYouTube: &filterstorage.ConfigSafeSearch{
SafeSearchYouTube: &filterstorage.SafeSearchConfig{
ID: filter.IDYoutubeSafeSearch,
Enabled: false,
},
@@ -177,8 +177,8 @@ func newDisabledConfig(
// newConfigBlockedServices is a test helper that returns a new enabled
// *ConfigBlockedServices with the given index URL. The rest of the fields are
// set to the corresponding [filtertest] values.
func newConfigBlockedServices(indexURL *url.URL) (c *filterstorage.ConfigBlockedServices) {
return &filterstorage.ConfigBlockedServices{
func newConfigBlockedServices(indexURL *url.URL) (c *filterstorage.BlockedServicesConfig) {
return &filterstorage.BlockedServicesConfig{
IndexURL: indexURL,
IndexMaxSize: filtertest.FilterMaxSize,
IndexRefreshTimeout: filtertest.Timeout,
@@ -192,8 +192,8 @@ func newConfigBlockedServices(indexURL *url.URL) (c *filterstorage.ConfigBlocked
// newConfigRuleLists is a test helper that returns a new *ConfigRuleLists with
// the given index URL. The rest of the fields are set to the corresponding
// [filtertest] values.
func newConfigRuleLists(indexURL *url.URL) (c *filterstorage.ConfigRuleLists) {
return &filterstorage.ConfigRuleLists{
func newConfigRuleLists(indexURL *url.URL) (c *filterstorage.RuleListsConfig) {
return &filterstorage.RuleListsConfig{
IndexURL: indexURL,
IndexMaxSize: filtertest.FilterMaxSize,
MaxSize: filtertest.FilterMaxSize,
@@ -209,8 +209,8 @@ func newConfigRuleLists(indexURL *url.URL) (c *filterstorage.ConfigRuleLists) {
// newConfigSafeSearch is a test helper that returns a new enabled
// *ConfigSafeSearch with the given filter URL and ID. The rest of the fields
// are set to the corresponding [filtertest] values.
func newConfigSafeSearch(u *url.URL, id filter.ID) (c *filterstorage.ConfigSafeSearch) {
return &filterstorage.ConfigSafeSearch{
func newConfigSafeSearch(u *url.URL, id filter.ID) (c *filterstorage.SafeSearchConfig) {
return &filterstorage.SafeSearchConfig{
URL: u,
ID: id,
MaxSize: filtertest.FilterMaxSize,

View File

@@ -8,7 +8,6 @@ import (
"path"
"path/filepath"
"slices"
"strings"
"github.com/AdguardTeam/AdGuardDNS/internal/errcoll"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
@@ -81,13 +80,13 @@ func (s *Default) loadIndex(
ctx context.Context,
acceptStale bool,
) (resp *indexResp, err error) {
text, err := s.ruleListIdxRefr.Refresh(ctx, acceptStale)
b, err := s.ruleListIdxRefr.Refresh(ctx, acceptStale)
if err != nil {
return nil, fmt.Errorf("loading index: %w", err)
}
resp = &indexResp{}
err = json.NewDecoder(strings.NewReader(text)).Decode(resp)
err = json.Unmarshal(b, resp)
if err != nil {
return nil, fmt.Errorf("decoding: %w", err)
}

View File

@@ -0,0 +1,252 @@
package filterstorage
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log/slog"
"net/url"
"path/filepath"
"github.com/AdguardTeam/AdGuardDNS/internal/access"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/refreshable"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/netutil/urlutil"
"github.com/AdguardTeam/golibs/service"
"github.com/AdguardTeam/golibs/validate"
"github.com/google/renameio/v2"
)
// StandardAccessStorage is the interface for a storage of standard access
// settings.
type StandardAccessStorage interface {
// Config returns the standard access settings. conf must not be modified
// after calling this method.
Config(ctx context.Context) (conf *access.StandardBlockerConfig, err error)
}
// EmptyStandardAccessStorage is the empty implementation of the
// [StandardAccessStorage] interface.
type EmptyStandardAccessStorage struct{}
// type check
var _ StandardAccessStorage = EmptyStandardAccessStorage{}
// Config implements the [StandardAccessStorage] interface for
// EmptyStandardAccessStorage. It always returns nil.
func (EmptyStandardAccessStorage) Config(
_ context.Context,
) (conf *access.StandardBlockerConfig, err error) {
return nil, nil
}
// StandardAccessConfig is the configuration of a standard access storage for a
// default filter storage.
//
// TODO(e.burkov): Move to another package, when internal/refreshable is moved
// to golibs.
type StandardAccessConfig struct {
// BaseLogger is used to log cache loading.
BaseLogger *slog.Logger
// Logger is used to log refresh operations.
Logger *slog.Logger
// Getter is the storage of standard access settings. It must not be nil.
Getter StandardAccessStorage
// Setter is the standard access to refresh from storage. It must not be
// nil.
Setter access.StandardSetter
// CacheDir is the directory where the cache files are stored.
CacheDir string
}
// StandardAccess updates the standard access settings from storage, caching
// them in the cache directory.
type StandardAccess struct {
getter StandardAccessStorage
logger *slog.Logger
setter access.StandardSetter
latest *access.StandardBlockerConfig
cache *refreshable.Refreshable
cachePath string
}
// NewStandardAccess creates a new properly initialized standard access. c must
// be valid. It uses the latest cached settings if available, use the
// [StandardAccess.Refresh] method to update them.
func NewStandardAccess(ctx context.Context, c *StandardAccessConfig) (s *StandardAccess, err error) {
cachePath := filepath.Join(c.CacheDir, indexFileNameStandardProfileAccess)
refr, err := refreshable.New(&refreshable.Config{
Logger: c.BaseLogger.With(slogutil.KeyPrefix, "standard_access_cache"),
URL: &url.URL{
Scheme: urlutil.SchemeFile,
Path: cachePath,
},
ID: FilterIDStandardProfileAccess,
// Don't set CachePath, Timeout and MaxSize, since this refreshable is
// used in file-only mode. Also don't set Staleness, since it always
// accepts stale.
})
if err != nil {
return nil, fmt.Errorf("creating refreshable: %w", err)
}
s = &StandardAccess{
getter: c.Getter,
logger: c.Logger,
setter: c.Setter,
cache: refr,
cachePath: cachePath,
}
err = s.loadFromCache(ctx)
if err != nil {
if !errors.Is(err, errors.ErrNoValue) {
// Don't wrap the error, since it's informative enough as is.
return nil, err
}
s.logger.WarnContext(ctx, "cache is empty")
s.latest = &access.StandardBlockerConfig{}
}
s.setter.SetConfig(s.latest)
return s, nil
}
// type check
var _ service.Refresher = (*StandardAccess)(nil)
// Refresh implements the [service.Refresher] interface for *StandardAccess.
func (s *StandardAccess) Refresh(ctx context.Context) (err error) {
s.logger.InfoContext(ctx, "refresh started")
defer s.logger.InfoContext(ctx, "refresh finished")
conf, err := s.getter.Config(ctx)
if err != nil {
return err
}
if conf == nil {
conf = &access.StandardBlockerConfig{}
}
if conf.Equal(s.latest) {
s.logger.DebugContext(ctx, "no changes")
return nil
}
s.latest = conf
s.setter.SetConfig(s.latest)
err = s.writeCache()
if err != nil {
return fmt.Errorf("writing cache: %w", err)
}
return nil
}
// StandardAccessVersion is the current schema version of the standard access
// settings cache.
//
// NOTE: Increment this value on every change in [access.StandardBlockerConfig]
// that requires a change in the JSON representation.
const StandardAccessVersion uint = 1
// jsonStandardAccessSettings is the JSON representation of
// [access.StandardBlockerConfig].
type jsonStandardAccessSettings struct {
AllowedNets []netutil.Prefix `json:"allowed_nets"`
BlockedNets []netutil.Prefix `json:"blocked_nets"`
AllowedASN []geoip.ASN `json:"allowed_asns"`
BlockedASN []geoip.ASN `json:"blocked_asns"`
BlocklistDomainRules []string `json:"rules"`
SchemaVersion uint `json:"schema_version"`
}
// standardAccessConfigToJSON converts the standard access settings to the JSON
// representation.
func standardAccessConfigToJSON(conf *access.StandardBlockerConfig) (s *jsonStandardAccessSettings) {
s = &jsonStandardAccessSettings{
AllowedNets: make([]netutil.Prefix, 0, len(conf.AllowedNets)),
BlockedNets: make([]netutil.Prefix, 0, len(conf.BlockedNets)),
AllowedASN: conf.AllowedASN,
BlockedASN: conf.BlockedASN,
BlocklistDomainRules: conf.BlocklistDomainRules,
SchemaVersion: StandardAccessVersion,
}
for _, p := range conf.AllowedNets {
s.AllowedNets = append(s.AllowedNets, netutil.Prefix{Prefix: p})
}
for _, p := range conf.BlockedNets {
s.BlockedNets = append(s.BlockedNets, netutil.Prefix{Prefix: p})
}
return s
}
// toInternal converts the JSON representation of the standard access settings
// to the internal one.
func (s *jsonStandardAccessSettings) toInternal() (conf *access.StandardBlockerConfig) {
return &access.StandardBlockerConfig{
AllowedNets: netutil.UnembedPrefixes(s.AllowedNets),
BlockedNets: netutil.UnembedPrefixes(s.BlockedNets),
AllowedASN: s.AllowedASN,
BlockedASN: s.BlockedASN,
BlocklistDomainRules: s.BlocklistDomainRules,
}
}
// loadFromCache loads the standard access settings from the cache.
func (s *StandardAccess) loadFromCache(ctx context.Context) (err error) {
raw, err := s.cache.Refresh(ctx, true)
if err != nil {
return fmt.Errorf("loading cache: %w", err)
}
err = validate.NotEmptySlice("cache", raw)
if err != nil {
// Don't wrap the error, since it's informative enough as is.
return err
}
settings := &jsonStandardAccessSettings{}
err = json.Unmarshal(raw, settings)
if err != nil {
return fmt.Errorf("decoding cache: %w", err)
}
v := settings.SchemaVersion
err = validate.InRange("schema_version", v, StandardAccessVersion, StandardAccessVersion)
if err != nil {
return fmt.Errorf("malformed cache: %w", err)
}
s.latest = settings.toInternal()
return nil
}
// writeCache writes the latest standard access settings to the cache.
func (s *StandardAccess) writeCache() (err error) {
settings := standardAccessConfigToJSON(s.latest)
b := &bytes.Buffer{}
err = json.NewEncoder(b).Encode(settings)
if err != nil {
return fmt.Errorf("encoding cache: %w", err)
}
return renameio.WriteFile(s.cachePath, b.Bytes(), 0o600)
}

View File

@@ -0,0 +1,210 @@
package filterstorage_test
import (
"context"
"net/netip"
"path/filepath"
"testing"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/access"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/filterstorage"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testTimeout is the common timeout for tests and contexts.
const testTimeout = 1 * time.Second
// testLogger is the common logger for tests.
var testLogger = slogutil.NewDiscardLogger()
// testStandardAccessStorage is the mock implementation of the
// [filterstorage.StandardAccessStorage] interface for tests.
//
// TODO(e.burkov): Move to agdtest.
type testStandardAccessStorage struct {
OnConfig func(ctx context.Context) (conf *access.StandardBlockerConfig, err error)
}
// type check
var _ filterstorage.StandardAccessStorage = (*testStandardAccessStorage)(nil)
// Config implements the [filterstorage.StandardAccessStorage] interface for
// *testStandardAccessStorage.
func (s *testStandardAccessStorage) Config(
ctx context.Context,
) (conf *access.StandardBlockerConfig, err error) {
return s.OnConfig(ctx)
}
// testStandardSetter is the mock implementation of the [access.StandardSetter]
// interface for tests.
//
// TODO(e.burkov): Move to agdtest.
type testStandardSetter struct {
OnSetConfig func(conf *access.StandardBlockerConfig)
}
// type check
var _ access.StandardSetter = (*testStandardSetter)(nil)
// SetConfig implements the [access.StandardSetter] interface for
// *testStandardSetter.
func (s *testStandardSetter) SetConfig(conf *access.StandardBlockerConfig) {
s.OnSetConfig(conf)
}
// panicSetter is the mock implementation of the [access.StandardSetter]
// interface for tests that panics on any call.
var panicSetter = &testStandardSetter{
OnSetConfig: func(conf *access.StandardBlockerConfig) { panic("should not be called") },
}
func TestStandardAccess(t *testing.T) {
t.Parallel()
testConf := &access.StandardBlockerConfig{
AllowedNets: []netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")},
BlockedNets: []netip.Prefix{netip.MustParsePrefix("192.0.2.2/32")},
AllowedASN: []geoip.ASN{10},
BlockedASN: []geoip.ASN{20},
BlocklistDomainRules: []string{
"blocked.std.test",
"@@allowed.std.test",
},
}
errStorage := &testStandardAccessStorage{
OnConfig: func(_ context.Context) (conf *access.StandardBlockerConfig, err error) {
return nil, assert.AnError
},
}
okStorage := &testStandardAccessStorage{
OnConfig: func(_ context.Context) (conf *access.StandardBlockerConfig, err error) {
return testConf, nil
},
}
pt := testutil.PanicT{}
emptySetter := &testStandardSetter{
OnSetConfig: func(conf *access.StandardBlockerConfig) { require.Empty(pt, conf) },
}
testSetter := &testStandardSetter{
OnSetConfig: func(conf *access.StandardBlockerConfig) { require.Equal(pt, testConf, conf) },
}
testCases := []struct {
storage filterstorage.StandardAccessStorage
setter access.StandardSetter
wantRefrErr error
name string
}{{
storage: okStorage,
setter: testSetter,
wantRefrErr: nil,
name: "success",
}, {
storage: filterstorage.EmptyStandardAccessStorage{},
setter: emptySetter,
wantRefrErr: nil,
name: "empty",
}, {
storage: errStorage,
setter: panicSetter,
wantRefrErr: assert.AnError,
name: "error",
}}
for _, tc := range testCases {
setter := &testStandardSetter{
// Use empty setter to ensure that nothing stored in cache.
OnSetConfig: emptySetter.OnSetConfig,
}
newCtx := testutil.ContextWithTimeout(t, testTimeout)
sa, newErr := filterstorage.NewStandardAccess(newCtx, &filterstorage.StandardAccessConfig{
Logger: testLogger,
BaseLogger: testLogger,
Getter: tc.storage,
Setter: setter,
CacheDir: t.TempDir(),
})
require.NoError(t, newErr)
setter.OnSetConfig = tc.setter.SetConfig
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.ContextWithTimeout(t, testTimeout)
err := sa.Refresh(ctx)
require.ErrorIs(t, err, tc.wantRefrErr)
})
}
}
func TestStandardAccess_cache(t *testing.T) {
t.Parallel()
testConf := &access.StandardBlockerConfig{
AllowedNets: []netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")},
BlockedNets: []netip.Prefix{netip.MustParsePrefix("192.0.2.2/32")},
AllowedASN: []geoip.ASN{10},
BlockedASN: []geoip.ASN{20},
BlocklistDomainRules: []string{
"blocked.std.test",
"@@allowed.std.test",
},
}
pt := testutil.PanicT{}
testSetter := &testStandardSetter{
OnSetConfig: func(conf *access.StandardBlockerConfig) {
require.Equal(pt, testConf, conf)
},
}
emptySetter := &testStandardSetter{
OnSetConfig: func(conf *access.StandardBlockerConfig) {
require.Empty(pt, conf)
},
}
testCases := []struct {
setter access.StandardSetter
wantErrMsg string
name string
}{{
setter: testSetter,
wantErrMsg: "",
name: "success",
}, {
setter: panicSetter,
wantErrMsg: "malformed cache: schema_version: out of range: " +
"must be no less than 1, got 0",
name: "bad_version",
}, {
setter: emptySetter,
wantErrMsg: "",
name: "non-existent",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.ContextWithTimeout(t, testTimeout)
_, err := filterstorage.NewStandardAccess(ctx, &filterstorage.StandardAccessConfig{
Logger: testLogger,
BaseLogger: testLogger,
Getter: filterstorage.EmptyStandardAccessStorage{},
Setter: tc.setter,
CacheDir: filepath.Join("./testdata", t.Name()),
})
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
})
}
}

View File

@@ -0,0 +1,4 @@
{
"unknown_field": "value",
"schema_version": 0
}

View File

@@ -0,0 +1,19 @@
{
"allowed_nets": [
"192.0.2.1/32"
],
"blocked_nets": [
"192.0.2.2/32"
],
"allowed_asns": [
10
],
"blocked_asns": [
20
],
"rules": [
"blocked.std.test",
"@@allowed.std.test"
],
"schema_version": 1
}

View File

@@ -81,16 +81,6 @@ type FilterConfig struct {
MaxSize datasize.ByteSize
}
// cacheItem represents an item that we will store in the cache.
type cacheItem struct {
// res is the filtering result.
res filter.Result
// host is the cached normalized hostname for later cache key collision
// checks.
host string
}
// Filter is a filter that matches hosts by their hashes based on a hash-prefix
// table. It should be initially refreshed with [Filter.RefreshInitial].
type Filter struct {
@@ -101,7 +91,7 @@ type Filter struct {
errColl errcoll.Interface
hashprefixMtcs Metrics
metrics filter.Metrics
resCache agdcache.Interface[rulelist.CacheKey, *cacheItem]
resCache agdcache.Interface[rulelist.CacheKey, filter.Result]
id filter.ID
repIP netip.Addr
repFQDN string
@@ -119,7 +109,7 @@ const IDPrefix = "filters/hashprefix"
func NewFilter(c *FilterConfig) (f *Filter, err error) {
id := c.ID
resCache := agdcache.NewLRU[rulelist.CacheKey, *cacheItem](&agdcache.LRUConfig{
resCache := agdcache.NewLRU[rulelist.CacheKey, filter.Result](&agdcache.LRUConfig{
Count: c.CacheCount,
})
@@ -174,10 +164,10 @@ func (f *Filter) FilterRequest(
host, qt, cl := req.Host, req.QType, req.QClass
cacheKey := rulelist.NewCacheKey(host, qt, cl, false)
item, ok := f.itemFromCache(ctx, cacheKey, host)
item, ok := f.resCache.Get(cacheKey)
f.hashprefixMtcs.IncrementLookups(ctx, ok)
if ok {
return f.clonedResult(req.DNS, item.res), nil
return f.clonedResult(req.DNS, item), nil
}
fam, ok := isFilterable(qt)
@@ -196,10 +186,7 @@ func (f *Filter) FilterRequest(
}
if matched == "" {
f.resCache.Set(cacheKey, &cacheItem{
res: nil,
host: host,
})
f.resCache.Set(cacheKey, nil)
return nil, nil
}
@@ -210,35 +197,13 @@ func (f *Filter) FilterRequest(
return nil, err
}
f.setInCache(cacheKey, r, host)
f.setInCache(cacheKey, r)
f.hashprefixMtcs.UpdateCacheSize(ctx, f.resCache.Len())
return r, nil
}
// itemFromCache retrieves a cache item for the given key. host is used to
// detect key collisions. If there is a key collision, it returns nil and
// false.
func (f *Filter) itemFromCache(
ctx context.Context,
key rulelist.CacheKey,
host string,
) (item *cacheItem, ok bool) {
item, ok = f.resCache.Get(key)
if !ok {
return nil, false
}
if item.host != host {
f.logger.WarnContext(ctx, "collision: bad cache item", "item", item, "host", host)
return nil, false
}
return item, true
}
// isFilterable returns true if the question type is filterable. If the type is
// filterable with a blocked page, fam is the address family for the IP
// addresses of the blocked page; otherwise fam is [netutil.AddrFamilyNone].
@@ -335,18 +300,12 @@ func (f *Filter) respForFamily(
// [*filter.ResultModifiedResponse].
//
// See AGDNS-359.
func (f *Filter) setInCache(k rulelist.CacheKey, r filter.Result, host string) {
func (f *Filter) setInCache(k rulelist.CacheKey, r filter.Result) {
switch r := r.(type) {
case *filter.ResultModifiedRequest:
f.resCache.Set(k, &cacheItem{
res: r.Clone(f.cloner),
host: host,
})
f.resCache.Set(k, r.Clone(f.cloner))
case *filter.ResultModifiedResponse:
f.resCache.Set(k, &cacheItem{
res: r.Clone(f.cloner),
host: host,
})
f.resCache.Set(k, r.Clone(f.cloner))
default:
panic(fmt.Errorf("hashprefix: unexpected type for result: %T(%[1]v)", r))
}
@@ -392,13 +351,13 @@ func (f *Filter) refresh(ctx context.Context, acceptStale bool) (err error) {
f.metrics.SetFilterStatus(ctx, string(f.id), time.Now(), count, err)
}()
text, err := f.refr.Refresh(ctx, acceptStale)
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.hashes.Reset(text)
count, err = f.hashes.Reset(b)
if err != nil {
return fmt.Errorf("%s: resetting: %w", f.id, err)
}

View File

@@ -241,7 +241,7 @@ func TestFilter_Refresh(t *testing.T) {
refrCh := make(chan struct{}, 1)
cachePath, srvURL := filtertest.PrepareRefreshable(t, refrCh, testHashes, http.StatusOK)
strg, err := hashprefix.NewStorage("")
strg, err := hashprefix.NewStorage(nil)
require.NoError(t, err)
f, err := hashprefix.NewFilter(&hashprefix.FilterConfig{
@@ -292,7 +292,7 @@ func TestFilter_FilterRequest_staleCache(t *testing.T) {
// Create the filter.
strg, err := hashprefix.NewStorage("")
strg, err := hashprefix.NewStorage(nil)
require.NoError(t, err)
cloner := agdtest.NewCloner()

View File

@@ -6,3 +6,6 @@ import (
// testHashes is the host data for tests.
const testHashes = filtertest.HostAdultContent + "\n"
// testHashesData is the host data for tests.
var testHashesData = []byte(testHashes)

View File

@@ -4,9 +4,9 @@ import (
"context"
"crypto/sha256"
"encoding/hex"
"strings"
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agdurlflt"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/hashprefix"
"github.com/stretchr/testify/assert"
@@ -43,7 +43,7 @@ func TestMatcher(t *testing.T) {
hashStrs[i] = hex.EncodeToString(sum[:])
}
hashes, err := hashprefix.NewStorage(strings.Join(hosts, "\n"))
hashes, err := hashprefix.NewStorage(agdurlflt.RulesToBytes(hosts))
require.NoError(t, err)
ctx := context.Background()

View File

@@ -2,6 +2,7 @@ package hashprefix
import (
"bufio"
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
@@ -15,8 +16,7 @@ import (
//
// TODO(a.garipov): See if we could unexport this.
type Storage struct {
// resetMu makes sure that only one reset is taking place at a time. It
// also protects prev.
// resetMu makes sure that only one reset is taking place at a time.
resetMu *sync.Mutex
// hashSuffixes contains the hashSuffixes map. It is an atomic pointer to
@@ -29,9 +29,11 @@ type Storage struct {
type suffixMap = map[Prefix][]suffix
// NewStorage returns a new hash storage containing hashes of the domain names
// listed in hostnames, one domain name per line, requirements are described in
// [Storage.Reset]. Empty string causes no errors.
func NewStorage(hostnames string) (s *Storage, err error) {
// listed in hostnameData (see [Storage.Reset]). hostnameData may be nil.
//
// TODO(a.garipov): Consider moving the version with the initial data into
// tests.
func NewStorage(hostnameData []byte) (s *Storage, err error) {
s = &Storage{
resetMu: &sync.Mutex{},
hashSuffixes: &atomic.Pointer[suffixMap]{},
@@ -39,8 +41,8 @@ func NewStorage(hostnames string) (s *Storage, err error) {
s.hashSuffixes.Store(&suffixMap{})
if hostnames != "" {
_, err = s.Reset(hostnames)
if hostnameData != nil {
_, err = s.Reset(hostnameData)
if err != nil {
return nil, err
}
@@ -130,23 +132,23 @@ func (s *Storage) loadHashSuffixes(pref Prefix) (sufs []suffix, ok bool) {
}
// Reset resets the hosts in the index using the domain names listed in
// hostnames and returns the total number of processed rules. hostnames should
// be a list of valid, lowercased domain names, one per line, and may include
// hostnameData and returns the total number of processed rules. hostnameData
// should contain valid, lowercased domain names, one per line, and may include
// empty lines and comments ('#' at the first position).
func (s *Storage) Reset(hostnames string) (n int, err error) {
func (s *Storage) Reset(hostnameData []byte) (n int, err error) {
s.resetMu.Lock()
defer s.resetMu.Unlock()
next := make(suffixMap, len(*s.hashSuffixes.Load()))
sc := bufio.NewScanner(strings.NewReader(hostnames))
sc := bufio.NewScanner(bytes.NewReader(hostnameData))
for sc.Scan() {
host := sc.Text()
host := sc.Bytes()
if len(host) == 0 || host[0] == '#' {
continue
}
sum := sha256.Sum256([]byte(host))
sum := sha256.Sum256(host)
pref := Prefix(sum[:PrefixLen])
suf := suffix(sum[PrefixLen:])
next[pref] = append(next[pref], suf)

View File

@@ -5,9 +5,9 @@ import (
"encoding/hex"
"fmt"
"strconv"
"strings"
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agdurlflt"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/hashprefix"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/filtertest"
"github.com/stretchr/testify/assert"
@@ -15,7 +15,7 @@ import (
)
func TestStorage_Hashes(t *testing.T) {
s, err := hashprefix.NewStorage(testHashes)
s, err := hashprefix.NewStorage(testHashesData)
require.NoError(t, err)
h := sha256.Sum256([]byte(filtertest.HostAdultContent))
@@ -30,7 +30,7 @@ func TestStorage_Hashes(t *testing.T) {
}
func TestStorage_Matches(t *testing.T) {
s, err := hashprefix.NewStorage(testHashes)
s, err := hashprefix.NewStorage(testHashesData)
require.NoError(t, err)
got := s.Matches(filtertest.HostAdultContent)
@@ -41,12 +41,12 @@ func TestStorage_Matches(t *testing.T) {
}
func TestStorage_Reset(t *testing.T) {
s, err := hashprefix.NewStorage(testHashes)
s, err := hashprefix.NewStorage(testHashesData)
require.NoError(t, err)
assert.True(t, s.Matches(filtertest.HostAdultContent))
const newHashes = filtertest.Host + "\n"
newHashes := []byte(filtertest.Host + "\n")
n, err := s.Reset(newHashes)
require.NoError(t, err)
@@ -83,7 +83,7 @@ func BenchmarkStorage_Hashes(b *testing.B) {
hosts = append(hosts, fmt.Sprintf("%d."+filtertest.HostAdultContent, i))
}
s, err := hashprefix.NewStorage(strings.Join(hosts, "\n"))
s, err := hashprefix.NewStorage(agdurlflt.RulesToBytes(hosts))
require.NoError(b, err)
var hashPrefixes []hashprefix.Prefix
@@ -105,16 +105,16 @@ func BenchmarkStorage_Hashes(b *testing.B) {
})
}
// Most recent results, on a ThinkPad X13 with a Ryzen Pro 7 CPU:
// Most recent results:
//
// goos: darwin
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/filter/hashprefix
// cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
// BenchmarkStorage_Hashes/1-12 7134991 173.2 ns/op 80 B/op 2 allocs/op
// BenchmarkStorage_Hashes/2-12 6062851 200.0 ns/op 80 B/op 2 allocs/op
// BenchmarkStorage_Hashes/3-12 5138690 233.9 ns/op 80 B/op 2 allocs/op
// BenchmarkStorage_Hashes/4-12 4361190 271.8 ns/op 80 B/op 2 allocs/op
// goos: darwin
// goarch: arm64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/filter/hashprefix
// cpu: Apple M1 Pro
// BenchmarkStorage_Hashes/1-8 10519970 102.7 ns/op 80 B/op 2 allocs/op
// BenchmarkStorage_Hashes/2-8 10045784 118.1 ns/op 80 B/op 2 allocs/op
// BenchmarkStorage_Hashes/3-8 9088449 129.2 ns/op 80 B/op 2 allocs/op
// BenchmarkStorage_Hashes/4-8 8577764 139.4 ns/op 80 B/op 2 allocs/op
}
func BenchmarkStorage_ResetHosts(b *testing.B) {
@@ -125,22 +125,22 @@ func BenchmarkStorage_ResetHosts(b *testing.B) {
hosts = append(hosts, fmt.Sprintf("%d."+filtertest.HostAdultContent, i))
}
hostnames := strings.Join(hosts, "\n")
s, err := hashprefix.NewStorage(hostnames)
hostnameData := agdurlflt.RulesToBytes(hosts)
s, err := hashprefix.NewStorage(hostnameData)
require.NoError(b, err)
b.ReportAllocs()
for b.Loop() {
_, err = s.Reset(hostnames)
_, err = s.Reset(hostnameData)
}
require.NoError(b, err)
// Most recent results:
//
// goos: darwin
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/filter/hashprefix
// cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
// BenchmarkStorage_ResetHosts-12 3814 313231 ns/op 118385 B/op 1009 allocs/op
// goos: darwin
// goarch: arm64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/filter/hashprefix
// cpu: Apple M1 Pro
// BenchmarkStorage_ResetHosts-8 8610 128756 ns/op 118380 B/op 1009 allocs/op
}

View File

@@ -9,12 +9,19 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/rulelist"
"github.com/AdguardTeam/urlfilter"
"github.com/miekg/dns"
)
// Filter is a composite filter based on several types of safe-search and
// rule-list filters.
type Filter struct {
// ufReq is the URLFilter request data to use and reuse during filtering.
ufReq *urlfilter.DNSRequest
// ufRes is the URLFilter result data to use and reuse during filtering.
ufRes *urlfilter.DNSResult
// custom is the custom rule-list filter of the profile, if any.
custom filter.Custom
@@ -26,13 +33,20 @@ type Filter struct {
// services, if any.
svcLists []*rulelist.Immutable
// reqFilters are the safe-browsing and safe-search request filters in the
// composite filter.
// reqFilters are the safe-browsing request filters in the composite filter.
reqFilters []RequestFilter
}
// Config is the configuration structure for the composite filter.
type Config struct {
// URLFilterRequest is the request data to use and reuse during filtering.
// It must not be nil.
URLFilterRequest *urlfilter.DNSRequest
// URLFilterResult is the result data to use and reuse during filtering. It
// must not be nil.
URLFilterResult *urlfilter.DNSResult
// SafeBrowsing is the safe-browsing filter to apply, if any.
SafeBrowsing RequestFilter
@@ -44,10 +58,10 @@ type Config struct {
NewRegisteredDomains RequestFilter
// GeneralSafeSearch is the general safe-search filter to apply, if any.
GeneralSafeSearch RequestFilter
GeneralSafeSearch RequestFilterUF
// YouTubeSafeSearch is the youtube safe-search filter to apply, if any.
YouTubeSafeSearch RequestFilter
YouTubeSafeSearch RequestFilterUF
// Custom is the custom rule-list filter of the profile, if any.
Custom filter.Custom
@@ -61,16 +75,14 @@ type Config struct {
ServiceLists []*rulelist.Immutable
}
// RequestFilter can filter a request based on the request info.
type RequestFilter interface {
// FilterRequest filters a DNS request based on the information provided
// about the request. req must be valid.
FilterRequest(ctx context.Context, req *filter.Request) (r filter.Result, err error)
}
// New returns a new composite filter. c must not be nil.
//
// TODO(a.garipov): Consider reusing composite filters and adding function Set
// and method Reset.
func New(c *Config) (f *Filter) {
f = &Filter{
ufReq: c.URLFilterRequest,
ufRes: c.URLFilterResult,
custom: c.Custom,
ruleLists: c.RuleLists,
svcLists: c.ServiceLists,
@@ -79,15 +91,17 @@ func New(c *Config) (f *Filter) {
// DO NOT change the order of request filters without necessity.
f.reqFilters = appendIfNotNil(f.reqFilters, c.SafeBrowsing)
f.reqFilters = appendIfNotNil(f.reqFilters, c.AdultBlocking)
f.reqFilters = appendIfNotNil(f.reqFilters, c.GeneralSafeSearch)
f.reqFilters = appendIfNotNil(f.reqFilters, c.YouTubeSafeSearch)
f.reqFilters = appendIfNotNilUF(f.reqFilters, c.GeneralSafeSearch, f.ufReq, f.ufRes)
f.reqFilters = appendIfNotNilUF(f.reqFilters, c.YouTubeSafeSearch, f.ufReq, f.ufRes)
f.reqFilters = appendIfNotNil(f.reqFilters, c.NewRegisteredDomains)
return f
}
// appendIfNotNil appends flt to flts if flt is not nil.
func appendIfNotNil(flts []RequestFilter, flt RequestFilter) (res []RequestFilter) {
// appendIfNotNil appends flt to orig if flt is not nil.
func appendIfNotNil(orig []RequestFilter, flt RequestFilter) (flts []RequestFilter) {
flts = orig
if flt != nil {
flts = append(flts, flt)
}
@@ -95,6 +109,27 @@ func appendIfNotNil(flts []RequestFilter, flt RequestFilter) (res []RequestFilte
return flts
}
// appendIfNotNilUF wraps flt and appends it to orig if flt is not nil.
func appendIfNotNilUF(
orig []RequestFilter,
flt RequestFilterUF,
req *urlfilter.DNSRequest,
res *urlfilter.DNSResult,
) (flts []RequestFilter) {
flts = orig
if flt != nil {
// TODO(a.garipov): Consider reusing wrapper structures.
flts = append(flts, &ufRequestFilter{
flt: flt,
req: req,
res: res,
})
}
return flts
}
// type check
var _ filter.Interface = (*Filter)(nil)
@@ -137,6 +172,7 @@ func (f *Filter) FilterRequest(
// Go on.
}
// Secondly, check the safe-browsing and safe-search filters.
for _, rf := range f.reqFilters {
r, err = rf.FilterRequest(ctx, req)
if err != nil {
@@ -156,45 +192,87 @@ func (f *Filter) filterReqWithRuleLists(
ctx context.Context,
req *filter.Request,
) (r filter.Result) {
ip, host, qt := req.RemoteIP, req.Host, req.QType
f.ufReq.Reset()
// TODO(a.garipov): Consider adding a pool of results to the default
// storage and use it here.
ufRes := newURLFilterResult()
if f.custom != nil {
id := filter.IDCustom
f.ufReq.ClientIP = req.RemoteIP
f.ufReq.ClientName = req.ClientName
f.ufReq.DNSType = req.QType
f.ufReq.Hostname = req.Host
// Only use the device name for custom filters of profiles with devices.
dr := f.custom.DNSResult(ctx, ip, req.ClientName, host, qt, false)
mod := rulelist.ProcessDNSRewrites(req, dr.DNSRewrites(), id)
if mod != nil {
// Process the DNS rewrites of the custom list and return them
// first, because custom rules have priority over other rules.
return mod
}
ufRes.add(id, "", dr)
c := newURLFilterResultCollector()
mod := f.filterReqWithCustom(ctx, req, c, f.ufReq, f.ufRes)
if mod != nil {
// Custom DNS rewrites have priority over other rules.
return mod
}
for _, rl := range f.ruleLists {
id, _ := rl.ID()
dr := rl.DNSResult(ip, "", host, qt, false)
mod := rulelist.ProcessDNSRewrites(req, dr.DNSRewrites(), id)
if mod != nil {
// DNS rewrites have higher priority, so a modified request must be
// returned immediately.
return mod
}
// Don't use the device name for non-custom filters.
f.ufReq.ClientName = ""
ufRes.add(id, "", dr)
for _, rl := range f.ruleLists {
f.ufRes.Reset()
ok := rl.SetURLFilterResult(ctx, f.ufReq, f.ufRes)
if ok {
id, _ := rl.ID()
mod = rulelist.ProcessDNSRewrites(req, f.ufRes.DNSRewrites(), id)
if mod != nil {
// DNS rewrites have higher priority, so a modified request must
// be returned immediately.
return mod
}
c.add(id, "", f.ufRes)
}
}
for _, rl := range f.svcLists {
id, svcID := rl.ID()
ufRes.add(id, svcID, rl.DNSResult(ip, "", host, qt, false))
f.ufRes.Reset()
ok := rl.SetURLFilterResult(ctx, f.ufReq, f.ufRes)
if ok {
c.add(id, svcID, f.ufRes)
}
}
return ufRes.toInternal(qt)
return c.toInternal(req.QType)
}
// filterReqWithCustom filters one question's information through the custom
// rule-list filter of the composite filter, if there is one. All arguments
// must not be nil.
func (f *Filter) filterReqWithCustom(
ctx context.Context,
req *filter.Request,
c *urlFilterResultCollector,
ufReq *urlfilter.DNSRequest,
ufRes *urlfilter.DNSResult,
) (res filter.Result) {
if f.custom == nil {
return nil
}
// Only use the device name for custom filters of profiles with devices.
ufReq.ClientName = req.ClientName
ufRes.Reset()
ok := f.custom.SetURLFilterResult(ctx, ufReq, ufRes)
if !ok {
return nil
}
id := filter.IDCustom
mod := rulelist.ProcessDNSRewrites(req, ufRes.DNSRewrites(), id)
if mod != nil {
return mod
}
c.add(id, "", ufRes)
return nil
}
// FilterResponse implements the [filter.Interface] interface for *Filter. It
@@ -242,23 +320,49 @@ func (f *Filter) filterRespWithRuleLists(
host string,
rrType dnsmsg.RRType,
) (r filter.Result) {
ufRes := newURLFilterResult()
f.ufReq.Reset()
f.ufReq.Answer = true
f.ufReq.ClientIP = resp.RemoteIP
f.ufReq.DNSType = rrType
f.ufReq.Hostname = host
c := newURLFilterResultCollector()
for _, rl := range f.ruleLists {
id, _ := rl.ID()
ufRes.add(id, "", rl.DNSResult(resp.RemoteIP, "", host, rrType, true))
f.ufRes.Reset()
ok := rl.SetURLFilterResult(ctx, f.ufReq, f.ufRes)
if ok {
c.add(id, "", f.ufRes)
}
}
if f.custom != nil {
dr := f.custom.DNSResult(ctx, resp.RemoteIP, resp.ClientName, host, rrType, true)
ufRes.add(filter.IDCustom, "", dr)
f.ufReq.ClientName = resp.ClientName
f.ufRes.Reset()
ok := f.custom.SetURLFilterResult(ctx, f.ufReq, f.ufRes)
if ok {
c.add(filter.IDCustom, "", f.ufRes)
}
}
f.ufReq.ClientName = ""
for _, rl := range f.svcLists {
id, svcID := rl.ID()
ufRes.add(id, svcID, rl.DNSResult(resp.RemoteIP, "", host, rrType, true))
f.ufRes.Reset()
ok := rl.SetURLFilterResult(ctx, f.ufReq, f.ufRes)
if ok {
c.add(id, svcID, f.ufRes)
}
}
return ufRes.toInternal(rrType)
return c.toInternal(rrType)
}
// filterHTTPSAnswer filters HTTPS answers information through all rule list
@@ -290,7 +394,7 @@ func (f *Filter) filterSVCBHint(
hint string,
resp *filter.Response,
) (r filter.Result) {
for _, s := range strings.Split(hint, ",") {
for s := range strings.SplitSeq(hint, ",") {
r = f.filterRespWithRuleLists(ctx, resp, s, dns.TypeHTTPS)
if r != nil {
return r

View File

@@ -7,6 +7,7 @@ import (
"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"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
)
@@ -20,7 +21,9 @@ func BenchmarkFilter_FilterReqWithRuleLists(b *testing.B) {
)
f := New(&Config{
RuleLists: []*rulelist.Refreshable{blockingRL},
URLFilterRequest: &urlfilter.DNSRequest{},
URLFilterResult: &urlfilter.DNSResult{},
RuleLists: []*rulelist.Refreshable{blockingRL},
})
ctx := context.Background()
@@ -37,9 +40,9 @@ func BenchmarkFilter_FilterReqWithRuleLists(b *testing.B) {
// Most recent results:
//
// goos: darwin
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/composite
// cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
// BenchmarkFilter_FilterReqWithRuleLists-12 760046 1336 ns/op 592 B/op 12 allocs/op
// goos: linux
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/composite
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkFilter_FilterReqWithRuleLists-16 807964 1904 ns/op 469 B/op 8 allocs/op
}

View File

@@ -1,6 +1,7 @@
package composite_test
import (
"cmp"
"context"
"net/http"
"net/netip"
@@ -20,13 +21,14 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/filter/internal/safesearch"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/urlfilter"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// newReqData returns data for calling FilterRequest. The context uses
// [filtertest.Timeout] and [tb.Cleanup] is used for its cancelation. req uses
// [filtertest.Timeout] and [tb.Cleanup] is used for its cancellation. req uses
// [filtertest.FQDNBlocked], [dns.TypeA], and [dns.ClassINET] for the request
// data.
func newReqData(tb testing.TB) (ctx context.Context, req *filter.Request) {
@@ -52,8 +54,20 @@ func newReqDataWithFQDN(tb testing.TB, fqdn string) (ctx context.Context, req *f
return ctx, req
}
// newComposite is a helper for creating composite filters tests. c may be nil,
// and all zero-value fields in c are replaced with defaults for tests.
func newComposite(tb testing.TB, c *composite.Config) (f *composite.Filter) {
tb.Helper()
c = cmp.Or(c, &composite.Config{})
c.URLFilterRequest = cmp.Or(c.URLFilterRequest, &urlfilter.DNSRequest{})
c.URLFilterResult = cmp.Or(c.URLFilterResult, &urlfilter.DNSResult{})
return composite.New(c)
}
func TestFilter_FilterRequest_customWithClientName(t *testing.T) {
f := composite.New(&composite.Config{
f := newComposite(t, &composite.Config{
Custom: custom.New(&custom.Config{
Logger: slogutil.NewDiscardLogger(),
Rules: []filter.RuleText{
@@ -113,7 +127,7 @@ func TestFilter_FilterRequest_badfilter(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
f := composite.New(&composite.Config{
f := newComposite(t, &composite.Config{
RuleLists: tc.ruleLists,
})
@@ -143,7 +157,7 @@ func TestFilter_FilterRequest_customAllow(t *testing.T) {
Rules: []filter.RuleText{allowRule},
})
f := composite.New(&composite.Config{
f := newComposite(t, &composite.Config{
Custom: customRL,
RuleLists: []*rulelist.Refreshable{blockingRL},
})
@@ -287,12 +301,10 @@ func TestFilter_FilterRequest_dnsrewrite(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c := &composite.Config{
f := newComposite(t, &composite.Config{
Custom: tc.custom,
RuleLists: tc.ruleLists,
}
f := composite.New(c)
})
ctx := context.Background()
res, fltErr := f.FilterRequest(ctx, &filter.Request{
@@ -309,7 +321,7 @@ func TestFilter_FilterRequest_dnsrewrite(t *testing.T) {
}
}
// newCustom is a helper to create a cusotm filter from a rule text.
// newCustom is a helper to create a custom filter from a rule text.
func newCustom(tb testing.TB, text string) (f *custom.Filter) {
tb.Helper()
@@ -334,7 +346,7 @@ func TestFilter_FilterRequest_hostsRules(t *testing.T) {
)
rl := newFromStr(t, rules, filtertest.RuleListID1)
f := composite.New(&composite.Config{
f := newComposite(t, &composite.Config{
RuleLists: []*rulelist.Refreshable{rl},
})
@@ -433,7 +445,7 @@ func TestFilter_FilterRequest_safeSearch(t *testing.T) {
err = gen.Refresh(testutil.ContextWithTimeout(t, filtertest.Timeout), false)
require.NoError(t, err)
f := composite.New(&composite.Config{
f := newComposite(t, &composite.Config{
GeneralSafeSearch: gen,
})
@@ -458,13 +470,13 @@ func TestFilter_FilterRequest_safeSearch(t *testing.T) {
func TestFilter_FilterRequest_services(t *testing.T) {
svcRL := rulelist.NewImmutable(
filtertest.RuleBlockStr,
[]byte(filtertest.RuleBlockStr),
filter.IDBlockedService,
filtertest.BlockedServiceID1,
rulelist.EmptyResultCache{},
)
f := composite.New(&composite.Config{
f := newComposite(t, &composite.Config{
ServiceLists: []*rulelist.Immutable{svcRL},
})
@@ -498,7 +510,7 @@ func TestFilter_FilterResponse(t *testing.T) {
)
blockingRL := newFromStr(t, blockRules, filtertest.RuleListID1)
f := composite.New(&composite.Config{
f := newComposite(t, &composite.Config{
RuleLists: []*rulelist.Refreshable{blockingRL},
})

View File

@@ -0,0 +1,51 @@
package composite
import (
"context"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
"github.com/AdguardTeam/urlfilter"
)
// RequestFilter can filter a request based on the request info.
type RequestFilter interface {
// FilterRequest filters a DNS request based on the information provided
// about the request. req must be valid.
FilterRequest(ctx context.Context, req *filter.Request) (r filter.Result, err error)
}
// RequestFilterUF can filter a request based on the request info and using
// URLFilter data to optimize allocations.
type RequestFilterUF interface {
// FilterRequestUF filters a DNS request based on the information provided
// about the request and using URLFilter data to optimize allocations. req
// must be valid. ufReq and ufRes must not be nil and must be reset.
FilterRequestUF(
ctx context.Context,
req *filter.Request,
ufReq *urlfilter.DNSRequest,
ufRes *urlfilter.DNSResult,
) (r filter.Result, err error)
}
// ufRequestFilter is a wrapper around a [RequestFilterUF] that uses the
// provided URLFilter data.
type ufRequestFilter struct {
flt RequestFilterUF
req *urlfilter.DNSRequest
res *urlfilter.DNSResult
}
// type check
var _ RequestFilter = (*ufRequestFilter)(nil)
// FilterRequest implements the [RequestFilter] interface for *ufRequestFilter.
func (f *ufRequestFilter) FilterRequest(
ctx context.Context,
req *filter.Request,
) (r filter.Result, err error) {
f.req.Reset()
f.res.Reset()
return f.flt.FilterRequestUF(ctx, req, f.req, f.res)
}

View File

@@ -10,10 +10,10 @@ import (
"github.com/miekg/dns"
)
// urlFilterResult is an entity simplifying the collection and compilation of
// urlfilter results. It contains per-pointer indexes of the IDs of filters
// producing network and host rules.
type urlFilterResult struct {
// urlFilterResultCollector is an entity simplifying the collection and
// compilation of urlfilter results. It contains per-pointer indexes of the IDs
// of filters producing network and host rules.
type urlFilterResultCollector struct {
netRuleIDs map[*rules.NetworkRule]filter.ID
hostRuleIDs map[*rules.HostRule]filter.ID
@@ -25,9 +25,9 @@ type urlFilterResult struct {
hostRules6 []*rules.HostRule
}
// newURLFilterResult returns a properly initialized *urlFilterResult.
func newURLFilterResult() (r *urlFilterResult) {
return &urlFilterResult{
// newURLFilterResultCollector returns a properly initialized *urlFilterResult.
func newURLFilterResultCollector() (r *urlFilterResultCollector) {
return &urlFilterResultCollector{
netRuleIDs: map[*rules.NetworkRule]filter.ID{},
hostRuleIDs: map[*rules.HostRule]filter.ID{},
@@ -36,66 +36,61 @@ func newURLFilterResult() (r *urlFilterResult) {
}
}
// add appends the rules from dr to the slices within r. If dr is nil, add does
// nothing.
func (r *urlFilterResult) add(
// add appends the rules from dr to the slices within c. dr must not be nil.
func (c *urlFilterResultCollector) add(
id filter.ID,
svcID filter.BlockedServiceID,
dr *urlfilter.DNSResult,
) {
if dr == nil {
return
}
for _, nr := range dr.NetworkRules {
r.networkRules = append(r.networkRules, nr)
r.netRuleIDs[nr] = id
c.networkRules = append(c.networkRules, nr)
c.netRuleIDs[nr] = id
if svcID != "" {
r.netRuleSvcIDs[nr] = svcID
c.netRuleSvcIDs[nr] = svcID
}
}
r.addHostRules(id, svcID, dr.HostRulesV4, dr.HostRulesV6)
c.addHostRules(id, svcID, dr.HostRulesV4, dr.HostRulesV6)
}
// addHostRules adds the host rules to the result.
func (r *urlFilterResult) addHostRules(
func (c *urlFilterResultCollector) addHostRules(
id filter.ID,
svcID filter.BlockedServiceID,
hostRules4 []*rules.HostRule,
hostRules6 []*rules.HostRule,
) {
for _, hr4 := range hostRules4 {
r.hostRules4 = append(r.hostRules4, hr4)
r.hostRuleIDs[hr4] = id
c.hostRules4 = append(c.hostRules4, hr4)
c.hostRuleIDs[hr4] = id
if svcID != "" {
r.hostRuleSvcIDs[hr4] = svcID
c.hostRuleSvcIDs[hr4] = svcID
}
}
for _, hr6 := range hostRules6 {
r.hostRules6 = append(r.hostRules6, hr6)
r.hostRuleIDs[hr6] = id
c.hostRules6 = append(c.hostRules6, hr6)
c.hostRuleIDs[hr6] = id
if svcID != "" {
r.hostRuleSvcIDs[hr6] = svcID
c.hostRuleSvcIDs[hr6] = svcID
}
}
}
// toInternal converts a result of using several urlfilter rulelists into a
// filter.Result.
func (r *urlFilterResult) toInternal(rrType dnsmsg.RRType) (res filter.Result) {
if nr := rules.GetDNSBasicRule(r.networkRules); nr != nil {
return r.netRuleDataToResult(nr)
func (c *urlFilterResultCollector) toInternal(rrType dnsmsg.RRType) (res filter.Result) {
if nr := rules.GetDNSBasicRule(c.networkRules); nr != nil {
return c.netRuleDataToResult(nr)
}
return r.hostsRulesToResult(rrType)
return c.hostsRulesToResult(rrType)
}
// netRuleDataToResult converts a urlfilter network rule into a filtering
// result.
func (r *urlFilterResult) netRuleDataToResult(nr *rules.NetworkRule) (res filter.Result) {
fltID, ok := r.netRuleIDs[nr]
func (c *urlFilterResultCollector) netRuleDataToResult(nr *rules.NetworkRule) (res filter.Result) {
fltID, ok := c.netRuleIDs[nr]
if !ok {
// Shouldn't happen, since fltID is supposed to be among the filters
// added to the result.
@@ -105,7 +100,7 @@ func (r *urlFilterResult) netRuleDataToResult(nr *rules.NetworkRule) (res filter
var rule filter.RuleText
if fltID == filter.IDBlockedService {
var svcID filter.BlockedServiceID
svcID, ok = r.netRuleSvcIDs[nr]
svcID, ok = c.netRuleSvcIDs[nr]
if !ok {
// Shouldn't happen, since svcID is supposed to be among the filters
// added to the result.
@@ -131,8 +126,8 @@ func (r *urlFilterResult) netRuleDataToResult(nr *rules.NetworkRule) (res filter
}
// hostsRulesToResult converts /etc/hosts-style rules into a filtering result.
func (r *urlFilterResult) hostsRulesToResult(rrType dnsmsg.RRType) (res filter.Result) {
if len(r.hostRules4) == 0 && len(r.hostRules6) == 0 {
func (c *urlFilterResultCollector) hostsRulesToResult(rrType dnsmsg.RRType) (res filter.Result) {
if len(c.hostRules4) == 0 && len(c.hostRules6) == 0 {
return nil
}
@@ -143,24 +138,24 @@ func (r *urlFilterResult) hostsRulesToResult(rrType dnsmsg.RRType) (res filter.R
//
// See also AGDNS-591.
var resHostRule *rules.HostRule
if rrType == dns.TypeA && len(r.hostRules4) > 0 {
resHostRule = r.hostRules4[0]
} else if rrType == dns.TypeAAAA && len(r.hostRules6) > 0 {
resHostRule = r.hostRules6[0]
if rrType == dns.TypeA && len(c.hostRules4) > 0 {
resHostRule = c.hostRules4[0]
} else if rrType == dns.TypeAAAA && len(c.hostRules6) > 0 {
resHostRule = c.hostRules6[0]
} else {
if len(r.hostRules4) > 0 {
resHostRule = r.hostRules4[0]
if len(c.hostRules4) > 0 {
resHostRule = c.hostRules4[0]
} else {
resHostRule = r.hostRules6[0]
resHostRule = c.hostRules6[0]
}
}
return r.hostRuleDataToResult(resHostRule)
return c.hostRuleDataToResult(resHostRule)
}
// hostRuleDataToResult converts a urlfilter host rule into a filtering result.
func (r *urlFilterResult) hostRuleDataToResult(hr *rules.HostRule) (res filter.Result) {
fltID, ok := r.hostRuleIDs[hr]
func (c *urlFilterResultCollector) hostRuleDataToResult(hr *rules.HostRule) (res filter.Result) {
fltID, ok := c.hostRuleIDs[hr]
if !ok {
// Shouldn't happen, since fltID is supposed to be among the filters
// added to the result.
@@ -170,7 +165,7 @@ func (r *urlFilterResult) hostRuleDataToResult(hr *rules.HostRule) (res filter.R
var rule filter.RuleText
if fltID == filter.IDBlockedService {
var svcID filter.BlockedServiceID
svcID, ok = r.hostRuleSvcIDs[hr]
svcID, ok = c.hostRuleSvcIDs[hr]
if !ok {
// Shouldn't happen, since svcID is supposed to be among the filters
// added to the result.

View File

@@ -58,7 +58,7 @@ func NewHashprefixFilterWithRepl(
cachePath, srvURL := PrepareRefreshable(tb, nil, data, http.StatusOK)
strg, err := hashprefix.NewStorage("")
strg, err := hashprefix.NewStorage(nil)
require.NoError(tb, err)
f, err = hashprefix.NewFilter(&hashprefix.FilterConfig{

View File

@@ -19,6 +19,8 @@ import (
// as well as creates a cache file. If reqCh not nil, a signal is sent every
// time the server is called. The server uses [ServerName] as the value of the
// Server header.
//
// TODO(a.garipov): Rewrite to use []byte for text.
func PrepareRefreshable(
tb testing.TB,
reqCh chan<- struct{},

View File

@@ -2,6 +2,7 @@
package refreshable
import (
"bytes"
"context"
"fmt"
"io"
@@ -26,6 +27,8 @@ import (
// Refreshable contains the logic common to filters and indexes that can refresh
// themselves from a file and a URL.
//
// TODO(a.garipov, e.burkov): Move to golibs.
type Refreshable struct {
logger *slog.Logger
http *agdhttp.Client
@@ -48,20 +51,26 @@ type Config struct {
// ID is the filter list ID for this filter.
ID filter.ID
// CachePath is the path to the file containing the cached data.
// CachePath is the path to the file containing the cached data. It only
// used for non-file URLs.
CachePath string
// Staleness is the time after which a file is considered stale.
// Staleness is the time after which a file is considered stale. It should
// be positive, otherwise any cache will be discarded.
Staleness time.Duration
// Timeout is the timeout for the HTTP client used by this refreshable.
// Timeout is the timeout for the HTTP client used by this refreshable. It
// must be positive, if the non-file URL is used.
Timeout time.Duration
// MaxSize is the maximum size of the downloadable data.
// MaxSize is the maximum size of the downloadable data. It must be
// positive, if the non-file URL is used.
MaxSize datasize.ByteSize
}
// New returns a new refreshable. c must not be nil.
//
// TODO(e.burkov): Consider validating c more thoroughly.
func New(c *Config) (f *Refreshable, err error) {
if c.URL == nil {
return nil, fmt.Errorf("refreshable.New: nil url for refreshable with ID %q", c.ID)
@@ -87,31 +96,31 @@ func New(c *Config) (f *Refreshable, err error) {
// load the data from its URL when there is already a file in the cache
// directory, regardless of its staleness.
//
// TODO(a.garipov): Consider making refresh return a reader instead of a string.
func (f *Refreshable) Refresh(ctx context.Context, acceptStale bool) (text string, err error) {
// TODO(a.garipov): Consider making refresh return a reader instead of bytes.
func (f *Refreshable) Refresh(ctx context.Context, acceptStale bool) (b []byte, err error) {
defer func() { err = errors.Annotate(err, "%s: %w", f.id) }()
if strings.EqualFold(f.url.Scheme, urlutil.SchemeFile) {
text, err = f.refreshFromFileOnly(ctx)
b, err = f.refreshFromFileOnly(ctx)
} else {
text, err = f.useCachedOrRefreshFromURL(ctx, acceptStale)
b, err = f.useCachedOrRefreshFromURL(ctx, acceptStale)
}
return text, err
return b, err
}
// refreshFromFileOnly refreshes from the file in the URL. It must only be
// called when the URL of this refreshable is a file URI.
func (f *Refreshable) refreshFromFileOnly(ctx context.Context) (text string, err error) {
func (f *Refreshable) refreshFromFileOnly(ctx context.Context) (b []byte, err error) {
filePath := f.url.Path
f.logger.InfoContext(ctx, "using data from file", "path", filePath)
text, err = f.refreshFromFile(true, filePath, time.Time{})
b, err = f.refreshFromFile(true, filePath, time.Time{})
if err != nil {
return "", fmt.Errorf("refreshing from file %q: %w", filePath, err)
return nil, fmt.Errorf("refreshing from file %q: %w", filePath, err)
}
return text, nil
return b, nil
}
// useCachedOrRefreshFromURL reloads the data from the cache file or the http
@@ -122,46 +131,47 @@ func (f *Refreshable) refreshFromFileOnly(ctx context.Context) (text string, err
func (f *Refreshable) useCachedOrRefreshFromURL(
ctx context.Context,
acceptStale bool,
) (text string, err error) {
) (b []byte, err error) {
// TODO(e.burkov): Add [timeutil.Clock].
now := time.Now()
text, err = f.refreshFromFile(acceptStale, f.cachePath, now)
b, err = f.refreshFromFile(acceptStale, f.cachePath, now)
if err != nil {
return "", fmt.Errorf("refreshing from cache file %q: %w", f.cachePath, err)
return nil, fmt.Errorf("refreshing from cache file %q: %w", f.cachePath, err)
}
if text == "" {
if len(b) == 0 {
ru := urlutil.RedactUserinfo(f.url)
f.logger.InfoContext(ctx, "refreshing from url", "url", ru)
text, err = f.refreshFromURL(ctx, now)
b, err = f.refreshFromURL(ctx, now)
if err != nil {
return "", fmt.Errorf("refreshing from url %q: %w", ru, err)
return nil, fmt.Errorf("refreshing from url %q: %w", ru, err)
}
} else {
f.logger.InfoContext(ctx, "using cached data from file", "path", f.cachePath)
}
return text, nil
return b, nil
}
// refreshFromFile loads data from filePath if the file's mtime shows that it's
// still fresh relative to updTime. If acceptStale is true, and the file
// exists, the data is read from there regardless of its staleness. If err is
// nil and text is empty, a refresh from a URL is required.
// exists, the data is read from there regardless of its staleness. If both b
// and err are nil, a refresh from a URL is required.
func (f *Refreshable) refreshFromFile(
acceptStale bool,
filePath string,
updTime time.Time,
) (text string, err error) {
) (b []byte, err error) {
// #nosec G304 -- Assume that filePath is always either cacheDir + a valid,
// no-slash ID or a path from the index env.
file, err := os.Open(filePath)
if errors.Is(err, os.ErrNotExist) {
// File does not exist. Refresh from the URL.
return "", nil
return nil, nil
} else if err != nil {
return "", fmt.Errorf("opening refreshable file: %w", err)
return nil, fmt.Errorf("opening refreshable file: %w", err)
}
defer func() { err = errors.WithDeferred(err, file.Close()) }()
@@ -169,21 +179,21 @@ func (f *Refreshable) refreshFromFile(
var fi fs.FileInfo
fi, err = file.Stat()
if err != nil {
return "", fmt.Errorf("reading refreshable file stat: %w", err)
return nil, fmt.Errorf("reading refreshable file stat: %w", err)
}
if mtime := fi.ModTime(); !mtime.Add(f.staleness).After(updTime) {
return "", nil
return nil, nil
}
}
b := &strings.Builder{}
_, err = io.Copy(b, file)
// Consider cache files to be of a prevalidated size.
b, err = io.ReadAll(file)
if err != nil {
return "", fmt.Errorf("reading refreshable file: %w", err)
return nil, fmt.Errorf("reading refreshable file: %w", err)
}
return b.String(), nil
return b, nil
}
// refreshFromURL loads the data from u, puts it into the file specified by
@@ -191,18 +201,18 @@ func (f *Refreshable) refreshFromFile(
func (f *Refreshable) refreshFromURL(
ctx context.Context,
updTime time.Time,
) (text string, err error) {
) (b []byte, err error) {
// TODO(a.garipov): Cache these like renameio recommends.
tmpDir := renameio.TempDir(filepath.Dir(f.cachePath))
tmpFile, err := renameio.TempFile(tmpDir, f.cachePath)
if err != nil {
return "", fmt.Errorf("creating temporary refreshable file: %w", err)
return nil, fmt.Errorf("creating temporary refreshable file: %w", err)
}
defer func() { err = f.withDeferredTmpCleanup(err, tmpFile, updTime) }()
resp, err := f.http.Get(ctx, f.url)
if err != nil {
return "", fmt.Errorf("requesting: %w", err)
return nil, fmt.Errorf("requesting: %w", err)
}
defer func() { err = errors.WithDeferred(err, resp.Body.Close()) }()
@@ -218,24 +228,24 @@ func (f *Refreshable) refreshFromURL(
err = agdhttp.CheckStatus(resp, http.StatusOK)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return "", err
return nil, err
}
b := &strings.Builder{}
mw := io.MultiWriter(b, tmpFile)
buf := &bytes.Buffer{}
mw := io.MultiWriter(buf, tmpFile)
_, err = io.Copy(mw, ioutil.LimitReader(resp.Body, f.maxSize.Bytes()))
if err != nil {
return "", agdhttp.WrapServerError(fmt.Errorf("reading into file: %w", err), resp)
return nil, agdhttp.WrapServerError(fmt.Errorf("reading into file: %w", err), resp)
}
// TODO(a.garipov): Make a more sophisticated data size ratio check.
//
// See AGDNS-598.
if b.Len() == 0 {
return "", agdhttp.WrapServerError(errors.Error("empty text, not resetting"), resp)
if buf.Len() == 0 {
return nil, agdhttp.WrapServerError(errors.Error("empty text, not resetting"), resp)
}
return b.String(), nil
return buf.Bytes(), nil
}
// withDeferredTmpCleanup is a helper that performs the necessary cleanups and

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