Files
routedns/response-blocklist-name.go
2025-01-14 07:06:13 +01:00

139 lines
3.9 KiB
Go

package rdns
import (
"strings"
"sync"
"time"
"github.com/miekg/dns"
)
// ResponseBlocklistName is a resolver that filters by matching the strings in CNAME, MX,
// NS, PTR and SRV response records against a blocklist.
type ResponseBlocklistName struct {
id string
ResponseBlocklistNameOptions
resolver Resolver
mu sync.RWMutex
}
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.
BlocklistRefresh time.Duration
// Inverted behavior, only allow responses that can be found on at least one list.
Inverted bool
// Optional, allows specifying extended errors to be used in the
// response when blocking.
EDNS0EDETemplate *EDNS0EDETemplate
}
// NewResponseBlocklistName returns a new instance of a response blocklist resolver.
func NewResponseBlocklistName(id string, resolver Resolver, opt ResponseBlocklistNameOptions) (*ResponseBlocklistName, error) {
blocklist := &ResponseBlocklistName{id: id, resolver: resolver, ResponseBlocklistNameOptions: opt}
// 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 by first querying the upstream resolver, then checking any responses with
// strings against a blocklist. Responds with NXDOMAIN if the response matches the filter.
func (r *ResponseBlocklistName) Resolve(q *dns.Msg, ci ClientInfo) (*dns.Msg, error) {
answer, err := r.resolver.Resolve(q, ci)
if err != nil || answer == nil {
return answer, err
}
return r.blockIfMatch(q, answer, ci)
}
func (r *ResponseBlocklistName) String() string {
return r.id
}
func (r *ResponseBlocklistName) refreshLoopBlocklist(refresh time.Duration) {
for {
time.Sleep(refresh)
log := Log.With("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 = db
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
case *dns.HTTPS:
name = svcbString(&r.SVCB)
case *dns.TXT:
name = strings.Join(r.Txt, " ")
case *dns.SVCB:
name = svcbString(r)
case *dns.SOA:
name = r.Ns
default:
continue
}
msg := new(dns.Msg)
msg.SetQuestion(name, 0)
if _, _, rule, ok := r.BlocklistDB.Match(msg); ok != r.Inverted {
log := logger(r.id, query, ci).With("rule", rule.GetRule())
if r.BlocklistResolver != nil {
log.With("resolver", r.BlocklistResolver).Debug("blocklist match, forwarding to blocklist-resolver")
return r.BlocklistResolver.Resolve(query, ci)
}
log.Debug("blocking response")
answer = nxdomain(query)
if err := r.EDNS0EDETemplate.Apply(answer, EDNS0EDEInput{query, rule}); err != nil {
log.Error("failed to apply edns0ede template", "error", err)
}
return answer, nil
}
}
}
return answer, nil
}
// Format an SVCB (and HTTPS) record as string like so "TARGET key1=value1 key2=value2"
// For example: ". alpn=h2,h3"
func svcbString(rr *dns.SVCB) string {
var s strings.Builder
s.WriteString(rr.Target)
for _, v := range rr.Value {
s.WriteString(" ")
s.WriteString(v.Key().String())
s.WriteString("=")
s.WriteString(v.String())
}
return s.String()
}