Let decision reasons decide on the DNS reply
This commit is contained in:
@@ -3,7 +3,6 @@ package nameserver
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
@@ -13,6 +12,7 @@ import (
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portmaster/detection/dga"
|
||||
"github.com/safing/portmaster/firewall"
|
||||
"github.com/safing/portmaster/nameserver/nsutil"
|
||||
"github.com/safing/portmaster/netenv"
|
||||
"github.com/safing/portmaster/network"
|
||||
"github.com/safing/portmaster/network/netutils"
|
||||
@@ -89,29 +89,6 @@ func stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func returnNXDomain(w dns.ResponseWriter, query *dns.Msg, reason string, reasonContext interface{}) {
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(query, dns.RcodeNameError)
|
||||
rr, _ := dns.NewRR("portmaster.block-reason. 0 IN TXT " + fmt.Sprintf("%q", reason))
|
||||
m.Extra = []dns.RR{rr}
|
||||
|
||||
if reasonContext != nil {
|
||||
if v, ok := reasonContext.(interface {
|
||||
ToRRs() []dns.RR
|
||||
}); ok {
|
||||
m.Extra = append(m.Extra, v.ToRRs()...)
|
||||
} else if v, ok := reasonContext.(interface {
|
||||
ToRR() dns.RR
|
||||
}); ok {
|
||||
m.Extra = append(m.Extra, v.ToRR())
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.WriteMsg(m); err != nil {
|
||||
log.Errorf("nameserver: failed to send response: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func returnServerFailure(w dns.ResponseWriter, query *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(query, dns.RcodeServerFailure)
|
||||
@@ -145,7 +122,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
|
||||
if question.Qclass != dns.ClassINET {
|
||||
// we only serve IN records, return nxdomain
|
||||
log.Warningf("nameserver: only IN record requests are supported but received Qclass %d, returning NXDOMAIN", question.Qclass)
|
||||
returnNXDomain(w, query, "wrong type", nil)
|
||||
sendResponse(w, query, 0, "qclass not served", nsutil.Refused())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -185,7 +162,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
|
||||
// check if valid domain name
|
||||
if !netutils.IsValidFqdn(q.FQDN) {
|
||||
log.Debugf("nameserver: domain name %s is invalid, returning nxdomain", q.FQDN)
|
||||
returnNXDomain(w, query, "invalid domain", nil)
|
||||
sendResponse(w, query, 0, "invalid FQDN", nsutil.Refused())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -224,7 +201,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
|
||||
// NOTE(ppacher): saving unknown process connection might end up in a lot of
|
||||
// processes. Consider disabling that via config.
|
||||
conn.Failed("Unknown process")
|
||||
returnNXDomain(w, query, "unknown process", conn.ReasonContext)
|
||||
sendResponse(w, query, conn.Verdict, conn.Reason, conn.ReasonContext)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -238,7 +215,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
|
||||
if lms < 10 {
|
||||
tracer.Warningf("nameserver: possible data tunnel by %s: %s has lms score of %f, returning nxdomain", conn.Process(), q.FQDN, lms)
|
||||
conn.Block("Possible data tunnel")
|
||||
returnNXDomain(w, query, "lms", conn.ReasonContext)
|
||||
sendResponse(w, query, conn.Verdict, conn.Reason, conn.ReasonContext)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -248,13 +225,34 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
|
||||
switch conn.Verdict {
|
||||
case network.VerdictBlock:
|
||||
tracer.Infof("nameserver: %s blocked, returning nxdomain", conn)
|
||||
returnNXDomain(w, query, conn.Reason, conn.ReasonContext)
|
||||
sendResponse(w, query, conn.Verdict, conn.Reason, conn.ReasonContext)
|
||||
return nil
|
||||
case network.VerdictDrop, network.VerdictFailed:
|
||||
tracer.Infof("nameserver: %s dropped, not replying", conn)
|
||||
return nil
|
||||
}
|
||||
|
||||
// the firewall now decided on the connection and set it to accept
|
||||
// If we have a reason context and that context implements nsutil.Responder
|
||||
// we may need to responde with something else.
|
||||
// A reason for this might be that the request is sink-holed to a forced
|
||||
// ip address in which case we "Accept" it but handle the resolving
|
||||
// differently.
|
||||
if responder, ok := conn.ReasonContext.(nsutil.Responder); ok {
|
||||
tracer.Infof("nameserver: %s handing over to reason-responder: %s", q.FQDN, conn.Reason)
|
||||
reply := responder.ReplyWithDNS(query, conn.Reason, conn.ReasonContext)
|
||||
if err := w.WriteMsg(reply); err != nil {
|
||||
log.Warningf("nameserver: failed to return response %s%s to %s: %s", q.FQDN, q.QType, conn.Process(), err)
|
||||
} else {
|
||||
tracer.Debugf("nameserver: returning response %s%s to %s", q.FQDN, q.QType, conn.Process())
|
||||
}
|
||||
|
||||
// save dns request as open
|
||||
network.SaveOpenDNSRequest(conn)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolve
|
||||
rrCache, err := resolver.Resolve(ctx, q)
|
||||
if err != nil {
|
||||
@@ -267,13 +265,13 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
|
||||
conn.Failed("failed to resolve: " + err.Error())
|
||||
}
|
||||
|
||||
returnNXDomain(w, query, conn.Reason, conn.ReasonContext)
|
||||
sendResponse(w, query, conn.Verdict, conn.Reason, conn.ReasonContext)
|
||||
return nil
|
||||
}
|
||||
|
||||
rrCache = firewall.DecideOnResolvedDNS(conn, q, rrCache)
|
||||
if rrCache == nil {
|
||||
returnNXDomain(w, query, conn.Reason, conn.ReasonContext)
|
||||
sendResponse(w, query, conn.Verdict, conn.Reason, conn.ReasonContext)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
92
nameserver/nsutil/nsutil.go
Normal file
92
nameserver/nsutil/nsutil.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package nsutil
|
||||
|
||||
import (
|
||||
"github.com/miekg/dns"
|
||||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
||||
// 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(query *dns.Msg, reason string, reasonCtx interface{}) *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 {
|
||||
// GetExtraRR is called when a DNS response to a DNS message is
|
||||
// crafted because the request is either denied or blocked.
|
||||
GetExtraRR(query *dns.Msg, reason string, reasonCtx interface{}) []dns.RR
|
||||
}
|
||||
|
||||
// ResponderFunc is a convenience type to use a function
|
||||
// directly as a Responder.
|
||||
type ResponderFunc func(query *dns.Msg, reason string, reasonCtx interface{}) *dns.Msg
|
||||
|
||||
// ReplyWithDNS implements the Responder interface and calls rf.
|
||||
func (rf ResponderFunc) ReplyWithDNS(query *dns.Msg, reason string, reasonCtx interface{}) *dns.Msg {
|
||||
return rf(query, reason, reasonCtx)
|
||||
}
|
||||
|
||||
// ZeroIP is a ResponderFunc than replies with either 0.0.0.0 or :: for
|
||||
// each A or AAAA question respectively.
|
||||
func ZeroIP() ResponderFunc {
|
||||
return func(query *dns.Msg, _ string, _ interface{}) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
hasErr := false
|
||||
|
||||
for _, question := range query.Question {
|
||||
var rr dns.RR
|
||||
var err error
|
||||
|
||||
switch question.Qtype {
|
||||
case dns.TypeA:
|
||||
rr, err = dns.NewRR(question.Name + " 0 IN A 0.0.0.0")
|
||||
case dns.TypeAAAA:
|
||||
rr, err = dns.NewRR(question.Name + " 0 IN AAAA ::")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("nameserver: failed to create zero-ip response for %s: %s", question.Name, err)
|
||||
hasErr = true
|
||||
} else {
|
||||
m.Answer = append(m.Answer, rr)
|
||||
}
|
||||
}
|
||||
|
||||
if hasErr && len(m.Answer) == 0 {
|
||||
m.SetRcode(query, dns.RcodeServerFailure)
|
||||
} else {
|
||||
m.SetRcode(query, dns.RcodeSuccess)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
||||
// NxDomain returns a ResponderFunc that replies with NXDOMAIN.
|
||||
func NxDomain() ResponderFunc {
|
||||
return func(query *dns.Msg, _ string, _ interface{}) *dns.Msg {
|
||||
return new(dns.Msg).SetRcode(query, dns.RcodeNameError)
|
||||
}
|
||||
}
|
||||
|
||||
// Refused returns a ResponderFunc that replies with REFUSED.
|
||||
func Refused() ResponderFunc {
|
||||
return func(query *dns.Msg, _ string, _ interface{}) *dns.Msg {
|
||||
return new(dns.Msg).SetRcode(query, dns.RcodeRefused)
|
||||
}
|
||||
}
|
||||
|
||||
// ServeFail returns a ResponderFunc that replies with SERVFAIL.
|
||||
func ServeFail() ResponderFunc {
|
||||
return func(query *dns.Msg, _ string, _ interface{}) *dns.Msg {
|
||||
return new(dns.Msg).SetRcode(query, dns.RcodeServerFailure)
|
||||
}
|
||||
}
|
||||
36
nameserver/response.go
Normal file
36
nameserver/response.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"github.com/miekg/dns"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/nameserver/nsutil"
|
||||
"github.com/safing/portmaster/network"
|
||||
)
|
||||
|
||||
// sendResponse sends a response to query using w. If reasonCtx is not
|
||||
// nil and implements either the Responder or RRProvider interface then
|
||||
// those functions are used to craft a DNS response. If reasonCtx is nil
|
||||
// or does not implement the Responder interface and verdict is not set
|
||||
// to failed a ZeroIP response will be sent. If verdict is set to failed
|
||||
// then a ServFail will be sent instead.
|
||||
func sendResponse(w dns.ResponseWriter, query *dns.Msg, verdict network.Verdict, reason string, reasonCtx interface{}) {
|
||||
responder, ok := reasonCtx.(nsutil.Responder)
|
||||
if !ok {
|
||||
if verdict == network.VerdictFailed {
|
||||
responder = nsutil.ServeFail()
|
||||
} else {
|
||||
responder = nsutil.ZeroIP()
|
||||
}
|
||||
}
|
||||
|
||||
reply := responder.ReplyWithDNS(query, reason, reasonCtx)
|
||||
|
||||
if extra, ok := reasonCtx.(nsutil.RRProvider); ok {
|
||||
rrs := extra.GetExtraRR(query, reason, reasonCtx)
|
||||
reply.Extra = append(reply.Extra, rrs...)
|
||||
}
|
||||
|
||||
if err := w.WriteMsg(reply); err != nil {
|
||||
log.Errorf("nameserver: failed to send response: %s", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user