mirror of
https://github.com/AdguardTeam/AdGuardDNS.git
synced 2025-02-20 11:23:36 +08:00
Sync with upstream
This commit is contained in:
parent
48a2aee015
commit
d48cb726fb
11
.gitignore
vendored
11
.gitignore
vendored
@ -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
71
Corefile
Normal 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
|
||||
}
|
60
Makefile
60
Makefile
@ -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
|
18
README.md
18
README.md
@ -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
60
VENDOR_PATCHES.md
Normal 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
5
alternate/README.md
Normal 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
70
alternate/alternate.go
Normal 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
221
alternate/alternate_test.go
Normal 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
84
alternate/setup.go
Normal 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
85
alternate/setup_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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" }
|
@ -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
|
||||
})
|
||||
}
|
@ -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)
|
||||
}
|
@ -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" }
|
@ -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")
|
||||
}
|
||||
}
|
@ -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
23
dnsdb/README.md
Normal 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
256
dnsdb/db.go
Normal 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
59
dnsdb/db_test.go
Normal 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
149
dnsdb/listen.go
Normal 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
33
dnsdb/metrics.go
Normal 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
52
dnsdb/plugin.go
Normal 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
81
dnsdb/plugin_test.go
Normal 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
38
dnsdb/record.go
Normal 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
99
dnsdb/setup.go
Normal 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
25
dnsfilter/README.md
Normal 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
289
dnsfilter/dnsfilter.go
Normal 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
481
dnsfilter/dnsfilter_test.go
Normal 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
66
dnsfilter/engines.go
Normal 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
207
dnsfilter/helper.go
Normal 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
85
dnsfilter/metrics.go
Normal 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
111
dnsfilter/reload.go
Normal 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
|
||||
}
|
73
dnsfilter/safe_services/safe_browsing.md
Normal file
73
dnsfilter/safe_services/safe_browsing.md
Normal 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.
|
216
dnsfilter/safe_services/safe_search.go
Normal file
216
dnsfilter/safe_services/safe_search.go
Normal 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",
|
||||
}
|
183
dnsfilter/safe_services/safe_services.go
Normal file
183
dnsfilter/safe_services/safe_services.go
Normal 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
|
||||
}
|
48
dnsfilter/safe_services/safe_services_test.go
Normal file
48
dnsfilter/safe_services/safe_services_test.go
Normal 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
376
dnsfilter/setup.go
Normal 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
106
dnsfilter/setup_test.go
Normal 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
95
dnsfilter/stats.go
Normal 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
28
dnsfilter/stats_test.go
Normal 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
110
dnsfilter/update.go
Normal 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
54
dnsfilter/update_test.go
Normal 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
5
forward/README.md
Normal 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
137
forward/connect.go
Normal 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
61
forward/dnstap.go
Normal 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
63
forward/dnstap_test.go
Normal 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
232
forward/forward.go
Normal 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
34
forward/fuzz.go
Normal 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
86
forward/health.go
Normal 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
225
forward/health_test.go
Normal 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
5
forward/log_test.go
Normal 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
54
forward/metrics.go
Normal 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
158
forward/persistent.go
Normal 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
109
forward/persistent_test.go
Normal 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
81
forward/proxy.go
Normal 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
123
forward/proxy_test.go
Normal 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
253
forward/setup.go
Normal 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.
|
47
forward/setup_policy_test.go
Normal file
47
forward/setup_policy_test.go
Normal 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
257
forward/setup_test.go
Normal 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
37
forward/type.go
Normal 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
22
go.mod
Normal 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
713
go.sum
Normal 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
5
health/README.md
Normal 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
69
health/health.go
Normal 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
49
health/overloaded.go
Normal 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
73
health/setup.go
Normal 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
34
info/README.md
Normal 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
129
info/info.go
Normal 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
114
info/info_test.go
Normal 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
108
info/setup.go
Normal 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
52
info/setup_test.go
Normal 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
7
lrucache/README.md
Normal 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
263
lrucache/cache.go
Normal 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
37
lrucache/cache_test.go
Normal 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
31
lrucache/cache_writer.go
Normal 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
33
lrucache/handler.go
Normal 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
20
lrucache/metrics.go
Normal 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
87
lrucache/setup.go
Normal 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
74
main.go
Normal 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()
|
||||
}
|
36
plugin.cfg
36
plugin.cfg
@ -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
|
@ -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
|
@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e -x -o pipefail
|
||||
echo "executing $0"
|
18
ratelimit/README.md
Normal file
18
ratelimit/README.md
Normal 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
72
ratelimit/consul.go
Normal 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
50
ratelimit/metrics.go
Normal 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
163
ratelimit/ratelimit.go
Normal 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
136
ratelimit/ratelimit_test.go
Normal 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
136
ratelimit/setup.go
Normal 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
119
ratelimit/setup_test.go
Normal 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
3
refuseany/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# refuseany
|
||||
|
||||
This is a very simple plugin that drops all `ANY` requests.
|
19
refuseany/metrics.go
Normal file
19
refuseany/metrics.go
Normal 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
37
refuseany/refuseany.go
Normal 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
43
refuseany/setup.go
Normal 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
28685
tests/dns.txt
Normal file
File diff suppressed because it is too large
Load Diff
18
tests/dnscheck.txt
Normal file
18
tests/dnscheck.txt
Normal 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
1
tests/parental.txt
Normal file
@ -0,0 +1 @@
|
||||
testparental.example.org
|
1
tests/sb.txt
Normal file
1
tests/sb.txt
Normal file
@ -0,0 +1 @@
|
||||
testsb.example.org
|
22
tests/test.crt
Normal file
22
tests/test.crt
Normal 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
Loading…
x
Reference in New Issue
Block a user