Sync v2.2.0

This commit is contained in:
Andrey Meshkov 2023-06-11 12:58:40 +03:00
parent 17d0b4d513
commit 16fd7a2fd0
214 changed files with 12156 additions and 4969 deletions

2
.gitignore vendored
View File

@ -14,7 +14,7 @@
/github-mirror/
AdGuardDNS
asn.mmdb
config.yml
config.yaml
country.mmdb
dnsdb.bolt
querylog.jsonl

View File

@ -11,6 +11,117 @@ The format is **not** based on [Keep a Changelog][kec], since the project
## AGDNS-1498 / Build 527
* Object `ratelimit` has a new property, `connection_limit`, which allows
setting stream-connection limits. Example configuration:
```yaml
ratelimit:
# …
connection_limit:
enabled: true
stop: 1000
resume: 800
```
## AGDNS-1383 / Build 525
* The environment variable `PROFILES_CACHE_PATH` is now sensitive to the file
extension. Use `.json` for the previous behavior of encoding the cache into
a JSON file or `.pb` for encoding it into protobuf. Other extensions are
invalid.
## AGDNS-1381 / Build 518
* The new object `network` has been added:
```yaml
network:
so_sndbuf: 0
so_rcvbuf: 0
```
## AGDNS-1383 / Build 515
* The environment variable `PROFILES_CACHE_PATH` now has a new special value,
`none`, which disables profile caching entirely. The default value of
`./profilecache.json` has not been changed.
## AGDNS-1479 / Build 513
* The profile-cache version has been changed to `6`. Versions of the profile
cache from `3` to `5` are invalid and should not be reused.
## AGDNS-1473 / Build 506
* The profile-cache version has been changed to `5`.
## AGDNS-1247 / Build 484
* The new object `interface_listeners` has been added:
```yaml
interface_listeners:
channel_buffer_size: 1000
list:
eth0_plain_dns:
interface: 'eth0'
port': 53
eth0_plain_dns_secondary:
interface: 'eth0'
port': 5353
```
* The objects within the `server_groups.*.servers` array have a new optional
property, `bind_interfaces`:
```yaml
server_groups:
-
# …
servers:
- name: 'default_dns'
# …
bind_interfaces:
- id: 'eth0_plain_dns'
subnet: '127.0.0.0/8'
- id: 'eth0_plain_dns_secondary'
subnet: '127.0.0.0/8'
```
It is mutually exclusive with the current `bind_addresses` field.
## AGDNS-1406 / Build 480
* The default behavior of the environment variable `DNSDB_PATH` has been
changed. Previously, if the variable was unset then the default value,
`./dnsdb.bolt`, was used, but if it was an empty string, DNSDB was disabled.
Now both unset and empty value disable DNSDB, which is consistent with the
documentation.
This means that DNSDB is disabled by default.
* The default configuration file path has been changed from `config.yml` to
<code>./config.y<strong>a</strong>ml</code> for consistency with other
services.
## AGDNS-916 / Build 456
* `ratelimit` now defines rate of requests per second for IPv4 and IPv6
@ -181,7 +292,7 @@ The format is **not** based on [Keep a Changelog][kec], since the project
## AGDNS-842 / Build 372
* The new environment variable `PROFILES_CACHE_PATH` has been added. Its
* The new environment variable `PROFILES_CACHE_PATH` has been added. Its
default value is `./profilecache.json`. Adjust the value, if necessary.
@ -189,7 +300,7 @@ The format is **not** based on [Keep a Changelog][kec], since the project
## AGDNS-891 / Build 371
* The property `server` of `upstream` object has been changed. Now it
is a URL optionally starting with `tcp://` or `udp://`, and then an address
is a URL optionally starting with `tcp://` or `udp://`, and then an address
in `ip:port` format.
```yaml

View File

@ -1 +1 @@
See Adguard Home [`HACKING.md`](https://github.com/AdguardTeam/AdGuardHome/blob/master/HACKING.md).
See the [Adguard Code Guidelines](https://github.com/AdguardTeam/CodeGuidelines/).

View File

@ -59,6 +59,9 @@ go-gen:
cd ./internal/agd/ && "$(GO.MACRO)" run ./country_generate.go
cd ./internal/geoip/ && "$(GO.MACRO)" run ./asntops_generate.go
cd ./internal/profiledb/internal/filecachepb/ &&\
protoc --go_opt=paths=source_relative --go_out=. ./filecache.proto
go-check: go-tools go-lint go-test
# A quick check to make sure that all operating systems relevant to the

View File

@ -86,7 +86,7 @@ following features:
you need that.
[rules system]: https://adguard-dns.io/kb/general/dns-filtering-syntax/
[API]: https://adguard-dns.io/kb/private-dns/api/
[API]: https://adguard-dns.io/kb/private-dns/api/overview/

View File

@ -43,6 +43,15 @@ ratelimit:
# Time between two updates of allow list.
refresh_interval: 1h
# Configuration for the stream connection limiting.
connection_limit:
enabled: true
# The point at which the limiter stops accepting new connections. Once
# the number of active connections reaches this limit, new connections
# wait for the number to decrease below resume.
stop: 1000
resume: 800
# DNS cache configuration.
cache:
# The type of cache to use. Can be 'simple' (a simple LRU cache) or 'ecs'
@ -257,6 +266,21 @@ filtering_groups:
block_private_relay: false
block_firefox_canary: true
# The configuration for the device-listening feature. Works only on Linux with
# SO_BINDTODEVICE support.
interface_listeners:
# The size of the buffers of the channels used to dispatch TCP connections
# and UDP sessions.
channel_buffer_size: 1000
# List is the mapping of interface-listener IDs to their configuration.
list:
'eth0_plain_dns':
interface: 'eth0'
port: 53
'eth0_plain_dns_secondary':
interface: 'eth0'
port: 5353
# Server groups and servers.
server_groups:
- name: 'adguard_dns_default'
@ -302,8 +326,13 @@ server_groups:
# See README for the list of protocol values.
protocol: 'dns'
linked_ip_enabled: true
bind_addresses:
- '127.0.0.1:53'
# Either bind_interfaces or bind_addresses (see below) can be used for
# the plain-DNS servers.
bind_interfaces:
- id: 'eth0_plain_dns'
subnet: '127.0.0.0/8'
- id: 'eth0_plain_dns_secondary'
subnet: '127.0.0.0/8'
- name: 'default_dot'
protocol: 'tls'
linked_ip_enabled: false
@ -351,3 +380,12 @@ connectivity_check:
# Additional information to be exposed through metrics.
additional_metrics_info:
test_key: 'test_value'
# Network settings.
network:
# Defines the size of socket send buffer in bytes. Default is zero (uses
# system settings).
so_sndbuf: 0
# Defines the size of socket receive buffer in bytes. Default is zero
# (uses system settings).
so_rcvbuf: 0

View File

@ -6,9 +6,12 @@ configuration file with comments.
## Contents
* [Recommended values](#recommended)
* [Recommended values and notes](#recommended)
* [Result cache sizes](#recommended-result_cache)
* [`SO_RCVBUF` and `SO_SNDBUF` on Linux](#recommended-buffers)
* [Connection limiter](#recommended-connection_limit)
* [Rate limiting](#ratelimit)
* [Stream connection limit](#ratelimit-connection_limit)
* [Cache](#cache)
* [Upstream](#upstream)
* [Healthcheck](#upstream-healthcheck)
@ -21,11 +24,13 @@ configuration file with comments.
* [Adult-content blocking](#adult_blocking)
* [Filters](#filters)
* [Filtering groups](#filtering_groups)
* [Network interface listeners](#interface_listeners)
* [Server groups](#server_groups)
* [TLS](#server_groups-*-tls)
* [DDR](#server_groups-*-ddr)
* [Servers](#server_groups-*-servers-*)
* [Connectivity check](#connectivity-check)
* [Network settings](#network)
* [Additional metrics information](#additional_metrics_info)
[dist]: ../config.dist.yml
@ -34,7 +39,7 @@ configuration file with comments.
## <a href="#recommended" id="recommended" name="recommended">Recommended values</a>
## <a href="#recommended" id="recommended" name="recommended">Recommended values and notes</a>
### <a href="#recommended-result_cache" id="recommended-result_cache" name="recommended-result_cache">Result cache sizes</a>
@ -59,6 +64,55 @@ from answers, you'll need to multiply the value from the statistic by 5 or 6.
### <a href="#recommended-buffers" id="recommended-buffers" name="recommended-buffers">`SO_RCVBUF` and `SO_SNDBUF` on Linux</a>
On Linux OSs the values for these socket options coming from the configuration
file (parameters [`network.so_rcvbuf`](#network-so_rcvbuf) and
[`network.so_sndbuf`](#network-so_sndbuf)) is doubled, and the maximum and
minimum values are controlled by the values in `/proc/`. See `man 7 socket`:
> `SO_RCVBUF`
>
> \[…\] The kernel doubles this value (to allow space for bookkeeping
> overhead) when it is set using setsockopt(2), and this doubled value is
> returned by getsockopt(2). The default value is set by the
> `/proc/sys/net/core/rmem_default` file, and the maximum allowed value is set
> by the `/proc/sys/net/core/rmem_max` file. The minimum (doubled) value for
> this option is `256`.
>
> \[…\]
>
> `SO_SNDBUF`
>
> \[…\] The default value is set by the `/proc/sys/net/core/wmem_default`
> file, and the maximum allowed value is set by the
> `/proc/sys/net/core/wmem_max` file. The minimum (doubled) value for this
> option is `2048`.
### <a href="#recommended-connection_limit" id="recommended-connection_limit" name="recommended-connection_limit">Stream connection limit</a>
Currently, there are the following recommendations for parameters
[`ratelimit.connection_limit.stop`](#ratelimit-connection_limit-stop) and
[`ratelimit.connection_limit.resume`](#ratelimit-connection_limit-resume):
* `stop` should be about 25 % above the current maximum daily number of used
TCP sockets. That is, if the instance currently has a maximum of 100 000
TCP sockets in use every day, `stop` should be set to about `125000`.
* `resume` should be about 20 % above the current maximum daily number of used
TCP sockets. That is, if the instance currently has a maximum of 100 000
TCP sockets in use every day, `resume` should be set to about `120000`.
**NOTE:** The number of active stream-connections includes sockets that are
in the process of accepting new connections but have not yet accepted one. That
means that `resume` should be greater than the number of bound addresses.
These recommendations are to be revised based on the metrics.
## <a href="#ratelimit" id="ratelimit" name="ratelimit">Rate limiting</a>
The `ratelimit` object has the following properties:
@ -138,6 +192,30 @@ by `ipv4-subnet_key_len`) that made 15 requests in one second or 6 requests
(one above `rps`) every second for 10 seconds within one minute, the client is
blocked for `back_off_duration`.
### <a href="#ratelimit-connection_limit" id="ratelimit-connection_limit" name="ratelimit-connection_limit">Stream connection limit</a>
The `connection_limit` object has the following properties:
* <a href="#ratelimit-connection_limit-enabled" id="ratelimit-connection_limit-enabled" name="ratelimit-connection_limit-enabled">`enabled`</a>:
Whether or not the stream-connection limit should be enforced.
**Example:** `true`.
* <a href="#ratelimit-connection_limit-stop" id="ratelimit-connection_limit-stop" name="ratelimit-connection_limit-stop">`stop`</a>:
The point at which the limiter stops accepting new connections. Once the
number of active connections reaches this limit, new connections wait for
the number to decrease to or below `resume`.
**Example:** `1000`.
* <a href="#ratelimit-connection_limit-resume" id="ratelimit-connection_limit-resume" name="ratelimit-connection_limit-resume">`resume`</a>:
The point at which the limiter starts accepting new connections again after
reaching `stop`.
**Example:** `800`.
See also [notes on these parameters](#recommended-connection_limit).
[env-consul_allowlist_url]: environment.md#CONSUL_ALLOWLIST_URL
@ -660,6 +738,39 @@ The items of the `filtering_groups` array have the following properties:
## <a href="#interface_listeners" id="interface_listeners" name="interface_listeners">Network interface listeners</a>
**NOTE:** The network interface listening works only on Linux with
`SO_BINDTODEVICE` support (2.0.30 and later) and properly setup IP routes. See
the [section on testing `SO_BINDTODEVICE` using Docker][dev-btd].
The `interface_listeners` object has the following properties:
* <a href="#ifl-channel_buffer_size" id="ifl-channel_buffer_size" name="ifl-channel_buffer_size">`channel_buffer_size`</a>:
The size of the buffers of the channels used to dispatch TCP connections and
UDP sessions.
**Example:** `1000`.
* <a href="#ifl-list" id="ifl-list" name="ifl-list">`list`</a>:
The mapping of interface-listener IDs to their configuration.
**Property example:**
```yaml
list:
'eth0_plain_dns':
interface: 'eth0'
port: 53
'eth0_plain_dns_secondary':
interface: 'eth0'
port: 5353
```
[dev-btd]: development.md#testing-bindtodevice
## <a href="#server_groups" id="server_groups" name="server_groups">Server groups</a>
The items of the `server_groups` array have the following properties:
@ -829,10 +940,25 @@ The items of the `servers` array have the following properties:
**Example:** `true`.
* <a href="#sg-s-*-bind_addresses" id="sg-s-*-bind_addresses" name="sg-s-*-bind_addresses">`bind_addresses`</a>:
The array of `ip:port` addresses to listen on.
The array of `ip:port` addresses to listen on. If `bind_addresses` is set,
`bind_interfaces` (see below) should not be set.
**Example:** `[127.0.0.1:53, 192.168.1.1:53]`.
* <a href="#sg-s-*-bind_interfaces" id="sg-s-*-bind_interfaces" name="sg-s-*-bind_interfaces">`bind_interfaces`</a>:
The array of [interface listener](#ifl-list) data. If `bind_interfaces` is
set, `bind_addresses` (see above) should not be set.
**Property example:**
```yaml
'bind_interfaces':
- 'id': eth0_plain_dns'
'subnet': '172.17.0.0/16'
- 'id': eth0_plain_dns_secondary'
'subnet': '172.17.0.0/16'
```
* <a href="#sg-s-*-dnscrypt" id="sg-s-*-dnscrypt" name="sg-s-*-dnscrypt">`dnscrypt`</a>:
The optional DNSCrypt configuration object. It has the following
properties:
@ -886,6 +1012,28 @@ The `connectivity_check` object has the following properties:
## <a href="#network" id="network" name="network">Network settings</a>
The `network` object has the following properties:
* <a href="#network-so_rcvbuf" id="network-so_rcvbuf" name="network-so_rcvbuf">`so_rcvbuf`</a>:
The size of socket receive buffer (`SO_RCVBUF`), in bytes. Default is zero,
which means use the default system settings.
See also [notes on these parameters](#recommended-buffers).
**Example:** `1048576`.
* <a href="#network-so_sndbuf" id="network-so_sndbuf" name="network-so_sndbuf">`so_sndbuf`</a>:
The size of socket send buffer (`SO_SNDBUF`), in bytes. Default is zero,
which means use the default system settings.
See also [notes on these parameters](#recommended-buffers).
**Example:** `1048576`.
## <a href="#additional_metrics_info" id="additional_metrics_info" name="additional_metrics_info">Additional metrics information</a>
The `additional_metrics_info` object is a map of strings with extra information

View File

@ -87,6 +87,17 @@ In the `ADDITIONAL SECTION`, the following debug information is returned:
```none
asn.adguard-dns.com. 10 CH TXT "1234"
```
* <a href="#additional-subdivision" id="additional-subdivision" name="additional-subdivision">`subdivision`</a>:
User's location subdivision code. This field could be empty even if user's
country code is present. The full name is `subdivision.adguard-dns.com`.
**Example:**
```none
country.adguard-dns.com. 10 CH TXT "US"
subdivision.adguard-dns.com. 10 CH TXT "CA"
```
The following debug records can have one of two prefixes: `req` or `resp`. The
prefix depends on whether the filtering was applied to the request or the

View File

@ -68,10 +68,26 @@ This is not an extensive list. See `../Makefile`.
</dd>
<dt><code>make go-gen</code></dt>
<dd>
Regenerate the automatically generated Go files. Those generated files
are <code>../internal/agd/country_generate.go</code> and
<code>../internal/geoip/asntops_generate.go</code>. They need to be
periodically updated.
<p>
Regenerate the automatically generated Go files that need to be
periodically updated. Those generated files are:
</p>
<ul>
<li>
<code>../internal/agd/country_generate.go</code>;
</li>
<li>
<code>../internal/geoip/asntops_generate.go</code>;
</li>
<li>
<code>../internal/profiledb/internal/filecachepb/filecache.pb.go</code>.
</li>
</ul>
<p>
You'll need to
<a href="https://protobuf.dev/getting-started/gotutorial/#compiling-protocol-buffers">install <code>protoc</code></a>
for the last one.
</p>
</dd>
<dt><code>make go-lint</code></dt>
<dd>
@ -158,7 +174,7 @@ dnscrypt generate -p testdns -o ./dnscrypt.yml
```sh
cd ../
cp -f config.dist.yml config.yml
cp -f config.dist.yaml config.yaml
```
@ -190,6 +206,7 @@ We'll use the test versions of the GeoIP databases here.
rm -f -r ./test/cache/
mkdir ./test/cache
curl 'https://raw.githubusercontent.com/maxmind/MaxMind-DB/main/test-data/GeoIP2-Country-Test.mmdb' -o ./test/GeoIP2-Country-Test.mmdb
curl 'https://raw.githubusercontent.com/maxmind/MaxMind-DB/main/test-data/GeoIP2-City-Test.mmdb' -o ./test/GeoIP2-City-Test.mmdb
curl 'https://raw.githubusercontent.com/maxmind/MaxMind-DB/main/test-data/GeoLite2-ASN-Test.mmdb' -o ./test/GeoLite2-ASN-Test.mmdb
```
@ -206,8 +223,8 @@ You'll need to supply the following:
See the [external HTTP API documentation][externalhttp].
You may need to change the listen ports in `config.yml` which are less than 1024
to some other ports. Otherwise, `sudo` or `doas` is required to run
You may need to change the listen ports in `config.yaml` which are less than
1024 to some other ports. Otherwise, `sudo` or `doas` is required to run
`AdGuardDNS`.
Examples below are for the configuration with the following changes:
@ -224,14 +241,14 @@ env \
BACKEND_ENDPOINT='PUT BACKEND URL HERE' \
BLOCKED_SERVICE_INDEX_URL='https://atropnikov.github.io/HostlistsRegistry/assets/services.json'\
CONSUL_ALLOWLIST_URL='PUT CONSUL ALLOWLIST URL HERE' \
CONFIG_PATH='./config.yml' \
CONFIG_PATH='./config.yaml' \
DNSDB_PATH='./test/cache/dnsdb.bolt' \
FILTER_INDEX_URL='https://atropnikov.github.io/HostlistsRegistry/assets/filters.json' \
FILTER_CACHE_PATH='./test/cache' \
PROFILES_CACHE_PATH='./test/profilecache.json' \
GENERAL_SAFE_SEARCH_URL='https://adguardteam.github.io/HostlistsRegistry/assets/engines_safe_search.txt' \
GEOIP_ASN_PATH='./test/GeoLite2-ASN-Test.mmdb' \
GEOIP_COUNTRY_PATH='./test/GeoIP2-Country-Test.mmdb' \
GEOIP_COUNTRY_PATH='./test/GeoIP2-City-Test.mmdb' \
QUERYLOG_PATH='./test/cache/querylog.jsonl' \
LISTEN_ADDR='127.0.0.1' \
LISTEN_PORT='8081' \

View File

@ -59,7 +59,7 @@ requirements section][ext-blocked] on the expected format of the response.
The path to the configuration file.
**Default:** `./config.yml`.
**Default:** `./config.yaml`.
@ -122,8 +122,29 @@ The path to the directory with the filter lists cache.
## <a href="#PROFILES_CACHE_PATH" id="PROFILES_CACHE_PATH" name="PROFILES_CACHE_PATH">`PROFILES_CACHE_PATH`</a>
The path to the profile cache file. The profile cache is read on start and is
later updated on every [full refresh][conf-backend-full_refresh_interval].
The path to the profile cache file:
* `none` means that the profile caching is disabled.
* A file with the extension `.pb` means that the profiles are cached in the
protobuf format.
Use the following command to inspect the cache, assuming that the version is
correct:
```sh
protoc\
--decode\
profiledb.FileCache\
./internal/profiledb/internal/filecachepb/filecache.proto\
< /path/to/profilecache.pb
```
* A file with the extension `.json` means that the profiles are cached in the
JSON format. This format is **deprecated** and is not recommended.
The profile cache is read on start and is later updated on every
[full refresh][conf-backend-full_refresh_interval].
**Default:** `./profilecache.json`.

12
go.mod
View File

