mirror of
https://github.com/safing/portmaster
synced 2025-09-02 10:39:22 +00:00
Merge pull request #47 from safing/feature/custom-dns-response
Let decision reasons decide on the DNS reply
This commit is contained in:
commit
587cbb4f21
7 changed files with 173 additions and 47 deletions
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue