Initial commit
This commit is contained in:
507
coredns_plugin/coredns_plugin.go
Normal file
507
coredns_plugin/coredns_plugin.go
Normal file
@@ -0,0 +1,507 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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/plugin/pkg/upstream"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/AdguardTeam/AdguardDNS/dnsfilter"
|
||||
"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 Plugin struct {
|
||||
d *dnsfilter.Dnsfilter
|
||||
Next plugin.Handler
|
||||
upstream upstream.Upstream
|
||||
hosts map[string]net.IP
|
||||
|
||||
SafeBrowsingBlockHost string
|
||||
ParentalBlockHost string
|
||||
QueryLogEnabled bool
|
||||
}
|
||||
|
||||
var defaultPlugin = Plugin{
|
||||
SafeBrowsingBlockHost: "safebrowsing.block.dns.adguard.com",
|
||||
ParentalBlockHost: "family.block.dns.adguard.com",
|
||||
}
|
||||
|
||||
func newDnsCounter(name string, help string) prometheus.Counter {
|
||||
return prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "dnsfilter",
|
||||
Name: name,
|
||||
Help: help,
|
||||
})
|
||||
}
|
||||
|
||||
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.")
|
||||
)
|
||||
|
||||
//
|
||||
// coredns handling functions
|
||||
//
|
||||
func setupPlugin(c *caddy.Controller) (*Plugin, error) {
|
||||
// create new Plugin and copy default values
|
||||
var d = new(Plugin)
|
||||
*d = defaultPlugin
|
||||
d.d = dnsfilter.New()
|
||||
d.hosts = make(map[string]net.IP)
|
||||
|
||||
var filterFileName string
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
// must have at least one argument
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
filterFileName = args[0]
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "safebrowsing":
|
||||
d.d.EnableSafeBrowsing()
|
||||
if c.NextArg() {
|
||||
if len(c.Val()) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
d.d.SetSafeBrowsingServer(c.Val())
|
||||
}
|
||||
case "safesearch":
|
||||
d.d.EnableSafeSearch()
|
||||
case "parental":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
sensitivity, err := strconv.Atoi(c.Val())
|
||||
if err != nil {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
err = d.d.EnableParental(sensitivity)
|
||||
if err != nil {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
if c.NextArg() {
|
||||
if len(c.Val()) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
d.ParentalBlockHost = c.Val()
|
||||
}
|
||||
case "querylog":
|
||||
d.QueryLogEnabled = true
|
||||
once.Do(func() {
|
||||
go startQueryLogServer() // TODO: how to handle errors?
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file, err := os.Open(filterFileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
text := scanner.Text()
|
||||
if d.parseEtcHosts(text) {
|
||||
continue
|
||||
}
|
||||
err = d.d.AddRule(text, 0)
|
||||
if err == dnsfilter.ErrInvalidSyntax {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err = scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.upstream, err = upstream.New(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
d, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := dnsserver.GetConfig(c)
|
||||
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
d.Next = next
|
||||
return d
|
||||
})
|
||||
|
||||
c.OnStartup(func() error {
|
||||
once.Do(func() {
|
||||
m := dnsserver.GetConfig(c).Handler("prometheus")
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
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(d)
|
||||
}
|
||||
})
|
||||
return nil
|
||||
})
|
||||
c.OnShutdown(d.OnShutdown)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Plugin) parseEtcHosts(text string) bool {
|
||||
if pos := strings.IndexByte(text, '#'); pos != -1 {
|
||||
text = text[0:pos]
|
||||
}
|
||||
fields := strings.Fields(text)
|
||||
if len(fields) < 2 {
|
||||
return false
|
||||
}
|
||||
addr := net.ParseIP(fields[0])
|
||||
if addr == nil {
|
||||
return false
|
||||
}
|
||||
for _, host := range fields[1:] {
|
||||
if val, ok := d.hosts[host]; ok {
|
||||
log.Printf("warning: host %s already has value %s, will overwrite it with %s", host, val, addr)
|
||||
}
|
||||
d.hosts[host] = addr
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *Plugin) OnShutdown() error {
|
||||
d.d.Destroy()
|
||||
d.d = nil
|
||||
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 == false {
|
||||
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 == false {
|
||||
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 (d *Plugin) doStats(ch interface{}, doFunc statsFunc) {
|
||||
stats := d.d.GetStats()
|
||||
gen(ch, doFunc, "coredns_dnsfilter_safebrowsing_requests", "Number of safebrowsing HTTP requests that were sent", float64(stats.Safebrowsing.Requests), prometheus.CounterValue)
|
||||
gen(ch, doFunc, "coredns_dnsfilter_safebrowsing_cachehits", "Number of safebrowsing lookups that didn't need HTTP requests", float64(stats.Safebrowsing.CacheHits), prometheus.CounterValue)
|
||||
gen(ch, doFunc, "coredns_dnsfilter_safebrowsing_pending", "Number of currently pending safebrowsing HTTP requests", float64(stats.Safebrowsing.Pending), prometheus.GaugeValue)
|
||||
gen(ch, doFunc, "coredns_dnsfilter_safebrowsing_pending_max", "Maximum number of pending safebrowsing HTTP requests", float64(stats.Safebrowsing.PendingMax), prometheus.GaugeValue)
|
||||
gen(ch, doFunc, "coredns_dnsfilter_parental_requests", "Number of parental HTTP requests that were sent", float64(stats.Parental.Requests), prometheus.CounterValue)
|
||||
gen(ch, doFunc, "coredns_dnsfilter_parental_cachehits", "Number of parental lookups that didn't need HTTP requests", float64(stats.Parental.CacheHits), prometheus.CounterValue)
|
||||
gen(ch, doFunc, "coredns_dnsfilter_parental_pending", "Number of currently pending parental HTTP requests", float64(stats.Parental.Pending), prometheus.GaugeValue)
|
||||
gen(ch, doFunc, "coredns_dnsfilter_parental_pending_max", "Maximum number of pending parental HTTP requests", float64(stats.Parental.PendingMax), prometheus.GaugeValue)
|
||||
}
|
||||
|
||||
func (d *Plugin) Describe(ch chan<- *prometheus.Desc) {
|
||||
d.doStats(ch, doDesc)
|
||||
}
|
||||
|
||||
func (d *Plugin) Collect(ch chan<- prometheus.Metric) {
|
||||
d.doStats(ch, doMetric)
|
||||
}
|
||||
|
||||
func (d *Plugin) 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)
|
||||
if addr != nil {
|
||||
// this is an IP address, return it
|
||||
result, err := dns.NewRR(host + " A " + 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
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion(dns.Fqdn(val), question.Qtype)
|
||||
req.RecursionDesired = true
|
||||
reqstate := request.Request{W: w, Req: req, Context: ctx}
|
||||
result, err := d.upstream.Lookup(reqstate, dns.Fqdn(val), reqstate.QType())
|
||||
if err != nil {
|
||||
log.Printf("Got error %s\n", err)
|
||||
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
|
||||
}
|
||||
if result != nil {
|
||||
for _, answer := range result.Answer {
|
||||
answer.Header().Name = question.Name
|
||||
}
|
||||
records = result.Answer
|
||||
}
|
||||
}
|
||||
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 genSOA(r *dns.Msg) []dns.RR {
|
||||
zone := r.Question[0].Name
|
||||
header := dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Ttl: 3600, 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 = uint32(time.Now().Unix())
|
||||
return []dns.RR{soa}
|
||||
}
|
||||
|
||||
func 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 = 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 (d *Plugin) serveDNSInternal(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error, dnsfilter.Result) {
|
||||
if len(r.Question) != 1 {
|
||||
// google DNS, bind and others do the same
|
||||
return dns.RcodeFormatError, fmt.Errorf("Got DNS request with != 1 questions"), dnsfilter.Result{}
|
||||
}
|
||||
for _, question := range r.Question {
|
||||
host := strings.ToLower(strings.TrimSuffix(question.Name, "."))
|
||||
// if input is empty host, filter it out right away
|
||||
if index := strings.IndexByte(host, byte('.')); index == -1 {
|
||||
rcode, err := writeNXdomain(ctx, w, r)
|
||||
if err != nil {
|
||||
return rcode, err, dnsfilter.Result{}
|
||||
}
|
||||
return rcode, err, dnsfilter.Result{Reason: dnsfilter.FilteredInvalid}
|
||||
}
|
||||
// is it a safesearch domain?
|
||||
if val, ok := d.d.SafeSearchDomain(host); ok {
|
||||
rcode, err := d.replaceHostWithValAndReply(ctx, w, r, host, val, question)
|
||||
if err != nil {
|
||||
return rcode, err, dnsfilter.Result{}
|
||||
}
|
||||
return rcode, err, dnsfilter.Result{Reason: dnsfilter.FilteredSafeSearch}
|
||||
}
|
||||
|
||||
// is it in hosts?
|
||||
if val, ok := d.hosts[host]; ok {
|
||||
// it is, if it's a loopback host, reply with NXDOMAIN
|
||||
if val.IsLoopback() {
|
||||
rcode, err := writeNXdomain(ctx, w, r)
|
||||
if err != nil {
|
||||
return rcode, err, dnsfilter.Result{}
|
||||
}
|
||||
return rcode, err, dnsfilter.Result{Reason: dnsfilter.FilteredInvalid}
|
||||
}
|
||||
// it's not a loopback host, replace it with value specified
|
||||
rcode, err := d.replaceHostWithValAndReply(ctx, w, r, host, val.String(), question)
|
||||
if err != nil {
|
||||
return rcode, err, dnsfilter.Result{}
|
||||
}
|
||||
return rcode, err, dnsfilter.Result{Reason: dnsfilter.FilteredSafeSearch}
|
||||
}
|
||||
|
||||
// needs to be filtered instead
|
||||
result, err := d.d.CheckHost(host)
|
||||
if err != nil {
|
||||
log.Printf("plugin/dnsfilter: %s\n", err)
|
||||
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err), dnsfilter.Result{}
|
||||
}
|
||||
|
||||
// safebrowsing
|
||||
if result.IsFiltered == true && result.Reason == dnsfilter.FilteredSafeBrowsing {
|
||||
// return cname safebrowsing.block.dns.adguard.com
|
||||
val := d.SafeBrowsingBlockHost
|
||||
rcode, err := d.replaceHostWithValAndReply(ctx, w, r, host, val, question)
|
||||
if err != nil {
|
||||
return rcode, err, dnsfilter.Result{}
|
||||
}
|
||||
return rcode, err, result
|
||||
}
|
||||
|
||||
// parental
|
||||
if result.IsFiltered == true && result.Reason == dnsfilter.FilteredParental {
|
||||
// return cname
|
||||
val := d.ParentalBlockHost
|
||||
rcode, err := d.replaceHostWithValAndReply(ctx, w, r, host, val, question)
|
||||
if err != nil {
|
||||
return rcode, err, dnsfilter.Result{}
|
||||
}
|
||||
return rcode, err, result
|
||||
}
|
||||
|
||||
// blacklist
|
||||
if result.IsFiltered == true && result.Reason == dnsfilter.FilteredBlackList {
|
||||
rcode, err := writeNXdomain(ctx, w, r)
|
||||
if err != nil {
|
||||
return rcode, err, dnsfilter.Result{}
|
||||
}
|
||||
return rcode, err, result
|
||||
}
|
||||
if result.IsFiltered == false && result.Reason == dnsfilter.NotFilteredWhiteList {
|
||||
rcode, err := plugin.NextOrFailure(d.Name(), d.Next, ctx, w, r)
|
||||
return rcode, err, result
|
||||
}
|
||||
}
|
||||
rcode, err := plugin.NextOrFailure(d.Name(), d.Next, ctx, w, r)
|
||||
return rcode, err, dnsfilter.Result{}
|
||||
}
|
||||
|
||||
func (d *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
start := time.Now()
|
||||
requests.Inc()
|
||||
|
||||
// capture the written answer
|
||||
rrw := dnstest.NewRecorder(w)
|
||||
rcode, err, result := d.serveDNSInternal(ctx, rrw, r)
|
||||
if rcode > 0 {
|
||||
// actually send the answer if we have one
|
||||
state := request.Request{W: w, Req: r}
|
||||
answer := new(dns.Msg)
|
||||
answer.SetRcode(r, rcode)
|
||||
state.SizeAndDo(answer)
|
||||
w.WriteMsg(answer)
|
||||
}
|
||||
|
||||
// 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
|
||||
if d.QueryLogEnabled {
|
||||
logRequest(rrw.Msg, result, time.Since(start))
|
||||
}
|
||||
return rcode, err
|
||||
}
|
||||
|
||||
func (d *Plugin) Name() string { return "dnsfilter" }
|
||||
|
||||
var once sync.Once
|
||||
149
coredns_plugin/coredns_plugin_test.go
Normal file
149
coredns_plugin/coredns_plugin_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
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`, true},
|
||||
{`dnsfilter ../tests/dns.txt`, false},
|
||||
{`dnsfilter ../tests/dns.txt { safebrowsing }`, false},
|
||||
{`dnsfilter ../tests/dns.txt { parental }`, 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 TestEtcHostsParse(t *testing.T) {
|
||||
addr := "216.239.38.120"
|
||||
text := []byte(fmt.Sprintf(" %s google.com www.google.com # enforce google's safesearch ", addr))
|
||||
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())
|
||||
|
||||
c := caddy.NewTestController("dns", fmt.Sprintf("dnsfilter %s", tmpfile.Name()))
|
||||
p, err := setupPlugin(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(p.hosts) != 2 {
|
||||
t.Fatal("Expected p.hosts to have two keys")
|
||||
}
|
||||
|
||||
val, ok := p.hosts["google.com"]
|
||||
if !ok {
|
||||
t.Fatal("Expected google.com to be set in p.hosts")
|
||||
}
|
||||
if !val.Equal(net.ParseIP(addr)) {
|
||||
t.Fatalf("Expected google.com's value %s to match %s", val, addr)
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
c := caddy.NewTestController("dns", fmt.Sprintf("dnsfilter %s", tmpfile.Name()))
|
||||
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)
|
||||
}
|
||||
filtered := rcode == dns.RcodeNameError
|
||||
if testcase.filtered == true && testcase.filtered != filtered {
|
||||
t.Fatalf("Host %s expected to be filtered, instead it is not filtered", testcase.host)
|
||||
}
|
||||
if testcase.filtered == false && 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
|
||||
})
|
||||
}
|
||||
146
coredns_plugin/querylog.go
Normal file
146
coredns_plugin/querylog.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package dnsfilter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdguardDNS/dnsfilter"
|
||||
"github.com/coredns/coredns/plugin/pkg/response"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/zfjagann/golang-ring"
|
||||
)
|
||||
|
||||
var logBuffer = ring.Ring{}
|
||||
|
||||
type logEntry struct {
|
||||
R *dns.Msg
|
||||
Result dnsfilter.Result
|
||||
Time time.Time
|
||||
Elapsed time.Duration
|
||||
}
|
||||
|
||||
func init() {
|
||||
logBuffer.SetCapacity(1000)
|
||||
}
|
||||
|
||||
func logRequest(r *dns.Msg, result dnsfilter.Result, elapsed time.Duration) {
|
||||
entry := logEntry{
|
||||
R: r,
|
||||
Result: result,
|
||||
Time: time.Now(),
|
||||
Elapsed: elapsed,
|
||||
}
|
||||
logBuffer.Enqueue(entry)
|
||||
}
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
values := logBuffer.Values()
|
||||
var data = []map[string]interface{}{}
|
||||
for _, value := range values {
|
||||
entry, ok := value.(logEntry)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
jsonentry := map[string]interface{}{
|
||||
"reason": entry.Result.Reason.String(),
|
||||
"elapsed_ms": strconv.FormatFloat(entry.Elapsed.Seconds()*1000, 'f', -1, 64),
|
||||
"time": entry.Time.Format(time.RFC3339),
|
||||
}
|
||||
question := map[string]interface{}{
|
||||
"host": strings.ToLower(strings.TrimSuffix(entry.R.Question[0].Name, ".")),
|
||||
"type": dns.Type(entry.R.Question[0].Qtype).String(),
|
||||
"class": dns.Class(entry.R.Question[0].Qclass).String(),
|
||||
}
|
||||
jsonentry["question"] = question
|
||||
|
||||
status, _ := response.Typify(entry.R, time.Now().UTC())
|
||||
jsonentry["status"] = status.String()
|
||||
if len(entry.Result.Rule) > 0 {
|
||||
jsonentry["rule"] = entry.Result.Rule
|
||||
}
|
||||
|
||||
if len(entry.R.Answer) > 0 {
|
||||
var answers = []map[string]interface{}{}
|
||||
for _, k := range entry.R.Answer {
|
||||
header := k.Header()
|
||||
answer := map[string]interface{}{
|
||||
"type": dns.TypeToString[header.Rrtype],
|
||||
"ttl": header.Ttl,
|
||||
}
|
||||
// try most common record types
|
||||
switch v := k.(type) {
|
||||
case *dns.A:
|
||||
answer["value"] = v.A
|
||||
case *dns.AAAA:
|
||||
answer["value"] = v.AAAA
|
||||
case *dns.MX:
|
||||
answer["value"] = fmt.Sprintf("%v %v", v.Preference, v.Mx)
|
||||
case *dns.CNAME:
|
||||
answer["value"] = v.Target
|
||||
case *dns.NS:
|
||||
answer["value"] = v.Ns
|
||||
case *dns.SPF:
|
||||
answer["value"] = v.Txt
|
||||
case *dns.TXT:
|
||||
answer["value"] = v.Txt
|
||||
case *dns.PTR:
|
||||
answer["value"] = v.Ptr
|
||||
case *dns.SOA:
|
||||
answer["value"] = fmt.Sprintf("%v %v %v %v %v %v %v", v.Ns, v.Mbox, v.Serial, v.Refresh, v.Retry, v.Expire, v.Minttl)
|
||||
case *dns.CAA:
|
||||
answer["value"] = fmt.Sprintf("%v %v \"%v\"", v.Flag, v.Tag, v.Value)
|
||||
case *dns.HINFO:
|
||||
answer["value"] = fmt.Sprintf("\"%v\" \"%v\"", v.Cpu, v.Os)
|
||||
case *dns.RRSIG:
|
||||
answer["value"] = fmt.Sprintf("%v %v %v %v %v %v %v %v %v", dns.TypeToString[v.TypeCovered], v.Algorithm, v.Labels, v.OrigTtl, v.Expiration, v.Inception, v.KeyTag, v.SignerName, v.Signature)
|
||||
default:
|
||||
// type unknown, marshall it as-is
|
||||
answer["value"] = v
|
||||
}
|
||||
answers = append(answers, answer)
|
||||
}
|
||||
jsonentry["answer"] = answers
|
||||
}
|
||||
|
||||
data = append(data, jsonentry)
|
||||
}
|
||||
|
||||
json, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
errortext := fmt.Sprintf("Couldn't marshal data into json: %s", err)
|
||||
log.Println(errortext)
|
||||
http.Error(w, errortext, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err = w.Write(json)
|
||||
if err != nil {
|
||||
errortext := fmt.Sprintf("Unable to write response json: %s", err)
|
||||
log.Println(errortext)
|
||||
http.Error(w, errortext, 500)
|
||||
}
|
||||
}
|
||||
|
||||
func startQueryLogServer() {
|
||||
listenAddr := "127.0.0.1:8618" // sha512sum of "querylog" then each byte summed
|
||||
|
||||
http.HandleFunc("/querylog", handler)
|
||||
if err := http.ListenAndServe(listenAddr, nil); err != nil {
|
||||
log.Fatalf("error in ListenAndServe: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func trace(text string) {
|
||||
pc := make([]uintptr, 10) // at least 1 entry needed
|
||||
runtime.Callers(2, pc)
|
||||
f := runtime.FuncForPC(pc[0])
|
||||
log.Printf("%s(): %s\n", f.Name(), text)
|
||||
}
|
||||
142
coredns_plugin/ratelimit/ratelimit.go
Normal file
142
coredns_plugin/ratelimit/ratelimit.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package ratelimit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"strconv"
|
||||
"sync"
|
||||
"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/request"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
const defaultRatelimit = 100
|
||||
const defaultMaxRateLimitedIPs = 1024 * 1024
|
||||
|
||||
var (
|
||||
tokenBuckets = cache.New(time.Hour, time.Hour)
|
||||
)
|
||||
|
||||
// main function
|
||||
func (p *Plugin) 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
|
||||
}
|
||||
return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
func (p *Plugin) allowRequest(ip string) (bool, error) {
|
||||
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 == false {
|
||||
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 Plugin struct {
|
||||
Next plugin.Handler
|
||||
|
||||
// configuration for creating above
|
||||
ratelimit int // in requests per second per IP
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
p := &Plugin{ratelimit: defaultRatelimit}
|
||||
config := dnsserver.GetConfig(c)
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
if len(args) <= 0 {
|
||||
continue
|
||||
}
|
||||
ratelimit, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return c.ArgErr()
|
||||
}
|
||||
p.ratelimit = ratelimit
|
||||
}
|
||||
|
||||
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
p.Next = next
|
||||
return p
|
||||
})
|
||||
|
||||
c.OnStartup(func() error {
|
||||
once.Do(func() {
|
||||
m := dnsserver.GetConfig(c).Handler("prometheus")
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
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")
|
||||
)
|
||||
|
||||
func (d *Plugin) Name() string { return "ratelimit" }
|
||||
|
||||
var once sync.Once
|
||||
96
coredns_plugin/refuseany/refuseany.go
Normal file
96
coredns_plugin/refuseany/refuseany.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package refuseany
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"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 Plugin struct {
|
||||
Next plugin.Handler
|
||||
}
|
||||
|
||||
func (p *Plugin) 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 {
|
||||
log.Printf("Got request with type ANY, will respond with NOTIMP\n")
|
||||
|
||||
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
|
||||
} else {
|
||||
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 := &Plugin{}
|
||||
config := dnsserver.GetConfig(c)
|
||||
|
||||
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
p.Next = next
|
||||
return p
|
||||
})
|
||||
|
||||
c.OnStartup(func() error {
|
||||
once.Do(func() {
|
||||
m := dnsserver.GetConfig(c).Handler("prometheus")
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
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")
|
||||
)
|
||||
|
||||
func (d *Plugin) Name() string { return "refuseany" }
|
||||
|
||||
var once sync.Once
|
||||
Reference in New Issue
Block a user