@ -4,7 +4,7 @@ go 1.20
require (
github.com/AdguardTeam/AdGuardDNS/internal/dnsserver v0.100.0
github.com/AdguardTeam/golibs v0.12.1
github.com/AdguardTeam/golibs v0.13.2
github.com/AdguardTeam/urlfilter v0.16.1
github.com/ameshkov/dnscrypt/v2 v2.2.5
github.com/axiomhq/hyperloglog v0.0.0-20230201085229-3ddf4bad03dc
@ -19,13 +19,14 @@ require (
github.com/prometheus/client_golang v1.14.0
github.com/prometheus/client_model v0.3.0
github.com/prometheus/common v0.41.0
github.com/quic-go/quic-go v0.33.0
github.com/quic-go/quic-go v0.35.1
github.com/stretchr/testify v1.8.2
go.etcd.io/bbolt v1.3.7
golang.org/x/exp v0.0.0-20230307190834-24139beb5833
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
golang.org/x/net v0.8.0
golang.org/x/sys v0.6.0
golang.org/x/time v0.3.0
google.golang.org/protobuf v1.30.0
gopkg.in/yaml.v2 v2.4.0
)
@ -47,13 +48,12 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-19 v0.2.1 // indirect
github.com/quic-go/qtls-go1-20 v0.1.1 // indirect
github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/tools v0.7.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

24
go.sum
View File

@ -1,7 +1,7 @@
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
github.com/AdguardTeam/golibs v0.12.1 h1:bJfFzCnUCl+QsP6prUltM2Sjt0fTiDBPlxuAwfKP3g8=
github.com/AdguardTeam/golibs v0.12.1/go.mod h1:rIglKDHdLvFT1UbhumBLHO9S4cvWS9MEyT1njommI/Y=
github.com/AdguardTeam/golibs v0.13.2 h1:BPASsyQKmb+b8VnvsNOHp7bKfcZl9Z+Z2UhPjOiupSc=
github.com/AdguardTeam/golibs v0.13.2/go.mod h1:7ylQLv2Lqsc3UW3jHoITynYk6Y1tYtgEMkR09ppfsN8=
github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
github.com/AdguardTeam/urlfilter v0.16.1 h1:ZPi0rjqo8cQf2FVdzo6cqumNoHZx2KPXj2yZa1A5BBw=
github.com/AdguardTeam/urlfilter v0.16.1/go.mod h1:46YZDOV1+qtdRDuhZKVPSSp7JWWes0KayqHrKAFBdEI=
@ -91,12 +91,12 @@ github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJf
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-19 v0.2.1 h1:aJcKNMkH5ASEJB9FXNeZCyTEIHU1J7MmHyz1Q1TSG1A=
github.com/quic-go/qtls-go1-19 v0.2.1/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
github.com/quic-go/qtls-go1-20 v0.1.1 h1:KbChDlg82d3IHqaj2bn6GfKRj84Per2VGf5XV3wSwQk=
github.com/quic-go/qtls-go1-20 v0.1.1/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0=
github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA=
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
github.com/quic-go/quic-go v0.35.1 h1:b0kzj6b/cQAf05cT0CkQubHM31wiA+xH3IBkxP62poo=
github.com/quic-go/quic-go v0.35.1/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g=
github.com/shirou/gopsutil/v3 v3.21.8 h1:nKct+uP0TV8DjjNiHanKf8SAuub+GNsbrOtM9Nl9biA=
github.com/shirou/gopsutil/v3 v3.21.8/go.mod h1:YWp/H8Qs5fVmf17v7JNZzA0mPJ+mS2e9JdiUF9LlKzQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -120,8 +120,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s=
golang.org/x/exp v0.0.0-20230307190834-24139beb5833/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@ -170,8 +170,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

View File

@ -1,17 +1,27 @@
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=
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8=
cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA=
cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ=
cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU=
cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3 h1:hJiie5Bf3QucGRa4ymsAUOxyhYwGEz1xrsVk0P8erlw=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0 h1:SPOUaucgtVls75mg+X7CXigS71EnsfVUK/2CgVrwqgw=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412 h1:GvWw74lx5noHocd+f6HBMXK6DuggBB1dhVkuGZbv7qM=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c h1:ivON6cwHK1OH26MZyWDCnbTRZZf0IhNsENoNAKFS1g4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999 h1:OR8VhtwhcAI3U48/rzBsVOuHi0zDPzYI1xASVcdSgR8=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/AdguardTeam/golibs v0.10.7/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
github.com/AdguardTeam/gomitmproxy v0.2.0 h1:rvCOf17pd1/CnMyMQW891zrEiIQBpQ8cIGjKN9pinUU=
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=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c=
@ -22,39 +32,58 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625 h1:ckJgFhFWywOx+YLEMIJsTb+NV6NexWICk5+AMSuz3ss=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 h1:D21IyuvjDCshj1/qq+pCNd3VZOAEI9jy6Bi131YlXgI=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/readline v1.5.0 h1:lSwwFrbNviGePhkewF1az4oLmcwqCZijQ2/Wi3BGHAI=
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d h1:t5Wuyh53qYyg9eqn4BbnlIT+vmhyww0TatL+zT3uWgI=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o=
github.com/envoyproxy/go-control-plane v0.9.4 h1:rEvIZUSZ3fx39WIi3JkQqQBitGwpELBIYWeBVh6wn+E=
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo=
github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
github.com/gliderlabs/ssh v0.1.1 h1:j3L6gSLQalDETeEg/Jg0mGY0/y/N6zI2xX1978P0Uqw=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I=
github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
@ -68,42 +97,65 @@ github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJ
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 h1:2hRPrmiwPrp3fQX967rNJIhQPtiGXdlQWAxKbKw3VHA=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0 h1:pMen7vLs8nvgEYhywH3KDWJIJTeEr2ULsVWHWYHQyBs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99 h1:Ak8CrdlwwXwAZxzS66vgPt4U8yUZX7JwLvVR58FN5jM=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0 h1:WcmKMm43DR7RdtlkEXQJyo5ws8iTp98CyhCCbOHMvNI=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2 h1:rcanfLhLDA8nozr/K289V1zcntHr3V+SHlXwzz1ZI2g=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/ianlancetaylor/demangle v0.0.0-20220517205856-0058ec4f073c/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/influxdata/influxdb v1.7.6 h1:8mQ7A/V+3noMGCt/P9pD09ISaiz9XvgCk303UYA3gcs=
github.com/iris-contrib/jade v1.1.4 h1:WoYdfyJFfZIUgqNAeOyRfTNQZOksSlZ6+FnXR3AEpX0=
github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1 h1:ujPKutqRlJtcfWk6toYVYagwra7HQHbXOaS171b4Tg8=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/kataras/blocks v0.0.7 h1:cF3RDY/vxnSRezc7vLFlQFTYXG/yAr1o7WImJuZbzC4=
@ -113,20 +165,25 @@ github.com/kataras/pio v0.0.11 h1:kqreJ5KOEXGMwHAWHDwIl+mjfNCPhAwZPa8gK7MKlyw=
github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY=
github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kr/pty v1.1.3 h1:/Um6a/ZmD5tF7peoOJ5oN5KMQ0DrGVQSXLNwyckutPk=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY=
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/lucas-clemente/quic-go v0.25.0/go.mod h1:YtzP8bxRVCBlO77yRanE264+fY/T2U9ZlW1AaHOsMOg=
github.com/lucas-clemente/quic-go v0.27.1/go.mod h1:AzgQoPda7N+3IqMMMkywBKggIFo2KT6pfnlrQ2QieeI=
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailgun/raymond/v2 v2.0.46 h1:aOYHhvTpF5USySJ0o7cpPno/Uh2I5qg2115K25A+Ft4=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe h1:W/GaMY0y69G4cFlmsC6B9sbuo2fP8OFP1ABjt4kPz+w=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
github.com/marten-seemann/qtls-go1-16 v0.1.4/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
@ -137,54 +194,98 @@ github.com/marten-seemann/qtls-go1-18 v0.1.0/go.mod h1:PUhIQk19LoFt2174H4+an8TYv
github.com/marten-seemann/qtls-go1-18 v0.1.1/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 h1:D6paGObi5Wud7xg83MaEFyjxQB1W5bz5d0IFppr+ymk=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab h1:eFXv9Nu1lGbrNbj619aWwZfVF5HBrm9Plte8aNptuTI=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc=
github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM=
github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw=
github.com/openzipkin/zipkin-go v0.1.1 h1:A/ADD6HaPnAKj3yS7HjGHRK77qi41Hi0DirOOIQAeIw=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4 h1:Fth6mevc5rX7glNLpbAMJnqKlfIkcTjZCSHEeqvKbcI=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48 h1:vabduItPAIz9px5iryD5peyx7O3Ya8TBThapgXim98o=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470 h1:qb9IthCFBmROJ6YBS31BEMeSYjOscSiG+EO+JVNTz64=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d h1:Yoy/IzG4lULT6qZg62sVC+qyBL8DQkmD2zv6i7OImrc=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c h1:UOk+nlt1BJtTcH15CT7iNO7YVWTfTv/DNwEAQHLIaDQ=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b h1:vYEG87HxbU6dXj5npkeulCS96Dtz5xg3jcfCgpcvbIw=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20 h1:7pDq9pAMCQgRohFmd25X8hIH8VxmT3TaDm+r9LHxgBk=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9 h1:MPblCbqA5+z6XARjScMfz1TqtJC7TuTRj0U9VqIBs6k=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50 h1:crYRwvwjdVh1biHzzciFHe8DrZcYrVcZFlJtykhRctg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc h1:eHRtZoIi6n9Wo1uR+RU44C247msLWwyA89hVKwRLkMk=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 h1:SWV2fHctRpRrp49VXJ6UZja7gU9QLHwRpIPBN89SKEo=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9 h1:fxoFD0in0/CBzXoyNhMTjvBZYW6ilSnTw7N7y/8vkmM=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191 h1:T4wuULTrzCKMFlg3HmKHgXAF8oStFb/+lOIupLV2v+o=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241 h1:Y+TeIabU8sJD10Qwd/zMty2/LEaT9GNDaA6nyZf+jgo=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122 h1:TQVQrsyNaimGwF7bIhzoVC9QkKm4KsWd8cECGzFx8gI=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2 h1:bu666BQci+y4S0tVRVjsHUeRon6vUXmsGBwdowgMrg4=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82 h1:LneqU9PHDsg/AkPDU3AkqMxnMYL+imaqkpflHu73us8=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537 h1:YGaxtkYjb8mnTvtufv2LKLwCQu2/C7qFB7UtrOlTWOY=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133 h1:JtcyT0rk/9PKOdnKQzuDR+FSjh7SGtJwpgVpfZBRKlQ=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tdewolff/minify/v2 v2.12.4 h1:kejsHQMM17n6/gwdw53qsi6lg0TGddZADVyQOz1KMdE=
github.com/tdewolff/parse/v2 v2.6.4 h1:KCkDvNUMof10e3QExio9OPZJT8SbdKojLBumw8YZycQ=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
@ -193,60 +294,137 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/fasthttp v1.40.0 h1:CRq/00MfruPGFLTQKY8b+8SfdK60TxNztjRMnH0t1Yc=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/viant/assertly v0.4.8 h1:5x1GzBaRteIwTr5RAGFVG14uNeRFxVNbXPWrK2qAgpc=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0 h1:6TteTDQ68CjgcCe8wH3D3ZhUQQOJXMTbj/D9rkk2a1k=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA=
github.com/yuin/goldmark v1.4.1 h1:/vn0k+RBvwlxEmP5E7SZMqNxPhfMVFEJiykr15/0XKM=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto=
go4.org v0.0.0-20180809161055-417644f6feb5 h1:+hE86LblG4AyDgwMCLTE6FOlM9+qjHSYS+rKqxUVdsM=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d h1:E2M5QgjZ/Jg+ObCQAudsXxuTsLj7Nl5RV/lZcQZmKSo=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20221019170559-20944726eadf/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20230306221820-f0f767cdffd6/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220516155154-20f960328961/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852 h1:xYq6+9AtI+xP3M4r0N1hCkHrInHDBohhquRgx9Kk6gI=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 h1:PDIOdWxZ8eRizhKa1AAvY53xsvLB1cWorMjslvY3VA8=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 h1:tmXTu+dfa+d9Evp8NpJdgOy6+rt8/x4yG7qPBrtNfLY=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
sourcegraph.com/sourcegraph/go-diff v0.5.0 h1:eTiIR0CoWjGzJcnQ3OkhIl/b9GJovq4lSAVRt0ZFEG8=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=

View File

@ -1,11 +1,9 @@
package agd_test
import (
"net/netip"
"testing"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/golibs/testutil"
)
@ -17,12 +15,3 @@ func TestMain(m *testing.M) {
// testTimeout is the timeout for common test operations.
const testTimeout = 1 * time.Second
// testProfID is the profile ID for tests.
const testProfID agd.ProfileID = "prof1234"
// testDevID is the device ID for tests.
const testDevID agd.DeviceID = "dev1234"
// testClientIPv4 is the client IP for tests
var testClientIPv4 = netip.AddrFrom4([4]byte{1, 2, 3, 4})

View File

@ -10,6 +10,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log"
"golang.org/x/exp/slices"
)
@ -22,7 +23,7 @@ func main() {
req, err := http.NewRequest(http.MethodGet, csvURL, nil)
check(err)
req.Header.Add("User-Agent", agdhttp.UserAgent())
req.Header.Add(httphdr.UserAgent, agdhttp.UserAgent())
resp, err := c.Do(req)
check(err)

View File

@ -12,17 +12,25 @@ import (
// Devices
// Device is a device of a device attached to a profile.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
type Device struct {
// ID is the unique ID of the device.
ID DeviceID
// LinkedIP, when non-nil, allows AdGuard DNS to identify a device by its IP
// address when it can only use plain DNS.
LinkedIP *netip.Addr
// LinkedIP, when non-empty, allows AdGuard DNS to identify a device by its
// IP address when it can only use plain DNS.
LinkedIP netip.Addr
// Name is the human-readable name of the device.
Name DeviceName
// DedicatedIPs are the destination (server) IP-addresses dedicated to this
// device, if any. A device can use one of these addresses as a DNS server
// address for AdGuard DNS to recognize it.
DedicatedIPs []netip.Addr
// FilteringEnabled defines whether queries from the device should be
// filtered in any way at all.
FilteringEnabled bool

View File

@ -25,53 +25,6 @@ func (err *ArgumentError) Error() (msg string) {
return fmt.Sprintf("argument %s is invalid: %s", err.Name, err.Message)
}
// EntityName is the type for names of entities. Currently only used in errors.
type EntityName string
// Current entity names.
const (
EntityNameDevice EntityName = "device"
EntityNameProfile EntityName = "profile"
)
// NotFoundError is an error returned by lookup methods when an entity wasn't
// found.
//
// We use separate types that implement a common interface instead of a single
// structure to reduce allocations.
type NotFoundError interface {
error
// EntityName returns the name of the entity that couldn't be found.
EntityName() (e EntityName)
}
// DeviceNotFoundError is a NotFoundError returned by lookup methods when
// a device wasn't found.
type DeviceNotFoundError struct{}
// type check
var _ NotFoundError = DeviceNotFoundError{}
// Error implements the NotFoundError interface for DeviceNotFoundError.
func (DeviceNotFoundError) Error() (msg string) { return "device not found" }
// EntityName implements the NotFoundError interface for DeviceNotFoundError.
func (DeviceNotFoundError) EntityName() (e EntityName) { return EntityNameDevice }
// ProfileNotFoundError is a NotFoundError returned by lookup methods when
// a profile wasn't found.
type ProfileNotFoundError struct{}
// type check
var _ NotFoundError = ProfileNotFoundError{}
// Error implements the NotFoundError interface for ProfileNotFoundError.
func (ProfileNotFoundError) Error() (msg string) { return "profile not found" }
// EntityName implements the NotFoundError interface for ProfileNotFoundError.
func (ProfileNotFoundError) EntityName() (e EntityName) { return EntityNameProfile }
// NotACountryError is returned from NewCountry when the string doesn't represent
// a valid country.
type NotACountryError struct {

View File

@ -4,9 +4,19 @@ package agd
// Location represents the GeoIP location data about an IP address.
type Location struct {
Country Country
// Country is the country whose subnets contain the IP address.
Country Country
// Continent is the continent whose subnets contain the IP address.
Continent Continent
ASN ASN
// TopSubdivision is the ISO-code of the political subdivision of a country
// whose subnets contain the IP address. This field may be empty.
TopSubdivision string
// ASN is the number of the autonomous system whose subnets contain the IP
// address.
ASN ASN
}
// ASN is the autonomous system number of an IP address.

View File

@ -5,6 +5,7 @@ import (
"math"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtime"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/golibs/errors"
)
@ -15,8 +16,8 @@ import (
// the infrastructure, a profile is also called a “DNS server”. We call it
// profile, because it's less confusing.
//
// NOTE: Increment [defaultProfileDBCacheVersion] on any change of this
// structure.
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
//
// TODO(a.garipov): Consider making it closer to the config file and the backend
// response by grouping parental, rule list, and safe browsing settings into
@ -24,63 +25,107 @@ import (
type Profile struct {
// Parental are the parental settings for this profile. They are ignored if
// FilteringEnabled is set to false.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
Parental *ParentalProtectionSettings
// BlockingMode defines the way blocked responses are constructed.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
BlockingMode dnsmsg.BlockingModeCodec
// ID is the unique ID of this profile.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
ID ProfileID
// UpdateTime shows the last time this profile was updated from the backend.
// This is NOT the time of update in the backend's database, since the
// backend doesn't send this information.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
UpdateTime time.Time
// Devices are the devices attached to this profile. Every element of the
// slice must be non-nil.
Devices []*Device
// DeviceIDs are the IDs of devices attached to this profile.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
DeviceIDs []DeviceID
// RuleListIDs are the IDs of the filtering rule lists enabled for this
// profile. They are ignored if FilteringEnabled or RuleListsEnabled are
// set to false.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
RuleListIDs []FilterListID
// CustomRules are the custom filtering rules for this profile. They are
// ignored if RuleListsEnabled is set to false.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
CustomRules []FilterRuleText
// FilteredResponseTTL is the time-to-live value used for responses sent to
// the devices of this profile.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
FilteredResponseTTL time.Duration
// FilteringEnabled defines whether queries from devices of this profile
// should be filtered in any way at all.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
FilteringEnabled bool
// SafeBrowsingEnabled defines whether queries from devices of this profile
// should be filtered using the safe browsing filter. Requires
// FilteringEnabled to be set to true.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
SafeBrowsingEnabled bool
// RuleListsEnabled defines whether queries from devices of this profile
// should be filtered using the filtering rule lists in RuleListIDs.
// Requires FilteringEnabled to be set to true.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
RuleListsEnabled bool
// QueryLogEnabled defines whether query logs should be saved for the
// devices of this profile.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
QueryLogEnabled bool
// Deleted shows if this profile is deleted.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
Deleted bool
// BlockPrivateRelay shows if Apple Private Relay queries are blocked for
// requests from all devices in this profile.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
BlockPrivateRelay bool
// BlockFirefoxCanary shows if Firefox canary domain is blocked for
// requests from all devices in this profile.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
BlockFirefoxCanary bool
}
@ -163,23 +208,26 @@ type WeeklySchedule [7]DayRange
// ParentalProtectionSchedule is the schedule of a client's parental protection.
// All fields must not be nil.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
type ParentalProtectionSchedule struct {
// Week is the parental protection schedule for every day of the week.
Week *WeeklySchedule
// TimeZone is the profile's time zone.
TimeZone *time.Location
TimeZone *agdtime.Location
}
// Contains returns true if t is within the allowed schedule.
func (s *ParentalProtectionSchedule) Contains(t time.Time) (ok bool) {
t = t.In(s.TimeZone)
t = t.In(&s.TimeZone.Location)
r := s.Week[int(t.Weekday())]
if r.IsZeroLength() {
return false
}
day := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, s.TimeZone)
day := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, &s.TimeZone.Location)
start := day.Add(time.Duration(r.Start) * time.Minute)
end := day.Add(time.Duration(r.End+1)*time.Minute - 1*time.Nanosecond)
@ -187,6 +235,9 @@ func (s *ParentalProtectionSchedule) Contains(t time.Time) (ok bool) {
}
// ParentalProtectionSettings are the parental protection settings of a profile.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
type ParentalProtectionSettings struct {
Schedule *ParentalProtectionSchedule

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtime"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/stretchr/testify/assert"
@ -78,7 +79,7 @@ func TestParentalProtectionSchedule_Contains(t *testing.T) {
time.Saturday: agd.ZeroLengthDayRange(),
},
TimeZone: time.UTC,
TimeZone: agdtime.UTC(),
}
// allDaySchedule, 00:00:00 to 23:59:59.
@ -95,7 +96,7 @@ func TestParentalProtectionSchedule_Contains(t *testing.T) {
time.Saturday: agd.ZeroLengthDayRange(),
},
TimeZone: time.UTC,
TimeZone: agdtime.UTC(),
}
testCases := []struct {

View File

@ -1,423 +0,0 @@
package agd
import (
"context"
"encoding/json"
"fmt"
"net/netip"
"os"
"sync"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
)
// Data Storage
// ProfileDB is the local database of profiles and other data.
//
// TODO(a.garipov): move this logic to the backend package.
type ProfileDB interface {
ProfileByDeviceID(ctx context.Context, id DeviceID) (p *Profile, d *Device, err error)
ProfileByIP(ctx context.Context, ip netip.Addr) (p *Profile, d *Device, err error)
}
// DefaultProfileDB is the default implementation of the ProfileDB interface
// that can refresh itself from the provided storage.
type DefaultProfileDB struct {
// mapsMu protects the deviceToProfile, deviceIDToIP, and ipToDeviceID maps.
mapsMu *sync.RWMutex
// refreshMu protects syncTime and syncTimeFull. These are only used within
// Refresh, so this is also basically a refresh serializer.
refreshMu *sync.Mutex
// storage returns the data for this profiledb.
storage ProfileStorage
// deviceToProfile maps device IDs to their profiles. It is cleared lazily
// whenever a device is found to be missing from a profile.
deviceToProfile map[DeviceID]*Profile
// deviceIDToIP maps device IDs to their linked IP addresses. It is used to
// take changes in IP address linking into account during refreshes. It is
// cleared lazily whenever a device is found to be missing from a profile.
deviceIDToIP map[DeviceID]netip.Addr
// ipToDeviceID maps linked IP addresses to the IDs of their devices. It is
// cleared lazily whenever a device is found to be missing from a profile.
ipToDeviceID map[netip.Addr]DeviceID
// syncTime is the time of the last synchronization. It is used in refresh
// requests to the storage.
syncTime time.Time
// syncTimeFull is the time of the last full synchronization.
syncTimeFull time.Time
// cacheFilePath is the path to profiles cache file.
cacheFilePath string
// fullSyncIvl is the interval between two full synchronizations with the
// storage
fullSyncIvl time.Duration
}
// NewDefaultProfileDB returns a new default profile profiledb. The initial
// refresh is performed immediately with the constant timeout of 1 minute,
// beyond which an empty profiledb is returned. db is never nil.
func NewDefaultProfileDB(
ds ProfileStorage,
fullRefreshIvl time.Duration,
cacheFilePath string,
) (db *DefaultProfileDB, err error) {
db = &DefaultProfileDB{
mapsMu: &sync.RWMutex{},
refreshMu: &sync.Mutex{},
storage: ds,
syncTime: time.Time{},
syncTimeFull: time.Time{},
deviceToProfile: make(map[DeviceID]*Profile),
deviceIDToIP: make(map[DeviceID]netip.Addr),
ipToDeviceID: make(map[netip.Addr]DeviceID),
fullSyncIvl: fullRefreshIvl,
cacheFilePath: cacheFilePath,
}
err = db.loadProfileCache()
if err != nil {
log.Error("profiledb: cache: loading: %s", err)
}
// initialTimeout defines the maximum duration of the first attempt to load
// the profiledb.
const initialTimeout = 1 * time.Minute
ctx, cancel := context.WithTimeout(context.Background(), initialTimeout)
defer cancel()
log.Info("profiledb: initial refresh")
err = db.Refresh(ctx)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
log.Info("profiledb: warning: initial refresh timeout: %s", err)
return db, nil
}
return nil, fmt.Errorf("initial refresh: %w", err)
}
log.Info("profiledb: initial refresh succeeded")
return db, nil
}
// type check
var _ Refresher = (*DefaultProfileDB)(nil)
// Refresh implements the Refresher interface for *DefaultProfileDB. It updates
// the internal maps from the data it receives from the storage.
func (db *DefaultProfileDB) Refresh(ctx context.Context) (err error) {
startTime := time.Now()
defer func() {
metrics.ProfilesSyncTime.SetToCurrentTime()
metrics.ProfilesCountGauge.Set(float64(len(db.deviceToProfile)))
metrics.ProfilesSyncDuration.Observe(time.Since(startTime).Seconds())
metrics.SetStatusGauge(metrics.ProfilesSyncStatus, err)
}()
reqID := NewRequestID()
ctx = WithRequestID(ctx, reqID)
defer func() { err = errors.Annotate(err, "req %s: %w", reqID) }()
db.refreshMu.Lock()
defer db.refreshMu.Unlock()
isFullSync := time.Since(db.syncTimeFull) >= db.fullSyncIvl
syncTime := db.syncTime
if isFullSync {
syncTime = time.Time{}
}
var resp *PSProfilesResponse
resp, err = db.storage.Profiles(ctx, &PSProfilesRequest{
SyncTime: syncTime,
})
if err != nil {
return fmt.Errorf("updating profiles: %w", err)
}
profiles := resp.Profiles
devNum := db.setProfiles(profiles)
log.Debug("profiledb: req %s: got %d profiles with %d devices", reqID, len(profiles), devNum)
metrics.ProfilesNewCountGauge.Set(float64(len(profiles)))
db.syncTime = resp.SyncTime
if isFullSync {
db.syncTimeFull = time.Now()
err = db.saveProfileCache(ctx)
if err != nil {
return fmt.Errorf("saving cache: %w", err)
}
}
return nil
}
// profileCache is the structure for profiles db cache.
type profileCache struct {
SyncTime time.Time `json:"sync_time"`
Profiles []*Profile `json:"profiles"`
Version int `json:"version"`
}
// saveStorageCache saves profiles data to cache file.
func (db *DefaultProfileDB) saveProfileCache(ctx context.Context) (err error) {
log.Info("profiledb: saving profile cache")
var resp *PSProfilesResponse
resp, err = db.storage.Profiles(ctx, &PSProfilesRequest{
SyncTime: time.Time{},
})
if err != nil {
return err
}
data := &profileCache{
Profiles: resp.Profiles,
Version: defaultProfileDBCacheVersion,
SyncTime: time.Now(),
}
cache, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("encoding json: %w", err)
}
err = os.WriteFile(db.cacheFilePath, cache, 0o600)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
log.Info("profiledb: cache: saved %d profiles to %q", len(resp.Profiles), db.cacheFilePath)
return nil
}
// defaultProfileDBCacheVersion is the version of cached data structure. It's
// manually incremented on every change in [profileCache] structure.
const defaultProfileDBCacheVersion = 2
// loadProfileCache loads profiles data from cache file.
func (db *DefaultProfileDB) loadProfileCache() (err error) {
log.Info("profiledb: loading cache")
data, err := db.loadStorageCache()
if err != nil {
return fmt.Errorf("loading cache: %w", err)
}
if data == nil {
log.Info("profiledb: cache is empty")
return nil
}
if data.Version == defaultProfileDBCacheVersion {
profiles := data.Profiles
devNum := db.setProfiles(profiles)
log.Info("profiledb: cache: got %d profiles with %d devices", len(profiles), devNum)
db.syncTime = data.SyncTime
db.syncTimeFull = data.SyncTime
} else {
log.Info(
"profiledb: cache version %d is different from %d",
data.Version,
defaultProfileDBCacheVersion,
)
}
return nil
}
// loadStorageCache loads data from cache file.
func (db *DefaultProfileDB) loadStorageCache() (data *profileCache, err error) {
file, err := os.Open(db.cacheFilePath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// File could be deleted or not yet created, go on.
return nil, nil
}
return nil, err
}
defer func() { err = errors.WithDeferred(err, file.Close()) }()
data = &profileCache{}
err = json.NewDecoder(file).Decode(data)
if err != nil {
return nil, fmt.Errorf("decoding json: %w", err)
}
return data, nil
}
// setProfiles adds or updates the data for all profiles.
func (db *DefaultProfileDB) setProfiles(profiles []*Profile) (devNum int) {
db.mapsMu.Lock()
defer db.mapsMu.Unlock()
for _, p := range profiles {
devNum += len(p.Devices)
for _, d := range p.Devices {
db.deviceToProfile[d.ID] = p
if d.LinkedIP == nil {
// Delete any records from the device-to-IP map just in case
// there used to be one.
delete(db.deviceIDToIP, d.ID)
continue
}
newIP := *d.LinkedIP
if prevIP, ok := db.deviceIDToIP[d.ID]; !ok || prevIP != newIP {
// The IP has changed. Remove the previous records before
// setting the new ones.
delete(db.ipToDeviceID, prevIP)
delete(db.deviceIDToIP, d.ID)
}
db.ipToDeviceID[newIP] = d.ID
db.deviceIDToIP[d.ID] = newIP
}
}
return devNum
}
// type check
var _ ProfileDB = (*DefaultProfileDB)(nil)
// ProfileByDeviceID implements the ProfileDB interface for *DefaultProfileDB.
func (db *DefaultProfileDB) ProfileByDeviceID(
ctx context.Context,
id DeviceID,
) (p *Profile, d *Device, err error) {
db.mapsMu.RLock()
defer db.mapsMu.RUnlock()
return db.profileByDeviceID(ctx, id)
}
// profileByDeviceID returns the profile and the device by the ID of the device,
// if found. Any returned errors will have the underlying type of
// NotFoundError. It assumes that db is currently locked for reading.
func (db *DefaultProfileDB) profileByDeviceID(
_ context.Context,
id DeviceID,
) (p *Profile, d *Device, err error) {
// Do not use errors.Annotate here, because it allocates even when the error
// is nil. Also do not use fmt.Errorf in a defer, because it allocates when
// a device is not found, which is the most common case.
//
// TODO(a.garipov): Find out, why does it allocate and perhaps file an
// issue about that in the Go issue tracker.
var ok bool
p, ok = db.deviceToProfile[id]
if !ok {
return nil, nil, ProfileNotFoundError{}
}
for _, pd := range p.Devices {
if pd.ID == id {
d = pd
break
}
}
if d == nil {
// Perhaps, the device has been deleted. May happen when the device was
// found by a linked IP.
return nil, nil, fmt.Errorf("rechecking devices: %w", DeviceNotFoundError{})
}
return p, d, nil
}
// ProfileByIP implements the ProfileDB interface for *DefaultProfileDB. ip
// must be valid.
func (db *DefaultProfileDB) ProfileByIP(
ctx context.Context,
ip netip.Addr,
) (p *Profile, d *Device, err error) {
// Do not use errors.Annotate here, because it allocates even when the error
// is nil. Also do not use fmt.Errorf in a defer, because it allocates when
// a device is not found, which is the most common case.
db.mapsMu.RLock()
defer db.mapsMu.RUnlock()
id, ok := db.ipToDeviceID[ip]
if !ok {
return nil, nil, DeviceNotFoundError{}
}
p, d, err = db.profileByDeviceID(ctx, id)
if errors.Is(err, DeviceNotFoundError{}) {
// Probably, the device has been deleted. Remove it from our profiledb
// in a goroutine, since that requires a write lock.
go db.removeDeviceByIP(id, ip)
// Go on and return the error.
}
if err != nil {
// Don't add the device ID to the error here, since it is already added
// by profileByDeviceID.
return nil, nil, fmt.Errorf("profile by linked device id: %w", err)
}
return p, d, nil
}
// removeDeviceByIP removes the device with the given ID and linked IP address
// from the profiledb. It is intended to be used as a goroutine.
func (db *DefaultProfileDB) removeDeviceByIP(id DeviceID, ip netip.Addr) {
defer log.OnPanicAndExit("removeDeviceByIP", 1)
db.mapsMu.Lock()
defer db.mapsMu.Unlock()
delete(db.ipToDeviceID, ip)
delete(db.deviceIDToIP, id)
delete(db.deviceToProfile, id)
}
// ProfileStorage is a storage of data about profiles and other entities.
type ProfileStorage interface {
// Profiles returns all profiles known to this particular data storage. req
// must not be nil.
Profiles(ctx context.Context, req *PSProfilesRequest) (resp *PSProfilesResponse, err error)
}
// PSProfilesRequest is the ProfileStorage.Profiles request.
type PSProfilesRequest struct {
SyncTime time.Time
}
// PSProfilesResponse is the ProfileStorage.Profiles response.
type PSProfilesResponse struct {
SyncTime time.Time
Profiles []*Profile
}

View File

@ -1,153 +0,0 @@
package agd_test
import (
"context"
"net/netip"
"path/filepath"
"testing"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// newDefaultProfileDB returns a new default profile database for tests.
func newDefaultProfileDB(tb testing.TB, dev *agd.Device) (db *agd.DefaultProfileDB) {
tb.Helper()
onProfiles := func(
_ context.Context,
_ *agd.PSProfilesRequest,
) (resp *agd.PSProfilesResponse, err error) {
return &agd.PSProfilesResponse{
Profiles: []*agd.Profile{{
BlockingMode: dnsmsg.BlockingModeCodec{
Mode: &dnsmsg.BlockingModeNullIP{},
},
ID: testProfID,
Devices: []*agd.Device{dev},
}},
}, nil
}
ds := &agdtest.ProfileStorage{
OnProfiles: onProfiles,
}
cacheFilePath := filepath.Join(tb.TempDir(), "profiles.json")
db, err := agd.NewDefaultProfileDB(ds, 1*time.Minute, cacheFilePath)
require.NoError(tb, err)
return db
}
func TestDefaultProfileDB(t *testing.T) {
dev := &agd.Device{
ID: testDevID,
LinkedIP: &testClientIPv4,
}
db := newDefaultProfileDB(t, dev)
t.Run("by_device_id", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
defer cancel()
p, d, err := db.ProfileByDeviceID(ctx, testDevID)
require.NoError(t, err)
assert.Equal(t, testProfID, p.ID)
assert.Equal(t, d, dev)
})
t.Run("by_ip", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
defer cancel()
p, d, err := db.ProfileByIP(ctx, testClientIPv4)
require.NoError(t, err)
assert.Equal(t, testProfID, p.ID)
assert.Equal(t, d, dev)
})
}
var profSink *agd.Profile
var devSink *agd.Device
var errSink error
func BenchmarkDefaultProfileDB_ProfileByDeviceID(b *testing.B) {
dev := &agd.Device{
ID: testDevID,
}
db := newDefaultProfileDB(b, dev)
ctx := context.Background()
b.Run("success", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
profSink, devSink, errSink = db.ProfileByDeviceID(ctx, testDevID)
}
assert.NotNil(b, profSink)
assert.NotNil(b, devSink)
assert.NoError(b, errSink)
})
const wrongDevID = testDevID + "_bad"
b.Run("not_found", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
profSink, devSink, errSink = db.ProfileByDeviceID(ctx, wrongDevID)
}
assert.Nil(b, profSink)
assert.Nil(b, devSink)
assert.ErrorAs(b, errSink, new(agd.NotFoundError))
})
}
func BenchmarkDefaultProfileDB_ProfileByIP(b *testing.B) {
dev := &agd.Device{
ID: testDevID,
LinkedIP: &testClientIPv4,
}
db := newDefaultProfileDB(b, dev)
ctx := context.Background()
b.Run("success", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
profSink, devSink, errSink = db.ProfileByIP(ctx, testClientIPv4)
}
assert.NotNil(b, profSink)
assert.NotNil(b, devSink)
assert.NoError(b, errSink)
})
wrongClientIP := netip.MustParseAddr("5.6.7.8")
b.Run("not_found", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
profSink, devSink, errSink = db.ProfileByIP(ctx, wrongClientIP)
}
assert.Nil(b, profSink)
assert.Nil(b, devSink)
assert.ErrorAs(b, errSink, new(agd.NotFoundError))
})
}

