diff --git a/firewall/bypassing.go b/firewall/bypassing.go index ac3349f5..5beb96b0 100644 --- a/firewall/bypassing.go +++ b/firewall/bypassing.go @@ -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 } diff --git a/firewall/master.go b/firewall/master.go index 06a0f5f1..89e8a1c4 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -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: } diff --git a/intel/block_reason.go b/intel/block_reason.go index 09b89db2..26bd0a2a 100644 --- a/intel/block_reason.go +++ b/intel/block_reason.go @@ -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) diff --git a/intel/entity.go b/intel/entity.go index af96343d..d6abeb66 100644 --- a/intel/entity.go +++ b/intel/entity.go @@ -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 } diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index 03d71701..8128388e 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -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 } diff --git a/nameserver/nsutil/nsutil.go b/nameserver/nsutil/nsutil.go new file mode 100644 index 00000000..a43bf26c --- /dev/null +++ b/nameserver/nsutil/nsutil.go @@ -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) + } +} diff --git a/nameserver/response.go b/nameserver/response.go new file mode 100644 index 00000000..dfdb74da --- /dev/null +++ b/nameserver/response.go @@ -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) + } +}