Allow filtering of responses in addition to blocking (#26)

* Allow filtering of responses in addition to blocking

* Update example

* Remove filter support from name response blocklist, support blocklist-resolver
This commit is contained in:
Frank Olbricht
2020-05-22 07:21:23 -06:00
committed by GitHub
parent 0d900dd35e
commit 82b5b536b7
10 changed files with 194 additions and 51 deletions
+1 -1
View File
@@ -306,7 +306,7 @@ Rather than filtering queries, response blocklists evaluate the response to a qu
- `response-blocklist-cidr` blocks backed on IP networks (in CIDR notation) on A or AAAA responses.
- `response-blocklist-name` filters based on domain names in CNAME, MX, NS, PRT and SRV records.
The configuration options of response blocklists are very similar to that of [query blocklists](#Queryblocklists) with the exception of the allowlists options which are not currently supported.
The configuration options of response blocklists are very similar to that of [query blocklists](#Queryblocklists) with the exception of the allowlists options which are not currently supported. If the `filter` option is set to `true` in `response-blocklist-cidr`, matching records will be removed from responses rather than the whole response. If there is no answer record left after applying the filter, NXDOMAIN will be returned.
Examples of simple response blocklists with static rules in the configuration file.
+1
View File
@@ -61,6 +61,7 @@ type group struct {
Refresh int // Blocklist refresh when using an external source, in seconds
// Blocklist-v2 options
Filter bool // Filter response records rather than return NXDOMAIN
BlockListResolver string `toml:"blocklist-resolver"`
AllowListResolver string `toml:"allowlist-resolver"`
BlocklistFormat string `toml:"blocklist-format"` // only used for static blocklists in the config
@@ -0,0 +1,32 @@
# Shows how to use an alternative blocklist-resolver with a response blocklist.
# 1) the query is sent to the default resolver ("default-do")
# 2) if the response contains an item on the blocklist and a blocklist-resolver
# is configured, the query will be sent there instead. Note, the response from
# blocklist-resolver is not re-evaluated.
[resolvers.default-dot]
address = "1.1.1.1:853"
protocol = "dot"
[resolvers.alternate-dot]
address = "8.8.8.8:853"
protocol = "dot"
[groups.cloudflare-blocklist]
type = "response-blocklist-cidr"
resolvers = ["default-dot"] # Default resolver, all queries are sent here first
blocklist-resolver = "alternate-dot" # Use this resolver when a response matches the blocklist
blocklist = [
'127.0.0.0/24',
'157.240.0.0/16',
]
[listeners.local-udp]
address = ":53"
protocol = "udp"
resolver = "cloudflare-blocklist"
[listeners.local-tcp]
address = ":53"
protocol = "tcp"
resolver = "cloudflare-blocklist"
@@ -9,6 +9,7 @@ blocklist = [
'127.0.0.0/24',
'157.240.0.0/16',
]
#filter = true # Set to true if the response RRs should be filtered rather than returning NXDOMAIN (default)
[listeners.local-udp]
address = ":53"
@@ -0,0 +1,33 @@
# Shows how to use an alternative blocklist-resolver with a response blocklist.
# 1) the query is sent to the default resolver ("default-do")
# 2) if the response contains an item on the blocklist and a blocklist-resolver
# is configured, the query will be sent there instead. Note, the response from
# blocklist-resolver is not re-evaluated.
[resolvers.default-dot]
address = "1.1.1.1:853"
protocol = "dot"
[resolvers.alternate-dot]
address = "8.8.8.8:853"
protocol = "dot"
[groups.cloudflare-blocklist]
type = "response-blocklist-name"
resolvers = ["default-dot"] # Default resolver, all queries are sent here first
blocklist-resolver = "alternate-dot" # Use this resolver when a response matches the blocklist
blocklist-format = "domain"
blocklist = [
'ns.evil.com.',
'*.acme.test',
]
[listeners.local-udp]
address = ":53"
protocol = "udp"
resolver = "cloudflare-blocklist"
[listeners.local-tcp]
address = ":53"
protocol = "tcp"
resolver = "cloudflare-blocklist"
@@ -7,7 +7,7 @@ type = "response-blocklist-name"
resolvers = ["cloudflare-dot"]
blocklist-format = "domain"
blocklist = [
'ns.evil.com',
'ns.evil.com.',
'*.acme.test',
]
+7 -4
View File
@@ -380,8 +380,10 @@ func instantiateGroup(id string, g group, resolvers map[string]rdns.Resolver) er
}
}
opt := rdns.ResponseBlocklistCIDROptions{
BlocklistDB: blocklistDB,
BlocklistRefresh: time.Duration(g.BlocklistRefresh) * time.Second,
BlocklistResolver: resolvers[g.BlockListResolver],
BlocklistDB: blocklistDB,
BlocklistRefresh: time.Duration(g.BlocklistRefresh) * time.Second,
Filter: g.Filter,
}
resolvers[id], err = rdns.NewResponseBlocklistCIDR(gr[0], opt)
if err != nil {
@@ -415,8 +417,9 @@ func instantiateGroup(id string, g group, resolvers map[string]rdns.Resolver) er
}
}
opt := rdns.ResponseBlocklistNameOptions{
BlocklistDB: blocklistDB,
BlocklistRefresh: time.Duration(g.BlocklistRefresh) * time.Second,
BlocklistResolver: resolvers[g.BlockListResolver],
BlocklistDB: blocklistDB,
BlocklistRefresh: time.Duration(g.BlocklistRefresh) * time.Second,
}
resolvers[id], err = rdns.NewResponseBlocklistName(gr[0], opt)
if err != nil {
+8
View File
@@ -9,3 +9,11 @@ func qName(q *dns.Msg) string {
}
return q.Question[0].Name
}
// Returns a NXDOMAIN answer for a query.
func nxdomain(q *dns.Msg) *dns.Msg {
a := new(dns.Msg)
a.SetReply(q)
a.SetRcode(q, dns.RcodeNameError)
return a
}
+74 -18
View File
@@ -1,6 +1,7 @@
package rdns
import (
"errors"
"fmt"
"net"
"sync"
@@ -27,16 +28,27 @@ type ResponseBlocklistCIDR struct {
var _ Resolver = &ResponseBlocklistCIDR{}
type ResponseBlocklistCIDROptions struct {
// Optional, if the response is found to match the blocklist, send the query to this resolver.
BlocklistResolver Resolver
BlocklistDB IPBlocklistDB
// Refresh period for the blocklist. Disabled if 0.
BlocklistRefresh time.Duration
// If true, removes matching records from the response rather than replying with NXDOMAIN. Can
// not be combined with alternative blockist-resolver
Filter bool
}
// NewResponseBlocklistCIDR returns a new instance of a response blocklist resolver.
func NewResponseBlocklistCIDR(resolver Resolver, opt ResponseBlocklistCIDROptions) (*ResponseBlocklistCIDR, error) {
blocklist := &ResponseBlocklistCIDR{resolver: resolver, ResponseBlocklistCIDROptions: opt}
if opt.Filter && opt.BlocklistResolver != nil {
return nil, errors.New("the 'filter' feature can not be used with 'blocklist-resolver'")
}
// Start the refresh goroutines if we have a list and a refresh period was given
if blocklist.BlocklistDB != nil && blocklist.BlocklistRefresh > 0 {
go blocklist.refreshLoopBlocklist(blocklist.BlocklistRefresh)
@@ -51,25 +63,10 @@ func (r *ResponseBlocklistCIDR) Resolve(q *dns.Msg, ci ClientInfo) (*dns.Msg, er
if err != nil {
return answer, err
}
for _, rr := range answer.Answer {
var ip net.IP
switch r := rr.(type) {
case *dns.A:
ip = r.A
case *dns.AAAA:
ip = r.AAAA
default:
continue
}
if rule, ok := r.BlocklistDB.Match(ip); ok {
Log.WithField("rule", rule).Debug("blocking response")
answer := new(dns.Msg)
answer.SetReply(q)
answer.SetRcode(q, dns.RcodeNameError)
return answer, nil
}
if r.Filter {
return r.filterMatch(q, answer)
}
return answer, err
return r.blockIfMatch(q, answer, ci)
}
func (r *ResponseBlocklistCIDR) String() string {
@@ -93,3 +90,62 @@ func (r *ResponseBlocklistCIDR) refreshLoopBlocklist(refresh time.Duration) {
r.mu.Unlock()
}
}
func (r *ResponseBlocklistCIDR) blockIfMatch(query, answer *dns.Msg, ci ClientInfo) (*dns.Msg, error) {
for _, records := range [][]dns.RR{answer.Answer, answer.Ns, answer.Extra} {
for _, rr := range records {
var ip net.IP
switch r := rr.(type) {
case *dns.A:
ip = r.A
case *dns.AAAA:
ip = r.AAAA
default:
continue
}
if rule, ok := r.BlocklistDB.Match(ip); ok {
log := Log.WithField("rule", rule)
if r.BlocklistResolver != nil {
log.WithField("resolver", r.BlocklistResolver).Debug("blocklist match, forwarding to blocklist-resolver")
return r.BlocklistResolver.Resolve(query, ci)
}
log.Debug("blocking response")
return nxdomain(query), nil
}
}
}
return answer, nil
}
func (r *ResponseBlocklistCIDR) filterMatch(query, answer *dns.Msg) (*dns.Msg, error) {
answer.Answer = r.filterRR(answer.Answer)
// If there's nothing left after applying the filter, return NXDOMAIN
if len(answer.Answer) == 0 {
return nxdomain(query), nil
}
answer.Ns = r.filterRR(answer.Ns)
answer.Extra = r.filterRR(answer.Extra)
return answer, nil
}
func (r *ResponseBlocklistCIDR) filterRR(rrs []dns.RR) []dns.RR {
newRRs := make([]dns.RR, 0, len(rrs))
for _, rr := range rrs {
var ip net.IP
switch r := rr.(type) {
case *dns.A:
ip = r.A
case *dns.AAAA:
ip = r.AAAA
default:
newRRs = append(newRRs, rr)
continue
}
if rule, ok := r.BlocklistDB.Match(ip); ok {
Log.WithField("rule", rule).Debug("filtering response")
continue
}
newRRs = append(newRRs, rr)
}
return newRRs
}
+36 -27
View File
@@ -19,6 +19,9 @@ type ResponseBlocklistName struct {
var _ Resolver = &ResponseBlocklistName{}
type ResponseBlocklistNameOptions struct {
// Optional, if the response is found to match the blocklist, send the query to this resolver.
BlocklistResolver Resolver
BlocklistDB BlocklistDB
// Refresh period for the blocklist. Disabled if 0.
@@ -43,33 +46,7 @@ func (r *ResponseBlocklistName) Resolve(q *dns.Msg, ci ClientInfo) (*dns.Msg, er
if err != nil {
return answer, err
}
for _, records := range [][]dns.RR{answer.Answer, answer.Ns, answer.Extra} {
for _, rr := range records {
var name string
switch r := rr.(type) {
case *dns.CNAME:
name = r.Target
case *dns.MX:
name = r.Mx
case *dns.NS:
name = r.Ns
case *dns.PTR:
name = r.Ptr
case *dns.SRV:
name = r.Target
default:
continue
}
if _, rule, ok := r.BlocklistDB.Match(dns.Question{Name: name}); ok {
Log.WithField("rule", rule).Debug("blocking response")
answer := new(dns.Msg)
answer.SetReply(q)
answer.SetRcode(q, dns.RcodeNameError)
return answer, nil
}
}
}
return answer, err
return r.blockIfMatch(q, answer, ci)
}
func (r *ResponseBlocklistName) String() string {
@@ -93,3 +70,35 @@ func (r *ResponseBlocklistName) refreshLoopBlocklist(refresh time.Duration) {
r.mu.Unlock()
}
}
func (r *ResponseBlocklistName) blockIfMatch(query, answer *dns.Msg, ci ClientInfo) (*dns.Msg, error) {
for _, records := range [][]dns.RR{answer.Answer, answer.Ns, answer.Extra} {
for _, rr := range records {
var name string
switch r := rr.(type) {
case *dns.CNAME:
name = r.Target
case *dns.MX:
name = r.Mx
case *dns.NS:
name = r.Ns
case *dns.PTR:
name = r.Ptr
case *dns.SRV:
name = r.Target
default:
continue
}
if _, rule, ok := r.BlocklistDB.Match(dns.Question{Name: name}); ok {
log := Log.WithField("rule", rule)
if r.BlocklistResolver != nil {
log.WithField("resolver", r.BlocklistResolver).Debug("blocklist match, forwarding to blocklist-resolver")
return r.BlocklistResolver.Resolve(query, ci)
}
log.Debug("blocking response")
return nxdomain(query), nil
}
}
}
return answer, nil
}