Sync with upstream

This commit is contained in:
Andrey Meshkov 2020-08-17 21:16:03 +03:00
parent 48a2aee015
commit d48cb726fb
1494 changed files with 460664 additions and 1367 deletions

11
.gitignore vendored
View File

@ -1,2 +1,9 @@
/*.deb
/go_*
.DS_Store
.idea/
bin
AdGuardDNS
example.crt
example.key
tests/parental-all-domains.txt
tests/safebrowsing-all-domains.txt
tests/dnsdb.bin

71
Corefile Normal file
View File

@ -0,0 +1,71 @@
.:53, tls://.:853, https://.:443 {
tls tests/test.crt tests/test.key
ratelimit 50 10000 {
whitelist 127.0.0.1
}
refuseany
dnsfilter {
filter tests/dns.txt
safebrowsing tests/sb.txt
parental tests/parental.txt
safesearch
}
file tests/dnscheck.txt dnscheck-default.adguard.com
lrucache 50000
forward . 8.8.8.8:53 {
prefer_udp
max_fails 0
}
alternate SERVFAIL . 8.8.8.8:53 {
prefer_udp
max_fails 0
}
log
info {
domain adguard.com
type test
protocol auto
addr 176.103.130.135
}
health 127.0.0.1:8181
pprof 127.0.0.1:6053
prometheus 127.0.0.1:9153
dnsdb 127.0.0.1:9154 tests/dnsdb.bin
}
.:5333 {
ratelimit 50 {
whitelist 127.0.0.1
}
refuseany
dnsfilter {
filter tests/dns.txt
filter tests/sb.txt
safebrowsing tests/sb.txt
parental tests/parental.txt
}
lrucache 50000
forward . 8.8.8.8:53 {
prefer_udp
max_fails 0
}
alternate SERVFAIL . 8.8.8.8:53 {
prefer_udp
max_fails 0
}
log
prometheus 127.0.0.1:9153
dnsdb 127.0.0.1:9154 tests/dnsdb.bin
}

View File

@ -1,60 +0,0 @@
NAME=dns
VERSION=$(version)
MAINTAINER="AdGuard Web Team"
USER="dns"
ARCHITECTURE=noarch
SHELL := /bin/bash
.PHONY: default
default: repo
mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
mkfile_dir := $(patsubst %/,%,$(dir $(mkfile_path)))
GOPATH := $(mkfile_dir)/go_$(VERSION)
clean:
rm -fv *.deb
build: check-vars clean
mkdir -p $(GOPATH)/src/bit.adguard.com/dns
if [ ! -h $(GOPATH)/src/bit.adguard.com/dns/adguard-internal-dns ]; then rm -rf $(GOPATH)/src/bit.adguard.com/dns/adguard-internal-dns && ln -fs $(mkfile_dir) $(GOPATH)/src/bit.adguard.com/dns/adguard-internal-dns; fi
GOPATH=$(GOPATH) go get -v -d github.com/coredns/coredns
cp plugin.cfg $(GOPATH)/src/github.com/coredns/coredns
cd $(GOPATH)/src/github.com/coredns/coredns; GOPATH=$(GOPATH) go generate
cd $(GOPATH)/src/github.com/coredns/coredns; GOPATH=$(GOPATH) go get -v -d -t .
cd $(GOPATH)/src/github.com/coredns/coredns; GOPATH=$(GOPATH) PATH=$(GOPATH)/bin:$(PATH) make
cd $(GOPATH)/src/github.com/coredns/coredns; GOPATH=$(GOPATH) go build -x -v -ldflags="-X github.com/coredns/coredns/coremain.GitCommit=$(VERSION)" -asmflags="-trimpath=$(GOPATH)" -gcflags="-trimpath=$(GOPATH)" -o $(GOPATH)/bin/coredns
package: build
fpm --prefix /usr/local/bin \
--deb-user $(USER) \
--after-install postinstall.sh \
--after-remove postrm.sh \
--before-install preinstall.sh \
--before-remove prerm.sh \
--template-scripts \
--template-value user=$(USER) \
--template-value project=$(NAME) \
--template-value version=1.$(VERSION) \
--license proprietary \
--url https://adguard.com/adguard-dns/overview.html \
--category non-free/web \
--description "AdGuard DNS (internal)" \
--deb-no-default-config-files \
-v 1.$(VERSION) \
-s dir \
-t deb \
-a $(ARCHITECTURE) \
-n adguard-$(NAME)-service \
-m $(MAINTAINER) \
--vendor $(MAINTAINER) \
-C go_$(VERSION)/bin \
coredns
repo: package
/usr/local/bin/add_package_to_repo.sh $(NAME)_service $(VERSION) *.deb
check-vars:
ifndef version
$(error VERSION is undefined)
endif

View File

@ -15,7 +15,7 @@
</p>
<p align="center">
<img src="https://cdn.adguard.com/public/Adguard/Common/adguard_dns_servers_map.png" width="800" />
<img src="https://cdn.adguard.com/public/Adguard/Common/adguard_dns_map.png" width="800" />
</p>
# AdGuard DNS
@ -60,16 +60,21 @@ Here's a list of the software that could be used:
### Regular DNS
`176.103.130.130` or `176.103.130.131` for "Default";
`176.103.130.132` or `176.103.130.134` for "Family protection".
* `176.103.130.130` or `176.103.130.131` for "Default";
* `176.103.130.132` or `176.103.130.134` for "Family protection";
* `176.103.130.136` or `176.103.130.137` for "Non-filtering".
### DNS-over-HTTPS
Use `https://dns.adguard.com/dns-query` for "Default" and `https://dns-family.adguard.com/dns-query` for "Family protection" mode.
* Use `https://dns.adguard.com/dns-query` for "Default";
* Use `https://dns-family.adguard.com/dns-query` for "Family protection" mode;
* Use `https://dns-unfiltered.adguard.com/dns-query` for "Non-filtering" mode;
### DNS-over-TLS
Use `dns.adguard.com` string for "Default" or `dns-family.adguard.com` for "Family protection".
* Use `dns.adguard.com` string for "Default";
* Use `dns-family.adguard.com` for "Family protection";
* Use `dns-unfiltered.adguard.com` for "Non-filtering";
### DNSCrypt
@ -79,6 +84,9 @@ Use `dns.adguard.com` string for "Default" or `dns-family.adguard.com` for "Fami
"Family protection":
`sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMjo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ`
"Non-filtering":
`sdns://AQcAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzNjo1NDQzILXoRNa4Oj4-EmjraB--pw3jxfpo29aIFB2_LsBmstr6JTIuZG5zY3J5cHQudW5maWx0ZXJlZC5uczEuYWRndWFyZC5jb20`
## Dependencies
AdGuard DNS shares a lot of code with [AdGuard Home](https://github.com/AdguardTeam/AdGuardHome) and uses pretty much [the same open source libraries](https://github.com/AdguardTeam/AdGuardHome#acknowledgments).

60
VENDOR_PATCHES.md Normal file
View File

@ -0,0 +1,60 @@
## Patches
Some of the vendored dependencies were patched.
1. request.go -- always compress responses
```
diff --git a/vendor/github.com/coredns/coredns/request/request.go b/vendor/github.com/coredns/coredns/request/request.go
index 7374b0b..268b008 100644
--- a/vendor/github.com/coredns/coredns/request/request.go
+++ b/vendor/github.com/coredns/coredns/request/request.go
@@ -219,27 +219,7 @@ func (r *Request) SizeAndDo(m *dns.Msg) bool {
// get the bit, the client should then retry with pigeons.
func (r *Request) Scrub(reply *dns.Msg) *dns.Msg {
reply.Truncate(r.Size())
-
- if reply.Compress {
- return reply
- }
-
- if r.Proto() == "udp" {
- rl := reply.Len()
- // Last ditch attempt to avoid fragmentation, if the size is bigger than the v4/v6 UDP fragmentation
- // limit and sent via UDP compress it (in the hope we go under that limit). Limits taken from NSD:
- //
- // .., 1480 (EDNS/IPv4), 1220 (EDNS/IPv6), or the advertised EDNS buffer size if that is
- // smaller than the EDNS default.
- // See: https://open.nlnetlabs.nl/pipermail/nsd-users/2011-November/001278.html
- if rl > 1480 && r.Family() == 1 {
- reply.Compress = true
- }
- if rl > 1220 && r.Family() == 2 {
- reply.Compress = true
- }
- }
-
+ reply.Compress = true
return reply
}
```
2. `forward` plugin fork
Exposed `parseStanza` to our "alternate" plugin fork.
```
// Exposed to our "alternate" plugin
func ParseForwardStanza(c *caddy.Controller) (*Forward, error) {
return parseStanza(c)
}
```
3. "alternate" plugin fork
Use our "forward" plugin fork instead of the original "forward".
4. "health" plugin fork
Use "/health-check" instead of "/health"

5
alternate/README.md Normal file
View File

@ -0,0 +1,5 @@
# alternate
Fork of https://github.com/coredns/alternate
The purpose is to keep it working with the new CoreDNS version and use our fork of "forward".

70
alternate/alternate.go Normal file
View File

@ -0,0 +1,70 @@
// Package alternate implements a alternate plugin for CoreDNS
package alternate
import (
"golang.org/x/net/context"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/nonwriter"
"github.com/miekg/dns"
)
// Alternate plugin allows an alternate set of upstreams be specified which will be used
// if the plugin chain returns specific error messages.
type Alternate struct {
Next plugin.Handler
rules map[int]rule
original bool // At least one rule has "original" flag
}
type rule struct {
original bool
handler HandlerWithCallbacks
}
// HandlerWithCallbacks interface is made for handling the requests
type HandlerWithCallbacks interface {
plugin.Handler
OnStartup() error
OnShutdown() error
}
// New initializes Alternate plugin
func New() (f *Alternate) {
return &Alternate{rules: make(map[int]rule)}
}
// ServeDNS implements the plugin.Handler interface.
func (f Alternate) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
// If alternate has original option set for any code then copy original request to use it instead of changed
var originalRequest *dns.Msg
if f.original {
originalRequest = r.Copy()
}
nw := nonwriter.New(w)
rcode, err := plugin.NextOrFailure(f.Name(), f.Next, ctx, nw, r)
//By default the rulesIndex is equal rcode, so in such way we handle the case
//when rcode is SERVFAIL and nw.Msg is nil, otherwise we use nw.Msg.Rcode
//because, for example, for the following cases like NXDOMAIN, REFUSED the rcode is 0 (returned by forward)
//A forward doesn't return 0 only in case SERVFAIL
rulesIndex := rcode
if nw.Msg != nil {
rulesIndex = nw.Msg.Rcode
}
if u, ok := f.rules[rulesIndex]; ok {
if u.original && originalRequest != nil {
return u.handler.ServeDNS(ctx, w, originalRequest)
}
return u.handler.ServeDNS(ctx, w, r)
}
if nw.Msg != nil {
w.WriteMsg(nw.Msg)
}
return rcode, err
}
// Name implements the Handler interface.
func (f Alternate) Name() string { return "alternate" }

221
alternate/alternate_test.go Normal file
View File

@ -0,0 +1,221 @@
package alternate
import (
"testing"
"golang.org/x/net/context"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test"
"github.com/miekg/dns"
)
// testHandler implements HandlerWithCallbacks to mock handler
type testHandler struct {
rcode int
called int
lastIsEdns0 bool
}
// newTestHandler sets up handler (forward plugin) mock. It returns rcode defined in parameter.
func newTestHandler(rcode int) *testHandler {
return &testHandler{rcode: rcode}
}
func (h *testHandler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
h.lastIsEdns0 = r.IsEdns0() != nil
h.called++
ret := new(dns.Msg)
ret.SetReply(r)
ret.Answer = append(ret.Answer, test.A("example.org. IN A 127.0.0.1"))
ret.Rcode = h.rcode
w.WriteMsg(ret)
return 0, nil
}
func (h *testHandler) Name() string { return "testHandler" }
func (h *testHandler) OnStartup() error { return nil }
func (h *testHandler) OnShutdown() error { return nil }
// stubNextHandler is used to simulate a rewrite and forward plugin.
// It returns a stub Handler that returns the rcode and err specified when invoked.
// Also it adds edns0 option to given request.
func stubNextHandler(rcode int, err error) test.Handler {
return test.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
returnCode := rcode
if r == nil {
r = &dns.Msg{}
}
r.SetEdns0(4096, false)
if rcode != dns.RcodeServerFailure {
r.MsgHdr.Rcode = rcode
returnCode = dns.RcodeSuccess
w.WriteMsg(r)
} else {
w.WriteMsg(nil)
}
return returnCode, err
})
}
// makeTestCall makes test call to handler
func makeTestCall(handler *Alternate) (*dnstest.Recorder, int, error) {
// Prepare query and make a call
ctx := context.TODO()
req := &dns.Msg{
Question: []dns.Question{{
Name: "abc.com.",
Qclass: dns.ClassINET,
Qtype: dns.TypeA,
}},
}
rec := dnstest.NewRecorder(&test.ResponseWriter{})
rcode, err := handler.ServeDNS(ctx, rec, req)
return rec, rcode, err
}
// Test case for alternate
type alternateTestCase struct {
nextRcode int // rcode to be returned by the stub Handler
expectedRcode int // this is expected rcode by test handler (forward plugin)
called int // this is expected number of calls reached test alternate server
}
func TestAlternate(t *testing.T) {
testCases := []alternateTestCase{
{
nextRcode: dns.RcodeNXRrset,
expectedRcode: dns.RcodeRefused,
called: 1,
},
{
nextRcode: dns.RcodeServerFailure,
expectedRcode: dns.RcodeRefused,
called: 1,
},
{
//No such code in table.
nextRcode: dns.RcodeBadName,
expectedRcode: dns.RcodeBadName, //Remains from nextRcode
called: 0,
},
{
//No such code in table.
nextRcode: dns.RcodeRefused,
expectedRcode: dns.RcodeRefused, //Remains from nextRcode
called: 0,
},
}
for testNum, tc := range testCases {
// mocked Forward for servicing a specific rcode
h := newTestHandler(dns.RcodeRefused)
handler := New()
// create stub handler to return the test rcode
handler.Next = stubNextHandler(tc.nextRcode, nil)
// add rules
handler.rules = map[int]rule{
dns.RcodeNXRrset: {handler: h},
dns.RcodeServerFailure: {handler: h},
}
// Prepare query and make a call
rec, rcode, err := makeTestCall(handler)
// Ensure that no errors returned
if rcode != dns.RcodeSuccess || err != nil {
t.Errorf("Test '%d': Alternate returned code '%d' error '%v'. Expected RcodeSuccess (0) and no error",
testNum, rcode, err)
}
// Ensure that overall returned code is correct
if rec.Rcode != tc.expectedRcode {
t.Errorf("Test '%d': Alternate returned code '%v (%d)', but expected '%v (%d)'",
testNum, dns.RcodeToString[rec.Rcode], rec.Rcode, dns.RcodeToString[tc.expectedRcode], tc.expectedRcode)
}
// Ensure that server was called required number of times
if h.called != tc.called {
t.Errorf("Test '%d': Server expected to be called %d time(s) but called %d times(s)",
testNum, tc.called, h.called)
}
}
}
func TestAlternateMultipleCalls(t *testing.T) {
testCases := []struct {
nextRcode int
called int
}{
{nextRcode: dns.RcodeNXRrset, called: 10},
// No RcodeBadName in table. So, no calls to test server made.
{nextRcode: dns.RcodeBadName, called: 0},
}
for testNum, tc := range testCases {
// mocked Forward for servicing a specific rcode
h := newTestHandler(dns.RcodeRefused)
handler := New()
// create stub handler to return the test rcode
handler.Next = stubNextHandler(tc.nextRcode, nil)
// add rules
handler.rules = map[int]rule{
dns.RcodeNXRrset: {handler: h},
dns.RcodeServerFailure: {handler: h},
}
// Prepare query and make 10 calls
for i := 0; i < 10; i++ {
makeTestCall(handler)
}
// Ensure that server was called required number of times
if h.called != tc.called {
t.Errorf("Test '%d': Server expected to be called %d time(s) but called %d times(s)",
testNum, tc.called, h.called)
}
}
}
func TestAlternateOriginal(t *testing.T) {
testCases := []struct {
nextRcode int
isEdns0 bool
}{
// isEdns0 is rewrited by original
{nextRcode: dns.RcodeNXRrset, isEdns0: false},
// RcodeServerFailure hasn't original flag set. isEdns0 remains the same
{nextRcode: dns.RcodeServerFailure, isEdns0: true},
}
for testNum, tc := range testCases {
// mocked Forward for servicing a specific rcode
h := newTestHandler(dns.RcodeRefused)
handler := New()
// One of rules has "original" flag set
handler.original = true
// create stub handler to return the test rcode
handler.Next = stubNextHandler(tc.nextRcode, nil)
// add rules
handler.rules = map[int]rule{
dns.RcodeNXRrset: {original: true, handler: h},
dns.RcodeServerFailure: {handler: h},
}
// Prepare query and make a call
makeTestCall(handler)
// Ensure edns0 option has expected state
if h.lastIsEdns0 != tc.isEdns0 {
if tc.isEdns0 {
t.Errorf("Test '%d': Server expected to recieve Edns0, but didn't", testNum)
} else {
t.Errorf("Test '%d': Server not expected to recieve Edns0, but received it", testNum)
}
}
}
}

84
alternate/setup.go Normal file
View File

@ -0,0 +1,84 @@
package alternate
import (
"fmt"
"strings"
"github.com/AdguardTeam/AdGuardDNS/forward"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/caddyserver/caddy"
"github.com/miekg/dns"
)
func init() {
caddy.RegisterPlugin("alternate", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
func setup(c *caddy.Controller) error {
a := New()
for c.Next() {
var (
original bool
rcode string
)
if !c.Dispenser.Args(&rcode) {
return c.ArgErr()
}
if rcode == "original" {
original = true
// Reread parameter is not rcode. Get it again.
if !c.Dispenser.Args(&rcode) {
return c.ArgErr()
}
}
rc, ok := dns.StringToRcode[strings.ToUpper(rcode)]
if !ok {
return fmt.Errorf("%s is not a valid rcode", rcode)
}
u, err := forward.ParseForwardStanza(c)
if err != nil {
return plugin.Error("alternate", err)
}
if _, ok := a.rules[rc]; ok {
return fmt.Errorf("rcode '%s' is specified more than once", rcode)
}
a.rules[rc] = rule{original: original, handler: u}
if original {
a.original = true
}
}
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
a.Next = next
return a
})
c.OnStartup(func() error {
for _, r := range a.rules {
if err := r.handler.OnStartup(); err != nil {
return err
}
}
return nil
})
c.OnShutdown(func() error {
for _, r := range a.rules {
if err := r.handler.OnShutdown(); err != nil {
return err
}
}
return nil
})
return nil
}

85
alternate/setup_test.go Normal file
View File

@ -0,0 +1,85 @@
package alternate
import (
"fmt"
"strings"
"testing"
"github.com/caddyserver/caddy"
)
type setupTestCase struct {
config string
expectedError string
}
func TestSetupAlternate(t *testing.T) {
testCases := []setupTestCase{
{
config: `alternate REFUSED . 192.168.1.1:53`,
},
{
config: `alternate SERVFAIL . 192.168.1.1:53`,
},
{
config: `alternate NXDOMAIN . 192.168.1.1:53`,
},
{
config: `alternate original NXDOMAIN . 192.168.1.1:53`,
},
{
config: `alternate REFUSE . 192.168.1.1:53`,
expectedError: `is not a valid rcode`,
},
{
config: `alternate SRVFAIL . 192.168.1.1:53`,
expectedError: `is not a valid rcode`,
},
{
config: `alternate NODOMAIN . 192.168.1.1:53`,
expectedError: `is not a valid rcode`,
},
{
config: `alternate original NODOMAIN . 192.168.1.1:53`,
expectedError: `is not a valid rcode`,
},
{
config: `alternate REFUSED . 192.168.1.1:53 {
max_fails 5
force_tcp
}`,
},
{
config: `alternate REFUSED . abc`,
expectedError: `not an IP address or file`,
},
{
config: `alternate REFUSED . 192.168.1.1:53
alternate REFUSED . 192.168.1.2:53`,
expectedError: `specified more than once`,
},
{
config: `alternate REFUSED . 192.168.1.1:53
alternate original REFUSED . 192.168.1.2:53`,
expectedError: `specified more than once`,
},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%s", tc.config), func(t *testing.T) {
c := caddy.NewTestController("dns", tc.config)
err := setup(c)
if err == nil {
if tc.expectedError != "" {
t.Errorf("Expected error '%s', but got no error", tc.expectedError)
}
} else {
if tc.expectedError == "" {
t.Errorf("Expected no error, but got '%s'", err)
} else if !strings.Contains(err.Error(), tc.expectedError) {
t.Errorf("Expected error '%s', but got '%s'", tc.expectedError, err)
}
}
})
}
}

View File

@ -1,546 +0,0 @@
package dnsfilter
import (
"bufio"
"errors"
"fmt"
"log"
"net"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metrics"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/request"
"github.com/mholt/caddy"
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context"
)
var defaultSOA = &dns.SOA{
// values copied from verisign's nonexistent .com domain
// their exact values are not important in our use case because they are used for domain transfers between primary/secondary DNS servers
Refresh: 1800,
Retry: 900,
Expire: 604800,
Minttl: 86400,
}
func init() {
caddy.RegisterPlugin("dnsfilter", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
type plugFilter struct {
ID int64
Path string
}
type plugSettings struct {
SafeBrowsingBlockHost string
ParentalBlockHost string
QueryLogEnabled bool
BlockedTTL uint32 // in seconds, default 3600
Filters []plugFilter
}
type plug struct {
d *dnsfilter.Dnsfilter
Next plugin.Handler
settings plugSettings
sync.RWMutex
}
var defaultPluginSettings = plugSettings{
SafeBrowsingBlockHost: "standard-block.dns.adguard.com",
ParentalBlockHost: "family-block.dns.adguard.com",
BlockedTTL: 3600, // in seconds
Filters: make([]plugFilter, 0),
}
//
// coredns handling functions
//
func setupPlugin(c *caddy.Controller) (*plug, error) {
// create new Plugin and copy default values
p := &plug{
settings: defaultPluginSettings,
d: dnsfilter.New(nil),
}
log.Println("Initializing the CoreDNS plugin")
for c.Next() {
for c.NextBlock() {
blockValue := c.Val()
switch blockValue {
case "safebrowsing":
log.Println("Browsing security service is enabled")
p.d.SafeBrowsingEnabled = true
if c.NextArg() {
if len(c.Val()) == 0 {
return nil, c.ArgErr()
}
p.d.SetSafeBrowsingServer(c.Val())
}
case "safesearch":
log.Println("Safe search is enabled")
p.d.SafeSearchEnabled = true
case "parental":
if !c.NextArg() {
return nil, c.ArgErr()
}
sensitivity, err := strconv.Atoi(c.Val())
if err != nil {
return nil, c.ArgErr()
}
log.Println("Parental control is enabled")
if !dnsfilter.IsParentalSensitivityValid(sensitivity) {
return nil, errors.New("dnsfilter: invalid parental sensitivity, must be either 3, 10, 13 or 17")
}
p.d.ParentalEnabled = true
p.d.ParentalSensitivity = sensitivity
if c.NextArg() {
if len(c.Val()) == 0 {
return nil, c.ArgErr()
}
p.settings.ParentalBlockHost = c.Val()
}
case "blocked_ttl":
if !c.NextArg() {
return nil, c.ArgErr()
}
blockedTTL, err := strconv.ParseUint(c.Val(), 10, 32)
if err != nil {
return nil, c.ArgErr()
}
log.Printf("Blocked request TTL is %d", blockedTTL)
p.settings.BlockedTTL = uint32(blockedTTL)
case "querylog":
log.Println("Query log is enabled")
p.settings.QueryLogEnabled = true
case "filter":
if !c.NextArg() {
return nil, c.ArgErr()
}
filterID, err := strconv.ParseInt(c.Val(), 10, 64)
if err != nil {
return nil, c.ArgErr()
}
if !c.NextArg() {
return nil, c.ArgErr()
}
filterPath := c.Val()
// Initialize filter and add it to the list
p.settings.Filters = append(p.settings.Filters, plugFilter{
ID: filterID,
Path: filterPath,
})
}
}
}
for _, filter := range p.settings.Filters {
log.Printf("Loading rules from %s", filter.Path)
file, err := os.Open(filter.Path)
if err != nil {
return nil, err
}
defer file.Close()
count := 0
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := scanner.Text()
err = p.d.AddRule(text, filter.ID)
if err == dnsfilter.ErrAlreadyExists || err == dnsfilter.ErrInvalidSyntax {
continue
}
if err != nil {
log.Printf("Cannot add rule %s: %s", text, err)
// Just ignore invalid rules
continue
}
count++
}
log.Printf("Added %d rules from filter ID=%d", count, filter.ID)
if err = scanner.Err(); err != nil {
return nil, err
}
}
return p, nil
}
func setup(c *caddy.Controller) error {
p, err := setupPlugin(c)
if err != nil {
return err
}
config := dnsserver.GetConfig(c)
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
p.Next = next
return p
})
c.OnStartup(func() error {
m := dnsserver.GetConfig(c).Handler("prometheus")
if m == nil {
return nil
}
if x, ok := m.(*metrics.Metrics); ok {
x.MustRegister(requests)
x.MustRegister(filtered)
x.MustRegister(filteredLists)
x.MustRegister(filteredSafebrowsing)
x.MustRegister(filteredParental)
x.MustRegister(whitelisted)
x.MustRegister(safesearch)
x.MustRegister(errorsTotal)
x.MustRegister(elapsedTime)
x.MustRegister(p)
}
return nil
})
c.OnShutdown(p.onShutdown)
c.OnFinalShutdown(p.onFinalShutdown)
return nil
}
func (p *plug) onShutdown() error {
p.Lock()
p.d.Destroy()
p.d = nil
p.Unlock()
return nil
}
func (p *plug) onFinalShutdown() error {
return nil
}
type statsFunc func(ch interface{}, name string, text string, value float64, valueType prometheus.ValueType)
func doDesc(ch interface{}, name string, text string, value float64, valueType prometheus.ValueType) {
realch, ok := ch.(chan<- *prometheus.Desc)
if !ok {
log.Printf("Couldn't convert ch to chan<- *prometheus.Desc\n")
return
}
realch <- prometheus.NewDesc(name, text, nil, nil)
}
func doMetric(ch interface{}, name string, text string, value float64, valueType prometheus.ValueType) {
realch, ok := ch.(chan<- prometheus.Metric)
if !ok {
log.Printf("Couldn't convert ch to chan<- prometheus.Metric\n")
return
}
desc := prometheus.NewDesc(name, text, nil, nil)
realch <- prometheus.MustNewConstMetric(desc, valueType, value)
}
func gen(ch interface{}, doFunc statsFunc, name string, text string, value float64, valueType prometheus.ValueType) {
doFunc(ch, name, text, value, valueType)
}
func doStatsLookup(ch interface{}, doFunc statsFunc, name string, lookupstats *dnsfilter.LookupStats) {
gen(ch, doFunc, fmt.Sprintf("coredns_dnsfilter_%s_requests", name), fmt.Sprintf("Number of %s HTTP requests that were sent", name), float64(lookupstats.Requests), prometheus.CounterValue)
gen(ch, doFunc, fmt.Sprintf("coredns_dnsfilter_%s_cachehits", name), fmt.Sprintf("Number of %s lookups that didn't need HTTP requests", name), float64(lookupstats.CacheHits), prometheus.CounterValue)
gen(ch, doFunc, fmt.Sprintf("coredns_dnsfilter_%s_pending", name), fmt.Sprintf("Number of currently pending %s HTTP requests", name), float64(lookupstats.Pending), prometheus.GaugeValue)
gen(ch, doFunc, fmt.Sprintf("coredns_dnsfilter_%s_pending_max", name), fmt.Sprintf("Maximum number of pending %s HTTP requests", name), float64(lookupstats.PendingMax), prometheus.GaugeValue)
}
func (p *plug) doStats(ch interface{}, doFunc statsFunc) {
p.RLock()
stats := p.d.GetStats()
doStatsLookup(ch, doFunc, "safebrowsing", &stats.Safebrowsing)
doStatsLookup(ch, doFunc, "parental", &stats.Parental)
p.RUnlock()
}
// Describe is called by prometheus handler to know stat types
func (p *plug) Describe(ch chan<- *prometheus.Desc) {
p.doStats(ch, doDesc)
}
// Collect is called by prometheus handler to collect stats
func (p *plug) Collect(ch chan<- prometheus.Metric) {
p.doStats(ch, doMetric)
}
// lookup host, but return answer as if it was a result of different lookup
// TODO: works only on A and AAAA, the go stdlib resolver can't do arbitrary types
func lookupReplaced(host string, question dns.Question) ([]dns.RR, error) {
var records []dns.RR
var res *net.Resolver // nil resolver is default resolver
switch question.Qtype {
case dns.TypeA:
addrs, err := res.LookupIPAddr(context.TODO(), host)
if err != nil {
return nil, err
}
for _, addr := range addrs {
if addr.IP.To4() != nil {
rr, err := dns.NewRR(fmt.Sprintf("%s A %s", question.Name, addr.IP.String()))
if err != nil {
return nil, err // fail entire request, TODO: return partial request?
}
records = append(records, rr)
}
}
case dns.TypeAAAA:
addrs, err := res.LookupIPAddr(context.TODO(), host)
if err != nil {
return nil, err
}
for _, addr := range addrs {
if addr.IP.To4() == nil {
rr, err := dns.NewRR(fmt.Sprintf("%s AAAA %s", question.Name, addr.IP.String()))
if err != nil {
return nil, err // fail entire request, TODO: return partial request?
}
records = append(records, rr)
}
}
}
return records, nil
}
func (p *plug) replaceHostWithValAndReply(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, host string, val string, question dns.Question) (int, error) {
// check if it's a domain name or IP address
addr := net.ParseIP(val)
var records []dns.RR
// log.Println("Will give", val, "instead of", host) // debug logging
if addr != nil {
// this is an IP address, return it
result, err := dns.NewRR(fmt.Sprintf("%s %d A %s", host, p.settings.BlockedTTL, val))
if err != nil {
log.Printf("Got error %s\n", err)
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
}
records = append(records, result)
} else {
// this is a domain name, need to look it up
var err error
records, err = lookupReplaced(dns.Fqdn(val), question)
if err != nil {
log.Printf("Got error %s\n", err)
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
}
}
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
m.Answer = append(m.Answer, records...)
state := request.Request{W: w, Req: r, Context: ctx}
state.SizeAndDo(m)
err := state.W.WriteMsg(m)
if err != nil {
log.Printf("Got error %s\n", err)
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
}
return dns.RcodeSuccess, nil
}
// generate SOA record that makes DNS clients cache NXdomain results
// the only value that is important is TTL in header, other values like refresh, retry, expire and minttl are irrelevant
func (p *plug) genSOA(r *dns.Msg) []dns.RR {
zone := r.Question[0].Name
header := dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Ttl: p.settings.BlockedTTL, Class: dns.ClassINET}
Mbox := "hostmaster."
if zone[0] != '.' {
Mbox += zone
}
Ns := "fake-for-negative-caching.adguard.com."
soa := *defaultSOA
soa.Hdr = header
soa.Mbox = Mbox
soa.Ns = Ns
soa.Serial = 100500 // faster than uint32(time.Now().Unix())
return []dns.RR{&soa}
}
func (p *plug) writeNXdomain(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r, Context: ctx}
m := new(dns.Msg)
m.SetRcode(state.Req, dns.RcodeNameError)
m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
m.Ns = p.genSOA(r)
state.SizeAndDo(m)
err := state.W.WriteMsg(m)
if err != nil {
log.Printf("Got error %s\n", err)
return dns.RcodeServerFailure, err
}
return dns.RcodeNameError, nil
}
func (p *plug) serveDNSInternal(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, dnsfilter.Result, error) {
if len(r.Question) != 1 {
// google DNS, bind and others do the same
return dns.RcodeFormatError, dnsfilter.Result{}, fmt.Errorf("got a DNS request with more than one Question")
}
for _, question := range r.Question {
host := strings.ToLower(strings.TrimSuffix(question.Name, "."))
// is it a safesearch domain?
p.RLock()
if val, ok := p.d.SafeSearchDomain(host); ok {
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, val, question)
if err != nil {
p.RUnlock()
return rcode, dnsfilter.Result{}, err
}
p.RUnlock()
return rcode, dnsfilter.Result{Reason: dnsfilter.FilteredSafeSearch}, err
}
p.RUnlock()
// needs to be filtered instead
p.RLock()
result, err := p.d.CheckHost(host)
if err != nil {
log.Printf("plugin/dnsfilter: %s\n", err)
p.RUnlock()
return dns.RcodeServerFailure, dnsfilter.Result{}, fmt.Errorf("plugin/dnsfilter: %s", err)
}
p.RUnlock()
if result.IsFiltered {
switch result.Reason {
case dnsfilter.FilteredSafeBrowsing:
// return cname safebrowsing.block.dns.adguard.com
val := p.settings.SafeBrowsingBlockHost
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, val, question)
if err != nil {
return rcode, dnsfilter.Result{}, err
}
return rcode, result, err
case dnsfilter.FilteredParental:
// return cname family.block.dns.adguard.com
val := p.settings.ParentalBlockHost
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, val, question)
if err != nil {
return rcode, dnsfilter.Result{}, err
}
return rcode, result, err
case dnsfilter.FilteredBlackList:
if result.Ip == nil {
// return NXDomain
rcode, err := p.writeNXdomain(ctx, w, r)
if err != nil {
return rcode, dnsfilter.Result{}, err
}
return rcode, result, err
}
// This is a hosts-syntax rule
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, result.Ip.String(), question)
if err != nil {
return rcode, dnsfilter.Result{}, err
}
return rcode, result, err
case dnsfilter.FilteredInvalid:
// return NXdomain
rcode, err := p.writeNXdomain(ctx, w, r)
if err != nil {
return rcode, dnsfilter.Result{}, err
}
return rcode, result, err
default:
log.Printf("SHOULD NOT HAPPEN -- got unknown reason for filtering host \"%s\": %v, %+v", host, result.Reason, result)
}
} else {
switch result.Reason {
case dnsfilter.NotFilteredWhiteList:
rcode, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
return rcode, result, err
case dnsfilter.NotFilteredNotFound:
// do nothing, pass through to lower code
default:
log.Printf("SHOULD NOT HAPPEN -- got unknown reason for not filtering host \"%s\": %v, %+v", host, result.Reason, result)
}
}
}
rcode, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
return rcode, dnsfilter.Result{}, err
}
// ServeDNS handles the DNS request and refuses if it's in filterlists
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
start := time.Now()
requests.Inc()
state := request.Request{W: w, Req: r}
// capture the written answer
rrw := dnstest.NewRecorder(w)
rcode, result, err := p.serveDNSInternal(ctx, rrw, r)
if rcode > 0 {
// actually send the answer if we have one
answer := new(dns.Msg)
answer.SetRcode(r, rcode)
state.SizeAndDo(answer)
err = w.WriteMsg(answer)
if err != nil {
return dns.RcodeServerFailure, err
}
}
// increment counters
switch {
case err != nil:
errorsTotal.Inc()
case result.Reason == dnsfilter.FilteredBlackList:
filtered.Inc()
filteredLists.Inc()
case result.Reason == dnsfilter.FilteredSafeBrowsing:
filtered.Inc()
filteredSafebrowsing.Inc()
case result.Reason == dnsfilter.FilteredParental:
filtered.Inc()
filteredParental.Inc()
case result.Reason == dnsfilter.FilteredInvalid:
filtered.Inc()
filteredInvalid.Inc()
case result.Reason == dnsfilter.FilteredSafeSearch:
// the request was passsed through but not filtered, don't increment filtered
safesearch.Inc()
case result.Reason == dnsfilter.NotFilteredWhiteList:
whitelisted.Inc()
case result.Reason == dnsfilter.NotFilteredNotFound:
// do nothing
case result.Reason == dnsfilter.NotFilteredError:
text := "SHOULD NOT HAPPEN: got DNSFILTER_NOTFILTERED_ERROR without err != nil!"
log.Println(text)
err = errors.New(text)
rcode = dns.RcodeServerFailure
}
// log
elapsed := time.Since(start)
elapsedTime.Observe(elapsed.Seconds())
return rcode, err
}
// Name returns name of the plugin as seen in Corefile and plugin.cfg
func (p *plug) Name() string { return "dnsfilter" }

View File

@ -1,131 +0,0 @@
package dnsfilter
import (
"context"
"fmt"
"io/ioutil"
"net"
"os"
"testing"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test"
"github.com/mholt/caddy"
"github.com/miekg/dns"
)
func TestSetup(t *testing.T) {
for i, testcase := range []struct {
config string
failing bool
}{
{`dnsfilter`, false},
{`dnsfilter {
filter 0 /dev/nonexistent/abcdef
}`, true},
{`dnsfilter {
filter 0 ../tests/dns.txt
}`, false},
{`dnsfilter {
safebrowsing
filter 0 ../tests/dns.txt
}`, false},
{`dnsfilter {
parental
filter 0 ../tests/dns.txt
}`, true},
} {
c := caddy.NewTestController("dns", testcase.config)
err := setup(c)
if err != nil {
if !testcase.failing {
t.Fatalf("Test #%d expected no errors, but got: %v", i, err)
}
continue
}
if testcase.failing {
t.Fatalf("Test #%d expected to fail but it didn't", i)
}
}
}
func TestEtcHostsFilter(t *testing.T) {
text := []byte("127.0.0.1 doubleclick.net\n" + "127.0.0.1 example.org example.net www.example.org www.example.net")
tmpfile, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
if _, err = tmpfile.Write(text); err != nil {
t.Fatal(err)
}
if err = tmpfile.Close(); err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name())
configText := fmt.Sprintf("dnsfilter {\nfilter 0 %s\n}", tmpfile.Name())
c := caddy.NewTestController("dns", configText)
p, err := setupPlugin(c)
if err != nil {
t.Fatal(err)
}
p.Next = zeroTTLBackend()
ctx := context.TODO()
for _, testcase := range []struct {
host string
filtered bool
}{
{"www.doubleclick.net", false},
{"doubleclick.net", true},
{"www2.example.org", false},
{"www2.example.net", false},
{"test.www.example.org", false},
{"test.www.example.net", false},
{"example.org", true},
{"example.net", true},
{"www.example.org", true},
{"www.example.net", true},
} {
req := new(dns.Msg)
req.SetQuestion(testcase.host+".", dns.TypeA)
resp := test.ResponseWriter{}
rrw := dnstest.NewRecorder(&resp)
rcode, err := p.ServeDNS(ctx, rrw, req)
if err != nil {
t.Fatalf("ServeDNS returned error: %s", err)
}
if rcode != rrw.Rcode {
t.Fatalf("ServeDNS return value for host %s has rcode %d that does not match captured rcode %d", testcase.host, rcode, rrw.Rcode)
}
A, ok := rrw.Msg.Answer[0].(*dns.A)
if !ok {
t.Fatalf("Host %s expected to have result A", testcase.host)
}
ip := net.IPv4(127, 0, 0, 1)
filtered := ip.Equal(A.A)
if testcase.filtered && testcase.filtered != filtered {
t.Fatalf("Host %s expected to be filtered, instead it is not filtered", testcase.host)
}
if !testcase.filtered && testcase.filtered != filtered {
t.Fatalf("Host %s expected to be not filtered, instead it is filtered", testcase.host)
}
}
}
func zeroTTLBackend() plugin.Handler {
return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
m := new(dns.Msg)
m.SetReply(r)
m.Response, m.RecursionAvailable = true, true
m.Answer = []dns.RR{test.A("example.org. 0 IN A 127.0.0.53")}
w.WriteMsg(m)
return dns.RcodeSuccess, nil
})
}

View File

@ -1,189 +0,0 @@
package dnsfilter
import (
"sync"
"time"
"github.com/coredns/coredns/plugin"
"github.com/prometheus/client_golang/prometheus"
)
var (
requests = newDNSCounter("requests_total", "Count of requests seen by dnsfilter.")
filtered = newDNSCounter("filtered_total", "Count of requests filtered by dnsfilter.")
filteredLists = newDNSCounter("filtered_lists_total", "Count of requests filtered by dnsfilter using lists.")
filteredSafebrowsing = newDNSCounter("filtered_safebrowsing_total", "Count of requests filtered by dnsfilter using safebrowsing.")
filteredParental = newDNSCounter("filtered_parental_total", "Count of requests filtered by dnsfilter using parental.")
filteredInvalid = newDNSCounter("filtered_invalid_total", "Count of requests filtered by dnsfilter because they were invalid.")
whitelisted = newDNSCounter("whitelisted_total", "Count of requests not filtered by dnsfilter because they are whitelisted.")
safesearch = newDNSCounter("safesearch_total", "Count of requests replaced by dnsfilter safesearch.")
errorsTotal = newDNSCounter("errors_total", "Count of requests that dnsfilter couldn't process because of transitive errors.")
elapsedTime = newDNSHistogram("request_duration", "Histogram of the time (in seconds) each request took.")
)
// entries for single time period (for example all per-second entries)
type statsEntries map[string][statsHistoryElements]float64
// how far back to keep the stats
const statsHistoryElements = 60 + 1 // +1 for calculating delta
// each periodic stat is a map of arrays
type periodicStats struct {
Entries statsEntries
period time.Duration // how long one entry lasts
LastRotate time.Time // last time this data was rotated
sync.RWMutex
}
type stats struct {
PerSecond periodicStats
PerMinute periodicStats
PerHour periodicStats
PerDay periodicStats
}
// per-second/per-minute/per-hour/per-day stats
var statistics stats
func initPeriodicStats(periodic *periodicStats, period time.Duration) {
periodic.Entries = statsEntries{}
periodic.LastRotate = time.Now()
periodic.period = period
}
func init() {
purgeStats()
}
func purgeStats() {
initPeriodicStats(&statistics.PerSecond, time.Second)
initPeriodicStats(&statistics.PerMinute, time.Minute)
initPeriodicStats(&statistics.PerHour, time.Hour)
initPeriodicStats(&statistics.PerDay, time.Hour*24)
}
func (p *periodicStats) Inc(name string, when time.Time) {
// calculate how many periods ago this happened
elapsed := int64(time.Since(when) / p.period)
// trace("%s: %v as %v -> [%v]", name, time.Since(when), p.period, elapsed)
if elapsed >= statsHistoryElements {
return // outside of our timeframe
}
p.Lock()
currentValues := p.Entries[name]
currentValues[elapsed]++
p.Entries[name] = currentValues
p.Unlock()
}
func (p *periodicStats) Observe(name string, when time.Time, value float64) {
// calculate how many periods ago this happened
elapsed := int64(time.Since(when) / p.period)
// trace("%s: %v as %v -> [%v]", name, time.Since(when), p.period, elapsed)
if elapsed >= statsHistoryElements {
return // outside of our timeframe
}
p.Lock()
{
countname := name + "_count"
currentValues := p.Entries[countname]
value := currentValues[elapsed]
// trace("Will change p.Entries[%s][%d] from %v to %v", countname, elapsed, value, value+1)
value++
currentValues[elapsed] = value
p.Entries[countname] = currentValues
}
{
totalname := name + "_sum"
currentValues := p.Entries[totalname]
currentValues[elapsed] += value
p.Entries[totalname] = currentValues
}
p.Unlock()
}
// counter that wraps around prometheus Counter but also adds to periodic stats
type counter struct {
name string // used as key in periodic stats
value int64
prom prometheus.Counter
}
func newDNSCounter(name string, help string) *counter {
// trace("called")
c := &counter{}
c.prom = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsfilter",
Name: name,
Help: help,
})
c.name = name
return c
}
func (c *counter) IncWithTime(when time.Time) {
statistics.PerSecond.Inc(c.name, when)
statistics.PerMinute.Inc(c.name, when)
statistics.PerHour.Inc(c.name, when)
statistics.PerDay.Inc(c.name, when)
c.value++
c.prom.Inc()
}
func (c *counter) Inc() {
c.IncWithTime(time.Now())
}
func (c *counter) Describe(ch chan<- *prometheus.Desc) {
c.prom.Describe(ch)
}
func (c *counter) Collect(ch chan<- prometheus.Metric) {
c.prom.Collect(ch)
}
type histogram struct {
name string // used as key in periodic stats
count int64
total float64
prom prometheus.Histogram
}
func newDNSHistogram(name string, help string) *histogram {
// trace("called")
h := &histogram{}
h.prom = prometheus.NewHistogram(prometheus.HistogramOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsfilter",
Name: name,
Help: help,
})
h.name = name
return h
}
func (h *histogram) ObserveWithTime(value float64, when time.Time) {
statistics.PerSecond.Observe(h.name, when, value)
statistics.PerMinute.Observe(h.name, when, value)
statistics.PerHour.Observe(h.name, when, value)
statistics.PerDay.Observe(h.name, when, value)
h.count++
h.total += value
h.prom.Observe(value)
}
func (h *histogram) Observe(value float64) {
h.ObserveWithTime(value, time.Now())
}
func (h *histogram) Describe(ch chan<- *prometheus.Desc) {
h.prom.Describe(ch)
}
func (h *histogram) Collect(ch chan<- prometheus.Metric) {
h.prom.Collect(ch)
}

View File

@ -1,182 +0,0 @@
package ratelimit
import (
"errors"
"log"
"sort"
"strconv"
"time"
// ratelimiting and per-ip buckets
"github.com/beefsack/go-rate"
"github.com/patrickmn/go-cache"
// coredns plugin
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metrics"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/request"
"github.com/mholt/caddy"
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context"
)
const defaultRatelimit = 30
const defaultResponseSize = 1000
var (
tokenBuckets = cache.New(time.Hour, time.Hour)
)
// ServeDNS handles the DNS request and refuses if it's an beyind specified ratelimit
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
ip := state.IP()
allow, err := p.allowRequest(ip)
if err != nil {
return 0, err
}
if !allow {
ratelimited.Inc()
return 0, nil
}
// Record response to get status code and size of the reply.
rw := dnstest.NewRecorder(w)
status, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, rw, r)
size := rw.Len
if size > defaultResponseSize && state.Proto() == "udp" {
// For large UDP responses we call allowRequest more times
// The exact number of times depends on the response size
for i := 0; i < size/defaultResponseSize; i++ {
p.allowRequest(ip)
}
}
return status, err
}
func (p *plug) allowRequest(ip string) (bool, error) {
if len(p.whitelist) > 0 {
i := sort.SearchStrings(p.whitelist, ip)
if i < len(p.whitelist) && p.whitelist[i] == ip {
return true, nil
}
}
if _, found := tokenBuckets.Get(ip); !found {
tokenBuckets.Set(ip, rate.New(p.ratelimit, time.Second), time.Hour)
}
value, found := tokenBuckets.Get(ip)
if !found {
// should not happen since we've just inserted it
text := "SHOULD NOT HAPPEN: just-inserted ratelimiter disappeared"
log.Println(text)
err := errors.New(text)
return true, err
}
rl, ok := value.(*rate.RateLimiter)
if !ok {
text := "SHOULD NOT HAPPEN: non-bool entry found in safebrowsing lookup cache"
log.Println(text)
err := errors.New(text)
return true, err
}
allow, _ := rl.Try()
return allow, nil
}
//
// helper functions
//
func init() {
caddy.RegisterPlugin("ratelimit", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
type plug struct {
Next plugin.Handler
// configuration for creating above
ratelimit int // in requests per second per IP
whitelist []string // a list of whitelisted IP addresses
}
func setupPlugin(c *caddy.Controller) (*plug, error) {
p := &plug{ratelimit: defaultRatelimit}
for c.Next() {
args := c.RemainingArgs()
if len(args) > 0 {
ratelimit, err := strconv.Atoi(args[0])
if err != nil {
return nil, c.ArgErr()
}
p.ratelimit = ratelimit
}
for c.NextBlock() {
switch c.Val() {
case "whitelist":
p.whitelist = c.RemainingArgs()
if len(p.whitelist) > 0 {
sort.Strings(p.whitelist)
}
}
}
}
return p, nil
}
func setup(c *caddy.Controller) error {
p, err := setupPlugin(c)
if err != nil {
return err
}
config := dnsserver.GetConfig(c)
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
p.Next = next
return p
})
c.OnStartup(func() error {
m := dnsserver.GetConfig(c).Handler("prometheus")
if m == nil {
return nil
}
if x, ok := m.(*metrics.Metrics); ok {
x.MustRegister(ratelimited)
}
return nil
})
return nil
}
func newDNSCounter(name string, help string) prometheus.Counter {
return prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "ratelimit",
Name: name,
Help: help,
})
}
var (
ratelimited = newDNSCounter("dropped_total", "Count of requests that have been dropped because of rate limit")
)
// Name returns name of the plugin as seen in Corefile and plugin.cfg
func (p *plug) Name() string { return "ratelimit" }

View File

@ -1,80 +0,0 @@
package ratelimit
import (
"testing"
"github.com/mholt/caddy"
)
func TestSetup(t *testing.T) {
for i, testcase := range []struct {
config string
failing bool
}{
{`ratelimit`, false},
{`ratelimit 100`, false},
{`ratelimit {
whitelist 127.0.0.1
}`, false},
{`ratelimit 50 {
whitelist 127.0.0.1 176.103.130.130
}`, false},
{`ratelimit test`, true},
} {
c := caddy.NewTestController("dns", testcase.config)
err := setup(c)
if err != nil {
if !testcase.failing {
t.Fatalf("Test #%d expected no errors, but got: %v", i, err)
}
continue
}
if testcase.failing {
t.Fatalf("Test #%d expected to fail but it didn't", i)
}
}
}
func TestRatelimiting(t *testing.T) {
// rate limit is 1 per sec
c := caddy.NewTestController("dns", `ratelimit 1`)
p, err := setupPlugin(c)
if err != nil {
t.Fatal("Failed to initialize the plugin")
}
allowed, err := p.allowRequest("127.0.0.1")
if err != nil || !allowed {
t.Fatal("First request must have been allowed")
}
allowed, err = p.allowRequest("127.0.0.1")
if err != nil || allowed {
t.Fatal("Second request must have been ratelimited")
}
}
func TestWhitelist(t *testing.T) {
// rate limit is 1 per sec
c := caddy.NewTestController("dns", `ratelimit 1 { whitelist 127.0.0.2 127.0.0.1 127.0.0.125 }`)
p, err := setupPlugin(c)
if err != nil {
t.Fatal("Failed to initialize the plugin")
}
allowed, err := p.allowRequest("127.0.0.1")
if err != nil || !allowed {
t.Fatal("First request must have been allowed")
}
allowed, err = p.allowRequest("127.0.0.1")
if err != nil || !allowed {
t.Fatal("Second request must have been allowed due to whitelist")
}
}

View File

@ -1,91 +0,0 @@
package refuseany
import (
"fmt"
"log"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metrics"
"github.com/coredns/coredns/request"
"github.com/mholt/caddy"
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context"
)
type plug struct {
Next plugin.Handler
}
// ServeDNS handles the DNS request and refuses if it's an ANY request
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
if len(r.Question) != 1 {
// google DNS, bind and others do the same
return dns.RcodeFormatError, fmt.Errorf("Got DNS request with != 1 questions")
}
q := r.Question[0]
if q.Qtype == dns.TypeANY {
state := request.Request{W: w, Req: r, Context: ctx}
rcode := dns.RcodeNotImplemented
m := new(dns.Msg)
m.SetRcode(r, rcode)
state.SizeAndDo(m)
err := state.W.WriteMsg(m)
if err != nil {
log.Printf("Got error %s\n", err)
return dns.RcodeServerFailure, err
}
return rcode, nil
}
return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
}
func init() {
caddy.RegisterPlugin("refuseany", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
func setup(c *caddy.Controller) error {
p := &plug{}
config := dnsserver.GetConfig(c)
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
p.Next = next
return p
})
c.OnStartup(func() error {
m := dnsserver.GetConfig(c).Handler("prometheus")
if m == nil {
return nil
}
if x, ok := m.(*metrics.Metrics); ok {
x.MustRegister(ratelimited)
}
return nil
})
return nil
}
func newDNSCounter(name string, help string) prometheus.Counter {
return prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "refuseany",
Name: name,
Help: help,
})
}
var (
ratelimited = newDNSCounter("refusedany_total", "Count of ANY requests that have been dropped")
)
// Name returns name of the plugin as seen in Corefile and plugin.cfg
func (p *plug) Name() string { return "refuseany" }

23
dnsdb/README.md Normal file
View File

@ -0,0 +1,23 @@
# dnsdb
A simple plugin that records domain names and their IP/CNAME addresses.
This data can then be retrieved by requesting a specified endpoint.
```
dnsdb [ADDR] [PATH]
```
* `[ADDR]` -- local address where you'll be able to retrieve the dnsdb data
* `[PATH]` -- path where we will create the local database
> Every time when you request the dnsdb data, the local database is re-created.
> It is not supposed to be persistent, this is just a cache.
## Example
```
dnsdb 127.0.0.1:9154 /var/tmp/dnsdb.bin
```
* `http://127.0.0.1:9154/csv` -- here you'll be able to retrieve the data.
* `/var/tmp/dnsdb.bin` -- here the local db will be created.

256
dnsdb/db.go Normal file
View File

@ -0,0 +1,256 @@
package dnsdb
import (
"fmt"
"os"
"strings"
"sync"
"time"
clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/miekg/dns"
bolt "go.etcd.io/bbolt"
)
const recordsBucket = "Records"
type dnsDB struct {
path string // path to the database file
db *bolt.DB
buffer map[string][]Record
bufferLock sync.Mutex
dbLock sync.Mutex
}
// NewDB creates a new instance of the DNSDB
func newDB(path string) (*dnsDB, error) {
clog.Infof("Initializing DNSDB: %s", path)
d := &dnsDB{
path: path,
buffer: map[string][]Record{},
}
err := d.InitDB()
if err != nil {
return nil, err
}
clog.Infof("Finished initializing DNSDB: %s", path)
return d, nil
}
// InitDB initializes the database file
func (d *dnsDB) InitDB() error {
// database is always created from scratch
_ = os.Remove(d.path)
db, err := bolt.Open(d.path, 0644, nil)
if err != nil {
clog.Errorf("Failed to initialize existing DB: %s", err)
return err
}
err = db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucket([]byte(recordsBucket))
return err
})
if err != nil {
clog.Errorf("Failed to create DB bucket: %s", err)
return err
}
d.db = db
return nil
}
// RotateDB closes the current DB, renames it to a temporary file,
// initializes a new empty DB, and returns the path to that temporary file
func (d *dnsDB) RotateDB() (string, error) {
d.dbLock.Lock()
defer d.dbLock.Unlock()
err := d.db.Close()
if err != nil {
return "", err
}
// Moving the old DB to a new location before returning it
path := fmt.Sprintf("%s.%d", d.path, time.Now().Unix())
err = os.Rename(d.path, path)
if err != nil {
return "", err
}
// Re-creating the database
err = d.InitDB()
if err != nil {
return "", err
}
dbSizeGauge.Set(0)
dbRotateTimestamp.SetToCurrentTime()
return path, nil
}
// RecordMsg saves a DNS response to the buffer
// this buffer will be then dumped to the database
func (d *dnsDB) RecordMsg(m *dns.Msg) {
if !m.Response {
// Not a response anyway
return
}
if len(m.Question) != 1 {
// Invalid DNS request
return
}
q := m.Question[0]
if q.Qtype != dns.TypeA && q.Qtype != dns.TypeAAAA {
// Only record A and AAAA
return
}
if m.Rcode != dns.RcodeSuccess {
// Discard unsuccessful responses
return
}
name := strings.TrimSuffix(q.Name, ".")
key := d.key(name, q.Qtype)
d.bufferLock.Lock()
if v, ok := d.buffer[key]; ok {
// Increment hits count
for i := 0; i < len(v); i++ {
v[i].Hits++
}
d.bufferLock.Unlock()
// Already buffered, doing nothing
return
}
d.bufferLock.Unlock()
records := d.toDBRecords(m, q)
d.saveToBuffer(name, q.Qtype, records)
}
// Save - saves the buffered records to the local bolt database
func (d *dnsDB) Save() {
clog.Infof("Saving the buffer to the DNSDB")
start := time.Now()
var buffer map[string][]Record
// Copy the old buffer
d.bufferLock.Lock()
buffer = d.buffer
d.buffer = map[string][]Record{}
bufferSizeGauge.Set(0)
d.bufferLock.Unlock()
if len(buffer) == 0 {
return
}
// Start writing
d.dbLock.Lock()
defer d.dbLock.Unlock()
err := d.db.Batch(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(recordsBucket))
for k, v := range buffer {
dbKey := []byte(k)
// First - look for existing records in the bucket
val := b.Get(dbKey)
if val != nil {
recs, err := DecodeRecords(val)
if err != nil || len(recs) == 0 {
// Do nothing
clog.Errorf("Failed to decode records for %s: %s", k, err)
} else {
// Use the "Hits" counter from the first record
// to set the proper "Hits" count
for _, r := range v {
r.Hits = r.Hits + recs[0].Hits
}
}
}
// Now encode the records list
dbValue, err := EncodeRecords(v)
if err != nil {
clog.Errorf("Failed to encode value for %s: %s", k, err)
continue
}
// Save the updated list to the DB
err = b.Put(dbKey, dbValue)
if err != nil {
clog.Errorf("Failed to save data for %s: %s", k, err)
}
}
dbSizeGauge.Set(float64(b.Stats().KeyN))
return nil
})
elapsedDBSave.Observe(time.Since(start).Seconds())
if err != nil {
clog.Errorf("Error while updating the DB: %s", err)
}
}
func (d *dnsDB) saveToBuffer(name string, qtype uint16, records []Record) {
d.bufferLock.Lock()
d.buffer[d.key(name, qtype)] = records
bufferSizeGauge.Inc()
d.bufferLock.Unlock()
}
func (d *dnsDB) key(name string, qtype uint16) string {
t, _ := dns.TypeToString[qtype]
return name + "_" + t
}
// toDBRecords converts DNS message to an array to "record"
func (d *dnsDB) toDBRecords(m *dns.Msg, q dns.Question) []Record {
if len(m.Answer) == 0 {
rec := d.toDBRecord(m, q, nil)
return []Record{rec}
}
records := []Record{}
for _, rr := range m.Answer {
rec := d.toDBRecord(m, q, rr)
records = append(records, rec)
}
return records
}
func (d *dnsDB) toDBRecord(m *dns.Msg, q dns.Question, rr dns.RR) Record {
rec := Record{}
rec.DomainName = strings.TrimSuffix(q.Name, ".")
rec.RCode = m.Rcode
rec.Hits = 1
if rr == nil {
rec.RRType = q.Qtype
rec.Answer = ""
} else {
rec.RRType = rr.Header().Rrtype
switch v := rr.(type) {
case *dns.CNAME:
rec.Answer = strings.TrimSuffix(v.Target, ".")
case *dns.A:
rec.Answer = v.A.String()
case *dns.AAAA:
rec.Answer = v.AAAA.String()
}
}
return rec
}

59
dnsdb/db_test.go Normal file
View File

@ -0,0 +1,59 @@
package dnsdb
import (
"bytes"
"os"
"path/filepath"
"testing"
"github.com/coredns/coredns/plugin/test"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
)
func TestDbRotateAndSave(t *testing.T) {
path := filepath.Join(os.TempDir(), "db.bin")
defer func() {
_ = os.Remove(path)
}()
db, err := newDB(path)
assert.Nil(t, err)
assert.NotNil(t, db)
// Test DNS message
m := new(dns.Msg)
m.SetQuestion("badhost.", dns.TypeA)
res := new(dns.Msg)
res.SetReply(m)
res.Response, m.RecursionAvailable = true, true
res.Answer = []dns.RR{
test.A("badhost. 0 IN A 37.220.26.135"),
}
// Record this message twice
db.RecordMsg(res)
db.RecordMsg(res)
// Check buffer size
assert.Equal(t, 1, len(db.buffer))
// Save to the DB
db.Save()
// Rotate
dbPath, err := db.RotateDB()
assert.Nil(t, err)
defer func() {
_ = os.Remove(dbPath)
}()
// Write CSV
buf := bytes.NewBufferString("")
err = dnsDBToCSV(dbPath, buf)
assert.Nil(t, err)
// Check CSV
assert.Equal(t, "badhost,A,NOERROR,37.220.26.135,2\n", buf.String())
}

149
dnsdb/listen.go Normal file
View File

@ -0,0 +1,149 @@
package dnsdb
import (
"bytes"
"compress/gzip"
"encoding/csv"
"encoding/gob"
"io"
"net"
"net/http"
"os"
"strconv"
"strings"
"sync"
"github.com/miekg/dns"
"github.com/pkg/errors"
clog "github.com/coredns/coredns/plugin/pkg/log"
bolt "go.etcd.io/bbolt"
)
// startListener starts HTTP listener that will rotate the database
// and return it's contents
func startListener(addr string, db *dnsDB) error {
if addr == "" {
clog.Infof("No dnsdb HTTP listener configured")
return nil
}
clog.Infof("Starting dnsdb HTTP listener on %s", addr)
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
server := &dbServer{
db: db,
}
srv := &http.Server{Handler: server}
go func() {
_ = srv.Serve(ln)
}()
return nil
}
type dbServer struct {
db *dnsDB
sync.RWMutex
}
func (c *dbServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/csv" {
http.Error(rw, "Not Found", http.StatusNotFound)
return
}
// Disallow parallel requests as this request
// changes the inner state of the DNSDB
c.Lock()
defer c.Unlock()
// Flush the current buffer to the database
c.db.Save()
path, err := c.db.RotateDB()
if err != nil {
clog.Errorf("Failed to rotate DNSDB: %s", err)
http.Error(rw, "Failed to rotate DNSDB", http.StatusInternalServerError)
return
}
defer func() {
// Remove the temporary database -- we don't need it anymore
_ = os.Remove(path)
}()
// Now serve the content
rw.Header().Set("Content-Type", "text/plain")
var writer io.Writer
writer = rw
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
rw.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(rw)
defer gz.Close()
writer = gz
}
rw.WriteHeader(http.StatusOK)
err = dnsDBToCSV(path, writer)
if err != nil {
clog.Errorf("Failed to convert DB to CSV: %s", err)
}
}
// dnsDBToCSV converts the DNSDB to CSV
func dnsDBToCSV(path string, writer io.Writer) error {
db, err := bolt.Open(path, 0644, nil)
if err != nil {
return err
}
defer func() {
_ = db.Close()
}()
return db.Batch(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(recordsBucket))
if b == nil {
return errors.New("records bucket not found")
}
csvWriter := csv.NewWriter(writer)
defer csvWriter.Flush()
// Iterating over all records
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
buf := bytes.NewBuffer(v)
dec := gob.NewDecoder(buf)
var recs []Record
err := dec.Decode(&recs)
if err != nil {
clog.Errorf("Failed to decode DNSDB record: %s", err)
// Don't interrupt - we'd better write other records
continue
}
for _, r := range recs {
csvRec := []string{
r.DomainName,
dns.TypeToString[r.RRType],
dns.RcodeToString[r.RCode],
r.Answer,
strconv.FormatInt(r.Hits, 10),
}
err = csvWriter.Write(csvRec)
if err != nil {
return err
}
}
}
return nil
})
}

33
dnsdb/metrics.go Normal file
View File

@ -0,0 +1,33 @@
package dnsdb
import (
"github.com/coredns/coredns/plugin"
"github.com/prometheus/client_golang/prometheus"
)
var (
dbSizeGauge = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsdb",
Name: "db_size",
Help: "Count of records in the local DNSDB.",
})
bufferSizeGauge = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsdb",
Name: "buffer_size",
Help: "Count of records in the temporary buffer.",
})
dbRotateTimestamp = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsdb",
Name: "rotate_time",
Help: "Time when the database was rotated.",
})
elapsedDBSave = prometheus.NewHistogram(prometheus.HistogramOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsdb",
Name: "elapsed_db_save",
Help: "Time elapsed on saving buffer to the database.",
})
)

52
dnsdb/plugin.go Normal file
View File

@ -0,0 +1,52 @@
package dnsdb
import (
"context"
"github.com/coredns/coredns/plugin"
"github.com/miekg/dns"
)
// plug represents the plugin itself
type plug struct {
Next plugin.Handler
addr string // Address for the HTTP server that serves the DB data
path string // Path to the DNSDB instance
}
// Name returns name of the plugin as seen in Corefile and plugin.cfg
func (p *plug) Name() string { return "dnsdb" }
// ServeDNS handles the DNS request and records it to the DNSDB
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
cw := &DBWriter{
ResponseWriter: w,
db: dnsDBMap[p.addr],
}
return plugin.NextOrFailure(p.Name(), p.Next, ctx, cw, r)
}
// Recorder is a type of ResponseWriter that captures
// the rcode code written to it and also the size of the message
// written in the response. A rcode code does not have
// to be written, however, in which case 0 must be assumed.
// It is best to have the constructor initialize this type
// with that default status code.
type DBWriter struct {
dns.ResponseWriter
db *dnsDB
}
// WriteMsg records the status code and calls the
// underlying ResponseWriter's WriteMsg method.
func (r *DBWriter) WriteMsg(res *dns.Msg) error {
r.db.RecordMsg(res)
return r.ResponseWriter.WriteMsg(res)
}
// Write is a wrapper that records the length of the message that gets written.
func (r *DBWriter) Write(buf []byte) (int, error) {
// Doing nothing in this case
return r.ResponseWriter.Write(buf)
}

81
dnsdb/plugin_test.go Normal file
View File

@ -0,0 +1,81 @@
package dnsdb
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/caddyserver/caddy"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test"
"github.com/miekg/dns"
)
func TestPluginRecordMsg(t *testing.T) {
path := filepath.Join(os.TempDir(), "db.bin")
defer func() {
_ = os.Remove(path)
}()
configText := fmt.Sprintf(`dnsdb %s`, path)
c := caddy.NewTestController("dns", configText)
c.ServerBlockKeys = []string{""}
p, err := parse(c)
if err != nil {
t.Fatal(err)
}
// Emulate a DNS response
p.Next = backendResponse()
ctx := context.TODO()
// Test DNS message
req := new(dns.Msg)
req.SetQuestion("badhost.", dns.TypeA)
resp := test.ResponseWriter{}
rrw := dnstest.NewRecorder(&resp)
// Call the plugin
rcode, err := p.ServeDNS(ctx, rrw, req)
if err != nil {
t.Fatalf("ServeDNS returned error: %s", err)
}
if rcode != rrw.Rcode {
t.Fatalf("ServeDNS return value %d that does not match captured rcode %d", rcode, rrw.Rcode)
}
// Get the db
db, ok := dnsDBMap[""]
assert.True(t, ok)
assert.NotNil(t, db)
// Assert that everything was written properly
assert.Equal(t, 1, len(db.buffer))
rec, _ := db.buffer["badhost_A"]
assert.NotNil(t, rec)
assert.Equal(t, 1, len(rec))
assert.Equal(t, "badhost", rec[0].DomainName)
}
// Return response with an A record
func backendResponse() plugin.Handler {
return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
m := new(dns.Msg)
m.SetReply(r)
m.Response, m.RecursionAvailable = true, true
m.Answer = []dns.RR{
test.A("badhost. 0 IN A 37.220.26.135"),
}
_ = w.WriteMsg(m)
return dns.RcodeSuccess, nil
})
}

38
dnsdb/record.go Normal file
View File

@ -0,0 +1,38 @@
package dnsdb
import (
"bytes"
"encoding/gob"
)
// Record of the DNS DB
type Record struct {
DomainName string // DomainName -- fqdn version
RRType uint16 // RRType - either A, AAAA, or CNAME
RCode int // RCode - DNS response RCode
Answer string // Answer - IP or hostname
Hits int64 // How many times this record was served
}
// EncodeRecords encodes an array of records to a byte array
func EncodeRecords(recs []Record) ([]byte, error) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
err := enc.Encode(recs)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// DecodeRecords decodes an array of records from a byte array
func DecodeRecords(b []byte) ([]Record, error) {
buf := bytes.NewBuffer(b)
dec := gob.NewDecoder(buf)
var recs []Record
err := dec.Decode(&recs)
if err != nil {
return nil, err
}
return recs, nil
}

99
dnsdb/setup.go Normal file
View File

@ -0,0 +1,99 @@
package dnsdb
import (
"fmt"
"time"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/caddyserver/caddy"
"github.com/coredns/coredns/plugin/metrics"
clog "github.com/coredns/coredns/plugin/pkg/log"
)
const bufferRotationPeriod = 15 * time.Minute
var (
// Keeping one dnsDB instance per address
dnsDBMap = map[string]*dnsDB{}
)
func init() {
caddy.RegisterPlugin("dnsdb", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
func setup(c *caddy.Controller) error {
clog.Infof("Initializing the dnsdb plugin for %s", c.ServerBlockKeys[c.ServerBlockKeyIndex])
p, err := parse(c)
if err != nil {
return err
}
config := dnsserver.GetConfig(c)
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
p.Next = next
return p
})
c.OnStartup(func() error {
metrics.MustRegister(c, dbSizeGauge, dbRotateTimestamp, bufferSizeGauge, elapsedDBSave)
return nil
})
clog.Infof("Finished initializing the dnsfilter plugin for %s", c.ServerBlockKeys[c.ServerBlockKeyIndex])
return nil
}
func parse(c *caddy.Controller) (*plug, error) {
p := &plug{}
for c.Next() {
args := c.RemainingArgs()
if len(args) == 1 {
p.path = args[0]
}
if len(args) == 2 {
p.addr = args[0]
p.path = args[1]
}
if len(args) == 0 || len(args) > 2 {
return nil, fmt.Errorf("cannot initialize DNSDB plugin - invalid args: %v", args)
}
}
if db, ok := dnsDBMap[p.addr]; ok {
if db.path != p.path {
return nil, fmt.Errorf("dnsdb with a different path already listens to %s", p.addr)
}
} else {
// Init the new dnsDB
d, err := newDB(p.path)
if err != nil {
return nil, err
}
dnsDBMap[p.addr] = d
// Start the listener
err = startListener(p.addr, d)
if err != nil {
return nil, err
}
ticker := time.NewTicker(bufferRotationPeriod)
go func() {
time.Sleep(bufferRotationPeriod)
for t := range ticker.C {
_ = t // we don't print the ticker time, so assign this `t` variable to underscore `_` to avoid error
d.Save()
}
}()
}
return p, nil
}

25
dnsfilter/README.md Normal file
View File

@ -0,0 +1,25 @@
# dnsfilter
This plugin implements the filtering logic.
It uses local blacklists to make a decision on whether the DNS request should be blocked or bypassed.
```
dnsfilter {
filter [PATH] [URL TTL]
safebrowsing [PATH] [HOST] [URL TTL]
parental [PATH] [HOST] [URL TTL]
safesearch
}
```
* `filter [PATH]` -- path to the blacklist that will be used for blocking ads and trackers
* `filter [URL TTL]` -- URL to the filter list and TTL. Once in in `TTL` seconds we will
try to reload the filter from the specified URL.
* `safebrowsing [PATH] [HOST]`
* path to the blacklist that will be used for blocking malicious/phishing domains
* hostname that we will use for DNS response when we block malicious/phishing domains
* `parental [PATH] [HOST]`
* path to the blacklist that will be used for blocking adult websites
* hostname that we will use for DNS response when we block adult websites
* `safesearch` - if specified, we'll enforce safe search on the popular search engines

289
dnsfilter/dnsfilter.go Normal file
View File

@ -0,0 +1,289 @@
package dnsfilter
import (
"fmt"
"strings"
"time"
safeservices "github.com/AdguardTeam/AdGuardDNS/dnsfilter/safe_services"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/AdguardTeam/urlfilter"
"github.com/coredns/coredns/plugin"
clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
const NotFiltered = -100
// https://support.mozilla.org/en-US/kb/configuring-networks-disable-dns-over-https
const FirefoxCanaryDomain = "use-application-dns.net"
const sbTXTSuffix = ".sb.dns.adguard.com"
const pcTXTSuffix = ".pc.dns.adguard.com"
// ServeDNS handles the DNS request and refuses if it's in filterlists
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
if len(r.Question) != 1 {
// google DNS, bind and others do the same
return dns.RcodeFormatError, fmt.Errorf("got a DNS request with more than one Question")
}
// measure time spent in dnsfilter
start := time.Now()
rcode, err := p.handleTXT(ctx, w, r)
if rcode == NotFiltered {
// pass the request to an upstream server and receive response
rec := responseRecorder{
ResponseWriter: w,
}
rcode, err = plugin.NextOrFailure(p.Name(), p.Next, ctx, &rec, r)
// measure time spent in dnsfilter
startFiltering := time.Now()
if err == nil && rec.resp != nil {
// check if request or response should be blocked
rcode2, err2 := p.filterRequest(ctx, w, r, rec.resp)
if rcode2 != NotFiltered {
filtered.Inc()
rcode = rcode2
err = err2
rec.resp = nil // filterRequest() has already written the response
}
elapsedFilterTime.Observe(time.Since(startFiltering).Seconds())
}
if rec.resp != nil {
err2 := w.WriteMsg(rec.resp) // pass through the original response
if err == nil {
err = err2
}
}
}
// increment requests counters
requests.Inc()
elapsedTime.Observe(time.Since(start).Seconds())
if err != nil {
errorsTotal.Inc()
}
return rcode, err
}
// Stores DNS response object
type responseRecorder struct {
dns.ResponseWriter
resp *dns.Msg
}
func (r *responseRecorder) WriteMsg(res *dns.Msg) error {
r.resp = res
return nil
}
func (p *plug) replyTXT(w dns.ResponseWriter, r *dns.Msg, txtData []string) error {
txt := dns.TXT{}
txt.Hdr = dns.RR_Header{
Name: r.Question[0].Name,
Rrtype: dns.TypeTXT,
Ttl: p.settings.BlockedTTL,
Class: dns.ClassINET,
}
txt.Txt = txtData
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative = true
m.RecursionAvailable = true
m.Compress = true
m.Answer = append(m.Answer, &txt)
state := request.Request{W: w, Req: r}
state.SizeAndDo(m)
return state.W.WriteMsg(m)
}
// Respond to TXT requests for safe-browsing and parental services.
// Return NotFiltered if request wasn't handled.
func (p *plug) handleTXT(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
host := strings.ToLower(strings.TrimSuffix(r.Question[0].Name, "."))
if p.settings.SafeBrowsingEnabled &&
r.Question[0].Qtype == dns.TypeTXT &&
strings.HasSuffix(host, sbTXTSuffix) {
requestsSafeBrowsingTXT.Inc()
hashStr := host[:len(host)-len(sbTXTSuffix)]
txtData, _ := p.getSafeBrowsingEngine().data.MatchHashes(hashStr)
err := p.replyTXT(w, r, txtData)
if err != nil {
clog.Infof("SafeBrowsing: WriteMsg(): %s\n", err)
return dns.RcodeServerFailure, fmt.Errorf("SafeBrowsing: WriteMsg(): %s", err)
}
return dns.RcodeSuccess, nil
}
if p.settings.ParentalEnabled &&
r.Question[0].Qtype == dns.TypeTXT &&
strings.HasSuffix(host, pcTXTSuffix) {
requestsParentalTXT.Inc()
hashStr := host[:len(host)-len(pcTXTSuffix)]
txtData, _ := p.getParentalEngine().data.MatchHashes(hashStr)
err := p.replyTXT(w, r, txtData)
if err != nil {
clog.Infof("Parental: WriteMsg(): %s\n", err)
return dns.RcodeServerFailure, fmt.Errorf("parental: WriteMsg(): %s", err)
}
return dns.RcodeSuccess, nil
}
return NotFiltered, nil
}
// filterRequest applies dnsfilter rules to the request. If the request should be blocked,
// it writes the response right away. Otherwise, it returns NotFiltered instead of the response code,
// which means that the request should processed further by the next plugins in the chain.
func (p *plug) filterRequest(ctx context.Context, w dns.ResponseWriter, req *dns.Msg, res *dns.Msg) (int, error) {
question := req.Question[0]
host := strings.ToLower(strings.TrimSuffix(question.Name, "."))
if (question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA) &&
host == FirefoxCanaryDomain {
return p.writeNXDomain(ctx, w, req)
}
// is it a safesearch domain?
if p.settings.SafeSearchEnabled {
if replacementHost, ok := safeservices.SafeSearchDomains[host]; ok {
safeSearch.Inc()
return p.replaceHostWithValAndReply(ctx, w, req, host, replacementHost, question)
}
}
// is it blocked by safebrowsing?
if p.settings.SafeBrowsingEnabled && p.getSafeBrowsingEngine().data.MatchHost(host) {
filteredSafeBrowsing.Inc()
// return cname safebrowsing.block.dns.adguard.com
replacementHost := p.settings.SafeBrowsingBlockHost
return p.replaceHostWithValAndReply(ctx, w, req, host, replacementHost, question)
}
// is it blocked by parental control?
if p.settings.ParentalEnabled && p.getParentalEngine().data.MatchHost(host) {
filteredParental.Inc()
// return cname family.block.dns.adguard.com
replacementHost := p.settings.ParentalBlockHost
return p.replaceHostWithValAndReply(ctx, w, req, host, replacementHost, question)
}
// is it blocked by filtering rules
ok, rule := p.matchesEngine(p.getBlockingEngine(), host, true)
if ok {
filteredLists.Inc()
return p.writeBlacklistedResponse(ctx, w, req)
}
if rule != nil {
if f, ok := rule.(*rules.NetworkRule); ok {
if f.Whitelist {
// Do nothing if this is a whitelist rule
return NotFiltered, nil
}
}
}
// try checking DNS response now
matched, rcode, err := p.filterResponse(ctx, w, req, res)
if matched {
return rcode, err
}
// indicate that the next plugin must be called
return NotFiltered, nil
}
// If response contains CNAME, A or AAAA records, we apply filtering to each canonical host name or IP address.
func (p *plug) filterResponse(ctx context.Context, w dns.ResponseWriter, req *dns.Msg, resp *dns.Msg) (bool, int, error) {
for _, a := range resp.Answer {
host := ""
switch v := a.(type) {
case *dns.CNAME:
clog.Debugf("Checking CNAME %s for %s", v.Target, v.Hdr.Name)
host = strings.TrimSuffix(v.Target, ".")
case *dns.A:
host = v.A.String()
clog.Debugf("Checking record A (%s) for %s", host, v.Hdr.Name)
case *dns.AAAA:
host = v.AAAA.String()
clog.Debugf("Checking record AAAA (%s) for %s", host, v.Hdr.Name)
default:
continue
}
if ok, _ := p.matchesEngine(p.getBlockingEngine(), host, true); ok {
clog.Debugf("Matched %s by response: %s", req.Question[0].Name, host)
filteredLists.Inc()
rcode, err := p.writeBlacklistedResponse(ctx, w, req)
return true, rcode, err
}
}
return false, 0, nil
}
// matchesEngine checks if there's a match for the specified host
// note, that if it matches a whitelist rule, the function returns false
// recordStats -- if true, we record hit for the matching rule
// returns true if request should be blocked
func (p *plug) matchesEngine(engine *urlfilter.DNSEngine, host string, recordStats bool) (bool, rules.Rule) {
if engine == nil {
return false, nil
}
res, ok := engine.Match(host, nil)
if !ok {
return false, nil
}
if res.NetworkRule != nil {
if recordStats {
recordRuleHit(res.NetworkRule.RuleText)
}
if res.NetworkRule.Whitelist {
return false, res.NetworkRule
}
return true, res.NetworkRule
}
var matchingRule rules.Rule
if len(res.HostRulesV4) > 0 {
matchingRule = res.HostRulesV4[0]
} else if len(res.HostRulesV6) > 0 {
matchingRule = res.HostRulesV6[0]
} else {
return false, nil
}
if recordStats {
recordRuleHit(matchingRule.Text())
}
return true, matchingRule
}

481
dnsfilter/dnsfilter_test.go Normal file
View File

@ -0,0 +1,481 @@
package dnsfilter
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io/ioutil"
"net"
"os"
"testing"
"github.com/caddyserver/caddy"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
)
func TestEtcHostsFilter(t *testing.T) {
text := []byte("127.0.0.1 doubleclick.net\n" + "127.0.0.1 example.org example.net www.example.org www.example.net")
tmpfile, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
if _, err = tmpfile.Write(text); err != nil {
t.Fatal(err)
}
if err = tmpfile.Close(); err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name())
configText := fmt.Sprintf("dnsfilter {\nfilter %s\n}", tmpfile.Name())
c := caddy.NewTestController("dns", configText)
c.ServerBlockKeys = []string{""}
p, err := setupPlugin(c)
if err != nil {
t.Fatal(err)
}
p.Next = zeroTTLBackend()
ctx := context.TODO()
for _, testcase := range []struct {
host string
filtered bool
}{
{"www.doubleclick.net", false},
{"doubleclick.net", true},
{"www2.example.org", false},
{"www2.example.net", false},
{"test.www.example.org", false},
{"test.www.example.net", false},
{"example.org", true},
{"example.net", true},
{"www.example.org", true},
{"www.example.net", true},
} {
req := new(dns.Msg)
req.SetQuestion(testcase.host+".", dns.TypeA)
resp := test.ResponseWriter{}
rrw := dnstest.NewRecorder(&resp)
rcode, err := p.ServeDNS(ctx, rrw, req)
if err != nil {
t.Fatalf("ServeDNS returned error: %s", err)
}
if rcode != rrw.Rcode {
t.Fatalf("ServeDNS return value for host %s has rcode %d that does not match captured rcode %d", testcase.host, rcode, rrw.Rcode)
}
A, ok := rrw.Msg.Answer[0].(*dns.A)
if !ok {
t.Fatalf("Host %s expected to have result A", testcase.host)
}
ip := net.IPv4(0, 0, 0, 0)
filtered := ip.Equal(A.A)
if testcase.filtered && testcase.filtered != filtered {
t.Fatalf("Host %s expected to be filtered, instead it is not filtered", testcase.host)
}
if !testcase.filtered && testcase.filtered != filtered {
t.Fatalf("Host %s expected to be not filtered, instead it is filtered", testcase.host)
}
}
}
func TestSafeSearchFilter(t *testing.T) {
configText := `dnsfilter {
safesearch
}`
c := caddy.NewTestController("dns", configText)
c.ServerBlockKeys = []string{""}
p, err := setupPlugin(c)
if err != nil {
t.Fatal(err)
}
p.Next = zeroTTLBackend()
ctx := context.TODO()
req := new(dns.Msg)
req.SetQuestion("www.google.com.", dns.TypeA)
resp := test.ResponseWriter{}
rrw := dnstest.NewRecorder(&resp)
rcode, err := p.ServeDNS(ctx, rrw, req)
if err != nil {
t.Fatalf("ServeDNS returned error: %s", err)
}
if rcode != rrw.Rcode {
t.Fatalf("ServeDNS return value %d that does not match captured rcode %d", rcode, rrw.Rcode)
}
assertResponseIP(t, rrw.Msg, "forcesafesearch.google.com")
}
// 4-character hash
func TestSafeBrowsingEngine(t *testing.T) {
configText := `dnsfilter {
safebrowsing ../tests/sb.txt example.net
}`
c := caddy.NewTestController("dns", configText)
c.ServerBlockKeys = []string{""}
p, err := setupPlugin(c)
if err != nil {
t.Fatal(err)
}
hash0 := sha256.Sum256([]byte("asdf.testsb.example.org"))
q0 := hex.EncodeToString(hash0[0:2])
hash1 := sha256.Sum256([]byte("testsb.example.org"))
q1 := hex.EncodeToString(hash1[0:2])
hash2 := sha256.Sum256([]byte("example.org"))
q2 := hex.EncodeToString(hash2[0:2])
result, _ := p.getSafeBrowsingEngine().data.MatchHashes(q0 + "." + q1 + "." + q2)
assert.True(t, len(result) == 1)
shash := hex.EncodeToString(hash1[:])
assert.True(t, result[0] == shash)
assert.True(t, p.getSafeBrowsingEngine().data.MatchHost("testsb.example.org"))
assert.True(t, !p.getSafeBrowsingEngine().data.MatchHost("example.org"))
}
// 8-character hash (legacy mode)
func TestSafeBrowsingEngineLegacy(t *testing.T) {
configText := `dnsfilter {
safebrowsing ../tests/sb.txt example.net
}`
c := caddy.NewTestController("dns", configText)
c.ServerBlockKeys = []string{""}
p, err := setupPlugin(c)
if err != nil {
t.Fatal(err)
}
hash0 := sha256.Sum256([]byte("asdf.testsb.example.org"))
q0 := hex.EncodeToString(hash0[0:4])
hash1 := sha256.Sum256([]byte("testsb.example.org"))
q1 := hex.EncodeToString(hash1[0:4])
hash2 := sha256.Sum256([]byte("example.org"))
q2 := hex.EncodeToString(hash2[0:4])
result, _ := p.getSafeBrowsingEngine().data.MatchHashes(q0 + "." + q1 + "." + q2)
assert.True(t, len(result) == 1)
shash := hex.EncodeToString(hash1[:])
assert.True(t, result[0] == shash)
assert.True(t, p.getSafeBrowsingEngine().data.MatchHost("testsb.example.org"))
assert.True(t, !p.getSafeBrowsingEngine().data.MatchHost("example.org"))
}
func TestSafeBrowsingFilter(t *testing.T) {
configText := `dnsfilter {
safebrowsing ../tests/sb.txt example.net
}`
c := caddy.NewTestController("dns", configText)
c.ServerBlockKeys = []string{""}
p, err := setupPlugin(c)
if err != nil {
t.Fatal(err)
}
p.Next = zeroTTLBackend()
ctx := context.TODO()
req := new(dns.Msg)
req.SetQuestion("testsb.example.org.", dns.TypeA)
resp := test.ResponseWriter{}
rrw := dnstest.NewRecorder(&resp)
rcode, err := p.ServeDNS(ctx, rrw, req)
if err != nil {
t.Fatalf("ServeDNS returned error: %s", err)
}
if rcode != rrw.Rcode {
t.Fatalf("ServeDNS return value %d that does not match captured rcode %d", rcode, rrw.Rcode)
}
assertResponseIP(t, rrw.Msg, "example.net")
}
// Send a TXT request with a hash prefix, receive response and find the target hash there
func TestSafeBrowsingFilterTXT(t *testing.T) {
configText := `dnsfilter {
safebrowsing ../tests/sb.txt example.net
}`
c := caddy.NewTestController("dns", configText)
c.ServerBlockKeys = []string{""}
p, err := setupPlugin(c)
if err != nil {
t.Fatal(err)
}
p.Next = zeroTTLBackend()
ctx := context.TODO()
hash := sha256.Sum256([]byte("testsb.example.org"))
q := hex.EncodeToString(hash[0:2])
req := new(dns.Msg)
req.SetQuestion(q+sbTXTSuffix+".", dns.TypeTXT)
resp := test.ResponseWriter{}
rrw := dnstest.NewRecorder(&resp)
rcode, err := p.ServeDNS(ctx, rrw, req)
if err != nil {
t.Fatalf("ServeDNS returned error: %s", err)
}
if rcode != rrw.Rcode {
t.Fatalf("ServeDNS return value %d that does not match captured rcode %d", rcode, rrw.Rcode)
}
assertResponseTXT(t, rrw.Msg, hex.EncodeToString(hash[:]))
}
func TestParentalEngine(t *testing.T) {
configText := `dnsfilter {
parental ../tests/parental.txt example.net
}`
c := caddy.NewTestController("dns", configText)
c.ServerBlockKeys = []string{""}
p, err := setupPlugin(c)
if err != nil {
t.Fatal(err)
}
hash0 := sha256.Sum256([]byte("asdf.testparental.example.org"))
q0 := hex.EncodeToString(hash0[0:2])
hash1 := sha256.Sum256([]byte("testparental.example.org"))
q1 := hex.EncodeToString(hash1[0:2])
hash2 := sha256.Sum256([]byte("example.org"))
q2 := hex.EncodeToString(hash2[0:2])
result, _ := p.getParentalEngine().data.MatchHashes(q0 + "." + q1 + "." + q2)
assert.True(t, len(result) == 1)
shash := hex.EncodeToString(hash1[:])
assert.True(t, result[0] == shash)
assert.True(t, p.getParentalEngine().data.MatchHost("testparental.example.org"))
assert.True(t, !p.getParentalEngine().data.MatchHost("example.org"))
}
func TestParentalFilter(t *testing.T) {
configText := `dnsfilter {
parental ../tests/parental.txt example.net
}`
c := caddy.NewTestController("dns", configText)
c.ServerBlockKeys = []string{""}
p, err := setupPlugin(c)
if err != nil {
t.Fatal(err)
}
p.Next = zeroTTLBackend()
ctx := context.TODO()
req := new(dns.Msg)
req.SetQuestion("testparental.example.org.", dns.TypeA)
resp := test.ResponseWriter{}
rrw := dnstest.NewRecorder(&resp)
rcode, err := p.ServeDNS(ctx, rrw, req)
if err != nil {
t.Fatalf("ServeDNS returned error: %s", err)
}
if rcode != rrw.Rcode {
t.Fatalf("ServeDNS return value %d that does not match captured rcode %d", rcode, rrw.Rcode)
}
assertResponseIP(t, rrw.Msg, "example.net")
}
// Send a TXT request with a hash prefix, receive response and find the target hash there
func TestParentalFilterTXT(t *testing.T) {
configText := `dnsfilter {
parental ../tests/parental.txt example.net
}`
c := caddy.NewTestController("dns", configText)
c.ServerBlockKeys = []string{""}
p, err := setupPlugin(c)
if err != nil {
t.Fatal(err)
}
p.Next = zeroTTLBackend()
ctx := context.TODO()
hash := sha256.Sum256([]byte("testparental.example.org"))
q := hex.EncodeToString(hash[0:2])
req := new(dns.Msg)
req.SetQuestion(q+pcTXTSuffix+".", dns.TypeTXT)
resp := test.ResponseWriter{}
rrw := dnstest.NewRecorder(&resp)
rcode, err := p.ServeDNS(ctx, rrw, req)
if err != nil {
t.Fatalf("ServeDNS returned error: %s", err)
}
if rcode != rrw.Rcode {
t.Fatalf("ServeDNS return value %d that does not match captured rcode %d", rcode, rrw.Rcode)
}
assertResponseTXT(t, rrw.Msg, hex.EncodeToString(hash[:]))
}
// 'badhost' has a canonical name 'badhost.eulerian.net' which is blocked by filters
func TestCNAMEFilter(t *testing.T) {
configText := `dnsfilter {
filter ../tests/dns.txt
}`
c := caddy.NewTestController("dns", configText)
c.ServerBlockKeys = []string{""}
p, err := setupPlugin(c)
if err != nil {
t.Fatal(err)
}
p.Next = backendCNAME()
ctx := context.TODO()
req := new(dns.Msg)
req.SetQuestion("badhost.", dns.TypeA)
resp := test.ResponseWriter{}
rrw := dnstest.NewRecorder(&resp)
rcode, err := p.ServeDNS(ctx, rrw, req)
if err != nil {
t.Fatalf("ServeDNS returned error: %s", err)
}
if rcode != rrw.Rcode {
t.Fatalf("ServeDNS return value %d that does not match captured rcode %d", rcode, rrw.Rcode)
}
assert.True(t, len(rrw.Msg.Answer) != 0)
haveA := false
for _, rec := range rrw.Msg.Answer {
if a, ok := rec.(*dns.A); ok {
haveA = true
assert.True(t, a.A.Equal(net.IP{0, 0, 0, 0}))
}
}
assert.True(t, haveA)
}
// 'badhost' has an IP '37.220.26.135' which is blocked by filters
func TestResponseFilter(t *testing.T) {
configText := `dnsfilter {
filter ../tests/dns.txt
}`
c := caddy.NewTestController("dns", configText)
c.ServerBlockKeys = []string{""}
p, err := setupPlugin(c)
if err != nil {
t.Fatal(err)
}
p.Next = backendBlockByIP()
ctx := context.TODO()
req := new(dns.Msg)
req.SetQuestion("badhost.", dns.TypeA)
resp := test.ResponseWriter{}
rrw := dnstest.NewRecorder(&resp)
rcode, err := p.ServeDNS(ctx, rrw, req)
if err != nil {
t.Fatalf("ServeDNS returned error: %s", err)
}
if rcode != rrw.Rcode {
t.Fatalf("ServeDNS return value %d that does not match captured rcode %d", rcode, rrw.Rcode)
}
assert.True(t, len(rrw.Msg.Answer) != 0)
haveA := false
for _, rec := range rrw.Msg.Answer {
if a, ok := rec.(*dns.A); ok {
haveA = true
assert.True(t, a.A.Equal(net.IP{0, 0, 0, 0}))
}
}
assert.True(t, haveA)
}
func assertResponseIP(t *testing.T, m *dns.Msg, expectedHost string) {
addrs, _ := net.LookupIP(expectedHost)
if len(m.Answer) == 0 {
t.Fatalf("no answer instead of %s", expectedHost)
}
for _, rec := range m.Answer {
if a, ok := rec.(*dns.A); ok {
for _, ip := range addrs {
if ip.Equal(a.A) {
// Found matching IP, all good
return
}
}
}
}
t.Fatalf("could not find %s IP addresses", expectedHost)
}
func assertResponseTXT(t *testing.T, m *dns.Msg, hash string) {
for _, rec := range m.Answer {
if txt, ok := rec.(*dns.TXT); ok {
for _, t := range txt.Txt {
if t == hash {
return
}
}
}
}
t.Fatalf("invalid TXT response")
}
func zeroTTLBackend() plugin.Handler {
return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
m := new(dns.Msg)
m.SetReply(r)
m.Response, m.RecursionAvailable = true, true
m.Answer = []dns.RR{test.A("example.org. 0 IN A 127.0.0.53")}
_ = w.WriteMsg(m)
return dns.RcodeSuccess, nil
})
}
// Return response with CNAME and A records
func backendCNAME() plugin.Handler {
return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
m := new(dns.Msg)
m.SetReply(r)
m.Response, m.RecursionAvailable = true, true
m.Answer = []dns.RR{
test.CNAME("badhost. 0 IN CNAME badhost.eulerian.net."),
test.A("badhost.eulerian.net. 0 IN A 127.0.0.53"),
}
_ = w.WriteMsg(m)
return dns.RcodeSuccess, nil
})
}
// Return response with an A record
func backendBlockByIP() plugin.Handler {
return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
m := new(dns.Msg)
m.SetReply(r)
m.Response, m.RecursionAvailable = true, true
m.Answer = []dns.RR{
test.A("badhost. 0 IN A 37.220.26.135"),
}
_ = w.WriteMsg(m)
return dns.RcodeSuccess, nil
})
}

66
dnsfilter/engines.go Normal file
View File

@ -0,0 +1,66 @@
package dnsfilter
import (
"sync"
safeservices "github.com/AdguardTeam/AdGuardDNS/dnsfilter/safe_services"
"github.com/AdguardTeam/urlfilter"
)
// Filtering engines are stored in this global map in order to avoid
// excessive memory usage.
// The key is path to the file with blocking rules.
var enginesMap = make(map[string]*engineInfo)
var enginesMapGuard = sync.Mutex{}
// engineInfo contains all the necessary information about DNS engines configuration.
// we use it to periodically reload DNS engines.
type engineInfo struct {
filtersPaths []string
dnsEngine *urlfilter.DNSEngine
data *safeservices.SafeService
}
// getSafeBrowsingEngine returns the safebrowsing filtering engineInfo
func (p *plug) getSafeBrowsingEngine() *engineInfo {
enginesMapGuard.Lock()
e, ok := enginesMap[p.settings.SafeBrowsingFilterPath]
enginesMapGuard.Unlock()
if ok {
return e
}
return nil
}
// getParentalEngine returns the parental filtering engineInfo
func (p *plug) getParentalEngine() *engineInfo {
enginesMapGuard.Lock()
e, ok := enginesMap[p.settings.ParentalFilterPath]
enginesMapGuard.Unlock()
if ok {
return e
}
return nil
}
// getBlockingEngines returns the list of blocking engines
func (p *plug) getBlockingEngine() *urlfilter.DNSEngine {
enginesMapGuard.Lock()
e, ok := enginesMap[p.settings.filterPathsKey]
enginesMapGuard.Unlock()
if ok {
return e.dnsEngine
}
return nil
}
func engineExists(key string) bool {
enginesMapGuard.Lock()
_, ok := enginesMap[key]
enginesMapGuard.Unlock()
return ok
}

207
dnsfilter/helper.go Normal file
View File

@ -0,0 +1,207 @@
package dnsfilter
import (
"fmt"
"net"
clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
// lookup host, but return answer as if it was a result of different lookup
// TODO: works only on A and AAAA, the go stdlib resolver can't do arbitrary types
func lookupReplaced(host string, question dns.Question) ([]dns.RR, error) {
var records []dns.RR
var res *net.Resolver // nil resolver is default resolver
switch question.Qtype {
case dns.TypeA:
addrs, err := res.LookupIPAddr(context.TODO(), host)
if err != nil {
return nil, err
}
for _, addr := range addrs {
if addr.IP.To4() != nil {
rr, err := dns.NewRR(fmt.Sprintf("%s A %s", question.Name, addr.IP.String()))
if err != nil {
return nil, err // fail entire request, TODO: return partial request?
}
records = append(records, rr)
}
}
case dns.TypeAAAA:
addrs, err := res.LookupIPAddr(context.TODO(), host)
if err != nil {
return nil, err
}
for _, addr := range addrs {
if addr.IP.To4() == nil {
rr, err := dns.NewRR(fmt.Sprintf("%s AAAA %s", question.Name, addr.IP.String()))
if err != nil {
return nil, err // fail entire request, TODO: return partial request?
}
records = append(records, rr)
}
}
}
return records, nil
}
func (p *plug) replaceHostWithValAndReply(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, host string, val string, question dns.Question) (int, error) {
// check if it's a domain name or IP address
addr := net.ParseIP(val)
var records []dns.RR
// log.Println("Will give", val, "instead of", host) // debug logging
if addr != nil {
// this is an IP address, return it
result, err := dns.NewRR(fmt.Sprintf("%s %d A %s", host, p.settings.BlockedTTL, val))
if err != nil {
clog.Infof("Got error %s\n", err)
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
}
records = append(records, result)
} else {
// this is a domain name, need to look it up
var err error
records, err = lookupReplaced(dns.Fqdn(val), question)
if err != nil {
clog.Infof("Got error %s\n", err)
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
}
}
m := new(dns.Msg)
m.SetReply(r)
m.RecursionAvailable = true
m.Compress = true
m.Answer = append(m.Answer, records...)
state := request.Request{W: w, Req: r}
state.SizeAndDo(m)
err := state.W.WriteMsg(m)
if err != nil {
clog.Infof("Got error %s\n", err)
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
}
return dns.RcodeSuccess, nil
}
// generate SOA record that makes DNS clients cache NXdomain results
// the only value that is important is TTL in header, other values like refresh, retry, expire and minttl are irrelevant
func (p *plug) genSOA(request *dns.Msg) []dns.RR {
zone := ""
if len(request.Question) > 0 {
zone = request.Question[0].Name
}
soa := dns.SOA{
// values copied from verisign's nonexistent .com domain
// their exact values are not important in our use case because they are used for domain transfers between primary/secondary DNS servers
Refresh: 1800,
Retry: 900,
Expire: 604800,
Minttl: 86400,
// copied from AdGuard DNS
Ns: "fake-for-negative-caching.adguard.com.",
Serial: 100500,
// rest is request-specific
Hdr: dns.RR_Header{
Name: zone,
Rrtype: dns.TypeSOA,
Ttl: p.settings.BlockedTTL,
Class: dns.ClassINET,
},
Mbox: "hostmaster.", // zone will be appended later if it's not empty or "."
}
if soa.Hdr.Ttl == 0 {
soa.Hdr.Ttl = p.settings.BlockedTTL
}
if len(zone) > 0 && zone[0] != '.' {
soa.Mbox += zone
}
return []dns.RR{&soa}
}
func (p *plug) genARecord(request *dns.Msg, ip net.IP) *dns.Msg {
resp := dns.Msg{}
resp.SetReply(request)
resp.Answer = append(resp.Answer, p.genAAnswer(request, ip))
resp.RecursionAvailable = true
resp.Compress = true
return &resp
}
func (p *plug) genAAAARecord(request *dns.Msg, ip net.IP) *dns.Msg {
resp := dns.Msg{}
resp.SetReply(request)
resp.Answer = append(resp.Answer, p.genAAAAAnswer(request, ip))
resp.RecursionAvailable = true
resp.Compress = true
return &resp
}
func (p *plug) genAAnswer(req *dns.Msg, ip net.IP) *dns.A {
answer := new(dns.A)
answer.Hdr = dns.RR_Header{
Name: req.Question[0].Name,
Rrtype: dns.TypeA,
Ttl: p.settings.BlockedTTL,
Class: dns.ClassINET,
}
answer.A = ip
return answer
}
func (p *plug) genAAAAAnswer(req *dns.Msg, ip net.IP) *dns.AAAA {
answer := new(dns.AAAA)
answer.Hdr = dns.RR_Header{
Name: req.Question[0].Name,
Rrtype: dns.TypeAAAA,
Ttl: p.settings.BlockedTTL,
Class: dns.ClassINET,
}
answer.AAAA = ip
return answer
}
func (p *plug) genNXDomain(request *dns.Msg) *dns.Msg {
resp := dns.Msg{}
resp.SetRcode(request, dns.RcodeNameError)
resp.RecursionAvailable = true
resp.Ns = p.genSOA(request)
return &resp
}
func (p *plug) writeNXDomain(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
m := p.genNXDomain(r)
state := request.Request{W: w, Req: r}
state.SizeAndDo(m)
err := state.W.WriteMsg(m)
if err != nil {
clog.Warningf("Got error %s\n", err)
return dns.RcodeServerFailure, err
}
return dns.RcodeNameError, nil
}
func (p *plug) writeBlacklistedResponse(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
var reply *dns.Msg
switch r.Question[0].Qtype {
case dns.TypeA:
reply = p.genARecord(r, []byte{0, 0, 0, 0})
case dns.TypeAAAA:
reply = p.genAAAARecord(r, net.IPv6zero)
default:
reply = p.genNXDomain(r)
}
state := request.Request{W: w, Req: r}
state.SizeAndDo(reply)
err := state.W.WriteMsg(reply)
if err != nil {
clog.Warningf("Got error %s\n", err)
return dns.RcodeServerFailure, err
}
return reply.Rcode, nil
}

85
dnsfilter/metrics.go Normal file
View File

@ -0,0 +1,85 @@
package dnsfilter
import (
"github.com/coredns/coredns/plugin"
"github.com/prometheus/client_golang/prometheus"
)
// Variables declared for monitoring.
var (
requests = newCounter("requests_total", "Count of requests seen by dnsfilter.")
filtered = newCounter("filtered_total", "Count of requests filtered by dnsfilter.")
filteredLists = newCounter("filtered_lists_total", "Count of requests filtered by dnsfilter using lists.")
filteredSafeBrowsing = newCounter("filtered_safebrowsing_total", "Count of requests filtered by dnsfilter using safebrowsing.")
filteredParental = newCounter("filtered_parental_total", "Count of requests filtered by dnsfilter using parental.")
safeSearch = newCounter("safesearch_total", "Count of requests replaced by dnsfilter safesearch.")
errorsTotal = newCounter("errors_total", "Count of requests that dnsfilter couldn't process because of transitive errors.")
requestsSafeBrowsingTXT = newCounter("requests_safebrowsing", "Safe-browsing TXT requests number.")
requestsParentalTXT = newCounter("requests_parental", "Parental-control TXT requests number.")
elapsedTime = newHistogram("request_duration", "Histogram of the time (in seconds) each request took.")
elapsedFilterTime = newHistogram("filter_duration", "Histogram of the time (in seconds) filtering of each request took.")
engineTimestamp = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsfilter",
Name: "engine_timestamp",
Help: "Last time when the engines were initialized.",
}, []string{"filter"})
engineSize = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsfilter",
Name: "engine_size",
Help: "Count of rules in the filtering engine.",
}, []string{"filter"})
engineStatus = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsfilter",
Name: "engine_status",
Help: "Status of the filtering engine (1 for loaded successfully).",
}, []string{"filter"})
statsCacheSize = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsfilter",
Name: "stats_cache_size",
Help: "Count of recorded rule hits not yet dumped.",
})
statsUploadStatus = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsfilter",
Name: "stats_upload_status",
Help: "Status of the last stats upload.",
})
statsUploadTimestamp = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsfilter",
Name: "stats_upload_timestamp",
Help: "Time when stats where uploaded last time.",
})
)
func newCounter(name string, help string) prometheus.Counter {
return prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsfilter",
Name: name,
Help: help,
})
}
func newHistogram(name string, help string) prometheus.Histogram {
return prometheus.NewHistogram(prometheus.HistogramOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsfilter",
Name: name,
Help: help,
})
}

111
dnsfilter/reload.go Normal file
View File

@ -0,0 +1,111 @@
package dnsfilter
import (
"sync"
"time"
clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/joomcode/errorx"
)
// updateCheckPeriod is a period that dnsfilter uses to check for filters updates
const updateCheckPeriod = time.Minute * 10
// updatesMap is used to store filter lists update information
// every 10 minutes we're checking if it's time to check for the filter list updates
// if it's time, and if the filter list was successfully updated,
// we reload filtering engines
var updatesMap = make(map[string]*updateInfo)
var updatesMapGuard = sync.Mutex{}
// Start reloading goroutine right away
func init() {
go reload()
}
func reload() {
// Wait first time
time.Sleep(updateCheckPeriod)
for range time.Tick(updateCheckPeriod) {
if updateCheck() {
reloadEngines()
}
}
}
// updateCheck - checks and download updates if necessary
// returns true if at least one filter list was updated
func updateCheck() bool {
wasUpdated := false
updatesMapGuard.Lock()
for key, u := range updatesMap {
updated, err := u.update()
if err != nil {
clog.Errorf("Failed to check updates for %s: %v", key, err)
}
if updated {
wasUpdated = true
}
}
updatesMapGuard.Unlock()
return wasUpdated
}
// reloadEngines reloads all filter lists from the files
func reloadEngines() {
clog.Info("Start reloading filters")
enginesMapCopy := make(map[string]*engineInfo)
enginesMapGuard.Lock()
for key, engine := range enginesMap {
enginesMapCopy[key] = engine
}
enginesMapGuard.Unlock()
for key, engine := range enginesMapCopy {
// TODO: maybe panic would be better here?
_ = reloadEngine(key, engine)
}
clog.Info("Finished reloading filters")
}
// reloadEngine reloads DNS engine and replaces it in the enginesMap
func reloadEngine(key string, engine *engineInfo) error {
clog.Infof("Reloading filtering engine for %s", key)
cnt := 0
var newEngine *engineInfo
if engine.dnsEngine == nil {
engine, count, err := createSecurityServiceEngine(key)
if err != nil {
return errorx.Decorate(err, "cannot create DNS engine: %s", engine.filtersPaths[0])
}
newEngine = engine
cnt = count
} else {
e, count, err := newDNSEngine(engine.filtersPaths)
if err != nil {
engineStatus.WithLabelValues(key).Set(float64(0))
clog.Errorf("failed to reload engine: %s", err)
return errorx.Decorate(err, "failed to reload engine for %s", key)
}
newEngine = &engineInfo{
dnsEngine: e,
filtersPaths: engine.filtersPaths,
}
cnt = count
}
enginesMapGuard.Lock()
enginesMap[key] = newEngine
enginesMapGuard.Unlock()
engineStatus.WithLabelValues(key).Set(float64(1))
engineSize.WithLabelValues(key).Set(float64(cnt))
engineTimestamp.WithLabelValues(key).SetToCurrentTime()
clog.Infof("Finished reloading filtering engine for %s", key)
return nil
}

View File

@ -0,0 +1,73 @@
# Parental Control and SafeBrowsing
## Initialization
Input data is a file with the list of host names that must be blocked (both PC & SB services have their own filter file):
badsite1
badsite2
...
When PC/SB services are initializing they:
* get the total number of lines in file and create a hash map
* read the file line by line
* get SHA256 hash sum of the host name
* add the sum value into the hash map as shown below
Suppose that there are 2 host names with similar hash sums:
01abcdef1234...
01abcdef0987...
Add these hashes to the hash map like so that:
* the key equals to bytes [0..1] of each hash sum
* the value equals to an array of bytes [2..31] of each hash sum
e.g.:
"01ab" -> []{
"cdef1234...",
"cdef0987..."
}
And for a faster search we sort the hashes:
"01ab" -> []{
"cdef0987..."
"cdef1234...",
}
## DNS messages
To check if the host is blocked, a client sends a TXT record with the Name field equal to the hash value of the host name.
DNS Question:
NAME=[0x04 "01ab" 0x04 "2345" 0x02 "sb" 0x03 "dns" 0x07 "adguard" 0x03 "com" 0x00]
TYPE=TXT
CLASS=IN
Legacy mode is also supported where the length of 1 hash is 8 characters, not 4.
For the server to distinguish between SB or PC requests, the Name field in the question has either "pc" or "sb" suffix. For example, the Name in the previous request, only now for PC service, will look like this:
NAME=[0x04 "01ab" 0x04 "2345" 0x02 "pc" 0x03 "dns" 0x07 "adguard" 0x03 "com" 0x00]
In this request a client wants to check 2 domains with the hash sums starting with "01ab" and "2345".
The response to this request is the list of SHA256 hash values that start with "01ab" and "2345".
DNS Answers:
[0]:
NAME=[0x04 "01ab" 0x04 "2345" 0x02 "sb" ...]
TYPE=TXT
CLASS=IN
TTL=...
LENGTH=...
DATA=["01abcdef1234...", "01abcdef0987...", "23456789abcd..." ]
Upon receiving the response the client compares each hash value with its target host.
If the hash values match, it means that this host is blocked by PC/SB services.
Note that since neither the client nor the server trasmits the full host name along with its hash sum, there may be a chance of a hash collision and so the host which is not in the blocklist will be treated as blocked.

View File

@ -0,0 +1,216 @@
package safeservices
var SafeSearchDomains = map[string]string{
"yandex.com": "213.180.193.56",
"yandex.ru": "213.180.193.56",
"yandex.ua": "213.180.193.56",
"yandex.by": "213.180.193.56",
"yandex.kz": "213.180.193.56",
"www.yandex.com": "213.180.193.56",
"www.yandex.ru": "213.180.193.56",
"www.yandex.ua": "213.180.193.56",
"www.yandex.by": "213.180.193.56",
"www.yandex.kz": "213.180.193.56",
"www.bing.com": "strict.bing.com",
"duckduckgo.com": "safe.duckduckgo.com",
"www.duckduckgo.com": "safe.duckduckgo.com",
"start.duckduckgo.com": "safe.duckduckgo.com",
"www.google.com": "forcesafesearch.google.com",
"www.google.ad": "forcesafesearch.google.com",
"www.google.ae": "forcesafesearch.google.com",
"www.google.com.af": "forcesafesearch.google.com",
"www.google.com.ag": "forcesafesearch.google.com",
"www.google.com.ai": "forcesafesearch.google.com",
"www.google.al": "forcesafesearch.google.com",
"www.google.am": "forcesafesearch.google.com",
"www.google.co.ao": "forcesafesearch.google.com",
"www.google.com.ar": "forcesafesearch.google.com",
"www.google.as": "forcesafesearch.google.com",
"www.google.at": "forcesafesearch.google.com",
"www.google.com.au": "forcesafesearch.google.com",
"www.google.az": "forcesafesearch.google.com",
"www.google.ba": "forcesafesearch.google.com",
"www.google.com.bd": "forcesafesearch.google.com",
"www.google.be": "forcesafesearch.google.com",
"www.google.bf": "forcesafesearch.google.com",
"www.google.bg": "forcesafesearch.google.com",
"www.google.com.bh": "forcesafesearch.google.com",
"www.google.bi": "forcesafesearch.google.com",
"www.google.bj": "forcesafesearch.google.com",
"www.google.com.bn": "forcesafesearch.google.com",
"www.google.com.bo": "forcesafesearch.google.com",
"www.google.com.br": "forcesafesearch.google.com",
"www.google.bs": "forcesafesearch.google.com",
"www.google.bt": "forcesafesearch.google.com",
"www.google.co.bw": "forcesafesearch.google.com",
"www.google.by": "forcesafesearch.google.com",
"www.google.com.bz": "forcesafesearch.google.com",
"www.google.ca": "forcesafesearch.google.com",
"www.google.cd": "forcesafesearch.google.com",
"www.google.cf": "forcesafesearch.google.com",
"www.google.cg": "forcesafesearch.google.com",
"www.google.ch": "forcesafesearch.google.com",
"www.google.ci": "forcesafesearch.google.com",
"www.google.co.ck": "forcesafesearch.google.com",
"www.google.cl": "forcesafesearch.google.com",
"www.google.cm": "forcesafesearch.google.com",
"www.google.cn": "forcesafesearch.google.com",
"www.google.com.co": "forcesafesearch.google.com",
"www.google.co.cr": "forcesafesearch.google.com",
"www.google.com.cu": "forcesafesearch.google.com",
"www.google.cv": "forcesafesearch.google.com",
"www.google.com.cy": "forcesafesearch.google.com",
"www.google.cz": "forcesafesearch.google.com",
"www.google.de": "forcesafesearch.google.com",
"www.google.dj": "forcesafesearch.google.com",
"www.google.dk": "forcesafesearch.google.com",
"www.google.dm": "forcesafesearch.google.com",
"www.google.com.do": "forcesafesearch.google.com",
"www.google.dz": "forcesafesearch.google.com",
"www.google.com.ec": "forcesafesearch.google.com",
"www.google.ee": "forcesafesearch.google.com",
"www.google.com.eg": "forcesafesearch.google.com",
"www.google.es": "forcesafesearch.google.com",
"www.google.com.et": "forcesafesearch.google.com",
"www.google.fi": "forcesafesearch.google.com",
"www.google.com.fj": "forcesafesearch.google.com",
"www.google.fm": "forcesafesearch.google.com",
"www.google.fr": "forcesafesearch.google.com",
"www.google.ga": "forcesafesearch.google.com",
"www.google.ge": "forcesafesearch.google.com",
"www.google.gg": "forcesafesearch.google.com",
"www.google.com.gh": "forcesafesearch.google.com",
"www.google.com.gi": "forcesafesearch.google.com",
"www.google.gl": "forcesafesearch.google.com",
"www.google.gm": "forcesafesearch.google.com",
"www.google.gp": "forcesafesearch.google.com",
"www.google.gr": "forcesafesearch.google.com",
"www.google.com.gt": "forcesafesearch.google.com",
"www.google.gy": "forcesafesearch.google.com",
"www.google.com.hk": "forcesafesearch.google.com",
"www.google.hn": "forcesafesearch.google.com",
"www.google.hr": "forcesafesearch.google.com",
"www.google.ht": "forcesafesearch.google.com",
"www.google.hu": "forcesafesearch.google.com",
"www.google.co.id": "forcesafesearch.google.com",
"www.google.ie": "forcesafesearch.google.com",
"www.google.co.il": "forcesafesearch.google.com",
"www.google.im": "forcesafesearch.google.com",
"www.google.co.in": "forcesafesearch.google.com",
"www.google.iq": "forcesafesearch.google.com",
"www.google.is": "forcesafesearch.google.com",
"www.google.it": "forcesafesearch.google.com",
"www.google.je": "forcesafesearch.google.com",
"www.google.com.jm": "forcesafesearch.google.com",
"www.google.jo": "forcesafesearch.google.com",
"www.google.co.jp": "forcesafesearch.google.com",
"www.google.co.ke": "forcesafesearch.google.com",
"www.google.com.kh": "forcesafesearch.google.com",
"www.google.ki": "forcesafesearch.google.com",
"www.google.kg": "forcesafesearch.google.com",
"www.google.co.kr": "forcesafesearch.google.com",
"www.google.com.kw": "forcesafesearch.google.com",
"www.google.kz": "forcesafesearch.google.com",
"www.google.la": "forcesafesearch.google.com",
"www.google.com.lb": "forcesafesearch.google.com",
"www.google.li": "forcesafesearch.google.com",
"www.google.lk": "forcesafesearch.google.com",
"www.google.co.ls": "forcesafesearch.google.com",
"www.google.lt": "forcesafesearch.google.com",
"www.google.lu": "forcesafesearch.google.com",
"www.google.lv": "forcesafesearch.google.com",
"www.google.com.ly": "forcesafesearch.google.com",
"www.google.co.ma": "forcesafesearch.google.com",
"www.google.md": "forcesafesearch.google.com",
"www.google.me": "forcesafesearch.google.com",
"www.google.mg": "forcesafesearch.google.com",
"www.google.mk": "forcesafesearch.google.com",
"www.google.ml": "forcesafesearch.google.com",
"www.google.com.mm": "forcesafesearch.google.com",
"www.google.mn": "forcesafesearch.google.com",
"www.google.ms": "forcesafesearch.google.com",
"www.google.com.mt": "forcesafesearch.google.com",
"www.google.mu": "forcesafesearch.google.com",
"www.google.mv": "forcesafesearch.google.com",
"www.google.mw": "forcesafesearch.google.com",
"www.google.com.mx": "forcesafesearch.google.com",
"www.google.com.my": "forcesafesearch.google.com",
"www.google.co.mz": "forcesafesearch.google.com",
"www.google.com.na": "forcesafesearch.google.com",
"www.google.com.nf": "forcesafesearch.google.com",
"www.google.com.ng": "forcesafesearch.google.com",
"www.google.com.ni": "forcesafesearch.google.com",
"www.google.ne": "forcesafesearch.google.com",
"www.google.nl": "forcesafesearch.google.com",
"www.google.no": "forcesafesearch.google.com",
"www.google.com.np": "forcesafesearch.google.com",
"www.google.nr": "forcesafesearch.google.com",
"www.google.nu": "forcesafesearch.google.com",
"www.google.co.nz": "forcesafesearch.google.com",
"www.google.com.om": "forcesafesearch.google.com",
"www.google.com.pa": "forcesafesearch.google.com",
"www.google.com.pe": "forcesafesearch.google.com",
"www.google.com.pg": "forcesafesearch.google.com",
"www.google.com.ph": "forcesafesearch.google.com",
"www.google.com.pk": "forcesafesearch.google.com",
"www.google.pl": "forcesafesearch.google.com",
"www.google.pn": "forcesafesearch.google.com",
"www.google.com.pr": "forcesafesearch.google.com",
"www.google.ps": "forcesafesearch.google.com",
"www.google.pt": "forcesafesearch.google.com",
"www.google.com.py": "forcesafesearch.google.com",
"www.google.com.qa": "forcesafesearch.google.com",
"www.google.ro": "forcesafesearch.google.com",
"www.google.ru": "forcesafesearch.google.com",
"www.google.rw": "forcesafesearch.google.com",
"www.google.com.sa": "forcesafesearch.google.com",
"www.google.com.sb": "forcesafesearch.google.com",
"www.google.sc": "forcesafesearch.google.com",
"www.google.se": "forcesafesearch.google.com",
"www.google.com.sg": "forcesafesearch.google.com",
"www.google.sh": "forcesafesearch.google.com",
"www.google.si": "forcesafesearch.google.com",
"www.google.sk": "forcesafesearch.google.com",
"www.google.com.sl": "forcesafesearch.google.com",
"www.google.sn": "forcesafesearch.google.com",
"www.google.so": "forcesafesearch.google.com",
"www.google.sm": "forcesafesearch.google.com",
"www.google.sr": "forcesafesearch.google.com",
"www.google.st": "forcesafesearch.google.com",
"www.google.com.sv": "forcesafesearch.google.com",
"www.google.td": "forcesafesearch.google.com",
"www.google.tg": "forcesafesearch.google.com",
"www.google.co.th": "forcesafesearch.google.com",
"www.google.com.tj": "forcesafesearch.google.com",
"www.google.tk": "forcesafesearch.google.com",
"www.google.tl": "forcesafesearch.google.com",
"www.google.tm": "forcesafesearch.google.com",
"www.google.tn": "forcesafesearch.google.com",
"www.google.to": "forcesafesearch.google.com",
"www.google.com.tr": "forcesafesearch.google.com",
"www.google.tt": "forcesafesearch.google.com",
"www.google.com.tw": "forcesafesearch.google.com",
"www.google.co.tz": "forcesafesearch.google.com",
"www.google.com.ua": "forcesafesearch.google.com",
"www.google.co.ug": "forcesafesearch.google.com",
"www.google.co.uk": "forcesafesearch.google.com",
"www.google.com.uy": "forcesafesearch.google.com",
"www.google.co.uz": "forcesafesearch.google.com",
"www.google.com.vc": "forcesafesearch.google.com",
"www.google.co.ve": "forcesafesearch.google.com",
"www.google.vg": "forcesafesearch.google.com",
"www.google.co.vi": "forcesafesearch.google.com",
"www.google.com.vn": "forcesafesearch.google.com",
"www.google.vu": "forcesafesearch.google.com",
"www.google.ws": "forcesafesearch.google.com",
"www.google.rs": "forcesafesearch.google.com",
"www.youtube.com": "restrictmoderate.youtube.com",
"m.youtube.com": "restrictmoderate.youtube.com",
"youtubei.googleapis.com": "restrictmoderate.youtube.com",
"youtube.googleapis.com": "restrictmoderate.youtube.com",
"www.youtube-nocookie.com": "restrictmoderate.youtube.com",
}

View File

@ -0,0 +1,183 @@
// Safe Browsing and Parental Control services
package safeservices
import (
"bufio"
"bytes"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"os"
"sort"
"strings"
clog "github.com/coredns/coredns/plugin/pkg/log"
)
// SafeService - safe service object
type SafeService struct {
// Data for safe-browsing and parental
// The key is the first 2 bytes of hash value.
// The value is a byte-array with 30-byte chunks of data.
// These chunks are sorted alphabetically.
// map[2_BYTE_HASH_CHUNK] = 30_BYTE_HASH1_CHUNK 30_BYTE_HASH2_CHUNK ...
data map[uint16][]byte
}
// Return the next non-empty line
func nextLine(reader *bufio.Reader) string {
for {
bytes, err := reader.ReadBytes('\n')
if len(bytes) != 0 {
if err == nil {
return string(bytes[:len(bytes)-1])
}
return string(bytes)
}
if err != nil {
return ""
}
}
}
// Get key for hash map
func getKey(hash2 []byte) uint16 {
return binary.BigEndian.Uint16(hash2)
}
type hashSort struct {
data []byte // 30-byte chunks
}
func (hs *hashSort) Len() int {
return len(hs.data) / 30
}
func (hs *hashSort) Less(i, j int) bool {
r := bytes.Compare(hs.data[i*30:i*30+30], hs.data[j*30:j*30+30])
return r < 0
}
func (hs *hashSort) Swap(i, j int) {
tmp := make([]byte, 30)
copy(tmp, hs.data[i*30:i*30+30])
copy(hs.data[i*30:i*30+30], hs.data[j*30:j*30+30])
copy(hs.data[j*30:j*30+30], tmp)
}
// CreateMap - read input file and fill the hash map
func CreateMap(file string) (*SafeService, int, error) {
clog.Infof("Initializing: %s", file)
f, err := os.Open(file)
if err != nil {
return nil, 0, err
}
reader := bufio.NewReaderSize(f, 4*1024)
lines := 0
for {
ln := nextLine(reader)
if len(ln) == 0 {
break
}
lines++
}
data := make(map[uint16][]byte, lines)
_, _ = f.Seek(0, 0)
reader.Reset(f)
for {
ln := nextLine(reader)
if len(ln) == 0 {
break
}
ln = strings.TrimSpace(ln)
if len(ln) == 0 || ln[0] == '#' {
continue
}
hash := sha256.Sum256([]byte(ln))
key := getKey(hash[0:2])
ar, _ := data[key]
ar = append(ar, hash[2:]...)
data[key] = ar
}
// sort the 30-byte chunks within the map's values
for k, v := range data {
hashSorter := hashSort{data: v}
sort.Sort(&hashSorter)
data[k] = hashSorter.data
}
clog.Infof("Finished initialization: processed %d entries", lines)
return &SafeService{data: data}, lines, nil
}
// MatchHashes - get the list of hash values matching the input string
func (ss *SafeService) MatchHashes(hashStr string) ([]string, error) {
result := []string{}
hashChunks := strings.Split(hashStr, ".")
for _, chunk := range hashChunks {
hash2, err := hex.DecodeString(chunk)
if err != nil {
return []string{}, err
}
if len(hash2) == 4 { // legacy mode
hash2 = hash2[0:2]
}
if len(hash2) != 2 {
return []string{}, fmt.Errorf("bad hash length: %d", len(hash2))
}
hashes, _ := ss.data[getKey(hash2)]
i := 0
for i != len(hashes) {
hash30 := hashes[i : i+30]
i += 30
hash := hash2
hash = append(hash, hash30...)
result = append(result, hex.EncodeToString(hash))
}
}
clog.Debugf("SB/PC: matched: %s: %v", hashStr, result)
return result, nil
}
// Search 30-byte data in array of 30-byte chunks
func searchHash(hashes []byte, search []byte) bool {
start := 0
end := len(hashes) / 30
for start != end {
i := start + (end-start)/2
r := bytes.Compare(hashes[i*30:i*30+30], search)
if r == 0 {
return true
} else if r > 0 {
end = i
} else {
start = i + 1
}
}
return false
}
// MatchHost - return TRUE if the host is found
func (ss *SafeService) MatchHost(host string) bool {
hashHost := sha256.Sum256([]byte(host))
hashes, _ := ss.data[getKey(hashHost[0:2])]
if searchHash(hashes, hashHost[2:32]) {
clog.Debugf("SB/PC: matched: %s", host)
return true
}
return false
}

View File

@ -0,0 +1,48 @@
package safeservices
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPrepareData(t *testing.T) {
// fill with test data
hashes := make([]byte, 30*5)
i := 0
copy(hashes[i:i+30], []byte("123456789012345678901234567898"))
i += 30
copy(hashes[i:i+30], []byte("123456789012345678901234567894"))
i += 30
copy(hashes[i:i+30], []byte("123456789012345678901234567896"))
i += 30
copy(hashes[i:i+30], []byte("123456789012345678901234567892"))
i += 30
copy(hashes[i:i+30], []byte("123456789012345678901234567890"))
// sort
hashSorter := hashSort{data: hashes}
sort.Sort(&hashSorter)
hashes = hashSorter.data
// check sorting
i = 0
assert.Equal(t, "123456789012345678901234567890", string(hashes[i:i+30]))
i += 30
assert.Equal(t, "123456789012345678901234567892", string(hashes[i:i+30]))
i += 30
assert.Equal(t, "123456789012345678901234567894", string(hashes[i:i+30]))
i += 30
assert.Equal(t, "123456789012345678901234567896", string(hashes[i:i+30]))
i += 30
assert.Equal(t, "123456789012345678901234567898", string(hashes[i:i+30]))
i += 30
assert.False(t, searchHash(hashes, []byte("123456789012345678901234567891")))
assert.True(t, searchHash(hashes, []byte("123456789012345678901234567890")))
assert.True(t, searchHash(hashes, []byte("123456789012345678901234567892")))
assert.True(t, searchHash(hashes, []byte("123456789012345678901234567894")))
assert.True(t, searchHash(hashes, []byte("123456789012345678901234567896")))
assert.True(t, searchHash(hashes, []byte("123456789012345678901234567898")))
}

376
dnsfilter/setup.go Normal file
View File

@ -0,0 +1,376 @@
package dnsfilter
import (
"bytes"
"fmt"
"io/ioutil"
"sort"
"strconv"
"strings"
"time"
safeservices "github.com/AdguardTeam/AdGuardDNS/dnsfilter/safe_services"
"github.com/AdguardTeam/urlfilter/filterlist"
"github.com/joomcode/errorx"
"github.com/AdguardTeam/urlfilter"
"github.com/caddyserver/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metrics"
clog "github.com/coredns/coredns/plugin/pkg/log"
)
func init() {
caddy.RegisterPlugin("dnsfilter", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
// plugSettings is the dnsfilter plugin settings
type plugSettings struct {
SafeSearchEnabled bool // If true -- safe search is enabled
SafeBrowsingEnabled bool // If true -- check requests against the safebrowsing filter list
SafeBrowsingBlockHost string // Hostname to use for requests blocked by safebrowsing
SafeBrowsingFilterPath string // Path to the safebrowsing filter list
ParentalEnabled bool // If true -- check requests against the parental filter list
ParentalBlockHost string // Hostname to use for requests blocked by parental control
ParentalFilterPath string // Path to the parental filter list
BlockedTTL uint32 // in seconds, default 3600
FilterPaths []string // List of filter lists for blocking ad/tracker request
// Update - map of update info for the filter lists
// It includes safebrowsing and parental filter lists
// Key is path to the filter list file
Update map[string]*updateInfo
// filterPathsKey is a key for the enginesMap to store the blockFilterEngine.
// it is composed from sorted FilterPaths joined by '#'
filterPathsKey string
}
// plug represents the plugin itself
type plug struct {
Next plugin.Handler
settings plugSettings
}
// Name returns name of the plugin as seen in Corefile and plugin.cfg
func (p *plug) Name() string { return "dnsfilter" }
func setup(c *caddy.Controller) error {
clog.Infof("Initializing the dnsfilter plugin for %s", c.ServerBlockKeys[c.ServerBlockKeyIndex])
p, err := setupPlugin(c)
if err != nil {
return err
}
config := dnsserver.GetConfig(c)
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
p.Next = next
return p
})
c.OnStartup(func() error {
metrics.MustRegister(c, requests, filtered, filteredLists, filteredSafeBrowsing,
filteredParental, safeSearch, errorsTotal,
requestsSafeBrowsingTXT, requestsParentalTXT,
elapsedTime, elapsedFilterTime,
engineTimestamp, engineSize, engineStatus,
statsCacheSize, statsUploadTimestamp, statsUploadStatus)
// Set to 1 by default
statsUploadStatus.Set(float64(1))
return nil
})
clog.Infof("Finished initializing the dnsfilter plugin for %s", c.ServerBlockKeys[c.ServerBlockKeyIndex])
return nil
}
// setupPlugin initializes the CoreDNS plugin
func setupPlugin(c *caddy.Controller) (*plug, error) {
settings, err := parseSettings(c)
if err != nil {
return nil, err
}
// It's important to call it before the initEnginesMap
// Because at this point we may need to download filter lists
// and this must be done before we attempt to init engines
err = initUpdatesMap(settings)
if err != nil {
return nil, err
}
err = initEnginesMap(settings)
if err != nil {
return nil, err
}
clog.Infof("Initialized dnsfilter settings for server block %d", c.ServerBlockIndex)
return &plug{settings: settings}, nil
}
func parseSettings(c *caddy.Controller) (plugSettings, error) {
settings := defaultPluginSettings
for c.Next() {
for c.NextBlock() {
blockValue := c.Val()
switch blockValue {
case "safebrowsing":
err := setupSafeBrowsing(c, &settings)
if err != nil {
return settings, err
}
case "safesearch":
clog.Info("Safe search is enabled")
settings.SafeSearchEnabled = true
case "parental":
err := setupParental(c, &settings)
if err != nil {
return settings, err
}
case "blocked_ttl":
if !c.NextArg() {
return settings, c.ArgErr()
}
blockedTTL, err := strconv.ParseUint(c.Val(), 10, 32)
if err != nil {
return settings, c.ArgErr()
}
clog.Infof("Blocked request TTL is %d", blockedTTL)
settings.BlockedTTL = uint32(blockedTTL)
case "filter":
if !c.NextArg() || len(c.Val()) == 0 {
return settings, c.ArgErr()
}
// Initialize filter and add it to the list
path := c.Val()
settings.FilterPaths = append(settings.FilterPaths, path)
clog.Infof("Added filter list %s", path)
err := setupUpdateInfo(path, &settings, c)
if err != nil {
clog.Errorf("Failed to setup update info: %v", err)
return settings, c.ArgErr()
}
}
}
}
sort.Strings(settings.FilterPaths)
settings.filterPathsKey = strings.Join(settings.FilterPaths, "#")
return settings, nil
}
// defaultPluginSettings -- settings to use if nothing is configured
var defaultPluginSettings = plugSettings{
SafeSearchEnabled: false,
SafeBrowsingEnabled: false,
SafeBrowsingBlockHost: "standard-block.dns.adguard.com",
SafeBrowsingFilterPath: "",
ParentalEnabled: false,
ParentalBlockHost: "family-block.dns.adguard.com",
ParentalFilterPath: "",
BlockedTTL: 3600, // in seconds
FilterPaths: make([]string, 0),
Update: map[string]*updateInfo{},
}
// setupUpdateInfo configures updateInfo for the specified filter list
func setupUpdateInfo(path string, settings *plugSettings, c *caddy.Controller) error {
u := &updateInfo{
path: path,
ttl: 1 * time.Hour,
lastTimeUpdated: time.Now(),
}
if c.NextArg() && len(c.Val()) > 0 {
u.url = c.Val()
clog.Infof("%s update URL is %s", path, u.url)
}
if c.NextArg() && len(c.Val()) > 0 {
ttl, err := strconv.Atoi(c.Val())
if err != nil || ttl <= 0 {
return c.ArgErr()
}
clog.Infof("%s filter list TTL is %d seconds", path, ttl)
u.ttl = time.Duration(ttl) * time.Second
}
if _, found := settings.Update[u.path]; !found && u.url != "" {
settings.Update[u.path] = u
}
return nil
}
// setupSafeBrowsing loads safebrowsing settings
func setupSafeBrowsing(c *caddy.Controller, settings *plugSettings) error {
clog.Info("SafeBrowsing is enabled")
settings.SafeBrowsingEnabled = true
if !c.NextArg() || len(c.Val()) == 0 {
clog.Info("SafeBrowsing filter list is not configured")
return c.ArgErr()
}
settings.SafeBrowsingFilterPath = c.Val()
clog.Infof("SafeBrowsing filter list is set to %s", settings.SafeBrowsingFilterPath)
if c.NextArg() && len(c.Val()) > 0 {
settings.SafeBrowsingBlockHost = c.Val()
clog.Infof("SafeBrowsing block host is set to %s", settings.SafeBrowsingBlockHost)
}
return setupUpdateInfo(settings.SafeBrowsingFilterPath, settings, c)
}
// setupParental loads parental control settings
func setupParental(c *caddy.Controller, settings *plugSettings) error {
clog.Info("Parental control is enabled")
settings.ParentalEnabled = true
if !c.NextArg() || len(c.Val()) == 0 {
clog.Info("Parental control filter list is not configured")
return c.ArgErr()
}
settings.ParentalFilterPath = c.Val()
clog.Infof("Parental control filter list is set to %s", settings.ParentalFilterPath)
if c.NextArg() && len(c.Val()) > 0 {
settings.ParentalBlockHost = c.Val()
clog.Infof("Parental control block host is set to %s", settings.ParentalBlockHost)
}
return setupUpdateInfo(settings.ParentalFilterPath, settings, c)
}
// newDNSEngine initializes a DNS engine using a list of specified filters
// it returns the DNS engine, and the number of lines in the filter files
func newDNSEngine(paths []string) (*urlfilter.DNSEngine, int, error) {
var lists []filterlist.RuleList
linesCount := 0
for i, path := range paths {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, 0, errorx.Decorate(err, "cannot read from %s", path)
}
linesCount += bytes.Count(b, []byte{'\n'})
if linesCount == 0 {
return nil, 0, fmt.Errorf("empty file %s", path)
}
list := &filterlist.StringRuleList{
ID: i,
RulesText: string(b),
IgnoreCosmetic: true,
}
lists = append(lists, list)
}
storage, err := filterlist.NewRuleStorage(lists)
if err != nil {
return nil, 0, errorx.Decorate(err, "cannot create rule storage")
}
return urlfilter.NewDNSEngine(storage), linesCount, nil
}
func createSecurityServiceEngine(filename string) (*engineInfo, int, error) {
rules, cnt, err := safeservices.CreateMap(filename)
if err != nil {
return nil, 0, err
}
e := &engineInfo{
filtersPaths: []string{filename},
data: rules,
}
return e, cnt, nil
}
// initEnginesMap initializes urlfilter filtering engines using the settings
// loaded from the plugin configuration
func initEnginesMap(settings plugSettings) error {
if settings.SafeBrowsingEnabled {
if !engineExists(settings.SafeBrowsingFilterPath) {
clog.Infof("Initializing SafeBrowsing filtering engine for %s", settings.SafeBrowsingFilterPath)
engine, cnt, err := createSecurityServiceEngine(settings.SafeBrowsingFilterPath)
if err != nil {
return errorx.Decorate(err, "cannot create safebrowsing DNS engine")
}
enginesMap[settings.SafeBrowsingFilterPath] = engine
engineStatus.WithLabelValues(settings.SafeBrowsingFilterPath).Set(float64(1))
engineSize.WithLabelValues(settings.SafeBrowsingFilterPath).Set(float64(cnt))
engineTimestamp.WithLabelValues(settings.SafeBrowsingFilterPath).SetToCurrentTime()
clog.Infof("Finished initializing SafeBrowsing filtering engine")
}
}
if settings.ParentalEnabled {
if !engineExists(settings.ParentalFilterPath) {
clog.Infof("Initializing Parental filtering engine for %s", settings.ParentalFilterPath)
engine, cnt, err := createSecurityServiceEngine(settings.ParentalFilterPath)
if err != nil {
return errorx.Decorate(err, "cannot create parental control DNS engine")
}
enginesMap[settings.ParentalFilterPath] = engine
engineStatus.WithLabelValues(settings.ParentalFilterPath).Set(float64(1))
engineSize.WithLabelValues(settings.ParentalFilterPath).Set(float64(cnt))
engineTimestamp.WithLabelValues(settings.ParentalFilterPath).SetToCurrentTime()
clog.Infof("Finished initializing Parental filtering engine")
}
}
if !engineExists(settings.filterPathsKey) {
clog.Infof("Initializing blocking filtering engine for %s", settings.filterPathsKey)
engine, cnt, err := newDNSEngine(settings.FilterPaths)
if err != nil {
return errorx.Decorate(err, "cannot create blocking DNS engine")
}
enginesMap[settings.filterPathsKey] = &engineInfo{
dnsEngine: engine,
filtersPaths: settings.FilterPaths,
}
engineStatus.WithLabelValues(settings.filterPathsKey).Set(float64(1))
engineSize.WithLabelValues(settings.filterPathsKey).Set(float64(cnt))
engineTimestamp.WithLabelValues(settings.filterPathsKey).SetToCurrentTime()
clog.Infof("Finished initializing blocking filtering engine")
}
return nil
}
// initUpdatesMap initializes the "updateMap" which is used for periodic updates check
func initUpdatesMap(settings plugSettings) error {
if len(settings.Update) == 0 {
// Do nothing if there are no registered updaters
return nil
}
updatesMapGuard.Lock()
defer updatesMapGuard.Unlock()
// Go through the list of updateInfo objects
// Check if the file exists. If not, download
for _, u := range settings.Update {
if _, ok := updatesMap[u.path]; !ok {
// Add to the map if it's not already there
updatesMap[u.path] = u
// Try to do the initial update (for the case when the file does not exist)
_, err := u.update()
if err != nil {
return err
}
}
}
return nil
}

106
dnsfilter/setup_test.go Normal file
View File

@ -0,0 +1,106 @@
package dnsfilter
import (
"fmt"
"net"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/caddyserver/caddy"
)
func TestSetup(t *testing.T) {
for i, testcase := range []struct {
config string
failing bool
}{
{`dnsfilter`, false},
{`dnsfilter {
filter /dev/nonexistent/abcdef
}`, true},
{`dnsfilter {
filter ../tests/dns.txt
}`, false},
{`dnsfilter {
safebrowsing ../tests/sb.txt
filter ../tests/dns.txt
}`, false},
{`dnsfilter {
parental ../tests/parental.txt
filter ../tests/dns.txt
}`, false},
} {
c := caddy.NewTestController("dns", testcase.config)
c.ServerBlockKeys = []string{""}
err := setup(c)
if err != nil {
if !testcase.failing {
t.Fatalf("Test #%d expected no errors, but got: %v", i, err)
}
continue
}
if testcase.failing {
t.Fatalf("Test #%d expected to fail but it didn't", i)
}
}
}
func TestSetupUpdate(t *testing.T) {
l := testStartFilterServer()
defer func() {
_ = l.Close()
_ = os.Remove("testsb.txt")
_ = os.Remove("testdns.txt")
_ = os.Remove("testparental.txt")
}()
port := l.Addr().(*net.TCPAddr).Port
cfg := fmt.Sprintf(`dnsfilter {
safebrowsing testsb.txt example.org http://127.0.0.1:%d/filter.txt 3600
filter testdns.txt http://127.0.0.1:%d/filter.txt 3600
parental testparental.txt example.org http://127.0.0.1:%d/filter.txt 3600
}`, port, port, port)
c := caddy.NewTestController("dns", cfg)
c.ServerBlockKeys = []string{""}
err := setup(c)
assert.Nil(t, err)
// Check that filters were downloaded
assert.FileExists(t, "testsb.txt")
assert.FileExists(t, "testdns.txt")
assert.FileExists(t, "testparental.txt")
// Check that they were added to the updatesMap
updatesMapGuard.Lock()
assert.Contains(t, updatesMap, "testsb.txt")
assert.Contains(t, updatesMap, "testdns.txt")
assert.Contains(t, updatesMap, "testparental.txt")
updatesMapGuard.Unlock()
// Check that enginesMap contain necessary elements
enginesMapGuard.Lock()
assert.Contains(t, enginesMap, "testsb.txt")
assert.Contains(t, enginesMap, "testdns.txt")
assert.Contains(t, enginesMap, "testparental.txt")
enginesMapGuard.Unlock()
// TTL is not expired yet
wasUpdated := updateCheck()
assert.False(t, wasUpdated)
// Trigger filters updates
updatesMapGuard.Lock()
for _, u := range updatesMap {
u.lastTimeUpdated = time.Now().Add(-u.ttl).Add(-1 * time.Second)
}
updatesMapGuard.Unlock()
// Check updates
wasUpdated = updateCheck()
assert.True(t, wasUpdated)
}

95
dnsfilter/stats.go Normal file
View File

@ -0,0 +1,95 @@
package dnsfilter
import (
"bytes"
"encoding/json"
"net/http"
"sync"
"time"
clog "github.com/coredns/coredns/plugin/pkg/log"
)
// AdGuard Simplified domain names filter list ID
const filterListID = 15
const statsURL = "https://chrome.adtidy.org/api/1.0/rulestats.html"
const uploadPeriod = 10 * time.Minute
type Stats struct {
FilterLists map[int]map[string]int `json:"filters"`
RecordedHits int64 `json:"-"`
}
var stats = &Stats{
FilterLists: map[int]map[string]int{},
}
var statsGuard = sync.Mutex{}
func init() {
go func() {
for range time.Tick(uploadPeriod) {
clog.Info("Uploading stats")
err := uploadStats()
if err != nil {
clog.Errorf("error while uploading status: %s", err)
} else {
clog.Info("Finished uploading stats successfully")
}
}
}()
}
// recordRuleHit records a new rule hit and increments the stats
func recordRuleHit(ruleText string) {
statsGuard.Lock()
defer statsGuard.Unlock()
v, ok := stats.FilterLists[filterListID]
if !ok {
v = map[string]int{}
stats.FilterLists[filterListID] = v
}
hits, ok := v[ruleText]
if !ok {
hits = 0
}
v[ruleText] = hits + 1
stats.RecordedHits++
statsCacheSize.Set(float64(stats.RecordedHits))
}
// uploadStats resets the current stats and sends them to the server
func uploadStats() error {
statsGuard.Lock()
statsToUpload := stats
stats = &Stats{
FilterLists: map[int]map[string]int{},
}
statsGuard.Unlock()
b, err := json.Marshal(statsToUpload)
if err != nil {
statsUploadStatus.Set(0)
return err
}
req, err := http.NewRequest(http.MethodPost, statsURL, bytes.NewReader(b))
if err != nil {
statsUploadStatus.Set(0)
return err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
var resp *http.Response
resp, err = client.Do(req)
if err != nil {
statsUploadStatus.Set(0)
return err
}
_ = resp.Body.Close()
statsUploadStatus.Set(1)
statsUploadTimestamp.SetToCurrentTime()
return nil
}

28
dnsfilter/stats_test.go Normal file
View File

@ -0,0 +1,28 @@
package dnsfilter
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
)
func TestStats(t *testing.T) {
stats = &Stats{
FilterLists: map[int]map[string]int{},
}
recordRuleHit("||example.org^")
recordRuleHit("||example.org^")
recordRuleHit("||example.org^")
recordRuleHit("||example.com^")
b, err := json.Marshal(stats)
assert.Nil(t, err)
assert.Equal(t, `{"filters":{"15":{"||example.com^":1,"||example.org^":3}}}`, string(b))
assert.Equal(t, int64(4), stats.RecordedHits)
err = uploadStats()
assert.Nil(t, err)
assert.Equal(t, int64(0), stats.RecordedHits)
}

110
dnsfilter/update.go Normal file
View File

@ -0,0 +1,110 @@
package dnsfilter
import (
"compress/gzip"
"fmt"
"io"
"net/http"
"os"
"time"
clog "github.com/coredns/coredns/plugin/pkg/log"
)
// don't allow too small files
const minFileSize = 1024
type updateInfo struct {
path string // path to the filter list file
url string // url to load filter list from
ttl time.Duration // update check period
lastTimeUpdated time.Time // last update we tried to check updates
}
// update does the update if necessary
// returns true if update was performed, otherwise - false
func (u *updateInfo) update() (bool, error) {
shouldUpdate := false
if _, err := os.Stat(u.path); os.IsNotExist(err) {
clog.Infof("File %s does not exist, we should download the filter list", u.path)
shouldUpdate = true
} else if u.lastTimeUpdated.Add(u.ttl).Before(time.Now()) {
clog.Infof("Time to download updates for %s", u.path)
shouldUpdate = true
}
if shouldUpdate {
err := u.download()
u.lastTimeUpdated = time.Now()
return err == nil, err
}
return false, nil
}
// download downloads the file from URL and replaces it in the path
func (u *updateInfo) download() error {
clog.Infof("Downloading filter %s", u.path)
client := new(http.Client)
request, err := http.NewRequest("GET", u.url, nil)
if err != nil {
return err
}
request.Header.Add("Accept-Encoding", "gzip")
// Make the request
response, err := client.Do(request)
if err != nil {
return err
}
defer func() { _ = response.Body.Close() }()
if response.StatusCode != http.StatusOK {
return fmt.Errorf("failed to download %s, response status is %d", u.url, response.StatusCode)
}
// Check that the server actually sent compressed data
var reader io.ReadCloser
switch response.Header.Get("Content-Encoding") {
case "gzip":
reader, err = gzip.NewReader(response.Body)
if err != nil {
return err
}
default:
reader = response.Body
}
defer func() { _ = reader.Close() }()
// Start reading the response to a temp file
tmpFilePath := u.path + ".tmp"
tmpFile, err := os.OpenFile(tmpFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
defer func() {
_ = tmpFile.Close()
_ = os.Remove(tmpFilePath)
}()
if err != nil {
return err
}
// Write the content to that file
// nolint (gosec)
written, err := io.Copy(tmpFile, reader)
if err != nil {
return err
}
if written < minFileSize {
return fmt.Errorf("the file downloaded from %s is too small: %d", u.url, written)
}
clog.Infof("Downloaded update for %s, size=%d", u.path, written)
// Now replace the file
_ = tmpFile.Close()
return os.Rename(tmpFilePath, u.path)
}

54
dnsfilter/update_test.go Normal file
View File

@ -0,0 +1,54 @@
package dnsfilter
import (
"fmt"
"net"
"net/http"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestUpdateDownload(t *testing.T) {
l := testStartFilterServer()
defer func() {
_ = l.Close()
}()
u := &updateInfo{
path: "testfilter.txt",
url: fmt.Sprintf("http://127.0.0.1:%d/filter.txt", l.Addr().(*net.TCPAddr).Port),
ttl: time.Minute,
}
defer func() {
_ = os.Remove(u.path)
}()
err := u.download()
assert.Nil(t, err)
assert.FileExists(t, u.path)
}
func testStartFilterServer() net.Listener {
content := ""
for i := 0; i < 1000; i++ {
content = content + "this is test line\n"
}
mux := http.NewServeMux()
mux.HandleFunc("/filter.txt", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(content))
})
listener, err := net.Listen("tcp", ":0")
if err != nil {
panic(err)
}
srv := &http.Server{Handler: mux}
go func() { _ = srv.Serve(listener) }()
return listener
}

5
forward/README.md Normal file
View File

@ -0,0 +1,5 @@
# forward
Fork of https://github.com/coredns/coredns/tree/master/plugin/forward.
The purpose is to expose "parseStanza" method to our fork of "alternate" module.

137
forward/connect.go Normal file
View File

@ -0,0 +1,137 @@
// Package forward implements a forwarding proxy. It caches an upstream net.Conn for some time, so if the same
// client returns the upstream's Conn will be precached. Depending on how you benchmark this looks to be
// 50% faster than just opening a new connection for every client. It works with UDP and TCP and uses
// inband healthchecking.
package forward
import (
"context"
"io"
"strconv"
"sync/atomic"
"time"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
// limitTimeout is a utility function to auto-tune timeout values
// average observed time is moved towards the last observed delay moderated by a weight
// next timeout to use will be the double of the computed average, limited by min and max frame.
func limitTimeout(currentAvg *int64, minValue time.Duration, maxValue time.Duration) time.Duration {
rt := time.Duration(atomic.LoadInt64(currentAvg))
if rt < minValue {
return minValue
}
if rt < maxValue/2 {
return 2 * rt
}
return maxValue
}
func averageTimeout(currentAvg *int64, observedDuration time.Duration, weight int64) {
dt := time.Duration(atomic.LoadInt64(currentAvg))
atomic.AddInt64(currentAvg, int64(observedDuration-dt)/weight)
}
func (t *Transport) dialTimeout() time.Duration {
return limitTimeout(&t.avgDialTime, minDialTimeout, maxDialTimeout)
}
func (t *Transport) updateDialTimeout(newDialTime time.Duration) {
averageTimeout(&t.avgDialTime, newDialTime, cumulativeAvgWeight)
}
// Dial dials the address configured in transport, potentially reusing a connection or creating a new one.
func (t *Transport) Dial(proto string) (*persistConn, bool, error) {
// If tls has been configured; use it.
if t.tlsConfig != nil {
proto = "tcp-tls"
}
t.dial <- proto
pc := <-t.ret
if pc != nil {
return pc, true, nil
}
reqTime := time.Now()
timeout := t.dialTimeout()
if proto == "tcp-tls" {
conn, err := dns.DialTimeoutWithTLS("tcp", t.addr, t.tlsConfig, timeout)
t.updateDialTimeout(time.Since(reqTime))
return &persistConn{c: conn}, false, err
}
conn, err := dns.DialTimeout(proto, t.addr, timeout)
t.updateDialTimeout(time.Since(reqTime))
return &persistConn{c: conn}, false, err
}
// Connect selects an upstream, sends the request and waits for a response.
func (p *Proxy) Connect(ctx context.Context, state request.Request, opts options) (*dns.Msg, error) {
start := time.Now()
proto := ""
switch {
case opts.forceTCP: // TCP flag has precedence over UDP flag
proto = "tcp"
case opts.preferUDP:
proto = "udp"
default:
proto = state.Proto()
}
pc, cached, err := p.transport.Dial(proto)
if err != nil {
return nil, err
}
// Set buffer size correctly for this client.
pc.c.UDPSize = uint16(state.Size())
if pc.c.UDPSize < 512 {
pc.c.UDPSize = 512
}
pc.c.SetWriteDeadline(time.Now().Add(maxTimeout))
if err := pc.c.WriteMsg(state.Req); err != nil {
pc.c.Close() // not giving it back
if err == io.EOF && cached {
return nil, ErrCachedClosed
}
return nil, err
}
var ret *dns.Msg
pc.c.SetReadDeadline(time.Now().Add(readTimeout))
for {
ret, err = pc.c.ReadMsg()
if err != nil {
pc.c.Close() // not giving it back
if err == io.EOF && cached {
return nil, ErrCachedClosed
}
return ret, err
}
// drop out-of-order responses
if state.Req.Id == ret.Id {
break
}
}
p.transport.Yield(pc)
rc, ok := dns.RcodeToString[ret.Rcode]
if !ok {
rc = strconv.Itoa(ret.Rcode)
}
RequestCount.WithLabelValues(p.addr).Add(1)
RcodeCount.WithLabelValues(rc, p.addr).Add(1)
RequestDuration.WithLabelValues(p.addr).Observe(time.Since(start).Seconds())
return ret, nil
}
const cumulativeAvgWeight = 4

61
forward/dnstap.go Normal file
View File

@ -0,0 +1,61 @@
package forward
import (
"context"
"time"
"github.com/coredns/coredns/plugin/dnstap"
"github.com/coredns/coredns/plugin/dnstap/msg"
"github.com/coredns/coredns/request"
tap "github.com/dnstap/golang-dnstap"
"github.com/miekg/dns"
)
func toDnstap(ctx context.Context, host string, f *Forward, state request.Request, reply *dns.Msg, start time.Time) error {
tapper := dnstap.TapperFromContext(ctx)
if tapper == nil {
return nil
}
// Query
b := msg.New().Time(start).HostPort(host)
opts := f.opts
t := ""
switch {
case opts.forceTCP: // TCP flag has precedence over UDP flag
t = "tcp"
case opts.preferUDP:
t = "udp"
default:
t = state.Proto()
}
if t == "tcp" {
b.SocketProto = tap.SocketProtocol_TCP
} else {
b.SocketProto = tap.SocketProtocol_UDP
}
if tapper.Pack() {
b.Msg(state.Req)
}
m, err := b.ToOutsideQuery(tap.Message_FORWARDER_QUERY)
if err != nil {
return err
}
tapper.TapMessage(m)
// Response
if reply != nil {
if tapper.Pack() {
b.Msg(reply)
}
m, err := b.Time(time.Now()).ToOutsideResponse(tap.Message_FORWARDER_RESPONSE)
if err != nil {
return err
}
tapper.TapMessage(m)
}
return nil
}

63
forward/dnstap_test.go Normal file
View File

@ -0,0 +1,63 @@
package forward
import (
"context"
"testing"
"time"
"github.com/coredns/coredns/plugin/dnstap"
"github.com/coredns/coredns/plugin/dnstap/msg"
"github.com/coredns/coredns/plugin/dnstap/test"
mwtest "github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
tap "github.com/dnstap/golang-dnstap"
"github.com/miekg/dns"
)
func testCase(t *testing.T, f *Forward, q, r *dns.Msg, datq, datr *msg.Builder) {
tapq, _ := datq.ToOutsideQuery(tap.Message_FORWARDER_QUERY)
tapr, _ := datr.ToOutsideResponse(tap.Message_FORWARDER_RESPONSE)
tapper := test.TrapTapper{}
ctx := dnstap.ContextWithTapper(context.TODO(), &tapper)
err := toDnstap(ctx, "10.240.0.1:40212", f,
request.Request{W: &mwtest.ResponseWriter{}, Req: q}, r, time.Now())
if err != nil {
t.Fatal(err)
}
if len(tapper.Trap) != 2 {
t.Fatalf("Messages: %d", len(tapper.Trap))
}
if !test.MsgEqual(tapper.Trap[0], tapq) {
t.Errorf("Want: %v\nhave: %v", tapq, tapper.Trap[0])
}
if !test.MsgEqual(tapper.Trap[1], tapr) {
t.Errorf("Want: %v\nhave: %v", tapr, tapper.Trap[1])
}
}
func TestDnstap(t *testing.T) {
q := mwtest.Case{Qname: "example.org", Qtype: dns.TypeA}.Msg()
r := mwtest.Case{
Qname: "example.org.", Qtype: dns.TypeA,
Answer: []dns.RR{
mwtest.A("example.org. 3600 IN A 10.0.0.1"),
},
}.Msg()
tapq, tapr := test.TestingData(), test.TestingData()
fu := New()
fu.opts.preferUDP = true
testCase(t, fu, q, r, tapq, tapr)
tapq.SocketProto = tap.SocketProtocol_TCP
tapr.SocketProto = tap.SocketProtocol_TCP
ft := New()
ft.opts.forceTCP = true
testCase(t, ft, q, r, tapq, tapr)
}
func TestNoDnstap(t *testing.T) {
err := toDnstap(context.TODO(), "", nil, request.Request{}, nil, time.Now())
if err != nil {
t.Fatal(err)
}
}

232
forward/forward.go Normal file
View File

@ -0,0 +1,232 @@
// Package forward implements a forwarding proxy. It caches an upstream net.Conn for some time, so if the same
// client returns the upstream's Conn will be precached. Depending on how you benchmark this looks to be
// 50% faster than just opening a new connection for every client. It works with UDP and TCP and uses
// inband healthchecking.
package forward
import (
"context"
"crypto/tls"
"errors"
"sync/atomic"
"time"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/debug"
clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/coredns/coredns/plugin/pkg/policy"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
ot "github.com/opentracing/opentracing-go"
)
var log = clog.NewWithPlugin("forward")
// Forward represents a plugin instance that can proxy requests to another (DNS) server. It has a list
// of proxies each representing one upstream proxy.
type Forward struct {
concurrent int64 // atomic counters need to be first in struct for proper alignment
proxies []*Proxy
p policy.Policy
hcInterval time.Duration
from string
ignored []string
tlsConfig *tls.Config
tlsServerName string
maxfails uint32
expire time.Duration
maxConcurrent int64
opts options // also here for testing
// ErrLimitExceeded indicates that a query was rejected because the number of concurrent queries has exceeded
// the maximum allowed (maxConcurrent)
ErrLimitExceeded error
Next plugin.Handler
}
// New returns a new Forward.
func New() *Forward {
f := &Forward{maxfails: 2, tlsConfig: new(tls.Config), expire: defaultExpire, p: new(policy.Random), from: ".", hcInterval: hcInterval, opts: options{forceTCP: false, preferUDP: false, hcRecursionDesired: true}}
return f
}
// SetProxy appends p to the proxy list and starts healthchecking.
func (f *Forward) SetProxy(p *Proxy) {
f.proxies = append(f.proxies, p)
p.start(f.hcInterval)
}
// Len returns the number of configured proxies.
func (f *Forward) Len() int { return len(f.proxies) }
// Name implements plugin.Handler.
func (f *Forward) Name() string { return "forward" }
// ServeDNS implements plugin.Handler.
func (f *Forward) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
if !f.match(state) {
return plugin.NextOrFailure(f.Name(), f.Next, ctx, w, r)
}
if f.maxConcurrent > 0 {
count := atomic.AddInt64(&(f.concurrent), 1)
defer atomic.AddInt64(&(f.concurrent), -1)
if count > f.maxConcurrent {
MaxConcurrentRejectCount.Add(1)
return dns.RcodeServerFailure, f.ErrLimitExceeded
}
}
fails := 0
var span, child ot.Span
var upstreamErr error
span = ot.SpanFromContext(ctx)
i := 0
list := f.List()
deadline := time.Now().Add(defaultTimeout)
start := time.Now()
for time.Now().Before(deadline) {
if i >= len(list) {
// reached the end of list, reset to begin
i = 0
fails = 0
}
proxy := list[i]
i++
if proxy.Down(f.maxfails) {
fails++
if fails < len(f.proxies) {
continue
}
// All upstream proxies are dead, assume healthcheck is completely broken and randomly
// select an upstream to connect to.
r := new(policy.Random)
proxy = r.List(f.proxies)[0].([]*Proxy)[0]
HealthcheckBrokenCount.Add(1)
}
if span != nil {
child = span.Tracer().StartSpan("connect", ot.ChildOf(span.Context()))
ctx = ot.ContextWithSpan(ctx, child)
}
var (
ret *dns.Msg
err error
)
opts := f.opts
for {
ret, err = proxy.Connect(ctx, state, opts)
if err == ErrCachedClosed { // Remote side closed conn, can only happen with TCP.
continue
}
// Retry with TCP if truncated and prefer_udp configured.
if ret != nil && ret.Truncated && !opts.forceTCP && opts.preferUDP {
opts.forceTCP = true
continue
}
break
}
if child != nil {
child.Finish()
}
taperr := toDnstap(ctx, proxy.addr, f, state, ret, start)
upstreamErr = err
if err != nil {
// Kick off health check to see if *our* upstream is broken.
if f.maxfails != 0 {
proxy.Healthcheck()
}
if fails < len(f.proxies) {
continue
}
break
}
// Check if the reply is correct; if not return FormErr.
if !state.Match(ret) {
debug.Hexdumpf(ret, "Wrong reply for id: %d, %s %d", ret.Id, state.QName(), state.QType())
formerr := new(dns.Msg)
formerr.SetRcode(state.Req, dns.RcodeFormatError)
w.WriteMsg(formerr)
return 0, taperr
}
w.WriteMsg(ret)
return 0, taperr
}
if upstreamErr != nil {
return dns.RcodeServerFailure, upstreamErr
}
return dns.RcodeServerFailure, ErrNoHealthy
}
func (f *Forward) match(state request.Request) bool {
if !plugin.Name(f.from).Matches(state.Name()) || !f.isAllowedDomain(state.Name()) {
return false
}
return true
}
func (f *Forward) isAllowedDomain(name string) bool {
if dns.Name(name) == dns.Name(f.from) {
return true
}
for _, ignore := range f.ignored {
if plugin.Name(ignore).Matches(name) {
return false
}
}
return true
}
// ForceTCP returns if TCP is forced to be used even when the request comes in over UDP.
func (f *Forward) ForceTCP() bool { return f.opts.forceTCP }
// PreferUDP returns if UDP is preferred to be used even when the request comes in over TCP.
func (f *Forward) PreferUDP() bool { return f.opts.preferUDP }
// List returns a set of proxies to be used for this client depending on the policy in f.
func (f *Forward) List() []*Proxy {
if len(f.p.List(f.proxies)) == 1 {
return f.p.List(f.proxies)[0].([]*Proxy)
}
return nil
}
var (
// ErrNoHealthy means no healthy proxies left.
ErrNoHealthy = errors.New("no healthy proxies")
// ErrNoForward means no forwarder defined.
ErrNoForward = errors.New("no forwarder defined")
// ErrCachedClosed means cached connection was closed by peer.
ErrCachedClosed = errors.New("cached connection was closed by peer")
)
// options holds various options that can be set.
type options struct {
forceTCP bool
preferUDP bool
hcRecursionDesired bool
}
const defaultTimeout = 5 * time.Second

34
forward/fuzz.go Normal file
View File

@ -0,0 +1,34 @@
// +build gofuzz
package forward
import (
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/pkg/fuzz"
"github.com/miekg/dns"
)
var f *Forward
// abuse init to setup an environment to test against. This start another server to that will
// reflect responses.
func init() {
f = New()
s := dnstest.NewServer(r{}.reflectHandler)
f.proxies = append(f.proxies, NewProxy(s.Addr, "tcp"))
f.proxies = append(f.proxies, NewProxy(s.Addr, "udp"))
}
// Fuzz fuzzes forward.
func Fuzz(data []byte) int {
return fuzz.Do(f, data)
}
type r struct{}
func (r r) reflectHandler(w dns.ResponseWriter, req *dns.Msg) {
m := new(dns.Msg)
m.SetReply(req)
w.WriteMsg(m)
}

86
forward/health.go Normal file
View File

@ -0,0 +1,86 @@
package forward
import (
"crypto/tls"
"sync/atomic"
"time"
"github.com/coredns/coredns/plugin/pkg/transport"
"github.com/miekg/dns"
)
// HealthChecker checks the upstream health.
type HealthChecker interface {
Check(*Proxy) error
SetTLSConfig(*tls.Config)
SetRecursionDesired(bool)
GetRecursionDesired() bool
}
// dnsHc is a health checker for a DNS endpoint (DNS, and DoT).
type dnsHc struct {
c *dns.Client
recursionDesired bool
}
// NewHealthChecker returns a new HealthChecker based on transport.
func NewHealthChecker(trans string, recursionDesired bool) HealthChecker {
switch trans {
case transport.DNS, transport.TLS:
c := new(dns.Client)
c.Net = "udp"
c.ReadTimeout = 1 * time.Second
c.WriteTimeout = 1 * time.Second
return &dnsHc{c: c, recursionDesired: recursionDesired}
}
log.Warningf("No healthchecker for transport %q", trans)
return nil
}
func (h *dnsHc) SetTLSConfig(cfg *tls.Config) {
h.c.Net = "tcp-tls"
h.c.TLSConfig = cfg
}
func (h *dnsHc) SetRecursionDesired(recursionDesired bool) {
h.recursionDesired = recursionDesired
}
func (h *dnsHc) GetRecursionDesired() bool {
return h.recursionDesired
}
// For HC we send to . IN NS +[no]rec message to the upstream. Dial timeouts and empty
// replies are considered fails, basically anything else constitutes a healthy upstream.
// Check is used as the up.Func in the up.Probe.
func (h *dnsHc) Check(p *Proxy) error {
err := h.send(p.addr)
if err != nil {
HealthcheckFailureCount.WithLabelValues(p.addr).Add(1)
atomic.AddUint32(&p.fails, 1)
return err
}
atomic.StoreUint32(&p.fails, 0)
return nil
}
func (h *dnsHc) send(addr string) error {
ping := new(dns.Msg)
ping.SetQuestion(".", dns.TypeNS)
ping.MsgHdr.RecursionDesired = h.recursionDesired
m, _, err := h.c.Exchange(ping, addr)
// If we got a header, we're alright, basically only care about I/O errors 'n stuff.
if err != nil && m != nil {
// Silly check, something sane came back.
if m.Response || m.Opcode == dns.OpcodeQuery {
err = nil
}
}
return err
}

225
forward/health_test.go Normal file
View File

@ -0,0 +1,225 @@
package forward
import (
"context"
"sync/atomic"
"testing"
"time"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/pkg/transport"
"github.com/coredns/coredns/plugin/test"
"github.com/miekg/dns"
)
func TestHealth(t *testing.T) {
const expected = 1
i := uint32(0)
q := uint32(0)
s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
if atomic.LoadUint32(&q) == 0 { //drop the first query to trigger health-checking
atomic.AddUint32(&q, 1)
return
}
if r.Question[0].Name == "." && r.RecursionDesired == true {
atomic.AddUint32(&i, 1)
}
ret := new(dns.Msg)
ret.SetReply(r)
w.WriteMsg(ret)
})
defer s.Close()
p := NewProxy(s.Addr, transport.DNS)
f := New()
f.SetProxy(p)
defer f.OnShutdown()
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeA)
f.ServeDNS(context.TODO(), &test.ResponseWriter{}, req)
time.Sleep(1 * time.Second)
i1 := atomic.LoadUint32(&i)
if i1 != expected {
t.Errorf("Expected number of health checks with RecursionDesired==true to be %d, got %d", expected, i1)
}
}
func TestHealthNoRecursion(t *testing.T) {
const expected = 1
i := uint32(0)
q := uint32(0)
s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
if atomic.LoadUint32(&q) == 0 { //drop the first query to trigger health-checking
atomic.AddUint32(&q, 1)
return
}
if r.Question[0].Name == "." && r.RecursionDesired == false {
atomic.AddUint32(&i, 1)
}
ret := new(dns.Msg)
ret.SetReply(r)
w.WriteMsg(ret)
})
defer s.Close()
p := NewProxy(s.Addr, transport.DNS)
p.health.SetRecursionDesired(false)
f := New()
f.SetProxy(p)
defer f.OnShutdown()
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeA)
f.ServeDNS(context.TODO(), &test.ResponseWriter{}, req)
time.Sleep(1 * time.Second)
i1 := atomic.LoadUint32(&i)
if i1 != expected {
t.Errorf("Expected number of health checks with RecursionDesired==false to be %d, got %d", expected, i1)
}
}
func TestHealthTimeout(t *testing.T) {
const expected = 1
i := uint32(0)
q := uint32(0)
s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
if r.Question[0].Name == "." {
// health check, answer
atomic.AddUint32(&i, 1)
ret := new(dns.Msg)
ret.SetReply(r)
w.WriteMsg(ret)
return
}
if atomic.LoadUint32(&q) == 0 { //drop only first query
atomic.AddUint32(&q, 1)
return
}
ret := new(dns.Msg)
ret.SetReply(r)
w.WriteMsg(ret)
})
defer s.Close()
p := NewProxy(s.Addr, transport.DNS)
f := New()
f.SetProxy(p)
defer f.OnShutdown()
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeA)
f.ServeDNS(context.TODO(), &test.ResponseWriter{}, req)
time.Sleep(1 * time.Second)
i1 := atomic.LoadUint32(&i)
if i1 != expected {
t.Errorf("Expected number of health checks to be %d, got %d", expected, i1)
}
}
func TestHealthFailTwice(t *testing.T) {
const expected = 2
i := uint32(0)
q := uint32(0)
s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
if r.Question[0].Name == "." {
atomic.AddUint32(&i, 1)
i1 := atomic.LoadUint32(&i)
// Timeout health until we get the second one
if i1 < 2 {
return
}
ret := new(dns.Msg)
ret.SetReply(r)
w.WriteMsg(ret)
return
}
if atomic.LoadUint32(&q) == 0 { //drop only first query
atomic.AddUint32(&q, 1)
return
}
ret := new(dns.Msg)
ret.SetReply(r)
w.WriteMsg(ret)
})
defer s.Close()
p := NewProxy(s.Addr, transport.DNS)
f := New()
f.SetProxy(p)
defer f.OnShutdown()
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeA)
f.ServeDNS(context.TODO(), &test.ResponseWriter{}, req)
time.Sleep(3 * time.Second)
i1 := atomic.LoadUint32(&i)
if i1 != expected {
t.Errorf("Expected number of health checks to be %d, got %d", expected, i1)
}
}
func TestHealthMaxFails(t *testing.T) {
s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
// timeout
})
defer s.Close()
p := NewProxy(s.Addr, transport.DNS)
f := New()
f.maxfails = 2
f.SetProxy(p)
defer f.OnShutdown()
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeA)
f.ServeDNS(context.TODO(), &test.ResponseWriter{}, req)
time.Sleep(readTimeout + 1*time.Second)
fails := atomic.LoadUint32(&p.fails)
if !p.Down(f.maxfails) {
t.Errorf("Expected Proxy fails to be greater than %d, got %d", f.maxfails, fails)
}
}
func TestHealthNoMaxFails(t *testing.T) {
const expected = 0
i := uint32(0)
s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
if r.Question[0].Name == "." {
// health check, answer
atomic.AddUint32(&i, 1)
ret := new(dns.Msg)
ret.SetReply(r)
w.WriteMsg(ret)
}
})
defer s.Close()
p := NewProxy(s.Addr, transport.DNS)
f := New()
f.maxfails = 0
f.SetProxy(p)
defer f.OnShutdown()
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeA)
f.ServeDNS(context.TODO(), &test.ResponseWriter{}, req)
time.Sleep(1 * time.Second)
i1 := atomic.LoadUint32(&i)
if i1 != expected {
t.Errorf("Expected number of health checks to be %d, got %d", expected, i1)
}
}

5
forward/log_test.go Normal file
View File

@ -0,0 +1,5 @@
package forward
import clog "github.com/coredns/coredns/plugin/pkg/log"
func init() { clog.Discard() }

54
forward/metrics.go Normal file
View File

@ -0,0 +1,54 @@
package forward
import (
"github.com/coredns/coredns/plugin"
"github.com/prometheus/client_golang/prometheus"
)
// Variables declared for monitoring.
var (
RequestCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "forward",
Name: "request_count_total",
Help: "Counter of requests made per upstream.",
}, []string{"to"})
RcodeCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "forward",
Name: "response_rcode_count_total",
Help: "Counter of requests made per upstream.",
}, []string{"rcode", "to"})
RequestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: plugin.Namespace,
Subsystem: "forward",
Name: "request_duration_seconds",
Buckets: plugin.TimeBuckets,
Help: "Histogram of the time each request took.",
}, []string{"to"})
HealthcheckFailureCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "forward",
Name: "healthcheck_failure_count_total",
Help: "Counter of the number of failed healthchecks.",
}, []string{"to"})
HealthcheckBrokenCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "forward",
Name: "healthcheck_broken_count_total",
Help: "Counter of the number of complete failures of the healthchecks.",
})
SocketGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: plugin.Namespace,
Subsystem: "forward",
Name: "sockets_open",
Help: "Gauge of open sockets per upstream.",
}, []string{"to"})
MaxConcurrentRejectCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "forward",
Name: "max_concurrent_reject_count_total",
Help: "Counter of the number of queries rejected because the concurrent queries were at maximum.",
})
)

158
forward/persistent.go Normal file
View File

@ -0,0 +1,158 @@
package forward
import (
"crypto/tls"
"sort"
"time"
"github.com/miekg/dns"
)
// a persistConn hold the dns.Conn and the last used time.
type persistConn struct {
c *dns.Conn
used time.Time
}
// Transport hold the persistent cache.
type Transport struct {
avgDialTime int64 // kind of average time of dial time
conns [typeTotalCount][]*persistConn // Buckets for udp, tcp and tcp-tls.
expire time.Duration // After this duration a connection is expired.
addr string
tlsConfig *tls.Config
dial chan string
yield chan *persistConn
ret chan *persistConn
stop chan bool
}
func newTransport(addr string) *Transport {
t := &Transport{
avgDialTime: int64(maxDialTimeout / 2),
conns: [typeTotalCount][]*persistConn{},
expire: defaultExpire,
addr: addr,
dial: make(chan string),
yield: make(chan *persistConn),
ret: make(chan *persistConn),
stop: make(chan bool),
}
return t
}
// connManagers manages the persistent connection cache for UDP and TCP.
func (t *Transport) connManager() {
ticker := time.NewTicker(t.expire)
Wait:
for {
select {
case proto := <-t.dial:
transtype := stringToTransportType(proto)
// take the last used conn - complexity O(1)
if stack := t.conns[transtype]; len(stack) > 0 {
pc := stack[len(stack)-1]
if time.Since(pc.used) < t.expire {
// Found one, remove from pool and return this conn.
t.conns[transtype] = stack[:len(stack)-1]
t.ret <- pc
continue Wait
}
// clear entire cache if the last conn is expired
t.conns[transtype] = nil
// now, the connections being passed to closeConns() are not reachable from
// transport methods anymore. So, it's safe to close them in a separate goroutine
go closeConns(stack)
}
t.ret <- nil
case pc := <-t.yield:
transtype := t.transportTypeFromConn(pc)
t.conns[transtype] = append(t.conns[transtype], pc)
case <-ticker.C:
t.cleanup(false)
case <-t.stop:
t.cleanup(true)
close(t.ret)
return
}
}
}
// closeConns closes connections.
func closeConns(conns []*persistConn) {
for _, pc := range conns {
pc.c.Close()
}
}
// cleanup removes connections from cache.
func (t *Transport) cleanup(all bool) {
staleTime := time.Now().Add(-t.expire)
for transtype, stack := range t.conns {
if len(stack) == 0 {
continue
}
if all {
t.conns[transtype] = nil
// now, the connections being passed to closeConns() are not reachable from
// transport methods anymore. So, it's safe to close them in a separate goroutine
go closeConns(stack)
continue
}
if stack[0].used.After(staleTime) {
continue
}
// connections in stack are sorted by "used"
good := sort.Search(len(stack), func(i int) bool {
return stack[i].used.After(staleTime)
})
t.conns[transtype] = stack[good:]
// now, the connections being passed to closeConns() are not reachable from
// transport methods anymore. So, it's safe to close them in a separate goroutine
go closeConns(stack[:good])
}
}
// It is hard to pin a value to this, the import thing is to no block forever, losing at cached connection is not terrible.
const yieldTimeout = 25 * time.Millisecond
// Yield return the connection to transport for reuse.
func (t *Transport) Yield(pc *persistConn) {
pc.used = time.Now() // update used time
// Make this non-blocking, because in the case of a very busy forwarder we will *block* on this yield. This
// blocks the outer go-routine and stuff will just pile up. We timeout when the send fails to as returning
// these connection is an optimization anyway.
select {
case t.yield <- pc:
return
case <-time.After(yieldTimeout):
return
}
}
// Start starts the transport's connection manager.
func (t *Transport) Start() { go t.connManager() }
// Stop stops the transport's connection manager.
func (t *Transport) Stop() { close(t.stop) }
// SetExpire sets the connection expire time in transport.
func (t *Transport) SetExpire(expire time.Duration) { t.expire = expire }
// SetTLSConfig sets the TLS config in transport.
func (t *Transport) SetTLSConfig(cfg *tls.Config) { t.tlsConfig = cfg }
const (
defaultExpire = 10 * time.Second
minDialTimeout = 1 * time.Second
maxDialTimeout = 30 * time.Second
// Some resolves might take quite a while, usually (cached) responses are fast. Set to 2s to give us some time to retry a different upstream.
readTimeout = 2 * time.Second
)

109
forward/persistent_test.go Normal file
View File

@ -0,0 +1,109 @@
package forward
import (
"testing"
"time"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/miekg/dns"
)
func TestCached(t *testing.T) {
s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
ret := new(dns.Msg)
ret.SetReply(r)
w.WriteMsg(ret)
})
defer s.Close()
tr := newTransport(s.Addr)
tr.Start()
defer tr.Stop()
c1, cache1, _ := tr.Dial("udp")
c2, cache2, _ := tr.Dial("udp")
if cache1 || cache2 {
t.Errorf("Expected non-cached connection")
}
tr.Yield(c1)
tr.Yield(c2)
c3, cached3, _ := tr.Dial("udp")
if !cached3 {
t.Error("Expected cached connection (c3)")
}
if c2 != c3 {
t.Error("Expected c2 == c3")
}
tr.Yield(c3)
// dial another protocol
c4, cached4, _ := tr.Dial("tcp")
if cached4 {
t.Errorf("Expected non-cached connection (c4)")
}
tr.Yield(c4)
}
func TestCleanupByTimer(t *testing.T) {
s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
ret := new(dns.Msg)
ret.SetReply(r)
w.WriteMsg(ret)
})
defer s.Close()
tr := newTransport(s.Addr)
tr.SetExpire(100 * time.Millisecond)
tr.Start()
defer tr.Stop()
c1, _, _ := tr.Dial("udp")
c2, _, _ := tr.Dial("udp")
tr.Yield(c1)
time.Sleep(10 * time.Millisecond)
tr.Yield(c2)
time.Sleep(120 * time.Millisecond)
c3, cached, _ := tr.Dial("udp")
if cached {
t.Error("Expected non-cached connection (c3)")
}
tr.Yield(c3)
time.Sleep(120 * time.Millisecond)
c4, cached, _ := tr.Dial("udp")
if cached {
t.Error("Expected non-cached connection (c4)")
}
tr.Yield(c4)
}
func TestCleanupAll(t *testing.T) {
s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
ret := new(dns.Msg)
ret.SetReply(r)
w.WriteMsg(ret)
})
defer s.Close()
tr := newTransport(s.Addr)
c1, _ := dns.DialTimeout("udp", tr.addr, maxDialTimeout)
c2, _ := dns.DialTimeout("udp", tr.addr, maxDialTimeout)
c3, _ := dns.DialTimeout("udp", tr.addr, maxDialTimeout)
tr.conns[typeUdp] = []*persistConn{{c1, time.Now()}, {c2, time.Now()}, {c3, time.Now()}}
if len(tr.conns[typeUdp]) != 3 {
t.Error("Expected 3 connections")
}
tr.cleanup(true)
if len(tr.conns[typeUdp]) > 0 {
t.Error("Expected no cached connections")
}
}

81
forward/proxy.go Normal file
View File

@ -0,0 +1,81 @@
package forward
import (
"crypto/tls"
"runtime"
"sync/atomic"
"time"
"github.com/coredns/coredns/plugin/pkg/up"
)
// Proxy defines an upstream host.
type Proxy struct {
fails uint32
addr string
transport *Transport
// health checking
probe *up.Probe
health HealthChecker
}
// NewProxy returns a new proxy.
func NewProxy(addr, trans string) *Proxy {
p := &Proxy{
addr: addr,
fails: 0,
probe: up.New(),
transport: newTransport(addr),
}
p.health = NewHealthChecker(trans, true)
runtime.SetFinalizer(p, (*Proxy).finalizer)
return p
}
// SetTLSConfig sets the TLS config in the lower p.transport and in the healthchecking client.
func (p *Proxy) SetTLSConfig(cfg *tls.Config) {
p.transport.SetTLSConfig(cfg)
p.health.SetTLSConfig(cfg)
}
// SetExpire sets the expire duration in the lower p.transport.
func (p *Proxy) SetExpire(expire time.Duration) { p.transport.SetExpire(expire) }
// Healthcheck kicks of a round of health checks for this proxy.
func (p *Proxy) Healthcheck() {
if p.health == nil {
log.Warning("No healthchecker")
return
}
p.probe.Do(func() error {
return p.health.Check(p)
})
}
// Down returns true if this proxy is down, i.e. has *more* fails than maxfails.
func (p *Proxy) Down(maxfails uint32) bool {
if maxfails == 0 {
return false
}
fails := atomic.LoadUint32(&p.fails)
return fails > maxfails
}
// close stops the health checking goroutine.
func (p *Proxy) stop() { p.probe.Stop() }
func (p *Proxy) finalizer() { p.transport.Stop() }
// start starts the proxy's healthchecking.
func (p *Proxy) start(duration time.Duration) {
p.probe.Start(duration)
p.transport.Start()
}
const (
maxTimeout = 2 * time.Second
hcInterval = 500 * time.Millisecond
)

123
forward/proxy_test.go Normal file
View File

@ -0,0 +1,123 @@
package forward
import (
"context"
"testing"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/pkg/transport"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/caddyserver/caddy"
"github.com/miekg/dns"
)
func TestProxyClose(t *testing.T) {
s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
ret := new(dns.Msg)
ret.SetReply(r)
w.WriteMsg(ret)
})
defer s.Close()
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeA)
state := request.Request{W: &test.ResponseWriter{}, Req: req}
ctx := context.TODO()
for i := 0; i < 100; i++ {
p := NewProxy(s.Addr, transport.DNS)
p.start(hcInterval)
go func() { p.Connect(ctx, state, options{}) }()
go func() { p.Connect(ctx, state, options{forceTCP: true}) }()
go func() { p.Connect(ctx, state, options{}) }()
go func() { p.Connect(ctx, state, options{forceTCP: true}) }()
p.stop()
}
}
func TestProxy(t *testing.T) {
s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
ret := new(dns.Msg)
ret.SetReply(r)
ret.Answer = append(ret.Answer, test.A("example.org. IN A 127.0.0.1"))
w.WriteMsg(ret)
})
defer s.Close()
c := caddy.NewTestController("dns", "forward . "+s.Addr)
f, err := parseForward(c)
if err != nil {
t.Errorf("Failed to create forwarder: %s", err)
}
f.OnStartup()
defer f.OnShutdown()
m := new(dns.Msg)
m.SetQuestion("example.org.", dns.TypeA)
rec := dnstest.NewRecorder(&test.ResponseWriter{})
if _, err := f.ServeDNS(context.TODO(), rec, m); err != nil {
t.Fatal("Expected to receive reply, but didn't")
}
if x := rec.Msg.Answer[0].Header().Name; x != "example.org." {
t.Errorf("Expected %s, got %s", "example.org.", x)
}
}
func TestProxyTLSFail(t *testing.T) {
// This is an udp/tcp test server, so we shouldn't reach it with TLS.
s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
ret := new(dns.Msg)
ret.SetReply(r)
ret.Answer = append(ret.Answer, test.A("example.org. IN A 127.0.0.1"))
w.WriteMsg(ret)
})
defer s.Close()
c := caddy.NewTestController("dns", "forward . tls://"+s.Addr)
f, err := parseForward(c)
if err != nil {
t.Errorf("Failed to create forwarder: %s", err)
}
f.OnStartup()
defer f.OnShutdown()
m := new(dns.Msg)
m.SetQuestion("example.org.", dns.TypeA)
rec := dnstest.NewRecorder(&test.ResponseWriter{})
if _, err := f.ServeDNS(context.TODO(), rec, m); err == nil {
t.Fatal("Expected *not* to receive reply, but got one")
}
}
func TestProtocolSelection(t *testing.T) {
p := NewProxy("bad_address", transport.DNS)
stateUDP := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)}
stateTCP := request.Request{W: &test.ResponseWriter{TCP: true}, Req: new(dns.Msg)}
ctx := context.TODO()
go func() {
p.Connect(ctx, stateUDP, options{})
p.Connect(ctx, stateUDP, options{forceTCP: true})
p.Connect(ctx, stateUDP, options{preferUDP: true})
p.Connect(ctx, stateUDP, options{preferUDP: true, forceTCP: true})
p.Connect(ctx, stateTCP, options{})
p.Connect(ctx, stateTCP, options{forceTCP: true})
p.Connect(ctx, stateTCP, options{preferUDP: true})
p.Connect(ctx, stateTCP, options{preferUDP: true, forceTCP: true})
}()
for i, exp := range []string{"udp", "tcp", "udp", "tcp", "tcp", "tcp", "udp", "tcp"} {
proto := <-p.transport.dial
p.transport.ret <- nil
if proto != exp {
t.Errorf("Unexpected protocol in case %d, expected %q, actual %q", i, exp, proto)
}
}
}

253
forward/setup.go Normal file
View File

@ -0,0 +1,253 @@
package forward
import (
"errors"
"fmt"
"strconv"
"time"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metrics"
"github.com/coredns/coredns/plugin/pkg/parse"
"github.com/coredns/coredns/plugin/pkg/policy"
pkgtls "github.com/coredns/coredns/plugin/pkg/tls"
"github.com/coredns/coredns/plugin/pkg/transport"
"github.com/caddyserver/caddy"
)
func init() { plugin.Register("forward", setup) }
func setup(c *caddy.Controller) error {
f, err := parseForward(c)
if err != nil {
return plugin.Error("forward", err)
}
if f.Len() > max {
return plugin.Error("forward", fmt.Errorf("more than %d TOs configured: %d", max, f.Len()))
}
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
f.Next = next
return f
})
c.OnStartup(func() error {
metrics.MustRegister(c, RequestCount, RcodeCount, RequestDuration, HealthcheckFailureCount, SocketGauge, MaxConcurrentRejectCount)
return f.OnStartup()
})
c.OnShutdown(func() error {
return f.OnShutdown()
})
return nil
}
// OnStartup starts a goroutines for all proxies.
func (f *Forward) OnStartup() (err error) {
for _, p := range f.proxies {
p.start(f.hcInterval)
}
return nil
}
// OnShutdown stops all configured proxies.
func (f *Forward) OnShutdown() error {
for _, p := range f.proxies {
p.stop()
}
return nil
}
func parseForward(c *caddy.Controller) (*Forward, error) {
var (
f *Forward
err error
i int
)
for c.Next() {
if i > 0 {
return nil, plugin.ErrOnce
}
i++
f, err = parseStanza(c)
if err != nil {
return nil, err
}
}
return f, nil
}
// Exposed to our "alternate" plugin
func ParseForwardStanza(c *caddy.Controller) (*Forward, error) {
return parseStanza(c)
}
func parseStanza(c *caddy.Controller) (*Forward, error) {
f := New()
if !c.Args(&f.from) {
return f, c.ArgErr()
}
f.from = plugin.Host(f.from).Normalize()
to := c.RemainingArgs()
if len(to) == 0 {
return f, c.ArgErr()
}
toHosts, err := parse.HostPortOrFile(to...)
if err != nil {
return f, err
}
transports := make([]string, len(toHosts))
for i, host := range toHosts {
trans, h := parse.Transport(host)
p := NewProxy(h, trans)
f.proxies = append(f.proxies, p)
transports[i] = trans
}
for c.NextBlock() {
if err := parseBlock(c, f); err != nil {
return f, err
}
}
if f.tlsServerName != "" {
f.tlsConfig.ServerName = f.tlsServerName
}
for i := range f.proxies {
// Only set this for proxies that need it.
if transports[i] == transport.TLS {
f.proxies[i].SetTLSConfig(f.tlsConfig)
}
f.proxies[i].SetExpire(f.expire)
f.proxies[i].health.SetRecursionDesired(f.opts.hcRecursionDesired)
}
return f, nil
}
func parseBlock(c *caddy.Controller, f *Forward) error {
switch c.Val() {
case "except":
ignore := c.RemainingArgs()
if len(ignore) == 0 {
return c.ArgErr()
}
for i := 0; i < len(ignore); i++ {
ignore[i] = plugin.Host(ignore[i]).Normalize()
}
f.ignored = ignore
case "max_fails":
if !c.NextArg() {
return c.ArgErr()
}
n, err := strconv.Atoi(c.Val())
if err != nil {
return err
}
if n < 0 {
return fmt.Errorf("max_fails can't be negative: %d", n)
}
f.maxfails = uint32(n)
case "health_check":
if !c.NextArg() {
return c.ArgErr()
}
dur, err := time.ParseDuration(c.Val())
if err != nil {
return err
}
if dur < 0 {
return fmt.Errorf("health_check can't be negative: %d", dur)
}
f.hcInterval = dur
for c.NextArg() {
switch hcOpts := c.Val(); hcOpts {
case "no_rec":
f.opts.hcRecursionDesired = false
default:
return fmt.Errorf("health_check: unknown option %s", hcOpts)
}
}
case "force_tcp":
if c.NextArg() {
return c.ArgErr()
}
f.opts.forceTCP = true
case "prefer_udp":
if c.NextArg() {
return c.ArgErr()
}
f.opts.preferUDP = true
case "tls":
args := c.RemainingArgs()
if len(args) > 3 {
return c.ArgErr()
}
tlsConfig, err := pkgtls.NewTLSConfigFromArgs(args...)
if err != nil {
return err
}
f.tlsConfig = tlsConfig
case "tls_servername":
if !c.NextArg() {
return c.ArgErr()
}
f.tlsServerName = c.Val()
case "expire":
if !c.NextArg() {
return c.ArgErr()
}
dur, err := time.ParseDuration(c.Val())
if err != nil {
return err
}
if dur < 0 {
return fmt.Errorf("expire can't be negative: %s", dur)
}
f.expire = dur
case "policy":
if !c.NextArg() {
return c.ArgErr()
}
switch x := c.Val(); x {
case "random":
f.p = &policy.Random{}
case "round_robin":
f.p = &policy.RoundRobin{}
case "sequential":
f.p = &policy.Sequential{}
default:
return c.Errf("unknown policy '%s'", x)
}
case "max_concurrent":
if !c.NextArg() {
return c.ArgErr()
}
n, err := strconv.Atoi(c.Val())
if err != nil {
return err
}
if n < 0 {
return fmt.Errorf("max_concurrent can't be negative: %d", n)
}
f.ErrLimitExceeded = errors.New("concurrent queries exceeded maximum " + c.Val())
f.maxConcurrent = int64(n)
default:
return c.Errf("unknown property '%s'", c.Val())
}
return nil
}
const max = 15 // Maximum number of upstreams.

View File

@ -0,0 +1,47 @@
package forward
import (
"strings"
"testing"
"github.com/caddyserver/caddy"
)
func TestSetupPolicy(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expectedPolicy string
expectedErr string
}{
// positive
{"forward . 127.0.0.1 {\npolicy random\n}\n", false, "random", ""},
{"forward . 127.0.0.1 {\npolicy round_robin\n}\n", false, "round_robin", ""},
{"forward . 127.0.0.1 {\npolicy sequential\n}\n", false, "sequential", ""},
// negative
{"forward . 127.0.0.1 {\npolicy random2\n}\n", true, "random", "unknown policy"},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
f, err := parseForward(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input)
}
if err != nil {
if !test.shouldErr {
t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input)
}
}
if !test.shouldErr && f.p.String() != test.expectedPolicy {
t.Errorf("Test %d: expected: %s, got: %s", i, test.expectedPolicy, f.p.String())
}
}
}

257
forward/setup_test.go Normal file
View File

@ -0,0 +1,257 @@
package forward
import (
"io/ioutil"
"os"
"reflect"
"strings"
"testing"
"github.com/caddyserver/caddy"
)
func TestSetup(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expectedFrom string
expectedIgnored []string
expectedFails uint32
expectedOpts options
expectedErr string
}{
// positive
{"forward . 127.0.0.1", false, ".", nil, 2, options{hcRecursionDesired: true}, ""},
{"forward . 127.0.0.1 {\nexcept miek.nl\n}\n", false, ".", nil, 2, options{hcRecursionDesired: true}, ""},
{"forward . 127.0.0.1 {\nmax_fails 3\n}\n", false, ".", nil, 3, options{hcRecursionDesired: true}, ""},
{"forward . 127.0.0.1 {\nforce_tcp\n}\n", false, ".", nil, 2, options{forceTCP: true, hcRecursionDesired: true}, ""},
{"forward . 127.0.0.1 {\nprefer_udp\n}\n", false, ".", nil, 2, options{preferUDP: true, hcRecursionDesired: true}, ""},
{"forward . 127.0.0.1 {\nforce_tcp\nprefer_udp\n}\n", false, ".", nil, 2, options{preferUDP: true, forceTCP: true, hcRecursionDesired: true}, ""},
{"forward . 127.0.0.1:53", false, ".", nil, 2, options{hcRecursionDesired: true}, ""},
{"forward . 127.0.0.1:8080", false, ".", nil, 2, options{hcRecursionDesired: true}, ""},
{"forward . [::1]:53", false, ".", nil, 2, options{hcRecursionDesired: true}, ""},
{"forward . [2003::1]:53", false, ".", nil, 2, options{hcRecursionDesired: true}, ""},
{"forward . 127.0.0.1 \n", false, ".", nil, 2, options{hcRecursionDesired: true}, ""},
// negative
{"forward . a27.0.0.1", true, "", nil, 0, options{hcRecursionDesired: true}, "not an IP"},
{"forward . 127.0.0.1 {\nblaatl\n}\n", true, "", nil, 0, options{hcRecursionDesired: true}, "unknown property"},
{`forward . ::1
forward com ::2`, true, "", nil, 0, options{hcRecursionDesired: true}, "plugin"},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
f, err := parseForward(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input)
}
if err != nil {
if !test.shouldErr {
t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input)
}
}
if !test.shouldErr && f.from != test.expectedFrom {
t.Errorf("Test %d: expected: %s, got: %s", i, test.expectedFrom, f.from)
}
if !test.shouldErr && test.expectedIgnored != nil {
if !reflect.DeepEqual(f.ignored, test.expectedIgnored) {
t.Errorf("Test %d: expected: %q, actual: %q", i, test.expectedIgnored, f.ignored)
}
}
if !test.shouldErr && f.maxfails != test.expectedFails {
t.Errorf("Test %d: expected: %d, got: %d", i, test.expectedFails, f.maxfails)
}
if !test.shouldErr && f.opts != test.expectedOpts {
t.Errorf("Test %d: expected: %v, got: %v", i, test.expectedOpts, f.opts)
}
}
}
func TestSetupTLS(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expectedServerName string
expectedErr string
}{
// positive
{`forward . tls://127.0.0.1 {
tls_servername dns
}`, false, "dns", ""},
{`forward . 127.0.0.1 {
tls_servername dns
}`, false, "", ""},
{`forward . 127.0.0.1 {
tls
}`, false, "", ""},
{`forward . tls://127.0.0.1`, false, "", ""},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
f, err := parseForward(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input)
}
if err != nil {
if !test.shouldErr {
t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input)
}
}
if !test.shouldErr && test.expectedServerName != "" && test.expectedServerName != f.tlsConfig.ServerName {
t.Errorf("Test %d: expected: %q, actual: %q", i, test.expectedServerName, f.tlsConfig.ServerName)
}
if !test.shouldErr && test.expectedServerName != "" && test.expectedServerName != f.proxies[0].health.(*dnsHc).c.TLSConfig.ServerName {
t.Errorf("Test %d: expected: %q, actual: %q", i, test.expectedServerName, f.proxies[0].health.(*dnsHc).c.TLSConfig.ServerName)
}
}
}
func TestSetupResolvconf(t *testing.T) {
const resolv = "resolv.conf"
if err := ioutil.WriteFile(resolv,
[]byte(`nameserver 10.10.255.252
nameserver 10.10.255.253`), 0666); err != nil {
t.Fatalf("Failed to write resolv.conf file: %s", err)
}
defer os.Remove(resolv)
tests := []struct {
input string
shouldErr bool
expectedErr string
expectedNames []string
}{
// pass
{`forward . ` + resolv, false, "", []string{"10.10.255.252:53", "10.10.255.253:53"}},
// fail
{`forward . /dev/null`, true, "no nameservers", nil},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
f, err := parseForward(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input)
continue
}
if err != nil {
if !test.shouldErr {
t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input)
}
}
if !test.shouldErr {
for j, n := range test.expectedNames {
addr := f.proxies[j].addr
if n != addr {
t.Errorf("Test %d, expected %q, got %q", j, n, addr)
}
}
}
if test.shouldErr {
continue
}
for _, p := range f.proxies {
p.health.Check(p) // this should almost always err, we don't care it shouldn't crash
}
}
}
func TestSetupMaxConcurrent(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expectedVal int64
expectedErr string
}{
// positive
{"forward . 127.0.0.1 {\nmax_concurrent 1000\n}\n", false, 1000, ""},
// negative
{"forward . 127.0.0.1 {\nmax_concurrent many\n}\n", true, 0, "invalid"},
{"forward . 127.0.0.1 {\nmax_concurrent -4\n}\n", true, 0, "negative"},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
f, err := parseForward(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input)
}
if err != nil {
if !test.shouldErr {
t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input)
}
}
if !test.shouldErr && f.maxConcurrent != test.expectedVal {
t.Errorf("Test %d: expected: %d, got: %d", i, test.expectedVal, f.maxConcurrent)
}
}
}
func TestSetupHealthCheck(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expectedVal bool
expectedErr string
}{
// positive
{"forward . 127.0.0.1\n", false, true, ""},
{"forward . 127.0.0.1 {\nhealth_check 0.5s\n}\n", false, true, ""},
{"forward . 127.0.0.1 {\nhealth_check 0.5s no_rec\n}\n", false, false, ""},
// negative
{"forward . 127.0.0.1 {\nhealth_check no_rec\n}\n", true, true, "time: invalid duration"},
{"forward . 127.0.0.1 {\nhealth_check 0.5s rec\n}\n", true, true, "health_check: unknown option rec"},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
f, err := parseForward(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input)
}
if err != nil {
if !test.shouldErr {
t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input)
}
}
if !test.shouldErr && (f.opts.hcRecursionDesired != test.expectedVal || f.proxies[0].health.GetRecursionDesired() != test.expectedVal) {
t.Errorf("Test %d: expected: %t, got: %d", i, test.expectedVal, f.maxConcurrent)
}
}
}

37
forward/type.go Normal file
View File

@ -0,0 +1,37 @@
package forward
import "net"
type transportType int
const (
typeUdp transportType = iota
typeTcp
typeTls
typeTotalCount // keep this last
)
func stringToTransportType(s string) transportType {
switch s {
case "udp":
return typeUdp
case "tcp":
return typeTcp
case "tcp-tls":
return typeTls
}
return typeUdp
}
func (t *Transport) transportTypeFromConn(pc *persistConn) transportType {
if _, ok := pc.c.Conn.(*net.UDPConn); ok {
return typeUdp
}
if t.tlsConfig == nil {
return typeTcp
}
return typeTls
}

22
go.mod Normal file
View File

@ -0,0 +1,22 @@
module github.com/AdguardTeam/AdGuardDNS
go 1.13
require (
github.com/AdguardTeam/urlfilter v0.10.0
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833
github.com/caddyserver/caddy v1.0.5
github.com/coredns/coredns v1.6.9
github.com/dnstap/golang-dnstap v0.0.0-20170829151710-2cf77a2b5e11
github.com/joomcode/errorx v1.0.1
github.com/miekg/dns v1.1.29
github.com/opentracing/opentracing-go v1.1.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.5.1
github.com/stretchr/testify v1.5.1
go.etcd.io/bbolt v1.3.4
go.uber.org/atomic v1.6.0
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
)

713
go.sum Normal file
View File

@ -0,0 +1,713 @@
cloud.google.com/go v0.26.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.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.41.0 h1:NFvqUTDnSNYPX5oReekmB+D+90jrJIcVImxQ3qrBVgM=
cloud.google.com/go v0.41.0/go.mod h1:OauMR7DV8fzvZIl2qg6rkaIhD/vmgk4iwEw/h6ercmg=
contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA=
github.com/AdguardTeam/golibs v0.4.0 h1:4VX6LoOqFe9p9Gf55BeD8BvJD6M6RDYmgEiHrENE9KU=
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
github.com/AdguardTeam/urlfilter v0.10.0 h1:/YZ4w/UF3KDkL4/QLrQtqalvwBfHHGgrMhk+u3Xm8Mo=
github.com/AdguardTeam/urlfilter v0.10.0/go.mod h1:aMuejlNxpWppOVjiEV87X6z0eMf7wsXHTAIWQuylfZY=
github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go v32.6.0+incompatible h1:PgaVceWF5idtJajyt1rzq1cep6eRPJ8+8hs4GnNzTo0=
github.com/Azure/azure-sdk-for-go v32.6.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg=
github.com/Azure/go-autorest/autorest v0.5.0/go.mod h1:9HLKlQjVBH6U3oDfsXOeVc56THsLPw1L03yban4xThw=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0=
github.com/Azure/go-autorest/autorest v0.10.0 h1:mvdtztBqcL8se7MdrUweNieTNi4kfNG6GOJuurQJpuY=
github.com/Azure/go-autorest/autorest v0.10.0/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
github.com/Azure/go-autorest/autorest/adal v0.1.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E=
github.com/Azure/go-autorest/autorest/adal v0.2.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=
github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
github.com/Azure/go-autorest/autorest/adal v0.8.2 h1:O1X4oexUxnZCaEUGsvMnr8ZGj8HI37tNezwY4npRqA0=
github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
github.com/Azure/go-autorest/autorest/azure/auth v0.1.0/go.mod h1:Gf7/i2FUpyb/sGBLIFxTBzrNzBo7aPXXE3ZVeDRwdpM=
github.com/Azure/go-autorest/autorest/azure/auth v0.4.2 h1:iM6UAvjR97ZIeR93qTcwpKNMpV+/FTWjwEbuPD495Tk=
github.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM=
github.com/Azure/go-autorest/autorest/azure/cli v0.1.0/go.mod h1:Dk8CUAt/b/PzkfeRsWzVG9Yj3ps8mS8ECztu43rdU8U=
github.com/Azure/go-autorest/autorest/azure/cli v0.3.1 h1:LXl088ZQlP0SBppGFsRZonW6hSvwgL5gRByMbvUbx8U=
github.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
github.com/Azure/go-autorest/autorest/date v0.2.0 h1:yW+Zlqf26583pE43KhfnhFcdmSWlm5Ew6bxipnr/tbM=
github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
github.com/Azure/go-autorest/autorest/to v0.2.0 h1:nQOZzFCudTh+TvquAtCRjM01VEYx85e9qbwt5ncW4L8=
github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc=
github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8=
github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88=
github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.3.1+incompatible h1:NT/ghvYzqIzTJGiqvc3n4t9cZy8waO+I2O3I8Cok6/k=
github.com/DataDog/datadog-go v3.3.1+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/zstd v1.3.5 h1:DtpNbljikUepEPD16hD4LvIcmhnhdLTiW/5pHgbmp14=
github.com/DataDog/zstd v1.3.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/sarama v1.21.0 h1:0GKs+e8mn1RRUzfg9oUXv3v7ZieQLmOZF/bfnmmGhM8=
github.com/Shopify/sarama v1.21.0/go.mod h1:yuqtN/pe8cXRWG5zPaO7hCfNJp5MwmkoJEoLjkm5tCQ=
github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.0/go.mod h1:zpDJeKyp9ScW4NNrbdr+Eyxvry3ilGPewKoXw3XGN1k=
github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75/go.mod h1:uAXEEpARkRhCZfEvy/y0Jcc888f9tHCc1W7/UeEtreE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190808125512-07798873deee/go.mod h1:myCDvQSzCW+wB1WAlocEru4wMGJxy+vlxHdhegi1CDQ=
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/apache/thrift v0.12.0 h1:pODnxUFNcjP9UTLZGTdeh+j16A8lJbRvD3rOtrk/7bs=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/aws/aws-sdk-go v1.23.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.29.29 h1:4TdSYzXL8bHKu80tzPjO4c0ALw4Fd8qZGqf1aozUcBU=
github.com/aws/aws-sdk-go v1.29.29/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6 h1:KXlsf+qt/X5ttPGEjR0tPH1xaWWoKBEg9Q1THAj2h3I=
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833 h1:yCfXxYaelOyqnia8F/Yng47qhmfC9nKTRIbYRrRueq4=
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833/go.mod h1:8c4/i2VlovMO2gBnHGQPN5EJw+H0lx1u/5p+cgsXtCk=
github.com/caddyserver/caddy v1.0.5 h1:5B1Hs0UF2x2tggr2X9jL2qOZtDXbIWQb9YLbmlxHSuM=
github.com/caddyserver/caddy v1.0.5/go.mod h1:AnFHB+/MrgRC+mJAvuAgQ38ePzw+wKeW0wzENpdQQKY=
github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
github.com/cenkalti/backoff/v4 v4.0.0 h1:6VeaLF9aI+MAUQ95106HwWzYZgJJpZ4stumjj6RFYAU=
github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg=
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go v0.10.2/go.mod h1:qhVI5MKwBGhdNU89ZRz2plgYutcJ5PCekLxXn56w6SY=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/coredns/coredns v1.6.9 h1:i3c8XAY6f9jzxtUUx07WH2/ft0RXOrP9zIRd4YODGOQ=
github.com/coredns/coredns v1.6.9/go.mod h1:KTV/oAc4nYoKBdQ6aOFI+m4tnZeh4iySMvJconHhF/c=
github.com/coredns/federation v0.0.0-20190818181423-e032b096babe h1:ND08lR/TclI9W4dScCwdRESOacCCdF3FkuB5pBIOv1U=
github.com/coredns/federation v0.0.0-20190818181423-e032b096babe/go.mod h1:MoqTEFX8GlnKkyq8eBCF94VzkNAOgjdlCJ+Pz/oCLPk=
github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.0.0 h1:XJIw/+VlJ+87J+doOxznsAWIdmWuViOVhkQamW5YV28=
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
github.com/coreos/license-bill-of-materials v0.0.0-20190913234955-13baff47494e/go.mod h1:4xMOusJ7xxc84WclVxKT8+lNfGYDwojOUC2OQNCwcj4=
github.com/cpu/goacmedns v0.0.1/go.mod h1:sesf/pNnCYwUevQEQfEwY0Y3DydlQWSGZbaMElOWxok=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decker502/dnspod-go v0.2.0/go.mod h1:qsurYu1FgxcDwfSwXJdLt4kRsBLZeosEb9uq4Sy+08g=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/dnaeon/go-vcr v0.0.0-20180814043457-aafff18a5cc2/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/dnsimple/dnsimple-go v0.30.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg=
github.com/dnstap/golang-dnstap v0.0.0-20170829151710-2cf77a2b5e11 h1:m8nX8hsUghn853BJ5qB0lX+VvS6LTJPksWyILFZRYN4=
github.com/dnstap/golang-dnstap v0.0.0-20170829151710-2cf77a2b5e11/go.mod h1:s1PfVYYVmTMgCSPtho4LKBDecEHJWtiVDPNv78Z985U=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/exoscale/egoscale v0.18.1/go.mod h1:Z7OOdzzTOz1Q1PjQXumlz9Wn/CddH0zSYdCF3rnBKXE=
github.com/farsightsec/golang-framestream v0.0.0-20181102145529-8a0cb8ba8710 h1:QdyRyGZWLEvJG5Kw3VcVJvhXJ5tZ1MkRgqpJOEZSySM=
github.com/farsightsec/golang-framestream v0.0.0-20181102145529-8a0cb8ba8710/go.mod h1:eNde4IQyEiA5br02AouhEHCu3p3UzrCdFR4LuQHklMI=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
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/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-acme/lego/v3 v3.1.0/go.mod h1:074uqt+JS6plx+c9Xaiz6+L+GBb+7itGtzfcDM2AhEE=
github.com/go-acme/lego/v3 v3.2.0/go.mod h1:074uqt+JS6plx+c9Xaiz6+L+GBb+7itGtzfcDM2AhEE=
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-ini/ini v1.44.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I=
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
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-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
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/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
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/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g=
github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
github.com/gophercloud/gophercloud v0.3.0 h1:6sjpKIpVwRIIwmcEGp+WwNovNsem+c+2vm6oxshRpL8=
github.com/gophercloud/gophercloud v0.3.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
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/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk=
github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/infobloxopen/go-trees v0.0.0-20190313150506-2af4e13f9062 h1:d3VSuNcgTCn21dNMm8g412Fck/XWFmMj4nJhhHT7ZZ0=
github.com/infobloxopen/go-trees v0.0.0-20190313150506-2af4e13f9062/go.mod h1:PcNJqIlcX/dj3DTG/+QQnRvSgTMG6CLpRMjWcv4+J6w=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/joomcode/errorx v1.0.1 h1:CalpDWz14ZHd68fIqluJasJosAewpz2TFaJALrUxjrk=
github.com/joomcode/errorx v1.0.1/go.mod h1:kgco15ekB6cs+4Xjzo7SPeXzx38PbJzBwbnu9qfVNHQ=
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/kolo/xmlrpc v0.0.0-20190717152603-07c4ee3fd181/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
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/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA=
github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w=
github.com/linode/linodego v0.10.0/go.mod h1:cziNP7pbvE3mXIPneHj0oRY8L1WtGEIKlZ8LANE4eXA=
github.com/liquidweb/liquidweb-go v1.6.0/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVLEIG/i5J9cyixzQ=
github.com/lucas-clemente/quic-go v0.13.1/go.mod h1:Vn3/Fb0/77b02SGhQk36KzOUmXgVpFfizUfW5WMaqyU=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/marten-seemann/chacha20 v0.2.0/go.mod h1:HSdjFau7GzYRj+ahFNwsO3ouVJr1HFkWoEwNDb4TMtE=
github.com/marten-seemann/qpack v0.1.0/go.mod h1:LFt1NU/Ptjip0C2CPkhimBz5CGE3WGDAUWqna+CNTrI=
github.com/marten-seemann/qtls v0.4.1/go.mod h1:pxVXcHHw1pNIt8Qo0pwSYQEoZ8yYOOPXTCZLQQunvRc=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mholt/certmagic v0.8.3/go.mod h1:91uJzK5K8IWtYQqTi5R2tsxV1pCde+wdGfaRaOZi6aQ=
github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
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 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nrdcg/auroradns v1.0.0/go.mod h1:6JPXKzIRzZzMqtTDgueIhTi6rFf1QvYE/HzqidhOhjw=
github.com/nrdcg/goinwx v0.6.1/go.mod h1:XPiut7enlbEdntAqalBIqcYcTEVhpv/dKWgDCX2SwKQ=
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 h1:lM6RxxfUMrYL/f8bWEUqdXrANWtrL7Nndbm9iFN0DlU=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin-contrib/zipkin-go-opentracing v0.3.5 h1:82Tnq9OJpn+h5xgGpss5/mOv3KXdjtkdorFSOUusjM8=
github.com/openzipkin-contrib/zipkin-go-opentracing v0.3.5/go.mod h1:uVHyebswE1cCXr2A73cRM2frx5ld1RJUCJkFNZ90ZiI=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/oracle/oci-go-sdk v7.0.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=
github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014/go.mod h1:joRatxRJaZBsY3JAOEMcoOp05CnZzsx4scTxi95DHyQ=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA=
github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sacloud/libsacloud v1.26.1/go.mod h1:79ZwATmHLIFZIMd7sxA3LwzVy/B77uj3LDoToVTxDoQ=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shirou/gopsutil v2.20.3+incompatible h1:0JVooMPsT7A7HqEYdydp/OfjSOYSjhXV7w1hkKj/NPQ=
github.com/shirou/gopsutil v2.20.3+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY=
github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/transip/gotransip v0.0.0-20190812104329-6d8d9179b66f/go.mod h1:i0f4R4o2HM0m3DZYQWsj6/MEowD57VzoH0v3d7igeFY=
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/vultr/govultr v0.1.4/go.mod h1:9H008Uxr/C4vFNGLqKx232C206GL0PBHzOP0809bGNA=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/etcd v0.5.0-alpha.5.0.20200306183522-221f0cc107cb h1:TcJ8iNja1CH/h/3QcsydKL5krb0MIPjMJLYgzClNaSQ=
go.etcd.io/etcd v0.5.0-alpha.5.0.20200306183522-221f0cc107cb/go.mod h1:VZB9Yx4s43MHItytoe8jcvaEFEgF2QzHDZGfQ/XQjvQ=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y=
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo=
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180611182652-db08ff08e862/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/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-20190125091013-d26f9f9a57f3/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-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191027093000-83d349e8ac1a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/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-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
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-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0 h1:xQwXv67TxFo9nC1GJFyab5eq/5B590r6RlnL/G8Sz7w=
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
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-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200306153348-d950eab6f860 h1:QmnwU8dKvY8c/vZikd2jhBNwrrGS5qeyK/2Aeeh9Grk=
google.golang.org/genproto v0.0.0-20200306153348-d950eab6f860/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0 h1:bO/TA4OxCOummhSf10siHuG7vJOiwh7SpRpFZDkOgl4=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
gopkg.in/DataDog/dd-trace-go.v1 v1.22.0 h1:gpWsqqkwUldNZXGJqT69NU9MdEDhLboK1C4nMgR0MWw=
gopkg.in/DataDog/dd-trace-go.v1 v1.22.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
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.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190730140822-b51389932cbc/go.mod h1:VV+3haRsgDiVLxyifmMBrBIuCWFBPYKbRssXB9z67Hw=
gopkg.in/resty.v1 v1.9.1/go.mod h1:vo52Hzryw9PnPHcJfPsBiFW62XhNx5OczbV9y+IMpgc=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.17.4 h1:HbwOhDapkguO8lTAE8OX3hdF2qp8GtpC9CW/MQATXXo=
k8s.io/api v0.17.4/go.mod h1:5qxx6vjmwUVG2nHQTKGlLts8Tbok8PzHl4vHtVFuZCA=
k8s.io/apimachinery v0.17.4 h1:UzM+38cPUJnzqSQ+E1PY4YxMHIzQyCg29LOoGfo79Zw=
k8s.io/apimachinery v0.17.4/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g=
k8s.io/client-go v0.17.4 h1:VVdVbpTY70jiNHS1eiFkUt7ZIJX3txd29nDxxXH4en8=
k8s.io/client-go v0.17.4/go.mod h1:ouF6o5pz3is8qU0/qYL2RnoxOPqgfuidYLowytyLJmc=
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo=
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e h1:4Z09Hglb792X0kfOBBJUPFEyvVfQWrYT/l8h5EKA6JQ=
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=

5
health/README.md Normal file
View File

@ -0,0 +1,5 @@
# health
Fork of https://github.com/coredns/coredns/tree/master/plugin/health
In order to change the URL.

69
health/health.go Normal file
View File

@ -0,0 +1,69 @@
// Package health implements an HTTP handler that responds to health checks.
package health
import (
"io"
"net"
"net/http"
"time"
clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/coredns/coredns/plugin/pkg/reuseport"
)
var log = clog.NewWithPlugin("health")
// Health implements healthchecks by exporting a HTTP endpoint.
type health struct {
Addr string
lameduck time.Duration
ln net.Listener
nlSetup bool
mux *http.ServeMux
stop chan bool
}
func (h *health) OnStartup() error {
if h.Addr == "" {
h.Addr = ":8080"
}
h.stop = make(chan bool)
ln, err := reuseport.Listen("tcp", h.Addr)
if err != nil {
return err
}
h.ln = ln
h.mux = http.NewServeMux()
h.nlSetup = true
h.mux.HandleFunc("/health-check", func(w http.ResponseWriter, r *http.Request) {
// We're always healthy.
w.WriteHeader(http.StatusOK)
io.WriteString(w, http.StatusText(http.StatusOK))
})
go func() { http.Serve(h.ln, h.mux) }()
go func() { h.overloaded() }()
return nil
}
func (h *health) OnFinalShutdown() error {
if !h.nlSetup {
return nil
}
if h.lameduck > 0 {
log.Infof("Going into lameduck mode for %s", h.lameduck)
time.Sleep(h.lameduck)
}
h.ln.Close()
h.nlSetup = false
close(h.stop)
return nil
}

49
health/overloaded.go Normal file
View File

@ -0,0 +1,49 @@
package health
import (
"net/http"
"time"
"github.com/coredns/coredns/plugin"
"github.com/prometheus/client_golang/prometheus"
)
// overloaded queries the health end point and updates a metrics showing how long it took.
func (h *health) overloaded() {
timeout := time.Duration(5 * time.Second)
client := http.Client{
Timeout: timeout,
}
url := "http://" + h.Addr
tick := time.NewTicker(1 * time.Second)
defer tick.Stop()
for {
select {
case <-tick.C:
start := time.Now()
resp, err := client.Get(url)
if err != nil {
HealthDuration.Observe(timeout.Seconds())
continue
}
resp.Body.Close()
HealthDuration.Observe(time.Since(start).Seconds())
case <-h.stop:
return
}
}
}
var (
// HealthDuration is the metric used for exporting how fast we can retrieve the /health endpoint.
HealthDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
Namespace: plugin.Namespace,
Subsystem: "health",
Name: "request_duration_seconds",
Buckets: plugin.TimeBuckets,
Help: "Histogram of the time (in seconds) each request took.",
})
)

73
health/setup.go Normal file
View File

@ -0,0 +1,73 @@
package health
import (
"fmt"
"net"
"time"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metrics"
"github.com/caddyserver/caddy"
)
func init() { plugin.Register("health", setup) }
func setup(c *caddy.Controller) error {
addr, lame, err := parse(c)
if err != nil {
return plugin.Error("health", err)
}
h := &health{Addr: addr, stop: make(chan bool), lameduck: lame}
c.OnStartup(func() error {
metrics.MustRegister(c, HealthDuration)
return nil
})
c.OnStartup(h.OnStartup)
c.OnRestart(h.OnFinalShutdown)
c.OnFinalShutdown(h.OnFinalShutdown)
c.OnRestartFailed(h.OnStartup)
// Don't do AddPlugin, as health is not *really* a plugin just a separate webserver running.
return nil
}
func parse(c *caddy.Controller) (string, time.Duration, error) {
addr := ""
dur := time.Duration(0)
for c.Next() {
args := c.RemainingArgs()
switch len(args) {
case 0:
case 1:
addr = args[0]
if _, _, e := net.SplitHostPort(addr); e != nil {
return "", 0, e
}
default:
return "", 0, c.ArgErr()
}
for c.NextBlock() {
switch c.Val() {
case "lameduck":
args := c.RemainingArgs()
if len(args) != 1 {
return "", 0, c.ArgErr()
}
l, err := time.ParseDuration(args[0])
if err != nil {
return "", 0, fmt.Errorf("unable to parse lameduck duration value: '%v' : %v", args[0], err)
}
dur = l
default:
return "", 0, c.ArgErr()
}
}
}
return addr, dur, nil
}

34
info/README.md Normal file
View File

@ -0,0 +1,34 @@
# Info
This plugin makes it possible to check what AdGuard DNS server is in use.
```
info {
domain adguard.com
type unfiltered
protocol auto
addr 176.103.130.136 176.103.130.137
canary dnscheck.adguard.com
}
```
Discovery requests look like: `*-{protocol}-{type}-dnscheck.{domain}`.
For instance, `12321-doh-unfiltered-dnscheck.adguard.com`. If the domain is queried
using `doh` protocol from a server with type `unfiltered`, the request will return
the specified `addr`. Otherwise, it will return `NXDOMAIN`.
* `domain` - registered domain that will be used in the discovery DNS queries.
* `type` - server type (any string).
* `protocol` - possible values are `dns`, `doh`, `dot`, `dnscrypt`, `auto`.
If it's set to `auto`, the plugin will try to detect the protocol by itself.
If it's set to a specific protocol, the plugin won't try to detect anything.
* `addr` - the list of addresses to return in the discovery response.
You can specify multiple addresses here.
IPv4 addresses will be used for A responses, IPv6 - for AAAA.
* `canary` - (optional) simple "canary" domain which only purpose is to test whether AdGuard DNS
is enabled or not without any additional logic (protocol or type detection) on top of it.
> Note: canary domain is used by third-party services that may want to discover if AdGuard DNS is used or not.
> For instance, Keenetic routers use it.

129
info/info.go Normal file
View File

@ -0,0 +1,129 @@
package info
import (
"context"
"fmt"
"net"
"strings"
"github.com/AdguardTeam/AdGuardDNS/util"
clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/coredns/coredns/request"
"github.com/coredns/coredns/plugin"
"github.com/miekg/dns"
)
type info struct {
Next plugin.Handler
domain string // etld domain name for the check DNS requests
protocol string // protocol (can be auto, dns, doh, dot, dnscrypt)
serverType string // server type (arbitrary string)
canary string // canary domain
addrs4 []net.IP // list of IPv4 addresses to return in response to an A check request
addrs6 []net.IP // list of IPv4 addresses to return in response to an AAAA check request
}
// Name returns name of the plugin as seen in Corefile and plugin.cfg
func (i *info) Name() string { return "info" }
// ServeDNS handles the DNS request and refuses if it's an ANY request
func (i *info) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
if len(r.Question) != 1 {
// google DNS, bind and others do the same
return dns.RcodeFormatError, fmt.Errorf("got DNS request with != 1 questions")
}
question := r.Question[0]
host := strings.ToLower(strings.TrimSuffix(question.Name, "."))
if i.canary != "" && host == i.canary {
return i.writeAnswer(w, r)
}
protocol := i.getProtocol(ctx)
checkDomain := fmt.Sprintf("-%s-%s-dnscheck.%s", protocol, i.serverType, i.domain)
if strings.HasSuffix(host, checkDomain) {
return i.writeAnswer(w, r)
}
return plugin.NextOrFailure(i.Name(), i.Next, ctx, w, r)
}
func (i *info) getProtocol(ctx context.Context) string {
if i.protocol == "auto" {
addr := util.GetServer(ctx)
if strings.HasPrefix(addr, "tls") {
return "dot"
} else if strings.HasPrefix(addr, "https") {
return "doh"
}
return "dns"
}
return i.protocol
}
func (i *info) writeAnswer(w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
m := i.genAnswer(r)
state.SizeAndDo(m)
err := state.W.WriteMsg(m)
if err != nil {
clog.Infof("Got error %s\n", err)
return dns.RcodeServerFailure, err
}
return m.Rcode, nil
}
func (i *info) genAnswer(r *dns.Msg) *dns.Msg {
m := new(dns.Msg)
m.SetRcode(r, dns.RcodeSuccess)
qType := r.Question[0].Qtype
if qType == dns.TypeA && len(i.addrs4) > 0 {
for _, ip := range i.addrs4 {
m.Answer = append(m.Answer, i.genA(r, ip))
}
} else if qType == dns.TypeAAAA && len(i.addrs6) > 0 {
for _, ip := range i.addrs6 {
m.Answer = append(m.Answer, i.genAAAA(r, ip))
}
}
m.RecursionAvailable = true
m.Compress = true
return m
}
func (i *info) genA(r *dns.Msg, ip net.IP) *dns.A {
answer := new(dns.A)
answer.Hdr = dns.RR_Header{
Name: r.Question[0].Name,
Rrtype: dns.TypeA,
Ttl: 100,
Class: dns.ClassINET,
}
answer.A = ip
return answer
}
func (i *info) genAAAA(r *dns.Msg, ip net.IP) *dns.AAAA {
answer := new(dns.AAAA)
answer.Hdr = dns.RR_Header{
Name: r.Question[0].Name,
Rrtype: dns.TypeAAAA,
Ttl: 100,
Class: dns.ClassINET,
}
answer.AAAA = ip
return answer
}

114
info/info_test.go Normal file
View File

@ -0,0 +1,114 @@
package info
import (
"context"
"testing"
"github.com/miekg/dns"
"github.com/caddyserver/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test"
"github.com/stretchr/testify/assert"
)
func TestInfoCheckRequest(t *testing.T) {
cfg := `info {
domain adguard.com
canary dnscheck.adguard.com
protocol auto
type test
addr 176.103.130.132 176.103.130.134 2a00:5a60::bad1:ff 2a00:5a60::bad2:ff
}`
c := caddy.NewTestController("info", cfg)
c.ServerBlockKeys = []string{""}
i, err := setupPlugin(c)
assert.Nil(t, err)
// Prepare context
srv := &dnsserver.Server{Addr: "https://"}
ctx := context.WithValue(context.Background(), dnsserver.Key{}, srv)
// Prepare response writer
resp := test.ResponseWriter{}
rrw := dnstest.NewRecorder(&resp)
// --
// Test type=A queries
// Prepare test request
req := new(dns.Msg)
req.SetQuestion("32132124-doh-test-dnscheck.adguard.com", dns.TypeA)
// Pass to the plugin
rCode, err := i.ServeDNS(ctx, rrw, req)
// Check rcode and error first
assert.Nil(t, err)
assert.Equal(t, dns.RcodeSuccess, rCode)
// Now let's check the response
assert.NotNil(t, rrw.Msg)
assert.Equal(t, len(rrw.Msg.Answer), 2)
a1, ok := rrw.Msg.Answer[0].(*dns.A)
assert.True(t, ok)
assert.Equal(t, "176.103.130.132", a1.A.String())
a2, ok := rrw.Msg.Answer[1].(*dns.A)
assert.True(t, ok)
assert.Equal(t, "176.103.130.134", a2.A.String())
// --
// Test type=AAAA queries
// Prepare test request
req = new(dns.Msg)
req.SetQuestion("32132124-doh-test-dnscheck.adguard.com", dns.TypeAAAA)
// Pass to the plugin
rCode, err = i.ServeDNS(ctx, rrw, req)
// Check rcode and error first
assert.Nil(t, err)
assert.Equal(t, dns.RcodeSuccess, rCode)
// Now let's check the response
assert.NotNil(t, rrw.Msg)
assert.Equal(t, len(rrw.Msg.Answer), 2)
aaaa1, ok := rrw.Msg.Answer[0].(*dns.AAAA)
assert.True(t, ok)
assert.Equal(t, "2a00:5a60::bad1:ff", aaaa1.AAAA.String())
aaaa2, ok := rrw.Msg.Answer[1].(*dns.AAAA)
assert.True(t, ok)
assert.Equal(t, "2a00:5a60::bad2:ff", aaaa2.AAAA.String())
// --
// Test canary domain
// Prepare test request
req = new(dns.Msg)
req.SetQuestion("dnscheck.adguard.com", dns.TypeA)
// Pass to the plugin
rCode, err = i.ServeDNS(ctx, rrw, req)
// Check rcode and error first
assert.Nil(t, err)
assert.Equal(t, dns.RcodeSuccess, rCode)
// Now let's check the response
assert.NotNil(t, rrw.Msg)
assert.Equal(t, len(rrw.Msg.Answer), 2)
a1, ok = rrw.Msg.Answer[0].(*dns.A)
assert.True(t, ok)
assert.Equal(t, "176.103.130.132", a1.A.String())
a2, ok = rrw.Msg.Answer[1].(*dns.A)
assert.True(t, ok)
assert.Equal(t, "176.103.130.134", a2.A.String())
}

108
info/setup.go Normal file
View File

@ -0,0 +1,108 @@
package info
import (
"errors"
"fmt"
"net"
"github.com/caddyserver/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
clog "github.com/coredns/coredns/plugin/pkg/log"
)
func init() {
caddy.RegisterPlugin("info", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
func setup(c *caddy.Controller) error {
clog.Infof("Initializing the info plugin for %s", c.ServerBlockKeys[c.ServerBlockKeyIndex])
p, err := setupPlugin(c)
if err != nil {
return err
}
config := dnsserver.GetConfig(c)
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
p.Next = next
return p
})
clog.Infof("Finished initializing the info plugin for %s", c.ServerBlockKeys[c.ServerBlockKeyIndex])
return nil
}
// setupPlugin parses and validates the plugin configuration
func setupPlugin(c *caddy.Controller) (*info, error) {
i := &info{}
for c.Next() {
for c.NextBlock() {
switch c.Val() {
case "domain":
if !c.NextArg() || len(c.Val()) == 0 {
return nil, c.ArgErr()
}
i.domain = c.Val()
case "type":
if !c.NextArg() || len(c.Val()) == 0 {
return nil, c.ArgErr()
}
i.serverType = c.Val()
case "protocol":
if !c.NextArg() || len(c.Val()) == 0 {
return nil, c.ArgErr()
}
i.protocol = c.Val()
case "canary":
if !c.NextArg() || len(c.Val()) == 0 {
return nil, c.ArgErr()
}
i.canary = c.Val()
case "addr":
args := c.RemainingArgs()
for _, arg := range args {
ip := net.ParseIP(arg)
if ip == nil {
return nil, fmt.Errorf("invalid IP %s", arg)
}
if ip.To4() == nil {
i.addrs6 = append(i.addrs6, ip)
} else {
i.addrs4 = append(i.addrs4, ip)
}
}
}
}
}
return validate(i)
}
func validate(i *info) (*info, error) {
if i.domain == "" {
return nil, errors.New("domain must be set")
}
if i.serverType == "" {
return nil, errors.New("server type must be set")
}
if i.protocol != "auto" &&
i.protocol != "dns" &&
i.protocol != "doh" &&
i.protocol != "dot" &&
i.protocol != "dnscrypt" {
return nil, fmt.Errorf("invalid protocol %s", i.protocol)
}
if len(i.addrs4) == 0 && len(i.addrs6) == 0 {
return nil, errors.New("addr must be set")
}
return i, nil
}

52
info/setup_test.go Normal file
View File

@ -0,0 +1,52 @@
package info
import (
"testing"
"github.com/caddyserver/caddy"
)
func TestSetup(t *testing.T) {
for i, testcase := range []struct {
config string
failing bool
}{
// Failing
{`info`, true},
{`info 100`, true},
{`info {
domain adguard.com
}`, true},
{`info {
domain adguard.com
protocol test
}`, true},
// Success
{`info {
domain adguard.com
protocol auto
type test
addr 176.103.130.135
}`, false},
{`info {
canary dnscheck.adguard.com
domain adguard.com
protocol auto
type test
addr 176.103.130.132 176.103.130.134 2a00:5a60::bad1:ff 2a00:5a60::bad2:ff
}`, false},
} {
c := caddy.NewTestController("info", testcase.config)
c.ServerBlockKeys = []string{""}
_, err := setupPlugin(c)
if err != nil {
if !testcase.failing {
t.Fatalf("Test #%d expected no errors, but got: %v", i, err)
}
continue
}
if testcase.failing {
t.Fatalf("Test #%d expected to fail but it didn't", i)
}
}
}

7
lrucache/README.md Normal file
View File

@ -0,0 +1,7 @@
# lrucache
This is a very simple LRU cache that we're using instead of the default coredns cache plugin.
```
lrucache [SIZE]
```

263
lrucache/cache.go Normal file
View File

@ -0,0 +1,263 @@
package lrucache
import (
"encoding/binary"
"math"
"strings"
"sync"
"time"
"github.com/bluele/gcache"
clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/miekg/dns"
)
const defaultCacheSize = 1000 // in number of elements
type item struct {
m *dns.Msg // dns message
when time.Time // time when m was cached
}
type cache struct {
items gcache.Cache // cache
cacheSize int // cache size
sync.RWMutex // lock
}
func (c *cache) Get(request *dns.Msg) (*dns.Msg, bool) {
if request == nil {
return nil, false
}
// create key for request
ok, key := key(request)
if !ok {
clog.Debug("key returned !ok")
return nil, false
}
c.Lock()
if c.items == nil {
c.Unlock()
return nil, false
}
c.Unlock()
rawValue, err := c.items.Get(key)
if err == gcache.KeyNotFoundError {
// not a real error, just no key found
return nil, false
}
if err != nil {
// real error
clog.Errorf("can't get response for %s from cache: %s", request.Question[0].Name, err)
return nil, false
}
cachedValue, ok := rawValue.(item)
if !ok {
clog.Errorf("entry with invalid type in cache for %s", request.Question[0].Name)
return nil, false
}
response := cachedValue.fromItem(request)
return response, true
}
func (c *cache) Set(m *dns.Msg) {
if m == nil {
return // no-op
}
if !isCacheable(m) {
return
}
ok, key := key(m)
if !ok {
return
}
i := toItem(m)
c.Lock()
// lazy initialization for cache
if c.items == nil {
size := defaultCacheSize
if c.cacheSize > 0 {
size = c.cacheSize
}
c.items = gcache.New(size).LRU().Build()
}
c.Unlock()
// set ttl as expiration time for item
ttl := time.Duration(findLowestTTL(m)) * time.Second
err := c.items.SetWithExpire(key, i, ttl)
if err != nil {
clog.Warning("Couldn't set cache item")
}
}
// check if message is cacheable
func isCacheable(m *dns.Msg) bool {
// truncated messages aren't valid
if m.Truncated {
clog.Debug("Refusing to cache truncated message")
return false
}
// if has wrong number of questions, also don't cache
if len(m.Question) != 1 {
clog.Debugf("Refusing to cache message with wrong number of questions")
return false
}
qName := m.Question[0].Name
qType := m.Question[0].Qtype
ttl := findLowestTTL(m)
if ttl == 0 {
return false
}
if m.Rcode != dns.RcodeSuccess && m.Rcode != dns.RcodeNameError {
clog.Debugf("%s: refusing to cache message with response type %s", qName, dns.RcodeToString[m.Rcode])
return false
}
if m.Rcode == dns.RcodeSuccess && (qType == dns.TypeA || qType == dns.TypeAAAA) {
// Now verify that it contains at least one A or AAAA record
if len(m.Answer) == 0 {
clog.Debugf("%s: refusing to cache a NOERROR response with no answers", qName)
return false
}
found := false
for _, rr := range m.Answer {
if rr.Header().Rrtype == dns.TypeA || rr.Header().Rrtype == dns.TypeAAAA {
found = true
break
}
}
if !found {
clog.Debugf("%s: refusing to cache a response with no A and AAAA answers", qName)
return false
}
}
return true
}
func findLowestTTL(m *dns.Msg) uint32 {
var ttl uint32 = math.MaxUint32
if m.Answer != nil {
for _, r := range m.Answer {
ttl = getTTLIfLower(r.Header(), ttl)
}
}
if m.Ns != nil {
for _, r := range m.Ns {
ttl = getTTLIfLower(r.Header(), ttl)
}
}
if m.Extra != nil {
for _, r := range m.Extra {
ttl = getTTLIfLower(r.Header(), ttl)
}
}
if ttl == math.MaxUint32 {
return 0
}
return ttl
}
func getTTLIfLower(h *dns.RR_Header, ttl uint32) uint32 {
if h.Rrtype == dns.TypeOPT {
return ttl
}
if h.Ttl < ttl {
return h.Ttl
}
return ttl
}
// key is binary little endian in sequence:
// uint8(do)
// uint16(qtype)
// uint16(qclass)
// name
func key(m *dns.Msg) (bool, string) {
if len(m.Question) != 1 {
clog.Debugf("got msg with len(m.Question) != 1: %d", len(m.Question))
return false, ""
}
q := m.Question[0]
b := make([]byte, 1+2+2+len(q.Name))
// put do
opt := m.IsEdns0()
do := false
if opt != nil {
do = opt.Do()
}
if do {
b[0] = 1
} else {
b[0] = 0
}
// put qtype, qclass, name
binary.BigEndian.PutUint16(b[1:], q.Qtype)
binary.BigEndian.PutUint16(b[3:], q.Qclass)
name := strings.ToLower(q.Name)
copy(b[5:], name)
return true, string(b)
}
func toItem(m *dns.Msg) item {
return item{
m: m,
when: time.Now(),
}
}
func (i *item) fromItem(request *dns.Msg) *dns.Msg {
response := &dns.Msg{}
response.SetReply(request)
response.Authoritative = false
response.AuthenticatedData = i.m.AuthenticatedData
response.RecursionAvailable = i.m.RecursionAvailable
response.Rcode = i.m.Rcode
ttl := findLowestTTL(i.m)
timeleft := math.Round(float64(ttl) - time.Since(i.when).Seconds())
var newttl uint32
if timeleft > 0 {
newttl = uint32(timeleft)
}
for _, r := range i.m.Answer {
answer := dns.Copy(r)
answer.Header().Ttl = newttl
response.Answer = append(response.Answer, answer)
}
for _, r := range i.m.Ns {
ns := dns.Copy(r)
ns.Header().Ttl = newttl
response.Ns = append(response.Ns, ns)
}
for _, r := range i.m.Extra {
// don't return OPT records as these are hop-by-hop
if r.Header().Rrtype == dns.TypeOPT {
continue
}
extra := dns.Copy(r)
extra.Header().Ttl = newttl
response.Extra = append(response.Extra, extra)
}
return response
}

37
lrucache/cache_test.go Normal file
View File

@ -0,0 +1,37 @@
package lrucache
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/coredns/coredns/plugin/test"
"github.com/miekg/dns"
)
func TestCache(t *testing.T) {
cache := &cache{}
// request with do
req := new(dns.Msg)
req.SetQuestion("testhost.", dns.TypeA)
req.SetEdns0(4096, true)
res := new(dns.Msg)
res.SetReply(req)
res.SetEdns0(4096, true)
res.RecursionAvailable = true
res.Answer = []dns.RR{
test.A("testhost. 255 IN A 37.220.26.135"),
}
// save to cache
cache.Set(res)
// get from cache
cachedRes, ok := cache.Get(req)
assert.True(t, ok)
assert.NotNil(t, cachedRes)
assert.Equal(t, req.Question[0].Name, cachedRes.Question[0].Name)
}

31
lrucache/cache_writer.go Normal file
View File

@ -0,0 +1,31 @@
package lrucache
import (
clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/miekg/dns"
)
// Recorder is a type of ResponseWriter that captures
// the rcode code written to it and also the size of the message
// written in the response. A rcode code does not have
// to be written, however, in which case 0 must be assumed.
// It is best to have the constructor initialize this type
// with that default status code.
type CacheWriter struct {
dns.ResponseWriter
cache *cache
}
// WriteMsg records the status code and calls the
// underlying ResponseWriter's WriteMsg method.
func (r *CacheWriter) WriteMsg(res *dns.Msg) error {
r.cache.Set(res)
return r.ResponseWriter.WriteMsg(res)
}
// Write is a wrapper that records the length of the message that gets written.
func (r *CacheWriter) Write(buf []byte) (int, error) {
clog.Debugf("Caching called with Write: not caching reply")
// Not caching in this case
return r.ResponseWriter.Write(buf)
}

33
lrucache/handler.go Normal file
View File

@ -0,0 +1,33 @@
package lrucache
import (
"fmt"
"github.com/coredns/coredns/plugin"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
// ServeDNS handles the DNS request and refuses if it's an ANY request
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
if len(r.Question) != 1 {
// google DNS, bind and others do the same
return dns.RcodeFormatError, fmt.Errorf("got DNS request with != 1 questions")
}
reply, ok := p.cache.Get(r)
if ok {
lruCacheHits.Inc()
_ = w.WriteMsg(reply)
return reply.Rcode, nil
}
lruCacheMisses.Inc()
cw := &CacheWriter{
ResponseWriter: w,
cache: p.cache,
}
return plugin.NextOrFailure(p.Name(), p.Next, ctx, cw, r)
}

20
lrucache/metrics.go Normal file
View File

@ -0,0 +1,20 @@
package lrucache
import (
"github.com/coredns/coredns/plugin"
"github.com/prometheus/client_golang/prometheus"
)
func newDNSCounter(name string, help string) prometheus.Counter {
return prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "lrucache",
Name: name,
Help: help,
})
}
var (
lruCacheHits = newDNSCounter("lrucache_hits_total", "Count of LRU cache hits")
lruCacheMisses = newDNSCounter("lrucache_misses_total", "Count of LRU cache misses")
)

87
lrucache/setup.go Normal file
View File

@ -0,0 +1,87 @@
package lrucache
import (
"strconv"
"github.com/caddyserver/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metrics"
clog "github.com/coredns/coredns/plugin/pkg/log"
)
// serverBlockCaches stores a map of caches and server blocks.
// The idea is to have one cache per server block, and not per listen address
// as it works by default.
var serverBlockCaches = make(map[int]*cache)
// plug represents the CoreDNS plugin and contains
// a link to the next plugin in the chain.
type plug struct {
Next plugin.Handler
cache *cache
}
// Name returns name of the plugin as seen in Corefile and plugin.cfg
func (p *plug) Name() string { return "lrucache" }
func init() {
caddy.RegisterPlugin("lrucache", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
func setupPlugin(c *caddy.Controller) (*plug, error) {
p := &plug{
cache: &cache{},
}
if serverBlockCache, ok := serverBlockCaches[c.ServerBlockIndex]; ok {
clog.Infof("Cache was already initialized for server block %d", c.ServerBlockIndex)
p.cache = serverBlockCache
return p, nil
}
var serverBlockCache = &cache{}
for c.Next() {
args := c.RemainingArgs()
if len(args) > 0 {
size, err := strconv.Atoi(args[0])
if err != nil {
return nil, c.ArgErr()
}
clog.Infof("Cache size is %d", size)
serverBlockCache.cacheSize = size
}
}
clog.Infof("Initialized cache for server block %d", c.ServerBlockIndex)
serverBlockCaches[c.ServerBlockIndex] = serverBlockCache
p.cache = serverBlockCache
return p, nil
}
func setup(c *caddy.Controller) error {
clog.Infof("Initializing the lrucache plugin for %s", c.ServerBlockKeys[c.ServerBlockKeyIndex])
p, err := setupPlugin(c)
if err != nil {
return err
}
config := dnsserver.GetConfig(c)
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
p.Next = next
return p
})
c.OnStartup(func() error {
metrics.MustRegister(c, lruCacheHits, lruCacheMisses)
return nil
})
clog.Infof("Finished initializing the lrucache plugin for %s", c.ServerBlockKeys[c.ServerBlockKeyIndex])
return nil
}

74
main.go Normal file
View File

@ -0,0 +1,74 @@
package main
import (
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/coremain"
// Plug in CoreDNS plugins that are needed
_ "github.com/coredns/coredns/plugin/any"
_ "github.com/coredns/coredns/plugin/bind"
_ "github.com/coredns/coredns/plugin/cache"
_ "github.com/coredns/coredns/plugin/debug"
_ "github.com/coredns/coredns/plugin/errors"
_ "github.com/coredns/coredns/plugin/file"
_ "github.com/coredns/coredns/plugin/log"
_ "github.com/coredns/coredns/plugin/metrics"
_ "github.com/coredns/coredns/plugin/pprof"
_ "github.com/coredns/coredns/plugin/tls"
_ "github.com/coredns/coredns/plugin/whoami"
// Our CoreDNS plugins forks
_ "github.com/AdguardTeam/AdGuardDNS/alternate"
_ "github.com/AdguardTeam/AdGuardDNS/forward"
_ "github.com/AdguardTeam/AdGuardDNS/health"
// Our plugins
_ "github.com/AdguardTeam/AdGuardDNS/dnsdb"
_ "github.com/AdguardTeam/AdGuardDNS/dnsfilter"
_ "github.com/AdguardTeam/AdGuardDNS/info"
_ "github.com/AdguardTeam/AdGuardDNS/lrucache"
_ "github.com/AdguardTeam/AdGuardDNS/ratelimit"
_ "github.com/AdguardTeam/AdGuardDNS/refuseany"
)
// Directives are registered in the order they should be
// executed.
//
// Ordering is VERY important. Every plugin will
// feel the effects of all other plugin below
// (after) them during a request, but they must not
// care what plugin above them are doing.
var directives = []string{
"bind",
"tls",
"debug",
"pprof",
"prometheus",
"errors",
"log",
// Start: our plugins. The order is important
"info",
"refuseany",
"ratelimit",
"dnsfilter", // It will process cached responses as well
"dnsdb", // DNSDB plugin is after the dnsfilter -- to see the real responses
"lrucache", // Cache: set it to be the last of our plugins
// End: our plugins
"cache",
"file",
// Start: our forked CoreDNS plugins
"health",
"alternate",
"forward",
// End: our forked CoreDNS plugins
"whoami",
"on",
}
func init() {
dnsserver.Directives = directives
}
func main() {
coremain.Run()
}

View File

@ -1,36 +0,0 @@
# Directives are registered in the order they should be
# executed.
#
# Ordering is VERY important. Every plugin will
# feel the effects of all other plugin below
# (after) them during a request, but they must not
# care what plugin above them are doing.
# How to rebuild with updated plugin configurations:
# Modify the list below and run `go gen && go build`
# The parser takes the input format of
# <plugin-name>:<package-name>
# Or
# <plugin-name>:<fully-qualified-package-name>
#
# External plugin example:
# log:github.com/coredns/coredns/plugin/log
# Local plugin example:
# log:log
tls:tls
bind:bind
debug:debug
pprof:pprof
prometheus:metrics
errors:errors
log:log
ratelimit:bit.adguard.com/dns/adguard-internal-dns/coredns_plugin/ratelimit
refuseany:bit.adguard.com/dns/adguard-internal-dns/coredns_plugin/refuseany
dnsfilter:bit.adguard.com/dns/adguard-internal-dns/coredns_plugin
cache:cache
rewrite:rewrite
template:template
file:file
forward:forward

View File

@ -1,36 +0,0 @@
#!/bin/bash
set -e -x -o pipefail
echo "executing $0"
case "$1" in
configure)
if ! getent passwd "<%= user %>" > /dev/null ; then
echo "Adding user for <%= project %>" >&2
adduser --system -ingroup nogroup --quiet \
--no-create-home \
--disabled-login \
--gecos "<%= project %> user" \
--shell /bin/bash "<%= user %>"
fi
OUTFILE="/var/lib/dnsfilter/dns.txt"
if [ ! -f "${OUTFILE}" ]; then
echo "${OUTFILE} does not exists. Downloading..."
URL="https://filters.adtidy.org/android/filters/15.txt"
mkdir -p /var/lib/dnsfilter
wget -q --timeout=90 "$URL" -O "$OUTFILE"
if [ $? -ne 0 ]
then
echo "Filter rules could not be downloaded."
fi
fi
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac

View File

@ -1,3 +0,0 @@
#!/bin/bash
set -e -x -o pipefail
echo "executing $0"

View File

@ -1,3 +0,0 @@
#!/bin/bash
set -e -x -o pipefail
echo "executing $0"

View File

@ -1,3 +0,0 @@
#!/bin/bash
set -e -x -o pipefail
echo "executing $0"

18
ratelimit/README.md Normal file
View File

@ -0,0 +1,18 @@
# ratelimit
This plugin allows to configure an arbitrary rate limit for the DNS server.
```
ratelimit [RPS] [BACKOFF_LIMIT] {
whitelist [[ADDR1], ..., ADDRN]
consul URL TTL
}
```
* `[RPS]` - maximum number of requests per second
* `[BACKOFF_LIMIT]` - supposed to help with repeated offenders. If some IP gets rate-limited for more than `[BACKOFF_LIMIT]` times in 30 minutes, this IP will be blocked until this 30 mins period ends.
* `whitelist` -- allows to configure IP addresses excluded from the ratelimit.
* `consul` -- allows to use Consul as a source for the whitelisted IP addresses.
The first parameter is the URL where the plugin can download services list from.
The second parameter is TTL of this list in seconds. The plugin will reload it automatically.

72
ratelimit/consul.go Normal file
View File

@ -0,0 +1,72 @@
package ratelimit
import (
"encoding/json"
"io/ioutil"
"net/http"
"sort"
"time"
clog "github.com/coredns/coredns/plugin/pkg/log"
)
func (p *plug) periodicConsulWhitelistReload() {
ttl := time.Duration(p.consulTTL) * time.Second
clog.Infof("Reloading consul whitelist every %s", ttl.String())
ticker := time.NewTicker(ttl)
defer ticker.Stop()
// sleep the first time -- we've already loaded the list
time.Sleep(ttl)
for t := range ticker.C {
_ = t // we don't print the ticker time, so assign this `t` variable to underscore `_` to avoid error
_ = p.reloadConsulWhitelist()
}
}
func (p *plug) reloadConsulWhitelist() error {
clog.Infof("Loading consul whitelist from %s", p.consulURL)
resp, err := http.Get(p.consulURL)
if err != nil {
clog.Errorf("Failed to load whitelist: %v", err)
return err
}
defer func() { _ = resp.Body.Close() }()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
clog.Errorf("Failed to read response body: %v", err)
return err
}
var raw []map[string]interface{}
err = json.Unmarshal(body, &raw)
if err != nil {
clog.Errorf("Failed to unmarshal response: %v", err)
return err
}
var whitelist []string
for _, item := range raw {
if addr, found := item["Address"]; found {
if addrStr, ok := addr.(string); ok {
whitelist = append(whitelist, addrStr)
}
}
}
if len(whitelist) > 0 {
sort.Strings(whitelist)
}
whitelistLen := len(whitelist) + len(p.whitelist)
WhitelistCountGauge.Set(float64(whitelistLen))
clog.Infof("Loaded %d records from %s", len(whitelist), p.consulURL)
p.consulWhitelistGuard.Lock()
p.consulWhitelist = whitelist
p.consulWhitelistGuard.Unlock()
return nil
}

50
ratelimit/metrics.go Normal file
View File

@ -0,0 +1,50 @@
package ratelimit
import (
"github.com/coredns/coredns/plugin"
"github.com/prometheus/client_golang/prometheus"
)
var (
RateLimitedCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "ratelimit",
Name: "dropped_count",
Help: "Count of requests that have been dropped because of rate limit",
}, []string{"server"})
BackOffCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "ratelimit",
Name: "dropped_backoff_count",
Help: "Count of requests that have been dropped because of the backoff period",
}, []string{"server"})
WhitelistedCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "ratelimit",
Name: "whitelisted_count",
Help: "Count of requests that have been whitelisted in the rate limiter",
}, []string{"server"})
WhitelistCountGauge = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: plugin.Namespace,
Subsystem: "ratelimit",
Name: "whitelist_size",
Help: "Size of the whitelist",
})
RateLimitersCountGauge = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: plugin.Namespace,
Subsystem: "ratelimit",
Name: "ratelimiters_total",
Help: "Count of the currently active rate limiters",
})
RateLimitedIPAddressesCountGauge = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: plugin.Namespace,
Subsystem: "ratelimit",
Name: "ratelimited_addresses_total",
Help: "Count of the addresses which are currently rate limited",
})
)

163
ratelimit/ratelimit.go Normal file
View File

@ -0,0 +1,163 @@
package ratelimit
import (
"sort"
"time"
"github.com/AdguardTeam/AdGuardDNS/util"
"go.uber.org/atomic"
// ratelimiting and per-ip buckets
"github.com/beefsack/go-rate"
"github.com/patrickmn/go-cache"
// coredns plugin
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
const defaultRatelimit = 30
const defaultBackOffLimit = 1000
const defaultResponseSize = 1000
const rateLimitersCacheTTL = time.Minute * 10
const backOffTTL = time.Minute * 30
var (
rateLimitersCache = cache.New(rateLimitersCacheTTL, rateLimitersCacheTTL)
backOffCache = cache.New(backOffTTL, backOffTTL)
)
// ServeDNS handles the DNS request and refuses if it's an beyind specified ratelimit
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
server := util.GetServer(ctx)
if state.Proto() != "udp" {
// Do not apply ratelimit plugin to non-UDP requests
return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
}
ip := state.IP()
if p.isBackOff(ip) {
RateLimitedCounter.WithLabelValues(server).Inc()
BackOffCounter.WithLabelValues(server).Inc()
return 0, nil
}
allow, whitelisted, err := p.allowRequest(ip)
if err != nil {
return 0, err
}
if whitelisted {
WhitelistedCounter.WithLabelValues(server).Inc()
}
if !allow {
RateLimitedCounter.WithLabelValues(server).Inc()
return 0, nil
}
// Record response to get status code and size of the reply.
rw := dnstest.NewRecorder(w)
status, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, rw, r)
size := rw.Len
if size > defaultResponseSize && state.Proto() == "udp" {
// For large UDP responses we call allowRequest more times
// The exact number of times depends on the response size
for i := 0; i < size/defaultResponseSize; i++ {
_, _, _ = p.allowRequest(ip)
}
}
return status, err
}
// allowRequest checks if this IP address is rate-limited or not
// returns allow, whitelisted, error
func (p *plug) allowRequest(ip string) (bool, bool, error) {
if p.isWhitelisted(ip) {
return true, true, nil
}
var rateLimiter *rate.RateLimiter
rl, found := rateLimitersCache.Get(ip)
if found {
rateLimiter = rl.(*rate.RateLimiter)
} else {
rateLimiter = rate.New(p.ratelimit, time.Second)
rateLimitersCache.Set(ip, rateLimiter, rateLimitersCacheTTL)
RateLimitersCountGauge.Set(float64(rateLimitersCache.ItemCount()))
}
allow, _ := rateLimiter.Try()
if !allow {
p.countRateLimited(ip)
}
return allow, false, nil
}
// countRateLimited is called for the IP address which already got rate-limited
// if this continues to happen, and the IP address gets rate-limited more than X
// times during the backOffTTL period, the IP gets blocked until the backOffTTL
// period ends.
func (p *plug) countRateLimited(ip string) {
var counter *atomic.Int64
c, found := backOffCache.Get(ip)
if !found {
counter = atomic.NewInt64(0)
backOffCache.Set(ip, counter, backOffTTL)
RateLimitedIPAddressesCountGauge.Set(float64(backOffCache.ItemCount()))
} else {
counter = c.(*atomic.Int64)
}
counter.Inc()
}
// isBackOff checks if it is the backoff period for the specified IP
func (p *plug) isBackOff(ip string) bool {
// backOffCache.
c, found := backOffCache.Get(ip)
if !found {
return false
}
counter := c.(*atomic.Int64)
return counter.Load() > int64(p.backOffLimit)
}
// isWhitelisted checks if the specified IP is whitelisted
func (p *plug) isWhitelisted(ip string) bool {
if len(p.whitelist) > 0 {
i := sort.SearchStrings(p.whitelist, ip)
if i < len(p.whitelist) && p.whitelist[i] == ip {
return true
}
}
if p.consulURL == "" {
return false
}
p.consulWhitelistGuard.Lock()
if len(p.consulWhitelist) > 0 {
i := sort.SearchStrings(p.consulWhitelist, ip)
if i < len(p.consulWhitelist) && p.consulWhitelist[i] == ip {
p.consulWhitelistGuard.Unlock()
return true
}
}
p.consulWhitelistGuard.Unlock()
return false
}

136
ratelimit/ratelimit_test.go Normal file
View File

@ -0,0 +1,136 @@
package ratelimit
import (
"fmt"
"net"
"testing"
"github.com/caddyserver/caddy"
"github.com/stretchr/testify/assert"
)
func TestRatelimiting(t *testing.T) {
// rate limit is 1 per sec
c := caddy.NewTestController("dns", `ratelimit 1`)
c.ServerBlockKeys = []string{""}
p, err := setupPlugin(c)
if err != nil {
t.Fatal("Failed to initialize the plugin")
}
allowed, _, err := p.allowRequest("127.0.0.1")
if err != nil || !allowed {
t.Fatal("First request must have been allowed")
}
allowed, _, err = p.allowRequest("127.0.0.1")
if err != nil || allowed {
t.Fatal("Second request must have been ratelimited")
}
}
func TestBackOff(t *testing.T) {
// rate limit is 1 per sec
// backoff is 2 for 30 minutes
c := caddy.NewTestController("dns", `ratelimit 1 2`)
c.ServerBlockKeys = []string{""}
p, err := setupPlugin(c)
rateLimitersCache.Flush()
backOffCache.Flush()
if err != nil {
t.Fatal("Failed to initialize the plugin")
}
ip := "127.0.0.1"
allowed, _, err := p.allowRequest(ip)
if err != nil || !allowed {
t.Fatal("First request must have been allowed")
}
allowed, _, err = p.allowRequest(ip)
if err != nil || allowed {
t.Fatal("Second request must have been ratelimited")
}
// Not enough for backoff to kick in
assert.False(t, p.isBackOff(ip))
// Get it ratelimited one more time
_, _, _ = p.allowRequest(ip)
// Still not enough
assert.False(t, p.isBackOff(ip))
// Ok, do it again
_, _, _ = p.allowRequest(ip)
// Now we're talking
assert.True(t, p.isBackOff(ip))
}
func TestWhitelist(t *testing.T) {
// rate limit is 1 per sec
c := caddy.NewTestController("dns", `ratelimit 1 { whitelist 127.0.0.2 127.0.0.1 127.0.0.125 }`)
c.ServerBlockKeys = []string{""}
p, err := setupPlugin(c)
if err != nil {
t.Fatal("Failed to initialize the plugin")
}
allowed, whitelisted, err := p.allowRequest("127.0.0.1")
if err != nil || !allowed {
t.Fatal("First request must have been allowed")
}
assert.True(t, whitelisted)
allowed, whitelisted, err = p.allowRequest("127.0.0.1")
if err != nil || !allowed {
t.Fatal("Second request must have been allowed due to whitelist")
}
assert.True(t, whitelisted)
}
func TestConsulWhitelist(t *testing.T) {
l := testStartConsulService()
defer func() { _ = l.Close() }()
// rate limit is 1 per sec
cfg := fmt.Sprintf(`ratelimit 1 {
consul http://127.0.0.1:%d/v1/catalog/service/test 123
}`, l.Addr().(*net.TCPAddr).Port)
c := caddy.NewTestController("dns", cfg)
c.ServerBlockKeys = []string{""}
p, err := setupPlugin(c)
assert.Nil(t, p.reloadConsulWhitelist())
if err != nil {
t.Fatalf("Failed to initialize the plugin: %v", err)
}
allowed, whitelisted, err := p.allowRequest("123.123.123.122")
if err != nil || !allowed {
t.Fatal("First request must have been allowed")
}
assert.True(t, whitelisted)
allowed, whitelisted, err = p.allowRequest("123.123.123.122")
if err != nil || !allowed {
t.Fatal("Second request must have been allowed due to whitelist")
}
assert.True(t, whitelisted)
}

136
ratelimit/setup.go Normal file
View File

@ -0,0 +1,136 @@
package ratelimit
import (
"sort"
"strconv"
"sync"
"github.com/caddyserver/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metrics"
clog "github.com/coredns/coredns/plugin/pkg/log"
)
//
// helper functions
//
func init() {
caddy.RegisterPlugin("ratelimit", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
type plug struct {
Next plugin.Handler
// configuration for creating above
ratelimit int // in requests per second per IP
// if the IP gets blocked more times than the specified backOffTTL
// it will stay blocked until this period ends
backOffLimit int
// whitelist is a list of whitelisted IP addresses
// IMPORTANT: must be sorted
whitelist []string
// consulURL - URL of the consul service where we can get a list
// of services to add to the whitelist
consulURL string
// consulTTL - ttl of the consul list. The plugin will attempt
// to reload this list every "consulTTL" seconds.
consulTTL int
// consulWhitelist -- whitelist loaded from the consul web service
// IMPORTANT: must be sorted
consulWhitelist []string
consulWhitelistGuard sync.Mutex
}
// Name returns name of the plugin as seen in Corefile and plugin.cfg
func (p *plug) Name() string { return "ratelimit" }
func setupPlugin(c *caddy.Controller) (*plug, error) {
p := &plug{
ratelimit: defaultRatelimit,
backOffLimit: defaultBackOffLimit,
}
for c.Next() {
args := c.RemainingArgs()
if len(args) > 0 {
ratelimit, err := strconv.Atoi(args[0])
if err != nil {
return nil, c.ArgErr()
}
p.ratelimit = ratelimit
}
if len(args) > 1 {
backOffLimit, err := strconv.Atoi(args[1])
if err != nil {
return nil, c.ArgErr()
}
p.backOffLimit = backOffLimit
}
for c.NextBlock() {
switch c.Val() {
case "whitelist":
p.whitelist = c.RemainingArgs()
if len(p.whitelist) > 0 {
sort.Strings(p.whitelist)
}
case "consul":
args = c.RemainingArgs()
if len(args) != 2 {
return nil, c.ArgErr()
}
p.consulURL = args[0]
consulTTL, err := strconv.Atoi(args[1])
if err != nil || consulTTL <= 0 {
return nil, c.ArgErr()
}
p.consulTTL = consulTTL
}
}
}
return p, nil
}
func setup(c *caddy.Controller) error {
clog.Infof("Initializing the ratelimit plugin for %s", c.ServerBlockKeys[c.ServerBlockKeyIndex])
p, err := setupPlugin(c)
if err != nil {
return err
}
if p.consulURL != "" {
err = p.reloadConsulWhitelist()
if err != nil {
return err
}
// Start the periodic reload job
go p.periodicConsulWhitelistReload()
}
config := dnsserver.GetConfig(c)
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
p.Next = next
return p
})
c.OnStartup(func() error {
metrics.MustRegister(c, RateLimitedCounter, BackOffCounter, RateLimitersCountGauge,
RateLimitedIPAddressesCountGauge, WhitelistedCounter, WhitelistCountGauge)
return nil
})
clog.Infof("Finished initializing the ratelimit plugin for %s", c.ServerBlockKeys[c.ServerBlockKeyIndex])
return nil
}

119
ratelimit/setup_test.go Normal file
View File

@ -0,0 +1,119 @@
package ratelimit
import (
"fmt"
"net"
"net/http"
"testing"
"github.com/caddyserver/caddy"
)
func TestSetup(t *testing.T) {
l := testStartConsulService()
defer func() { _ = l.Close() }()
for i, testcase := range []struct {
config string
failing bool
}{
{`ratelimit`, false},
{`ratelimit 100`, false},
{`ratelimit {
whitelist 127.0.0.1
}`, false},
{`ratelimit 50 {
whitelist 127.0.0.1 176.103.130.130
}`, false},
{`ratelimit test`, true},
{fmt.Sprintf(`ratelimit 50 {
whitelist 127.0.0.1 176.103.130.130
consul http://127.0.0.1:%d/v1/catalog/service/test 123
}`, l.Addr().(*net.TCPAddr).Port), false},
} {
c := caddy.NewTestController("dns", testcase.config)
c.ServerBlockKeys = []string{""}
err := setup(c)
if err != nil {
if !testcase.failing {
t.Fatalf("Test #%d expected no errors, but got: %v", i, err)
}
continue
}
if testcase.failing {
t.Fatalf("Test #%d expected to fail but it didn't", i)
}
}
}
func testStartConsulService() net.Listener {
mux := http.NewServeMux()
mux.HandleFunc("/v1/catalog/service/test", func(w http.ResponseWriter, r *http.Request) {
content := `[{
"ID": "5c6183d2-20fe-7615-d49e-080000000025",
"Node": "some-host-name",
"Address": "123.123.123.123",
"Datacenter": "eu",
"TaggedAddresses": {
"lan": "123.123.123.123",
"wan": "123.123.123.123"
},
"NodeMeta": {},
"ServiceKind": "",
"ServiceID": "test",
"ServiceName": "test",
"ServiceTags": ["prod"],
"ServiceAddress": "",
"ServiceWeights": {
"Passing": 1,
"Warning": 1
},
"ServiceMeta": {},
"ServicePort": 1987,
"ServiceEnableTagOverride": false,
"ServiceProxyDestination": "",
"ServiceProxy": {},
"ServiceConnect": {},
"CreateIndex": 1584089033,
"ModifyIndex": 1584089033
},{
"ID": "5c6183d2-20fe-7615-d49e-080000000026",
"Node": "some-host-name2",
"Address": "123.123.123.122",
"Datacenter": "eu",
"TaggedAddresses": {
"lan": "123.123.123.122",
"wan": "123.123.123.122"
},
"NodeMeta": {},
"ServiceKind": "",
"ServiceID": "test",
"ServiceName": "test",
"ServiceTags": ["prod"],
"ServiceAddress": "",
"ServiceWeights": {
"Passing": 1,
"Warning": 1
},
"ServiceMeta": {},
"ServicePort": 1987,
"ServiceEnableTagOverride": false,
"ServiceProxyDestination": "",
"ServiceProxy": {},
"ServiceConnect": {},
"CreateIndex": 1584089033,
"ModifyIndex": 1584089033
}]`
_, _ = w.Write([]byte(content))
})
listener, err := net.Listen("tcp", ":0")
if err != nil {
panic(err)
}
srv := &http.Server{Handler: mux}
go func() { _ = srv.Serve(listener) }()
return listener
}

3
refuseany/README.md Normal file
View File

@ -0,0 +1,3 @@
# refuseany
This is a very simple plugin that drops all `ANY` requests.

19
refuseany/metrics.go Normal file
View File

@ -0,0 +1,19 @@
package refuseany
import (
"github.com/coredns/coredns/plugin"
"github.com/prometheus/client_golang/prometheus"
)
func newDNSCounter(name string, help string) prometheus.Counter {
return prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "refuseany",
Name: name,
Help: help,
})
}
var (
refusedAnyTotal = newDNSCounter("refusedany_total", "Count of ANY requests that have been dropped")
)

37
refuseany/refuseany.go Normal file
View File

@ -0,0 +1,37 @@
package refuseany
import (
"fmt"
"github.com/coredns/coredns/plugin"
clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
// ServeDNS handles the DNS request and refuses if it's an ANY request
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
if len(r.Question) != 1 {
// google DNS, bind and others do the same
return dns.RcodeFormatError, fmt.Errorf("got DNS request with != 1 questions")
}
q := r.Question[0]
if q.Qtype == dns.TypeANY {
state := request.Request{W: w, Req: r}
rcode := dns.RcodeNotImplemented
m := new(dns.Msg)
m.SetRcode(r, rcode)
state.SizeAndDo(m)
err := state.W.WriteMsg(m)
if err != nil {
clog.Infof("Got error %s\n", err)
return dns.RcodeServerFailure, err
}
return rcode, nil
}
return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
}

43
refuseany/setup.go Normal file
View File

@ -0,0 +1,43 @@
package refuseany
import (
"github.com/caddyserver/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metrics"
clog "github.com/coredns/coredns/plugin/pkg/log"
)
func init() {
caddy.RegisterPlugin("refuseany", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
type plug struct {
Next plugin.Handler
}
// Name returns name of the plugin as seen in Corefile and plugin.cfg
func (p *plug) Name() string { return "refuseany" }
func setup(c *caddy.Controller) error {
clog.Infof("Initializing the refuseany plugin for %s", c.ServerBlockKeys[c.ServerBlockKeyIndex])
p := &plug{}
config := dnsserver.GetConfig(c)
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
p.Next = next
return p
})
c.OnStartup(func() error {
metrics.MustRegister(c, refusedAnyTotal)
return nil
})
clog.Infof("Finished initializing the refuseany plugin for %s", c.ServerBlockKeys[c.ServerBlockKeyIndex])
return nil
}

28685
tests/dns.txt Normal file

File diff suppressed because it is too large Load Diff

18
tests/dnscheck.txt Normal file
View File

@ -0,0 +1,18 @@
$ORIGIN .
$TTL 7200 ; 2 hours
dnscheck-default.adguard.com IN SOA dnscheck-default.adguard.com. hostmaster.dnscheck-default.adguard.com. (
2017010100 ; serial
3600 ; refresh
1800 ; retry
864000 ; expire
1800 ; minimum
)
NS 176.103.130.130
NS 176.103.130.131
NS 2a00:5a60::ad1:ff
NS 2a00:5a60::ad2:ff
$ORIGIN dnscheck-default.adguard.com.
A 176.103.130.130
A 176.103.130.131
AAAA 2a00:5a60::ad1:ff
AAAA 2a00:5a60::ad2:ff

1
tests/parental.txt Normal file
View File

@ -0,0 +1 @@
testparental.example.org

1
tests/sb.txt Normal file
View File

@ -0,0 +1 @@
testsb.example.org

22
tests/test.crt Normal file
View File

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDpDCCAoygAwIBAgIJAMmRiqdFYWIPMA0GCSqGSIb3DQEBBQUAMEsxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMQ0wCwYDVQQHDARUZXN0MRgwFgYD
VQQKDA9ET19OT1RfVFJVU1RfQ0EwHhcNMjAwNDA4MTUwMDQxWhcNMjAwODA2MTUw
MDQxWjBeMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTESMBAGA1UE
BwwJVGhlIENsb3VkMQ0wCwYDVQQKDAREZW1vMRcwFQYDVQQDDA5NeSBDZXJ0aWZp
Y2F0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALM+5xsmk8OD074q
ZqJTv8/3Ojoqv0de0aG3cTsS9WQi8s2G5a5mXtMvleCtl1wP0BGjVsNn37rWzHv3
rx1rK84quxus9zzaUbmfyt1aLYEM4cJBtcrWXty7NTNwrPteuXx1/0lE5N+7HSLx
bP7MnE29WuVrKDhXC36qkzs8Z25EpwQyJuV3qC1OhMoBm7T3srRot5LJuER9K8FH
Nuw2cUbfPCZdTSUpQURfgP5U7OR5WR31UobVXreFp8/TBwBluyz11TK9xTsRLbwo
jgIxOOuDWja1nnFjjB2kuJRWs5CD4R7ErFqfx95a5k73gPkYfYvcgRvN5H8IBSB2
DnJiiWMCAwEAAaN4MHYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUFOmwAk1TIUZMRPoG
l52lHNdziiwwCwYDVR0PBAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEF
BQcDATAeBgNVHREEFzAVhwR/AAABgg1leGFtcGxlLmxvY2FsMA0GCSqGSIb3DQEB
BQUAA4IBAQCya8A51Rlv85pv+mmNkEJ13jdmB5qd6ydfNK3N5xQijdycA71TB0Hv
jtPq4wkbufyiPjRd7Ou1oh7i3dwQB08DFZFZJ1EQ3Nhq1/ACBU6+TdWJn+4/nFH3
3PMx38dmoV0Dxj/W849J64ioZwGByWLD9pAR6uZ2XYXiAqsxfBSMdbsBn6PxsC1L
fVmj1LA3DEphp+BtP2woTIKCHzhnLic9EJ4Rz2EqIfMAanRufGLno9IkNxUJ/2H+
kRQRQ8K3GADgkuLlb1GEyZ1e86ce8RxTK4G7WMgM6cJjh1IWTpphee6kDcnYardc
5SswBUp9BotOyGdwuRcusx9op9UJu+V+
-----END CERTIFICATE-----

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