mirror of
https://github.com/folbricht/routedns.git
synced 2026-01-06 09:40:03 -06:00
Support allowlists as part of blocklists to override rules
This commit is contained in:
112
blocklist.go
112
blocklist.go
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
27
cmd/routedns/example-config/blocklist-allow.toml
Normal file
27
cmd/routedns/example-config/blocklist-allow.toml
Normal 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"
|
||||
25
cmd/routedns/example-config/blocklist-remote-allow.toml
Normal file
25
cmd/routedns/example-config/blocklist-remote-allow.toml
Normal 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"
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user