diff --git a/firewall/bypassing.go b/firewall/bypassing.go index 5beb96b0..602fbea2 100644 --- a/firewall/bypassing.go +++ b/firewall/bypassing.go @@ -13,7 +13,9 @@ import ( 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", nsutil.NxDomain() + return endpoints.Denied, + "blocked canary domain to prevent enabling of DNS-over-HTTPs", + nsutil.NxDomain("blocked canary domain to prevent enabling of DNS-over-HTTPs") } return endpoints.NoMatch, "", nil diff --git a/intel/block_reason.go b/intel/block_reason.go index ad140f4f..266e7140 100644 --- a/intel/block_reason.go +++ b/intel/block_reason.go @@ -1,6 +1,7 @@ package intel import ( + "context" "encoding/json" "fmt" "strings" @@ -66,31 +67,31 @@ func (br ListBlockReason) MarshalJSON() ([]byte, error) { // 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 { +func (br ListBlockReason) GetExtraRRs(ctx context.Context, _ *dns.Msg) []dns.RR { rrs := make([]dns.RR, 0, len(br)) for _, lm := range br { - blockedBy, err := dns.NewRR(fmt.Sprintf( - `%s 0 IN TXT "blocked by filter lists %s"`, + blockedBy, err := nsutil.MakeMessageRecord(log.InfoLevel, fmt.Sprintf( + "%s is blocked by filter lists %s", lm.Entity, strings.Join(lm.ActiveLists, ", "), )) if err == nil { rrs = append(rrs, blockedBy) } else { - log.Errorf("intel: failed to create TXT RR for block reason: %s", err) + log.Tracer(ctx).Errorf("intel: failed to create TXT RR for block reason: %s", err) } if len(lm.InactiveLists) > 0 { - wouldBeBlockedBy, err := dns.NewRR(fmt.Sprintf( - `%s 0 IN TXT "would be blocked by filter lists %s"`, + wouldBeBlockedBy, err := nsutil.MakeMessageRecord(log.InfoLevel, fmt.Sprintf( + "%s would be blocked by filter lists %s", lm.Entity, strings.Join(lm.InactiveLists, ", "), )) if err == nil { rrs = append(rrs, wouldBeBlockedBy) } else { - log.Errorf("intel: failed to create TXT RR for block reason: %s", err) + log.Tracer(ctx).Errorf("intel: failed to create TXT RR for block reason: %s", err) } } } diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index dc0cde3d..2b8f77e7 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -3,7 +3,6 @@ package nameserver import ( "context" "errors" - "fmt" "net" "strings" @@ -28,11 +27,10 @@ var ( dnsServer *dns.Server listenAddress = "0.0.0.0:53" - localhostRRs []dns.RR ) func init() { - module = modules.Register("nameserver", prep, start, stop, "core", "resolver") + module = modules.Register("nameserver", nil, start, stop, "core", "resolver") subsystems.Register( "dns", "Secure DNS", @@ -43,22 +41,6 @@ func init() { ) } -func prep() error { - localhostIPv4, err := dns.NewRR("localhost. 17 IN A 127.0.0.1") - if err != nil { - return err - } - - localhostIPv6, err := dns.NewRR("localhost. 17 IN AAAA ::1") - if err != nil { - return err - } - - localhostRRs = []dns.RR{localhostIPv4, localhostIPv6} - - return nil -} - func start() error { dnsServer = &dns.Server{Addr: listenAddress, Net: "udp"} dns.HandleFunc(".", handleRequestAsWorker) @@ -89,12 +71,6 @@ func stop() error { return nil } -func returnServerFailure(w dns.ResponseWriter, query *dns.Msg) { - m := new(dns.Msg) - m.SetRcode(query, dns.RcodeServerFailure) - _ = writeDNSResponse(w, m) -} - func handleRequestAsWorker(w dns.ResponseWriter, query *dns.Msg) { err := module.RunWorker("dns request", func(ctx context.Context) error { return handleRequest(ctx, w, query) @@ -104,86 +80,80 @@ func handleRequestAsWorker(w dns.ResponseWriter, query *dns.Msg) { } } -func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) error { //nolint:gocognit // TODO - // only process first question, that's how everyone does it. - question := query.Question[0] +func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) error { //nolint:gocognit // TODO + // Only process first question, that's how everyone does it. + question := request.Question[0] q := &resolver.Query{ FQDN: question.Name, QType: dns.Type(question.Qtype), } - // return with server failure if offline - if netenv.GetOnlineStatus() == netenv.StatusOffline && - !netenv.IsConnectivityDomain(q.FQDN) { - log.Tracer(ctx).Debugf("resolver: not resolving %s, device is offline", q.FQDN) - returnServerFailure(w, query) - return nil - } - - // check class - 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) - sendResponse(w, query, 0, "qclass not served", nsutil.Refused()) - return nil - } - - // handle request for localhost - if strings.HasSuffix(q.FQDN, "localhost.") { - m := new(dns.Msg) - m.SetReply(query) - m.Answer = localhostRRs - if err := writeDNSResponse(w, m); err != nil { - log.Warningf("nameserver: failed to handle request to %s: %s", q.FQDN, err) - } - return nil - } - - // get remote address + // Get remote address of request. remoteAddr, ok := w.RemoteAddr().(*net.UDPAddr) if !ok { log.Warningf("nameserver: failed to get remote address of request for %s%s, ignoring", q.FQDN, q.QType) return nil } - // check if the request is local - local, err := netenv.IsMyIP(remoteAddr.IP) - if err != nil { - log.Warningf("nameserver: failed to check if request for %s%s is local: %s", q.FQDN, q.QType, err) - return nil - } - if !local { - log.Warningf("nameserver: external request for %s%s, ignoring", q.FQDN, q.QType) - return nil - } - - // check if valid domain name - if !netutils.IsValidFqdn(q.FQDN) { - log.Debugf("nameserver: domain name %s is invalid, returning nxdomain", q.FQDN) - sendResponse(w, query, 0, "invalid FQDN", nsutil.Refused()) - return nil - } - - // start tracer + // Start context tracer for context-aware logging. ctx, tracer := log.AddTracer(ctx) defer tracer.Submit() - tracer.Tracef("nameserver: handling new request for %s%s from %s:%d, getting connection", q.FQDN, q.QType, remoteAddr.IP, remoteAddr.Port) + tracer.Tracef("nameserver: handling new request for %s%s from %s:%d", q.FQDN, q.QType, remoteAddr.IP, remoteAddr.Port) - // TODO: if there are 3 request for the same domain/type in a row, delete all caches of that domain + // Setup quick reply function. + reply := func(responder nsutil.Responder, rrProviders ...nsutil.RRProvider) error { + return sendResponse(ctx, w, request, responder, rrProviders...) + } - // get connection + // Return with server failure if offline. + if netenv.GetOnlineStatus() == netenv.StatusOffline && + !netenv.IsConnectivityDomain(q.FQDN) { + tracer.Debugf("resolver: not resolving %s, device is offline", q.FQDN) + return reply(nsutil.ServerFailure("resolving disabled, device is offline")) + } + + // Check the Query Class. + if question.Qclass != dns.ClassINET { + // we only serve IN records, return nxdomain + tracer.Warningf("nameserver: only IN record requests are supported but received Qclass %d, returning NXDOMAIN", question.Qclass) + return reply(nsutil.Refused("unsupported qclass")) + } + + // Handle request for localhost. + if strings.HasSuffix(q.FQDN, "localhost.") { + return reply(nsutil.Localhost("")) + } + + // Authenticate request - only requests from the local host, but with any of its IPs, are allowed. + local, err := netenv.IsMyIP(remoteAddr.IP) + if err != nil { + tracer.Warningf("nameserver: failed to check if request for %s%s is local: %s", q.FQDN, q.QType, err) + return nil // Do no reply, drop request immediately. + } + if !local { + tracer.Warningf("nameserver: external request for %s%s, ignoring", q.FQDN, q.QType) + return nil // Do no reply, drop request immediately. + } + + // Validate domain name. + if !netutils.IsValidFqdn(q.FQDN) { + tracer.Debugf("nameserver: domain name %s is invalid, refusing", q.FQDN) + return reply(nsutil.Refused("invalid domain")) + } + + // Get connection for this request. This identifies the process behind the request. conn := network.NewConnectionFromDNSRequest(ctx, q.FQDN, nil, packet.IPv4, remoteAddr.IP, uint16(remoteAddr.Port)) - // once we decided on the connection we might need to save it to the database - // so we defer that check right now. + // Once we decided on the connection we might need to save it to the database, + // so we defer that check for now. defer func() { switch conn.Verdict { - // we immediately save blocked, dropped or failed verdicts so - // the pop up in the UI. + // We immediately save blocked, dropped or failed verdicts so + // they pop up in the UI. case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed: conn.Save() - // for undecided or accepted connections we don't save them yet because + // For undecided or accepted connections we don't save them yet, because // that will happen later anyway. case network.VerdictUndecided, network.VerdictAccept, network.VerdictRerouteToNameserver, network.VerdictRerouteToTunnel: @@ -194,104 +164,72 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er } }() - // TODO: this has been obsoleted due to special profiles - if conn.Process().Profile() == nil { - tracer.Infof("nameserver: failed to find process for request %s, returning NXDOMAIN", conn) - // NOTE(ppacher): saving unknown process connection might end up in a lot of - // processes. Consider disabling that via config. - conn.Failed("Unknown process") - sendResponse(w, query, conn.Verdict, conn.Reason, conn.ReasonContext) - return nil - } - - // save security level to query - q.SecurityLevel = conn.Process().Profile().SecurityLevel() - - // check profile before we even get intel and rr + // Check request with the privacy filter before resolving. firewall.DecideOnConnection(ctx, conn, nil) + // Check if there is Verdict to act upon. switch conn.Verdict { - case network.VerdictBlock: - tracer.Infof("nameserver: %s blocked, returning nxdomain", conn) - 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 + case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed: + tracer.Infof("nameserver: request for %s from %s %s", q.ID(), conn.Process(), conn.Verdict.Verb()) + return reply(conn, conn) } - // 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. + // Check if there is a responder from the firewall. + // In special cases, the firewall might want to respond the query itself. // 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. + // IP address in which case we "accept" it, but let the firewall handle + // the resolving as it wishes. 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 { - tracer.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 + // Save the request as open, as we don't know if there will be a connection or not. network.SaveOpenDNSRequest(conn) - return nil + tracer.Infof("nameserver: handing over request for %s to filter responder: %s", q.ID(), conn.Reason) + return reply(responder) } - // resolve + // Save security level to query, so that the resolver can react to configuration. + q.SecurityLevel = conn.Process().Profile().SecurityLevel() + + // Resolve request. rrCache, err := resolver.Resolve(ctx, q) if err != nil { - // TODO: analyze nxdomain requests, malware could be trying DGA-domains - tracer.Debugf("nameserver: %s requested %s%s: %s", conn.Process(), q.FQDN, q.QType, err) - - if errors.Is(err, resolver.ErrBlocked) { - conn.Block(err.Error()) - } else { - conn.Failed("failed to resolve: " + err.Error()) + // React to special errors. + switch { + case errors.Is(err, resolver.ErrNotFound): + return reply(nsutil.NxDomain(""), nil) + case errors.Is(err, resolver.ErrBlocked): + return reply(nsutil.ZeroIP(""), nil) + case errors.Is(err, resolver.ErrLocalhost): + return reply(nsutil.Localhost(""), nil) + default: + return reply(nsutil.ServerFailure("internal error: "+err.Error()), nil) } - - sendResponse(w, query, conn.Verdict, conn.Reason, conn.ReasonContext) - return nil } tracer.Trace("nameserver: deciding on resolved dns") rrCache = firewall.DecideOnResolvedDNS(ctx, conn, q, rrCache) if rrCache == nil { - sendResponse(w, query, conn.Verdict, conn.Reason, conn.ReasonContext) - return nil - } + // Check again if there is a responder from the firewall. + if responder, ok := conn.ReasonContext.(nsutil.Responder); ok { + // Save the request as open, as we don't know if there will be a connection or not. + network.SaveOpenDNSRequest(conn) - // reply to query - m := new(dns.Msg) - m.SetReply(query) - m.Answer = rrCache.Answer - m.Ns = rrCache.Ns - m.Extra = rrCache.Extra - - if err := writeDNSResponse(w, m); err != nil { - tracer.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 -} - -func writeDNSResponse(w dns.ResponseWriter, m *dns.Msg) (err error) { - defer func() { - // recover from panic - if panicErr := recover(); panicErr != nil { - err = fmt.Errorf("panic: %s", panicErr) - log.Warningf("nameserver: panic caused by this msg: %#v", m) + tracer.Infof("nameserver: handing over request for %s to filter responder: %s", q.ID(), conn.Reason) + return reply(responder) } - }() - err = w.WriteMsg(m) - return + // Request was blocked by the firewall. + switch conn.Verdict { + case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed: + tracer.Infof("nameserver: request for %s from %s %s", q.ID(), conn.Process(), conn.Verdict.Verb()) + return reply(conn, conn) + } + } + + // Save dns request as open. + defer network.SaveOpenDNSRequest(conn) + + // Reply with successful response. + tracer.Infof("nameserver: returning %s response %s to %s", conn.Verdict.Verb(), q.ID(), conn.Process()) + return reply(rrCache, conn, rrCache) } diff --git a/nameserver/nsutil/nsutil.go b/nameserver/nsutil/nsutil.go index a43bf26c..cd42daba 100644 --- a/nameserver/nsutil/nsutil.go +++ b/nameserver/nsutil/nsutil.go @@ -1,6 +1,10 @@ package nsutil import ( + "context" + "fmt" + "strings" + "github.com/miekg/dns" "github.com/safing/portbase/log" ) @@ -13,35 +17,35 @@ import ( 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 + ReplyWithDNS(ctx context.Context, request *dns.Msg) *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 + // GetExtraRRs 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 + GetExtraRRs(ctx context.Context, request *dns.Msg) []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 +type ResponderFunc func(ctx context.Context, request *dns.Msg) *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) +func (rf ResponderFunc) ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns.Msg { + return rf(ctx, request) } // 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) +func ZeroIP(msg string) ResponderFunc { + return func(ctx context.Context, request *dns.Msg) *dns.Msg { + reply := new(dns.Msg) hasErr := false - for _, question := range query.Question { + for _, question := range request.Question { var rr dns.RR var err error @@ -53,40 +57,110 @@ func ZeroIP() ResponderFunc { } if err != nil { - log.Errorf("nameserver: failed to create zero-ip response for %s: %s", question.Name, err) + log.Tracer(ctx).Errorf("nameserver: failed to create zero-ip response for %s: %s", question.Name, err) hasErr = true } else { - m.Answer = append(m.Answer, rr) + reply.Answer = append(reply.Answer, rr) } } - if hasErr && len(m.Answer) == 0 { - m.SetRcode(query, dns.RcodeServerFailure) - } else { - m.SetRcode(query, dns.RcodeSuccess) + switch { + case hasErr && len(reply.Answer) == 0: + reply.SetRcode(request, dns.RcodeServerFailure) + case len(reply.Answer) == 0: + reply.SetRcode(request, dns.RcodeNameError) + default: + reply.SetRcode(request, dns.RcodeSuccess) } - return m + AddMessageToReply(ctx, reply, log.InfoLevel, msg) + + return reply + } +} + +func Localhost(msg string) ResponderFunc { + return func(ctx context.Context, request *dns.Msg) *dns.Msg { + reply := new(dns.Msg) + hasErr := false + + for _, question := range request.Question { + var rr dns.RR + var err error + + switch question.Qtype { + case dns.TypeA: + rr, err = dns.NewRR("localhost. 0 IN A 127.0.0.1") + case dns.TypeAAAA: + rr, err = dns.NewRR("localhost. 0 IN AAAA ::1") + } + + if err != nil { + log.Tracer(ctx).Errorf("nameserver: failed to create localhost response for %s: %s", question.Name, err) + hasErr = true + } else { + reply.Answer = append(reply.Answer, rr) + } + } + + switch { + case hasErr && len(reply.Answer) == 0: + reply.SetRcode(request, dns.RcodeServerFailure) + case len(reply.Answer) == 0: + reply.SetRcode(request, dns.RcodeNameError) + default: + reply.SetRcode(request, dns.RcodeSuccess) + } + + AddMessageToReply(ctx, reply, log.InfoLevel, msg) + + return reply } } // 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) +func NxDomain(msg string) ResponderFunc { + return func(ctx context.Context, request *dns.Msg) *dns.Msg { + reply := new(dns.Msg).SetRcode(request, dns.RcodeNameError) + AddMessageToReply(ctx, reply, log.InfoLevel, msg) + return reply } } // 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) +func Refused(msg string) ResponderFunc { + return func(ctx context.Context, request *dns.Msg) *dns.Msg { + reply := new(dns.Msg).SetRcode(request, dns.RcodeRefused) + AddMessageToReply(ctx, reply, log.InfoLevel, msg) + return reply } } -// 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) +// ServerFailure returns a ResponderFunc that replies with SERVFAIL. +func ServerFailure(msg string) ResponderFunc { + return func(ctx context.Context, request *dns.Msg) *dns.Msg { + reply := new(dns.Msg).SetRcode(request, dns.RcodeServerFailure) + AddMessageToReply(ctx, reply, log.InfoLevel, msg) + return reply + } +} + +func MakeMessageRecord(level log.Severity, msg string) (dns.RR, error) { + return dns.NewRR(fmt.Sprintf( + `%s.portmaster. 0 IN TXT "%s"`, + strings.ToLower(level.String()), + msg, + )) +} + +func AddMessageToReply(ctx context.Context, reply *dns.Msg, level log.Severity, msg string) { + if msg != "" { + rr, err := MakeMessageRecord(level, msg) + if err != nil { + log.Tracer(ctx).Warningf("nameserver: failed to add message to reply: %s", err) + return + } + + reply.Extra = append(reply.Extra, rr) } } diff --git a/nameserver/response.go b/nameserver/response.go index 17c63964..0780edf0 100644 --- a/nameserver/response.go +++ b/nameserver/response.go @@ -1,36 +1,55 @@ package nameserver import ( + "context" + "fmt" + "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() - } +// sendResponse sends a response to query using w. The response message is +// created by responder. If addExtraRRs is not nil and implements the +// RRProvider interface then it will be also used to add more RRs in the +// extra section. +func sendResponse( + ctx context.Context, + w dns.ResponseWriter, + request *dns.Msg, + responder nsutil.Responder, + rrProviders ...nsutil.RRProvider, +) error { + // Have the Responder craft a DNS reply. + reply := responder.ReplyWithDNS(ctx, request) + if reply == nil { + // Dropping query. + return nil } - reply := responder.ReplyWithDNS(query, reason, reasonCtx) - - if extra, ok := reasonCtx.(nsutil.RRProvider); ok { - rrs := extra.GetExtraRR(query, reason, reasonCtx) + // Add extra RRs through a custom RRProvider. + for _, rrProvider := range rrProviders { + rrs := rrProvider.GetExtraRRs(ctx, request) reply.Extra = append(reply.Extra, rrs...) } + // Write reply. if err := writeDNSResponse(w, reply); err != nil { - log.Errorf("nameserver: failed to send response: %s", err) + return fmt.Errorf("nameserver: failed to send response: %w", err) } + + return nil +} + +func writeDNSResponse(w dns.ResponseWriter, m *dns.Msg) (err error) { + defer func() { + // recover from panic + if panicErr := recover(); panicErr != nil { + err = fmt.Errorf("panic: %s", panicErr) + log.Warningf("nameserver: panic caused by this msg: %#v", m) + } + }() + + err = w.WriteMsg(m) + return } diff --git a/network/dns.go b/network/dns.go index d5f71faa..de46d8de 100644 --- a/network/dns.go +++ b/network/dns.go @@ -2,10 +2,14 @@ package network import ( "context" + "fmt" "strconv" "sync" "time" + "github.com/miekg/dns" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/nameserver/nsutil" "github.com/safing/portmaster/process" ) @@ -88,3 +92,48 @@ func writeOpenDNSRequestsToDB() { conn.Unlock() } } + +// ReplyWithDNS creates a new reply to the given request with the data from the RRCache, and additional informational records. +func (conn *Connection) ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns.Msg { + // Select request responder. + switch conn.Verdict { + case VerdictBlock: + return nsutil.ZeroIP("").ReplyWithDNS(ctx, request) + case VerdictDrop: + return nil // Do not respond to request. + case VerdictFailed: + return nsutil.ZeroIP("").ReplyWithDNS(ctx, request) + default: + reply := nsutil.ServerFailure("").ReplyWithDNS(ctx, request) + nsutil.AddMessageToReply(ctx, reply, log.ErrorLevel, "INTERNAL ERROR: incorrect use of network.Connection's DNS Responder") + return reply + } +} + +// GetExtraRRs returns a slice of RRs with additional informational records. +func (conn *Connection) GetExtraRRs(ctx context.Context, request *dns.Msg) []dns.RR { + // Select level to add the verdict record with. + var level log.Severity + switch conn.Verdict { + case VerdictFailed: + level = log.ErrorLevel + default: + level = log.InfoLevel + } + + // Create resource record with verdict and reason. + rr, err := nsutil.MakeMessageRecord(level, fmt.Sprintf("%s: %s", conn.Verdict.Verb(), conn.Reason)) + if err != nil { + log.Tracer(ctx).Warningf("filter: failed to add informational record to reply: %s", err) + return nil + } + extra := []dns.RR{rr} + + // Add additional records from ReasonContext. + if rrProvider, ok := conn.ReasonContext.(nsutil.RRProvider); ok { + rrs := rrProvider.GetExtraRRs(ctx, request) + extra = append(extra, rrs...) + } + + return extra +} diff --git a/network/status.go b/network/status.go index c0930acc..149434ee 100644 --- a/network/status.go +++ b/network/status.go @@ -39,6 +39,30 @@ func (v Verdict) String() string { } } +// Verb returns the verdict as a past tense verb. +func (v Verdict) Verb() string { + switch v { + case VerdictUndecided: + return "undecided" + case VerdictUndeterminable: + return "undeterminable" + case VerdictAccept: + return "accepted" + case VerdictBlock: + return "blocked" + case VerdictDrop: + return "dropped" + case VerdictRerouteToNameserver: + return "rerouted to nameserver" + case VerdictRerouteToTunnel: + return "rerouted to tunnel" + case VerdictFailed: + return "failed" + default: + return "invalid" + } +} + // Packer Directions const ( Inbound = true diff --git a/resolver/rrcache.go b/resolver/rrcache.go index da1bd0a0..d9ada8d3 100644 --- a/resolver/rrcache.go +++ b/resolver/rrcache.go @@ -28,6 +28,7 @@ type RRCache struct { Server string // constant ServerScope int8 // constant + ServerInfo string // constant servedFromCache bool // mutable requestingNew bool // mutable @@ -246,3 +247,61 @@ func (rrCache *RRCache) ShallowCopy() *RRCache { FilteredEntries: rrCache.FilteredEntries, } } + +// ReplyWithDNS creates a new reply to the given query with the data from the RRCache, and additional informational records. +func (rrCache *RRCache) ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns.Msg { + // reply to query + reply := new(dns.Msg) + reply.SetRcode(request, dns.RcodeSuccess) + reply.Answer = rrCache.Answer + reply.Ns = rrCache.Ns + reply.Extra = rrCache.Extra + + // Set NXDomain return code. + if rrCache.IsNXDomain() { + reply.Rcode = dns.RcodeNameError + } + + return reply +} + +// GetExtraRRs returns a slice of RRs with additional informational records. +func (rrCache *RRCache) GetExtraRRs(ctx context.Context, query *dns.Msg) (extra []dns.RR) { + // Add cache status and source of data. + if rrCache.servedFromCache { + extra = addExtra(ctx, extra, log.InfoLevel, "served from cache, resolved by "+rrCache.ServerInfo) + } else { + extra = addExtra(ctx, extra, log.InfoLevel, "freshly resolved by "+rrCache.ServerInfo) + } + + // Add expiry and cache information. + if rrCache.Expired() { + extra = addExtra(ctx, extra, log.InfoLevel, fmt.Sprintf("record expired since %s, requesting new", time.Since(time.Unix(rrCache.TTL, 0)))) + } else { + extra = addExtra(ctx, extra, log.InfoLevel, fmt.Sprintf("record valid for %s", time.Until(time.Unix(rrCache.TTL, 0)))) + } + if rrCache.requestingNew { + extra = addExtra(ctx, extra, log.InfoLevel, "async request to refresh the cache has been started") + } + + // Add information about filtered entries. + if rrCache.Filtered { + if len(rrCache.FilteredEntries) > 1 { + extra = addExtra(ctx, extra, log.InfoLevel, fmt.Sprintf("%d records have been filtered", len(rrCache.FilteredEntries))) + } else { + extra = addExtra(ctx, extra, log.InfoLevel, fmt.Sprintf("%d record has been filtered", len(rrCache.FilteredEntries))) + } + } + + return extra +} + +func addExtra(ctx context.Context, extra []dns.RR, level log.Severity, msg string) []dns.RR { + rr, err := nsutil.MakeMessageRecord(level, msg) + if err != nil { + log.Tracer(ctx).Warningf("resolver: failed to add informational record to reply: %s", err) + return extra + } + extra = append(extra, rr) + return extra +}