safing-portmaster/nameserver/nsutil/nsutil.go

197 lines
5.7 KiB
Go

package nsutil
import (
"context"
"errors"
"fmt"
"strings"
"github.com/miekg/dns"
"github.com/safing/portbase/log"
)
var (
// ErrNilRR is returned when a parsed RR is nil.
ErrNilRR = errors.New("is nil")
)
// Responder defines the interface that any block/deny reason interface
// may implement to support sending custom DNS responses for a given reason.
// That is, if a reason context implements the Responder interface the
// ReplyWithDNS method will be called instead of creating the default
// zero-ip response.
type Responder interface {
// ReplyWithDNS is called when a DNS response to a DNS message is
// crafted because the request is either denied or blocked.
ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns.Msg
}
// RRProvider defines the interface that any block/deny reason interface
// may implement to support adding additional DNS resource records to
// the DNS responses extra (additional) section.
type RRProvider interface {
// GetExtraRRs is called when a DNS response to a DNS message is
// crafted because the request is either denied or blocked.
GetExtraRRs(ctx context.Context, request *dns.Msg) []dns.RR
}
// ResponderFunc is a convenience type to use a function
// directly as a Responder.
type ResponderFunc func(ctx context.Context, request *dns.Msg) *dns.Msg
// ReplyWithDNS implements the Responder interface and calls rf.
func (rf ResponderFunc) ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns.Msg {
return rf(ctx, request)
}
// ZeroIP is a ResponderFunc than replies with either 0.0.0.0 or :: for each A
// or AAAA question respectively. If there is no A or AAAA question, it
// defaults to replying with NXDomain.
func ZeroIP(msgs ...string) ResponderFunc {
return func(ctx context.Context, request *dns.Msg) *dns.Msg {
reply := new(dns.Msg)
hasErr := false
for _, question := range request.Question {
var rr dns.RR
var err error
switch question.Qtype {
case dns.TypeA:
rr, err = dns.NewRR(question.Name + " 1 IN A 0.0.0.17")
case dns.TypeAAAA:
rr, err = dns.NewRR(question.Name + " 1 IN AAAA ::17")
}
switch {
case err != nil:
log.Tracer(ctx).Errorf("nameserver: failed to create zero-ip response for %s: %s", question.Name, err)
hasErr = true
case rr != nil:
reply.Answer = append(reply.Answer, rr)
}
}
switch {
case hasErr && len(reply.Answer) == 0:
reply.SetRcode(request, dns.RcodeServerFailure)
case len(reply.Answer) == 0:
reply.SetRcode(request, dns.RcodeNameError)
default:
reply.SetRcode(request, dns.RcodeSuccess)
}
AddMessagesToReply(ctx, reply, log.InfoLevel, msgs...)
return reply
}
}
// Localhost is a ResponderFunc than replies with localhost IP addresses.
// If there is no A or AAAA question, it defaults to replying with NXDomain.
func Localhost(msgs ...string) ResponderFunc {
return func(ctx context.Context, request *dns.Msg) *dns.Msg {
reply := new(dns.Msg)
hasErr := false
for _, question := range request.Question {
var rr dns.RR
var err error
switch question.Qtype {
case dns.TypeA:
rr, err = dns.NewRR("localhost. 1 IN A 127.0.0.1")
case dns.TypeAAAA:
rr, err = dns.NewRR("localhost. 1 IN AAAA ::1")
}
switch {
case err != nil:
log.Tracer(ctx).Errorf("nameserver: failed to create localhost response for %s: %s", question.Name, err)
hasErr = true
case rr != nil:
reply.Answer = append(reply.Answer, rr)
}
}
switch {
case hasErr && len(reply.Answer) == 0:
reply.SetRcode(request, dns.RcodeServerFailure)
case len(reply.Answer) == 0:
reply.SetRcode(request, dns.RcodeNameError)
default:
reply.SetRcode(request, dns.RcodeSuccess)
}
AddMessagesToReply(ctx, reply, log.InfoLevel, msgs...)
return reply
}
}
// NxDomain returns a ResponderFunc that replies with NXDOMAIN.
func NxDomain(msgs ...string) ResponderFunc {
return func(ctx context.Context, request *dns.Msg) *dns.Msg {
reply := new(dns.Msg).SetRcode(request, dns.RcodeNameError)
AddMessagesToReply(ctx, reply, log.InfoLevel, msgs...)
return reply
}
}
// Refused returns a ResponderFunc that replies with REFUSED.
func Refused(msgs ...string) ResponderFunc {
return func(ctx context.Context, request *dns.Msg) *dns.Msg {
reply := new(dns.Msg).SetRcode(request, dns.RcodeRefused)
AddMessagesToReply(ctx, reply, log.InfoLevel, msgs...)
return reply
}
}
// ServerFailure returns a ResponderFunc that replies with SERVFAIL.
func ServerFailure(msgs ...string) ResponderFunc {
return func(ctx context.Context, request *dns.Msg) *dns.Msg {
reply := new(dns.Msg).SetRcode(request, dns.RcodeServerFailure)
AddMessagesToReply(ctx, reply, log.InfoLevel, msgs...)
return reply
}
}
// MakeMessageRecord creates an informational resource record that can be added
// to the extra section of a reply.
func MakeMessageRecord(level log.Severity, msg string) (dns.RR, error) { //nolint:interfacer
rr, err := dns.NewRR(fmt.Sprintf(
`%s.portmaster. 0 IN TXT "%s"`,
strings.ToLower(level.String()),
msg,
))
if err != nil {
return nil, err
}
if rr == nil {
return nil, ErrNilRR
}
return rr, nil
}
// AddMessagesToReply creates information resource records using
// MakeMessageRecord and immediately adds them to the extra section of the given
// reply. If an error occurs, the resource record will not be added, and the
// error will be logged.
func AddMessagesToReply(ctx context.Context, reply *dns.Msg, level log.Severity, msgs ...string) {
for _, msg := range msgs {
// Ignore empty messages.
if msg == "" {
continue
}
// Create resources record.
rr, err := MakeMessageRecord(level, msg)
if err != nil {
log.Tracer(ctx).Warningf("nameserver: failed to add message to reply: %s", err)
continue
}
// Add to extra section of the reply.
reply.Extra = append(reply.Extra, rr)
}
}