mirror of
https://github.com/AdguardTeam/AdGuardDNS.git
synced 2025-12-23 23:38:37 -05:00
Sync v2.15.1
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
2
Makefile
2
Makefile
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -112,6 +112,7 @@ Supported IDs:
|
||||
- `profiledb_full`
|
||||
- `profiledb`
|
||||
- `rulestat`
|
||||
- `standard_profile_access`
|
||||
- `ticket_rotator`
|
||||
- `tlsconfig`
|
||||
- `websvc`
|
||||
|
||||
@@ -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
78
go.mod
@@ -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
185
go.sum
@@ -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=
|
||||
|
||||
15
go.work.sum
15
go.work.sum
@@ -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=
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
38
internal/access/blocker.go
Normal file
38
internal/access/blocker.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
57
internal/access/profileconstructor.go
Normal file
57
internal/access/profileconstructor.go
Normal 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,
|
||||
})
|
||||
}
|
||||
183
internal/access/standardaccess.go
Normal file
183
internal/access/standardaccess.go
Normal 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
|
||||
}
|
||||
}
|
||||
58
internal/access/standardaccess_test.go
Normal file
58
internal/access/standardaccess_test.go
Normal 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
|
||||
}
|
||||
13
internal/agdcache/agdcache_test.go
Normal file
13
internal/agdcache/agdcache_test.go
Normal 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
|
||||
)
|
||||
126
internal/agdcache/default.go
Normal file
126
internal/agdcache/default.go
Normal 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()
|
||||
}
|
||||
182
internal/agdcache/default_test.go
Normal file
182
internal/agdcache/default_test.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
31
internal/agdtest/profile.go
Normal file
31
internal/agdtest/profile.go
Normal 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)
|
||||
}
|
||||
68
internal/agdurlflt/agdurlflt.go
Normal file
68
internal/agdurlflt/agdurlflt.go
Normal 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()
|
||||
}
|
||||
57
internal/agdurlflt/agdurlflt_test.go
Normal file
57
internal/agdurlflt/agdurlflt_test.go
Normal 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
|
||||
}
|
||||
@@ -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{},
|
||||
})
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
96
internal/backendpb/standardaccess.go
Normal file
96
internal/backendpb/standardaccess.go
Normal 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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
5
internal/dnsserver/cache/cache.go
vendored
5
internal/dnsserver/cache/cache.go
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()) },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
@@ -40,5 +40,3 @@ func TestRoundDiv(t *testing.T) {
|
||||
MaxCount: 100_000,
|
||||
}))
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Add benchmarks for the new ECS cache key packing.
|
||||
|
||||
@@ -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{},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
252
internal/filter/filterstorage/standardaccess.go
Normal file
252
internal/filter/filterstorage/standardaccess.go
Normal 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)
|
||||
}
|
||||
210
internal/filter/filterstorage/standardaccess_test.go
Normal file
210
internal/filter/filterstorage/standardaccess_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"unknown_field": "value",
|
||||
"schema_version": 0
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
})
|
||||
|
||||
|
||||
51
internal/filter/internal/composite/request.go
Normal file
51
internal/filter/internal/composite/request.go
Normal 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)
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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{},
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user