Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,35 @@ The _filter_ plugins enables blocking requests based on predefined lists and rul
- Regex and simple string matching support.
- Inspection of CNAME, SVCB and HTTPS records detects and blocks cloaking.
- Block replies are fully cacheable by the _cache_ plugin.
- Load allow/block/allow-ips/block-ips from file or S3 bucket
- The allow-ips will only allow networks if a block is made with a smaller network prefix, like 0.0.0.0/0 or ::/0 as the default is allow.

## Environment

To use S3 buckets, the environment variables must be set: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_REGION`. Once set the bucket path must start with `s3::`.


## Syntax

```corefile
filter {
allow FILE
block FILE
allow-ips FILE
block-ips FILE
uncloak
empty
ttl DURATION
reload DURATION
}
```

- `allow` load **FILE** to the whitelist.
- `block` load **FILE** to the blacklist.
- `allow-ips` load **FILE** to the IP response whitelist.
- `block-ips` load **FILE** to the IP response blacklist.
- `empty` return an empty answer record for every blocked request instead of an all zero record.
- `reload` **DURATION** read in the allow/block lists periodically, (example: reload 15s).
- `uncloak` enables response uncloaking, disabled by default.
- `ttl` sets **TTL** for blocked responses, default is 3600s.

Expand All @@ -46,6 +61,7 @@ If monitoring is enabled (via the _prometheus_ plugin) then the following metric
filter {
allow /lists/allowlist.txt
block /lists/denylist.txt
block-ips /lists/bad-ips.txt
uncloak
ttl 600
}
Expand Down
58 changes: 58 additions & 0 deletions cidr_matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package filter

import (
"bufio"
"errors"
"io"
"net"
"strings"

ranger "github.com/yl2chen/cidranger"
)

func LoadCIDR(r io.Reader, ranger4, ranger6 ranger.Ranger) (err error) {
if r == nil {
return errors.New("invalid list source")
}
//cr := ranger.NewPCTrieRanger()
scanner := bufio.NewScanner(r)
for scanner.Scan() {
pattern := strings.TrimSpace(scanner.Text())

if pattern == "" || strings.HasPrefix(pattern, "#") {
continue
}
if strings.Contains(pattern, "#") {
i := strings.Index(pattern, "#")
pattern = strings.TrimSpace(pattern[:i])
}

var network *net.IPNet

ip := net.ParseIP(pattern)
if ip != nil {
if x := ip.To4(); x != nil {
network = &net.IPNet{IP: ip, Mask: net.CIDRMask(32, 32)}
} else {
network = &net.IPNet{IP: ip, Mask: net.CIDRMask(128, 128)}
}
} else {
ip, network, err = net.ParseCIDR("192.168.1.0/24")
if err != nil {
log.Error(err)
continue
}
}

if x := ip.To4(); x != nil {
ranger4.Insert(ranger.NewBasicRangerEntry(*network))
} else {
ranger6.Insert(ranger.NewBasicRangerEntry(*network))
}

if scanner.Err() != nil {
return scanner.Err()
}
}
return nil
}
129 changes: 119 additions & 10 deletions filter.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package filter

import (
"bytes"
"context"
"crypto/sha256"
"io"
"strings"
"time"

"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metrics"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
ranger "github.com/yl2chen/cidranger"
)

const defaultResponseTTL = 3600 // Default TTL used for generated responses.
Expand All @@ -17,9 +22,22 @@ const defaultResponseTTL = 3600 // Default TTL used for generated responses.
type Filter struct {
Next plugin.Handler

// lists to allow or block domains from a file
allowlist *PatternMatcher
denylist *PatternMatcher

// lists to allow or block records
allowCIDR4list ranger.Ranger
allowCIDR6list ranger.Ranger
denyCIDR4list ranger.Ranger
denyCIDR6list ranger.Ranger

reload time.Duration
hash []byte

// return empty answers in the requests.
emptyResponse bool

// sources to load data into filters.
sources []listSource

Expand All @@ -32,9 +50,13 @@ type Filter struct {

func New() *Filter {
return &Filter{
allowlist: NewPatternMatcher(),
denylist: NewPatternMatcher(),
ttl: defaultResponseTTL,
allowlist: NewPatternMatcher(),
denylist: NewPatternMatcher(),
allowCIDR4list: ranger.NewPCTrieRanger(),
allowCIDR6list: ranger.NewPCTrieRanger(),
denyCIDR4list: ranger.NewPCTrieRanger(),
denyCIDR6list: ranger.NewPCTrieRanger(),
ttl: defaultResponseTTL,
}
}

Expand All @@ -46,7 +68,12 @@ func (f *Filter) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
if f.Match(state.Name()) {
BlockCount.WithLabelValues(server).Inc()

msg := createSyntheticResponse(r, f.ttl)
var msg *dns.Msg
if !f.emptyResponse {
msg = createSyntheticResponse(r, f.ttl)
} else {
msg = newEmptyResponse(r, f.ttl)
}
w.WriteMsg(msg) //nolint
return dns.RcodeSuccess, nil
}
Expand Down Expand Up @@ -75,24 +102,74 @@ func (f *Filter) Match(name string) bool {
return false
}

// Does a hash on the list files to determine if anything has changed
func (f *Filter) checkHash() (ck bool) {
h := sha256.New()
for _, src := range f.sources {
rc, err := src.Open()
if err != nil {
log.Error(err)
return false
}
defer rc.Close()
_, err = io.Copy(h, rc)
if err != nil {
log.Error(err)
return false
}
rc.Close()
}
s := h.Sum(nil)
ck = bytes.Compare(s, f.hash) != 0
f.hash = s
return
}

// Load in the files and set the denylist and allowlist if no errors are encountered
func (f *Filter) Load() error {
denylist := NewPatternMatcher()
allowlist := NewPatternMatcher()
allowCIDR4list := ranger.NewPCTrieRanger()
allowCIDR6list := ranger.NewPCTrieRanger()
denyCIDR4list := ranger.NewPCTrieRanger()
denyCIDR6list := ranger.NewPCTrieRanger()
for _, src := range f.sources {
rc, err := src.Open()
if err != nil {
return err
}
defer rc.Close()

if src.IsBlock {
if err := f.denylist.LoadRules(rc); err != nil {
return err
if !src.IsCIDR {
if src.IsBlock {
if err := denylist.LoadRules(rc); err != nil {
return err
}
} else {
if err := allowlist.LoadRules(rc); err != nil {
return err
}
}
} else {
if err := f.allowlist.LoadRules(rc); err != nil {
return err
if src.IsBlock {
if err := LoadCIDR(rc, denyCIDR4list, denyCIDR6list); err != nil {
return err
}
} else {
if err := LoadCIDR(rc, allowCIDR4list, allowCIDR6list); err != nil {
return err
}
}
}
rc.Close()
}
f.denylist = denylist
f.allowlist = allowlist
f.allowCIDR4list = allowCIDR4list
f.allowCIDR6list = allowCIDR6list
f.denyCIDR4list = denyCIDR4list
f.denyCIDR6list = denyCIDR6list

return nil
}

Expand All @@ -115,9 +192,11 @@ func (w *ResponseWriter) WriteMsg(m *dns.Msg) error {
return w.ResponseWriter.WriteMsg(m)
}

var answers []dns.RR
for _, r := range m.Answer {
header := r.Header()
if header.Class != dns.ClassINET {
answers = append(answers, r)
continue
}

Expand All @@ -129,7 +208,24 @@ func (w *ResponseWriter) WriteMsg(m *dns.Msg) error {
target = r.(*dns.SVCB).Target //nolint
case dns.TypeHTTPS:
target = r.(*dns.HTTPS).Target //nolint
case dns.TypeA:
ip := r.(*dns.A).A //nolint
if c, err := w.denyCIDR4list.Contains(ip); !c && err == nil {
answers = append(answers, r)
} else if c, err := w.allowCIDR4list.Contains(ip); !c && err == nil {
answers = append(answers, r)
}
continue
case dns.TypeAAAA:
ip := r.(*dns.AAAA).AAAA //nolint
if c, err := w.denyCIDR6list.Contains(ip); !c && err == nil {
answers = append(answers, r)
} else if c, err := w.allowCIDR6list.Contains(ip); !c && err == nil {
answers = append(answers, r)
}
continue
default:
answers = append(answers, r)
continue
}

Expand All @@ -138,10 +234,23 @@ func (w *ResponseWriter) WriteMsg(m *dns.Msg) error {
BlockCount.WithLabelValues(w.server).Inc()

r := w.state.Req
msg := createSyntheticResponse(r, w.ttl)
var msg *dns.Msg
if !w.emptyResponse {
msg = createSyntheticResponse(r, w.ttl)
} else {
msg = newEmptyResponse(r, w.ttl)
}
w.WriteMsg(msg) //nolint
return nil
}
answers = append(answers, r)
}

// If all the answers were stripped away, return server failure. Doing so may make the client retry and get a new set of IPs.
if len(m.Answer) > 0 && len(answers) == 0 {
m.Rcode = dns.RcodeServerFailure
}

m.Answer = answers
return w.ResponseWriter.WriteMsg(m)
}
33 changes: 19 additions & 14 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ require (
github.com/hashicorp/go-immutable-radix v1.3.1
github.com/miekg/dns v1.1.55
github.com/prometheus/client_golang v1.16.0
github.com/yl2chen/cidranger v1.0.2
)

require (
cloud.google.com/go v0.107.0 // indirect
cloud.google.com/go/compute v1.15.1 // indirect
cloud.google.com/go v0.110.2 // indirect
cloud.google.com/go/compute v1.19.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v0.8.0 // indirect
cloud.google.com/go/storage v1.27.0 // indirect
cloud.google.com/go/iam v1.1.0 // indirect
cloud.google.com/go/storage v1.31.0 // indirect
github.com/apparentlymart/go-cidr v1.1.0 // indirect
github.com/aws/aws-sdk-go v1.44.194 // indirect
github.com/beorn7/perks v1.0.1 // indirect
Expand All @@ -26,9 +27,10 @@ require (
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/s2a-go v0.1.4 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.1 // indirect
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.11.0 // indirect
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // indirect
Expand All @@ -46,16 +48,19 @@ require (
github.com/prometheus/procfs v0.10.1 // indirect
github.com/ulikunitz/xz v0.5.10 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/oauth2 v0.5.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/tools v0.4.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.6.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/api v0.109.0 // indirect
google.golang.org/api v0.126.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
google.golang.org/grpc v1.53.0 // indirect
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/grpc v1.55.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
)
Loading