Support allowlists as part of blocklists to override rules

This commit is contained in:
folbrich
2020-04-16 22:03:32 -06:00
parent 5b46c2c6a1
commit 387618a585
7 changed files with 214 additions and 35 deletions

View File

@@ -14,33 +14,68 @@ import (
// Blocklist is a resolver that returns NXDOMAIN or a spoofed IP for every query that
// matches. Everything else is passed through to another resolver.
type Blocklist struct {
BlocklistOptions
resolver Resolver
db BlocklistDB
loader BlocklistLoader
mu sync.Mutex
mu sync.RWMutex
}
var _ Resolver = &Blocklist{}
// NewBlocklist returns a new instance of a blocklist resolver. If a non-nil loader is provided
// the rules are loaded from it immediately into the DB. If refresh is >0, the rules are reloaded
// periodically.
func NewBlocklist(resolver Resolver, db BlocklistDB, l BlocklistLoader, refresh time.Duration) (*Blocklist, error) {
if l != nil {
rules, err := l.Load()
type BlocklistOptions struct {
BlocklistDB BlocklistDB
// Loader for the blacklist during initialization and refresh. Disabled
// if nil.
BlocklistLoader BlocklistLoader
// Refresh period for the blocklist. Disabled if 0.
BlocklistRefresh time.Duration
// Rules that override the blocklist rules, effecively negate them.
AllowlistDB BlocklistDB
// Loader for the allowlist during initialization and refresh. Disabled
// if nil.
AllowlistLoader BlocklistLoader
// Refresh period for the allowlist. Disabled if 0.
AllowlistRefresh time.Duration
}
// NewBlocklist returns a new instance of a blocklist resolver.
func NewBlocklist(resolver Resolver, opt BlocklistOptions) (*Blocklist, error) {
blocklist := &Blocklist{resolver: resolver, BlocklistOptions: opt}
// Load the blocklist immediately if a loader was given
if blocklist.BlocklistLoader != nil {
rules, err := blocklist.BlocklistLoader.Load()
if err != nil {
return nil, fmt.Errorf("failed to retrieve rules: %w", err)
}
db, err = db.New(rules)
blocklist.BlocklistDB, err = blocklist.BlocklistDB.New(rules)
if err != nil {
return nil, fmt.Errorf("failed to load rules: %w", err)
}
}
blocklist := &Blocklist{resolver: resolver, db: db, loader: l}
// Start the refresh goroutine if we have a loader and a refresh period was given
if l != nil && refresh > 0 {
go blocklist.refreshLoop(refresh)
// Load the allowlist immediately if a loader was given
if blocklist.AllowlistLoader != nil {
rules, err := blocklist.AllowlistLoader.Load()
if err != nil {
return nil, fmt.Errorf("failed to retrieve rules: %w", err)
}
blocklist.AllowlistDB, err = blocklist.AllowlistDB.New(rules)
if err != nil {
return nil, fmt.Errorf("failed to load rules: %w", err)
}
}
// Start the refresh goroutines if we have a loader and a refresh period was given
if blocklist.BlocklistLoader != nil && blocklist.BlocklistRefresh > 0 {
go blocklist.refreshLoopBlocklist(blocklist.BlocklistRefresh)
}
if blocklist.AllowlistLoader != nil && blocklist.AllowlistRefresh > 0 {
go blocklist.refreshLoopAllowlist(blocklist.AllowlistRefresh)
}
return blocklist, nil
}
@@ -53,7 +88,21 @@ func (r *Blocklist) Resolve(q *dns.Msg, ci ClientInfo) (*dns.Msg, error) {
}
question := q.Question[0]
log := Log.WithFields(logrus.Fields{"client": ci.SourceIP, "qname": question.Name})
ip, rule, ok := r.db.Match(question)
r.mu.RLock()
blocklistDB := r.BlocklistDB
allowlistDB := r.AllowlistDB
r.mu.RUnlock()
// Forward to upstream immediately if there's a match in the allowlist
if allowlistDB != nil {
if _, rule, ok := allowlistDB.Match(question); ok {
log.WithField("rule", rule).WithField("resolver", r.resolver.String()).Trace("matched allowlist, forwarding")
return r.resolver.Resolve(q, ci)
}
}
ip, rule, ok := blocklistDB.Match(question)
if !ok {
// Didn't match anything, pass it on to the next resolver
log.WithField("resolver", r.resolver.String()).Trace("forwarding unmodified query to resolver")
@@ -102,26 +151,49 @@ func (r *Blocklist) Resolve(q *dns.Msg, ci ClientInfo) (*dns.Msg, error) {
}
func (r *Blocklist) String() string {
return fmt.Sprintf("Blocklist(%s)", r.db)
r.mu.RLock()
blocklistDB := r.BlocklistDB
r.mu.RUnlock()
return fmt.Sprintf("Blocklist(%s)", blocklistDB)
}
func (r *Blocklist) refreshLoop(refresh time.Duration) {
func (r *Blocklist) refreshLoopBlocklist(refresh time.Duration) {
for {
time.Sleep(refresh)
Log.Debug("reloading blocklist")
rules, err := r.loader.Load()
rules, err := r.BlocklistLoader.Load()
if err != nil {
Log.WithError(err).Error("failed to retrieve rules")
continue
}
db, err := r.db.New(rules)
db, err := r.BlocklistDB.New(rules)
if err != nil {
Log.WithError(err).Error("failed to load rules")
continue
}
r.mu.Lock()
r.db = db
r.BlocklistDB = db
r.mu.Unlock()
}
}
func (r *Blocklist) refreshLoopAllowlist(refresh time.Duration) {
for {
time.Sleep(refresh)
Log.Debug("reloading allowlist")
rules, err := r.AllowlistLoader.Load()
if err != nil {
Log.WithError(err).Error("failed to retrieve rules")
continue
}
db, err := r.AllowlistDB.New(rules)
if err != nil {
Log.WithError(err).Error("failed to load rules")
continue
}
r.mu.Lock()
r.AllowlistDB = db
r.mu.Unlock()
}
}

View File

@@ -15,7 +15,10 @@ func TestBlocklistRegexp(t *testing.T) {
m, err := NewRegexpDB(`(^|\.)block\.test`, `(^|\.)evil\.test`)
require.NoError(t, err)
b, err := NewBlocklist(r, m, nil, 0)
opt := BlocklistOptions{
BlocklistDB: m,
}
b, err := NewBlocklist(r, opt)
require.NoError(t, err)
// First query a domain not blocked. Should be passed through to the resolver

View File

@@ -47,16 +47,22 @@ type doh struct {
type group struct {
Resolvers []string
Type string
Blocklist []string // Blocklist rules, only used by "blocklist" type
Format string // Blocklist input format: "regex", "domain", or "hosts"
Source string // Location of external blocklist, can be a local path or remote URL
Refresh int // Blocklist refresh when using an external source, in seconds
Replace []rdns.ReplaceOperation // only used by "replace" type
GCPeriod int `toml:"gc-period"` // Time-period (seconds) used to expire cached items in the "cache" type
ECSOp string `toml:"ecs-op"` // ECS modifier operation, "add", "delete", "privacy"
ECSAddress net.IP `toml:"ecs-address"` // ECS address. If empty for "add", uses the client IP. Ignored for "privacy" and "delete"
ECSPrefix4 uint8 `toml:"ecs-prefix4"` // ECS IPv4 address prefix, 0-32. Used for "add" and "privacy"
ECSPrefix6 uint8 `toml:"ecs-prefix6"` // ECS IPv6 address prefix, 0-128. Used for "add" and "privacy"
// Blocklist options
Blocklist []string // Blocklist rules, only used by "blocklist" type
Format string // Blocklist input format: "regex", "domain", or "hosts"
Source string // Location of external blocklist, can be a local path or remote URL
Refresh int // Blocklist refresh when using an external source, in seconds
Allowlist []string // Rules to override the blocklist rules
AllowlistFormat string `toml:"allowlist-format"`
AllowlistSource string `toml:"allowlist-source"`
AllowlistRefresh int `toml:"allowlist-refresh"`
}
type router struct {

View File

@@ -0,0 +1,27 @@
[resolvers.cloudflare-dot]
address = "1.1.1.1:853"
protocol = "dot"
[groups.cloudflare-blocklist]
type = "blocklist"
resolvers = ["cloudflare-dot"]
format = "domain"
blocklist = [
'evil.com',
'.facebook.com',
'*.twitter.com',
]
allowlist-format = "domain"
allowlist = [ # Allowlist items override/bypass blocklist items
'allowed.facebook.com',
]
[listeners.local-udp]
address = ":53"
protocol = "udp"
resolver = "cloudflare-blocklist"
[listeners.local-tcp]
address = ":53"
protocol = "tcp"
resolver = "cloudflare-blocklist"

View File

@@ -0,0 +1,25 @@
# Config with a remote blocklist that is refreshed once a day and also a remote
# allowlist that overrides any blocking rules
[resolvers.cloudflare-dot]
address = "1.1.1.1:853"
protocol = "dot"
[groups.cloudflare-blocklist]
type = "blocklist"
resolvers = ["cloudflare-dot"]
format = "regexp"
source = "https://raw.githubusercontent.com/cbuijs/accomplist/master/deugniets/routedns.regexp.list"
refresh = 86400
allowlist-format = "domain"
allowlist-source = "https://some.other.list"
allowlist-refresh = 86400
[listeners.local-udp]
address = ":53"
protocol = "udp"
resolver = "cloudflare-blocklist"
[listeners.local-tcp]
address = ":53"
protocol = "tcp"
resolver = "cloudflare-blocklist"

View File

@@ -6,7 +6,7 @@ protocol = "dot"
type = "blocklist"
resolvers = ["cloudflare-dot"] # Anything that passes the filter is sent on to this resolver
format = "regexp" # "domain", "hosts" or "regexp", defaults to "regexp"
source = "https://raw.githubusercontent.com/cbuijs/accomplist/master/deugniets/plain.black.regex.list"
source = "https://raw.githubusercontent.com/cbuijs/accomplist/master/deugniets/routedns.regexp.list"
refresh = 86400 # Time to refresh the blocklist from the URL in seconds
[listeners.local-udp]

View File

@@ -238,9 +238,12 @@ func instantiateGroup(id string, g group, resolvers map[string]rdns.Resolver) er
return fmt.Errorf("type blocklist only supports one resolver in '%s'", id)
}
if len(g.Blocklist) > 0 && g.Source != "" {
return fmt.Errorf("type static blocklist can't be used with 'source' in '%s'", id)
return fmt.Errorf("static blocklist can't be used with 'source' in '%s'", id)
}
var loader rdns.BlocklistLoader
if len(g.Allowlist) > 0 && g.AllowlistSource != "" {
return fmt.Errorf("static allowlist can't be used with 'source' in '%s'", id)
}
var blocklistLoader rdns.BlocklistLoader
if g.Source != "" {
loc, err := url.Parse(g.Source)
if err != nil {
@@ -248,34 +251,77 @@ func instantiateGroup(id string, g group, resolvers map[string]rdns.Resolver) er
}
switch loc.Scheme {
case "http", "https":
loader = rdns.NewHTTPLoader(g.Source)
blocklistLoader = rdns.NewHTTPLoader(g.Source)
case "":
loader = rdns.NewFileLoader(g.Source)
blocklistLoader = rdns.NewFileLoader(g.Source)
default:
return fmt.Errorf("unsupported scheme '%s' in '%s'", loc.Scheme, g.Source)
}
}
var db rdns.BlocklistDB
var allowlistLoader rdns.BlocklistLoader
if g.AllowlistSource != "" {
loc, err := url.Parse(g.AllowlistSource)
if err != nil {
return err
}
switch loc.Scheme {
case "http", "https":
allowlistLoader = rdns.NewHTTPLoader(g.AllowlistSource)
case "":
allowlistLoader = rdns.NewFileLoader(g.AllowlistSource)
default:
return fmt.Errorf("unsupported scheme '%s' in '%s'", loc.Scheme, g.AllowlistSource)
}
}
var blocklistDB rdns.BlocklistDB
switch g.Format {
case "regexp", "":
db, err = rdns.NewRegexpDB(g.Blocklist...)
blocklistDB, err = rdns.NewRegexpDB(g.Blocklist...)
if err != nil {
return err
}
case "domain":
db, err = rdns.NewDomainDB(g.Blocklist...)
blocklistDB, err = rdns.NewDomainDB(g.Blocklist...)
if err != nil {
return err
}
case "hosts":
db, err = rdns.NewHostsDB(g.Blocklist...)
blocklistDB, err = rdns.NewHostsDB(g.Blocklist...)
if err != nil {
return err
}
default:
return fmt.Errorf("unsupported blocklist format '%s'", g.Format)
}
resolvers[id], err = rdns.NewBlocklist(gr[0], db, loader, time.Duration(g.Refresh)*time.Second)
var allowlistDB rdns.BlocklistDB
switch g.AllowlistFormat {
case "regexp", "":
allowlistDB, err = rdns.NewRegexpDB(g.Allowlist...)
if err != nil {
return err
}
case "domain":
allowlistDB, err = rdns.NewDomainDB(g.Allowlist...)
if err != nil {
return err
}
case "hosts":
allowlistDB, err = rdns.NewHostsDB(g.Allowlist...)
if err != nil {
return err
}
default:
return fmt.Errorf("unsupported allowlist format '%s'", g.Format)
}
opt := rdns.BlocklistOptions{
BlocklistDB: blocklistDB,
BlocklistLoader: blocklistLoader,
BlocklistRefresh: time.Duration(g.Refresh) * time.Second,
AllowlistDB: allowlistDB,
AllowlistLoader: allowlistLoader,
AllowlistRefresh: time.Duration(g.AllowlistRefresh) * time.Second,
}
resolvers[id], err = rdns.NewBlocklist(gr[0], opt)
if err != nil {
return err
}