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 ( import (
"strings" "strings"
"github.com/safing/portmaster/nameserver/nsutil"
"github.com/safing/portmaster/network" "github.com/safing/portmaster/network"
"github.com/safing/portmaster/profile/endpoints" "github.com/safing/portmaster/profile/endpoints"
) )
// PreventBypassing checks if the connection should be denied or permitted // PreventBypassing checks if the connection should be denied or permitted
// based on some bypass protection checks. // 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 // Block firefox canary domain to disable DoH
if strings.ToLower(conn.Entity.Domain) == "use-application-dns.net." { 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 { func checkBypassPrevention(conn *network.Connection, _ packet.Packet) bool {
if conn.Process().Profile().PreventBypassing() { if conn.Process().Profile().PreventBypassing() {
// check for bypass protection // check for bypass protection
result, reason := PreventBypassing(conn) result, reason, reasonCtx := PreventBypassing(conn)
switch result { switch result {
case endpoints.Denied: case endpoints.Denied:
conn.Block("bypass prevention: " + reason) conn.BlockWithContext("bypass prevention: "+reason, reasonCtx)
return true return true
case endpoints.Permitted: case endpoints.Permitted:
conn.Accept("bypass prevention: " + reason) conn.AcceptWithContext("bypass prevention: "+reason, reasonCtx)
return true return true
case endpoints.NoMatch: case endpoints.NoMatch:
} }

View file

@ -7,6 +7,7 @@ import (
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portmaster/nameserver/nsutil"
) )
// ListMatch represents an entity that has been // 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 // GetExtraRR implements the nsutil.RRProvider interface
// block reason. // and adds additional TXT records justifying the reason
func (br ListBlockReason) ToRRs() []dns.RR { // the request was blocked.
func (br ListBlockReason) GetExtraRR(_ *dns.Msg, _ string, _ interface{}) []dns.RR {
rrs := make([]dns.RR, 0, len(br)) rrs := make([]dns.RR, 0, len(br))
for _, lm := range br { for _, lm := range br {
@ -95,3 +97,5 @@ func (br ListBlockReason) ToRRs() []dns.RR {
return rrs 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.ListOccurences[key] = mergeStringList(e.ListOccurences[key], list)
//e.Lists = mergeStringList(e.Lists, list)
//e.ListsMap = buildLookupMap(e.Lists)
} }
func (e *Entity) getDomainLists() { func (e *Entity) getDomainLists() {
@ -289,8 +286,6 @@ func (e *Entity) getDomainLists() {
for _, domain := range domainsToInspect { for _, domain := range domainsToInspect {
subdomains := splitDomain(domain) subdomains := splitDomain(domain)
domains = append(domains, subdomains...) domains = append(domains, subdomains...)
log.Tracef("intel: subdomain list resolving is enabled: %s => %v", domains, subdomains)
} }
} else { } else {
domains = domainsToInspect domains = domainsToInspect
@ -446,8 +441,8 @@ func (e *Entity) MatchLists(lists []string) bool {
} }
} }
makeDistinct(e.BlockedByLists) e.BlockedByLists = makeDistinct(e.BlockedByLists)
makeDistinct(e.BlockedEntities) e.BlockedEntities = makeDistinct(e.BlockedEntities)
return len(e.BlockedByLists) > 0 return len(e.BlockedByLists) > 0
} }

View file

@ -3,7 +3,6 @@ package nameserver
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"net" "net"
"strings" "strings"
@ -13,6 +12,7 @@ import (
"github.com/safing/portbase/modules" "github.com/safing/portbase/modules"
"github.com/safing/portmaster/detection/dga" "github.com/safing/portmaster/detection/dga"
"github.com/safing/portmaster/firewall" "github.com/safing/portmaster/firewall"
"github.com/safing/portmaster/nameserver/nsutil"
"github.com/safing/portmaster/netenv" "github.com/safing/portmaster/netenv"
"github.com/safing/portmaster/network" "github.com/safing/portmaster/network"
"github.com/safing/portmaster/network/netutils" "github.com/safing/portmaster/network/netutils"
@ -89,29 +89,6 @@ func stop() error {
return nil 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) { func returnServerFailure(w dns.ResponseWriter, query *dns.Msg) {
m := new(dns.Msg) m := new(dns.Msg)
m.SetRcode(query, dns.RcodeServerFailure) 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 { if question.Qclass != dns.ClassINET {
// we only serve IN records, return nxdomain // we only serve IN records, return nxdomain
log.Warningf("nameserver: only IN record requests are supported but received Qclass %d, returning NXDOMAIN", question.Qclass) 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 return nil
} }
@ -185,7 +162,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
// check if valid domain name // check if valid domain name
if !netutils.IsValidFqdn(q.FQDN) { if !netutils.IsValidFqdn(q.FQDN) {
log.Debugf("nameserver: domain name %s is invalid, returning nxdomain", 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 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 // NOTE(ppacher): saving unknown process connection might end up in a lot of
// processes. Consider disabling that via config. // processes. Consider disabling that via config.
conn.Failed("Unknown process") conn.Failed("Unknown process")
returnNXDomain(w, query, "unknown process", conn.ReasonContext) sendResponse(w, query, conn.Verdict, conn.Reason, conn.ReasonContext)
return nil return nil
} }
@ -238,7 +215,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
if lms < 10 { if lms < 10 {
tracer.Warningf("nameserver: possible data tunnel by %s: %s has lms score of %f, returning nxdomain", conn.Process(), q.FQDN, lms) 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") conn.Block("Possible data tunnel")
returnNXDomain(w, query, "lms", conn.ReasonContext) sendResponse(w, query, conn.Verdict, conn.Reason, conn.ReasonContext)
return nil return nil
} }
@ -248,13 +225,34 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
switch conn.Verdict { switch conn.Verdict {
case network.VerdictBlock: case network.VerdictBlock:
tracer.Infof("nameserver: %s blocked, returning nxdomain", conn) 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 return nil
case network.VerdictDrop, network.VerdictFailed: case network.VerdictDrop, network.VerdictFailed:
tracer.Infof("nameserver: %s dropped, not replying", conn) tracer.Infof("nameserver: %s dropped, not replying", conn)
return nil 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 // resolve
rrCache, err := resolver.Resolve(ctx, q) rrCache, err := resolver.Resolve(ctx, q)
if err != nil { 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()) 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 return nil
} }
rrCache = firewall.DecideOnResolvedDNS(conn, q, rrCache) rrCache = firewall.DecideOnResolvedDNS(conn, q, rrCache)
if rrCache == nil { if rrCache == nil {
returnNXDomain(w, query, conn.Reason, conn.ReasonContext) sendResponse(w, query, conn.Verdict, conn.Reason, conn.ReasonContext)
return nil 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)
}
}