View File

@ -102,8 +102,6 @@ type Server struct {
// ServerBindData are the socket binding data for a server. Either AddrPort or
// ListenConfig with Address must be set.
//
// TODO(a.garipov): Add support for ListenConfig in the config file.
//
// TODO(a.garipov): Consider turning this into a sum type.
//
// TODO(a.garipov): Consider renaming this and the one in websvc to something

View File

@ -1,26 +0,0 @@
package agd
import (
"net/netip"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/forward"
)
// Upstream
// Upstream module configuration.
type Upstream struct {
// Server is the upstream server we're using to forward DNS queries.
Server netip.AddrPort
// Network is the Server network protocol.
Network forward.Network
// FallbackServers is a list of the DNS servers we're using to fallback to
// when the upstream server fails to respond.
FallbackServers []netip.AddrPort
// Timeout is the timeout for all outgoing DNS requests.
Timeout time.Duration
}

View File

@ -12,20 +12,6 @@ import (
// Common Constants, Functions And Types
// HTTP header name constants.
const (
HdrNameAcceptEncoding = "Accept-Encoding"
HdrNameAccessControlAllowOrigin = "Access-Control-Allow-Origin"
HdrNameContentType = "Content-Type"
HdrNameContentEncoding = "Content-Encoding"
HdrNameServer = "Server"
HdrNameTrailer = "Trailer"
HdrNameUserAgent = "User-Agent"
HdrNameXError = "X-Error"
HdrNameXRequestID = "X-Request-Id"
)
// HTTP header value constants.
const (
HdrValApplicationJSON = "application/json"

View File

@ -9,6 +9,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/golibs/httphdr"
)
// Client is a wrapper around http.Client.
@ -85,15 +86,15 @@ func (c *Client) do(
}
if contentType != "" {
req.Header.Set(HdrNameContentType, contentType)
req.Header.Set(httphdr.ContentType, contentType)
}
reqID, ok := agd.RequestIDFromContext(ctx)
if ok {
req.Header.Set(HdrNameXRequestID, string(reqID))
req.Header.Set(httphdr.XRequestID, string(reqID))
}
req.Header.Set(HdrNameUserAgent, c.userAgent)
req.Header.Set(httphdr.UserAgent, c.userAgent)
resp, err = c.http.Do(req)
if err != nil && resp != nil && resp.Header != nil {

View File

@ -5,6 +5,7 @@ import (
"net/http"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/httphdr"
)
// Common HTTP Errors
@ -40,7 +41,7 @@ func CheckStatus(resp *http.Response, expected int) (err error) {
}
return &StatusError{
ServerName: resp.Header.Get(HdrNameServer),
ServerName: resp.Header.Get(httphdr.Server),
Expected: expected,
Got: resp.StatusCode,
}
@ -73,6 +74,6 @@ func (err *ServerError) Unwrap() (unwrapped error) {
func WrapServerError(err error, resp *http.Response) (wrapped *ServerError) {
return &ServerError{
Err: err,
ServerName: resp.Header.Get(HdrNameServer),
ServerName: resp.Header.Get(httphdr.Server),
}
}

View File

@ -5,6 +5,7 @@ import (
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
)
@ -41,7 +42,7 @@ func TestCheckStatus(t *testing.T) {
resp := &http.Response{
StatusCode: tc.got,
Header: http.Header{
agdhttp.HdrNameServer: []string{tc.srv},
httphdr.Server: []string{tc.srv},
},
}
err := agdhttp.CheckStatus(resp, tc.exp)
@ -73,7 +74,7 @@ func TestServerError(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
resp := &http.Response{
Header: http.Header{
agdhttp.HdrNameServer: []string{tc.srv},
httphdr.Server: []string{tc.srv},
},
}
err := agdhttp.WrapServerError(tc.err, resp)

View File

@ -7,14 +7,18 @@ import (
)
func ExampleAndroidMetricDomainReplacement() {
printResult := func(input string) {
fmt.Printf("%-42q: %q\n", input, agdnet.AndroidMetricDomainReplacement(input))
}
anAndroidDomain := "12345678-dnsotls-ds.metric.gstatic.com."
fmt.Printf("%-42q: %q\n", anAndroidDomain, agdnet.AndroidMetricDomainReplacement(anAndroidDomain))
printResult(anAndroidDomain)
anAndroidDomain = "123456-dnsohttps-ds.metric.gstatic.com."
fmt.Printf("%-42q: %q\n", anAndroidDomain, agdnet.AndroidMetricDomainReplacement(anAndroidDomain))
printResult(anAndroidDomain)
notAndroidDomain := "example.com"
fmt.Printf("%-42q: %q\n", notAndroidDomain, agdnet.AndroidMetricDomainReplacement(notAndroidDomain))
printResult(notAndroidDomain)
// Output:
// "12345678-dnsotls-ds.metric.gstatic.com." : "00000000-dnsotls-ds.metric.gstatic.com."

View File

@ -10,7 +10,11 @@ import (
// FilteredResponseTTL is the common filtering response TTL for tests. It is
// also used by [NewConstructor].
const FilteredResponseTTL = 10 * time.Second
const FilteredResponseTTL = FilteredResponseTTLSec * time.Second
// FilteredResponseTTLSec is the common filtering response TTL for tests, as a
// number to simplify message creation.
const FilteredResponseTTLSec = 10
// NewConstructor returns a standard dnsmsg.Constructor for tests.
func NewConstructor() (c *dnsmsg.Constructor) {

View File

@ -11,9 +11,11 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/billstat"
"github.com/AdguardTeam/AdGuardDNS/internal/dnscheck"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsdb"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/netext"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/ratelimit"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
"github.com/AdguardTeam/AdGuardDNS/internal/querylog"
"github.com/AdguardTeam/AdGuardDNS/internal/rulestat"
"github.com/AdguardTeam/golibs/netutil"
@ -22,7 +24,144 @@ import (
// Interface Mocks
//
// Keep entities in this file in alphabetic order.
// Keep entities within a module/package in alphabetic order.
// Module std
// Package net
//
// TODO(a.garipov): Move these to golibs?
// type check
var _ net.Conn = (*Conn)(nil)
// Conn is the [net.Conn] for tests.
type Conn struct {
OnClose func() (err error)
OnLocalAddr func() (laddr net.Addr)
OnRead func(b []byte) (n int, err error)
OnRemoteAddr func() (raddr net.Addr)
OnSetDeadline func(t time.Time) (err error)
OnSetReadDeadline func(t time.Time) (err error)
OnSetWriteDeadline func(t time.Time) (err error)
OnWrite func(b []byte) (n int, err error)
}
// Close implements the [net.Conn] interface for *Conn.
func (c *Conn) Close() (err error) {
return c.OnClose()
}
// LocalAddr implements the [net.Conn] interface for *Conn.
func (c *Conn) LocalAddr() (laddr net.Addr) {
return c.OnLocalAddr()
}
// Read implements the [net.Conn] interface for *Conn.
func (c *Conn) Read(b []byte) (n int, err error) {
return c.OnRead(b)
}
// RemoteAddr implements the [net.Conn] interface for *Conn.
func (c *Conn) RemoteAddr() (raddr net.Addr) {
return c.OnRemoteAddr()
}
// SetDeadline implements the [net.Conn] interface for *Conn.
func (c *Conn) SetDeadline(t time.Time) (err error) {
return c.OnSetDeadline(t)
}
// SetReadDeadline implements the [net.Conn] interface for *Conn.
func (c *Conn) SetReadDeadline(t time.Time) (err error) {
return c.OnSetReadDeadline(t)
}
// SetWriteDeadline implements the [net.Conn] interface for *Conn.
func (c *Conn) SetWriteDeadline(t time.Time) (err error) {
return c.OnSetWriteDeadline(t)
}
// Write implements the [net.Conn] interface for *Conn.
func (c *Conn) Write(b []byte) (n int, err error) {
return c.OnWrite(b)
}
// type check
var _ net.Listener = (*Listener)(nil)
// Listener is a [net.Listener] for tests.
type Listener struct {
OnAccept func() (c net.Conn, err error)
OnAddr func() (addr net.Addr)
OnClose func() (err error)
}
// Accept implements the [net.Listener] interface for *Listener.
func (l *Listener) Accept() (c net.Conn, err error) {
return l.OnAccept()
}
// Addr implements the [net.Listener] interface for *Listener.
func (l *Listener) Addr() (addr net.Addr) {
return l.OnAddr()
}
// Close implements the [net.Listener] interface for *Listener.
func (l *Listener) Close() (err error) {
return l.OnClose()
}
// type check
var _ net.PacketConn = (*PacketConn)(nil)
// PacketConn is the [net.PacketConn] for tests.
type PacketConn struct {
OnClose func() (err error)
OnLocalAddr func() (laddr net.Addr)
OnReadFrom func(b []byte) (n int, addr net.Addr, err error)
OnSetDeadline func(t time.Time) (err error)
OnSetReadDeadline func(t time.Time) (err error)
OnSetWriteDeadline func(t time.Time) (err error)
OnWriteTo func(b []byte, addr net.Addr) (n int, err error)
}
// Close implements the [net.PacketConn] interface for *PacketConn.
func (c *PacketConn) Close() (err error) {
return c.OnClose()
}
// LocalAddr implements the [net.PacketConn] interface for *PacketConn.
func (c *PacketConn) LocalAddr() (laddr net.Addr) {
return c.OnLocalAddr()
}
// ReadFrom implements the [net.PacketConn] interface for *PacketConn.
func (c *PacketConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
return c.OnReadFrom(b)
}
// SetDeadline implements the [net.PacketConn] interface for *PacketConn.
func (c *PacketConn) SetDeadline(t time.Time) (err error) {
return c.OnSetDeadline(t)
}
// SetReadDeadline implements the [net.PacketConn] interface for *PacketConn.
func (c *PacketConn) SetReadDeadline(t time.Time) (err error) {
return c.OnSetReadDeadline(t)
}
// SetWriteDeadline implements the [net.PacketConn] interface for *PacketConn.
func (c *PacketConn) SetWriteDeadline(t time.Time) (err error) {
return c.OnSetWriteDeadline(t)
}
// WriteTo implements the [net.PacketConn] interface for *PacketConn.
func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
return c.OnWriteTo(b, addr)
}
// Module AdGuardDNS
// type check
var _ agd.ErrorCollector = (*ErrorCollector)(nil)
@ -39,56 +178,6 @@ func (c *ErrorCollector) Collect(ctx context.Context, err error) {
c.OnCollect(ctx, err)
}
// type check
var _ agd.ProfileDB = (*ProfileDB)(nil)
// ProfileDB is an agd.ProfileDB for tests.
type ProfileDB struct {
OnProfileByDeviceID func(
ctx context.Context,
id agd.DeviceID,
) (p *agd.Profile, d *agd.Device, err error)
OnProfileByIP func(
ctx context.Context,
ip netip.Addr,
) (p *agd.Profile, d *agd.Device, err error)
}
// ProfileByDeviceID implements the agd.ProfileDB interface for *ProfileDB.
func (db *ProfileDB) ProfileByDeviceID(
ctx context.Context,
id agd.DeviceID,
) (p *agd.Profile, d *agd.Device, err error) {
return db.OnProfileByDeviceID(ctx, id)
}
// ProfileByIP implements the agd.ProfileDB interface for *ProfileDB.
func (db *ProfileDB) ProfileByIP(
ctx context.Context,
ip netip.Addr,
) (p *agd.Profile, d *agd.Device, err error) {
return db.OnProfileByIP(ctx, ip)
}
// type check
var _ agd.ProfileStorage = (*ProfileStorage)(nil)
// ProfileStorage is a agd.ProfileStorage for tests.
type ProfileStorage struct {
OnProfiles func(
ctx context.Context,
req *agd.PSProfilesRequest,
) (resp *agd.PSProfilesResponse, err error)
}
// Profiles implements the agd.ProfileStorage interface for *ProfileStorage.
func (ds *ProfileStorage) Profiles(
ctx context.Context,
req *agd.PSProfilesRequest,
) (resp *agd.PSProfilesResponse, err error) {
return ds.OnProfiles(ctx, req)
}
// type check
var _ agd.Refresher = (*Refresher)(nil)
@ -206,7 +295,7 @@ func (db *DNSDB) Record(ctx context.Context, resp *dns.Msg, ri *agd.RequestInfo)
// type check
var _ filter.Interface = (*Filter)(nil)
// Filter is a filter.Interface for tests.
// Filter is a [filter.Interface] for tests.
type Filter struct {
OnFilterRequest func(
ctx context.Context,
@ -218,10 +307,9 @@ type Filter struct {
resp *dns.Msg,
ri *agd.RequestInfo,
) (r filter.Result, err error)
OnClose func() (err error)
}
// FilterRequest implements the filter.Interface interface for *Filter.
// FilterRequest implements the [filter.Interface] interface for *Filter.
func (f *Filter) FilterRequest(
ctx context.Context,
req *dns.Msg,
@ -230,7 +318,7 @@ func (f *Filter) FilterRequest(
return f.OnFilterRequest(ctx, req, ri)
}
// FilterResponse implements the filter.Interface interface for *Filter.
// FilterResponse implements the [filter.Interface] interface for *Filter.
func (f *Filter) FilterResponse(
ctx context.Context,
resp *dns.Msg,
@ -239,21 +327,36 @@ func (f *Filter) FilterResponse(
return f.OnFilterResponse(ctx, resp, ri)
}
// Close implements the filter.Interface interface for *Filter.
func (f *Filter) Close() (err error) {
return f.OnClose()
// type check
var _ filter.HashMatcher = (*HashMatcher)(nil)
// HashMatcher is a [filter.HashMatcher] for tests.
type HashMatcher struct {
OnMatchByPrefix func(
ctx context.Context,
host string,
) (hashes []string, matched bool, err error)
}
// MatchByPrefix implements the [filter.HashMatcher] interface for *HashMatcher.
func (m *HashMatcher) MatchByPrefix(
ctx context.Context,
host string,
) (hashes []string, matched bool, err error) {
return m.OnMatchByPrefix(ctx, host)
}
// type check
var _ filter.Storage = (*FilterStorage)(nil)
// FilterStorage is an filter.Storage for tests.
// FilterStorage is a [filter.Storage] for tests.
type FilterStorage struct {
OnFilterFromContext func(ctx context.Context, ri *agd.RequestInfo) (f filter.Interface)
OnHasListID func(id agd.FilterListID) (ok bool)
}
// FilterFromContext implements the filter.Storage interface for *FilterStorage.
// FilterFromContext implements the [filter.Storage] interface for
// *FilterStorage.
func (s *FilterStorage) FilterFromContext(
ctx context.Context,
ri *agd.RequestInfo,
@ -261,7 +364,7 @@ func (s *FilterStorage) FilterFromContext(
return s.OnFilterFromContext(ctx, ri)
}
// HasListID implements the filter.Storage interface for *FilterStorage.
// HasListID implements the [filter.Storage] interface for *FilterStorage.
func (s *FilterStorage) HasListID(id agd.FilterListID) (ok bool) {
return s.OnHasListID(id)
}
@ -295,6 +398,73 @@ func (g *GeoIP) Data(host string, ip netip.Addr) (l *agd.Location, err error) {
return g.OnData(host, ip)
}
// Package profiledb
// type check
var _ profiledb.Interface = (*ProfileDB)(nil)
// ProfileDB is a [profiledb.Interface] for tests.
type ProfileDB struct {
OnProfileByDeviceID func(
ctx context.Context,
id agd.DeviceID,
) (p *agd.Profile, d *agd.Device, err error)
OnProfileByDedicatedIP func(
ctx context.Context,
ip netip.Addr,
) (p *agd.Profile, d *agd.Device, err error)
OnProfileByLinkedIP func(
ctx context.Context,
ip netip.Addr,
) (p *agd.Profile, d *agd.Device, err error)
}
// ProfileByDeviceID implements the [profiledb.Interface] interface for
// *ProfileDB.
func (db *ProfileDB) ProfileByDeviceID(
ctx context.Context,
id agd.DeviceID,
) (p *agd.Profile, d *agd.Device, err error) {
return db.OnProfileByDeviceID(ctx, id)
}
// ProfileByDedicatedIP implements the [profiledb.Interface] interface for
// *ProfileDB.
func (db *ProfileDB) ProfileByDedicatedIP(
ctx context.Context,
ip netip.Addr,
) (p *agd.Profile, d *agd.Device, err error) {
return db.OnProfileByDedicatedIP(ctx, ip)
}
// ProfileByLinkedIP implements the [profiledb.Interface] interface for
// *ProfileDB.
func (db *ProfileDB) ProfileByLinkedIP(
ctx context.Context,
ip netip.Addr,
) (p *agd.Profile, d *agd.Device, err error) {
return db.OnProfileByLinkedIP(ctx, ip)
}
// type check
var _ profiledb.Storage = (*ProfileStorage)(nil)
// ProfileStorage is a profiledb.Storage for tests.
type ProfileStorage struct {
OnProfiles func(
ctx context.Context,
req *profiledb.StorageRequest,
) (resp *profiledb.StorageResponse, err error)
}
// Profiles implements the [profiledb.Storage] interface for *ProfileStorage.
func (s *ProfileStorage) Profiles(
ctx context.Context,
req *profiledb.StorageRequest,
) (resp *profiledb.StorageResponse, err error) {
return s.OnProfiles(ctx, req)
}
// Package querylog
// type check
@ -327,6 +497,39 @@ func (s *RuleStat) Collect(ctx context.Context, id agd.FilterListID, text agd.Fi
// Module dnsserver
// Package netext
var _ netext.ListenConfig = (*ListenConfig)(nil)
// ListenConfig is a [netext.ListenConfig] for tests.
type ListenConfig struct {
OnListen func(ctx context.Context, network, address string) (l net.Listener, err error)
OnListenPacket func(
ctx context.Context,
network string,
address string,
) (conn net.PacketConn, err error)
}
// Listen implements the [netext.ListenConfig] interface for *ListenConfig.
func (c *ListenConfig) Listen(
ctx context.Context,
network string,
address string,
) (l net.Listener, err error) {
return c.OnListen(ctx, network, address)
}
// ListenPacket implements the [netext.ListenConfig] interface for
// *ListenConfig.
func (c *ListenConfig) ListenPacket(
ctx context.Context,
network string,
address string,
) (conn net.PacketConn, err error) {
return c.OnListenPacket(ctx, network, address)
}
// Package ratelimit
// type check

View File

@ -0,0 +1,63 @@
// Package agdtime contains time-related utilities.
package agdtime
import (
"encoding"
"time"
"github.com/AdguardTeam/golibs/errors"
)
// Location is a wrapper around time.Location that can de/serialize itself from
// and to JSON.
//
// TODO(a.garipov): Move to timeutil.
type Location struct {
time.Location
}
// LoadLocation is a wrapper around [time.LoadLocation] that returns a
// *Location instead.
func LoadLocation(name string) (l *Location, err error) {
tl, err := time.LoadLocation(name)
if err != nil {
// Don't wrap the error, because this function is a wrapper.
return nil, err
}
return &Location{
Location: *tl,
}, nil
}
// UTC returns [time.UTC] as *Location.
func UTC() (l *Location) {
return &Location{
Location: *time.UTC,
}
}
// type check
var _ encoding.TextMarshaler = Location{}
// MarshalText implements the [encoding.TextMarshaler] interface for Location.
func (l Location) MarshalText() (text []byte, err error) {
return []byte(l.String()), nil
}
var _ encoding.TextUnmarshaler = (*Location)(nil)
// UnmarshalText implements the [encoding.TextUnmarshaler] interface for
// *Location.
func (l *Location) UnmarshalText(b []byte) (err error) {
defer func() { err = errors.Annotate(err, "unmarshaling location: %w") }()
tl, err := time.LoadLocation(string(b))
if err != nil {
return err
}
l.Location = *tl
return nil
}

View File

@ -0,0 +1,41 @@
package agdtime_test
import (
"bytes"
"encoding/json"
"fmt"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtime"
)
func ExampleLocation() {
var req struct {
TimeZone *agdtime.Location `json:"tmz"`
}
l, err := agdtime.LoadLocation("Europe/Brussels")
if err != nil {
panic(err)
}
req.TimeZone = l
buf := &bytes.Buffer{}
err = json.NewEncoder(buf).Encode(req)
if err != nil {
panic(err)
}
fmt.Print(buf)
req.TimeZone = nil
err = json.NewDecoder(buf).Decode(&req)
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", req)
// Output:
// {"tmz":"Europe/Brussels"}
// {TimeZone:Europe/Brussels}
}

View File

@ -12,10 +12,13 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtime"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil"
"golang.org/x/exp/slices"
)
// Profile Storage
@ -47,7 +50,7 @@ func NewProfileStorage(c *ProfileStorageConfig) (s *ProfileStorage) {
}
}
// ProfileStorage is the implementation of the [agd.ProfileStorage] interface
// ProfileStorage is the implementation of the [profiledb.Storage] interface
// that retrieves the profile and device information from the business logic
// backend. It is safe for concurrent use.
//
@ -61,13 +64,13 @@ type ProfileStorage struct {
}
// type check
var _ agd.ProfileStorage = (*ProfileStorage)(nil)
var _ profiledb.Storage = (*ProfileStorage)(nil)
// Profiles implements the [agd.ProfileStorage] interface for *ProfileStorage.
// Profiles implements the [profiledb.Storage] interface for *ProfileStorage.
func (s *ProfileStorage) Profiles(
ctx context.Context,
req *agd.PSProfilesRequest,
) (resp *agd.PSProfilesResponse, err error) {
req *profiledb.StorageRequest,
) (resp *profiledb.StorageResponse, err error) {
q := url.Values{}
if !req.SyncTime.IsZero() {
syncTimeStr := strconv.FormatInt(req.SyncTime.UnixMilli(), 10)
@ -127,7 +130,12 @@ type v1SettingsRespSchedule struct {
Friday *[2]timeutil.Duration `json:"fri"`
Saturday *[2]timeutil.Duration `json:"sat"`
Sunday *[2]timeutil.Duration `json:"sun"`
TimeZone string `json:"tmz"`
// TimeZone is the tzdata name of the time zone.
//
// NOTE: Do not use *agdtime.Location here so that lookup failures are
// properly mitigated in [v1SettingsRespParental.toInternal].
TimeZone string `json:"tmz"`
}
// v1SettingsRespParental is the structure for decoding the settings.*.parental
@ -146,10 +154,11 @@ type v1SettingsRespParental struct {
// v1SettingsRespDevice is the structure for decoding the settings.devices
// property of the response from the backend.
type v1SettingsRespDevice struct {
LinkedIP *netip.Addr `json:"linked_ip"`
ID string `json:"id"`
Name string `json:"name"`
FilteringEnabled bool `json:"filtering_enabled"`
LinkedIP netip.Addr `json:"linked_ip"`
ID string `json:"id"`
Name string `json:"name"`
DedicatedIPs []netip.Addr `json:"dedicated_ips"`
FilteringEnabled bool `json:"filtering_enabled"`
}
// v1SettingsRespSettings is the structure for decoding the settings property of
@ -232,12 +241,12 @@ func (p *v1SettingsRespParental) toInternal(
sch = &agd.ParentalProtectionSchedule{}
// TODO(a.garipov): Cache location lookup results.
sch.TimeZone, err = time.LoadLocation(psch.TimeZone)
sch.TimeZone, err = agdtime.LoadLocation(psch.TimeZone)
if err != nil {
// Report the error and assume UTC.
reportf(ctx, errColl, "settings at index %d: schedule: time zone: %w", settIdx, err)
sch.TimeZone = time.UTC
sch.TimeZone = agdtime.UTC()
}
sch.Week = &agd.WeeklySchedule{}
@ -330,10 +339,10 @@ func devicesToInternal(
errColl agd.ErrorCollector,
settIdx int,
respDevices []*v1SettingsRespDevice,
) (devices []*agd.Device) {
) (devices []*agd.Device, ids []agd.DeviceID) {
l := len(respDevices)
if l == 0 {
return nil
return nil, nil
}
devices = make([]*agd.Device, 0, l)
@ -344,8 +353,11 @@ func devicesToInternal(
continue
}
// TODO(a.garipov): Consider validating uniqueness of linked and
// dedicated IPs.
dev := &agd.Device{
LinkedIP: d.LinkedIP,
DedicatedIPs: slices.Clone(d.DedicatedIPs),
FilteringEnabled: d.FilteringEnabled,
}
@ -368,10 +380,11 @@ func devicesToInternal(
continue
}
ids = append(ids, dev.ID)
devices = append(devices, dev)
}
return devices
return devices, ids
}
// filterListsToInternal is a helper that converts the filter lists from the
@ -458,12 +471,12 @@ func (r *v1SettingsResp) toInternal(
// TODO(a.garipov): Here and in other functions, consider just adding the
// error collector to the context.
errColl agd.ErrorCollector,
) (pr *agd.PSProfilesResponse) {
) (pr *profiledb.StorageResponse) {
if r == nil {
return nil
}
pr = &agd.PSProfilesResponse{
pr = &profiledb.StorageResponse{
SyncTime: time.Unix(0, r.SyncTime*1_000_000),
Profiles: make([]*agd.Profile, 0, len(r.Settings)),
}
@ -476,7 +489,7 @@ func (r *v1SettingsResp) toInternal(
continue
}
devices := devicesToInternal(ctx, errColl, i, s.Devices)
devices, deviceIDs := devicesToInternal(ctx, errColl, i, s.Devices)
rlEnabled, ruleLists := filterListsToInternal(ctx, errColl, i, s.RuleLists)
rules := rulesToInternal(ctx, errColl, i, s.CustomRules)
@ -499,12 +512,14 @@ func (r *v1SettingsResp) toInternal(
sbEnabled := s.SafeBrowsing != nil && s.SafeBrowsing.Enabled
pr.Devices = append(pr.Devices, devices...)
pr.Profiles = append(pr.Profiles, &agd.Profile{
Parental: parental,
BlockingMode: s.BlockingMode,
ID: id,
UpdateTime: updTime,
Devices: devices,
DeviceIDs: deviceIDs,
RuleListIDs: ruleLists,
CustomRules: rules,
FilteredResponseTTL: fltRespTTL,

View File

@ -13,8 +13,10 @@ import (
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtime"
"github.com/AdguardTeam/AdGuardDNS/internal/backend"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -61,7 +63,7 @@ func TestProfileStorage_Profiles(t *testing.T) {
require.NotNil(t, ds)
ctx := context.Background()
req := &agd.PSProfilesRequest{
req := &profiledb.StorageRequest{
SyncTime: syncTime,
}
@ -81,10 +83,10 @@ func TestProfileStorage_Profiles(t *testing.T) {
// testProfileResp returns profile resp corresponding with testdata.
//
// Keep in sync with the testdata one.
func testProfileResp(t *testing.T) *agd.PSProfilesResponse {
func testProfileResp(t *testing.T) (resp *profiledb.StorageResponse) {
t.Helper()
wantLoc, err := time.LoadLocation("GMT")
wantLoc, err := agdtime.LoadLocation("GMT")
require.NoError(t, err)
dayRange := agd.DayRange{
@ -121,7 +123,7 @@ func testProfileResp(t *testing.T) *agd.PSProfilesResponse {
},
}
want := &agd.PSProfilesResponse{
want := &profiledb.StorageResponse{
SyncTime: syncTime,
Profiles: []*agd.Profile{{
Parental: nil,
@ -130,15 +132,10 @@ func testProfileResp(t *testing.T) *agd.PSProfilesResponse {
},
ID: "37f97ee9",
UpdateTime: updTime,
Devices: []*agd.Device{{
ID: "118ffe93",
Name: "Device 1",
FilteringEnabled: true,
}, {
ID: "b9e1a762",
Name: "Device 2",
FilteringEnabled: true,
}},
DeviceIDs: []agd.DeviceID{
"118ffe93",
"b9e1a762",
},
RuleListIDs: []agd.FilterListID{"1"},
CustomRules: nil,
FilteredResponseTTL: 10 * time.Second,
@ -154,24 +151,12 @@ func testProfileResp(t *testing.T) *agd.PSProfilesResponse {
BlockingMode: wantBlockingMode,
ID: "83f3ea8f",
UpdateTime: updTime,
Devices: []*agd.Device{{
ID: "0d7724fa",
Name: "Device 1",
FilteringEnabled: true,
}, {
ID: "6d2ac775",
Name: "Device 2",
FilteringEnabled: true,
}, {
ID: "94d4c481",
Name: "Device 3",
FilteringEnabled: true,
}, {
ID: "ada436e3",
LinkedIP: &wantLinkedIP,
Name: "Device 4",
FilteringEnabled: true,
}},
DeviceIDs: []agd.DeviceID{
"0d7724fa",
"6d2ac775",
"94d4c481",
"ada436e3",
},
RuleListIDs: []agd.FilterListID{"1"},
CustomRules: []agd.FilterRuleText{"||example.org^"},
FilteredResponseTTL: 3600 * time.Second,
@ -183,6 +168,35 @@ func testProfileResp(t *testing.T) *agd.PSProfilesResponse {
BlockPrivateRelay: false,
BlockFirefoxCanary: false,
}},
Devices: []*agd.Device{{
ID: "118ffe93",
Name: "Device 1",
FilteringEnabled: true,
}, {
ID: "b9e1a762",
Name: "Device 2",
FilteringEnabled: true,
}, {
ID: "0d7724fa",
Name: "Device 1",
FilteringEnabled: true,
}, {
ID: "6d2ac775",
Name: "Device 2",
FilteringEnabled: true,
}, {
ID: "94d4c481",
Name: "Device 3",
DedicatedIPs: []netip.Addr{
netip.MustParseAddr("1.2.3.4"),
},
FilteringEnabled: true,
}, {
ID: "ada436e3",
LinkedIP: wantLinkedIP,
Name: "Device 4",
FilteringEnabled: true,
}},
}
return want

View File

@ -50,18 +50,23 @@
{
"id": "6d2ac775",
"name": "Device 2",
"linked_ip": null,
"filtering_enabled": true
},
{
"id": "94d4c481",
"name": "Device 3",
"linked_ip": "",
"dedicated_ips": [
"1.2.3.4"
],
"filtering_enabled": true
},
{
"id": "ada436e3",
"name": "Device 4",
"filtering_enabled": true,
"linked_ip": "1.2.3.4"
"linked_ip": "1.2.3.4",
"filtering_enabled": true
}
],
"parental": {

View File

@ -1,40 +1,6 @@
// Package bindtodevice contains an implementation of the [netext.ListenConfig]
// interface that uses Linux's SO_BINDTODEVICE socket option to be able to bind
// to a device.
//
// TODO(a.garipov): Finish the package. The current plan is to eventually have
// something like this:
//
// mgr, err := bindtodevice.New()
// err := mgr.Add("wlp3s0_plain_dns", "wlp3s0", 53)
// subnet := netip.MustParsePrefix("1.2.3.0/24")
// lc, err := mgr.ListenConfig("wlp3s0_plain_dns", subnet)
// err := mgr.Start()
//
// Approximate YAML configuration example:
//
// 'interface_listeners':
// # Put listeners into a list so that there is space for future additional
// # settings, such as timeouts and buffer sizes.
// 'list':
// 'iface0_plain_dns':
// 'interface': 'iface0'
// 'port': 53
// 'iface0_plain_dns_secondary':
// 'interface': 'iface0'
// 'port': 5353
// # …
// # …
// 'server_groups':
// # …
// 'servers':
// - 'name': 'default_dns'
// # …
// bind_interfaces:
// - 'id': 'iface0_plain_dns'
// 'subnet': '1.2.3.0/24'
// - 'id': 'iface0_plain_dns_secondary'
// 'subnet': '1.2.3.0/24'
package bindtodevice
import (

View File

@ -2,9 +2,13 @@ package bindtodevice
import (
"net"
"net/netip"
"time"
)
// Common timeout for tests
const testTimeout = 1 * time.Second
// Common addresses for tests.
var (
testLAddr = &net.UDPAddr{
@ -17,5 +21,8 @@ var (
}
)
// Common timeout for tests
const testTimeout = 1 * time.Second
// Common subnets for tests.
var (
testSubnetIPv4 = netip.MustParsePrefix("1.2.3.0/24")
testSubnetIPv6 = netip.MustParsePrefix("1234:5678::/64")
)

View File

@ -1,6 +1,16 @@
package bindtodevice_test
import "github.com/AdguardTeam/AdGuardDNS/internal/bindtodevice"
import (
"net/netip"
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/bindtodevice"
"github.com/AdguardTeam/golibs/testutil"
)
func TestMain(m *testing.M) {
testutil.DiscardLogOutput(m)
}
// Common interface listener IDs for tests
const (
@ -18,3 +28,6 @@ const (
// testIfaceName is the common network interface name for tests.
const testIfaceName = "not_a_real_iface0"
// testSubnetIPv4 is a common subnet for tests.
var testSubnetIPv4 = netip.MustParsePrefix("1.2.3.0/24")

View File

@ -11,8 +11,8 @@ import (
)
func TestChanListenConfig(t *testing.T) {
pc := newChanPacketConn(nil, nil, testLAddr)
lsnr := newChanListener(nil, testLAddr)
pc := newChanPacketConn(nil, testSubnetIPv4, nil, testLAddr)
lsnr := newChanListener(nil, testSubnetIPv4, testLAddr)
c := chanListenConfig{
packetConn: pc,
listener: lsnr,

View File

@ -4,6 +4,7 @@ package bindtodevice
import (
"net"
"net/netip"
"sync"
)
@ -13,17 +14,22 @@ import (
// Listeners of this type are returned by [chanListenConfig.Listen] and are used
// in module dnsserver to make the bind-to-device logic work in DNS-over-TCP.
type chanListener struct {
closeOnce *sync.Once
conns chan net.Conn
laddr net.Addr
// mu protects conns (against closure) and isClosed.
mu *sync.Mutex
conns chan net.Conn
laddr net.Addr
subnet netip.Prefix
isClosed bool
}
// newChanListener returns a new properly initialized *chanListener.
func newChanListener(conns chan net.Conn, laddr net.Addr) (l *chanListener) {
func newChanListener(conns chan net.Conn, subnet netip.Prefix, laddr net.Addr) (l *chanListener) {
return &chanListener{
closeOnce: &sync.Once{},
conns: conns,
laddr: laddr,
mu: &sync.Mutex{},
conns: conns,
laddr: laddr,
subnet: subnet,
isClosed: false,
}
}
@ -46,15 +52,30 @@ func (l *chanListener) Addr() (addr net.Addr) { return l.laddr }
// Close implements the [net.Listener] interface for *chanListener.
func (l *chanListener) Close() (err error) {
closedNow := false
l.closeOnce.Do(func() {
close(l.conns)
closedNow = true
})
l.mu.Lock()
defer l.mu.Unlock()
if !closedNow {
if l.isClosed {
return wrapConnError(tnChanLsnr, "Close", l.laddr, net.ErrClosed)
}
close(l.conns)
l.isClosed = true
return nil
}
// send is a helper method to send a conn to the listener's channel. ok is
// false if the listener is closed.
func (l *chanListener) send(conn net.Conn) (ok bool) {
l.mu.Lock()
defer l.mu.Unlock()
if l.isClosed {
return false
}
l.conns <- conn
return true
}

View File

@ -12,7 +12,7 @@ import (
func TestChanListener_Accept(t *testing.T) {
conns := make(chan net.Conn, 1)
l := newChanListener(conns, testLAddr)
l := newChanListener(conns, testSubnetIPv4, testLAddr)
// A simple way to have a distinct net.Conn without actually implementing
// the entire interface.
@ -32,14 +32,14 @@ func TestChanListener_Accept(t *testing.T) {
}
func TestChanListener_Addr(t *testing.T) {
l := newChanListener(nil, testLAddr)
l := newChanListener(nil, testSubnetIPv4, testLAddr)
got := l.Addr()
assert.Equal(t, testLAddr, got)
}
func TestChanListener_Close(t *testing.T) {
conns := make(chan net.Conn)
l := newChanListener(conns, testLAddr)
l := newChanListener(conns, testSubnetIPv4, testLAddr)
err := l.Close()
assert.NoError(t, err)

View File

@ -5,6 +5,7 @@ package bindtodevice
import (
"fmt"
"net"
"net/netip"
"os"
"sync"
"time"
@ -19,32 +20,38 @@ import (
// are used in module dnsserver to make the bind-to-device logic work in
// DNS-over-UDP.
type chanPacketConn struct {
closeOnce *sync.Once
sessions chan *packetSession
laddr net.Addr
// mu protects sessions (against closure) and isClosed.
mu *sync.Mutex
sessions chan *packetSession
writeRequests chan *packetConnWriteReq
// deadlineMu protects readDeadline and writeDeadline.
deadlineMu *sync.RWMutex
readDeadline time.Time
writeDeadline time.Time
writeRequests chan *packetConnWriteReq
laddr net.Addr
subnet netip.Prefix
isClosed bool
}
// newChanPacketConn returns a new properly initialized *chanPacketConn.
func newChanPacketConn(
sessions chan *packetSession,
subnet netip.Prefix,
writeRequests chan *packetConnWriteReq,
laddr net.Addr,
) (c *chanPacketConn) {
return &chanPacketConn{
closeOnce: &sync.Once{},
sessions: sessions,
laddr: laddr,
mu: &sync.Mutex{},
sessions: sessions,
writeRequests: writeRequests,
deadlineMu: &sync.RWMutex{},
writeRequests: writeRequests,
laddr: laddr,
subnet: subnet,
}
}
@ -70,16 +77,16 @@ var _ netext.SessionPacketConn = (*chanPacketConn)(nil)
// Close implements the [netext.SessionPacketConn] interface for
// *chanPacketConn.
func (c *chanPacketConn) Close() (err error) {
closedNow := false
c.closeOnce.Do(func() {
close(c.sessions)
closedNow = true
})
c.mu.Lock()
defer c.mu.Unlock()
if !closedNow {
if c.isClosed {
return wrapConnError(tnChanPConn, "Close", c.laddr, net.ErrClosed)
}
close(c.sessions)
c.isClosed = true
return nil
}
@ -289,3 +296,18 @@ func sendWithTimer[T any](ch chan<- T, v T, timerCh <-chan time.Time) (err error
return os.ErrDeadlineExceeded
}
}
// send is a helper method to send a session to the packet connection's channel.
// ok is false if the listener is closed.
func (c *chanPacketConn) send(sess *packetSession) (ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
if c.isClosed {
return false
}
c.sessions <- sess
return true
}

View File

@ -14,7 +14,7 @@ import (
func TestChanPacketConn_Close(t *testing.T) {
sessions := make(chan *packetSession)
c := newChanPacketConn(sessions, nil, testLAddr)
c := newChanPacketConn(sessions, testSubnetIPv4, nil, testLAddr)
err := c.Close()
assert.NoError(t, err)
@ -23,14 +23,14 @@ func TestChanPacketConn_Close(t *testing.T) {
}
func TestChanPacketConn_LocalAddr(t *testing.T) {
c := newChanPacketConn(nil, nil, testLAddr)
c := newChanPacketConn(nil, testSubnetIPv4, nil, testLAddr)
got := c.LocalAddr()
assert.Equal(t, testLAddr, got)
}
func TestChanPacketConn_ReadFromSession(t *testing.T) {
sessions := make(chan *packetSession, 1)
c := newChanPacketConn(sessions, nil, testLAddr)
c := newChanPacketConn(sessions, testSubnetIPv4, nil, testLAddr)
body := []byte("hello")
bodyLen := len(body)
@ -79,7 +79,7 @@ func TestChanPacketConn_ReadFromSession(t *testing.T) {
func TestChanPacketConn_WriteToSession(t *testing.T) {
sessions := make(chan *packetSession, 1)
writes := make(chan *packetConnWriteReq, 1)
c := newChanPacketConn(sessions, writes, testLAddr)
c := newChanPacketConn(sessions, testSubnetIPv4, writes, testLAddr)
body := []byte("hello")
bodyLen := len(body)
@ -148,7 +148,7 @@ func checkWriteReqAndRespond(
}
func TestChanPacketConn_deadlines(t *testing.T) {
c := newChanPacketConn(nil, nil, testLAddr)
c := newChanPacketConn(nil, testSubnetIPv4, nil, testLAddr)
deadline := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
testCases := []struct {

View File

@ -4,33 +4,20 @@ package bindtodevice
import (
"fmt"
"net"
"net/netip"
"golang.org/x/exp/slices"
)
// chanIndex is the data structure that contains the channels, to which the
// [Manager] sends new connections and packets based on their protocol (TCP vs.
// UDP), and subnet.
// connIndex is the data structure that contains the channel listeners and
// packet connections, to which the [Manager] sends new connections and packets
// based on their protocol (TCP vs. UDP), and subnet.
//
// In both slices a subnet with the largest prefix (the narrowest subnet) is
// sorted closer to the beginning.
type chanIndex struct {
packetConns []*indexPacketConn
listeners []*indexListener
}
// indexPacketConn contains data of a [chanPacketConn] in the index.
type indexPacketConn struct {
channel chan *packetSession
subnet netip.Prefix
}
// indexListener contains data of a [chanListener] in the index.
type indexListener struct {
channel chan net.Conn
subnet netip.Prefix
type connIndex struct {
packetConns []*chanPacketConn
listeners []*chanListener
}
// subnetSortsBefore returns true if subnet x sorts before subnet y.
@ -58,26 +45,18 @@ func subnetCompare(x, y netip.Prefix) (cmp int) {
}
}
// addPacketConnChannel adds the channel to the subnet index. It returns an
// error if there is already one for this subnet. subnet should be masked.
// addPacketConn adds the channel packet connection to the index. It returns an
// error if there is already one for this subnet. c.subnet should be masked.
//
// TODO(a.garipov): Merge with [addListenerChannel].
func (idx *chanIndex) addPacketConnChannel(
subnet netip.Prefix,
ch chan *packetSession,
) (err error) {
c := &indexPacketConn{
channel: ch,
subnet: subnet,
}
cmpFunc := func(x, y *indexPacketConn) (cmp int) {
func (idx *connIndex) addPacketConn(c *chanPacketConn) (err error) {
cmpFunc := func(x, y *chanPacketConn) (cmp int) {
return subnetCompare(x.subnet, y.subnet)
}
newIdx, ok := slices.BinarySearchFunc(idx.packetConns, c, cmpFunc)
if ok {
return fmt.Errorf("packetconn channel for subnet %s already registered", subnet)
return fmt.Errorf("packetconn channel for subnet %s already registered", c.subnet)
}
// TODO(a.garipov): Consider using a list for idx.packetConns. Currently,
@ -88,23 +67,18 @@ func (idx *chanIndex) addPacketConnChannel(
return nil
}
// addListenerChannel adds the channel to the subnet index. It returns an error
// if there is already one for this subnet. subnet should be masked.
// addListener adds the channel listener to the index. It returns an error if
// there is already one for this subnet. l.subnet should be masked.
//
// TODO(a.garipov): Merge with [addPacketConnChannel].
func (idx *chanIndex) addListenerChannel(subnet netip.Prefix, ch chan net.Conn) (err error) {
l := &indexListener{
channel: ch,
subnet: subnet,
}
cmpFunc := func(x, y *indexListener) (cmp int) {
func (idx *connIndex) addListener(l *chanListener) (err error) {
cmpFunc := func(x, y *chanListener) (cmp int) {
return subnetCompare(x.subnet, y.subnet)
}
newIdx, ok := slices.BinarySearchFunc(idx.listeners, l, cmpFunc)
if ok {
return fmt.Errorf("listener channel for subnet %s already registered", subnet)
return fmt.Errorf("listener channel for subnet %s already registered", l.subnet)
}
// TODO(a.garipov): Consider using a list for idx.listeners. Currently,
@ -115,24 +89,24 @@ func (idx *chanIndex) addListenerChannel(subnet netip.Prefix, ch chan net.Conn)
return nil
}
// packetConnChannel returns a packet-connection channel which accepts
// connections to local address laddr or nil if there is no such channel
func (idx *chanIndex) packetConnChannel(laddr netip.Addr) (ch chan *packetSession) {
for _, c := range idx.packetConns {
// packetConn returns a channel packet connection which accepts connections to
// local address laddr or nil if there is no such channel
func (idx *connIndex) packetConn(laddr netip.Addr) (c *chanPacketConn) {
for _, c = range idx.packetConns {
if c.subnet.Contains(laddr) {
return c.channel
return c
}
}
return nil
}
// listenerChannel returns a listener channel which accepts connections to local
// listener returns a channel listener which accepts connections to local
// address laddr or nil if there is no such channel
func (idx *chanIndex) listenerChannel(laddr netip.Addr) (ch chan net.Conn) {
for _, l := range idx.listeners {
func (idx *connIndex) listener(laddr netip.Addr) (l *chanListener) {
for _, l = range idx.listeners {
if l.subnet.Contains(laddr) {
return l.channel
return l
}
}

View File

@ -17,7 +17,7 @@ import (
// interfaceListener contains information about a single interface listener.
type interfaceListener struct {
channels *chanIndex
conns *connIndex
writeRequests chan *packetConnWriteReq
done chan unit
listenConf *net.ListenConfig
@ -64,14 +64,16 @@ func (l *interfaceListener) listenTCP(errCh chan<- error) {
}
laddr := netutil.NetAddrToAddrPort(conn.LocalAddr())
ch := l.channels.listenerChannel(laddr.Addr())
if ch == nil {
lsnr := l.conns.listener(laddr.Addr())
if lsnr == nil {
log.Info("%s: no channel for laddr %s", logPrefix, laddr)
continue
}
ch <- conn
if !lsnr.send(conn) {
log.Info("%s: channel for laddr %s is closed", logPrefix, laddr)
}
}
}
@ -120,14 +122,16 @@ func (l *interfaceListener) listenUDP(errCh chan<- error) {
}
laddr := sess.laddr.AddrPort().Addr()
ch := l.channels.packetConnChannel(laddr)
if ch == nil {
chanPConn := l.conns.packetConn(laddr)
if chanPConn == nil {
log.Info("%s: no channel for laddr %s", logPrefix, laddr)
continue
}
ch <- sess
if !chanPConn.send(sess) {
log.Info("%s: channel for laddr %s is closed", logPrefix, laddr)
}
}
}

View File

@ -0,0 +1,77 @@
package bindtodevice
import (
"fmt"
"net"
"net/netip"
"github.com/AdguardTeam/golibs/netutil"
)
// NetInterface represents a network interface (aka device).
//
// TODO(a.garipov): Consider moving this and InterfaceStorage to netutil.
type NetInterface interface {
Subnets() (subnets []netip.Prefix, err error)
}
// type check
var _ NetInterface = osInterface{}
// osInterface is a wapper around [*net.Interface] that implements the
// [NetInterface] interface.
type osInterface struct {
iface *net.Interface
}
// Subnets implements the [NetInterface] interface for osInterface.
func (osIface osInterface) Subnets() (subnets []netip.Prefix, err error) {
name := osIface.iface.Name
ifaceAddrs, err := osIface.iface.Addrs()
if err != nil {
return nil, fmt.Errorf("getting addrs for interface %s: %w", name, err)
}
subnets = make([]netip.Prefix, 0, len(ifaceAddrs))
for _, addr := range ifaceAddrs {
ipNet, ok := addr.(*net.IPNet)
if !ok {
return nil, fmt.Errorf("addr for interface %s is %T, not *net.IPNet", name, addr)
}
var subnet netip.Prefix
subnet, err = netutil.IPNetToPrefixNoMapped(ipNet)
if err != nil {
return nil, fmt.Errorf("converting addr for interface %s: %w", name, err)
}
subnets = append(subnets, subnet)
}
return subnets, nil
}
// InterfaceStorage is the interface for storages of network interfaces (aka
// devices). Its main implementation is [DefaultInterfaceStorage].
type InterfaceStorage interface {
InterfaceByName(name string) (iface NetInterface, err error)
}
// type check
var _ InterfaceStorage = DefaultInterfaceStorage{}
// DefaultInterfaceStorage is the storage that uses the OS's network interfaces.
type DefaultInterfaceStorage struct{}
// InterfaceByName implements the [InterfaceStorage] interface for
// DefaultInterfaceStorage.
func (DefaultInterfaceStorage) InterfaceByName(name string) (iface NetInterface, err error) {
netIface, err := net.InterfaceByName(name)
if err != nil {
return nil, fmt.Errorf("looking up interface %s: %w", name, err)
}
return &osInterface{
iface: netIface,
}, nil
}

View File

@ -5,6 +5,10 @@ import "github.com/AdguardTeam/AdGuardDNS/internal/agd"
// ManagerConfig is the configuration structure for [NewManager]. All fields
// must be set.
type ManagerConfig struct {
// InterfaceStorage is used to get the information about the system's
// network interfaces. Normally, this is [DefaultInterfaceStorage].
InterfaceStorage InterfaceStorage
// ErrColl is the error collector that is used to collect non-critical
// errors.
ErrColl agd.ErrorCollector
@ -13,3 +17,14 @@ type ManagerConfig struct {
// dispatch TCP connections and UDP sessions.
ChannelBufferSize int
}
// ControlConfig is the configuration of socket options.
type ControlConfig struct {
// RcvBufSize defines the size of socket receive buffer in bytes. Default
// is zero (uses system settings).
RcvBufSize int
// SndBufSize defines the size of socket send buffer in bytes. Default is
// zero (uses system settings).
SndBufSize int
}

View File

@ -18,6 +18,7 @@ import (
// Manager creates individual listeners and dispatches connections to them.
type Manager struct {
interfaces InterfaceStorage
closeOnce *sync.Once
ifaceListeners map[ID]*interfaceListener
errColl agd.ErrorCollector
@ -28,6 +29,7 @@ type Manager struct {
// NewManager returns a new manager of interface listeners.
func NewManager(c *ManagerConfig) (m *Manager) {
return &Manager{
interfaces: c.InterfaceStorage,
closeOnce: &sync.Once{},
ifaceListeners: map[ID]*interfaceListener{},
errColl: c.ErrColl,
@ -36,12 +38,25 @@ func NewManager(c *ManagerConfig) (m *Manager) {
}
}
// Add creates a new interface-listener record in m.
// defaultCtrlConf is the default control config. By default, don't alter
// anything. defaultCtrlConf must not be mutated.
var defaultCtrlConf = &ControlConfig{
RcvBufSize: 0,
SndBufSize: 0,
}
// Add creates a new interface-listener record in m. If conf is nil, a default
// configuration is used.
//
// Add must not be called after Start is called.
func (m *Manager) Add(id ID, ifaceName string, port uint16) (err error) {
func (m *Manager) Add(id ID, ifaceName string, port uint16, conf *ControlConfig) (err error) {
defer func() { err = errors.Annotate(err, "adding interface listener with id %q: %w", id) }()
_, err = m.interfaces.InterfaceByName(ifaceName)
if err != nil {
return fmt.Errorf("looking up interface %q: %w", ifaceName, err)
}
validateDup := func(lsnrID ID, lsnr *interfaceListener) (lsnrErr error) {
lsnrIfaceName, lsnrPort := lsnr.ifaceName, lsnr.port
if lsnrID == id {
@ -71,11 +86,15 @@ func (m *Manager) Add(id ID, ifaceName string, port uint16) (err error) {
return err
}
if conf == nil {
conf = defaultCtrlConf
}
m.ifaceListeners[id] = &interfaceListener{
channels: &chanIndex{},
conns: &connIndex{},
writeRequests: make(chan *packetConnWriteReq, m.chanBufSize),
done: m.done,
listenConf: newListenConfig(ifaceName),
listenConf: newListenConfig(ifaceName, conf),
errColl: m.errColl,
ifaceName: ifaceName,
port: port,
@ -90,53 +109,96 @@ func (m *Manager) Add(id ID, ifaceName string, port uint16) (err error) {
//
// ListenConfig must not be called after Start is called.
func (m *Manager) ListenConfig(id ID, subnet netip.Prefix) (c netext.ListenConfig, err error) {
if masked := subnet.Masked(); subnet != masked {
return nil, fmt.Errorf(
"subnet %s for interface listener %q not masked (expected %s)",
defer func() {
err = errors.Annotate(
err,
"creating listen config for subnet %s and listener with id %q: %w",
subnet,
id,
masked,
)
}
}()
l, ok := m.ifaceListeners[id]
if !ok {
return nil, fmt.Errorf("no listener for interface %q", id)
return nil, errors.Error("no interface listener found")
}
connCh := make(chan net.Conn, m.chanBufSize)
err = l.channels.addListenerChannel(subnet, connCh)
err = m.validateIfaceSubnet(l.ifaceName, subnet)
if err != nil {
return nil, fmt.Errorf("adding tcp conn channel: %w", err)
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
lsnrCh := make(chan net.Conn, m.chanBufSize)
lsnr := newChanListener(lsnrCh, subnet, &prefixNetAddr{
prefix: subnet,
network: "tcp",
port: l.port,
})
err = l.conns.addListener(lsnr)
if err != nil {
return nil, fmt.Errorf("adding tcp conn: %w", err)
}
sessCh := make(chan *packetSession, m.chanBufSize)
err = l.channels.addPacketConnChannel(subnet, sessCh)
pConn := newChanPacketConn(sessCh, subnet, l.writeRequests, &prefixNetAddr{
prefix: subnet,
network: "udp",
port: l.port,
})
err = l.conns.addPacketConn(pConn)
if err != nil {
// Technically shouldn't happen, since [chanIndex.addListenerChannel]
// has already checked for duplicates.
return nil, fmt.Errorf("adding udp conn channel: %w", err)
return nil, fmt.Errorf("adding udp conn: %w", err)
}
return &chanListenConfig{
packetConn: newChanPacketConn(sessCh, l.writeRequests, &prefixNetAddr{
prefix: subnet,
network: "udp",
port: l.port,
}),
listener: newChanListener(connCh, &prefixNetAddr{
prefix: subnet,
network: "tcp",
port: l.port,
}),
packetConn: pConn,
listener: lsnr,
}, nil
}
// validateIfaceSubnet validates the interface with the name ifaceName exists
// and that it can accept addresses from subnet.
func (m *Manager) validateIfaceSubnet(ifaceName string, subnet netip.Prefix) (err error) {
if masked := subnet.Masked(); subnet != masked {
return fmt.Errorf("subnet not masked (expected %s)", masked)
}
iface, err := m.interfaces.InterfaceByName(ifaceName)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
ifaceSubnets, err := iface.Subnets()
if err != nil {
return fmt.Errorf("getting subnets: %w", err)
}
for _, s := range ifaceSubnets {
if s.Contains(subnet.Addr()) && s.Bits() <= subnet.Bits() {
return nil
}
}
return fmt.Errorf("interface %s does not contain subnet %s", ifaceName, subnet)
}
// type check
var _ agd.Service = (*Manager)(nil)
// Start implements the [agd.Service] interface for *Manager.
// Start implements the [agd.Service] interface for *Manager. If m is nil,
// Start returns nil, since this feature is optional.
//
// TODO(a.garipov): Consider an interface solution.
func (m *Manager) Start() (err error) {
if m == nil {
return nil
}
numListen := 2 * len(m.ifaceListeners)
errCh := make(chan error, numListen)
@ -162,10 +224,17 @@ func (m *Manager) Start() (err error) {
return nil
}
// Shutdown implements the [agd.Service] interface for *Manager.
// Shutdown implements the [agd.Service] interface for *Manager. If m is nil,
// Shutdown returns nil, since this feature is optional.
//
// TODO(a.garipov): Consider an interface solution.
//
// TODO(a.garipov): Consider waiting for all sockets to close.
func (m *Manager) Shutdown(_ context.Context) (err error) {
if m == nil {
return nil
}
closedNow := false
m.closeOnce.Do(func() {
close(m.done)

View File

@ -18,12 +18,46 @@ import (
// TODO(a.garipov): Add tests for other platforms?
// type check
var _ bindtodevice.InterfaceStorage = (*fakeInterfaceStorage)(nil)
// fakeInterfaceStorage is a fake [bindtodevice.InterfaceStorage] for tests.
type fakeInterfaceStorage struct {
OnInterfaceByName func(name string) (iface bindtodevice.NetInterface, err error)
}
// InterfaceByName implements the [bindtodevice.InterfaceStorage] interface
// for *fakeInterfaceStorage.
func (s *fakeInterfaceStorage) InterfaceByName(
name string,
) (iface bindtodevice.NetInterface, err error) {
return s.OnInterfaceByName(name)
}
// type check
var _ bindtodevice.NetInterface = (*fakeInterface)(nil)
// fakeInterface is a fake [bindtodevice.Interface] for tests.
type fakeInterface struct {
OnSubnets func() (subnets []netip.Prefix, err error)
}
// Subnets implements the [bindtodevice.Interface] interface for *fakeInterface.
func (iface *fakeInterface) Subnets() (subnets []netip.Prefix, err error) {
return iface.OnSubnets()
}
func TestManager_Add(t *testing.T) {
errColl := &agdtest.ErrorCollector{
OnCollect: func(_ context.Context, _ error) { panic("not implemented") },
}
m := bindtodevice.NewManager(&bindtodevice.ManagerConfig{
InterfaceStorage: &fakeInterfaceStorage{
OnInterfaceByName: func(_ string) (iface bindtodevice.NetInterface, err error) {
return nil, nil
},
},
ErrColl: errColl,
ChannelBufferSize: 1,
})
@ -32,22 +66,22 @@ func TestManager_Add(t *testing.T) {
// Don't use a table, since the results of these subtests depend on each
// other.
t.Run("success", func(t *testing.T) {
err := m.Add(testID1, testIfaceName, testPort1)
err := m.Add(testID1, testIfaceName, testPort1, nil)
assert.NoError(t, err)
})
t.Run("dup_id", func(t *testing.T) {
err := m.Add(testID1, testIfaceName, testPort1)
err := m.Add(testID1, testIfaceName, testPort1, nil)
assert.Error(t, err)
})
t.Run("dup_iface_port", func(t *testing.T) {
err := m.Add(testID2, testIfaceName, testPort1)
err := m.Add(testID2, testIfaceName, testPort1, nil)
assert.Error(t, err)
})
t.Run("success_other", func(t *testing.T) {
err := m.Add(testID2, testIfaceName, testPort2)
err := m.Add(testID2, testIfaceName, testPort2, nil)
assert.NoError(t, err)
})
}
@ -57,17 +91,27 @@ func TestManager_ListenConfig(t *testing.T) {
OnCollect: func(_ context.Context, _ error) { panic("not implemented") },
}
subnet := testSubnetIPv4
ifaceWithSubnet := &fakeInterface{
OnSubnets: func() (subnets []netip.Prefix, err error) {
return []netip.Prefix{subnet}, nil
},
}
m := bindtodevice.NewManager(&bindtodevice.ManagerConfig{
InterfaceStorage: &fakeInterfaceStorage{
OnInterfaceByName: func(_ string) (iface bindtodevice.NetInterface, err error) {
return ifaceWithSubnet, nil
},
},
ErrColl: errColl,
ChannelBufferSize: 1,
})
require.NotNil(t, m)
err := m.Add(testID1, testIfaceName, testPort1)
err := m.Add(testID1, testIfaceName, testPort1, nil)
require.NoError(t, err)
subnet := netip.MustParsePrefix("1.2.3.0/24")
// Don't use a table, since the results of these subtests depend on each
// other.
t.Run("not_found", func(t *testing.T) {
@ -94,6 +138,60 @@ func TestManager_ListenConfig(t *testing.T) {
assert.Nil(t, lc)
assert.Error(t, lcErr)
})
t.Run("no_subnet", func(t *testing.T) {
ifaceWithoutSubnet := &fakeInterface{
OnSubnets: func() (subnets []netip.Prefix, err error) {
return nil, nil
},
}
noSubnetMgr := bindtodevice.NewManager(&bindtodevice.ManagerConfig{
InterfaceStorage: &fakeInterfaceStorage{
OnInterfaceByName: func(_ string) (iface bindtodevice.NetInterface, err error) {
return ifaceWithoutSubnet, nil
},
},
ErrColl: errColl,
ChannelBufferSize: 1,
})
require.NotNil(t, noSubnetMgr)
subTestErr := noSubnetMgr.Add(testID1, testIfaceName, testPort1, nil)
require.NoError(t, subTestErr)
lc, subTestErr := noSubnetMgr.ListenConfig(testID1, subnet)
assert.Nil(t, lc)
assert.Error(t, subTestErr)
})
t.Run("narrower_subnet", func(t *testing.T) {
ifaceWithNarrowerSubnet := &fakeInterface{
OnSubnets: func() (subnets []netip.Prefix, err error) {
narrowerSubnet := netip.PrefixFrom(subnet.Addr(), subnet.Bits()+4)
return []netip.Prefix{narrowerSubnet}, nil
},
}
narrowSubnetMgr := bindtodevice.NewManager(&bindtodevice.ManagerConfig{
InterfaceStorage: &fakeInterfaceStorage{
OnInterfaceByName: func(_ string) (iface bindtodevice.NetInterface, err error) {
return ifaceWithNarrowerSubnet, nil
},
},
ErrColl: errColl,
ChannelBufferSize: 1,
})
require.NotNil(t, narrowSubnetMgr)
subTestErr := narrowSubnetMgr.Add(testID1, testIfaceName, testPort1, nil)
require.NoError(t, subTestErr)
lc, subTestErr := narrowSubnetMgr.ListenConfig(testID1, subnet)
assert.Nil(t, lc)
assert.Error(t, subTestErr)
})
}
func TestManager(t *testing.T) {
@ -113,13 +211,14 @@ func TestManager(t *testing.T) {
}
m := bindtodevice.NewManager(&bindtodevice.ManagerConfig{
InterfaceStorage: bindtodevice.DefaultInterfaceStorage{},
ErrColl: errColl,
ChannelBufferSize: 1,
})
require.NotNil(t, m)
// TODO(a.garipov): Add support for zero port.
err := m.Add(testID1, ifaceName, testPort1)
err := m.Add(testID1, ifaceName, testPort1, nil)
require.NoError(t, err)
subnet, err := netutil.IPNetToPrefixNoMapped(&net.IPNet{

View File

@ -13,12 +13,12 @@ import (
// Manager creates individual listeners and dispatches connections to them.
//
// It is only suported on Linux.
// It is only supported on Linux.
type Manager struct{}
// NewManager returns a new manager of interface listeners.
//
// It is only suported on Linux.
// It is only supported on Linux.
func NewManager(c *ManagerConfig) (m *Manager) {
return &Manager{}
}
@ -29,14 +29,16 @@ const errUnsupported errors.Error = "bindtodevice is only supported on linux"
// Add creates a new interface-listener record in m.
//
// It is only suported on Linux.
func (m *Manager) Add(id ID, ifaceName string, port uint16) (err error) { return errUnsupported }
// It is only supported on Linux.
func (m *Manager) Add(id ID, ifaceName string, port uint16, cc *ControlConfig) (err error) {
return errUnsupported
}
// ListenConfig returns a new netext.ListenConfig that receives connections from
// the interface listener with the given id and the destination addresses of
// which fall within subnet. subnet should be masked.
//
// It is only suported on Linux.
// It is only supported on Linux.
func (m *Manager) ListenConfig(id ID, subnet netip.Prefix) (c netext.ListenConfig, err error) {
return nil, errUnsupported
}
@ -44,12 +46,26 @@ func (m *Manager) ListenConfig(id ID, subnet netip.Prefix) (c netext.ListenConfi
// type check
var _ agd.Service = (*Manager)(nil)
// Start implements the [agd.Service] interface for *Manager.
// Start implements the [agd.Service] interface for *Manager. If m is nil,
// Start returns nil, since this feature is optional.
//
// It is only suported on Linux.
func (m *Manager) Start() (err error) { return errUnsupported }
// It is only supported on Linux.
func (m *Manager) Start() (err error) {
if m == nil {
return nil
}
// Shutdown implements the [agd.Service] interface for *Manager.
return errUnsupported
}
// Shutdown implements the [agd.Service] interface for *Manager. If m is nil,
// Shutdown returns nil, since this feature is optional.
//
// It is only suported on Linux.
func (m *Manager) Shutdown(_ context.Context) (err error) { return errUnsupported }
// It is only supported on Linux.
func (m *Manager) Shutdown(_ context.Context) (err error) {
if m == nil {
return nil
}
return errUnsupported
}

View File

@ -3,6 +3,7 @@
package bindtodevice
import (
"fmt"
"net/netip"
"testing"
@ -11,16 +12,42 @@ import (
func TestPrefixAddr(t *testing.T) {
const (
wantStr = "1.2.3.0:56789/24"
port = 56789
network = "tcp"
)
pa := &prefixNetAddr{
prefix: netip.MustParsePrefix("1.2.3.0/24"),
network: network,
port: 56789,
}
testCases := []struct {
in *prefixNetAddr
want string
name string
}{{
in: &prefixNetAddr{
prefix: testSubnetIPv4,
network: network,
port: port,
},
want: fmt.Sprintf(
"%s/%d",
netip.AddrPortFrom(testSubnetIPv4.Addr(), port), testSubnetIPv4.Bits(),
),
name: "ipv4",
}, {
in: &prefixNetAddr{
prefix: testSubnetIPv6,
network: network,
port: port,
},
want: fmt.Sprintf(
"%s/%d",
netip.AddrPortFrom(testSubnetIPv6.Addr(), port), testSubnetIPv6.Bits(),
),
name: "ipv6",
}}
assert.Equal(t, wantStr, pa.String())
assert.Equal(t, network, pa.Network())
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, tc.in.String())
assert.Equal(t, network, tc.in.Network())
})
}
}

View File

@ -12,106 +12,100 @@ import (
"golang.org/x/sys/unix"
)
// newListenConfig returns a [net.ListenConfig] that can bind to a network
// interface (device) by its name.
func newListenConfig(devName string) (lc *net.ListenConfig) {
c := &net.ListenConfig{
Control: func(network, address string, c syscall.RawConn) (err error) {
return listenControl(devName, network, address, c)
},
}
// setSockOptFunc is a function that sets a socket option on fd.
type setSockOptFunc func(fd int) (err error)
return c
// newIntSetSockOptFunc returns an integer socket-option function with the given
// parameters.
func newIntSetSockOptFunc(name string, lvl, opt, val int) (o setSockOptFunc) {
return func(fd int) (err error) {
opErr := unix.SetsockoptInt(fd, lvl, opt, val)
return errors.Annotate(opErr, "setting %s: %w", name)
}
}
// listenControl is used as a [net.ListenConfig.Control] function to set
// additional socket options, including SO_BINDTODEVICE.
func listenControl(devName, network, _ string, c syscall.RawConn) (err error) {
var ctrlFunc func(fd uintptr, devName string) (err error)
// newStringSetSockOptFunc returns a string socket-option function with the
// given parameters.
func newStringSetSockOptFunc(name string, lvl, opt int, val string) (o setSockOptFunc) {
return func(fd int) (err error) {
opErr := unix.SetsockoptString(fd, lvl, opt, val)
return errors.Annotate(opErr, "setting %s: %w", name)
}
}
// newListenConfig returns a [net.ListenConfig] that can bind to a network
// interface (device) by its name. ctrlConf must not be nil.
func newListenConfig(devName string, ctrlConf *ControlConfig) (lc *net.ListenConfig) {
return &net.ListenConfig{
Control: func(network, address string, c syscall.RawConn) (err error) {
return listenControlWithSO(ctrlConf, devName, network, address, c)
},
}
}
// listenControlWithSO is used as a [net.ListenConfig.Control] function to set
// additional socket options.
func listenControlWithSO(
ctrlConf *ControlConfig,
devName string,
network string,
_ string,
c syscall.RawConn,
) (err error) {
opts := []setSockOptFunc{
newStringSetSockOptFunc("SO_BINDTODEVICE", unix.SOL_SOCKET, unix.SO_BINDTODEVICE, devName),
// Use SO_REUSEADDR as well, which is not technically necessary, to
// help with the situation of sockets hanging in CLOSE_WAIT for too
// long.
newIntSetSockOptFunc("SO_REUSEADDR", unix.SOL_SOCKET, unix.SO_REUSEADDR, 1),
newIntSetSockOptFunc("SO_REUSEPORT", unix.SOL_SOCKET, unix.SO_REUSEPORT, 1),
}
switch network {
case "tcp", "tcp4", "tcp6":
ctrlFunc = setTCPSockOpt
// Socket options for TCP connection already set. Go on.
case "udp", "udp4", "udp6":
ctrlFunc = setUDPSockOpt
opts = append(
opts,
newIntSetSockOptFunc("IP_RECVORIGDSTADDR", unix.IPPROTO_IP, unix.IP_RECVORIGDSTADDR, 1),
newIntSetSockOptFunc("IP_FREEBIND", unix.IPPROTO_IP, unix.IP_FREEBIND, 1),
newIntSetSockOptFunc("IPV6_RECVORIGDSTADDR", unix.IPPROTO_IPV6, unix.IPV6_RECVORIGDSTADDR, 1),
newIntSetSockOptFunc("IPV6_FREEBIND", unix.IPPROTO_IPV6, unix.IPV6_FREEBIND, 1),
)
default:
return fmt.Errorf("bad network %q", network)
}
if ctrlConf.SndBufSize > 0 {
opts = append(
opts,
newIntSetSockOptFunc("SO_SNDBUF", unix.SOL_SOCKET, unix.SO_SNDBUF, ctrlConf.SndBufSize),
)
}
if ctrlConf.RcvBufSize > 0 {
opts = append(
opts,
newIntSetSockOptFunc("SO_RCVBUF", unix.SOL_SOCKET, unix.SO_RCVBUF, ctrlConf.RcvBufSize),
)
}
var opErr error
err = c.Control(func(fd uintptr) {
opErr = ctrlFunc(fd, devName)
d := int(fd)
for _, opt := range opts {
opErr = opt(d)
if opErr != nil {
return
}
}
})
return errors.WithDeferred(opErr, err)
}
// setTCPSockOpt sets the SO_BINDTODEVICE and other socket options for a TCP
// connection.
func setTCPSockOpt(fd uintptr, devName string) (err error) {
defer func() { err = errors.Annotate(err, "setting tcp opts: %w") }()
fdInt := int(fd)
err = unix.SetsockoptString(fdInt, unix.SOL_SOCKET, unix.SO_BINDTODEVICE, devName)
if err != nil {
return fmt.Errorf("setting SO_BINDTODEVICE: %w", err)
}
err = unix.SetsockoptInt(fdInt, unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
if err != nil {
return fmt.Errorf("setting SO_REUSEPORT: %w", err)
}
return nil
}
// setUDPSockOpt sets the SO_BINDTODEVICE and other socket options for a UDP
// connection.
func setUDPSockOpt(fd uintptr, devName string) (err error) {
defer func() { err = errors.Annotate(err, "setting udp opts: %w") }()
fdInt := int(fd)
err = unix.SetsockoptString(fdInt, unix.SOL_SOCKET, unix.SO_BINDTODEVICE, devName)
if err != nil {
return fmt.Errorf("setting SO_BINDTODEVICE: %w", err)
}
intOpts := []struct {
name string
level int
opt int
}{{
name: "SO_REUSEPORT",
level: unix.SOL_SOCKET,
opt: unix.SO_REUSEPORT,
}, {
name: "IP_RECVORIGDSTADDR",
level: unix.IPPROTO_IP,
opt: unix.IP_RECVORIGDSTADDR,
}, {
name: "IP_FREEBIND",
level: unix.IPPROTO_IP,
opt: unix.IP_FREEBIND,
}, {
name: "IPV6_RECVORIGDSTADDR",
level: unix.IPPROTO_IPV6,
opt: unix.IPV6_RECVORIGDSTADDR,
}, {
name: "IPV6_FREEBIND",
level: unix.IPPROTO_IPV6,
opt: unix.IPV6_FREEBIND,
}}
for _, o := range intOpts {
err = unix.SetsockoptInt(fdInt, o.level, o.opt, 1)
if err != nil {
return fmt.Errorf("setting %s: %w", o.name, err)
}
}
return nil
}
// readPacketSession is a helper that reads a packet-session data from a UDP
// connection.
func readPacketSession(c *net.UDPConn, bodySize int) (sess *packetSession, err error) {
@ -165,8 +159,11 @@ func sockAddrData(sockAddr unix.Sockaddr) (origDstAddr *net.UDPAddr, respOOB []b
Port: sockAddr.Port,
}
// Set both addresses to make sure that users receive the correct source
// IP address even when virtual interfaces are involved.
pktInfo := &unix.Inet4Pktinfo{
Addr: sockAddr.Addr,
Addr: sockAddr.Addr,
Spec_dst: sockAddr.Addr,
}
respOOB = unix.PktInfo4(pktInfo)

View File

@ -9,6 +9,7 @@ import (
"net/netip"
"os"
"strings"
"syscall"
"testing"
"time"
@ -18,6 +19,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"
"golang.org/x/sys/unix"
)
// TestInterfaceEnvVarName is the environment variable name the presence and
@ -88,7 +90,7 @@ func TestListenControl(t *testing.T) {
}
ifaceName := iface.Name
lc := newListenConfig(ifaceName)
lc := newListenConfig(ifaceName, &ControlConfig{})
require.NotNil(t, lc)
t.Run("tcp", func(t *testing.T) {
@ -380,3 +382,81 @@ func closestIP(t testing.TB, n *net.IPNet, ip net.IP) (closest net.IP) {
return nil
}
func TestListenControlWithSO(t *testing.T) {
const (
sndBufSize = 10000
rcvBufSize = 20000
)
iface, _ := InterfaceForTests(t)
if iface == nil {
t.Skipf("test %s skipped: please set env var %s", t.Name(), TestInterfaceEnvVarName)
}
ifaceName := iface.Name
lc := newListenConfig(
ifaceName,
&ControlConfig{
RcvBufSize: rcvBufSize,
SndBufSize: sndBufSize,
},
)
require.NotNil(t, lc)
type syscallConner interface {
SyscallConn() (c syscall.RawConn, err error)
}
t.Run("udp", func(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()
require.NoError(t, err)
err = sc.Control(func(fd uintptr) {
val, opErr := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_SNDBUF)
require.NoError(t, opErr)
assert.Equal(t, sndBufSize*2, val)
})
require.NoError(t, err)
err = sc.Control(func(fd uintptr) {
val, opErr := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_RCVBUF)
require.NoError(t, opErr)
assert.Equal(t, rcvBufSize*2, val)
})
require.NoError(t, err)
})
t.Run("tcp", func(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()
require.NoError(t, err)
err = sc.Control(func(fd uintptr) {
val, opErr := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_SNDBUF)
require.NoError(t, opErr)
assert.Equal(t, sndBufSize*2, val)
})
require.NoError(t, err)
err = sc.Control(func(fd uintptr) {
val, opErr := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_RCVBUF)
require.NoError(t, opErr)
assert.Equal(t, rcvBufSize*2, val)
})
require.NoError(t, err)
})
}

View File

@ -8,6 +8,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/backend"
"github.com/AdguardTeam/AdGuardDNS/internal/billstat"
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil"
)
@ -76,7 +77,7 @@ func setupBackend(
envs *environments,
sigHdlr signalHandler,
errColl agd.ErrorCollector,
) (profDB *agd.DefaultProfileDB, rec *billstat.RuntimeRecorder, err error) {
) (profDB *profiledb.Default, rec *billstat.RuntimeRecorder, err error) {
profStrgConf, billStatConf := conf.toInternal(envs, errColl)
rec = billstat.NewRuntimeRecorder(&billstat.RuntimeRecorderConfig{
Uploader: backend.NewBillStat(billStatConf),
@ -103,7 +104,7 @@ func setupBackend(
sigHdlr.add(billStatRefr)
profStrg := backend.NewProfileStorage(profStrgConf)
profDB, err = agd.NewDefaultProfileDB(
profDB, err = profiledb.New(
profStrg,
conf.FullRefreshIvl.Duration,
envs.ProfilesCachePath,

View File

@ -16,9 +16,9 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/dnscheck"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/forward"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/prometheus"
"github.com/AdguardTeam/AdGuardDNS/internal/dnssvc"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/hashprefix"
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/AdGuardDNS/internal/websvc"
@ -130,11 +130,23 @@ func Main() {
fltGroups, err := c.FilteringGroups.toInternal(fltStrg)
check(err)
// Network interface listener
btdCtrlConf, ctrlConf := c.Network.toInternal()
btdMgr, err := c.InterfaceListeners.toInternal(errColl, btdCtrlConf)
check(err)
err = btdMgr.Start()
check(err)
sigHdlr.add(btdMgr)
// Server groups
messages := dnsmsg.NewConstructor(&dnsmsg.BlockingModeNullIP{}, c.Filters.ResponseTTL.Duration)
srvGrps, err := c.ServerGroups.toInternal(messages, fltGroups)
srvGrps, err := c.ServerGroups.toInternal(messages, btdMgr, fltGroups)
check(err)
// TLS keys logging
@ -173,7 +185,7 @@ func Main() {
// Rate limiting
consulAllowlistURL := &envs.ConsulAllowlistURL.URL
rateLimiter, err := setupRateLimiter(c.RateLimit, consulAllowlistURL, sigHdlr, errColl)
rateLimiter, connLimiter, err := setupRateLimiter(c.RateLimit, consulAllowlistURL, sigHdlr, errColl)
check(err)
// GeoIP database
@ -197,24 +209,21 @@ func Main() {
// DNS service
metricsListener := prometheus.NewForwardMetricsListener(len(c.Upstream.FallbackServers) + 1)
upstream, err := c.Upstream.toInternal()
fwdConf, err := c.Upstream.toInternal()
check(err)
handler := forward.NewHandler(&forward.HandlerConfig{
Address: upstream.Server,
Network: upstream.Network,
MetricsListener: metricsListener,
HealthcheckDomainTmpl: c.Upstream.Healthcheck.DomainTmpl,
FallbackAddresses: c.Upstream.FallbackServers,
Timeout: c.Upstream.Timeout.Duration,
HealthcheckBackoffDuration: c.Upstream.Healthcheck.BackoffDuration.Duration,
}, c.Upstream.Healthcheck.Enabled)
handler := forward.NewHandler(fwdConf)
// TODO(a.garipov): Consider making these configurable via the configuration
// file.
hashStorages := map[string]*hashprefix.Storage{
filter.GeneralTXTSuffix: safeBrowsingHashes,
filter.AdultBlockingTXTSuffix: adultBlockingHashes,
}
dnsConf := &dnssvc.Config{
Messages: messages,
SafeBrowsing: filter.NewSafeBrowsingServer(safeBrowsingHashes, adultBlockingHashes),
SafeBrowsing: hashprefix.NewMatcher(hashStorages),
BillStat: billStatRec,
ProfileDB: profDB,
DNSCheck: dnsCk,
@ -226,21 +235,20 @@ func Main() {
Handler: handler,
QueryLog: c.buildQueryLog(envs),
RuleStat: ruleStat,
Upstream: upstream,
RateLimit: rateLimiter,
ConnLimiter: connLimiter,
FilteringGroups: fltGroups,
ServerGroups: srvGrps,
CacheSize: c.Cache.Size,
ECSCacheSize: c.Cache.ECSSize,
UseECSCache: c.Cache.Type == cacheTypeECS,
ResearchMetrics: bool(envs.ResearchMetrics),
ControlConf: ctrlConf,
}
dnsSvc, err := dnssvc.New(dnsConf)
check(err)
sigHdlr.add(dnsSvc)
// Connectivity check
err = connectivityCheck(dnsConf, c.ConnectivityCheck)

View File

@ -61,6 +61,13 @@ type configuration struct {
// ConnectivityCheck is the connectivity check configuration.
ConnectivityCheck *connCheckConfig `yaml:"connectivity_check"`
// InterfaceListeners is the configuration for the network interface
// listeners and their common parameters.
InterfaceListeners *interfaceListenersConfig `yaml:"interface_listeners"`
// Network is the configuration for network listeners.
Network *network `yaml:"network"`
// AdditionalMetricsInfo is extra information, which is exposed by metrics.
AdditionalMetricsInfo additionalInfo `yaml:"additional_metrics_info"`
@ -140,6 +147,12 @@ func (c *configuration) validate() (err error) {
}, {
validate: c.ConnectivityCheck.validate,
name: "connectivity_check",
}, {
validate: c.InterfaceListeners.validate,
name: "interface_listeners",
}, {
validate: c.Network.validate,
name: "network",
}, {
validate: c.AdditionalMetricsInfo.validate,
name: "additional_metrics_info",

View File

@ -34,8 +34,8 @@ type environments struct {
YoutubeSafeSearchURL *agdhttp.URL `env:"YOUTUBE_SAFE_SEARCH_URL,notEmpty"`
RuleStatURL *agdhttp.URL `env:"RULESTAT_URL"`
ConfPath string `env:"CONFIG_PATH" envDefault:"./config.yml"`
DNSDBPath string `env:"DNSDB_PATH" envDefault:"./dnsdb.bolt"`
ConfPath string `env:"CONFIG_PATH" envDefault:"./config.yaml"`
DNSDBPath string `env:"DNSDB_PATH"`
FilterCachePath string `env:"FILTER_CACHE_PATH" envDefault:"./filters/"`
ProfilesCachePath string `env:"PROFILES_CACHE_PATH" envDefault:"./profilecache.json"`
GeoIPASNPath string `env:"GEOIP_ASN_PATH" envDefault:"./asn.mmdb"`

View File

@ -8,6 +8,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdnet"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/hashprefix"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil"
)
@ -51,8 +52,8 @@ func (c *filtersConfig) toInternal(
errColl agd.ErrorCollector,
resolver agdnet.Resolver,
envs *environments,
safeBrowsing *filter.HashPrefix,
adultBlocking *filter.HashPrefix,
safeBrowsing *hashprefix.Filter,
adultBlocking *hashprefix.Filter,
) (conf *filter.DefaultStorageConfig) {
return &filter.DefaultStorageConfig{
FilterIndexURL: netutil.CloneURL(&envs.FilterIndexURL.URL),

View File

@ -0,0 +1,102 @@
package cmd
import (
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdmaps"
"github.com/AdguardTeam/AdGuardDNS/internal/bindtodevice"
"github.com/AdguardTeam/golibs/errors"
)
// Network interface listener configuration
// interfaceListenersConfig contains the optional configuration for the network
// interface listeners and their common parameters.
type interfaceListenersConfig struct {
// List is the ID-to-configuration mapping of network interface listeners.
List map[bindtodevice.ID]*interfaceListener `yaml:"list"`
// ChannelBufferSize is the size of the buffers of the channels used to
// dispatch TCP connections and UDP sessions.
ChannelBufferSize int `yaml:"channel_buffer_size"`
}
// toInternal converts c to a bindtodevice.Manager. c is assumed to be valid.
func (c *interfaceListenersConfig) toInternal(
errColl agd.ErrorCollector,
ctrlConf *bindtodevice.ControlConfig,
) (m *bindtodevice.Manager, err error) {
if c == nil {
return nil, nil
}
m = bindtodevice.NewManager(&bindtodevice.ManagerConfig{
InterfaceStorage: bindtodevice.DefaultInterfaceStorage{},
ErrColl: errColl,
ChannelBufferSize: c.ChannelBufferSize,
})
err = agdmaps.OrderedRangeError(
c.List,
func(id bindtodevice.ID, l *interfaceListener) (addErr error) {
return errors.Annotate(m.Add(id, l.Interface, l.Port, ctrlConf), "adding listener %q: %w", id)
},
)
if err != nil {
return nil, err
}
return m, nil
}
// validate returns an error if the network interface listeners configuration is
// invalid.
func (c *interfaceListenersConfig) validate() (err error) {
switch {
case c == nil:
// This configuration is optional.
//
// TODO(a.garipov): Consider making required or not relying on nil
// values.
return nil
case c.ChannelBufferSize <= 0:
return newMustBePositiveError("channel_buffer_size", c.ChannelBufferSize)
case len(c.List) == 0:
return errors.Error("no list")
default:
// Go on.
}
err = agdmaps.OrderedRangeError(
c.List,
func(id bindtodevice.ID, l *interfaceListener) (lsnrErr error) {
return errors.Annotate(l.validate(), "interface %q: %w", id)
},
)
return err
}
// interfaceListener contains configuration for a single network interface
// listener.
type interfaceListener struct {
// Interface is the name of the network interface in the system.
Interface string `yaml:"interface"`
// Port is the port number on which to listen for incoming connections.
Port uint16 `yaml:"port"`
}
// validate returns an error if the interface listener configuration is invalid.
func (l *interfaceListener) validate() (err error) {
switch {
case l == nil:
return errNilConfig
case l.Port == 0:
return errors.Error("port must not be zero")
case l.Interface == "":
return errors.Error("no interface")
default:
return nil
}
}

51
internal/cmd/network.go Normal file
View File

@ -0,0 +1,51 @@
package cmd
import (
"github.com/AdguardTeam/AdGuardDNS/internal/bindtodevice"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/netext"
)
// network defines the network settings.
//
// TODO(a.garipov): Use [datasize.ByteSize] for sizes.
type network struct {
// SndBufSize defines the size of socket send buffer in bytes. Default is
// zero (uses system settings).
SndBufSize int `yaml:"so_sndbuf"`
// RcvBufSize defines the size of socket receive buffer in bytes. Default
// is zero (uses system settings).
RcvBufSize int `yaml:"so_rcvbuf"`
}
// validate returns an error if the network configuration is invalid.
func (n *network) validate() (err error) {
if n == nil {
return errNilConfig
}
if n.SndBufSize < 0 {
return newMustBeNonNegativeError("so_sndbuf", n.SndBufSize)
}
if n.RcvBufSize < 0 {
return newMustBeNonNegativeError("so_rcvbuf", n.RcvBufSize)
}
return nil
}
// toInternal converts n to the bindtodevice control configuration and network
// extension control configuration.
func (n *network) toInternal() (bc *bindtodevice.ControlConfig, nc *netext.ControlConfig) {
bc = &bindtodevice.ControlConfig{
SndBufSize: n.SndBufSize,
RcvBufSize: n.RcvBufSize,
}
nc = &netext.ControlConfig{
SndBufSize: n.SndBufSize,
RcvBufSize: n.RcvBufSize,
}
return bc, nc
}

View File

@ -6,8 +6,11 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdnet"
"github.com/AdguardTeam/AdGuardDNS/internal/connlimiter"
"github.com/AdguardTeam/AdGuardDNS/internal/consul"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/ratelimit"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/c2h5oh/datasize"
)
@ -19,6 +22,10 @@ type rateLimitConfig struct {
// AllowList is the allowlist of clients.
Allowlist *allowListConfig `yaml:"allowlist"`
// ConnectionLimit is the configuration for the limits on stream
// connections.
ConnectionLimit *connLimitConfig `yaml:"connection_limit"`
// Rate limit options for IPv4 addresses.
IPv4 *rateLimitOptions `yaml:"ipv4"`
@ -97,12 +104,18 @@ func (c *rateLimitConfig) toInternal(al ratelimit.Allowlist) (conf *ratelimit.Ba
// validate returns an error if the safe rate limiting configuration is invalid.
func (c *rateLimitConfig) validate() (err error) {
if c == nil {
switch {
case c == nil:
return errNilConfig
} else if c.Allowlist == nil {
case c.Allowlist == nil:
return fmt.Errorf("allowlist: %w", errNilConfig)
}
err = c.ConnectionLimit.validate()
if err != nil {
return fmt.Errorf("connection_limit: %w", err)
}
err = c.IPv4.validate()
if err != nil {
return fmt.Errorf("ipv4: %w", err)
@ -129,16 +142,16 @@ func setupRateLimiter(
consulAllowlist *url.URL,
sigHdlr signalHandler,
errColl agd.ErrorCollector,
) (rateLimiter *ratelimit.BackOff, err error) {
) (rateLimiter *ratelimit.BackOff, connLimiter *connlimiter.Limiter, err error) {
allowSubnets, err := agdnet.ParseSubnets(conf.Allowlist.List...)
if err != nil {
return nil, fmt.Errorf("parsing allowlist subnets: %w", err)
return nil, nil, fmt.Errorf("parsing allowlist subnets: %w", err)
}
allowlist := ratelimit.NewDynamicAllowlist(allowSubnets, nil)
refresher, err := consul.NewAllowlistRefresher(allowlist, consulAllowlist)
if err != nil {
return nil, fmt.Errorf("creating allowlist refresher: %w", err)
return nil, nil, fmt.Errorf("creating allowlist refresher: %w", err)
}
refr := agd.NewRefreshWorker(&agd.RefreshWorkerConfig{
@ -152,10 +165,67 @@ func setupRateLimiter(
})
err = refr.Start()
if err != nil {
return nil, fmt.Errorf("starting allowlist refresher: %w", err)
return nil, nil, fmt.Errorf("starting allowlist refresher: %w", err)
}
sigHdlr.add(refr)
return ratelimit.NewBackOff(conf.toInternal(allowlist)), nil
return ratelimit.NewBackOff(conf.toInternal(allowlist)), conf.ConnectionLimit.toInternal(), nil
}
// connLimitConfig is the configuration structure for the stream-connection
// limiter.
type connLimitConfig struct {
// Stop is the point at which the limiter stops accepting new connections.
// Once the number of active connections reaches this limit, new connections
// wait for the number to decrease below Resume.
//
// Stop must be greater than zero and greater than or equal to Resume.
Stop uint64 `yaml:"stop"`
// Resume is the point at which the limiter starts accepting new connections
// again.
//
// Resume must be greater than zero and less than or equal to Stop.
Resume uint64 `yaml:"resume"`
// Enabled, if true, enables stream-connection limiting.
Enabled bool `yaml:"enabled"`
}
// toInternal converts c to the connection limiter to use. c is assumed to be
// valid.
func (c *connLimitConfig) toInternal() (l *connlimiter.Limiter) {
if !c.Enabled {
return nil
}
l, err := connlimiter.New(&connlimiter.Config{
Stop: c.Stop,
Resume: c.Resume,
})
if err != nil {
panic(err)
}
metrics.ConnLimiterLimits.WithLabelValues("stop").Set(float64(c.Stop))
metrics.ConnLimiterLimits.WithLabelValues("resume").Set(float64(c.Resume))
return l
}
// validate returns an error if the connection limit configuration is invalid.
func (c *connLimitConfig) validate() (err error) {
switch {
case c == nil:
return errNilConfig
case !c.Enabled:
return nil
case c.Stop == 0:
return newMustBePositiveError("stop", c.Stop)
case c.Resume > c.Stop:
return errors.Error("resume: must be less than or equal to stop")
default:
return nil
}
}

View File

@ -7,8 +7,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
"github.com/AdguardTeam/AdGuardDNS/internal/agdnet"
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/hashstorage"
"github.com/AdguardTeam/AdGuardDNS/internal/filter/hashprefix"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil"
@ -45,13 +44,13 @@ func (c *safeBrowsingConfig) toInternal(
resolver agdnet.Resolver,
id agd.FilterListID,
cacheDir string,
) (fltConf *filter.HashPrefixConfig, err error) {
hashes, err := hashstorage.New("")
) (fltConf *hashprefix.FilterConfig, err error) {
hashes, err := hashprefix.NewStorage("")
if err != nil {
return nil, err
}
return &filter.HashPrefixConfig{
return &hashprefix.FilterConfig{
Hashes: hashes,
URL: netutil.CloneURL(&c.URL.URL),
ErrColl: errColl,
@ -95,13 +94,13 @@ func setupHashPrefixFilter(
cachePath string,
sigHdlr signalHandler,
errColl agd.ErrorCollector,
) (strg *hashstorage.Storage, flt *filter.HashPrefix, err error) {
) (strg *hashprefix.Storage, flt *hashprefix.Filter, err error) {
fltConf, err := conf.toInternal(errColl, resolver, id, cachePath)
if err != nil {
return nil, nil, fmt.Errorf("configuring hash prefix filter %s: %w", id, err)
}
flt, err = filter.NewHashPrefix(fltConf)
flt, err = hashprefix.NewFilter(fltConf)
if err != nil {
return nil, nil, fmt.Errorf("creating hash prefix filter %s: %w", id, err)
}

View File

@ -5,6 +5,8 @@ import (
"net/netip"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/bindtodevice"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/netext"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/stringutil"
@ -14,10 +16,18 @@ import (
// toInternal returns the configuration of DNS servers for a single server
// group. srvs is assumed to be valid.
func (srvs servers) toInternal(tlsConfig *agd.TLS) (dnsSrvs []*agd.Server, err error) {
func (srvs servers) toInternal(
tlsConfig *agd.TLS,
btdMgr *bindtodevice.Manager,
) (dnsSrvs []*agd.Server, err error) {
dnsSrvs = make([]*agd.Server, 0, len(srvs))
for _, srv := range srvs {
bindData := srv.bindData()
var bindData []*agd.ServerBindData
bindData, err = srv.bindData(btdMgr)
if err != nil {
return nil, fmt.Errorf("server %q: %w", srv.Name, err)
}
name := agd.ServerName(srv.Name)
switch p := srv.Protocol; p {
case srvProtoDNS:
@ -158,27 +168,56 @@ type server struct {
// Protocol is the protocol of the server.
Protocol serverProto `yaml:"protocol"`
// BindAddresses are addresses this server binds to.
// BindAddresses are addresses this server binds to. If BindAddresses is
// set, BindInterfaces must not be set.
BindAddresses []netip.AddrPort `yaml:"bind_addresses"`
// BindInterfaces are network interface data for this server to bind to. If
// BindInterfaces is set, BindAddresses must not be set.
BindInterfaces []*serverBindInterface `yaml:"bind_interfaces"`
// LinkedIPEnabled shows if the linked IP addresses should be used to detect
// profiles on this server.
LinkedIPEnabled bool `yaml:"linked_ip_enabled"`
}
// bindData returns the socket binding data for this server.
func (s *server) bindData() (bindData []*agd.ServerBindData) {
addrs := s.BindAddresses
bindData = make([]*agd.ServerBindData, 0, len(addrs))
for _, addr := range addrs {
func (s *server) bindData(
btdMgr *bindtodevice.Manager,
) (bindData []*agd.ServerBindData, err error) {
if addrs := s.BindAddresses; len(addrs) > 0 {
bindData = make([]*agd.ServerBindData, 0, len(addrs))
for _, addr := range addrs {
bindData = append(bindData, &agd.ServerBindData{
AddrPort: addr,
})
}
return bindData, nil
}
if btdMgr == nil {
err = errors.Error("bind_interfaces are only supported when interface_listeners are set")
return nil, err
}
ifaces := s.BindInterfaces
bindData = make([]*agd.ServerBindData, 0, len(ifaces))
for i, iface := range s.BindInterfaces {
var lc netext.ListenConfig
lc, err = btdMgr.ListenConfig(iface.ID, iface.Subnet)
if err != nil {
return nil, fmt.Errorf("bind_interface at index %d: %w", i, err)
}
bindData = append(bindData, &agd.ServerBindData{
AddrPort: addr,
ListenConfig: lc,
Address: string(iface.ID),
})
}
// TODO(a.garipov): Support bind_interfaces.
return bindData
return bindData, nil
}
// validate returns an error if the configuration is invalid.
@ -188,13 +227,12 @@ func (s *server) validate() (err error) {
return errNilConfig
case s.Name == "":
return errors.Error("no name")
case len(s.BindAddresses) == 0:
return errors.Error("no bind_addresses")
}
err = validateAddrs(s.BindAddresses)
err = s.validateBindData()
if err != nil {
return fmt.Errorf("bind_addresses: %w", err)
// Don't wrap the error, because it's informative enough as is.
return err
}
err = s.Protocol.validate()
@ -209,3 +247,62 @@ func (s *server) validate() (err error) {
return nil
}
// validateBindData returns an error if the server's binding data aren't valid.
func (s *server) validateBindData() (err error) {
bindAddrsSet, bindIfacesSet := len(s.BindAddresses) > 0, len(s.BindInterfaces) > 0
if bindAddrsSet {
if bindIfacesSet {
return errors.Error("bind_addresses and bind_interfaces cannot both be set")
}
err = validateAddrs(s.BindAddresses)
if err != nil {
return fmt.Errorf("bind_addresses: %w", err)
}
return nil
}
if !bindIfacesSet {
return errors.Error("neither bind_addresses nor bind_interfaces is set")
}
if s.Protocol != srvProtoDNS {
return fmt.Errorf(
"bind_interfaces: only supported for protocol %q, got %q",
srvProtoDNS,
s.Protocol,
)
}
for i, bindIface := range s.BindInterfaces {
err = bindIface.validate()
if err != nil {
return fmt.Errorf("bind_interfaces: at index %d: %w", i, err)
}
}
return nil
}
// serverBindInterface contains the data for a network interface binding.
type serverBindInterface struct {
ID bindtodevice.ID `yaml:"id"`
Subnet netip.Prefix `yaml:"subnet"`
}
// validate returns an error if the network interface binding configuration is
// invalid.
func (c *serverBindInterface) validate() (err error) {
switch {
case c == nil:
return errNilConfig
case c.ID == "":
return errors.Error("no id")
case !c.Subnet.IsValid():
return errors.Error("bad subnet")
default:
return nil
}
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/bindtodevice"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/stringutil"
@ -19,6 +20,7 @@ type serverGroups []*serverGroup
// service. srvGrps is assumed to be valid.
func (srvGrps serverGroups) toInternal(
messages *dnsmsg.Constructor,
btdMgr *bindtodevice.Manager,
fltGrps map[agd.FilteringGroupID]*agd.FilteringGroup,
) (svcSrvGrps []*agd.ServerGroup, err error) {
svcSrvGrps = make([]*agd.ServerGroup, len(srvGrps))
@ -42,7 +44,7 @@ func (srvGrps serverGroups) toInternal(
FilteringGroup: fltGrpID,
}
svcSrvGrps[i].Servers, err = g.Servers.toInternal(tlsConf)
svcSrvGrps[i].Servers, err = g.Servers.toInternal(tlsConf, btdMgr)
if err != nil {
return nil, fmt.Errorf("server group %q: %w", g.Name, err)
}

View File

@ -6,9 +6,11 @@ import (
"net/netip"
"net/url"
"strings"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/forward"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/prometheus"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/timeutil"
)
@ -33,18 +35,32 @@ type upstreamConfig struct {
}
// toInternal converts c to the data storage configuration for the DNS server.
func (c *upstreamConfig) toInternal() (conf *agd.Upstream, err error) {
net, addrPort, err := splitUpstreamURL(c.Server)
func (c *upstreamConfig) toInternal() (fwdConf *forward.HandlerConfig, err error) {
network, addrPort, err := splitUpstreamURL(c.Server)
if err != nil {
return nil, err
}
return &agd.Upstream{
Server: addrPort,
Network: net,
FallbackServers: c.FallbackServers,
Timeout: c.Timeout.Duration,
}, nil
fallbacks := c.FallbackServers
metricsListener := prometheus.NewForwardMetricsListener(len(fallbacks) + 1)
var hcInit time.Duration
if c.Healthcheck.Enabled {
hcInit = c.Healthcheck.Timeout.Duration
}
fwdConf = &forward.HandlerConfig{
Address: addrPort,
Network: network,
MetricsListener: metricsListener,
HealthcheckDomainTmpl: c.Healthcheck.DomainTmpl,
FallbackAddresses: c.FallbackServers,
Timeout: c.Timeout.Duration,
HealthcheckBackoffDuration: c.Healthcheck.BackoffDuration.Duration,
HealthcheckInitDuration: hcInit,
}
return fwdConf, nil
}
// validate returns an error if the upstream configuration is invalid.

View File

@ -13,6 +13,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
"github.com/AdguardTeam/AdGuardDNS/internal/websvc"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil"
)
@ -413,8 +414,8 @@ func (f *staticFile) toInternal() (file *websvc.StaticFile, err error) {
// Check Content-Type here as opposed to in validate, because we need
// all keys to be canonicalized first.
if file.Headers.Get(agdhttp.HdrNameContentType) == "" {
return nil, errors.Error("content: " + agdhttp.HdrNameContentType + " header is required")
if file.Headers.Get(httphdr.ContentType) == "" {
return nil, errors.Error("content: " + httphdr.ContentType + " header is required")
}
return file, nil

View File

@ -0,0 +1,51 @@
package connlimiter
import (
"net"
"sync/atomic"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/AdGuardDNS/internal/optlog"
"github.com/AdguardTeam/golibs/errors"
)
// limitConn is a wrapper for a stream connection that decreases the counter
// value on close.
//
// See https://pkg.go.dev/golang.org/x/net/netutil#LimitListener.
type limitConn struct {
net.Conn
decrement func()
start time.Time
serverInfo dnsserver.ServerInfo
isClosed atomic.Bool
}
// Close closes the underlying connection and decrements the counter.
func (c *limitConn) Close() (err error) {
defer func() { err = errors.Annotate(err, "limit conn: %w") }()
if !c.isClosed.CompareAndSwap(false, true) {
return net.ErrClosed
}
// Close the connection immediately and wait for the counter decrement and
// metrics later.
err = c.Conn.Close()
connLife := time.Since(c.start).Seconds()
name := c.serverInfo.Name
optlog.Debug3("connlimiter: %s: closed conn from %s after %fs", name, c.RemoteAddr(), connLife)
metrics.StreamConnLifeDuration.WithLabelValues(
name,
c.serverInfo.Proto.String(),
c.serverInfo.Addr,
).Observe(connLife)
c.decrement()
return err
}

View File

@ -0,0 +1,35 @@
package connlimiter
// counter is the simultaneous stream-connection counter. It stops accepting
// new connections once it reaches stop and resumes when the number of active
// connections goes back to resume.
//
// Note that current is the number of both active stream-connections as well as
// goroutines that are currently in the process of accepting a new connection
// but haven't accepted one yet.
type counter struct {
current uint64
stop uint64
resume uint64
isAccepting bool
}
// increment tries to add the connection to the current active connection count.
// If the counter does not accept new connections, shouldAccept is false.
func (c *counter) increment() (shouldAccept bool) {
if !c.isAccepting {
return false
}
c.current++
c.isAccepting = c.current < c.stop
return true
}
// decrement decreases the number of current active connections.
func (c *counter) decrement() {
c.current--
c.isAccepting = c.isAccepting || c.current <= c.resume
}

View File

@ -0,0 +1,42 @@
package connlimiter
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCounter(t *testing.T) {
t.Run("same", func(t *testing.T) {
c := &counter{
current: 0,
stop: 1,
resume: 1,
isAccepting: true,
}
assert.True(t, c.increment())
assert.False(t, c.increment())
c.decrement()
assert.True(t, c.increment())
assert.False(t, c.increment())
})
t.Run("more", func(t *testing.T) {
c := &counter{
current: 0,
stop: 2,
resume: 1,
isAccepting: true,
}
assert.True(t, c.increment())
assert.True(t, c.increment())
assert.False(t, c.increment())
c.decrement()
assert.True(t, c.increment())
assert.False(t, c.increment())
})
}

View File

@ -0,0 +1,73 @@
package connlimiter
import (
"fmt"
"net"
"sync"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
)
// Config is the configuration structure for the stream-connection limiter.
type Config struct {
// Stop is the point at which the limiter stops accepting new connections.
// Once the number of active connections reaches this limit, new connections
// wait for the number to decrease to or below Resume.
//
// Stop must be greater than zero and greater than or equal to Resume.
Stop uint64
// Resume is the point at which the limiter starts accepting new connections
// again.
//
// Resume must be greater than zero and less than or equal to Stop.
Resume uint64
}
// Limiter is the stream-connection limiter.
type Limiter struct {
// counterCond is the shared condition variable that protects counter.
counterCond *sync.Cond
// counter is the shared counter of active stream-connections.
counter *counter
}
// New returns a new *Limiter.
func New(c *Config) (l *Limiter, err error) {
if c == nil || c.Stop == 0 || c.Resume > c.Stop {
return nil, fmt.Errorf("bad limiter config: %+v", c)
}
return &Limiter{
counterCond: sync.NewCond(&sync.Mutex{}),
counter: &counter{
current: 0,
stop: c.Stop,
resume: c.Resume,
isAccepting: true,
},
}, nil
}
// Limit wraps lsnr to control the number of active connections. srvInfo is
// used for logging and metrics.
func (l *Limiter) Limit(lsnr net.Listener, srvInfo dnsserver.ServerInfo) (limited net.Listener) {
name, addr := srvInfo.Name, srvInfo.Addr
proto := srvInfo.Proto.String()
return &limitListener{
Listener: lsnr,
counterCond: l.counterCond,
counter: l.counter,
serverInfo: srvInfo,
activeGauge: metrics.ConnLimiterActiveStreamConns.WithLabelValues(name, proto, addr),
waitingHist: metrics.StreamConnWaitDuration.WithLabelValues(name, proto, addr),
isClosed: false,
}
}

View File

@ -0,0 +1,122 @@
package connlimiter_test
import (
"net"
"testing"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/connlimiter"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
testutil.DiscardLogOutput(m)
}
// testTimeout is the common timeout for tests.
const testTimeout = 1 * time.Second
// testServerInfo is the common server information for tests.
var testServerInfo = dnsserver.ServerInfo{
Name: "test_server",
Addr: "127.0.0.1:0",
Proto: agd.ProtoDoT,
}
func TestLimiter(t *testing.T) {
l, err := connlimiter.New(&connlimiter.Config{
Stop: 1,
Resume: 1,
})
require.NoError(t, err)
conn := &agdtest.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") },
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") },
}
lsnr := &agdtest.Listener{
OnAccept: func() (c net.Conn, err error) { return conn, nil },
OnAddr: func() (addr net.Addr) {
return &net.TCPAddr{
IP: netutil.IPv4Localhost().AsSlice(),
Port: 853,
}
},
OnClose: func() (err error) { return nil },
}
limited := l.Limit(lsnr, testServerInfo)
// Accept one connection.
gotConn, err := limited.Accept()
require.NoError(t, err)
// Try accepting another connection. This should block until gotConn is
// closed.
otherStarted, otherListened := make(chan struct{}, 1), make(chan struct{}, 1)
go func() {
pt := &testutil.PanicT{}
otherStarted <- struct{}{}
otherConn, otherErr := limited.Accept()
require.NoError(pt, otherErr)
otherListened <- struct{}{}
require.NoError(pt, otherConn.Close())
}()
// Wait for the other goroutine to start.
testutil.RequireReceive(t, otherStarted, testTimeout)
// Assert that the other connection hasn't been accepted.
var otherAccepted bool
select {
case <-otherListened:
otherAccepted = true
default:
otherAccepted = false
}
assert.False(t, otherAccepted)
require.NoError(t, gotConn.Close())
// Check that double close causes an error.
assert.ErrorIs(t, gotConn.Close(), net.ErrClosed)
testutil.RequireReceive(t, otherListened, testTimeout)
err = limited.Close()
require.NoError(t, err)
// Check that double close causes an error.
assert.ErrorIs(t, limited.Close(), net.ErrClosed)
}
func TestLimiter_badConf(t *testing.T) {
l, err := connlimiter.New(&connlimiter.Config{
Stop: 1,
Resume: 2,
})
assert.Nil(t, l)
assert.Error(t, err)
}

View File

@ -0,0 +1,54 @@
package connlimiter
import (
"context"
"net"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/netext"
)
// type check
var _ netext.ListenConfig = (*ListenConfig)(nil)
// ListenConfig is a [netext.ListenConfig] that uses a [*Limiter] to limit the
// number of active stream-connections.
type ListenConfig struct {
listenConfig netext.ListenConfig
limiter *Limiter
}
// NewListenConfig returns a new netext.ListenConfig that uses l to limit the
// number of active stream-connections.
func NewListenConfig(c netext.ListenConfig, l *Limiter) (limited *ListenConfig) {
return &ListenConfig{
listenConfig: c,
limiter: l,
}
}
// ListenPacket implements the [netext.ListenConfig] interface for
// *ListenConfig.
func (c *ListenConfig) ListenPacket(
ctx context.Context,
network string,
address string,
) (conn net.PacketConn, err error) {
return c.listenConfig.ListenPacket(ctx, network, address)
}
// Listen implements the [netext.ListenConfig] interface for *ListenConfig.
// Listen returns a net.Listener wrapped by c's limiter. ctx must contain a
// [dnsserver.ServerInfo].
func (c *ListenConfig) Listen(
ctx context.Context,
network string,
address string,
) (l net.Listener, err error) {
l, err = c.listenConfig.Listen(ctx, network, address)
if err != nil {
return nil, err
}
return c.limiter.Limit(l, dnsserver.MustServerInfoFromContext(ctx)), nil
}

View File

@ -0,0 +1,75 @@
package connlimiter_test
import (
"context"
"net"
"testing"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/connlimiter"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestListenConfig(t *testing.T) {
pc := &agdtest.PacketConn{
OnClose: func() (err error) { panic("not implemented") },
OnLocalAddr: func() (laddr net.Addr) { panic("not implemented") },
OnReadFrom: func(b []byte) (n int, addr net.Addr, err error) { panic("not implemented") },
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") },
OnWriteTo: func(b []byte, addr net.Addr) (n int, err error) { panic("not implemented") },
}
lsnr := &agdtest.Listener{
OnAccept: func() (c net.Conn, err error) { panic("not implemented") },
OnAddr: func() (addr net.Addr) { panic("not implemented") },
OnClose: func() (err error) { return nil },
}
c := &agdtest.ListenConfig{
OnListen: func(
ctx context.Context,
network string,
address string,
) (l net.Listener, err error) {
return lsnr, nil
},
OnListenPacket: func(
ctx context.Context,
network string,
address string,
) (conn net.PacketConn, err error) {
return pc, nil
},
}
l, err := connlimiter.New(&connlimiter.Config{
Stop: 1,
Resume: 1,
})
require.NoError(t, err)
limited := connlimiter.NewListenConfig(c, l)
ctx := dnsserver.ContextWithServerInfo(context.Background(), testServerInfo)
gotLsnr, err := limited.Listen(ctx, "", "")
require.NoError(t, err)
// TODO(a.garipov): Add more testing logic here if [Limiter] becomes
// unexported.
assert.NotEqual(t, lsnr, gotLsnr)
err = gotLsnr.Close()
require.NoError(t, err)
gotPC, err := limited.ListenPacket(ctx, "", "")
require.NoError(t, err)
// TODO(a.garipov): Add more testing logic here if [Limiter] becomes
// unexported.
assert.Equal(t, pc, gotPC)
}

View File

@ -0,0 +1,139 @@
// Package connlimiter describes a limiter of the number of active
// stream-connections.
package connlimiter
import (
"net"
"sync"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/AdGuardDNS/internal/optlog"
"github.com/AdguardTeam/golibs/errors"
"github.com/prometheus/client_golang/prometheus"
)
// limitListener is a wrapper that uses a counter to limit the number of active
// stream-connections.
//
// See https://pkg.go.dev/golang.org/x/net/netutil#LimitListener.
type limitListener struct {
net.Listener
// counterCond is the condition variable that protects counter and isClosed
// through its locker, as well as signals when connections can be accepted
// again or when the listener has been closed.
counterCond *sync.Cond
// counter is the shared counter for all listeners.
counter *counter
// activeGauge is the metrics gauge of currently active stream-connections.
activeGauge prometheus.Gauge
// waitingHist is the metrics histogram of how much a connection spends
// waiting for an accept.
waitingHist prometheus.Observer
// serverInfo is used for logging and metrics in both the listener itself
// and in its conns.
serverInfo dnsserver.ServerInfo
// isClosed shows whether this listener has been closed.
isClosed bool
}
// Accept returns a new connection if the counter allows it. Otherwise, it
// waits until the counter allows it or the listener is closed.
func (l *limitListener) Accept() (conn net.Conn, err error) {
defer func() { err = errors.Annotate(err, "limit listener: %w") }()
waitStart := time.Now()
isClosed := l.increment()
if isClosed {
return nil, net.ErrClosed
}
l.waitingHist.Observe(time.Since(waitStart).Seconds())
l.activeGauge.Inc()
conn, err = l.Listener.Accept()
if err != nil {
l.decrement()
return nil, err
}
return &limitConn{
Conn: conn,
decrement: l.decrement,
start: time.Now(),
serverInfo: l.serverInfo,
}, nil
}
// increment waits until it can increase the number of active connections
// in the counter. If the listener is closed while waiting, increment exits and
// returns true
func (l *limitListener) increment() (isClosed bool) {
l.counterCond.L.Lock()
defer l.counterCond.L.Unlock()
// Make sure to check both that the counter allows this connection and that
// the listener hasn't been closed. Only log about waiting for an increment
// when such waiting actually took place.
waited := false
for !l.counter.increment() && !l.isClosed {
if !waited {
optlog.Debug1("connlimiter: server %s: accept waiting", l.serverInfo.Name)
waited = true
}
l.counterCond.Wait()
}
if waited {
optlog.Debug1("connlimiter: server %s: accept stopped waiting", l.serverInfo.Name)
}
return l.isClosed
}
// decrement decreases the number of active connections in the counter and
// broadcasts the change.
func (l *limitListener) decrement() {
l.counterCond.L.Lock()
defer l.counterCond.L.Unlock()
l.activeGauge.Dec()
l.counter.decrement()
l.counterCond.Signal()
}
// Close closes the underlying listener and signals to all goroutines waiting
// for an accept that the listener is closed now.
func (l *limitListener) Close() (err error) {
defer func() { err = errors.Annotate(err, "limit listener: %w") }()
l.counterCond.L.Lock()
defer l.counterCond.L.Unlock()
if l.isClosed {
return net.ErrClosed
}
// Close the listener immediately; change the boolean and broadcast the
// change later.
err = l.Listener.Close()
l.isClosed = true
l.counterCond.Broadcast()
return err
}

View File

@ -17,6 +17,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/miekg/dns"
@ -303,8 +304,8 @@ func (cc *Consul) serveCheckTest(ctx context.Context, w http.ResponseWriter, r *
}
h := w.Header()
h.Set(agdhttp.HdrNameContentType, agdhttp.HdrValApplicationJSON)
h.Set(agdhttp.HdrNameAccessControlAllowOrigin, agdhttp.HdrValWildcard)
h.Set(httphdr.ContentType, agdhttp.HdrValApplicationJSON)
h.Set(httphdr.AccessControlAllowOrigin, agdhttp.HdrValWildcard)
err = json.NewEncoder(w).Encode(inf)
if err != nil {

View File

@ -8,6 +8,7 @@ import (
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
@ -15,6 +16,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsdb"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/dnsservertest"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -39,9 +41,9 @@ func TestBolt_ServeHTTP(t *testing.T) {
const dname = "some-domain.name"
successHdr := http.Header{
agdhttp.HdrNameContentType: []string{agdhttp.HdrValTextCSV},
agdhttp.HdrNameTrailer: []string{agdhttp.HdrNameXError},
agdhttp.HdrNameContentEncoding: []string{"gzip"},
httphdr.ContentType: []string{agdhttp.HdrValTextCSV},
httphdr.Trailer: []string{httphdr.XError},
httphdr.ContentEncoding: []string{"gzip"},
}
newMsg := func(rcode int, name string, qtype uint16) (m *dns.Msg) {
@ -104,7 +106,10 @@ func TestBolt_ServeHTTP(t *testing.T) {
for _, m := range msgs {
ctx := context.Background()
db.Record(ctx, m, &agd.RequestInfo{
Host: m.Question[0].Name,
// Emulate the logic from init middleware.
//
// See [dnssvc.initMw.newRequestInfo].
Host: strings.TrimSuffix(m.Question[0].Name, "."),
})
err := db.Refresh(context.Background())
@ -117,7 +122,7 @@ func TestBolt_ServeHTTP(t *testing.T) {
(&url.URL{Scheme: "http", Host: "example.com"}).String(),
nil,
)
r.Header.Add(agdhttp.HdrNameAcceptEncoding, "gzip")
r.Header.Add(httphdr.AcceptEncoding, "gzip")
for _, tc := range testCases {
db := newTmpBolt(t)

View File

@ -15,6 +15,7 @@ import (
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/httphdr"
"go.etcd.io/bbolt"
)
@ -38,7 +39,7 @@ func (db *Bolt) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
h := w.Header()
h.Add(agdhttp.HdrNameContentType, agdhttp.HdrValTextCSV)
h.Add(httphdr.ContentType, agdhttp.HdrValTextCSV)
if dbPath == "" {
// No data.
@ -47,10 +48,10 @@ func (db *Bolt) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
h.Set(agdhttp.HdrNameTrailer, agdhttp.HdrNameXError)
h.Set(httphdr.Trailer, httphdr.XError)
defer func() {
if err != nil {
h.Set(agdhttp.HdrNameXError, err.Error())
h.Set(httphdr.XError, err.Error())
agd.Collectf(ctx, db.errColl, "dnsdb: http handler error: %w", err)
}
}()
@ -59,8 +60,8 @@ func (db *Bolt) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var rw io.Writer = w
// TODO(a.garipov): Consider parsing the quality value.
if strings.Contains(r.Header.Get(agdhttp.HdrNameAcceptEncoding), "gzip") {
h.Set(agdhttp.HdrNameContentEncoding, "gzip")
if strings.Contains(r.Header.Get(httphdr.AcceptEncoding), "gzip") {
h.Set(httphdr.ContentEncoding, "gzip")
gw := gzip.NewWriter(w)
defer func() { err = errors.WithDeferred(err, gw.Close()) }()

View File

@ -195,12 +195,12 @@ func (c *Constructor) newHdr(req *dns.Msg, rrType RRType) (hdr dns.RR_Header) {
}
// newHdrWithClass returns a new resource record header with specified class.
func (c *Constructor) newHdrWithClass(req *dns.Msg, rrType RRType, class dns.Class) (hdr dns.RR_Header) {
func (c *Constructor) newHdrWithClass(req *dns.Msg, rrType RRType, cl dns.Class) (h dns.RR_Header) {
return dns.RR_Header{
Name: req.Question[0].Name,
Rrtype: rrType,
Ttl: uint32(c.fltRespTTL.Seconds()),
Class: uint16(class),
Class: uint16(cl),
}
}

View File

@ -23,14 +23,8 @@ func TestMiddleware_Wrap(t *testing.T) {
aReq := dnsservertest.NewReq(reqHostname, dns.TypeA, dns.ClassINET)
cnameReq := dnsservertest.NewReq(reqHostname, dns.TypeCNAME, dns.ClassINET)
cnameAns := dnsservertest.RRSection{
RRs: []dns.RR{dnsservertest.NewCNAME(reqHostname, 3600, reqCname)},
Sec: dnsservertest.SectionAnswer,
}
soaNs := dnsservertest.RRSection{
RRs: []dns.RR{dnsservertest.NewSOA(reqHostname, 3600, reqNs1, reqNs2)},
Sec: dnsservertest.SectionNs,
}
cnameAns := dnsservertest.SectionAnswer{dnsservertest.NewCNAME(reqHostname, 3600, reqCname)}
soaNs := dnsservertest.SectionNs{dnsservertest.NewSOA(reqHostname, 3600, reqNs1, reqNs2)}
const N = 5
testCases := []struct {
@ -40,8 +34,8 @@ func TestMiddleware_Wrap(t *testing.T) {
wantNumReq int
}{{
req: aReq,
resp: dnsservertest.NewResp(dns.RcodeSuccess, aReq, dnsservertest.RRSection{
RRs: []dns.RR{dnsservertest.NewA(reqHostname, 3600, net.IP{1, 2, 3, 4})},
resp: dnsservertest.NewResp(dns.RcodeSuccess, aReq, dnsservertest.SectionAnswer{
dnsservertest.NewA(reqHostname, 3600, net.IP{1, 2, 3, 4}),
}),
name: "simple_a",
wantNumReq: 1,
@ -67,9 +61,8 @@ func TestMiddleware_Wrap(t *testing.T) {
wantNumReq: N,
}, {
req: aReq,
resp: dnsservertest.NewResp(dns.RcodeNameError, aReq, dnsservertest.RRSection{
RRs: []dns.RR{dnsservertest.NewNS(reqHostname, 3600, reqNs1)},
Sec: dnsservertest.SectionNs,
resp: dnsservertest.NewResp(dns.RcodeNameError, aReq, dnsservertest.SectionNs{
dnsservertest.NewNS(reqHostname, 3600, reqNs1),
}),
name: "non_authoritative_nxdomain",
// TODO(ameshkov): Consider https://datatracker.ietf.org/doc/html/rfc2308#section-3.
@ -86,15 +79,15 @@ func TestMiddleware_Wrap(t *testing.T) {
wantNumReq: 1,
}, {
req: cnameReq,
resp: dnsservertest.NewResp(dns.RcodeSuccess, cnameReq, dnsservertest.RRSection{
RRs: []dns.RR{dnsservertest.NewCNAME(reqHostname, 3600, reqCname)},
resp: dnsservertest.NewResp(dns.RcodeSuccess, cnameReq, dnsservertest.SectionAnswer{
dnsservertest.NewCNAME(reqHostname, 3600, reqCname),
}),
name: "simple_cname_ans",
wantNumReq: 1,
}, {
req: aReq,
resp: dnsservertest.NewResp(dns.RcodeSuccess, aReq, dnsservertest.RRSection{
RRs: []dns.RR{dnsservertest.NewA(reqHostname, 0, net.IP{1, 2, 3, 4})},
resp: dnsservertest.NewResp(dns.RcodeSuccess, aReq, dnsservertest.SectionAnswer{
dnsservertest.NewA(reqHostname, 0, net.IP{1, 2, 3, 4}),
}),
name: "expired_one",
wantNumReq: N,

View File

@ -2,14 +2,19 @@ package dnsservertest
import (
"context"
"net"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
"github.com/miekg/dns"
)
// CreateTestHandler creates a [dnsserver.Handler] with the specified parameters.
// AnswerTTL is the default TTL of the test handler's answers.
const AnswerTTL time.Duration = 100 * time.Second
// CreateTestHandler creates a [dnsserver.Handler] with the specified
// parameters. All responses will have the [TestAnsTTL] TTL.
func CreateTestHandler(recordsCount int) (h dnsserver.Handler) {
f := func(ctx context.Context, rw dnsserver.ResponseWriter, req *dns.Msg) (err error) {
// Check that necessary context keys are set.
@ -20,30 +25,23 @@ func CreateTestHandler(recordsCount int) (h dnsserver.Handler) {
return errors.Error("client info does not contain server name")
}
hostname := req.Question[0].Name
resp := &dns.Msg{
Compress: true,
ans := make(SectionAnswer, 0, recordsCount)
hdr := dns.RR_Header{
Name: req.Question[0].Name,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: uint32(AnswerTTL.Seconds()),
}
resp.SetReply(req)
ip := netutil.IPv4Localhost().Prev()
for i := 0; i < recordsCount; i++ {
hdr := dns.RR_Header{
Name: hostname,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: 100,
}
a := &dns.A{
// Add 1 to make sure that each IP is valid.
A: net.IP{127, 0, 0, byte(i + 1)},
Hdr: hdr,
}
resp.Answer = append(resp.Answer, a)
// Add 1 to make sure that each IP is valid.
ip = ip.Next()
ans = append(ans, &dns.A{Hdr: hdr, A: ip.AsSlice()})
}
resp := NewResp(dns.RcodeSuccess, req, ans)
_ = rw.WriteMsg(ctx, req, resp)
return nil

View File

@ -4,23 +4,17 @@ import (
"net"
"testing"
"github.com/AdguardTeam/golibs/testutil"
"github.com/miekg/dns"
"github.com/stretchr/testify/require"
)
// CreateMessage creates a DNS message for the specified hostname and qtype.
func CreateMessage(hostname string, qtype uint16) (m *dns.Msg) {
return &dns.Msg{
MsgHdr: dns.MsgHdr{
Id: dns.Id(),
RecursionDesired: true,
},
Question: []dns.Question{{
Name: dns.Fqdn(hostname),
Qtype: qtype,
Qclass: dns.ClassINET,
}},
}
m = NewReq(hostname, qtype, dns.ClassINET)
m.RecursionDesired = true
return m
}
// RequireResponse checks that the DNS response we received is what was
@ -29,9 +23,9 @@ func RequireResponse(
t *testing.T,
req *dns.Msg,
resp *dns.Msg,
expectedRecordsCount int,
expectedRCode int,
expectedTruncated bool,
wantAnsLen int,
wantRCode int,
wantTruncated bool,
) {
t.Helper()
@ -40,26 +34,64 @@ func RequireResponse(
// Check that Opcode is not changed in the response
// regardless of the response status
require.Equal(t, req.Opcode, resp.Opcode)
require.Equal(t, expectedRCode, resp.Rcode)
require.Equal(t, expectedTruncated, resp.Truncated)
require.Equal(t, wantRCode, resp.Rcode)
require.Equal(t, wantTruncated, resp.Truncated)
require.True(t, resp.Response)
// Response must not have a Z flag set even for a query that does
// See https://github.com/miekg/dns/issues/975
require.False(t, resp.Zero)
require.Equal(t, expectedRecordsCount, len(resp.Answer))
require.Len(t, resp.Answer, wantAnsLen)
// Check that there's an OPT record in the response
if len(req.Extra) > 0 {
require.True(t, len(resp.Extra) > 0)
require.NotEmpty(t, resp.Extra)
}
if expectedRecordsCount > 0 {
a, ok := resp.Answer[0].(*dns.A)
require.True(t, ok)
if wantAnsLen > 0 {
a := testutil.RequireTypeAssert[*dns.A](t, resp.Answer[0])
require.Equal(t, req.Question[0].Name, a.Hdr.Name)
}
}
// RRSection is the resource record set to be appended to a new message created
// by [NewReq] and [NewResp]. It's essentially a sum type of:
//
// - [SectionAnswer]
// - [SectionNs]
// - [SectionExtra]
type RRSection interface {
// appendTo modifies m adding the resource record set into it appropriately.
appendTo(m *dns.Msg)
}
// type check
var (
_ RRSection = SectionAnswer{}
_ RRSection = SectionNs{}
_ RRSection = SectionExtra{}
)
// SectionAnswer should wrap a resource record set for the Answer section of DNS
// message.
type SectionAnswer []dns.RR
// appendTo implements the [RRSection] interface for SectionAnswer.
func (rrs SectionAnswer) appendTo(m *dns.Msg) { m.Answer = append(m.Answer, ([]dns.RR)(rrs)...) }
// SectionNs should wrap a resource record set for the Ns section of DNS
// message.
type SectionNs []dns.RR
// appendTo implements the [RRSection] interface for SectionNs.
func (rrs SectionNs) appendTo(m *dns.Msg) { m.Ns = append(m.Ns, ([]dns.RR)(rrs)...) }
// SectionExtra should wrap a resource record set for the Extra section of DNS
// message.
type SectionExtra []dns.RR
// appendTo implements the [RRSection] interface for SectionExtra.
func (rrs SectionExtra) appendTo(m *dns.Msg) { m.Extra = append(m.Extra, ([]dns.RR)(rrs)...) }
// NewReq returns the new DNS request with a single question for name, qtype,
// qclass, and rrs added.
func NewReq(name string, qtype, qclass uint16, rrs ...RRSection) (req *dns.Msg) {
@ -68,13 +100,15 @@ func NewReq(name string, qtype, qclass uint16, rrs ...RRSection) (req *dns.Msg)
Id: dns.Id(),
},
Question: []dns.Question{{
Name: name,
Name: dns.Fqdn(name),
Qtype: qtype,
Qclass: qclass,
}},
}
withRRs(req, rrs...)
for _, rr := range rrs {
rr.appendTo(req)
}
return req
}
@ -86,50 +120,13 @@ func NewResp(rcode int, req *dns.Msg, rrs ...RRSection) (resp *dns.Msg) {
resp.RecursionAvailable = true
resp.Compress = true
withRRs(resp, rrs...)
for _, rr := range rrs {
rr.appendTo(resp)
}
return resp
}
// MsgSection is used to specify the resource record set of the DNS message.
type MsgSection int
// Possible values of the MsgSection.
const (
SectionAnswer MsgSection = iota
SectionNs
SectionExtra
)
// RRSection is the slice of resource records to be appended to a new message
// created by NewReq and NewResp.
//
// TODO(e.burkov): Use separate types for different sections of DNS message
// instead of constants.
type RRSection struct {
RRs []dns.RR
Sec MsgSection
}
// withRRs adds rrs to the m. Invalid rrs are skipped.
func withRRs(m *dns.Msg, rrs ...RRSection) {
for _, r := range rrs {
var msgRR *[]dns.RR
switch r.Sec {
case SectionAnswer:
msgRR = &m.Answer
case SectionNs:
msgRR = &m.Ns
case SectionExtra:
msgRR = &m.Extra
default:
continue
}
*msgRR = append(*msgRR, r.RRs...)
}
}
// NewCNAME constructs the new resource record of type CNAME.
func NewCNAME(name string, ttl uint32, target string) (rr dns.RR) {
return &dns.CNAME{

View File

@ -0,0 +1,76 @@
package dnsservertest_test
import (
"fmt"
"net"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/dnsservertest"
"github.com/AdguardTeam/golibs/netutil"
"github.com/miekg/dns"
)
func ExampleNewReq() {
const nonUniqueID = 1234
m := dnsservertest.NewReq("example.org.", dns.TypeA, dns.ClassINET, dnsservertest.SectionExtra{
dnsservertest.NewECSExtra(netutil.IPv4Zero(), uint16(netutil.AddrFamilyIPv4), 0, 0),
})
m.Id = nonUniqueID
fmt.Println(m)
// Output:
//
// ;; opcode: QUERY, status: NOERROR, id: 1234
// ;; flags:; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
//
// ;; OPT PSEUDOSECTION:
// ; EDNS: version 0; flags:; udp: 0
// ; SUBNET: 0.0.0.0/0/0
//
// ;; QUESTION SECTION:
// ;example.org. IN A
}
func ExampleNewResp() {
const (
nonUniqueID = 1234
testFQDN = "example.org."
realTestFQDN = "real." + testFQDN
)
m := dnsservertest.NewReq(testFQDN, dns.TypeA, dns.ClassINET, dnsservertest.SectionExtra{
dnsservertest.NewECSExtra(netutil.IPv4Zero(), uint16(netutil.AddrFamilyIPv4), 0, 0),
})
m.Id = nonUniqueID
m = dnsservertest.NewResp(dns.RcodeSuccess, m, dnsservertest.SectionAnswer{
dnsservertest.NewCNAME(testFQDN, 3600, realTestFQDN),
dnsservertest.NewA(realTestFQDN, 3600, net.IP{1, 2, 3, 4}),
}, dnsservertest.SectionNs{
dnsservertest.NewSOA(realTestFQDN, 1000, "ns."+realTestFQDN, "mbox."+realTestFQDN),
dnsservertest.NewNS(testFQDN, 1000, "ns."+testFQDN),
}, dnsservertest.SectionExtra{
m.IsEdns0(),
})
fmt.Println(m)
// Output:
//
// ;; opcode: QUERY, status: NOERROR, id: 1234
// ;; flags: qr ra; QUERY: 1, ANSWER: 2, AUTHORITY: 2, ADDITIONAL: 1
//
// ;; OPT PSEUDOSECTION:
// ; EDNS: version 0; flags:; udp: 0
// ; SUBNET: 0.0.0.0/0/0
//
// ;; QUESTION SECTION:
// ;example.org. IN A
//
// ;; ANSWER SECTION:
// example.org. 3600 IN CNAME real.example.org.
// real.example.org. 3600 IN A 1.2.3.4
//
// ;; AUTHORITY SECTION:
// real.example.org. 1000 IN SOA ns.real.example.org. mbox.real.example.org. 0 0 0 0 0
// example.org. 1000 IN NS ns.example.org.
}

View File

@ -59,7 +59,7 @@ func ExampleWithMiddlewares() {
forwarder := forward.NewHandler(&forward.HandlerConfig{
Address: netip.MustParseAddrPort("94.140.14.140:53"),
Network: forward.NetworkAny,
}, true)
})
middleware := querylog.NewLogMiddleware(os.Stdout)
handler := dnsserver.WithMiddlewares(forwarder, middleware)

View File

@ -20,7 +20,7 @@ func ExampleNewHandler() {
FallbackAddresses: []netip.AddrPort{
netip.MustParseAddrPort("1.1.1.1:53"),
},
}, false),
}),
},
}

View File

@ -38,20 +38,6 @@ import (
// queries to the specified upstreams. It also implements [io.Closer], allowing
// resource reuse.
type Handler struct {
// lastFailedHealthcheck shows the last time of failed healthcheck.
//
// It is of type int64 to be accessed by package atomic. The field is
// arranged for 64-bit alignment on the first position.
lastFailedHealthcheck int64
// useFallbacks is not zero if the main upstream server failed health check
// probes and therefore the fallback upstream servers should be used for
// resolving.
//
// It is of type uint64 to be accessed by package atomic. The field is
// arranged for 64-bit alignment on the second position.
useFallbacks uint64
// metrics is a listener for the handler events.
metrics MetricsListener
@ -65,12 +51,21 @@ type Handler struct {
// fallbacks is a list of fallback DNS servers.
fallbacks []Upstream
// lastFailedHealthcheck contains the Unix time of the last time of failed
// healthcheck.
lastFailedHealthcheck atomic.Int64
// timeout specifies the query timeout for upstreams and fallbacks.
timeout time.Duration
// hcBackoffTime specifies the delay before returning to the main upstream
// after failed healthcheck probe.
hcBackoff time.Duration
// useFallbacks is true if the main upstream server failed health check
// probes and therefore the fallback upstream servers should be used for
// resolving.
useFallbacks atomic.Bool
}
// ErrNoResponse is returned from Handler's methods when the desired response
@ -113,14 +108,21 @@ type HandlerConfig struct {
// upstream until this time has passed. If the healthcheck is still
// performed, each failed check advances the backoff.
HealthcheckBackoffDuration time.Duration
// HealthcheckInitDuration is the time duration for initial upstream
// healthcheck.
HealthcheckInitDuration time.Duration
}
// NewHandler initializes a new instance of Handler. It also performs a health
// check afterwards if initialHealthcheck is true. Note, that this handler only
// support plain DNS upstreams. c must not be nil.
func NewHandler(c *HandlerConfig, initialHealthcheck bool) (h *Handler) {
// check afterwards if c.HealthcheckInitDuration is not zero. Note, that this
// handler only support plain DNS upstreams. c must not be nil.
func NewHandler(c *HandlerConfig) (h *Handler) {
h = &Handler{
upstream: NewUpstreamPlain(c.Address, c.Network),
upstream: NewUpstreamPlain(&UpstreamPlainConfig{
Network: c.Network,
Address: c.Address,
}),
hcDomainTmpl: c.HealthcheckDomainTmpl,
timeout: c.Timeout,
hcBackoff: c.HealthcheckBackoffDuration,
@ -134,13 +136,19 @@ func NewHandler(c *HandlerConfig, initialHealthcheck bool) (h *Handler) {
h.fallbacks = make([]Upstream, len(c.FallbackAddresses))
for i, addr := range c.FallbackAddresses {
h.fallbacks[i] = NewUpstreamPlain(addr, NetworkAny)
h.fallbacks[i] = NewUpstreamPlain(&UpstreamPlainConfig{
Network: NetworkAny,
Address: addr,
})
}
if initialHealthcheck {
if c.HealthcheckInitDuration > 0 {
ctx, cancel := context.WithTimeout(context.Background(), c.HealthcheckInitDuration)
defer cancel()
// Ignore the error since it's considered non-critical and also should
// have been logged already.
_ = h.refresh(context.Background(), true)
_ = h.refresh(ctx, true)
}
return h
@ -176,7 +184,7 @@ func (h *Handler) ServeDNS(
) (err error) {
defer func() { err = annotate(err, h.upstream) }()
useFallbacks := atomic.LoadUint64(&h.useFallbacks) != 0
useFallbacks := h.useFallbacks.Load()
var resp *dns.Msg
if !useFallbacks {
resp, err = h.exchange(ctx, h.upstream, req)

View File

@ -4,6 +4,7 @@ import (
"context"
"net/netip"
"testing"
"time"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/dnsservertest"
@ -17,6 +18,21 @@ func TestMain(m *testing.M) {
testutil.DiscardLogOutput(m)
}
// testTimeout is the timeout for tests.
const testTimeout = 1 * time.Second
// newTimeoutCtx is a test helper that returns a context with a timeout of
// [testTimeout] and its cancel function being called in the test cleanup.
// It should not be used where cancellation is expected sooner.
func newTimeoutCtx(tb testing.TB, parent context.Context) (ctx context.Context) {
tb.Helper()
ctx, cancel := context.WithTimeout(parent, testTimeout)
tb.Cleanup(cancel)
return ctx
}
func TestHandler_ServeDNS(t *testing.T) {
srv, addr := dnsservertest.RunDNSServer(t, dnsservertest.DefaultHandler())
@ -24,13 +40,14 @@ func TestHandler_ServeDNS(t *testing.T) {
handler := forward.NewHandler(&forward.HandlerConfig{
Address: netip.MustParseAddrPort(addr),
Network: forward.NetworkAny,
}, true)
Timeout: testTimeout,
})
req := dnsservertest.CreateMessage("example.org.", dns.TypeA)
rw := dnsserver.NewNonWriterResponseWriter(srv.LocalUDPAddr(), srv.LocalUDPAddr())
// Check the handler's ServeDNS method
err := handler.ServeDNS(context.Background(), rw, req)
err := handler.ServeDNS(newTimeoutCtx(t, context.Background()), rw, req)
require.NoError(t, err)
res := rw.Msg()
@ -46,7 +63,8 @@ func TestHandler_ServeDNS_fallbackNetError(t *testing.T) {
FallbackAddresses: []netip.AddrPort{
netip.MustParseAddrPort(srv.LocalUDPAddr().String()),
},
}, true)
Timeout: testTimeout,
})
req := dnsservertest.CreateMessage("example.org.", dns.TypeA)
rw := dnsserver.NewNonWriterResponseWriter(srv.LocalUDPAddr(), srv.LocalUDPAddr())

View File

@ -6,7 +6,6 @@ import (
"math/rand"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/AdguardTeam/golibs/errors"
@ -25,25 +24,25 @@ func (h *Handler) refresh(ctx context.Context, shouldReport bool) (err error) {
return nil
}
var useFallbacks uint64
lastFailed := atomic.LoadInt64(&h.lastFailedHealthcheck)
var useFallbacks bool
lastFailed := h.lastFailedHealthcheck.Load()
shouldReturnToMain := time.Since(time.Unix(lastFailed, 0)) >= h.hcBackoff
if !shouldReturnToMain {
// Make sure that useFallbacks is left true if the main upstream is
// still in the backoff mode.
useFallbacks = 1
useFallbacks = true
log.Debug("forward: healthcheck: in backoff, will not return to main on success")
}
err = h.healthcheck(ctx)
if err != nil {
atomic.StoreInt64(&h.lastFailedHealthcheck, time.Now().Unix())
useFallbacks = 1
h.lastFailedHealthcheck.Store(time.Now().Unix())
useFallbacks = true
}
statusChanged := atomic.CompareAndSwapUint64(&h.useFallbacks, 1-useFallbacks, useFallbacks)
statusChanged := h.useFallbacks.CompareAndSwap(!useFallbacks, useFallbacks)
if statusChanged || shouldReport {
h.setUpstreamStatus(useFallbacks == 0)
h.setUpstreamStatus(!useFallbacks)
}
return errors.Annotate(err, "forward: %w")

View File

@ -15,8 +15,10 @@ import (
)
func TestHandler_Refresh(t *testing.T) {
var upstreamUp uint64
var upstreamRequestsCount uint64
var upstreamIsUp atomic.Bool
var upstreamRequestsCount atomic.Int64
defaultHandler := dnsservertest.DefaultHandler()
// This handler writes an empty message if upstreamUp flag is false.
handlerFunc := dnsserver.HandlerFunc(func(
@ -24,16 +26,15 @@ func TestHandler_Refresh(t *testing.T) {
rw dnsserver.ResponseWriter,
req *dns.Msg,
) (err error) {
atomic.AddUint64(&upstreamRequestsCount, 1)
upstreamRequestsCount.Add(1)
nrw := dnsserver.NewNonWriterResponseWriter(rw.LocalAddr(), rw.RemoteAddr())
handler := dnsservertest.DefaultHandler()
err = handler.ServeDNS(ctx, nrw, req)
err = defaultHandler.ServeDNS(ctx, nrw, req)
if err != nil {
return err
}
if atomic.LoadUint64(&upstreamUp) == 0 {
if !upstreamIsUp.Load() {
return rw.WriteMsg(ctx, req, &dns.Msg{})
}
@ -41,7 +42,7 @@ func TestHandler_Refresh(t *testing.T) {
})
upstream, _ := dnsservertest.RunDNSServer(t, handlerFunc)
fallback, _ := dnsservertest.RunDNSServer(t, dnsservertest.DefaultHandler())
fallback, _ := dnsservertest.RunDNSServer(t, defaultHandler)
handler := forward.NewHandler(&forward.HandlerConfig{
Address: netip.MustParseAddrPort(upstream.LocalUDPAddr().String()),
Network: forward.NetworkAny,
@ -49,38 +50,41 @@ func TestHandler_Refresh(t *testing.T) {
FallbackAddresses: []netip.AddrPort{
netip.MustParseAddrPort(fallback.LocalUDPAddr().String()),
},
// Make sure that the handler routs queries back to the main upstream
Timeout: testTimeout,
// Make sure that the handler routes queries back to the main upstream
// immediately.
HealthcheckBackoffDuration: 0,
}, false)
})
req := dnsservertest.CreateMessage("example.org.", dns.TypeA)
rw := dnsserver.NewNonWriterResponseWriter(fallback.LocalUDPAddr(), fallback.LocalUDPAddr())
err := handler.ServeDNS(context.Background(), rw, req)
require.Error(t, err)
assert.Equal(t, uint64(1), atomic.LoadUint64(&upstreamRequestsCount))
ctx := context.Background()
err = handler.Refresh(context.Background())
err := handler.ServeDNS(newTimeoutCtx(t, ctx), rw, req)
require.Error(t, err)
assert.Equal(t, uint64(2), atomic.LoadUint64(&upstreamRequestsCount))
assert.Equal(t, int64(2), upstreamRequestsCount.Load())
err = handler.ServeDNS(context.Background(), rw, req)
err = handler.Refresh(newTimeoutCtx(t, ctx))
require.Error(t, err)
assert.Equal(t, int64(4), upstreamRequestsCount.Load())
err = handler.ServeDNS(newTimeoutCtx(t, ctx), rw, req)
require.NoError(t, err)
assert.Equal(t, uint64(2), atomic.LoadUint64(&upstreamRequestsCount))
assert.Equal(t, int64(4), upstreamRequestsCount.Load())
// Now, set upstream up.
atomic.StoreUint64(&upstreamUp, 1)
upstreamIsUp.Store(true)
err = handler.ServeDNS(context.Background(), rw, req)
err = handler.ServeDNS(newTimeoutCtx(t, ctx), rw, req)
require.NoError(t, err)
assert.Equal(t, uint64(2), atomic.LoadUint64(&upstreamRequestsCount))
assert.Equal(t, int64(4), upstreamRequestsCount.Load())
err = handler.Refresh(context.Background())
err = handler.Refresh(newTimeoutCtx(t, ctx))
require.NoError(t, err)
assert.Equal(t, uint64(3), atomic.LoadUint64(&upstreamRequestsCount))
assert.Equal(t, int64(5), upstreamRequestsCount.Load())
err = handler.ServeDNS(context.Background(), rw, req)
err = handler.ServeDNS(newTimeoutCtx(t, ctx), rw, req)
require.NoError(t, err)
assert.Equal(t, uint64(4), atomic.LoadUint64(&upstreamRequestsCount))
assert.Equal(t, int64(6), upstreamRequestsCount.Load())
}

View File

@ -7,6 +7,7 @@ import (
"io"
"net"
"net/netip"
"strings"
"sync"
"time"
@ -15,7 +16,7 @@ import (
"github.com/miekg/dns"
)
// Network is a enumeration of networks UpstreamPlain supports
// Network is a enumeration of networks UpstreamPlain supports.
type Network string
const (
@ -72,11 +73,21 @@ type UpstreamPlain struct {
// type check
var _ Upstream = (*UpstreamPlain)(nil)
// NewUpstreamPlain creates and initializes a new instance of UpstreamPlain.
func NewUpstreamPlain(addr netip.AddrPort, network Network) (ups *UpstreamPlain) {
// UpstreamPlainConfig is the configuration structure for a plain-DNS upstream.
type UpstreamPlainConfig struct {
// Network is the network to use for this upstream.
Network Network
// Address is the address of the upstream DNS server.
Address netip.AddrPort
}
// NewUpstreamPlain returns a new properly initialized *UpstreamPlain. c must
// not be nil.
func NewUpstreamPlain(c *UpstreamPlainConfig) (ups *UpstreamPlain) {
ups = &UpstreamPlain{
addr: addr,
network: network,
addr: c.Address,
network: c.Network,
}
ups.connsPoolUDP = pool.NewPool(poolMaxCapacity, makeConnsPoolFactory(ups, NetworkUDP))
@ -127,33 +138,39 @@ func (u *UpstreamPlain) String() (str string) {
return fmt.Sprintf("%s://%s", u.network, u.addr)
}
// exchangeUDP attempts to send the DNS request over UDP. It returns a
// fallbackToTCP flag to signal if we should fallback to using TCP instead.
// this may happen if the response received over UDP was truncated and
// exchangeUDP attempts to send the DNS request over UDP. It returns a
// fallbackToTCP flag to signal if the caller should fallback to using TCP
// instead. This may happen if the response received over UDP was truncated and
// TCP is enabled for this upstream or if UDP is disabled.
func (u *UpstreamPlain) exchangeUDP(
ctx context.Context,
req *dns.Msg,
) (fallbackToTCP bool, resp *dns.Msg, err error) {
if u.network == NetworkTCP {
// fallback to TCP immediately.
// Fallback to TCP immediately.
return true, nil, nil
}
resp, err = u.exchangeNet(ctx, req, NetworkUDP)
if err != nil {
// error means that the upstream is dead, no need to fallback to TCP.
return false, resp, err
// The network error always causes the subsequent query attempt using
// fresh UDP connection, so if it happened again, the upstream is likely
// dead and using TCP appears meaningless. See [exchangeNet].
//
// Thus, non-network errors are considered being related to the
// response. It may also happen the received response is intended for
// another timeouted request sent from the same source port, but falling
// back to TCP in this case shouldn't hurt.
fallbackToTCP = !isExpectedConnErr(err)
return fallbackToTCP, resp, err
}
// If the response is truncated and we can use TCP, make sure that we'll
// fallback to TCP. We also fallback to TCP if we received a response with
// the wrong ID (it may happen with the servers under heavy load).
if (resp.Truncated || resp.Id != req.Id) && u.network != NetworkUDP {
fallbackToTCP = true
}
// Also, fallback to TCP if the received response is truncated and the
// upstream isn't UDP-only.
fallbackToTCP = u.network != NetworkUDP && resp != nil && resp.Truncated
return fallbackToTCP, resp, err
return fallbackToTCP, resp, nil
}
// exchangeNet sends a DNS query using the specified network (either TCP or UDP).
@ -203,25 +220,36 @@ func (u *UpstreamPlain) exchangeNet(
return resp, err
}
// validateResponse checks if the response is valid for the original query. For
// instance, it is possible to receive a response to a different query, and we
// must be sure that we received what was expected.
func (u *UpstreamPlain) validateResponse(req, resp *dns.Msg) (err error) {
// validatePlainResponse returns an error if the response is not valid for the
// original request. This is required because we might receive a response to a
// different query, e.g. when the server is under heavy load.
func validatePlainResponse(req, resp *dns.Msg) (err error) {
if req.Id != resp.Id {
return dns.ErrId
}
if len(resp.Question) != 1 {
return ErrQuestion
if qlen := len(resp.Question); qlen != 1 {
return fmt.Errorf("%w: only 1 question allowed; got %d", ErrQuestion, qlen)
}
if req.Question[0].Name != resp.Question[0].Name {
return ErrQuestion
reqQ, respQ := req.Question[0], resp.Question[0]
if reqQ.Qtype != respQ.Qtype {
return fmt.Errorf("%w: mismatched type %s", ErrQuestion, dns.Type(respQ.Qtype))
}
// Compare the names case-insensitively, just like CoreDNS does.
if !strings.EqualFold(reqQ.Name, respQ.Name) {
return fmt.Errorf("%w: mismatched name %q", ErrQuestion, respQ.Name)
}
return nil
}
// defaultUDPTimeout is the default timeout for waiting a valid DNS message or
// network error.
const defaultUDPTimeout = 1 * time.Minute
// processConn writes the query to the connection and then reads the response
// from it. We might be dealing with an idle dead connection so if we get
// a network error here, we'll attempt to open a new connection and call this
@ -236,7 +264,7 @@ func (u *UpstreamPlain) processConn(
req *dns.Msg,
buf []byte,
bufLen int,
) (msg *dns.Msg, err error) {
) (resp *dns.Msg, err error) {
// Make sure that we return the connection to the pool in the end or close
// if there was any error.
defer func() {
@ -248,7 +276,12 @@ func (u *UpstreamPlain) processConn(
}()
// Prepare a context with a deadline if needed.
if deadline, ok := ctx.Deadline(); ok {
deadline, ok := ctx.Deadline()
if !ok && network == NetworkUDP {
deadline, ok = time.Now().Add(defaultUDPTimeout), true
}
if ok {
err = conn.SetDeadline(deadline)
if err != nil {
return nil, fmt.Errorf("setting deadline: %w", err)
@ -261,19 +294,28 @@ func (u *UpstreamPlain) processConn(
return nil, fmt.Errorf("writing request: %w", err)
}
var resp *dns.Msg
return u.readValidMsg(req, network, conn, buf)
}
// readValidMsg reads the response from conn to buf, parses and validates it.
func (u *UpstreamPlain) readValidMsg(
req *dns.Msg,
network Network,
conn net.Conn,
buf []byte,
) (resp *dns.Msg, err error) {
resp, err = u.readMsg(network, conn, buf)
if err != nil {
// Error is already wrapped.
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
err = u.validateResponse(req, resp)
err = validatePlainResponse(req, resp)
if err != nil {
return nil, fmt.Errorf("validating response: %w", err)
return resp, fmt.Errorf("validating %s response: %w", network, err)
}
return resp, err
return resp, nil
}
// readMsg reads the response from the specified connection and parses it.
@ -302,7 +344,8 @@ func (u *UpstreamPlain) readMsg(network Network, conn net.Conn, buf []byte) (*dn
if n < minDNSMessageSize {
return nil, fmt.Errorf("invalid msg: %w", dns.ErrShortRead)
}
ret := new(dns.Msg)
ret := &dns.Msg{}
err = ret.Unpack(buf)
if err != nil {
return nil, fmt.Errorf("unpacking msg: %w", err)

View File

@ -5,11 +5,14 @@ import (
"net/netip"
"testing"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/dnsservertest"
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/forward"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/testutil"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -31,11 +34,14 @@ func TestUpstreamPlain_Exchange(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, addr := dnsservertest.RunDNSServer(t, dnsservertest.DefaultHandler())
u := forward.NewUpstreamPlain(netip.MustParseAddrPort(addr), tc.network)
u := forward.NewUpstreamPlain(&forward.UpstreamPlainConfig{
Network: tc.network,
Address: netip.MustParseAddrPort(addr),
})
defer log.OnCloserError(u, log.DEBUG)
req := dnsservertest.CreateMessage("example.org.", dns.TypeA)
res, err := u.Exchange(context.Background(), req)
res, err := u.Exchange(newTimeoutCtx(t, context.Background()), req)
require.NoError(t, err)
require.NotNil(t, res)
dnsservertest.RequireResponse(t, req, res, 1, dns.RcodeSuccess, false)
@ -71,34 +77,199 @@ func TestUpstreamPlain_Exchange_truncated(t *testing.T) {
return rw.WriteMsg(ctx, req, res)
})
_, addr := dnsservertest.RunDNSServer(t, handlerFunc)
_, addrStr := dnsservertest.RunDNSServer(t, handlerFunc)
// Create a test message.
req := dnsservertest.CreateMessage("example.org.", dns.TypeA)
// First, check that we receive truncated response over UDP.
uAddr := netip.MustParseAddrPort(addr)
uUDP := forward.NewUpstreamPlain(uAddr, forward.NetworkUDP)
addr := netip.MustParseAddrPort(addrStr)
uUDP := forward.NewUpstreamPlain(&forward.UpstreamPlainConfig{
Network: forward.NetworkUDP,
Address: addr,
})
defer log.OnCloserError(uUDP, log.DEBUG)
res, err := uUDP.Exchange(context.Background(), req)
ctx := context.Background()
res, err := uUDP.Exchange(newTimeoutCtx(t, ctx), req)
require.NoError(t, err)
dnsservertest.RequireResponse(t, req, res, 0, dns.RcodeSuccess, true)
// Second, check that nothing is truncated over TCP.
uTCP := forward.NewUpstreamPlain(uAddr, forward.NetworkTCP)
uTCP := forward.NewUpstreamPlain(&forward.UpstreamPlainConfig{
Network: forward.NetworkTCP,
Address: addr,
})
defer log.OnCloserError(uTCP, log.DEBUG)
res, err = uTCP.Exchange(context.Background(), req)
res, err = uTCP.Exchange(newTimeoutCtx(t, ctx), req)
require.NoError(t, err)
dnsservertest.RequireResponse(t, req, res, 1, dns.RcodeSuccess, false)
// Now with NetworkANY response is also not truncated since the upstream
// fallbacks to TCP.
uAny := forward.NewUpstreamPlain(uAddr, forward.NetworkAny)
uAny := forward.NewUpstreamPlain(&forward.UpstreamPlainConfig{
Network: forward.NetworkAny,
Address: addr,
})
defer log.OnCloserError(uAny, log.DEBUG)
res, err = uAny.Exchange(context.Background(), req)
res, err = uAny.Exchange(newTimeoutCtx(t, ctx), req)
require.NoError(t, err)
dnsservertest.RequireResponse(t, req, res, 1, dns.RcodeSuccess, false)
}
func TestUpstreamPlain_Exchange_fallbackFail(t *testing.T) {
pt := testutil.PanicT{}
// Use only unbuffered channels to block until received and validated.
netCh := make(chan string)
respCh := make(chan struct{})
h := dnsserver.HandlerFunc(func(
ctx context.Context,
rw dnsserver.ResponseWriter,
req *dns.Msg,
) (err error) {
testutil.RequireSend(pt, netCh, rw.RemoteAddr().Network(), testTimeout)
resp := dnsservertest.NewResp(dns.RcodeSuccess, req)
// Make all responses invalid.
resp.Id = req.Id + 1
return rw.WriteMsg(ctx, req, resp)
})
_, addr := dnsservertest.RunDNSServer(t, h)
u := forward.NewUpstreamPlain(&forward.UpstreamPlainConfig{
Network: forward.NetworkUDP,
Address: netip.MustParseAddrPort(addr),
})
testutil.CleanupAndRequireSuccess(t, u.Close)
req := dnsservertest.CreateMessage("example.org.", dns.TypeA)
var resp *dns.Msg
var err error
go func() {
resp, err = u.Exchange(newTimeoutCtx(t, context.Background()), req)
testutil.RequireSend(pt, respCh, struct{}{}, testTimeout)
}()
// First attempt should use UDP and fail due to bad ID.
network, _ := testutil.RequireReceive(t, netCh, testTimeout)
require.Equal(t, string(forward.NetworkUDP), network)
// Second attempt should use TCP and succeed.
network, _ = testutil.RequireReceive(t, netCh, testTimeout)
require.Equal(t, string(forward.NetworkTCP), network)
testutil.RequireReceive(t, respCh, testTimeout)
require.ErrorIs(t, err, dns.ErrId)
assert.NotNil(t, resp)
}
func TestUpstreamPlain_Exchange_fallbackSuccess(t *testing.T) {
const (
// network is set to UDP to ensure that falling back to TCP will still
// be performed.
network = forward.NetworkUDP
goodDomain = "domain.example."
badDomain = "bad.example."
)
pt := testutil.PanicT{}
req := dnsservertest.CreateMessage(goodDomain, dns.TypeA)
resp := dnsservertest.NewResp(dns.RcodeSuccess, req)
// Prepare malformed responses.
badIDResp := dnsmsg.Clone(resp)
badIDResp.Id = ^req.Id
badQNumResp := dnsmsg.Clone(resp)
badQNumResp.Question = append(badQNumResp.Question, req.Question[0])
badQnameResp := dnsmsg.Clone(resp)
badQnameResp.Question[0].Name = badDomain
badQtypeResp := dnsmsg.Clone(resp)
badQtypeResp.Question[0].Qtype = dns.TypeMX
testCases := []struct {
udpResp *dns.Msg
name string
}{{
udpResp: badIDResp,
name: "wrong_id",
}, {
udpResp: badQNumResp,
name: "wrong_question)_number",
}, {
udpResp: badQnameResp,
name: "wrong_qname",
}, {
udpResp: badQtypeResp,
name: "wrong_qtype",
}}
for _, tc := range testCases {
clonedReq := dnsmsg.Clone(req)
badResp := dnsmsg.Clone(tc.udpResp)
goodResp := dnsmsg.Clone(resp)
// Use only unbuffered channels to block until received and validated.
netCh := make(chan string)
respCh := make(chan struct{})
h := dnsserver.HandlerFunc(func(
ctx context.Context,
rw dnsserver.ResponseWriter,
req *dns.Msg,
) (err error) {
network := rw.RemoteAddr().Network()
testutil.RequireSend(pt, netCh, network, testTimeout)
if network == string(forward.NetworkUDP) {
// Respond with invalid message via UDP.
return rw.WriteMsg(ctx, req, badResp)
}
// Respond with valid message via TCP.
return rw.WriteMsg(ctx, req, goodResp)
})
t.Run(tc.name, func(t *testing.T) {
_, addr := dnsservertest.RunDNSServer(t, dnsserver.HandlerFunc(h))
u := forward.NewUpstreamPlain(&forward.UpstreamPlainConfig{
Network: network,
Address: netip.MustParseAddrPort(addr),
})
testutil.CleanupAndRequireSuccess(t, u.Close)
var actualResp *dns.Msg
var err error
go func() {
actualResp, err = u.Exchange(newTimeoutCtx(t, context.Background()), clonedReq)
testutil.RequireSend(pt, respCh, struct{}{}, testTimeout)
}()
// First attempt should use UDP and fail due to bad ID.
network, _ := testutil.RequireReceive(t, netCh, testTimeout)
require.Equal(t, string(forward.NetworkUDP), network)
// Second attempt should use TCP and succeed.
network, _ = testutil.RequireReceive(t, netCh, testTimeout)
require.Equal(t, string(forward.NetworkTCP), network)
testutil.RequireReceive(t, respCh, testTimeout)
require.NoError(t, err)
dnsservertest.RequireResponse(t, req, actualResp, 0, dns.RcodeSuccess, false)
})
}
}

View File

@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardDNS/internal/dnsserver
go 1.20
require (
github.com/AdguardTeam/golibs v0.12.1
github.com/AdguardTeam/golibs v0.13.2
github.com/ameshkov/dnscrypt/v2 v2.2.5
github.com/ameshkov/dnsstamps v1.0.3
github.com/bluele/gcache v0.0.2
@ -13,7 +13,7 @@ require (
github.com/prometheus/client_golang v1.14.0
github.com/quic-go/quic-go v0.33.0
github.com/stretchr/testify v1.8.2
golang.org/x/exp v0.0.0-20230307190834-24139beb5833
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
golang.org/x/net v0.8.0
golang.org/x/sys v0.6.0
)

View File

@ -1,5 +1,4 @@
github.com/AdguardTeam/golibs v0.12.1 h1:bJfFzCnUCl+QsP6prUltM2Sjt0fTiDBPlxuAwfKP3g8=
github.com/AdguardTeam/golibs v0.12.1/go.mod h1:rIglKDHdLvFT1UbhumBLHO9S4cvWS9MEyT1njommI/Y=
github.com/AdguardTeam/golibs v0.13.2 h1:BPASsyQKmb+b8VnvsNOHp7bKfcZl9Z+Z2UhPjOiupSc=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
@ -80,8 +79,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s=
golang.org/x/exp v0.0.0-20230307190834-24139beb5833/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=

View File

@ -5,6 +5,7 @@ import (
"context"
"fmt"
"net"
"syscall"
)
// ListenConfig is the interface that allows controlling options of connections
@ -18,23 +19,43 @@ type ListenConfig interface {
ListenPacket(ctx context.Context, network, address string) (c net.PacketConn, err error)
}
// defaultCtrlConf is the default control config. By default, don't alter
// anything. defaultCtrlConf must not be mutated.
var defaultCtrlConf = &ControlConfig{
RcvBufSize: 0,
SndBufSize: 0,
}
// DefaultListenConfig returns the default [ListenConfig] used by the servers in
// this module except for the plain-DNS ones, which use
// [DefaultListenConfigWithOOB].
func DefaultListenConfig() (lc ListenConfig) {
// [DefaultListenConfigWithOOB]. If conf is nil, a default configuration is
// used.
func DefaultListenConfig(conf *ControlConfig) (lc ListenConfig) {
if conf == nil {
conf = defaultCtrlConf
}
return &net.ListenConfig{
Control: defaultListenControl,
Control: func(_, _ string, c syscall.RawConn) (err error) {
return listenControlWithSO(conf, c)
},
}
}
// DefaultListenConfigWithOOB returns the default [ListenConfig] used by the
// plain-DNS servers in this module. The resulting ListenConfig sets additional
// socket flags and processes the control-messages of connections created with
// ListenPacket.
func DefaultListenConfigWithOOB() (lc ListenConfig) {
// ListenPacket. If conf is nil, a default configuration is used.
func DefaultListenConfigWithOOB(conf *ControlConfig) (lc ListenConfig) {
if conf == nil {
conf = defaultCtrlConf
}
return &listenConfigOOB{
ListenConfig: net.ListenConfig{
Control: defaultListenControl,
Control: func(_, _ string, c syscall.RawConn) (err error) {
return listenControlWithSO(conf, c)
},
},
}
}
@ -71,3 +92,14 @@ func (lc *listenConfigOOB) ListenPacket(
return wrapPacketConn(c), nil
}
// ControlConfig is the configuration of socket options.
type ControlConfig struct {
// RcvBufSize defines the size of socket receive buffer in bytes. Default
// is zero (uses system settings).
RcvBufSize int
// SndBufSize defines the size of socket send buffer in bytes. Default is
// zero (uses system settings).
SndBufSize int
}

View File

@ -13,17 +13,50 @@ import (
"golang.org/x/sys/unix"
)
// defaultListenControl is used as a [net.ListenConfig.Control] function to set
// the SO_REUSEPORT socket option on all sockets used by the DNS servers in this
// package.
func defaultListenControl(_, _ string, c syscall.RawConn) (err error) {
// setSockOptFunc is a function that sets a socket option on fd.
type setSockOptFunc func(fd int) (err error)
// newSetSockOptFunc returns a socket-option function with the given parameters.
func newSetSockOptFunc(name string, lvl, opt, val int) (o setSockOptFunc) {
return func(fd int) (err error) {
err = unix.SetsockoptInt(fd, lvl, opt, val)
return errors.Annotate(err, "setting %s: %w", name)
}
}
// listenControlWithSO is used as a [net.ListenConfig.Control] function to set
// the SO_REUSEPORT, SO_SNDBUF, and SO_RCVBUF socket options on all sockets
// used by the DNS servers in this package. conf must not be nil.
func listenControlWithSO(conf *ControlConfig, c syscall.RawConn) (err error) {
opts := []setSockOptFunc{
newSetSockOptFunc("SO_REUSEPORT", unix.SOL_SOCKET, unix.SO_REUSEPORT, 1),
}
if conf.SndBufSize > 0 {
opts = append(
opts,
newSetSockOptFunc("SO_SNDBUF", unix.SOL_SOCKET, unix.SO_SNDBUF, conf.SndBufSize),
)
}
if conf.RcvBufSize > 0 {
opts = append(
opts,
newSetSockOptFunc("SO_RCVBUF", unix.SOL_SOCKET, unix.SO_RCVBUF, conf.RcvBufSize),
)
}
var opErr error
err = c.Control(func(fd uintptr) {
opErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
fdInt := int(fd)
for _, opt := range opts {
opErr = opt(fdInt)
if opErr != nil {
return
}
}
})
if err != nil {
return err
}
return errors.WithDeferred(opErr, err)
}

View File

@ -15,7 +15,7 @@ import (
)
func TestDefaultListenConfigWithOOB(t *testing.T) {
lc := netext.DefaultListenConfigWithOOB()
lc := netext.DefaultListenConfigWithOOB(nil)
require.NotNil(t, lc)
type syscallConner interface {
@ -65,3 +65,81 @@ func TestDefaultListenConfigWithOOB(t *testing.T) {
require.NoError(t, err)
})
}
func TestDefaultListenConfigWithSO(t *testing.T) {
const (
sndBufSize = 10000
rcvBufSize = 20000
)
lc := netext.DefaultListenConfigWithOOB(&netext.ControlConfig{
SndBufSize: sndBufSize,
RcvBufSize: rcvBufSize,
})
require.NotNil(t, lc)
type syscallConner interface {
SyscallConn() (c syscall.RawConn, err error)
}
t.Run("ipv4", func(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()
require.NoError(t, err)
err = sc.Control(func(fd uintptr) {
val, opErr := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_SNDBUF)
require.NoError(t, opErr)
// TODO(a.garipov): Rewrite this to use actual expected values for
// each OS.
assert.LessOrEqual(t, sndBufSize, val)
})
require.NoError(t, err)
err = sc.Control(func(fd uintptr) {
val, opErr := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_RCVBUF)
require.NoError(t, opErr)
assert.LessOrEqual(t, rcvBufSize, val)
})
require.NoError(t, err)
})
t.Run("ipv6", func(t *testing.T) {
c, err := lc.ListenPacket(context.Background(), "udp6", "[::1]:0")
if errors.Is(err, syscall.EADDRNOTAVAIL) {
// Some CI machines have IPv6 disabled.
t.Skipf("ipv6 seems to not be supported: %s", err)
}
require.NoError(t, err)
require.NotNil(t, c)
require.Implements(t, (*syscallConner)(nil), c)
sc, err := c.(syscallConner).SyscallConn()
require.NoError(t, err)
err = sc.Control(func(fd uintptr) {
val, opErr := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_SNDBUF)
require.NoError(t, opErr)
// TODO(a.garipov): Rewrite this to use actual expected values for
// each OS.
assert.LessOrEqual(t, sndBufSize, val)
})
require.NoError(t, err)
err = sc.Control(func(fd uintptr) {
val, opErr := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_RCVBUF)
require.NoError(t, opErr)
assert.LessOrEqual(t, rcvBufSize, val)
})
require.NoError(t, err)
})
}

View File

@ -7,9 +7,9 @@ import (
"syscall"
)
// defaultListenControl is nil on Windows, because it doesn't support
// SO_REUSEPORT.
var defaultListenControl func(_, _ string, _ syscall.RawConn) (_ error)
// listenControlWithSO is nil on Windows, because it doesn't support socket
// options.
var listenControlWithSO func(_ *ControlConfig, _ syscall.RawConn) (_ error)
// setIPOpts sets the IPv4 and IPv6 options on a packet connection.
func setIPOpts(c net.PacketConn) (err error) {

View File

@ -51,7 +51,7 @@ func TestSessionPacketConn(t *testing.T) {
}
func testSessionPacketConn(t *testing.T, proto, addr string, dstIP net.IP) (isTimeout bool) {
lc := netext.DefaultListenConfigWithOOB()
lc := netext.DefaultListenConfigWithOOB(nil)
require.NotNil(t, lc)
c, err := lc.ListenPacket(context.Background(), proto, addr)

View File

@ -24,7 +24,7 @@ func TestForwardMetricsListener_integration_request(t *testing.T) {
Address: netip.MustParseAddrPort(addr),
Network: forward.NetworkAny,
MetricsListener: prometheus.NewForwardMetricsListener(0),
}, true)
})
// Prepare a test DNS message and call the handler's ServeDNS function.
// It will then call the metrics listener and prom metrics should be

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