Merge pull request #47 from safing/feature/custom-dns-response

Let decision reasons decide on the DNS reply
This commit is contained in:
Daniel 2020-04-30 15:23:14 +02:00 committed by GitHub
commit 587cbb4f21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 173 additions and 47 deletions

View file

@ -3,17 +3,18 @@ package firewall
import (
"strings"
"github.com/safing/portmaster/nameserver/nsutil"
"github.com/safing/portmaster/network"
"github.com/safing/portmaster/profile/endpoints"
)
// PreventBypassing checks if the connection should be denied or permitted
// based on some bypass protection checks.
func PreventBypassing(conn *network.Connection) (endpoints.EPResult, string) {
func PreventBypassing(conn *network.Connection) (endpoints.EPResult, string, nsutil.Responder) {
// Block firefox canary domain to disable DoH
if strings.ToLower(conn.Entity.Domain) == "use-application-dns.net." {
return endpoints.Denied, "blocked canary domain to prevent enabling DNS-over-HTTPs"
return endpoints.Denied, "blocked canary domain to prevent enabling DNS-over-HTTPs", nsutil.NxDomain()
}
return endpoints.NoMatch, ""
return endpoints.NoMatch, "", nil
}

View file

@ -216,13 +216,13 @@ func checkConnectionScope(conn *network.Connection, _ packet.Packet) bool {
func checkBypassPrevention(conn *network.Connection, _ packet.Packet) bool {
if conn.Process().Profile().PreventBypassing() {
// check for bypass protection
result, reason := PreventBypassing(conn)
result, reason, reasonCtx := PreventBypassing(conn)
switch result {
case endpoints.Denied:
conn.Block("bypass prevention: " + reason)
conn.BlockWithContext("bypass prevention: "+reason, reasonCtx)
return true
case endpoints.Permitted:
conn.Accept("bypass prevention: " + reason)
conn.AcceptWithContext("bypass prevention: "+reason, reasonCtx)
return true
case endpoints.NoMatch:
}

View file

@ -7,6 +7,7 @@ import (
"github.com/miekg/dns"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/nameserver/nsutil"
)
// ListMatch represents an entity that has been
@ -62,9 +63,10 @@ func (br ListBlockReason) MarshalJSON() ([]byte, error) {
})
}
// ToRRs returns a set of dns TXT records that describe the
// block reason.
func (br ListBlockReason) ToRRs() []dns.RR {
// GetExtraRR implements the nsutil.RRProvider interface
// and adds additional TXT records justifying the reason
// the request was blocked.
func (br ListBlockReason) GetExtraRR(_ *dns.Msg, _ string, _ interface{}) []dns.RR {
rrs := make([]dns.RR, 0, len(br))
for _, lm := range br {
@ -95,3 +97,5 @@ func (br ListBlockReason) ToRRs() []dns.RR {
return rrs
}
var _ nsutil.RRProvider = ListBlockReason(nil)

View file

@ -261,9 +261,6 @@ func (e *Entity) mergeList(key string, list []string) {
}
e.ListOccurences[key] = mergeStringList(e.ListOccurences[key], list)
//e.Lists = mergeStringList(e.Lists, list)
//e.ListsMap = buildLookupMap(e.Lists)
}
func (e *Entity) getDomainLists() {
@ -289,8 +286,6 @@ func (e *Entity) getDomainLists() {
for _, domain := range domainsToInspect {
subdomains := splitDomain(domain)
domains = append(domains, subdomains...)
log.Tracef("intel: subdomain list resolving is enabled: %s => %v", domains, subdomains)
}
} else {
domains = domainsToInspect
@ -446,8 +441,8 @@ func (e *Entity) MatchLists(lists []string) bool {
}
}
makeDistinct(e.BlockedByLists)
makeDistinct(e.BlockedEntities)
e.BlockedByLists = makeDistinct(e.BlockedByLists)
e.BlockedEntities = makeDistinct(e.BlockedEntities)
return len(e.BlockedByLists) > 0
}

View file

@ -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
}

View 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
View 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)
}
}