mirror of
https://github.com/folbricht/routedns.git
synced 2026-04-30 21:19:13 -05:00
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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user