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")
}

Check warning on line 16 in cidr_matcher.go

View check run for this annotation

Codecov / codecov/patch

cidr_matcher.go#L13-L16

Added lines #L13 - L16 were not covered by tests
//cr := ranger.NewPCTrieRanger()
scanner := bufio.NewScanner(r)
for scanner.Scan() {
pattern := strings.TrimSpace(scanner.Text())

if pattern == "" || strings.HasPrefix(pattern, "#") {
continue

Check warning on line 23 in cidr_matcher.go

View check run for this annotation

Codecov / codecov/patch

cidr_matcher.go#L18-L23

Added lines #L18 - L23 were not covered by tests
}
if strings.Contains(pattern, "#") {
i := strings.Index(pattern, "#")
pattern = strings.TrimSpace(pattern[:i])
}

Check warning on line 28 in cidr_matcher.go

View check run for this annotation

Codecov / codecov/patch

cidr_matcher.go#L25-L28

Added lines #L25 - L28 were not covered by tests

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

Check warning on line 43 in cidr_matcher.go

View check run for this annotation

Codecov / codecov/patch

cidr_matcher.go#L30-L43

Added lines #L30 - L43 were not covered by tests
}
}

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

Check warning on line 51 in cidr_matcher.go

View check run for this annotation

Codecov / codecov/patch

cidr_matcher.go#L47-L51

Added lines #L47 - L51 were not covered by tests

if scanner.Err() != nil {
return scanner.Err()
}

Check warning on line 55 in cidr_matcher.go

View check run for this annotation

Codecov / codecov/patch

cidr_matcher.go#L53-L55

Added lines #L53 - L55 were not covered by tests
}
return nil

Check warning on line 57 in cidr_matcher.go

View check run for this annotation

Codecov / codecov/patch

cidr_matcher.go#L57

Added line #L57 was not covered by tests
}
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 @@
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 @@

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 @@
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)
}

Check warning on line 76 in filter.go

View check run for this annotation

Codecov / codecov/patch

filter.go#L75-L76

Added lines #L75 - L76 were not covered by tests
w.WriteMsg(msg) //nolint
return dns.RcodeSuccess, nil
}
Expand Down Expand Up @@ -75,24 +102,74 @@
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()

Check warning on line 120 in filter.go

View check run for this annotation

Codecov / codecov/patch

filter.go#L106-L120

Added lines #L106 - L120 were not covered by tests
}
s := h.Sum(nil)
ck = bytes.Compare(s, f.hash) != 0
f.hash = s
return

Check warning on line 125 in filter.go

View check run for this annotation

Codecov / codecov/patch

filter.go#L122-L125

Added lines #L122 - L125 were not covered by tests
}

// 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
}

Check warning on line 147 in filter.go

View check run for this annotation

Codecov / codecov/patch

filter.go#L146-L147

Added lines #L146 - L147 were not covered by tests
} else {
if err := allowlist.LoadRules(rc); err != nil {
return err
}

Check warning on line 151 in filter.go

View check run for this annotation

Codecov / codecov/patch

filter.go#L150-L151

Added lines #L150 - L151 were not covered by tests
}
} 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
}

Check warning on line 161 in filter.go

View check run for this annotation

Codecov / codecov/patch

filter.go#L154-L161

Added lines #L154 - L161 were not covered by tests
}
}
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 @@
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)

Check warning on line 199 in filter.go

View check run for this annotation

Codecov / codecov/patch

filter.go#L199

Added line #L199 was not covered by tests
continue
}

Expand All @@ -129,7 +208,24 @@
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

Check warning on line 226 in filter.go

View check run for this annotation

Codecov / codecov/patch

filter.go#L211-L226

Added lines #L211 - L226 were not covered by tests
default:
answers = append(answers, r)

Check warning on line 228 in filter.go

View check run for this annotation

Codecov / codecov/patch

filter.go#L228

Added line #L228 was not covered by tests
continue
}

Expand All @@ -138,10 +234,23 @@
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)
}

Check warning on line 242 in filter.go

View check run for this annotation

Codecov / codecov/patch

filter.go#L241-L242

Added lines #L241 - L242 were not covered by tests
w.WriteMsg(msg) //nolint
return nil
}
answers = append(answers, r)

Check warning on line 246 in filter.go

View check run for this annotation

Codecov / codecov/patch

filter.go#L246

Added line #L246 was not covered by tests
}

// 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
}

Check warning on line 252 in filter.go

View check run for this annotation

Codecov / codecov/patch

filter.go#L250-L252

Added lines #L250 - L252 were not covered by tests

m.Answer = answers

Check warning on line 254 in filter.go

View check run for this annotation

Codecov / codecov/patch

filter.go#L254

Added line #L254 was not covered by tests
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