mirror of
https://github.com/folbricht/routedns.git
synced 2025-12-20 17:09:37 -06:00
119 lines
3.0 KiB
Go
119 lines
3.0 KiB
Go
package rdns
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"log/slog"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
// ClientBlocklist is a resolver that matches the IPs of clients against a blocklist
|
|
type ClientBlocklist struct {
|
|
id string
|
|
ClientBlocklistOptions
|
|
resolver Resolver
|
|
mu sync.RWMutex
|
|
metrics *BlocklistMetrics
|
|
}
|
|
|
|
var _ Resolver = &ClientBlocklist{}
|
|
|
|
type ClientBlocklistOptions struct {
|
|
// Optional, if the client 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
|
|
|
|
// Use the provided ECS address instead of the real client IP if one was
|
|
// provided. This can be used to "test" blocklists by simulating different
|
|
// client IPs.
|
|
UseECS bool
|
|
}
|
|
|
|
// NewClientBlocklistIP returns a new instance of a client blocklist resolver.
|
|
func NewClientBlocklist(id string, resolver Resolver, opt ClientBlocklistOptions) (*ClientBlocklist, error) {
|
|
blocklist := &ClientBlocklist{
|
|
id: id,
|
|
resolver: resolver,
|
|
ClientBlocklistOptions: opt,
|
|
metrics: NewBlocklistMetrics(id),
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
return blocklist, nil
|
|
}
|
|
|
|
// Resolve a DNS query after checking the client's IP against a blocklist. Responds with
|
|
// REFUSED if the client IP is on the blocklist, or sends the query to an alternative
|
|
// resolver if one is configured.
|
|
func (r *ClientBlocklist) Resolve(q *dns.Msg, ci ClientInfo) (*dns.Msg, error) {
|
|
ip := ci.SourceIP
|
|
|
|
// Use the ECS IP if one is available and useECS is set
|
|
if r.UseECS {
|
|
edns0 := q.IsEdns0()
|
|
if edns0 != nil {
|
|
for _, opt := range edns0.Option {
|
|
if ecs, ok := opt.(*dns.EDNS0_SUBNET); ok {
|
|
ip = ecs.Address
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if match, ok := r.BlocklistDB.Match(ip); ok {
|
|
log := Log.With(
|
|
slog.String("id", r.id),
|
|
slog.String("qname", qName(q)),
|
|
slog.String("list", match.List),
|
|
slog.String("rule", match.Rule),
|
|
slog.String("ip", ci.SourceIP.String()),
|
|
)
|
|
r.metrics.blocked.Add(1)
|
|
if r.BlocklistResolver != nil {
|
|
log.With(
|
|
slog.String("resolver", r.BlocklistResolver.String()),
|
|
).Debug("client on blocklist, forwarding to blocklist-resolver")
|
|
return r.BlocklistResolver.Resolve(q, ci)
|
|
}
|
|
log.Debug("blocking client")
|
|
return refused(q), nil
|
|
}
|
|
|
|
r.metrics.allowed.Add(1)
|
|
return r.resolver.Resolve(q, ci)
|
|
}
|
|
|
|
func (r *ClientBlocklist) String() string {
|
|
return r.id
|
|
}
|
|
|
|
func (r *ClientBlocklist) refreshLoopBlocklist(refresh time.Duration) {
|
|
for {
|
|
time.Sleep(refresh)
|
|
log := Log.With(
|
|
slog.String("id", r.id),
|
|
)
|
|
log.Debug("reloading blocklist")
|
|
db, err := r.BlocklistDB.Reload()
|
|
if err != nil {
|
|
log.Error("failed to load rules",
|
|
"error", err)
|
|
continue
|
|
}
|
|
r.mu.Lock()
|
|
r.BlocklistDB.Close()
|
|
r.BlocklistDB = db
|
|
r.mu.Unlock()
|
|
}
|
|
}
|