mirror of
https://github.com/AdguardTeam/AdGuardDNS.git
synced 2025-02-20 11:23:36 +08:00
Sync v2.2.0
This commit is contained in:
parent
17d0b4d513
commit
16fd7a2fd0
2
.gitignore
vendored
2
.gitignore
vendored
@ -14,7 +14,7 @@
|
|||||||
/github-mirror/
|
/github-mirror/
|
||||||
AdGuardDNS
|
AdGuardDNS
|
||||||
asn.mmdb
|
asn.mmdb
|
||||||
config.yml
|
config.yaml
|
||||||
country.mmdb
|
country.mmdb
|
||||||
dnsdb.bolt
|
dnsdb.bolt
|
||||||
querylog.jsonl
|
querylog.jsonl
|
||||||
|
115
CHANGELOG.md
115
CHANGELOG.md
@ -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
|
## AGDNS-916 / Build 456
|
||||||
|
|
||||||
* `ratelimit` now defines rate of requests per second for IPv4 and IPv6
|
* `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
|
## 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.
|
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
|
## AGDNS-891 / Build 371
|
||||||
|
|
||||||
* The property `server` of `upstream` object has been changed. Now it
|
* 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.
|
in `ip:port` format.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -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/).
|
||||||
|
3
Makefile
3
Makefile
@ -59,6 +59,9 @@ go-gen:
|
|||||||
cd ./internal/agd/ && "$(GO.MACRO)" run ./country_generate.go
|
cd ./internal/agd/ && "$(GO.MACRO)" run ./country_generate.go
|
||||||
cd ./internal/geoip/ && "$(GO.MACRO)" run ./asntops_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
|
go-check: go-tools go-lint go-test
|
||||||
|
|
||||||
# A quick check to make sure that all operating systems relevant to the
|
# A quick check to make sure that all operating systems relevant to the
|
||||||
|
@ -86,7 +86,7 @@ following features:
|
|||||||
you need that.
|
you need that.
|
||||||
|
|
||||||
[rules system]: https://adguard-dns.io/kb/general/dns-filtering-syntax/
|
[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/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,6 +43,15 @@ ratelimit:
|
|||||||
# Time between two updates of allow list.
|
# Time between two updates of allow list.
|
||||||
refresh_interval: 1h
|
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.
|
# DNS cache configuration.
|
||||||
cache:
|
cache:
|
||||||
# The type of cache to use. Can be 'simple' (a simple LRU cache) or 'ecs'
|
# 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_private_relay: false
|
||||||
block_firefox_canary: true
|
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 and servers.
|
||||||
server_groups:
|
server_groups:
|
||||||
- name: 'adguard_dns_default'
|
- name: 'adguard_dns_default'
|
||||||
@ -302,8 +326,13 @@ server_groups:
|
|||||||
# See README for the list of protocol values.
|
# See README for the list of protocol values.
|
||||||
protocol: 'dns'
|
protocol: 'dns'
|
||||||
linked_ip_enabled: true
|
linked_ip_enabled: true
|
||||||
bind_addresses:
|
# Either bind_interfaces or bind_addresses (see below) can be used for
|
||||||
- '127.0.0.1:53'
|
# 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'
|
- name: 'default_dot'
|
||||||
protocol: 'tls'
|
protocol: 'tls'
|
||||||
linked_ip_enabled: false
|
linked_ip_enabled: false
|
||||||
@ -351,3 +380,12 @@ connectivity_check:
|
|||||||
# Additional information to be exposed through metrics.
|
# Additional information to be exposed through metrics.
|
||||||
additional_metrics_info:
|
additional_metrics_info:
|
||||||
test_key: 'test_value'
|
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
|
@ -6,9 +6,12 @@ configuration file with comments.
|
|||||||
|
|
||||||
## Contents
|
## Contents
|
||||||
|
|
||||||
* [Recommended values](#recommended)
|
* [Recommended values and notes](#recommended)
|
||||||
* [Result cache sizes](#recommended-result_cache)
|
* [Result cache sizes](#recommended-result_cache)
|
||||||
|
* [`SO_RCVBUF` and `SO_SNDBUF` on Linux](#recommended-buffers)
|
||||||
|
* [Connection limiter](#recommended-connection_limit)
|
||||||
* [Rate limiting](#ratelimit)
|
* [Rate limiting](#ratelimit)
|
||||||
|
* [Stream connection limit](#ratelimit-connection_limit)
|
||||||
* [Cache](#cache)
|
* [Cache](#cache)
|
||||||
* [Upstream](#upstream)
|
* [Upstream](#upstream)
|
||||||
* [Healthcheck](#upstream-healthcheck)
|
* [Healthcheck](#upstream-healthcheck)
|
||||||
@ -21,11 +24,13 @@ configuration file with comments.
|
|||||||
* [Adult-content blocking](#adult_blocking)
|
* [Adult-content blocking](#adult_blocking)
|
||||||
* [Filters](#filters)
|
* [Filters](#filters)
|
||||||
* [Filtering groups](#filtering_groups)
|
* [Filtering groups](#filtering_groups)
|
||||||
|
* [Network interface listeners](#interface_listeners)
|
||||||
* [Server groups](#server_groups)
|
* [Server groups](#server_groups)
|
||||||
* [TLS](#server_groups-*-tls)
|
* [TLS](#server_groups-*-tls)
|
||||||
* [DDR](#server_groups-*-ddr)
|
* [DDR](#server_groups-*-ddr)
|
||||||
* [Servers](#server_groups-*-servers-*)
|
* [Servers](#server_groups-*-servers-*)
|
||||||
* [Connectivity check](#connectivity-check)
|
* [Connectivity check](#connectivity-check)
|
||||||
|
* [Network settings](#network)
|
||||||
* [Additional metrics information](#additional_metrics_info)
|
* [Additional metrics information](#additional_metrics_info)
|
||||||
|
|
||||||
[dist]: ../config.dist.yml
|
[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>
|
### <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>
|
## <a href="#ratelimit" id="ratelimit" name="ratelimit">Rate limiting</a>
|
||||||
|
|
||||||
The `ratelimit` object has the following properties:
|
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
|
(one above `rps`) every second for 10 seconds within one minute, the client is
|
||||||
blocked for `back_off_duration`.
|
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
|
[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>
|
## <a href="#server_groups" id="server_groups" name="server_groups">Server groups</a>
|
||||||
|
|
||||||
The items of the `server_groups` array have the following properties:
|
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`.
|
**Example:** `true`.
|
||||||
|
|
||||||
* <a href="#sg-s-*-bind_addresses" id="sg-s-*-bind_addresses" name="sg-s-*-bind_addresses">`bind_addresses`</a>:
|
* <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]`.
|
**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>:
|
* <a href="#sg-s-*-dnscrypt" id="sg-s-*-dnscrypt" name="sg-s-*-dnscrypt">`dnscrypt`</a>:
|
||||||
The optional DNSCrypt configuration object. It has the following
|
The optional DNSCrypt configuration object. It has the following
|
||||||
properties:
|
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>
|
## <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
|
The `additional_metrics_info` object is a map of strings with extra information
|
||||||
|
@ -87,6 +87,17 @@ In the `ADDITIONAL SECTION`, the following debug information is returned:
|
|||||||
```none
|
```none
|
||||||
asn.adguard-dns.com. 10 CH TXT "1234"
|
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
|
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
|
prefix depends on whether the filtering was applied to the request or the
|
||||||
|
@ -68,10 +68,26 @@ This is not an extensive list. See `../Makefile`.
|
|||||||
</dd>
|
</dd>
|
||||||
<dt><code>make go-gen</code></dt>
|
<dt><code>make go-gen</code></dt>
|
||||||
<dd>
|
<dd>
|
||||||
Regenerate the automatically generated Go files. Those generated files
|
<p>
|
||||||
are <code>../internal/agd/country_generate.go</code> and
|
Regenerate the automatically generated Go files that need to be
|
||||||
<code>../internal/geoip/asntops_generate.go</code>. They need to be
|
periodically updated. Those generated files are:
|
||||||
periodically updated.
|
</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>
|
</dd>
|
||||||
<dt><code>make go-lint</code></dt>
|
<dt><code>make go-lint</code></dt>
|
||||||
<dd>
|
<dd>
|
||||||
@ -158,7 +174,7 @@ dnscrypt generate -p testdns -o ./dnscrypt.yml
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd ../
|
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/
|
rm -f -r ./test/cache/
|
||||||
mkdir ./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-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
|
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].
|
See the [external HTTP API documentation][externalhttp].
|
||||||
|
|
||||||
You may need to change the listen ports in `config.yml` which are less than 1024
|
You may need to change the listen ports in `config.yaml` which are less than
|
||||||
to some other ports. Otherwise, `sudo` or `doas` is required to run
|
1024 to some other ports. Otherwise, `sudo` or `doas` is required to run
|
||||||
`AdGuardDNS`.
|
`AdGuardDNS`.
|
||||||
|
|
||||||
Examples below are for the configuration with the following changes:
|
Examples below are for the configuration with the following changes:
|
||||||
@ -224,14 +241,14 @@ env \
|
|||||||
BACKEND_ENDPOINT='PUT BACKEND URL HERE' \
|
BACKEND_ENDPOINT='PUT BACKEND URL HERE' \
|
||||||
BLOCKED_SERVICE_INDEX_URL='https://atropnikov.github.io/HostlistsRegistry/assets/services.json'\
|
BLOCKED_SERVICE_INDEX_URL='https://atropnikov.github.io/HostlistsRegistry/assets/services.json'\
|
||||||
CONSUL_ALLOWLIST_URL='PUT CONSUL ALLOWLIST URL HERE' \
|
CONSUL_ALLOWLIST_URL='PUT CONSUL ALLOWLIST URL HERE' \
|
||||||
CONFIG_PATH='./config.yml' \
|
CONFIG_PATH='./config.yaml' \
|
||||||
DNSDB_PATH='./test/cache/dnsdb.bolt' \
|
DNSDB_PATH='./test/cache/dnsdb.bolt' \
|
||||||
FILTER_INDEX_URL='https://atropnikov.github.io/HostlistsRegistry/assets/filters.json' \
|
FILTER_INDEX_URL='https://atropnikov.github.io/HostlistsRegistry/assets/filters.json' \
|
||||||
FILTER_CACHE_PATH='./test/cache' \
|
FILTER_CACHE_PATH='./test/cache' \
|
||||||
PROFILES_CACHE_PATH='./test/profilecache.json' \
|
PROFILES_CACHE_PATH='./test/profilecache.json' \
|
||||||
GENERAL_SAFE_SEARCH_URL='https://adguardteam.github.io/HostlistsRegistry/assets/engines_safe_search.txt' \
|
GENERAL_SAFE_SEARCH_URL='https://adguardteam.github.io/HostlistsRegistry/assets/engines_safe_search.txt' \
|
||||||
GEOIP_ASN_PATH='./test/GeoLite2-ASN-Test.mmdb' \
|
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' \
|
QUERYLOG_PATH='./test/cache/querylog.jsonl' \
|
||||||
LISTEN_ADDR='127.0.0.1' \
|
LISTEN_ADDR='127.0.0.1' \
|
||||||
LISTEN_PORT='8081' \
|
LISTEN_PORT='8081' \
|
||||||
|
@ -59,7 +59,7 @@ requirements section][ext-blocked] on the expected format of the response.
|
|||||||
|
|
||||||
The path to the configuration file.
|
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>
|
## <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
|
The path to the profile cache file:
|
||||||
later updated on every [full refresh][conf-backend-full_refresh_interval].
|
|
||||||
|
* `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`.
|
**Default:** `./profilecache.json`.
|
||||||
|
|
||||||
|
12
go.mod
12
go.mod
@ -4,7 +4,7 @@ go 1.20
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AdguardTeam/AdGuardDNS/internal/dnsserver v0.100.0
|
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/AdguardTeam/urlfilter v0.16.1
|
||||||
github.com/ameshkov/dnscrypt/v2 v2.2.5
|
github.com/ameshkov/dnscrypt/v2 v2.2.5
|
||||||
github.com/axiomhq/hyperloglog v0.0.0-20230201085229-3ddf4bad03dc
|
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_golang v1.14.0
|
||||||
github.com/prometheus/client_model v0.3.0
|
github.com/prometheus/client_model v0.3.0
|
||||||
github.com/prometheus/common v0.41.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
|
github.com/stretchr/testify v1.8.2
|
||||||
go.etcd.io/bbolt v1.3.7
|
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/net v0.8.0
|
||||||
golang.org/x/sys v0.6.0
|
golang.org/x/sys v0.6.0
|
||||||
golang.org/x/time v0.3.0
|
golang.org/x/time v0.3.0
|
||||||
|
google.golang.org/protobuf v1.30.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -47,13 +48,12 @@ require (
|
|||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/procfs v0.9.0 // indirect
|
github.com/prometheus/procfs v0.9.0 // indirect
|
||||||
github.com/quic-go/qpack v0.4.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-19 v0.3.2 // indirect
|
||||||
github.com/quic-go/qtls-go1-20 v0.1.1 // indirect
|
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
|
||||||
golang.org/x/crypto v0.7.0 // indirect
|
golang.org/x/crypto v0.7.0 // indirect
|
||||||
golang.org/x/mod v0.9.0 // indirect
|
golang.org/x/mod v0.9.0 // indirect
|
||||||
golang.org/x/text v0.8.0 // indirect
|
golang.org/x/text v0.8.0 // indirect
|
||||||
golang.org/x/tools v0.7.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
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
24
go.sum
24
go.sum
@ -1,7 +1,7 @@
|
|||||||
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
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.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
|
||||||
github.com/AdguardTeam/golibs v0.12.1 h1:bJfFzCnUCl+QsP6prUltM2Sjt0fTiDBPlxuAwfKP3g8=
|
github.com/AdguardTeam/golibs v0.13.2 h1:BPASsyQKmb+b8VnvsNOHp7bKfcZl9Z+Z2UhPjOiupSc=
|
||||||
github.com/AdguardTeam/golibs v0.12.1/go.mod h1:rIglKDHdLvFT1UbhumBLHO9S4cvWS9MEyT1njommI/Y=
|
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/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
|
||||||
github.com/AdguardTeam/urlfilter v0.16.1 h1:ZPi0rjqo8cQf2FVdzo6cqumNoHZx2KPXj2yZa1A5BBw=
|
github.com/AdguardTeam/urlfilter v0.16.1 h1:ZPi0rjqo8cQf2FVdzo6cqumNoHZx2KPXj2yZa1A5BBw=
|
||||||
github.com/AdguardTeam/urlfilter v0.16.1/go.mod h1:46YZDOV1+qtdRDuhZKVPSSp7JWWes0KayqHrKAFBdEI=
|
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/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 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
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.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
|
||||||
github.com/quic-go/qtls-go1-19 v0.2.1/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
|
github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
|
||||||
github.com/quic-go/qtls-go1-20 v0.1.1 h1:KbChDlg82d3IHqaj2bn6GfKRj84Per2VGf5XV3wSwQk=
|
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
|
||||||
github.com/quic-go/qtls-go1-20 v0.1.1/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
|
github.com/quic-go/qtls-go1-20 v0.2.2/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.35.1 h1:b0kzj6b/cQAf05cT0CkQubHM31wiA+xH3IBkxP62poo=
|
||||||
github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA=
|
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 h1:nKct+uP0TV8DjjNiHanKf8SAuub+GNsbrOtM9Nl9biA=
|
||||||
github.com/shirou/gopsutil/v3 v3.21.8/go.mod h1:YWp/H8Qs5fVmf17v7JNZzA0mPJ+mS2e9JdiUF9LlKzQ=
|
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=
|
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.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
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-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
|
||||||
golang.org/x/exp v0.0.0-20230307190834-24139beb5833/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
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.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 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
||||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
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=
|
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-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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
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 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-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
178
go.work.sum
178
go.work.sum
@ -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 v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8=
|
||||||
cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA=
|
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/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ=
|
||||||
cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU=
|
cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU=
|
||||||
cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA=
|
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 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/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 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 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 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 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/golibs v0.10.7/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
|
||||||
github.com/AdguardTeam/gomitmproxy v0.2.0 h1:rvCOf17pd1/CnMyMQW891zrEiIQBpQ8cIGjKN9pinUU=
|
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 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/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
|
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=
|
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/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
|
||||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
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 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/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 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 h1:D21IyuvjDCshj1/qq+pCNd3VZOAEI9jy6Bi131YlXgI=
|
||||||
|
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
|
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
|
||||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
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 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.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 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 h1:lSwwFrbNviGePhkewF1az4oLmcwqCZijQ2/Wi3BGHAI=
|
||||||
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
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 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 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 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU=
|
||||||
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q=
|
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 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/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 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||||
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o=
|
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4 h1:rEvIZUSZ3fx39WIi3JkQqQBitGwpELBIYWeBVh6wn+E=
|
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/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
|
||||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||||
github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw=
|
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 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 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/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 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo=
|
||||||
github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0=
|
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 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-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/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 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 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 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-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I=
|
||||||
github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk=
|
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/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||||
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
|
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 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-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 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/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 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/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/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.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.5.9/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 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 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/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 h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
||||||
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/martian/v3 v3.0.0 h1:pMen7vLs8nvgEYhywH3KDWJIJTeEr2ULsVWHWYHQyBs=
|
github.com/google/martian/v3 v3.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-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/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 h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU=
|
||||||
|
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
|
github.com/googleapis/gax-go/v2 v2.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 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
github.com/gorilla/css v1.0.0 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 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 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/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
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-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 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 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-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/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/jade v1.1.4 h1:WoYdfyJFfZIUgqNAeOyRfTNQZOksSlZ6+FnXR3AEpX0=
|
||||||
github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw=
|
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 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.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||||
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
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/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/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
|
||||||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||||
github.com/kataras/blocks v0.0.7 h1:cF3RDY/vxnSRezc7vLFlQFTYXG/yAr1o7WImJuZbzC4=
|
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/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY=
|
||||||
github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA=
|
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 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/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
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/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 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.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
|
||||||
github.com/kr/pty v1.1.3 h1:/Um6a/ZmD5tF7peoOJ5oN5KMQ0DrGVQSXLNwyckutPk=
|
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/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY=
|
||||||
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
|
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
|
||||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
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.25.0/go.mod h1:YtzP8bxRVCBlO77yRanE264+fY/T2U9ZlW1AaHOsMOg=
|
||||||
github.com/lucas-clemente/quic-go v0.27.1/go.mod h1:AzgQoPda7N+3IqMMMkywBKggIFo2KT6pfnlrQ2QieeI=
|
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 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/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 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/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-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=
|
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/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-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
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 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/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
|
||||||
github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
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 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/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
|
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 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 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.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM=
|
||||||
github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
|
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 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/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/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
|
||||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||||
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
|
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
||||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
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 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 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/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
|
||||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
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.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
|
||||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
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 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
|
||||||
|
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||||
github.com/tdewolff/minify/v2 v2.12.4 h1:kejsHQMM17n6/gwdw53qsi6lg0TGddZADVyQOz1KMdE=
|
github.com/tdewolff/minify/v2 v2.12.4 h1:kejsHQMM17n6/gwdw53qsi6lg0TGddZADVyQOz1KMdE=
|
||||||
github.com/tdewolff/parse/v2 v2.6.4 h1:KCkDvNUMof10e3QExio9OPZJT8SbdKojLBumw8YZycQ=
|
github.com/tdewolff/parse/v2 v2.6.4 h1:KCkDvNUMof10e3QExio9OPZJT8SbdKojLBumw8YZycQ=
|
||||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
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/fasthttp v1.40.0 h1:CRq/00MfruPGFLTQKY8b+8SfdK60TxNztjRMnH0t1Yc=
|
||||||
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
|
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 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 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/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
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/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.1 h1:/vn0k+RBvwlxEmP5E7SZMqNxPhfMVFEJiykr15/0XKM=
|
||||||
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
|
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=
|
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 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 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-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.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-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/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/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/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/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
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-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-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-20220516155154-20f960328961/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-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/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 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/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-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-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 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
|
||||||
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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.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.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||||
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
|
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 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
||||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
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/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.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.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-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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/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/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/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/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
|
||||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
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/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
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 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/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 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=
|
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/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
|
||||||
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
|
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
|
||||||
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
|
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 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 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c=
|
||||||
|
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
package agd_test
|
package agd_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,12 +15,3 @@ func TestMain(m *testing.M) {
|
|||||||
|
|
||||||
// testTimeout is the timeout for common test operations.
|
// testTimeout is the timeout for common test operations.
|
||||||
const testTimeout = 1 * time.Second
|
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})
|
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
|
||||||
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
@ -22,7 +23,7 @@ func main() {
|
|||||||
req, err := http.NewRequest(http.MethodGet, csvURL, nil)
|
req, err := http.NewRequest(http.MethodGet, csvURL, nil)
|
||||||
check(err)
|
check(err)
|
||||||
|
|
||||||
req.Header.Add("User-Agent", agdhttp.UserAgent())
|
req.Header.Add(httphdr.UserAgent, agdhttp.UserAgent())
|
||||||
|
|
||||||
resp, err := c.Do(req)
|
resp, err := c.Do(req)
|
||||||
check(err)
|
check(err)
|
||||||
|
@ -12,17 +12,25 @@ import (
|
|||||||
// Devices
|
// Devices
|
||||||
|
|
||||||
// Device is a device of a device attached to a profile.
|
// 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 {
|
type Device struct {
|
||||||
// ID is the unique ID of the device.
|
// ID is the unique ID of the device.
|
||||||
ID DeviceID
|
ID DeviceID
|
||||||
|
|
||||||
// LinkedIP, when non-nil, allows AdGuard DNS to identify a device by its IP
|
// LinkedIP, when non-empty, allows AdGuard DNS to identify a device by its
|
||||||
// address when it can only use plain DNS.
|
// IP address when it can only use plain DNS.
|
||||||
LinkedIP *netip.Addr
|
LinkedIP netip.Addr
|
||||||
|
|
||||||
// Name is the human-readable name of the device.
|
// Name is the human-readable name of the device.
|
||||||
Name DeviceName
|
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
|
// FilteringEnabled defines whether queries from the device should be
|
||||||
// filtered in any way at all.
|
// filtered in any way at all.
|
||||||
FilteringEnabled bool
|
FilteringEnabled bool
|
||||||
|
@ -25,53 +25,6 @@ func (err *ArgumentError) Error() (msg string) {
|
|||||||
return fmt.Sprintf("argument %s is invalid: %s", err.Name, err.Message)
|
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
|
// NotACountryError is returned from NewCountry when the string doesn't represent
|
||||||
// a valid country.
|
// a valid country.
|
||||||
type NotACountryError struct {
|
type NotACountryError struct {
|
||||||
|
@ -4,9 +4,19 @@ package agd
|
|||||||
|
|
||||||
// Location represents the GeoIP location data about an IP address.
|
// Location represents the GeoIP location data about an IP address.
|
||||||
type Location struct {
|
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
|
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.
|
// ASN is the autonomous system number of an IP address.
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardDNS/internal/agdtime"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
)
|
)
|
||||||
@ -15,8 +16,8 @@ import (
|
|||||||
// the infrastructure, a profile is also called a “DNS server”. We call it
|
// the infrastructure, a profile is also called a “DNS server”. We call it
|
||||||
// profile, because it's less confusing.
|
// profile, because it's less confusing.
|
||||||
//
|
//
|
||||||
// NOTE: Increment [defaultProfileDBCacheVersion] on any change of this
|
// NOTE: Do not change fields of this structure without incrementing
|
||||||
// structure.
|
// [internal/profiledb/internal.FileCacheVersion].
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Consider making it closer to the config file and the backend
|
// 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
|
// response by grouping parental, rule list, and safe browsing settings into
|
||||||
@ -24,63 +25,107 @@ import (
|
|||||||
type Profile struct {
|
type Profile struct {
|
||||||
// Parental are the parental settings for this profile. They are ignored if
|
// Parental are the parental settings for this profile. They are ignored if
|
||||||
// FilteringEnabled is set to false.
|
// FilteringEnabled is set to false.
|
||||||
|
//
|
||||||
|
// NOTE: Do not change fields of this structure without incrementing
|
||||||
|
// [internal/profiledb/internal.FileCacheVersion].
|
||||||
Parental *ParentalProtectionSettings
|
Parental *ParentalProtectionSettings
|
||||||
|
|
||||||
// BlockingMode defines the way blocked responses are constructed.
|
// 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
|
BlockingMode dnsmsg.BlockingModeCodec
|
||||||
|
|
||||||
// ID is the unique ID of this profile.
|
// ID is the unique ID of this profile.
|
||||||
|
//
|
||||||
|
// NOTE: Do not change fields of this structure without incrementing
|
||||||
|
// [internal/profiledb/internal.FileCacheVersion].
|
||||||
ID ProfileID
|
ID ProfileID
|
||||||
|
|
||||||
// UpdateTime shows the last time this profile was updated from the backend.
|
// 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
|
// This is NOT the time of update in the backend's database, since the
|
||||||
// backend doesn't send this information.
|
// backend doesn't send this information.
|
||||||
|
//
|
||||||
|
// NOTE: Do not change fields of this structure without incrementing
|
||||||
|
// [internal/profiledb/internal.FileCacheVersion].
|
||||||
UpdateTime time.Time
|
UpdateTime time.Time
|
||||||
|
|
||||||
// Devices are the devices attached to this profile. Every element of the
|
// DeviceIDs are the IDs of devices attached to this profile.
|
||||||
// slice must be non-nil.
|
//
|
||||||
Devices []*Device
|
// 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
|
// RuleListIDs are the IDs of the filtering rule lists enabled for this
|
||||||
// profile. They are ignored if FilteringEnabled or RuleListsEnabled are
|
// profile. They are ignored if FilteringEnabled or RuleListsEnabled are
|
||||||
// set to false.
|
// set to false.
|
||||||
|
//
|
||||||
|
// NOTE: Do not change fields of this structure without incrementing
|
||||||
|
// [internal/profiledb/internal.FileCacheVersion].
|
||||||
RuleListIDs []FilterListID
|
RuleListIDs []FilterListID
|
||||||
|
|
||||||
// CustomRules are the custom filtering rules for this profile. They are
|
// CustomRules are the custom filtering rules for this profile. They are
|
||||||
// ignored if RuleListsEnabled is set to false.
|
// ignored if RuleListsEnabled is set to false.
|
||||||
|
//
|
||||||
|
// NOTE: Do not change fields of this structure without incrementing
|
||||||
|
// [internal/profiledb/internal.FileCacheVersion].
|
||||||
CustomRules []FilterRuleText
|
CustomRules []FilterRuleText
|
||||||
|
|
||||||
// FilteredResponseTTL is the time-to-live value used for responses sent to
|
// FilteredResponseTTL is the time-to-live value used for responses sent to
|
||||||
// the devices of this profile.
|
// the devices of this profile.
|
||||||
|
//
|
||||||
|
// NOTE: Do not change fields of this structure without incrementing
|
||||||
|
// [internal/profiledb/internal.FileCacheVersion].
|
||||||
FilteredResponseTTL time.Duration
|
FilteredResponseTTL time.Duration
|
||||||
|
|
||||||
// FilteringEnabled defines whether queries from devices of this profile
|
// FilteringEnabled defines whether queries from devices of this profile
|
||||||
// should be filtered in any way at all.
|
// should be filtered in any way at all.
|
||||||
|
//
|
||||||
|
// NOTE: Do not change fields of this structure without incrementing
|
||||||
|
// [internal/profiledb/internal.FileCacheVersion].
|
||||||
FilteringEnabled bool
|
FilteringEnabled bool
|
||||||
|
|
||||||
// SafeBrowsingEnabled defines whether queries from devices of this profile
|
// SafeBrowsingEnabled defines whether queries from devices of this profile
|
||||||
// should be filtered using the safe browsing filter. Requires
|
// should be filtered using the safe browsing filter. Requires
|
||||||
// FilteringEnabled to be set to true.
|
// FilteringEnabled to be set to true.
|
||||||
|
//
|
||||||
|
// NOTE: Do not change fields of this structure without incrementing
|
||||||
|
// [internal/profiledb/internal.FileCacheVersion].
|
||||||
SafeBrowsingEnabled bool
|
SafeBrowsingEnabled bool
|
||||||
|
|
||||||
// RuleListsEnabled defines whether queries from devices of this profile
|
// RuleListsEnabled defines whether queries from devices of this profile
|
||||||
// should be filtered using the filtering rule lists in RuleListIDs.
|
// should be filtered using the filtering rule lists in RuleListIDs.
|
||||||
// Requires FilteringEnabled to be set to true.
|
// Requires FilteringEnabled to be set to true.
|
||||||
|
//
|
||||||
|
// NOTE: Do not change fields of this structure without incrementing
|
||||||
|
// [internal/profiledb/internal.FileCacheVersion].
|
||||||
RuleListsEnabled bool
|
RuleListsEnabled bool
|
||||||
|
|
||||||
// QueryLogEnabled defines whether query logs should be saved for the
|
// QueryLogEnabled defines whether query logs should be saved for the
|
||||||
// devices of this profile.
|
// devices of this profile.
|
||||||
|
//
|
||||||
|
// NOTE: Do not change fields of this structure without incrementing
|
||||||
|
// [internal/profiledb/internal.FileCacheVersion].
|
||||||
QueryLogEnabled bool
|
QueryLogEnabled bool
|
||||||
|
|
||||||
// Deleted shows if this profile is deleted.
|
// Deleted shows if this profile is deleted.
|
||||||
|
//
|
||||||
|
// NOTE: Do not change fields of this structure without incrementing
|
||||||
|
// [internal/profiledb/internal.FileCacheVersion].
|
||||||
Deleted bool
|
Deleted bool
|
||||||
|
|
||||||
// BlockPrivateRelay shows if Apple Private Relay queries are blocked for
|
// BlockPrivateRelay shows if Apple Private Relay queries are blocked for
|
||||||
// requests from all devices in this profile.
|
// requests from all devices in this profile.
|
||||||
|
//
|
||||||
|
// NOTE: Do not change fields of this structure without incrementing
|
||||||
|
// [internal/profiledb/internal.FileCacheVersion].
|
||||||
BlockPrivateRelay bool
|
BlockPrivateRelay bool
|
||||||
|
|
||||||
// BlockFirefoxCanary shows if Firefox canary domain is blocked for
|
// BlockFirefoxCanary shows if Firefox canary domain is blocked for
|
||||||
// requests from all devices in this profile.
|
// requests from all devices in this profile.
|
||||||
|
//
|
||||||
|
// NOTE: Do not change fields of this structure without incrementing
|
||||||
|
// [internal/profiledb/internal.FileCacheVersion].
|
||||||
BlockFirefoxCanary bool
|
BlockFirefoxCanary bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,23 +208,26 @@ type WeeklySchedule [7]DayRange
|
|||||||
|
|
||||||
// ParentalProtectionSchedule is the schedule of a client's parental protection.
|
// ParentalProtectionSchedule is the schedule of a client's parental protection.
|
||||||
// All fields must not be nil.
|
// All fields must not be nil.
|
||||||
|
//
|
||||||
|
// NOTE: Do not change fields of this structure without incrementing
|
||||||
|
// [internal/profiledb/internal.FileCacheVersion].
|
||||||
type ParentalProtectionSchedule struct {
|
type ParentalProtectionSchedule struct {
|
||||||
// Week is the parental protection schedule for every day of the week.
|
// Week is the parental protection schedule for every day of the week.
|
||||||
Week *WeeklySchedule
|
Week *WeeklySchedule
|
||||||
|
|
||||||
// TimeZone is the profile's time zone.
|
// TimeZone is the profile's time zone.
|
||||||
TimeZone *time.Location
|
TimeZone *agdtime.Location
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contains returns true if t is within the allowed schedule.
|
// Contains returns true if t is within the allowed schedule.
|
||||||
func (s *ParentalProtectionSchedule) Contains(t time.Time) (ok bool) {
|
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())]
|
r := s.Week[int(t.Weekday())]
|
||||||
if r.IsZeroLength() {
|
if r.IsZeroLength() {
|
||||||
return false
|
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)
|
start := day.Add(time.Duration(r.Start) * time.Minute)
|
||||||
end := day.Add(time.Duration(r.End+1)*time.Minute - 1*time.Nanosecond)
|
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.
|
// 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 {
|
type ParentalProtectionSettings struct {
|
||||||
Schedule *ParentalProtectionSchedule
|
Schedule *ParentalProtectionSchedule
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||||
|
"github.com/AdguardTeam/AdGuardDNS/internal/agdtime"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -78,7 +79,7 @@ func TestParentalProtectionSchedule_Contains(t *testing.T) {
|
|||||||
|
|
||||||
time.Saturday: agd.ZeroLengthDayRange(),
|
time.Saturday: agd.ZeroLengthDayRange(),
|
||||||
},
|
},
|
||||||
TimeZone: time.UTC,
|
TimeZone: agdtime.UTC(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// allDaySchedule, 00:00:00 to 23:59:59.
|
// allDaySchedule, 00:00:00 to 23:59:59.
|
||||||
@ -95,7 +96,7 @@ func TestParentalProtectionSchedule_Contains(t *testing.T) {
|
|||||||
|
|
||||||
time.Saturday: agd.ZeroLengthDayRange(),
|
time.Saturday: agd.ZeroLengthDayRange(),
|
||||||
},
|
},
|
||||||
TimeZone: time.UTC,
|
TimeZone: agdtime.UTC(),
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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))
|
|
||||||
})
|
|
||||||
}
|
|
@ -102,8 +102,6 @@ type Server struct {
|
|||||||
// ServerBindData are the socket binding data for a server. Either AddrPort or
|
// ServerBindData are the socket binding data for a server. Either AddrPort or
|
||||||
// ListenConfig with Address must be set.
|
// 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 turning this into a sum type.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Consider renaming this and the one in websvc to something
|
// TODO(a.garipov): Consider renaming this and the one in websvc to something
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -12,20 +12,6 @@ import (
|
|||||||
|
|
||||||
// Common Constants, Functions And Types
|
// 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.
|
// HTTP header value constants.
|
||||||
const (
|
const (
|
||||||
HdrValApplicationJSON = "application/json"
|
HdrValApplicationJSON = "application/json"
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||||
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client is a wrapper around http.Client.
|
// Client is a wrapper around http.Client.
|
||||||
@ -85,15 +86,15 @@ func (c *Client) do(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if contentType != "" {
|
if contentType != "" {
|
||||||
req.Header.Set(HdrNameContentType, contentType)
|
req.Header.Set(httphdr.ContentType, contentType)
|
||||||
}
|
}
|
||||||
|
|
||||||
reqID, ok := agd.RequestIDFromContext(ctx)
|
reqID, ok := agd.RequestIDFromContext(ctx)
|
||||||
if ok {
|
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)
|
resp, err = c.http.Do(req)
|
||||||
if err != nil && resp != nil && resp.Header != nil {
|
if err != nil && resp != nil && resp.Header != nil {
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Common HTTP Errors
|
// Common HTTP Errors
|
||||||
@ -40,7 +41,7 @@ func CheckStatus(resp *http.Response, expected int) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &StatusError{
|
return &StatusError{
|
||||||
ServerName: resp.Header.Get(HdrNameServer),
|
ServerName: resp.Header.Get(httphdr.Server),
|
||||||
Expected: expected,
|
Expected: expected,
|
||||||
Got: resp.StatusCode,
|
Got: resp.StatusCode,
|
||||||
}
|
}
|
||||||
@ -73,6 +74,6 @@ func (err *ServerError) Unwrap() (unwrapped error) {
|
|||||||
func WrapServerError(err error, resp *http.Response) (wrapped *ServerError) {
|
func WrapServerError(err error, resp *http.Response) (wrapped *ServerError) {
|
||||||
return &ServerError{
|
return &ServerError{
|
||||||
Err: err,
|
Err: err,
|
||||||
ServerName: resp.Header.Get(HdrNameServer),
|
ServerName: resp.Header.Get(httphdr.Server),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
|
||||||
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@ -41,7 +42,7 @@ func TestCheckStatus(t *testing.T) {
|
|||||||
resp := &http.Response{
|
resp := &http.Response{
|
||||||
StatusCode: tc.got,
|
StatusCode: tc.got,
|
||||||
Header: http.Header{
|
Header: http.Header{
|
||||||
agdhttp.HdrNameServer: []string{tc.srv},
|
httphdr.Server: []string{tc.srv},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := agdhttp.CheckStatus(resp, tc.exp)
|
err := agdhttp.CheckStatus(resp, tc.exp)
|
||||||
@ -73,7 +74,7 @@ func TestServerError(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
resp := &http.Response{
|
resp := &http.Response{
|
||||||
Header: http.Header{
|
Header: http.Header{
|
||||||
agdhttp.HdrNameServer: []string{tc.srv},
|
httphdr.Server: []string{tc.srv},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := agdhttp.WrapServerError(tc.err, resp)
|
err := agdhttp.WrapServerError(tc.err, resp)
|
||||||
|
@ -7,14 +7,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ExampleAndroidMetricDomainReplacement() {
|
func ExampleAndroidMetricDomainReplacement() {
|
||||||
|
printResult := func(input string) {
|
||||||
|
fmt.Printf("%-42q: %q\n", input, agdnet.AndroidMetricDomainReplacement(input))
|
||||||
|
}
|
||||||
|
|
||||||
anAndroidDomain := "12345678-dnsotls-ds.metric.gstatic.com."
|
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."
|
anAndroidDomain = "123456-dnsohttps-ds.metric.gstatic.com."
|
||||||
fmt.Printf("%-42q: %q\n", anAndroidDomain, agdnet.AndroidMetricDomainReplacement(anAndroidDomain))
|
printResult(anAndroidDomain)
|
||||||
|
|
||||||
notAndroidDomain := "example.com"
|
notAndroidDomain := "example.com"
|
||||||
fmt.Printf("%-42q: %q\n", notAndroidDomain, agdnet.AndroidMetricDomainReplacement(notAndroidDomain))
|
printResult(notAndroidDomain)
|
||||||
|
|
||||||
// Output:
|
// Output:
|
||||||
// "12345678-dnsotls-ds.metric.gstatic.com." : "00000000-dnsotls-ds.metric.gstatic.com."
|
// "12345678-dnsotls-ds.metric.gstatic.com." : "00000000-dnsotls-ds.metric.gstatic.com."
|
||||||
|
@ -10,7 +10,11 @@ import (
|
|||||||
|
|
||||||
// FilteredResponseTTL is the common filtering response TTL for tests. It is
|
// FilteredResponseTTL is the common filtering response TTL for tests. It is
|
||||||
// also used by [NewConstructor].
|
// 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.
|
// NewConstructor returns a standard dnsmsg.Constructor for tests.
|
||||||
func NewConstructor() (c *dnsmsg.Constructor) {
|
func NewConstructor() (c *dnsmsg.Constructor) {
|
||||||
|
@ -11,9 +11,11 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardDNS/internal/billstat"
|
"github.com/AdguardTeam/AdGuardDNS/internal/billstat"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnscheck"
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnscheck"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsdb"
|
"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/dnsserver/ratelimit"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
|
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
|
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
|
||||||
|
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/querylog"
|
"github.com/AdguardTeam/AdGuardDNS/internal/querylog"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/rulestat"
|
"github.com/AdguardTeam/AdGuardDNS/internal/rulestat"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
@ -22,7 +24,144 @@ import (
|
|||||||
|
|
||||||
// Interface Mocks
|
// 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
|
// type check
|
||||||
var _ agd.ErrorCollector = (*ErrorCollector)(nil)
|
var _ agd.ErrorCollector = (*ErrorCollector)(nil)
|
||||||
@ -39,56 +178,6 @@ func (c *ErrorCollector) Collect(ctx context.Context, err error) {
|
|||||||
c.OnCollect(ctx, err)
|
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
|
// type check
|
||||||
var _ agd.Refresher = (*Refresher)(nil)
|
var _ agd.Refresher = (*Refresher)(nil)
|
||||||
|
|
||||||
@ -206,7 +295,7 @@ func (db *DNSDB) Record(ctx context.Context, resp *dns.Msg, ri *agd.RequestInfo)
|
|||||||
// type check
|
// type check
|
||||||
var _ filter.Interface = (*Filter)(nil)
|
var _ filter.Interface = (*Filter)(nil)
|
||||||
|
|
||||||
// Filter is a filter.Interface for tests.
|
// Filter is a [filter.Interface] for tests.
|
||||||
type Filter struct {
|
type Filter struct {
|
||||||
OnFilterRequest func(
|
OnFilterRequest func(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
@ -218,10 +307,9 @@ type Filter struct {
|
|||||||
resp *dns.Msg,
|
resp *dns.Msg,
|
||||||
ri *agd.RequestInfo,
|
ri *agd.RequestInfo,
|
||||||
) (r filter.Result, err error)
|
) (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(
|
func (f *Filter) FilterRequest(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req *dns.Msg,
|
req *dns.Msg,
|
||||||
@ -230,7 +318,7 @@ func (f *Filter) FilterRequest(
|
|||||||
return f.OnFilterRequest(ctx, req, ri)
|
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(
|
func (f *Filter) FilterResponse(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
resp *dns.Msg,
|
resp *dns.Msg,
|
||||||
@ -239,21 +327,36 @@ func (f *Filter) FilterResponse(
|
|||||||
return f.OnFilterResponse(ctx, resp, ri)
|
return f.OnFilterResponse(ctx, resp, ri)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close implements the filter.Interface interface for *Filter.
|
// type check
|
||||||
func (f *Filter) Close() (err error) {
|
var _ filter.HashMatcher = (*HashMatcher)(nil)
|
||||||
return f.OnClose()
|
|
||||||
|
// 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
|
// type check
|
||||||
var _ filter.Storage = (*FilterStorage)(nil)
|
var _ filter.Storage = (*FilterStorage)(nil)
|
||||||
|
|
||||||
// FilterStorage is an filter.Storage for tests.
|
// FilterStorage is a [filter.Storage] for tests.
|
||||||
type FilterStorage struct {
|
type FilterStorage struct {
|
||||||
OnFilterFromContext func(ctx context.Context, ri *agd.RequestInfo) (f filter.Interface)
|
OnFilterFromContext func(ctx context.Context, ri *agd.RequestInfo) (f filter.Interface)
|
||||||
OnHasListID func(id agd.FilterListID) (ok bool)
|
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(
|
func (s *FilterStorage) FilterFromContext(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
ri *agd.RequestInfo,
|
ri *agd.RequestInfo,
|
||||||
@ -261,7 +364,7 @@ func (s *FilterStorage) FilterFromContext(
|
|||||||
return s.OnFilterFromContext(ctx, ri)
|
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) {
|
func (s *FilterStorage) HasListID(id agd.FilterListID) (ok bool) {
|
||||||
return s.OnHasListID(id)
|
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)
|
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
|
// Package querylog
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
@ -327,6 +497,39 @@ func (s *RuleStat) Collect(ctx context.Context, id agd.FilterListID, text agd.Fi
|
|||||||
|
|
||||||
// Module dnsserver
|
// 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
|
// Package ratelimit
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
|
63
internal/agdtime/agdtime.go
Normal file
63
internal/agdtime/agdtime.go
Normal 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
|
||||||
|
}
|
41
internal/agdtime/agdtime_example_test.go
Normal file
41
internal/agdtime/agdtime_example_test.go
Normal 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}
|
||||||
|
}
|
@ -12,10 +12,13 @@ import (
|
|||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
|
||||||
|
"github.com/AdguardTeam/AdGuardDNS/internal/agdtime"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
|
||||||
|
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Profile Storage
|
// 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
|
// that retrieves the profile and device information from the business logic
|
||||||
// backend. It is safe for concurrent use.
|
// backend. It is safe for concurrent use.
|
||||||
//
|
//
|
||||||
@ -61,13 +64,13 @@ type ProfileStorage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// type check
|
// 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(
|
func (s *ProfileStorage) Profiles(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req *agd.PSProfilesRequest,
|
req *profiledb.StorageRequest,
|
||||||
) (resp *agd.PSProfilesResponse, err error) {
|
) (resp *profiledb.StorageResponse, err error) {
|
||||||
q := url.Values{}
|
q := url.Values{}
|
||||||
if !req.SyncTime.IsZero() {
|
if !req.SyncTime.IsZero() {
|
||||||
syncTimeStr := strconv.FormatInt(req.SyncTime.UnixMilli(), 10)
|
syncTimeStr := strconv.FormatInt(req.SyncTime.UnixMilli(), 10)
|
||||||
@ -127,7 +130,12 @@ type v1SettingsRespSchedule struct {
|
|||||||
Friday *[2]timeutil.Duration `json:"fri"`
|
Friday *[2]timeutil.Duration `json:"fri"`
|
||||||
Saturday *[2]timeutil.Duration `json:"sat"`
|
Saturday *[2]timeutil.Duration `json:"sat"`
|
||||||
Sunday *[2]timeutil.Duration `json:"sun"`
|
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
|
// 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
|
// v1SettingsRespDevice is the structure for decoding the settings.devices
|
||||||
// property of the response from the backend.
|
// property of the response from the backend.
|
||||||
type v1SettingsRespDevice struct {
|
type v1SettingsRespDevice struct {
|
||||||
LinkedIP *netip.Addr `json:"linked_ip"`
|
LinkedIP netip.Addr `json:"linked_ip"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
FilteringEnabled bool `json:"filtering_enabled"`
|
DedicatedIPs []netip.Addr `json:"dedicated_ips"`
|
||||||
|
FilteringEnabled bool `json:"filtering_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// v1SettingsRespSettings is the structure for decoding the settings property of
|
// v1SettingsRespSettings is the structure for decoding the settings property of
|
||||||
@ -232,12 +241,12 @@ func (p *v1SettingsRespParental) toInternal(
|
|||||||
sch = &agd.ParentalProtectionSchedule{}
|
sch = &agd.ParentalProtectionSchedule{}
|
||||||
|
|
||||||
// TODO(a.garipov): Cache location lookup results.
|
// TODO(a.garipov): Cache location lookup results.
|
||||||
sch.TimeZone, err = time.LoadLocation(psch.TimeZone)
|
sch.TimeZone, err = agdtime.LoadLocation(psch.TimeZone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Report the error and assume UTC.
|
// Report the error and assume UTC.
|
||||||
reportf(ctx, errColl, "settings at index %d: schedule: time zone: %w", settIdx, err)
|
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{}
|
sch.Week = &agd.WeeklySchedule{}
|
||||||
@ -330,10 +339,10 @@ func devicesToInternal(
|
|||||||
errColl agd.ErrorCollector,
|
errColl agd.ErrorCollector,
|
||||||
settIdx int,
|
settIdx int,
|
||||||
respDevices []*v1SettingsRespDevice,
|
respDevices []*v1SettingsRespDevice,
|
||||||
) (devices []*agd.Device) {
|
) (devices []*agd.Device, ids []agd.DeviceID) {
|
||||||
l := len(respDevices)
|
l := len(respDevices)
|
||||||
if l == 0 {
|
if l == 0 {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
devices = make([]*agd.Device, 0, l)
|
devices = make([]*agd.Device, 0, l)
|
||||||
@ -344,8 +353,11 @@ func devicesToInternal(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Consider validating uniqueness of linked and
|
||||||
|
// dedicated IPs.
|
||||||
dev := &agd.Device{
|
dev := &agd.Device{
|
||||||
LinkedIP: d.LinkedIP,
|
LinkedIP: d.LinkedIP,
|
||||||
|
DedicatedIPs: slices.Clone(d.DedicatedIPs),
|
||||||
FilteringEnabled: d.FilteringEnabled,
|
FilteringEnabled: d.FilteringEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -368,10 +380,11 @@ func devicesToInternal(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ids = append(ids, dev.ID)
|
||||||
devices = append(devices, dev)
|
devices = append(devices, dev)
|
||||||
}
|
}
|
||||||
|
|
||||||
return devices
|
return devices, ids
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterListsToInternal is a helper that converts the filter lists from the
|
// 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
|
// TODO(a.garipov): Here and in other functions, consider just adding the
|
||||||
// error collector to the context.
|
// error collector to the context.
|
||||||
errColl agd.ErrorCollector,
|
errColl agd.ErrorCollector,
|
||||||
) (pr *agd.PSProfilesResponse) {
|
) (pr *profiledb.StorageResponse) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pr = &agd.PSProfilesResponse{
|
pr = &profiledb.StorageResponse{
|
||||||
SyncTime: time.Unix(0, r.SyncTime*1_000_000),
|
SyncTime: time.Unix(0, r.SyncTime*1_000_000),
|
||||||
Profiles: make([]*agd.Profile, 0, len(r.Settings)),
|
Profiles: make([]*agd.Profile, 0, len(r.Settings)),
|
||||||
}
|
}
|
||||||
@ -476,7 +489,7 @@ func (r *v1SettingsResp) toInternal(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
devices := devicesToInternal(ctx, errColl, i, s.Devices)
|
devices, deviceIDs := devicesToInternal(ctx, errColl, i, s.Devices)
|
||||||
rlEnabled, ruleLists := filterListsToInternal(ctx, errColl, i, s.RuleLists)
|
rlEnabled, ruleLists := filterListsToInternal(ctx, errColl, i, s.RuleLists)
|
||||||
rules := rulesToInternal(ctx, errColl, i, s.CustomRules)
|
rules := rulesToInternal(ctx, errColl, i, s.CustomRules)
|
||||||
|
|
||||||
@ -499,12 +512,14 @@ func (r *v1SettingsResp) toInternal(
|
|||||||
|
|
||||||
sbEnabled := s.SafeBrowsing != nil && s.SafeBrowsing.Enabled
|
sbEnabled := s.SafeBrowsing != nil && s.SafeBrowsing.Enabled
|
||||||
|
|
||||||
|
pr.Devices = append(pr.Devices, devices...)
|
||||||
|
|
||||||
pr.Profiles = append(pr.Profiles, &agd.Profile{
|
pr.Profiles = append(pr.Profiles, &agd.Profile{
|
||||||
Parental: parental,
|
Parental: parental,
|
||||||
BlockingMode: s.BlockingMode,
|
BlockingMode: s.BlockingMode,
|
||||||
ID: id,
|
ID: id,
|
||||||
UpdateTime: updTime,
|
UpdateTime: updTime,
|
||||||
Devices: devices,
|
DeviceIDs: deviceIDs,
|
||||||
RuleListIDs: ruleLists,
|
RuleListIDs: ruleLists,
|
||||||
CustomRules: rules,
|
CustomRules: rules,
|
||||||
FilteredResponseTTL: fltRespTTL,
|
FilteredResponseTTL: fltRespTTL,
|
||||||
|
@ -13,8 +13,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||||
|
"github.com/AdguardTeam/AdGuardDNS/internal/agdtime"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/backend"
|
"github.com/AdguardTeam/AdGuardDNS/internal/backend"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
|
||||||
|
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -61,7 +63,7 @@ func TestProfileStorage_Profiles(t *testing.T) {
|
|||||||
require.NotNil(t, ds)
|
require.NotNil(t, ds)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
req := &agd.PSProfilesRequest{
|
req := &profiledb.StorageRequest{
|
||||||
SyncTime: syncTime,
|
SyncTime: syncTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,10 +83,10 @@ func TestProfileStorage_Profiles(t *testing.T) {
|
|||||||
// testProfileResp returns profile resp corresponding with testdata.
|
// testProfileResp returns profile resp corresponding with testdata.
|
||||||
//
|
//
|
||||||
// Keep in sync with the testdata one.
|
// Keep in sync with the testdata one.
|
||||||
func testProfileResp(t *testing.T) *agd.PSProfilesResponse {
|
func testProfileResp(t *testing.T) (resp *profiledb.StorageResponse) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
wantLoc, err := time.LoadLocation("GMT")
|
wantLoc, err := agdtime.LoadLocation("GMT")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
dayRange := agd.DayRange{
|
dayRange := agd.DayRange{
|
||||||
@ -121,7 +123,7 @@ func testProfileResp(t *testing.T) *agd.PSProfilesResponse {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
want := &agd.PSProfilesResponse{
|
want := &profiledb.StorageResponse{
|
||||||
SyncTime: syncTime,
|
SyncTime: syncTime,
|
||||||
Profiles: []*agd.Profile{{
|
Profiles: []*agd.Profile{{
|
||||||
Parental: nil,
|
Parental: nil,
|
||||||
@ -130,15 +132,10 @@ func testProfileResp(t *testing.T) *agd.PSProfilesResponse {
|
|||||||
},
|
},
|
||||||
ID: "37f97ee9",
|
ID: "37f97ee9",
|
||||||
UpdateTime: updTime,
|
UpdateTime: updTime,
|
||||||
Devices: []*agd.Device{{
|
DeviceIDs: []agd.DeviceID{
|
||||||
ID: "118ffe93",
|
"118ffe93",
|
||||||
Name: "Device 1",
|
"b9e1a762",
|
||||||
FilteringEnabled: true,
|
},
|
||||||
}, {
|
|
||||||
ID: "b9e1a762",
|
|
||||||
Name: "Device 2",
|
|
||||||
FilteringEnabled: true,
|
|
||||||
}},
|
|
||||||
RuleListIDs: []agd.FilterListID{"1"},
|
RuleListIDs: []agd.FilterListID{"1"},
|
||||||
CustomRules: nil,
|
CustomRules: nil,
|
||||||
FilteredResponseTTL: 10 * time.Second,
|
FilteredResponseTTL: 10 * time.Second,
|
||||||
@ -154,24 +151,12 @@ func testProfileResp(t *testing.T) *agd.PSProfilesResponse {
|
|||||||
BlockingMode: wantBlockingMode,
|
BlockingMode: wantBlockingMode,
|
||||||
ID: "83f3ea8f",
|
ID: "83f3ea8f",
|
||||||
UpdateTime: updTime,
|
UpdateTime: updTime,
|
||||||
Devices: []*agd.Device{{
|
DeviceIDs: []agd.DeviceID{
|
||||||
ID: "0d7724fa",
|
"0d7724fa",
|
||||||
Name: "Device 1",
|
"6d2ac775",
|
||||||
FilteringEnabled: true,
|
"94d4c481",
|
||||||
}, {
|
"ada436e3",
|
||||||
ID: "6d2ac775",
|
},
|
||||||
Name: "Device 2",
|
|
||||||
FilteringEnabled: true,
|
|
||||||
}, {
|
|
||||||
ID: "94d4c481",
|
|
||||||
Name: "Device 3",
|
|
||||||
FilteringEnabled: true,
|
|
||||||
}, {
|
|
||||||
ID: "ada436e3",
|
|
||||||
LinkedIP: &wantLinkedIP,
|
|
||||||
Name: "Device 4",
|
|
||||||
FilteringEnabled: true,
|
|
||||||
}},
|
|
||||||
RuleListIDs: []agd.FilterListID{"1"},
|
RuleListIDs: []agd.FilterListID{"1"},
|
||||||
CustomRules: []agd.FilterRuleText{"||example.org^"},
|
CustomRules: []agd.FilterRuleText{"||example.org^"},
|
||||||
FilteredResponseTTL: 3600 * time.Second,
|
FilteredResponseTTL: 3600 * time.Second,
|
||||||
@ -183,6 +168,35 @@ func testProfileResp(t *testing.T) *agd.PSProfilesResponse {
|
|||||||
BlockPrivateRelay: false,
|
BlockPrivateRelay: false,
|
||||||
BlockFirefoxCanary: 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
|
return want
|
||||||
|
9
internal/backend/testdata/profiles.json
vendored
9
internal/backend/testdata/profiles.json
vendored
@ -50,18 +50,23 @@
|
|||||||
{
|
{
|
||||||
"id": "6d2ac775",
|
"id": "6d2ac775",
|
||||||
"name": "Device 2",
|
"name": "Device 2",
|
||||||
|
"linked_ip": null,
|
||||||
"filtering_enabled": true
|
"filtering_enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "94d4c481",
|
"id": "94d4c481",
|
||||||
"name": "Device 3",
|
"name": "Device 3",
|
||||||
|
"linked_ip": "",
|
||||||
|
"dedicated_ips": [
|
||||||
|
"1.2.3.4"
|
||||||
|
],
|
||||||
"filtering_enabled": true
|
"filtering_enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ada436e3",
|
"id": "ada436e3",
|
||||||
"name": "Device 4",
|
"name": "Device 4",
|
||||||
"filtering_enabled": true,
|
"linked_ip": "1.2.3.4",
|
||||||
"linked_ip": "1.2.3.4"
|
"filtering_enabled": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parental": {
|
"parental": {
|
||||||
|
@ -1,40 +1,6 @@
|
|||||||
// Package bindtodevice contains an implementation of the [netext.ListenConfig]
|
// Package bindtodevice contains an implementation of the [netext.ListenConfig]
|
||||||
// interface that uses Linux's SO_BINDTODEVICE socket option to be able to bind
|
// interface that uses Linux's SO_BINDTODEVICE socket option to be able to bind
|
||||||
// to a device.
|
// 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
|
package bindtodevice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -2,9 +2,13 @@ package bindtodevice
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Common timeout for tests
|
||||||
|
const testTimeout = 1 * time.Second
|
||||||
|
|
||||||
// Common addresses for tests.
|
// Common addresses for tests.
|
||||||
var (
|
var (
|
||||||
testLAddr = &net.UDPAddr{
|
testLAddr = &net.UDPAddr{
|
||||||
@ -17,5 +21,8 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Common timeout for tests
|
// Common subnets for tests.
|
||||||
const testTimeout = 1 * time.Second
|
var (
|
||||||
|
testSubnetIPv4 = netip.MustParsePrefix("1.2.3.0/24")
|
||||||
|
testSubnetIPv6 = netip.MustParsePrefix("1234:5678::/64")
|
||||||
|
)
|
||||||
|
@ -1,6 +1,16 @@
|
|||||||
package bindtodevice_test
|
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
|
// Common interface listener IDs for tests
|
||||||
const (
|
const (
|
||||||
@ -18,3 +28,6 @@ const (
|
|||||||
|
|
||||||
// testIfaceName is the common network interface name for tests.
|
// testIfaceName is the common network interface name for tests.
|
||||||
const testIfaceName = "not_a_real_iface0"
|
const testIfaceName = "not_a_real_iface0"
|
||||||
|
|
||||||
|
// testSubnetIPv4 is a common subnet for tests.
|
||||||
|
var testSubnetIPv4 = netip.MustParsePrefix("1.2.3.0/24")
|
||||||
|
@ -11,8 +11,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestChanListenConfig(t *testing.T) {
|
func TestChanListenConfig(t *testing.T) {
|
||||||
pc := newChanPacketConn(nil, nil, testLAddr)
|
pc := newChanPacketConn(nil, testSubnetIPv4, nil, testLAddr)
|
||||||
lsnr := newChanListener(nil, testLAddr)
|
lsnr := newChanListener(nil, testSubnetIPv4, testLAddr)
|
||||||
c := chanListenConfig{
|
c := chanListenConfig{
|
||||||
packetConn: pc,
|
packetConn: pc,
|
||||||
listener: lsnr,
|
listener: lsnr,
|
||||||
|
@ -4,6 +4,7 @@ package bindtodevice
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -13,17 +14,22 @@ import (
|
|||||||
// Listeners of this type are returned by [chanListenConfig.Listen] and are used
|
// 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.
|
// in module dnsserver to make the bind-to-device logic work in DNS-over-TCP.
|
||||||
type chanListener struct {
|
type chanListener struct {
|
||||||
closeOnce *sync.Once
|
// mu protects conns (against closure) and isClosed.
|
||||||
conns chan net.Conn
|
mu *sync.Mutex
|
||||||
laddr net.Addr
|
conns chan net.Conn
|
||||||
|
laddr net.Addr
|
||||||
|
subnet netip.Prefix
|
||||||
|
isClosed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// newChanListener returns a new properly initialized *chanListener.
|
// 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{
|
return &chanListener{
|
||||||
closeOnce: &sync.Once{},
|
mu: &sync.Mutex{},
|
||||||
conns: conns,
|
conns: conns,
|
||||||
laddr: laddr,
|
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.
|
// Close implements the [net.Listener] interface for *chanListener.
|
||||||
func (l *chanListener) Close() (err error) {
|
func (l *chanListener) Close() (err error) {
|
||||||
closedNow := false
|
l.mu.Lock()
|
||||||
l.closeOnce.Do(func() {
|
defer l.mu.Unlock()
|
||||||
close(l.conns)
|
|
||||||
closedNow = true
|
|
||||||
})
|
|
||||||
|
|
||||||
if !closedNow {
|
if l.isClosed {
|
||||||
return wrapConnError(tnChanLsnr, "Close", l.laddr, net.ErrClosed)
|
return wrapConnError(tnChanLsnr, "Close", l.laddr, net.ErrClosed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
close(l.conns)
|
||||||
|
l.isClosed = true
|
||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
func TestChanListener_Accept(t *testing.T) {
|
func TestChanListener_Accept(t *testing.T) {
|
||||||
conns := make(chan net.Conn, 1)
|
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
|
// A simple way to have a distinct net.Conn without actually implementing
|
||||||
// the entire interface.
|
// the entire interface.
|
||||||
@ -32,14 +32,14 @@ func TestChanListener_Accept(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestChanListener_Addr(t *testing.T) {
|
func TestChanListener_Addr(t *testing.T) {
|
||||||
l := newChanListener(nil, testLAddr)
|
l := newChanListener(nil, testSubnetIPv4, testLAddr)
|
||||||
got := l.Addr()
|
got := l.Addr()
|
||||||
assert.Equal(t, testLAddr, got)
|
assert.Equal(t, testLAddr, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestChanListener_Close(t *testing.T) {
|
func TestChanListener_Close(t *testing.T) {
|
||||||
conns := make(chan net.Conn)
|
conns := make(chan net.Conn)
|
||||||
l := newChanListener(conns, testLAddr)
|
l := newChanListener(conns, testSubnetIPv4, testLAddr)
|
||||||
err := l.Close()
|
err := l.Close()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ package bindtodevice
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -19,32 +20,38 @@ import (
|
|||||||
// are used in module dnsserver to make the bind-to-device logic work in
|
// are used in module dnsserver to make the bind-to-device logic work in
|
||||||
// DNS-over-UDP.
|
// DNS-over-UDP.
|
||||||
type chanPacketConn struct {
|
type chanPacketConn struct {
|
||||||
closeOnce *sync.Once
|
// mu protects sessions (against closure) and isClosed.
|
||||||
sessions chan *packetSession
|
mu *sync.Mutex
|
||||||
laddr net.Addr
|
sessions chan *packetSession
|
||||||
|
|
||||||
|
writeRequests chan *packetConnWriteReq
|
||||||
|
|
||||||
// deadlineMu protects readDeadline and writeDeadline.
|
// deadlineMu protects readDeadline and writeDeadline.
|
||||||
deadlineMu *sync.RWMutex
|
deadlineMu *sync.RWMutex
|
||||||
readDeadline time.Time
|
readDeadline time.Time
|
||||||
writeDeadline time.Time
|
writeDeadline time.Time
|
||||||
|
|
||||||
writeRequests chan *packetConnWriteReq
|
laddr net.Addr
|
||||||
|
subnet netip.Prefix
|
||||||
|
isClosed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// newChanPacketConn returns a new properly initialized *chanPacketConn.
|
// newChanPacketConn returns a new properly initialized *chanPacketConn.
|
||||||
func newChanPacketConn(
|
func newChanPacketConn(
|
||||||
sessions chan *packetSession,
|
sessions chan *packetSession,
|
||||||
|
subnet netip.Prefix,
|
||||||
writeRequests chan *packetConnWriteReq,
|
writeRequests chan *packetConnWriteReq,
|
||||||
laddr net.Addr,
|
laddr net.Addr,
|
||||||
) (c *chanPacketConn) {
|
) (c *chanPacketConn) {
|
||||||
return &chanPacketConn{
|
return &chanPacketConn{
|
||||||
closeOnce: &sync.Once{},
|
mu: &sync.Mutex{},
|
||||||
sessions: sessions,
|
sessions: sessions,
|
||||||
laddr: laddr,
|
writeRequests: writeRequests,
|
||||||
|
|
||||||
deadlineMu: &sync.RWMutex{},
|
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
|
// Close implements the [netext.SessionPacketConn] interface for
|
||||||
// *chanPacketConn.
|
// *chanPacketConn.
|
||||||
func (c *chanPacketConn) Close() (err error) {
|
func (c *chanPacketConn) Close() (err error) {
|
||||||
closedNow := false
|
c.mu.Lock()
|
||||||
c.closeOnce.Do(func() {
|
defer c.mu.Unlock()
|
||||||
close(c.sessions)
|
|
||||||
closedNow = true
|
|
||||||
})
|
|
||||||
|
|
||||||
if !closedNow {
|
if c.isClosed {
|
||||||
return wrapConnError(tnChanPConn, "Close", c.laddr, net.ErrClosed)
|
return wrapConnError(tnChanPConn, "Close", c.laddr, net.ErrClosed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
close(c.sessions)
|
||||||
|
c.isClosed = true
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,3 +296,18 @@ func sendWithTimer[T any](ch chan<- T, v T, timerCh <-chan time.Time) (err error
|
|||||||
return os.ErrDeadlineExceeded
|
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
|
||||||
|
}
|
||||||
|
@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
func TestChanPacketConn_Close(t *testing.T) {
|
func TestChanPacketConn_Close(t *testing.T) {
|
||||||
sessions := make(chan *packetSession)
|
sessions := make(chan *packetSession)
|
||||||
c := newChanPacketConn(sessions, nil, testLAddr)
|
c := newChanPacketConn(sessions, testSubnetIPv4, nil, testLAddr)
|
||||||
err := c.Close()
|
err := c.Close()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
@ -23,14 +23,14 @@ func TestChanPacketConn_Close(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestChanPacketConn_LocalAddr(t *testing.T) {
|
func TestChanPacketConn_LocalAddr(t *testing.T) {
|
||||||
c := newChanPacketConn(nil, nil, testLAddr)
|
c := newChanPacketConn(nil, testSubnetIPv4, nil, testLAddr)
|
||||||
got := c.LocalAddr()
|
got := c.LocalAddr()
|
||||||
assert.Equal(t, testLAddr, got)
|
assert.Equal(t, testLAddr, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestChanPacketConn_ReadFromSession(t *testing.T) {
|
func TestChanPacketConn_ReadFromSession(t *testing.T) {
|
||||||
sessions := make(chan *packetSession, 1)
|
sessions := make(chan *packetSession, 1)
|
||||||
c := newChanPacketConn(sessions, nil, testLAddr)
|
c := newChanPacketConn(sessions, testSubnetIPv4, nil, testLAddr)
|
||||||
|
|
||||||
body := []byte("hello")
|
body := []byte("hello")
|
||||||
bodyLen := len(body)
|
bodyLen := len(body)
|
||||||
@ -79,7 +79,7 @@ func TestChanPacketConn_ReadFromSession(t *testing.T) {
|
|||||||
func TestChanPacketConn_WriteToSession(t *testing.T) {
|
func TestChanPacketConn_WriteToSession(t *testing.T) {
|
||||||
sessions := make(chan *packetSession, 1)
|
sessions := make(chan *packetSession, 1)
|
||||||
writes := make(chan *packetConnWriteReq, 1)
|
writes := make(chan *packetConnWriteReq, 1)
|
||||||
c := newChanPacketConn(sessions, writes, testLAddr)
|
c := newChanPacketConn(sessions, testSubnetIPv4, writes, testLAddr)
|
||||||
|
|
||||||
body := []byte("hello")
|
body := []byte("hello")
|
||||||
bodyLen := len(body)
|
bodyLen := len(body)
|
||||||
@ -148,7 +148,7 @@ func checkWriteReqAndRespond(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestChanPacketConn_deadlines(t *testing.T) {
|
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)
|
deadline := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
|
@ -4,33 +4,20 @@ package bindtodevice
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// chanIndex is the data structure that contains the channels, to which the
|
// connIndex is the data structure that contains the channel listeners and
|
||||||
// [Manager] sends new connections and packets based on their protocol (TCP vs.
|
// packet connections, to which the [Manager] sends new connections and packets
|
||||||
// UDP), and subnet.
|
// based on their protocol (TCP vs. UDP), and subnet.
|
||||||
//
|
//
|
||||||
// In both slices a subnet with the largest prefix (the narrowest subnet) is
|
// In both slices a subnet with the largest prefix (the narrowest subnet) is
|
||||||
// sorted closer to the beginning.
|
// sorted closer to the beginning.
|
||||||
type chanIndex struct {
|
type connIndex struct {
|
||||||
packetConns []*indexPacketConn
|
packetConns []*chanPacketConn
|
||||||
listeners []*indexListener
|
listeners []*chanListener
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// subnetSortsBefore returns true if subnet x sorts before subnet y.
|
// 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
|
// addPacketConn adds the channel packet connection to the index. It returns an
|
||||||
// error if there is already one for this subnet. subnet should be masked.
|
// error if there is already one for this subnet. c.subnet should be masked.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Merge with [addListenerChannel].
|
// TODO(a.garipov): Merge with [addListenerChannel].
|
||||||
func (idx *chanIndex) addPacketConnChannel(
|
func (idx *connIndex) addPacketConn(c *chanPacketConn) (err error) {
|
||||||
subnet netip.Prefix,
|
cmpFunc := func(x, y *chanPacketConn) (cmp int) {
|
||||||
ch chan *packetSession,
|
|
||||||
) (err error) {
|
|
||||||
c := &indexPacketConn{
|
|
||||||
channel: ch,
|
|
||||||
subnet: subnet,
|
|
||||||
}
|
|
||||||
|
|
||||||
cmpFunc := func(x, y *indexPacketConn) (cmp int) {
|
|
||||||
return subnetCompare(x.subnet, y.subnet)
|
return subnetCompare(x.subnet, y.subnet)
|
||||||
}
|
}
|
||||||
|
|
||||||
newIdx, ok := slices.BinarySearchFunc(idx.packetConns, c, cmpFunc)
|
newIdx, ok := slices.BinarySearchFunc(idx.packetConns, c, cmpFunc)
|
||||||
if ok {
|
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,
|
// TODO(a.garipov): Consider using a list for idx.packetConns. Currently,
|
||||||
@ -88,23 +67,18 @@ func (idx *chanIndex) addPacketConnChannel(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// addListenerChannel adds the channel to the subnet index. It returns an error
|
// addListener adds the channel listener to the index. It returns an error if
|
||||||
// if there is already one for this subnet. subnet should be masked.
|
// there is already one for this subnet. l.subnet should be masked.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Merge with [addPacketConnChannel].
|
// TODO(a.garipov): Merge with [addPacketConnChannel].
|
||||||
func (idx *chanIndex) addListenerChannel(subnet netip.Prefix, ch chan net.Conn) (err error) {
|
func (idx *connIndex) addListener(l *chanListener) (err error) {
|
||||||
l := &indexListener{
|
cmpFunc := func(x, y *chanListener) (cmp int) {
|
||||||
channel: ch,
|
|
||||||
subnet: subnet,
|
|
||||||
}
|
|
||||||
|
|
||||||
cmpFunc := func(x, y *indexListener) (cmp int) {
|
|
||||||
return subnetCompare(x.subnet, y.subnet)
|
return subnetCompare(x.subnet, y.subnet)
|
||||||
}
|
}
|
||||||
|
|
||||||
newIdx, ok := slices.BinarySearchFunc(idx.listeners, l, cmpFunc)
|
newIdx, ok := slices.BinarySearchFunc(idx.listeners, l, cmpFunc)
|
||||||
if ok {
|
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,
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// packetConnChannel returns a packet-connection channel which accepts
|
// packetConn returns a channel packet connection which accepts connections to
|
||||||
// connections to local address laddr or nil if there is no such channel
|
// local address laddr or nil if there is no such channel
|
||||||
func (idx *chanIndex) packetConnChannel(laddr netip.Addr) (ch chan *packetSession) {
|
func (idx *connIndex) packetConn(laddr netip.Addr) (c *chanPacketConn) {
|
||||||
for _, c := range idx.packetConns {
|
for _, c = range idx.packetConns {
|
||||||
if c.subnet.Contains(laddr) {
|
if c.subnet.Contains(laddr) {
|
||||||
return c.channel
|
return c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
// address laddr or nil if there is no such channel
|
||||||
func (idx *chanIndex) listenerChannel(laddr netip.Addr) (ch chan net.Conn) {
|
func (idx *connIndex) listener(laddr netip.Addr) (l *chanListener) {
|
||||||
for _, l := range idx.listeners {
|
for _, l = range idx.listeners {
|
||||||
if l.subnet.Contains(laddr) {
|
if l.subnet.Contains(laddr) {
|
||||||
return l.channel
|
return l
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -17,7 +17,7 @@ import (
|
|||||||
|
|
||||||
// interfaceListener contains information about a single interface listener.
|
// interfaceListener contains information about a single interface listener.
|
||||||
type interfaceListener struct {
|
type interfaceListener struct {
|
||||||
channels *chanIndex
|
conns *connIndex
|
||||||
writeRequests chan *packetConnWriteReq
|
writeRequests chan *packetConnWriteReq
|
||||||
done chan unit
|
done chan unit
|
||||||
listenConf *net.ListenConfig
|
listenConf *net.ListenConfig
|
||||||
@ -64,14 +64,16 @@ func (l *interfaceListener) listenTCP(errCh chan<- error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
laddr := netutil.NetAddrToAddrPort(conn.LocalAddr())
|
laddr := netutil.NetAddrToAddrPort(conn.LocalAddr())
|
||||||
ch := l.channels.listenerChannel(laddr.Addr())
|
lsnr := l.conns.listener(laddr.Addr())
|
||||||
if ch == nil {
|
if lsnr == nil {
|
||||||
log.Info("%s: no channel for laddr %s", logPrefix, laddr)
|
log.Info("%s: no channel for laddr %s", logPrefix, laddr)
|
||||||
|
|
||||||
continue
|
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()
|
laddr := sess.laddr.AddrPort().Addr()
|
||||||
ch := l.channels.packetConnChannel(laddr)
|
chanPConn := l.conns.packetConn(laddr)
|
||||||
if ch == nil {
|
if chanPConn == nil {
|
||||||
log.Info("%s: no channel for laddr %s", logPrefix, laddr)
|
log.Info("%s: no channel for laddr %s", logPrefix, laddr)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ch <- sess
|
if !chanPConn.send(sess) {
|
||||||
|
log.Info("%s: channel for laddr %s is closed", logPrefix, laddr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
77
internal/bindtodevice/interfacestorage.go
Normal file
77
internal/bindtodevice/interfacestorage.go
Normal 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
|
||||||
|
}
|
@ -5,6 +5,10 @@ import "github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
|||||||
// ManagerConfig is the configuration structure for [NewManager]. All fields
|
// ManagerConfig is the configuration structure for [NewManager]. All fields
|
||||||
// must be set.
|
// must be set.
|
||||||
type ManagerConfig struct {
|
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
|
// ErrColl is the error collector that is used to collect non-critical
|
||||||
// errors.
|
// errors.
|
||||||
ErrColl agd.ErrorCollector
|
ErrColl agd.ErrorCollector
|
||||||
@ -13,3 +17,14 @@ type ManagerConfig struct {
|
|||||||
// dispatch TCP connections and UDP sessions.
|
// dispatch TCP connections and UDP sessions.
|
||||||
ChannelBufferSize int
|
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
|
||||||
|
}
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
|
|
||||||
// Manager creates individual listeners and dispatches connections to them.
|
// Manager creates individual listeners and dispatches connections to them.
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
|
interfaces InterfaceStorage
|
||||||
closeOnce *sync.Once
|
closeOnce *sync.Once
|
||||||
ifaceListeners map[ID]*interfaceListener
|
ifaceListeners map[ID]*interfaceListener
|
||||||
errColl agd.ErrorCollector
|
errColl agd.ErrorCollector
|
||||||
@ -28,6 +29,7 @@ type Manager struct {
|
|||||||
// NewManager returns a new manager of interface listeners.
|
// NewManager returns a new manager of interface listeners.
|
||||||
func NewManager(c *ManagerConfig) (m *Manager) {
|
func NewManager(c *ManagerConfig) (m *Manager) {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
|
interfaces: c.InterfaceStorage,
|
||||||
closeOnce: &sync.Once{},
|
closeOnce: &sync.Once{},
|
||||||
ifaceListeners: map[ID]*interfaceListener{},
|
ifaceListeners: map[ID]*interfaceListener{},
|
||||||
errColl: c.ErrColl,
|
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.
|
// 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) }()
|
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) {
|
validateDup := func(lsnrID ID, lsnr *interfaceListener) (lsnrErr error) {
|
||||||
lsnrIfaceName, lsnrPort := lsnr.ifaceName, lsnr.port
|
lsnrIfaceName, lsnrPort := lsnr.ifaceName, lsnr.port
|
||||||
if lsnrID == id {
|
if lsnrID == id {
|
||||||
@ -71,11 +86,15 @@ func (m *Manager) Add(id ID, ifaceName string, port uint16) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if conf == nil {
|
||||||
|
conf = defaultCtrlConf
|
||||||
|
}
|
||||||
|
|
||||||
m.ifaceListeners[id] = &interfaceListener{
|
m.ifaceListeners[id] = &interfaceListener{
|
||||||
channels: &chanIndex{},
|
conns: &connIndex{},
|
||||||
writeRequests: make(chan *packetConnWriteReq, m.chanBufSize),
|
writeRequests: make(chan *packetConnWriteReq, m.chanBufSize),
|
||||||
done: m.done,
|
done: m.done,
|
||||||
listenConf: newListenConfig(ifaceName),
|
listenConf: newListenConfig(ifaceName, conf),
|
||||||
errColl: m.errColl,
|
errColl: m.errColl,
|
||||||
ifaceName: ifaceName,
|
ifaceName: ifaceName,
|
||||||
port: port,
|
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.
|
// ListenConfig must not be called after Start is called.
|
||||||
func (m *Manager) ListenConfig(id ID, subnet netip.Prefix) (c netext.ListenConfig, err error) {
|
func (m *Manager) ListenConfig(id ID, subnet netip.Prefix) (c netext.ListenConfig, err error) {
|
||||||
if masked := subnet.Masked(); subnet != masked {
|
defer func() {
|
||||||
return nil, fmt.Errorf(
|
err = errors.Annotate(
|
||||||
"subnet %s for interface listener %q not masked (expected %s)",
|
err,
|
||||||
|
"creating listen config for subnet %s and listener with id %q: %w",
|
||||||
subnet,
|
subnet,
|
||||||
id,
|
id,
|
||||||
masked,
|
|
||||||
)
|
)
|
||||||
}
|
}()
|
||||||
|
|
||||||
l, ok := m.ifaceListeners[id]
|
l, ok := m.ifaceListeners[id]
|
||||||
if !ok {
|
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 = m.validateIfaceSubnet(l.ifaceName, subnet)
|
||||||
err = l.channels.addListenerChannel(subnet, connCh)
|
|
||||||
if err != nil {
|
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)
|
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 {
|
if err != nil {
|
||||||
// Technically shouldn't happen, since [chanIndex.addListenerChannel]
|
// Technically shouldn't happen, since [chanIndex.addListenerChannel]
|
||||||
// has already checked for duplicates.
|
// 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{
|
return &chanListenConfig{
|
||||||
packetConn: newChanPacketConn(sessCh, l.writeRequests, &prefixNetAddr{
|
packetConn: pConn,
|
||||||
prefix: subnet,
|
listener: lsnr,
|
||||||
network: "udp",
|
|
||||||
port: l.port,
|
|
||||||
}),
|
|
||||||
listener: newChanListener(connCh, &prefixNetAddr{
|
|
||||||
prefix: subnet,
|
|
||||||
network: "tcp",
|
|
||||||
port: l.port,
|
|
||||||
}),
|
|
||||||
}, nil
|
}, 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
|
// type check
|
||||||
var _ agd.Service = (*Manager)(nil)
|
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) {
|
func (m *Manager) Start() (err error) {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
numListen := 2 * len(m.ifaceListeners)
|
numListen := 2 * len(m.ifaceListeners)
|
||||||
errCh := make(chan error, numListen)
|
errCh := make(chan error, numListen)
|
||||||
|
|
||||||
@ -162,10 +224,17 @@ func (m *Manager) Start() (err error) {
|
|||||||
return nil
|
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.
|
// TODO(a.garipov): Consider waiting for all sockets to close.
|
||||||
func (m *Manager) Shutdown(_ context.Context) (err error) {
|
func (m *Manager) Shutdown(_ context.Context) (err error) {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
closedNow := false
|
closedNow := false
|
||||||
m.closeOnce.Do(func() {
|
m.closeOnce.Do(func() {
|
||||||
close(m.done)
|
close(m.done)
|
||||||
|
@ -18,12 +18,46 @@ import (
|
|||||||
|
|
||||||
// TODO(a.garipov): Add tests for other platforms?
|
// 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) {
|
func TestManager_Add(t *testing.T) {
|
||||||
errColl := &agdtest.ErrorCollector{
|
errColl := &agdtest.ErrorCollector{
|
||||||
OnCollect: func(_ context.Context, _ error) { panic("not implemented") },
|
OnCollect: func(_ context.Context, _ error) { panic("not implemented") },
|
||||||
}
|
}
|
||||||
|
|
||||||
m := bindtodevice.NewManager(&bindtodevice.ManagerConfig{
|
m := bindtodevice.NewManager(&bindtodevice.ManagerConfig{
|
||||||
|
InterfaceStorage: &fakeInterfaceStorage{
|
||||||
|
OnInterfaceByName: func(_ string) (iface bindtodevice.NetInterface, err error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
ErrColl: errColl,
|
ErrColl: errColl,
|
||||||
ChannelBufferSize: 1,
|
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
|
// Don't use a table, since the results of these subtests depend on each
|
||||||
// other.
|
// other.
|
||||||
t.Run("success", func(t *testing.T) {
|
t.Run("success", func(t *testing.T) {
|
||||||
err := m.Add(testID1, testIfaceName, testPort1)
|
err := m.Add(testID1, testIfaceName, testPort1, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("dup_id", func(t *testing.T) {
|
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)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("dup_iface_port", func(t *testing.T) {
|
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)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("success_other", func(t *testing.T) {
|
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)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -57,17 +91,27 @@ func TestManager_ListenConfig(t *testing.T) {
|
|||||||
OnCollect: func(_ context.Context, _ error) { panic("not implemented") },
|
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{
|
m := bindtodevice.NewManager(&bindtodevice.ManagerConfig{
|
||||||
|
InterfaceStorage: &fakeInterfaceStorage{
|
||||||
|
OnInterfaceByName: func(_ string) (iface bindtodevice.NetInterface, err error) {
|
||||||
|
return ifaceWithSubnet, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
ErrColl: errColl,
|
ErrColl: errColl,
|
||||||
ChannelBufferSize: 1,
|
ChannelBufferSize: 1,
|
||||||
})
|
})
|
||||||
require.NotNil(t, m)
|
require.NotNil(t, m)
|
||||||
|
|
||||||
err := m.Add(testID1, testIfaceName, testPort1)
|
err := m.Add(testID1, testIfaceName, testPort1, nil)
|
||||||
require.NoError(t, err)
|
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
|
// Don't use a table, since the results of these subtests depend on each
|
||||||
// other.
|
// other.
|
||||||
t.Run("not_found", func(t *testing.T) {
|
t.Run("not_found", func(t *testing.T) {
|
||||||
@ -94,6 +138,60 @@ func TestManager_ListenConfig(t *testing.T) {
|
|||||||
assert.Nil(t, lc)
|
assert.Nil(t, lc)
|
||||||
assert.Error(t, lcErr)
|
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) {
|
func TestManager(t *testing.T) {
|
||||||
@ -113,13 +211,14 @@ func TestManager(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m := bindtodevice.NewManager(&bindtodevice.ManagerConfig{
|
m := bindtodevice.NewManager(&bindtodevice.ManagerConfig{
|
||||||
|
InterfaceStorage: bindtodevice.DefaultInterfaceStorage{},
|
||||||
ErrColl: errColl,
|
ErrColl: errColl,
|
||||||
ChannelBufferSize: 1,
|
ChannelBufferSize: 1,
|
||||||
})
|
})
|
||||||
require.NotNil(t, m)
|
require.NotNil(t, m)
|
||||||
|
|
||||||
// TODO(a.garipov): Add support for zero port.
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
subnet, err := netutil.IPNetToPrefixNoMapped(&net.IPNet{
|
subnet, err := netutil.IPNetToPrefixNoMapped(&net.IPNet{
|
||||||
|
@ -13,12 +13,12 @@ import (
|
|||||||
|
|
||||||
// Manager creates individual listeners and dispatches connections to them.
|
// Manager creates individual listeners and dispatches connections to them.
|
||||||
//
|
//
|
||||||
// It is only suported on Linux.
|
// It is only supported on Linux.
|
||||||
type Manager struct{}
|
type Manager struct{}
|
||||||
|
|
||||||
// NewManager returns a new manager of interface listeners.
|
// 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) {
|
func NewManager(c *ManagerConfig) (m *Manager) {
|
||||||
return &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.
|
// Add creates a new interface-listener record in m.
|
||||||
//
|
//
|
||||||
// It is only suported on Linux.
|
// It is only supported on Linux.
|
||||||
func (m *Manager) Add(id ID, ifaceName string, port uint16) (err error) { return errUnsupported }
|
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
|
// ListenConfig returns a new netext.ListenConfig that receives connections from
|
||||||
// the interface listener with the given id and the destination addresses of
|
// the interface listener with the given id and the destination addresses of
|
||||||
// which fall within subnet. subnet should be masked.
|
// 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) {
|
func (m *Manager) ListenConfig(id ID, subnet netip.Prefix) (c netext.ListenConfig, err error) {
|
||||||
return nil, errUnsupported
|
return nil, errUnsupported
|
||||||
}
|
}
|
||||||
@ -44,12 +46,26 @@ func (m *Manager) ListenConfig(id ID, subnet netip.Prefix) (c netext.ListenConfi
|
|||||||
// type check
|
// type check
|
||||||
var _ agd.Service = (*Manager)(nil)
|
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.
|
// It is only supported on Linux.
|
||||||
func (m *Manager) Start() (err error) { return errUnsupported }
|
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.
|
// It is only supported on Linux.
|
||||||
func (m *Manager) Shutdown(_ context.Context) (err error) { return errUnsupported }
|
func (m *Manager) Shutdown(_ context.Context) (err error) {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errUnsupported
|
||||||
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
package bindtodevice
|
package bindtodevice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -11,16 +12,42 @@ import (
|
|||||||
|
|
||||||
func TestPrefixAddr(t *testing.T) {
|
func TestPrefixAddr(t *testing.T) {
|
||||||
const (
|
const (
|
||||||
wantStr = "1.2.3.0:56789/24"
|
port = 56789
|
||||||
network = "tcp"
|
network = "tcp"
|
||||||
)
|
)
|
||||||
|
|
||||||
pa := &prefixNetAddr{
|
testCases := []struct {
|
||||||
prefix: netip.MustParsePrefix("1.2.3.0/24"),
|
in *prefixNetAddr
|
||||||
network: network,
|
want string
|
||||||
port: 56789,
|
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())
|
for _, tc := range testCases {
|
||||||
assert.Equal(t, network, pa.Network())
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tc.want, tc.in.String())
|
||||||
|
assert.Equal(t, network, tc.in.Network())
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,106 +12,100 @@ import (
|
|||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newListenConfig returns a [net.ListenConfig] that can bind to a network
|
// setSockOptFunc is a function that sets a socket option on fd.
|
||||||
// interface (device) by its name.
|
type setSockOptFunc func(fd int) (err error)
|
||||||
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)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
// newStringSetSockOptFunc returns a string socket-option function with the
|
||||||
// additional socket options, including SO_BINDTODEVICE.
|
// given parameters.
|
||||||
func listenControl(devName, network, _ string, c syscall.RawConn) (err error) {
|
func newStringSetSockOptFunc(name string, lvl, opt int, val string) (o setSockOptFunc) {
|
||||||
var ctrlFunc func(fd uintptr, devName string) (err error)
|
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 {
|
switch network {
|
||||||
case "tcp", "tcp4", "tcp6":
|
case "tcp", "tcp4", "tcp6":
|
||||||
ctrlFunc = setTCPSockOpt
|
// Socket options for TCP connection already set. Go on.
|
||||||
case "udp", "udp4", "udp6":
|
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:
|
default:
|
||||||
return fmt.Errorf("bad network %q", network)
|
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
|
var opErr error
|
||||||
err = c.Control(func(fd uintptr) {
|
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)
|
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
|
// readPacketSession is a helper that reads a packet-session data from a UDP
|
||||||
// connection.
|
// connection.
|
||||||
func readPacketSession(c *net.UDPConn, bodySize int) (sess *packetSession, err error) {
|
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,
|
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{
|
pktInfo := &unix.Inet4Pktinfo{
|
||||||
Addr: sockAddr.Addr,
|
Addr: sockAddr.Addr,
|
||||||
|
Spec_dst: sockAddr.Addr,
|
||||||
}
|
}
|
||||||
|
|
||||||
respOOB = unix.PktInfo4(pktInfo)
|
respOOB = unix.PktInfo4(pktInfo)
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -18,6 +19,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestInterfaceEnvVarName is the environment variable name the presence and
|
// TestInterfaceEnvVarName is the environment variable name the presence and
|
||||||
@ -88,7 +90,7 @@ func TestListenControl(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ifaceName := iface.Name
|
ifaceName := iface.Name
|
||||||
lc := newListenConfig(ifaceName)
|
lc := newListenConfig(ifaceName, &ControlConfig{})
|
||||||
require.NotNil(t, lc)
|
require.NotNil(t, lc)
|
||||||
|
|
||||||
t.Run("tcp", func(t *testing.T) {
|
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
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/backend"
|
"github.com/AdguardTeam/AdGuardDNS/internal/backend"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/billstat"
|
"github.com/AdguardTeam/AdGuardDNS/internal/billstat"
|
||||||
|
"github.com/AdguardTeam/AdGuardDNS/internal/profiledb"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
)
|
)
|
||||||
@ -76,7 +77,7 @@ func setupBackend(
|
|||||||
envs *environments,
|
envs *environments,
|
||||||
sigHdlr signalHandler,
|
sigHdlr signalHandler,
|
||||||
errColl agd.ErrorCollector,
|
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)
|
profStrgConf, billStatConf := conf.toInternal(envs, errColl)
|
||||||
rec = billstat.NewRuntimeRecorder(&billstat.RuntimeRecorderConfig{
|
rec = billstat.NewRuntimeRecorder(&billstat.RuntimeRecorderConfig{
|
||||||
Uploader: backend.NewBillStat(billStatConf),
|
Uploader: backend.NewBillStat(billStatConf),
|
||||||
@ -103,7 +104,7 @@ func setupBackend(
|
|||||||
sigHdlr.add(billStatRefr)
|
sigHdlr.add(billStatRefr)
|
||||||
|
|
||||||
profStrg := backend.NewProfileStorage(profStrgConf)
|
profStrg := backend.NewProfileStorage(profStrgConf)
|
||||||
profDB, err = agd.NewDefaultProfileDB(
|
profDB, err = profiledb.New(
|
||||||
profStrg,
|
profStrg,
|
||||||
conf.FullRefreshIvl.Duration,
|
conf.FullRefreshIvl.Duration,
|
||||||
envs.ProfilesCachePath,
|
envs.ProfilesCachePath,
|
||||||
|
@ -16,9 +16,9 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnscheck"
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnscheck"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/forward"
|
"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/dnssvc"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
|
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
|
||||||
|
"github.com/AdguardTeam/AdGuardDNS/internal/filter/hashprefix"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
|
"github.com/AdguardTeam/AdGuardDNS/internal/geoip"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
|
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/websvc"
|
"github.com/AdguardTeam/AdGuardDNS/internal/websvc"
|
||||||
@ -130,11 +130,23 @@ func Main() {
|
|||||||
fltGroups, err := c.FilteringGroups.toInternal(fltStrg)
|
fltGroups, err := c.FilteringGroups.toInternal(fltStrg)
|
||||||
check(err)
|
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
|
// Server groups
|
||||||
|
|
||||||
messages := dnsmsg.NewConstructor(&dnsmsg.BlockingModeNullIP{}, c.Filters.ResponseTTL.Duration)
|
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)
|
check(err)
|
||||||
|
|
||||||
// TLS keys logging
|
// TLS keys logging
|
||||||
@ -173,7 +185,7 @@ func Main() {
|
|||||||
// Rate limiting
|
// Rate limiting
|
||||||
|
|
||||||
consulAllowlistURL := &envs.ConsulAllowlistURL.URL
|
consulAllowlistURL := &envs.ConsulAllowlistURL.URL
|
||||||
rateLimiter, err := setupRateLimiter(c.RateLimit, consulAllowlistURL, sigHdlr, errColl)
|
rateLimiter, connLimiter, err := setupRateLimiter(c.RateLimit, consulAllowlistURL, sigHdlr, errColl)
|
||||||
check(err)
|
check(err)
|
||||||
|
|
||||||
// GeoIP database
|
// GeoIP database
|
||||||
@ -197,24 +209,21 @@ func Main() {
|
|||||||
|
|
||||||
// DNS service
|
// DNS service
|
||||||
|
|
||||||
metricsListener := prometheus.NewForwardMetricsListener(len(c.Upstream.FallbackServers) + 1)
|
fwdConf, err := c.Upstream.toInternal()
|
||||||
|
|
||||||
upstream, err := c.Upstream.toInternal()
|
|
||||||
check(err)
|
check(err)
|
||||||
|
|
||||||
handler := forward.NewHandler(&forward.HandlerConfig{
|
handler := forward.NewHandler(fwdConf)
|
||||||
Address: upstream.Server,
|
|
||||||
Network: upstream.Network,
|
// TODO(a.garipov): Consider making these configurable via the configuration
|
||||||
MetricsListener: metricsListener,
|
// file.
|
||||||
HealthcheckDomainTmpl: c.Upstream.Healthcheck.DomainTmpl,
|
hashStorages := map[string]*hashprefix.Storage{
|
||||||
FallbackAddresses: c.Upstream.FallbackServers,
|
filter.GeneralTXTSuffix: safeBrowsingHashes,
|
||||||
Timeout: c.Upstream.Timeout.Duration,
|
filter.AdultBlockingTXTSuffix: adultBlockingHashes,
|
||||||
HealthcheckBackoffDuration: c.Upstream.Healthcheck.BackoffDuration.Duration,
|
}
|
||||||
}, c.Upstream.Healthcheck.Enabled)
|
|
||||||
|
|
||||||
dnsConf := &dnssvc.Config{
|
dnsConf := &dnssvc.Config{
|
||||||
Messages: messages,
|
Messages: messages,
|
||||||
SafeBrowsing: filter.NewSafeBrowsingServer(safeBrowsingHashes, adultBlockingHashes),
|
SafeBrowsing: hashprefix.NewMatcher(hashStorages),
|
||||||
BillStat: billStatRec,
|
BillStat: billStatRec,
|
||||||
ProfileDB: profDB,
|
ProfileDB: profDB,
|
||||||
DNSCheck: dnsCk,
|
DNSCheck: dnsCk,
|
||||||
@ -226,21 +235,20 @@ func Main() {
|
|||||||
Handler: handler,
|
Handler: handler,
|
||||||
QueryLog: c.buildQueryLog(envs),
|
QueryLog: c.buildQueryLog(envs),
|
||||||
RuleStat: ruleStat,
|
RuleStat: ruleStat,
|
||||||
Upstream: upstream,
|
|
||||||
RateLimit: rateLimiter,
|
RateLimit: rateLimiter,
|
||||||
|
ConnLimiter: connLimiter,
|
||||||
FilteringGroups: fltGroups,
|
FilteringGroups: fltGroups,
|
||||||
ServerGroups: srvGrps,
|
ServerGroups: srvGrps,
|
||||||
CacheSize: c.Cache.Size,
|
CacheSize: c.Cache.Size,
|
||||||
ECSCacheSize: c.Cache.ECSSize,
|
ECSCacheSize: c.Cache.ECSSize,
|
||||||
UseECSCache: c.Cache.Type == cacheTypeECS,
|
UseECSCache: c.Cache.Type == cacheTypeECS,
|
||||||
ResearchMetrics: bool(envs.ResearchMetrics),
|
ResearchMetrics: bool(envs.ResearchMetrics),
|
||||||
|
ControlConf: ctrlConf,
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsSvc, err := dnssvc.New(dnsConf)
|
dnsSvc, err := dnssvc.New(dnsConf)
|
||||||
check(err)
|
check(err)
|
||||||
|
|
||||||
sigHdlr.add(dnsSvc)
|
|
||||||
|
|
||||||
// Connectivity check
|
// Connectivity check
|
||||||
|
|
||||||
err = connectivityCheck(dnsConf, c.ConnectivityCheck)
|
err = connectivityCheck(dnsConf, c.ConnectivityCheck)
|
||||||
|
@ -61,6 +61,13 @@ type configuration struct {
|
|||||||
// ConnectivityCheck is the connectivity check configuration.
|
// ConnectivityCheck is the connectivity check configuration.
|
||||||
ConnectivityCheck *connCheckConfig `yaml:"connectivity_check"`
|
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 is extra information, which is exposed by metrics.
|
||||||
AdditionalMetricsInfo additionalInfo `yaml:"additional_metrics_info"`
|
AdditionalMetricsInfo additionalInfo `yaml:"additional_metrics_info"`
|
||||||
|
|
||||||
@ -140,6 +147,12 @@ func (c *configuration) validate() (err error) {
|
|||||||
}, {
|
}, {
|
||||||
validate: c.ConnectivityCheck.validate,
|
validate: c.ConnectivityCheck.validate,
|
||||||
name: "connectivity_check",
|
name: "connectivity_check",
|
||||||
|
}, {
|
||||||
|
validate: c.InterfaceListeners.validate,
|
||||||
|
name: "interface_listeners",
|
||||||
|
}, {
|
||||||
|
validate: c.Network.validate,
|
||||||
|
name: "network",
|
||||||
}, {
|
}, {
|
||||||
validate: c.AdditionalMetricsInfo.validate,
|
validate: c.AdditionalMetricsInfo.validate,
|
||||||
name: "additional_metrics_info",
|
name: "additional_metrics_info",
|
||||||
|
@ -34,8 +34,8 @@ type environments struct {
|
|||||||
YoutubeSafeSearchURL *agdhttp.URL `env:"YOUTUBE_SAFE_SEARCH_URL,notEmpty"`
|
YoutubeSafeSearchURL *agdhttp.URL `env:"YOUTUBE_SAFE_SEARCH_URL,notEmpty"`
|
||||||
RuleStatURL *agdhttp.URL `env:"RULESTAT_URL"`
|
RuleStatURL *agdhttp.URL `env:"RULESTAT_URL"`
|
||||||
|
|
||||||
ConfPath string `env:"CONFIG_PATH" envDefault:"./config.yml"`
|
ConfPath string `env:"CONFIG_PATH" envDefault:"./config.yaml"`
|
||||||
DNSDBPath string `env:"DNSDB_PATH" envDefault:"./dnsdb.bolt"`
|
DNSDBPath string `env:"DNSDB_PATH"`
|
||||||
FilterCachePath string `env:"FILTER_CACHE_PATH" envDefault:"./filters/"`
|
FilterCachePath string `env:"FILTER_CACHE_PATH" envDefault:"./filters/"`
|
||||||
ProfilesCachePath string `env:"PROFILES_CACHE_PATH" envDefault:"./profilecache.json"`
|
ProfilesCachePath string `env:"PROFILES_CACHE_PATH" envDefault:"./profilecache.json"`
|
||||||
GeoIPASNPath string `env:"GEOIP_ASN_PATH" envDefault:"./asn.mmdb"`
|
GeoIPASNPath string `env:"GEOIP_ASN_PATH" envDefault:"./asn.mmdb"`
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdnet"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agdnet"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
|
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
|
||||||
|
"github.com/AdguardTeam/AdGuardDNS/internal/filter/hashprefix"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
)
|
)
|
||||||
@ -51,8 +52,8 @@ func (c *filtersConfig) toInternal(
|
|||||||
errColl agd.ErrorCollector,
|
errColl agd.ErrorCollector,
|
||||||
resolver agdnet.Resolver,
|
resolver agdnet.Resolver,
|
||||||
envs *environments,
|
envs *environments,
|
||||||
safeBrowsing *filter.HashPrefix,
|
safeBrowsing *hashprefix.Filter,
|
||||||
adultBlocking *filter.HashPrefix,
|
adultBlocking *hashprefix.Filter,
|
||||||
) (conf *filter.DefaultStorageConfig) {
|
) (conf *filter.DefaultStorageConfig) {
|
||||||
return &filter.DefaultStorageConfig{
|
return &filter.DefaultStorageConfig{
|
||||||
FilterIndexURL: netutil.CloneURL(&envs.FilterIndexURL.URL),
|
FilterIndexURL: netutil.CloneURL(&envs.FilterIndexURL.URL),
|
||||||
|
102
internal/cmd/ifacelistener.go
Normal file
102
internal/cmd/ifacelistener.go
Normal 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
51
internal/cmd/network.go
Normal 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
|
||||||
|
}
|
@ -6,8 +6,11 @@ import (
|
|||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdnet"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agdnet"
|
||||||
|
"github.com/AdguardTeam/AdGuardDNS/internal/connlimiter"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/consul"
|
"github.com/AdguardTeam/AdGuardDNS/internal/consul"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/ratelimit"
|
"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/AdguardTeam/golibs/timeutil"
|
||||||
"github.com/c2h5oh/datasize"
|
"github.com/c2h5oh/datasize"
|
||||||
)
|
)
|
||||||
@ -19,6 +22,10 @@ type rateLimitConfig struct {
|
|||||||
// AllowList is the allowlist of clients.
|
// AllowList is the allowlist of clients.
|
||||||
Allowlist *allowListConfig `yaml:"allowlist"`
|
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.
|
// Rate limit options for IPv4 addresses.
|
||||||
IPv4 *rateLimitOptions `yaml:"ipv4"`
|
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.
|
// validate returns an error if the safe rate limiting configuration is invalid.
|
||||||
func (c *rateLimitConfig) validate() (err error) {
|
func (c *rateLimitConfig) validate() (err error) {
|
||||||
if c == nil {
|
switch {
|
||||||
|
case c == nil:
|
||||||
return errNilConfig
|
return errNilConfig
|
||||||
} else if c.Allowlist == nil {
|
case c.Allowlist == nil:
|
||||||
return fmt.Errorf("allowlist: %w", errNilConfig)
|
return fmt.Errorf("allowlist: %w", errNilConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = c.ConnectionLimit.validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connection_limit: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
err = c.IPv4.validate()
|
err = c.IPv4.validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ipv4: %w", err)
|
return fmt.Errorf("ipv4: %w", err)
|
||||||
@ -129,16 +142,16 @@ func setupRateLimiter(
|
|||||||
consulAllowlist *url.URL,
|
consulAllowlist *url.URL,
|
||||||
sigHdlr signalHandler,
|
sigHdlr signalHandler,
|
||||||
errColl agd.ErrorCollector,
|
errColl agd.ErrorCollector,
|
||||||
) (rateLimiter *ratelimit.BackOff, err error) {
|
) (rateLimiter *ratelimit.BackOff, connLimiter *connlimiter.Limiter, err error) {
|
||||||
allowSubnets, err := agdnet.ParseSubnets(conf.Allowlist.List...)
|
allowSubnets, err := agdnet.ParseSubnets(conf.Allowlist.List...)
|
||||||
if err != nil {
|
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)
|
allowlist := ratelimit.NewDynamicAllowlist(allowSubnets, nil)
|
||||||
refresher, err := consul.NewAllowlistRefresher(allowlist, consulAllowlist)
|
refresher, err := consul.NewAllowlistRefresher(allowlist, consulAllowlist)
|
||||||
if err != nil {
|
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{
|
refr := agd.NewRefreshWorker(&agd.RefreshWorkerConfig{
|
||||||
@ -152,10 +165,67 @@ func setupRateLimiter(
|
|||||||
})
|
})
|
||||||
err = refr.Start()
|
err = refr.Start()
|
||||||
if err != nil {
|
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)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdnet"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agdnet"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/filter"
|
"github.com/AdguardTeam/AdGuardDNS/internal/filter/hashprefix"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/filter/hashstorage"
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
@ -45,13 +44,13 @@ func (c *safeBrowsingConfig) toInternal(
|
|||||||
resolver agdnet.Resolver,
|
resolver agdnet.Resolver,
|
||||||
id agd.FilterListID,
|
id agd.FilterListID,
|
||||||
cacheDir string,
|
cacheDir string,
|
||||||
) (fltConf *filter.HashPrefixConfig, err error) {
|
) (fltConf *hashprefix.FilterConfig, err error) {
|
||||||
hashes, err := hashstorage.New("")
|
hashes, err := hashprefix.NewStorage("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &filter.HashPrefixConfig{
|
return &hashprefix.FilterConfig{
|
||||||
Hashes: hashes,
|
Hashes: hashes,
|
||||||
URL: netutil.CloneURL(&c.URL.URL),
|
URL: netutil.CloneURL(&c.URL.URL),
|
||||||
ErrColl: errColl,
|
ErrColl: errColl,
|
||||||
@ -95,13 +94,13 @@ func setupHashPrefixFilter(
|
|||||||
cachePath string,
|
cachePath string,
|
||||||
sigHdlr signalHandler,
|
sigHdlr signalHandler,
|
||||||
errColl agd.ErrorCollector,
|
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)
|
fltConf, err := conf.toInternal(errColl, resolver, id, cachePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("configuring hash prefix filter %s: %w", id, err)
|
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 {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("creating hash prefix filter %s: %w", id, err)
|
return nil, nil, fmt.Errorf("creating hash prefix filter %s: %w", id, err)
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
"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/AdGuardDNS/internal/metrics"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/stringutil"
|
"github.com/AdguardTeam/golibs/stringutil"
|
||||||
@ -14,10 +16,18 @@ import (
|
|||||||
|
|
||||||
// toInternal returns the configuration of DNS servers for a single server
|
// toInternal returns the configuration of DNS servers for a single server
|
||||||
// group. srvs is assumed to be valid.
|
// 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))
|
dnsSrvs = make([]*agd.Server, 0, len(srvs))
|
||||||
for _, srv := range 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)
|
name := agd.ServerName(srv.Name)
|
||||||
switch p := srv.Protocol; p {
|
switch p := srv.Protocol; p {
|
||||||
case srvProtoDNS:
|
case srvProtoDNS:
|
||||||
@ -158,27 +168,56 @@ type server struct {
|
|||||||
// Protocol is the protocol of the server.
|
// Protocol is the protocol of the server.
|
||||||
Protocol serverProto `yaml:"protocol"`
|
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"`
|
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
|
// LinkedIPEnabled shows if the linked IP addresses should be used to detect
|
||||||
// profiles on this server.
|
// profiles on this server.
|
||||||
LinkedIPEnabled bool `yaml:"linked_ip_enabled"`
|
LinkedIPEnabled bool `yaml:"linked_ip_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// bindData returns the socket binding data for this server.
|
// bindData returns the socket binding data for this server.
|
||||||
func (s *server) bindData() (bindData []*agd.ServerBindData) {
|
func (s *server) bindData(
|
||||||
addrs := s.BindAddresses
|
btdMgr *bindtodevice.Manager,
|
||||||
bindData = make([]*agd.ServerBindData, 0, len(addrs))
|
) (bindData []*agd.ServerBindData, err error) {
|
||||||
for _, addr := range addrs {
|
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{
|
bindData = append(bindData, &agd.ServerBindData{
|
||||||
AddrPort: addr,
|
ListenConfig: lc,
|
||||||
|
Address: string(iface.ID),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(a.garipov): Support bind_interfaces.
|
return bindData, nil
|
||||||
|
|
||||||
return bindData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate returns an error if the configuration is invalid.
|
// validate returns an error if the configuration is invalid.
|
||||||
@ -188,13 +227,12 @@ func (s *server) validate() (err error) {
|
|||||||
return errNilConfig
|
return errNilConfig
|
||||||
case s.Name == "":
|
case s.Name == "":
|
||||||
return errors.Error("no 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 {
|
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()
|
err = s.Protocol.validate()
|
||||||
@ -209,3 +247,62 @@ func (s *server) validate() (err error) {
|
|||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||||
|
"github.com/AdguardTeam/AdGuardDNS/internal/bindtodevice"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/stringutil"
|
"github.com/AdguardTeam/golibs/stringutil"
|
||||||
@ -19,6 +20,7 @@ type serverGroups []*serverGroup
|
|||||||
// service. srvGrps is assumed to be valid.
|
// service. srvGrps is assumed to be valid.
|
||||||
func (srvGrps serverGroups) toInternal(
|
func (srvGrps serverGroups) toInternal(
|
||||||
messages *dnsmsg.Constructor,
|
messages *dnsmsg.Constructor,
|
||||||
|
btdMgr *bindtodevice.Manager,
|
||||||
fltGrps map[agd.FilteringGroupID]*agd.FilteringGroup,
|
fltGrps map[agd.FilteringGroupID]*agd.FilteringGroup,
|
||||||
) (svcSrvGrps []*agd.ServerGroup, err error) {
|
) (svcSrvGrps []*agd.ServerGroup, err error) {
|
||||||
svcSrvGrps = make([]*agd.ServerGroup, len(srvGrps))
|
svcSrvGrps = make([]*agd.ServerGroup, len(srvGrps))
|
||||||
@ -42,7 +44,7 @@ func (srvGrps serverGroups) toInternal(
|
|||||||
FilteringGroup: fltGrpID,
|
FilteringGroup: fltGrpID,
|
||||||
}
|
}
|
||||||
|
|
||||||
svcSrvGrps[i].Servers, err = g.Servers.toInternal(tlsConf)
|
svcSrvGrps[i].Servers, err = g.Servers.toInternal(tlsConf, btdMgr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("server group %q: %w", g.Name, err)
|
return nil, fmt.Errorf("server group %q: %w", g.Name, err)
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,11 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/forward"
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/forward"
|
||||||
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/prometheus"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
)
|
)
|
||||||
@ -33,18 +35,32 @@ type upstreamConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// toInternal converts c to the data storage configuration for the DNS server.
|
// toInternal converts c to the data storage configuration for the DNS server.
|
||||||
func (c *upstreamConfig) toInternal() (conf *agd.Upstream, err error) {
|
func (c *upstreamConfig) toInternal() (fwdConf *forward.HandlerConfig, err error) {
|
||||||
net, addrPort, err := splitUpstreamURL(c.Server)
|
network, addrPort, err := splitUpstreamURL(c.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &agd.Upstream{
|
fallbacks := c.FallbackServers
|
||||||
Server: addrPort,
|
metricsListener := prometheus.NewForwardMetricsListener(len(fallbacks) + 1)
|
||||||
Network: net,
|
|
||||||
FallbackServers: c.FallbackServers,
|
var hcInit time.Duration
|
||||||
Timeout: c.Timeout.Duration,
|
if c.Healthcheck.Enabled {
|
||||||
}, nil
|
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.
|
// validate returns an error if the upstream configuration is invalid.
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/websvc"
|
"github.com/AdguardTeam/AdGuardDNS/internal/websvc"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"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
|
// Check Content-Type here as opposed to in validate, because we need
|
||||||
// all keys to be canonicalized first.
|
// all keys to be canonicalized first.
|
||||||
if file.Headers.Get(agdhttp.HdrNameContentType) == "" {
|
if file.Headers.Get(httphdr.ContentType) == "" {
|
||||||
return nil, errors.Error("content: " + agdhttp.HdrNameContentType + " header is required")
|
return nil, errors.Error("content: " + httphdr.ContentType + " header is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
return file, nil
|
return file, nil
|
||||||
|
51
internal/connlimiter/conn.go
Normal file
51
internal/connlimiter/conn.go
Normal 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
|
||||||
|
}
|
35
internal/connlimiter/counter.go
Normal file
35
internal/connlimiter/counter.go
Normal 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
|
||||||
|
}
|
42
internal/connlimiter/counter_internal_test.go
Normal file
42
internal/connlimiter/counter_internal_test.go
Normal 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())
|
||||||
|
})
|
||||||
|
}
|
73
internal/connlimiter/limiter.go
Normal file
73
internal/connlimiter/limiter.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
122
internal/connlimiter/limiter_test.go
Normal file
122
internal/connlimiter/limiter_test.go
Normal 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)
|
||||||
|
}
|
54
internal/connlimiter/listenconfig.go
Normal file
54
internal/connlimiter/listenconfig.go
Normal 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
|
||||||
|
}
|
75
internal/connlimiter/listenconfig_test.go
Normal file
75
internal/connlimiter/listenconfig_test.go
Normal 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)
|
||||||
|
}
|
139
internal/connlimiter/listener.go
Normal file
139
internal/connlimiter/listener.go
Normal 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
|
||||||
|
}
|
@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
|
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
@ -303,8 +304,8 @@ func (cc *Consul) serveCheckTest(ctx context.Context, w http.ResponseWriter, r *
|
|||||||
}
|
}
|
||||||
|
|
||||||
h := w.Header()
|
h := w.Header()
|
||||||
h.Set(agdhttp.HdrNameContentType, agdhttp.HdrValApplicationJSON)
|
h.Set(httphdr.ContentType, agdhttp.HdrValApplicationJSON)
|
||||||
h.Set(agdhttp.HdrNameAccessControlAllowOrigin, agdhttp.HdrValWildcard)
|
h.Set(httphdr.AccessControlAllowOrigin, agdhttp.HdrValWildcard)
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(inf)
|
err = json.NewEncoder(w).Encode(inf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||||
@ -15,6 +16,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agdtest"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsdb"
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnsdb"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/dnsservertest"
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/dnsservertest"
|
||||||
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -39,9 +41,9 @@ func TestBolt_ServeHTTP(t *testing.T) {
|
|||||||
const dname = "some-domain.name"
|
const dname = "some-domain.name"
|
||||||
|
|
||||||
successHdr := http.Header{
|
successHdr := http.Header{
|
||||||
agdhttp.HdrNameContentType: []string{agdhttp.HdrValTextCSV},
|
httphdr.ContentType: []string{agdhttp.HdrValTextCSV},
|
||||||
agdhttp.HdrNameTrailer: []string{agdhttp.HdrNameXError},
|
httphdr.Trailer: []string{httphdr.XError},
|
||||||
agdhttp.HdrNameContentEncoding: []string{"gzip"},
|
httphdr.ContentEncoding: []string{"gzip"},
|
||||||
}
|
}
|
||||||
|
|
||||||
newMsg := func(rcode int, name string, qtype uint16) (m *dns.Msg) {
|
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 {
|
for _, m := range msgs {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
db.Record(ctx, m, &agd.RequestInfo{
|
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())
|
err := db.Refresh(context.Background())
|
||||||
@ -117,7 +122,7 @@ func TestBolt_ServeHTTP(t *testing.T) {
|
|||||||
(&url.URL{Scheme: "http", Host: "example.com"}).String(),
|
(&url.URL{Scheme: "http", Host: "example.com"}).String(),
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
r.Header.Add(agdhttp.HdrNameAcceptEncoding, "gzip")
|
r.Header.Add(httphdr.AcceptEncoding, "gzip")
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
db := newTmpBolt(t)
|
db := newTmpBolt(t)
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agd"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
|
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
"go.etcd.io/bbolt"
|
"go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ func (db *Bolt) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h := w.Header()
|
h := w.Header()
|
||||||
h.Add(agdhttp.HdrNameContentType, agdhttp.HdrValTextCSV)
|
h.Add(httphdr.ContentType, agdhttp.HdrValTextCSV)
|
||||||
|
|
||||||
if dbPath == "" {
|
if dbPath == "" {
|
||||||
// No data.
|
// No data.
|
||||||
@ -47,10 +48,10 @@ func (db *Bolt) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.Set(agdhttp.HdrNameTrailer, agdhttp.HdrNameXError)
|
h.Set(httphdr.Trailer, httphdr.XError)
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
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)
|
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
|
var rw io.Writer = w
|
||||||
// TODO(a.garipov): Consider parsing the quality value.
|
// TODO(a.garipov): Consider parsing the quality value.
|
||||||
if strings.Contains(r.Header.Get(agdhttp.HdrNameAcceptEncoding), "gzip") {
|
if strings.Contains(r.Header.Get(httphdr.AcceptEncoding), "gzip") {
|
||||||
h.Set(agdhttp.HdrNameContentEncoding, "gzip")
|
h.Set(httphdr.ContentEncoding, "gzip")
|
||||||
gw := gzip.NewWriter(w)
|
gw := gzip.NewWriter(w)
|
||||||
defer func() { err = errors.WithDeferred(err, gw.Close()) }()
|
defer func() { err = errors.WithDeferred(err, gw.Close()) }()
|
||||||
|
|
||||||
|
@ -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.
|
// 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{
|
return dns.RR_Header{
|
||||||
Name: req.Question[0].Name,
|
Name: req.Question[0].Name,
|
||||||
Rrtype: rrType,
|
Rrtype: rrType,
|
||||||
Ttl: uint32(c.fltRespTTL.Seconds()),
|
Ttl: uint32(c.fltRespTTL.Seconds()),
|
||||||
Class: uint16(class),
|
Class: uint16(cl),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
27
internal/dnsserver/cache/cache_test.go
vendored
27
internal/dnsserver/cache/cache_test.go
vendored
@ -23,14 +23,8 @@ func TestMiddleware_Wrap(t *testing.T) {
|
|||||||
|
|
||||||
aReq := dnsservertest.NewReq(reqHostname, dns.TypeA, dns.ClassINET)
|
aReq := dnsservertest.NewReq(reqHostname, dns.TypeA, dns.ClassINET)
|
||||||
cnameReq := dnsservertest.NewReq(reqHostname, dns.TypeCNAME, dns.ClassINET)
|
cnameReq := dnsservertest.NewReq(reqHostname, dns.TypeCNAME, dns.ClassINET)
|
||||||
cnameAns := dnsservertest.RRSection{
|
cnameAns := dnsservertest.SectionAnswer{dnsservertest.NewCNAME(reqHostname, 3600, reqCname)}
|
||||||
RRs: []dns.RR{dnsservertest.NewCNAME(reqHostname, 3600, reqCname)},
|
soaNs := dnsservertest.SectionNs{dnsservertest.NewSOA(reqHostname, 3600, reqNs1, reqNs2)}
|
||||||
Sec: dnsservertest.SectionAnswer,
|
|
||||||
}
|
|
||||||
soaNs := dnsservertest.RRSection{
|
|
||||||
RRs: []dns.RR{dnsservertest.NewSOA(reqHostname, 3600, reqNs1, reqNs2)},
|
|
||||||
Sec: dnsservertest.SectionNs,
|
|
||||||
}
|
|
||||||
|
|
||||||
const N = 5
|
const N = 5
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
@ -40,8 +34,8 @@ func TestMiddleware_Wrap(t *testing.T) {
|
|||||||
wantNumReq int
|
wantNumReq int
|
||||||
}{{
|
}{{
|
||||||
req: aReq,
|
req: aReq,
|
||||||
resp: dnsservertest.NewResp(dns.RcodeSuccess, aReq, dnsservertest.RRSection{
|
resp: dnsservertest.NewResp(dns.RcodeSuccess, aReq, dnsservertest.SectionAnswer{
|
||||||
RRs: []dns.RR{dnsservertest.NewA(reqHostname, 3600, net.IP{1, 2, 3, 4})},
|
dnsservertest.NewA(reqHostname, 3600, net.IP{1, 2, 3, 4}),
|
||||||
}),
|
}),
|
||||||
name: "simple_a",
|
name: "simple_a",
|
||||||
wantNumReq: 1,
|
wantNumReq: 1,
|
||||||
@ -67,9 +61,8 @@ func TestMiddleware_Wrap(t *testing.T) {
|
|||||||
wantNumReq: N,
|
wantNumReq: N,
|
||||||
}, {
|
}, {
|
||||||
req: aReq,
|
req: aReq,
|
||||||
resp: dnsservertest.NewResp(dns.RcodeNameError, aReq, dnsservertest.RRSection{
|
resp: dnsservertest.NewResp(dns.RcodeNameError, aReq, dnsservertest.SectionNs{
|
||||||
RRs: []dns.RR{dnsservertest.NewNS(reqHostname, 3600, reqNs1)},
|
dnsservertest.NewNS(reqHostname, 3600, reqNs1),
|
||||||
Sec: dnsservertest.SectionNs,
|
|
||||||
}),
|
}),
|
||||||
name: "non_authoritative_nxdomain",
|
name: "non_authoritative_nxdomain",
|
||||||
// TODO(ameshkov): Consider https://datatracker.ietf.org/doc/html/rfc2308#section-3.
|
// TODO(ameshkov): Consider https://datatracker.ietf.org/doc/html/rfc2308#section-3.
|
||||||
@ -86,15 +79,15 @@ func TestMiddleware_Wrap(t *testing.T) {
|
|||||||
wantNumReq: 1,
|
wantNumReq: 1,
|
||||||
}, {
|
}, {
|
||||||
req: cnameReq,
|
req: cnameReq,
|
||||||
resp: dnsservertest.NewResp(dns.RcodeSuccess, cnameReq, dnsservertest.RRSection{
|
resp: dnsservertest.NewResp(dns.RcodeSuccess, cnameReq, dnsservertest.SectionAnswer{
|
||||||
RRs: []dns.RR{dnsservertest.NewCNAME(reqHostname, 3600, reqCname)},
|
dnsservertest.NewCNAME(reqHostname, 3600, reqCname),
|
||||||
}),
|
}),
|
||||||
name: "simple_cname_ans",
|
name: "simple_cname_ans",
|
||||||
wantNumReq: 1,
|
wantNumReq: 1,
|
||||||
}, {
|
}, {
|
||||||
req: aReq,
|
req: aReq,
|
||||||
resp: dnsservertest.NewResp(dns.RcodeSuccess, aReq, dnsservertest.RRSection{
|
resp: dnsservertest.NewResp(dns.RcodeSuccess, aReq, dnsservertest.SectionAnswer{
|
||||||
RRs: []dns.RR{dnsservertest.NewA(reqHostname, 0, net.IP{1, 2, 3, 4})},
|
dnsservertest.NewA(reqHostname, 0, net.IP{1, 2, 3, 4}),
|
||||||
}),
|
}),
|
||||||
name: "expired_one",
|
name: "expired_one",
|
||||||
wantNumReq: N,
|
wantNumReq: N,
|
||||||
|
@ -2,14 +2,19 @@ package dnsservertest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/miekg/dns"
|
"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) {
|
func CreateTestHandler(recordsCount int) (h dnsserver.Handler) {
|
||||||
f := func(ctx context.Context, rw dnsserver.ResponseWriter, req *dns.Msg) (err error) {
|
f := func(ctx context.Context, rw dnsserver.ResponseWriter, req *dns.Msg) (err error) {
|
||||||
// Check that necessary context keys are set.
|
// 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")
|
return errors.Error("client info does not contain server name")
|
||||||
}
|
}
|
||||||
|
|
||||||
hostname := req.Question[0].Name
|
ans := make(SectionAnswer, 0, recordsCount)
|
||||||
|
hdr := dns.RR_Header{
|
||||||
resp := &dns.Msg{
|
Name: req.Question[0].Name,
|
||||||
Compress: true,
|
Rrtype: dns.TypeA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: uint32(AnswerTTL.Seconds()),
|
||||||
}
|
}
|
||||||
resp.SetReply(req)
|
|
||||||
|
|
||||||
|
ip := netutil.IPv4Localhost().Prev()
|
||||||
for i := 0; i < recordsCount; i++ {
|
for i := 0; i < recordsCount; i++ {
|
||||||
hdr := dns.RR_Header{
|
// Add 1 to make sure that each IP is valid.
|
||||||
Name: hostname,
|
ip = ip.Next()
|
||||||
Rrtype: dns.TypeA,
|
ans = append(ans, &dns.A{Hdr: hdr, A: ip.AsSlice()})
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resp := NewResp(dns.RcodeSuccess, req, ans)
|
||||||
|
|
||||||
_ = rw.WriteMsg(ctx, req, resp)
|
_ = rw.WriteMsg(ctx, req, resp)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -4,23 +4,17 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateMessage creates a DNS message for the specified hostname and qtype.
|
// CreateMessage creates a DNS message for the specified hostname and qtype.
|
||||||
func CreateMessage(hostname string, qtype uint16) (m *dns.Msg) {
|
func CreateMessage(hostname string, qtype uint16) (m *dns.Msg) {
|
||||||
return &dns.Msg{
|
m = NewReq(hostname, qtype, dns.ClassINET)
|
||||||
MsgHdr: dns.MsgHdr{
|
m.RecursionDesired = true
|
||||||
Id: dns.Id(),
|
|
||||||
RecursionDesired: true,
|
return m
|
||||||
},
|
|
||||||
Question: []dns.Question{{
|
|
||||||
Name: dns.Fqdn(hostname),
|
|
||||||
Qtype: qtype,
|
|
||||||
Qclass: dns.ClassINET,
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequireResponse checks that the DNS response we received is what was
|
// RequireResponse checks that the DNS response we received is what was
|
||||||
@ -29,9 +23,9 @@ func RequireResponse(
|
|||||||
t *testing.T,
|
t *testing.T,
|
||||||
req *dns.Msg,
|
req *dns.Msg,
|
||||||
resp *dns.Msg,
|
resp *dns.Msg,
|
||||||
expectedRecordsCount int,
|
wantAnsLen int,
|
||||||
expectedRCode int,
|
wantRCode int,
|
||||||
expectedTruncated bool,
|
wantTruncated bool,
|
||||||
) {
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@ -40,26 +34,64 @@ func RequireResponse(
|
|||||||
// Check that Opcode is not changed in the response
|
// Check that Opcode is not changed in the response
|
||||||
// regardless of the response status
|
// regardless of the response status
|
||||||
require.Equal(t, req.Opcode, resp.Opcode)
|
require.Equal(t, req.Opcode, resp.Opcode)
|
||||||
require.Equal(t, expectedRCode, resp.Rcode)
|
require.Equal(t, wantRCode, resp.Rcode)
|
||||||
require.Equal(t, expectedTruncated, resp.Truncated)
|
require.Equal(t, wantTruncated, resp.Truncated)
|
||||||
require.True(t, resp.Response)
|
require.True(t, resp.Response)
|
||||||
// Response must not have a Z flag set even for a query that does
|
// Response must not have a Z flag set even for a query that does
|
||||||
// See https://github.com/miekg/dns/issues/975
|
// See https://github.com/miekg/dns/issues/975
|
||||||
require.False(t, resp.Zero)
|
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
|
// Check that there's an OPT record in the response
|
||||||
if len(req.Extra) > 0 {
|
if len(req.Extra) > 0 {
|
||||||
require.True(t, len(resp.Extra) > 0)
|
require.NotEmpty(t, resp.Extra)
|
||||||
}
|
}
|
||||||
|
|
||||||
if expectedRecordsCount > 0 {
|
if wantAnsLen > 0 {
|
||||||
a, ok := resp.Answer[0].(*dns.A)
|
a := testutil.RequireTypeAssert[*dns.A](t, resp.Answer[0])
|
||||||
require.True(t, ok)
|
|
||||||
require.Equal(t, req.Question[0].Name, a.Hdr.Name)
|
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,
|
// NewReq returns the new DNS request with a single question for name, qtype,
|
||||||
// qclass, and rrs added.
|
// qclass, and rrs added.
|
||||||
func NewReq(name string, qtype, qclass uint16, rrs ...RRSection) (req *dns.Msg) {
|
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(),
|
Id: dns.Id(),
|
||||||
},
|
},
|
||||||
Question: []dns.Question{{
|
Question: []dns.Question{{
|
||||||
Name: name,
|
Name: dns.Fqdn(name),
|
||||||
Qtype: qtype,
|
Qtype: qtype,
|
||||||
Qclass: qclass,
|
Qclass: qclass,
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
withRRs(req, rrs...)
|
for _, rr := range rrs {
|
||||||
|
rr.appendTo(req)
|
||||||
|
}
|
||||||
|
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
@ -86,50 +120,13 @@ func NewResp(rcode int, req *dns.Msg, rrs ...RRSection) (resp *dns.Msg) {
|
|||||||
resp.RecursionAvailable = true
|
resp.RecursionAvailable = true
|
||||||
resp.Compress = true
|
resp.Compress = true
|
||||||
|
|
||||||
withRRs(resp, rrs...)
|
for _, rr := range rrs {
|
||||||
|
rr.appendTo(resp)
|
||||||
|
}
|
||||||
|
|
||||||
return 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.
|
// NewCNAME constructs the new resource record of type CNAME.
|
||||||
func NewCNAME(name string, ttl uint32, target string) (rr dns.RR) {
|
func NewCNAME(name string, ttl uint32, target string) (rr dns.RR) {
|
||||||
return &dns.CNAME{
|
return &dns.CNAME{
|
||||||
|
76
internal/dnsserver/dnsservertest/msg_test.go
Normal file
76
internal/dnsserver/dnsservertest/msg_test.go
Normal 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.
|
||||||
|
}
|
@ -59,7 +59,7 @@ func ExampleWithMiddlewares() {
|
|||||||
forwarder := forward.NewHandler(&forward.HandlerConfig{
|
forwarder := forward.NewHandler(&forward.HandlerConfig{
|
||||||
Address: netip.MustParseAddrPort("94.140.14.140:53"),
|
Address: netip.MustParseAddrPort("94.140.14.140:53"),
|
||||||
Network: forward.NetworkAny,
|
Network: forward.NetworkAny,
|
||||||
}, true)
|
})
|
||||||
|
|
||||||
middleware := querylog.NewLogMiddleware(os.Stdout)
|
middleware := querylog.NewLogMiddleware(os.Stdout)
|
||||||
handler := dnsserver.WithMiddlewares(forwarder, middleware)
|
handler := dnsserver.WithMiddlewares(forwarder, middleware)
|
||||||
|
@ -20,7 +20,7 @@ func ExampleNewHandler() {
|
|||||||
FallbackAddresses: []netip.AddrPort{
|
FallbackAddresses: []netip.AddrPort{
|
||||||
netip.MustParseAddrPort("1.1.1.1:53"),
|
netip.MustParseAddrPort("1.1.1.1:53"),
|
||||||
},
|
},
|
||||||
}, false),
|
}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,20 +38,6 @@ import (
|
|||||||
// queries to the specified upstreams. It also implements [io.Closer], allowing
|
// queries to the specified upstreams. It also implements [io.Closer], allowing
|
||||||
// resource reuse.
|
// resource reuse.
|
||||||
type Handler struct {
|
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 is a listener for the handler events.
|
||||||
metrics MetricsListener
|
metrics MetricsListener
|
||||||
|
|
||||||
@ -65,12 +51,21 @@ type Handler struct {
|
|||||||
// fallbacks is a list of fallback DNS servers.
|
// fallbacks is a list of fallback DNS servers.
|
||||||
fallbacks []Upstream
|
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 specifies the query timeout for upstreams and fallbacks.
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
|
|
||||||
// hcBackoffTime specifies the delay before returning to the main upstream
|
// hcBackoffTime specifies the delay before returning to the main upstream
|
||||||
// after failed healthcheck probe.
|
// after failed healthcheck probe.
|
||||||
hcBackoff time.Duration
|
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
|
// 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
|
// upstream until this time has passed. If the healthcheck is still
|
||||||
// performed, each failed check advances the backoff.
|
// performed, each failed check advances the backoff.
|
||||||
HealthcheckBackoffDuration time.Duration
|
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
|
// NewHandler initializes a new instance of Handler. It also performs a health
|
||||||
// check afterwards if initialHealthcheck is true. Note, that this handler only
|
// check afterwards if c.HealthcheckInitDuration is not zero. Note, that this
|
||||||
// support plain DNS upstreams. c must not be nil.
|
// handler only support plain DNS upstreams. c must not be nil.
|
||||||
func NewHandler(c *HandlerConfig, initialHealthcheck bool) (h *Handler) {
|
func NewHandler(c *HandlerConfig) (h *Handler) {
|
||||||
h = &Handler{
|
h = &Handler{
|
||||||
upstream: NewUpstreamPlain(c.Address, c.Network),
|
upstream: NewUpstreamPlain(&UpstreamPlainConfig{
|
||||||
|
Network: c.Network,
|
||||||
|
Address: c.Address,
|
||||||
|
}),
|
||||||
hcDomainTmpl: c.HealthcheckDomainTmpl,
|
hcDomainTmpl: c.HealthcheckDomainTmpl,
|
||||||
timeout: c.Timeout,
|
timeout: c.Timeout,
|
||||||
hcBackoff: c.HealthcheckBackoffDuration,
|
hcBackoff: c.HealthcheckBackoffDuration,
|
||||||
@ -134,13 +136,19 @@ func NewHandler(c *HandlerConfig, initialHealthcheck bool) (h *Handler) {
|
|||||||
|
|
||||||
h.fallbacks = make([]Upstream, len(c.FallbackAddresses))
|
h.fallbacks = make([]Upstream, len(c.FallbackAddresses))
|
||||||
for i, addr := range 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
|
// Ignore the error since it's considered non-critical and also should
|
||||||
// have been logged already.
|
// have been logged already.
|
||||||
_ = h.refresh(context.Background(), true)
|
_ = h.refresh(ctx, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return h
|
return h
|
||||||
@ -176,7 +184,7 @@ func (h *Handler) ServeDNS(
|
|||||||
) (err error) {
|
) (err error) {
|
||||||
defer func() { err = annotate(err, h.upstream) }()
|
defer func() { err = annotate(err, h.upstream) }()
|
||||||
|
|
||||||
useFallbacks := atomic.LoadUint64(&h.useFallbacks) != 0
|
useFallbacks := h.useFallbacks.Load()
|
||||||
var resp *dns.Msg
|
var resp *dns.Msg
|
||||||
if !useFallbacks {
|
if !useFallbacks {
|
||||||
resp, err = h.exchange(ctx, h.upstream, req)
|
resp, err = h.exchange(ctx, h.upstream, req)
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/dnsservertest"
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/dnsservertest"
|
||||||
@ -17,6 +18,21 @@ func TestMain(m *testing.M) {
|
|||||||
testutil.DiscardLogOutput(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) {
|
func TestHandler_ServeDNS(t *testing.T) {
|
||||||
srv, addr := dnsservertest.RunDNSServer(t, dnsservertest.DefaultHandler())
|
srv, addr := dnsservertest.RunDNSServer(t, dnsservertest.DefaultHandler())
|
||||||
|
|
||||||
@ -24,13 +40,14 @@ func TestHandler_ServeDNS(t *testing.T) {
|
|||||||
handler := forward.NewHandler(&forward.HandlerConfig{
|
handler := forward.NewHandler(&forward.HandlerConfig{
|
||||||
Address: netip.MustParseAddrPort(addr),
|
Address: netip.MustParseAddrPort(addr),
|
||||||
Network: forward.NetworkAny,
|
Network: forward.NetworkAny,
|
||||||
}, true)
|
Timeout: testTimeout,
|
||||||
|
})
|
||||||
|
|
||||||
req := dnsservertest.CreateMessage("example.org.", dns.TypeA)
|
req := dnsservertest.CreateMessage("example.org.", dns.TypeA)
|
||||||
rw := dnsserver.NewNonWriterResponseWriter(srv.LocalUDPAddr(), srv.LocalUDPAddr())
|
rw := dnsserver.NewNonWriterResponseWriter(srv.LocalUDPAddr(), srv.LocalUDPAddr())
|
||||||
|
|
||||||
// Check the handler's ServeDNS method
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
res := rw.Msg()
|
res := rw.Msg()
|
||||||
@ -46,7 +63,8 @@ func TestHandler_ServeDNS_fallbackNetError(t *testing.T) {
|
|||||||
FallbackAddresses: []netip.AddrPort{
|
FallbackAddresses: []netip.AddrPort{
|
||||||
netip.MustParseAddrPort(srv.LocalUDPAddr().String()),
|
netip.MustParseAddrPort(srv.LocalUDPAddr().String()),
|
||||||
},
|
},
|
||||||
}, true)
|
Timeout: testTimeout,
|
||||||
|
})
|
||||||
|
|
||||||
req := dnsservertest.CreateMessage("example.org.", dns.TypeA)
|
req := dnsservertest.CreateMessage("example.org.", dns.TypeA)
|
||||||
rw := dnsserver.NewNonWriterResponseWriter(srv.LocalUDPAddr(), srv.LocalUDPAddr())
|
rw := dnsserver.NewNonWriterResponseWriter(srv.LocalUDPAddr(), srv.LocalUDPAddr())
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
@ -25,25 +24,25 @@ func (h *Handler) refresh(ctx context.Context, shouldReport bool) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var useFallbacks uint64
|
var useFallbacks bool
|
||||||
lastFailed := atomic.LoadInt64(&h.lastFailedHealthcheck)
|
lastFailed := h.lastFailedHealthcheck.Load()
|
||||||
shouldReturnToMain := time.Since(time.Unix(lastFailed, 0)) >= h.hcBackoff
|
shouldReturnToMain := time.Since(time.Unix(lastFailed, 0)) >= h.hcBackoff
|
||||||
if !shouldReturnToMain {
|
if !shouldReturnToMain {
|
||||||
// Make sure that useFallbacks is left true if the main upstream is
|
// Make sure that useFallbacks is left true if the main upstream is
|
||||||
// still in the backoff mode.
|
// still in the backoff mode.
|
||||||
useFallbacks = 1
|
useFallbacks = true
|
||||||
log.Debug("forward: healthcheck: in backoff, will not return to main on success")
|
log.Debug("forward: healthcheck: in backoff, will not return to main on success")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.healthcheck(ctx)
|
err = h.healthcheck(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
atomic.StoreInt64(&h.lastFailedHealthcheck, time.Now().Unix())
|
h.lastFailedHealthcheck.Store(time.Now().Unix())
|
||||||
useFallbacks = 1
|
useFallbacks = true
|
||||||
}
|
}
|
||||||
|
|
||||||
statusChanged := atomic.CompareAndSwapUint64(&h.useFallbacks, 1-useFallbacks, useFallbacks)
|
statusChanged := h.useFallbacks.CompareAndSwap(!useFallbacks, useFallbacks)
|
||||||
if statusChanged || shouldReport {
|
if statusChanged || shouldReport {
|
||||||
h.setUpstreamStatus(useFallbacks == 0)
|
h.setUpstreamStatus(!useFallbacks)
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Annotate(err, "forward: %w")
|
return errors.Annotate(err, "forward: %w")
|
||||||
|
@ -15,8 +15,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestHandler_Refresh(t *testing.T) {
|
func TestHandler_Refresh(t *testing.T) {
|
||||||
var upstreamUp uint64
|
var upstreamIsUp atomic.Bool
|
||||||
var upstreamRequestsCount uint64
|
var upstreamRequestsCount atomic.Int64
|
||||||
|
|
||||||
|
defaultHandler := dnsservertest.DefaultHandler()
|
||||||
|
|
||||||
// This handler writes an empty message if upstreamUp flag is false.
|
// This handler writes an empty message if upstreamUp flag is false.
|
||||||
handlerFunc := dnsserver.HandlerFunc(func(
|
handlerFunc := dnsserver.HandlerFunc(func(
|
||||||
@ -24,16 +26,15 @@ func TestHandler_Refresh(t *testing.T) {
|
|||||||
rw dnsserver.ResponseWriter,
|
rw dnsserver.ResponseWriter,
|
||||||
req *dns.Msg,
|
req *dns.Msg,
|
||||||
) (err error) {
|
) (err error) {
|
||||||
atomic.AddUint64(&upstreamRequestsCount, 1)
|
upstreamRequestsCount.Add(1)
|
||||||
|
|
||||||
nrw := dnsserver.NewNonWriterResponseWriter(rw.LocalAddr(), rw.RemoteAddr())
|
nrw := dnsserver.NewNonWriterResponseWriter(rw.LocalAddr(), rw.RemoteAddr())
|
||||||
handler := dnsservertest.DefaultHandler()
|
err = defaultHandler.ServeDNS(ctx, nrw, req)
|
||||||
err = handler.ServeDNS(ctx, nrw, req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if atomic.LoadUint64(&upstreamUp) == 0 {
|
if !upstreamIsUp.Load() {
|
||||||
return rw.WriteMsg(ctx, req, &dns.Msg{})
|
return rw.WriteMsg(ctx, req, &dns.Msg{})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +42,7 @@ func TestHandler_Refresh(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
upstream, _ := dnsservertest.RunDNSServer(t, handlerFunc)
|
upstream, _ := dnsservertest.RunDNSServer(t, handlerFunc)
|
||||||
fallback, _ := dnsservertest.RunDNSServer(t, dnsservertest.DefaultHandler())
|
fallback, _ := dnsservertest.RunDNSServer(t, defaultHandler)
|
||||||
handler := forward.NewHandler(&forward.HandlerConfig{
|
handler := forward.NewHandler(&forward.HandlerConfig{
|
||||||
Address: netip.MustParseAddrPort(upstream.LocalUDPAddr().String()),
|
Address: netip.MustParseAddrPort(upstream.LocalUDPAddr().String()),
|
||||||
Network: forward.NetworkAny,
|
Network: forward.NetworkAny,
|
||||||
@ -49,38 +50,41 @@ func TestHandler_Refresh(t *testing.T) {
|
|||||||
FallbackAddresses: []netip.AddrPort{
|
FallbackAddresses: []netip.AddrPort{
|
||||||
netip.MustParseAddrPort(fallback.LocalUDPAddr().String()),
|
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.
|
// immediately.
|
||||||
HealthcheckBackoffDuration: 0,
|
HealthcheckBackoffDuration: 0,
|
||||||
}, false)
|
})
|
||||||
|
|
||||||
req := dnsservertest.CreateMessage("example.org.", dns.TypeA)
|
req := dnsservertest.CreateMessage("example.org.", dns.TypeA)
|
||||||
rw := dnsserver.NewNonWriterResponseWriter(fallback.LocalUDPAddr(), fallback.LocalUDPAddr())
|
rw := dnsserver.NewNonWriterResponseWriter(fallback.LocalUDPAddr(), fallback.LocalUDPAddr())
|
||||||
|
|
||||||
err := handler.ServeDNS(context.Background(), rw, req)
|
ctx := context.Background()
|
||||||
require.Error(t, err)
|
|
||||||
assert.Equal(t, uint64(1), atomic.LoadUint64(&upstreamRequestsCount))
|
|
||||||
|
|
||||||
err = handler.Refresh(context.Background())
|
err := handler.ServeDNS(newTimeoutCtx(t, ctx), rw, req)
|
||||||
require.Error(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, uint64(2), atomic.LoadUint64(&upstreamRequestsCount))
|
assert.Equal(t, int64(4), upstreamRequestsCount.Load())
|
||||||
|
|
||||||
// Now, set upstream up.
|
// 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)
|
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)
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, uint64(4), atomic.LoadUint64(&upstreamRequestsCount))
|
assert.Equal(t, int64(6), upstreamRequestsCount.Load())
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -15,7 +16,7 @@ import (
|
|||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Network is a enumeration of networks UpstreamPlain supports
|
// Network is a enumeration of networks UpstreamPlain supports.
|
||||||
type Network string
|
type Network string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -72,11 +73,21 @@ type UpstreamPlain struct {
|
|||||||
// type check
|
// type check
|
||||||
var _ Upstream = (*UpstreamPlain)(nil)
|
var _ Upstream = (*UpstreamPlain)(nil)
|
||||||
|
|
||||||
// NewUpstreamPlain creates and initializes a new instance of UpstreamPlain.
|
// UpstreamPlainConfig is the configuration structure for a plain-DNS upstream.
|
||||||
func NewUpstreamPlain(addr netip.AddrPort, network Network) (ups *UpstreamPlain) {
|
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{
|
ups = &UpstreamPlain{
|
||||||
addr: addr,
|
addr: c.Address,
|
||||||
network: network,
|
network: c.Network,
|
||||||
}
|
}
|
||||||
|
|
||||||
ups.connsPoolUDP = pool.NewPool(poolMaxCapacity, makeConnsPoolFactory(ups, NetworkUDP))
|
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)
|
return fmt.Sprintf("%s://%s", u.network, u.addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// exchangeUDP attempts to send the DNS request over UDP. It returns a
|
// exchangeUDP attempts to send the DNS request over UDP. It returns a
|
||||||
// fallbackToTCP flag to signal if we should fallback to using TCP instead.
|
// fallbackToTCP flag to signal if the caller should fallback to using TCP
|
||||||
// this may happen if the response received over UDP was truncated and
|
// instead. This may happen if the response received over UDP was truncated and
|
||||||
// TCP is enabled for this upstream or if UDP is disabled.
|
// TCP is enabled for this upstream or if UDP is disabled.
|
||||||
func (u *UpstreamPlain) exchangeUDP(
|
func (u *UpstreamPlain) exchangeUDP(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req *dns.Msg,
|
req *dns.Msg,
|
||||||
) (fallbackToTCP bool, resp *dns.Msg, err error) {
|
) (fallbackToTCP bool, resp *dns.Msg, err error) {
|
||||||
if u.network == NetworkTCP {
|
if u.network == NetworkTCP {
|
||||||
// fallback to TCP immediately.
|
// Fallback to TCP immediately.
|
||||||
return true, nil, nil
|
return true, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = u.exchangeNet(ctx, req, NetworkUDP)
|
resp, err = u.exchangeNet(ctx, req, NetworkUDP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// error means that the upstream is dead, no need to fallback to TCP.
|
// The network error always causes the subsequent query attempt using
|
||||||
return false, resp, err
|
// 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
|
// Also, fallback to TCP if the received response is truncated and the
|
||||||
// fallback to TCP. We also fallback to TCP if we received a response with
|
// upstream isn't UDP-only.
|
||||||
// the wrong ID (it may happen with the servers under heavy load).
|
fallbackToTCP = u.network != NetworkUDP && resp != nil && resp.Truncated
|
||||||
if (resp.Truncated || resp.Id != req.Id) && u.network != NetworkUDP {
|
|
||||||
fallbackToTCP = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallbackToTCP, resp, err
|
return fallbackToTCP, resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// exchangeNet sends a DNS query using the specified network (either TCP or UDP).
|
// exchangeNet sends a DNS query using the specified network (either TCP or UDP).
|
||||||
@ -203,25 +220,36 @@ func (u *UpstreamPlain) exchangeNet(
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateResponse checks if the response is valid for the original query. For
|
// validatePlainResponse returns an error if the response is not valid for the
|
||||||
// instance, it is possible to receive a response to a different query, and we
|
// original request. This is required because we might receive a response to a
|
||||||
// must be sure that we received what was expected.
|
// different query, e.g. when the server is under heavy load.
|
||||||
func (u *UpstreamPlain) validateResponse(req, resp *dns.Msg) (err error) {
|
func validatePlainResponse(req, resp *dns.Msg) (err error) {
|
||||||
if req.Id != resp.Id {
|
if req.Id != resp.Id {
|
||||||
return dns.ErrId
|
return dns.ErrId
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(resp.Question) != 1 {
|
if qlen := len(resp.Question); qlen != 1 {
|
||||||
return ErrQuestion
|
return fmt.Errorf("%w: only 1 question allowed; got %d", ErrQuestion, qlen)
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Question[0].Name != resp.Question[0].Name {
|
reqQ, respQ := req.Question[0], resp.Question[0]
|
||||||
return ErrQuestion
|
|
||||||
|
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
|
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
|
// 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
|
// 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
|
// 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,
|
req *dns.Msg,
|
||||||
buf []byte,
|
buf []byte,
|
||||||
bufLen int,
|
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
|
// Make sure that we return the connection to the pool in the end or close
|
||||||
// if there was any error.
|
// if there was any error.
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -248,7 +276,12 @@ func (u *UpstreamPlain) processConn(
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Prepare a context with a deadline if needed.
|
// 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)
|
err = conn.SetDeadline(deadline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("setting deadline: %w", err)
|
return nil, fmt.Errorf("setting deadline: %w", err)
|
||||||
@ -261,19 +294,28 @@ func (u *UpstreamPlain) processConn(
|
|||||||
return nil, fmt.Errorf("writing request: %w", err)
|
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)
|
resp, err = u.readMsg(network, conn, buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Error is already wrapped.
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = u.validateResponse(req, resp)
|
err = validatePlainResponse(req, resp)
|
||||||
if err != nil {
|
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.
|
// 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 {
|
if n < minDNSMessageSize {
|
||||||
return nil, fmt.Errorf("invalid msg: %w", dns.ErrShortRead)
|
return nil, fmt.Errorf("invalid msg: %w", dns.ErrShortRead)
|
||||||
}
|
}
|
||||||
ret := new(dns.Msg)
|
|
||||||
|
ret := &dns.Msg{}
|
||||||
err = ret.Unpack(buf)
|
err = ret.Unpack(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unpacking msg: %w", err)
|
return nil, fmt.Errorf("unpacking msg: %w", err)
|
||||||
|
@ -5,11 +5,14 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnsmsg"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/dnsservertest"
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/dnsservertest"
|
||||||
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/forward"
|
"github.com/AdguardTeam/AdGuardDNS/internal/dnsserver/forward"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -31,11 +34,14 @@ func TestUpstreamPlain_Exchange(t *testing.T) {
|
|||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
_, addr := dnsservertest.RunDNSServer(t, dnsservertest.DefaultHandler())
|
_, 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)
|
defer log.OnCloserError(u, log.DEBUG)
|
||||||
|
|
||||||
req := dnsservertest.CreateMessage("example.org.", dns.TypeA)
|
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.NoError(t, err)
|
||||||
require.NotNil(t, res)
|
require.NotNil(t, res)
|
||||||
dnsservertest.RequireResponse(t, req, res, 1, dns.RcodeSuccess, false)
|
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)
|
return rw.WriteMsg(ctx, req, res)
|
||||||
})
|
})
|
||||||
|
|
||||||
_, addr := dnsservertest.RunDNSServer(t, handlerFunc)
|
_, addrStr := dnsservertest.RunDNSServer(t, handlerFunc)
|
||||||
|
|
||||||
// Create a test message.
|
// Create a test message.
|
||||||
req := dnsservertest.CreateMessage("example.org.", dns.TypeA)
|
req := dnsservertest.CreateMessage("example.org.", dns.TypeA)
|
||||||
|
|
||||||
// First, check that we receive truncated response over UDP.
|
// First, check that we receive truncated response over UDP.
|
||||||
uAddr := netip.MustParseAddrPort(addr)
|
addr := netip.MustParseAddrPort(addrStr)
|
||||||
uUDP := forward.NewUpstreamPlain(uAddr, forward.NetworkUDP)
|
uUDP := forward.NewUpstreamPlain(&forward.UpstreamPlainConfig{
|
||||||
|
Network: forward.NetworkUDP,
|
||||||
|
Address: addr,
|
||||||
|
})
|
||||||
defer log.OnCloserError(uUDP, log.DEBUG)
|
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)
|
require.NoError(t, err)
|
||||||
dnsservertest.RequireResponse(t, req, res, 0, dns.RcodeSuccess, true)
|
dnsservertest.RequireResponse(t, req, res, 0, dns.RcodeSuccess, true)
|
||||||
|
|
||||||
// Second, check that nothing is truncated over TCP.
|
// 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)
|
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)
|
require.NoError(t, err)
|
||||||
dnsservertest.RequireResponse(t, req, res, 1, dns.RcodeSuccess, false)
|
dnsservertest.RequireResponse(t, req, res, 1, dns.RcodeSuccess, false)
|
||||||
|
|
||||||
// Now with NetworkANY response is also not truncated since the upstream
|
// Now with NetworkANY response is also not truncated since the upstream
|
||||||
// fallbacks to TCP.
|
// 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)
|
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)
|
require.NoError(t, err)
|
||||||
dnsservertest.RequireResponse(t, req, res, 1, dns.RcodeSuccess, false)
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardDNS/internal/dnsserver
|
|||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
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/dnscrypt/v2 v2.2.5
|
||||||
github.com/ameshkov/dnsstamps v1.0.3
|
github.com/ameshkov/dnsstamps v1.0.3
|
||||||
github.com/bluele/gcache v0.0.2
|
github.com/bluele/gcache v0.0.2
|
||||||
@ -13,7 +13,7 @@ require (
|
|||||||
github.com/prometheus/client_golang v1.14.0
|
github.com/prometheus/client_golang v1.14.0
|
||||||
github.com/quic-go/quic-go v0.33.0
|
github.com/quic-go/quic-go v0.33.0
|
||||||
github.com/stretchr/testify v1.8.2
|
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/net v0.8.0
|
||||||
golang.org/x/sys v0.6.0
|
golang.org/x/sys v0.6.0
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
github.com/AdguardTeam/golibs v0.12.1 h1:bJfFzCnUCl+QsP6prUltM2Sjt0fTiDBPlxuAwfKP3g8=
|
github.com/AdguardTeam/golibs v0.13.2 h1:BPASsyQKmb+b8VnvsNOHp7bKfcZl9Z+Z2UhPjOiupSc=
|
||||||
github.com/AdguardTeam/golibs v0.12.1/go.mod h1:rIglKDHdLvFT1UbhumBLHO9S4cvWS9MEyT1njommI/Y=
|
|
||||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
|
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/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
||||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
|
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.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
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-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
|
||||||
golang.org/x/exp v0.0.0-20230307190834-24139beb5833/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
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 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
||||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListenConfig is the interface that allows controlling options of connections
|
// 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)
|
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
|
// DefaultListenConfig returns the default [ListenConfig] used by the servers in
|
||||||
// this module except for the plain-DNS ones, which use
|
// this module except for the plain-DNS ones, which use
|
||||||
// [DefaultListenConfigWithOOB].
|
// [DefaultListenConfigWithOOB]. If conf is nil, a default configuration is
|
||||||
func DefaultListenConfig() (lc ListenConfig) {
|
// used.
|
||||||
|
func DefaultListenConfig(conf *ControlConfig) (lc ListenConfig) {
|
||||||
|
if conf == nil {
|
||||||
|
conf = defaultCtrlConf
|
||||||
|
}
|
||||||
|
|
||||||
return &net.ListenConfig{
|
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
|
// DefaultListenConfigWithOOB returns the default [ListenConfig] used by the
|
||||||
// plain-DNS servers in this module. The resulting ListenConfig sets additional
|
// plain-DNS servers in this module. The resulting ListenConfig sets additional
|
||||||
// socket flags and processes the control-messages of connections created with
|
// socket flags and processes the control-messages of connections created with
|
||||||
// ListenPacket.
|
// ListenPacket. If conf is nil, a default configuration is used.
|
||||||
func DefaultListenConfigWithOOB() (lc ListenConfig) {
|
func DefaultListenConfigWithOOB(conf *ControlConfig) (lc ListenConfig) {
|
||||||
|
if conf == nil {
|
||||||
|
conf = defaultCtrlConf
|
||||||
|
}
|
||||||
|
|
||||||
return &listenConfigOOB{
|
return &listenConfigOOB{
|
||||||
ListenConfig: net.ListenConfig{
|
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
|
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
|
||||||
|
}
|
||||||
|
@ -13,17 +13,50 @@ import (
|
|||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// defaultListenControl is used as a [net.ListenConfig.Control] function to set
|
// setSockOptFunc is a function that sets a socket option on fd.
|
||||||
// the SO_REUSEPORT socket option on all sockets used by the DNS servers in this
|
type setSockOptFunc func(fd int) (err error)
|
||||||
// package.
|
|
||||||
func defaultListenControl(_, _ string, c syscall.RawConn) (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
|
var opErr error
|
||||||
err = c.Control(func(fd uintptr) {
|
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)
|
return errors.WithDeferred(opErr, err)
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestDefaultListenConfigWithOOB(t *testing.T) {
|
func TestDefaultListenConfigWithOOB(t *testing.T) {
|
||||||
lc := netext.DefaultListenConfigWithOOB()
|
lc := netext.DefaultListenConfigWithOOB(nil)
|
||||||
require.NotNil(t, lc)
|
require.NotNil(t, lc)
|
||||||
|
|
||||||
type syscallConner interface {
|
type syscallConner interface {
|
||||||
@ -65,3 +65,81 @@ func TestDefaultListenConfigWithOOB(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -7,9 +7,9 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
// defaultListenControl is nil on Windows, because it doesn't support
|
// listenControlWithSO is nil on Windows, because it doesn't support socket
|
||||||
// SO_REUSEPORT.
|
// options.
|
||||||
var defaultListenControl func(_, _ string, _ syscall.RawConn) (_ error)
|
var listenControlWithSO func(_ *ControlConfig, _ syscall.RawConn) (_ error)
|
||||||
|
|
||||||
// setIPOpts sets the IPv4 and IPv6 options on a packet connection.
|
// setIPOpts sets the IPv4 and IPv6 options on a packet connection.
|
||||||
func setIPOpts(c net.PacketConn) (err error) {
|
func setIPOpts(c net.PacketConn) (err error) {
|
||||||
|
@ -51,7 +51,7 @@ func TestSessionPacketConn(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testSessionPacketConn(t *testing.T, proto, addr string, dstIP net.IP) (isTimeout bool) {
|
func testSessionPacketConn(t *testing.T, proto, addr string, dstIP net.IP) (isTimeout bool) {
|
||||||
lc := netext.DefaultListenConfigWithOOB()
|
lc := netext.DefaultListenConfigWithOOB(nil)
|
||||||
require.NotNil(t, lc)
|
require.NotNil(t, lc)
|
||||||
|
|
||||||
c, err := lc.ListenPacket(context.Background(), proto, addr)
|
c, err := lc.ListenPacket(context.Background(), proto, addr)
|
||||||
|
@ -24,7 +24,7 @@ func TestForwardMetricsListener_integration_request(t *testing.T) {
|
|||||||
Address: netip.MustParseAddrPort(addr),
|
Address: netip.MustParseAddrPort(addr),
|
||||||
Network: forward.NetworkAny,
|
Network: forward.NetworkAny,
|
||||||
MetricsListener: prometheus.NewForwardMetricsListener(0),
|
MetricsListener: prometheus.NewForwardMetricsListener(0),
|
||||||
}, true)
|
})
|
||||||
|
|
||||||
// Prepare a test DNS message and call the handler's ServeDNS function.
|
// Prepare a test DNS message and call the handler's ServeDNS function.
|
||||||
// It will then call the metrics listener and prom metrics should be
|
// 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
Loading…
x
Reference in New Issue
Block a user