From 7b5803482c4e06b9ff327daa2fcd2280a8871a70 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 29 Sep 2020 09:02:49 +0200 Subject: [PATCH 1/2] Support non-standard format on dns query name --- nameserver/nameserver.go | 24 +++++++++++++++++++----- resolver/rrcache.go | 32 ++++++++++++++++++++------------ 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index 21b81151..0d780d87 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -82,10 +82,19 @@ func handleRequestAsWorker(w dns.ResponseWriter, query *dns.Msg) { 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] + originalQuestion := request.Question[0] + + // Check if we are handling a non-standard query name. + var nonStandardQuestionFormat bool + lowerCaseQuestion := strings.ToLower(originalQuestion.Name) + if lowerCaseQuestion != originalQuestion.Name { + nonStandardQuestionFormat = true + } + + // Create query for the resolver. q := &resolver.Query{ - FQDN: question.Name, - QType: dns.Type(question.Qtype), + FQDN: lowerCaseQuestion, + QType: dns.Type(originalQuestion.Qtype), } // Get remote address of request. @@ -118,9 +127,9 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) } // Check the Query Class. - if question.Qclass != dns.ClassINET { + if originalQuestion.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) + tracer.Warningf("nameserver: only IN record requests are supported but received QClass %d, returning NXDOMAIN", originalQuestion.Qclass) return reply(nsutil.Refused("unsupported qclass")) } @@ -243,6 +252,11 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) // Save dns request as open. defer network.SaveOpenDNSRequest(conn) + // Revert back to non-standard question format, if we had to convert. + if nonStandardQuestionFormat { + rrCache.ReplaceAnswerNames(originalQuestion.Name) + } + // Reply with successful response. tracer.Infof("nameserver: returning %s response for %s to %s", conn.Verdict.Verb(), q.ID(), conn.Process()) return reply(rrCache, conn, rrCache) diff --git a/resolver/rrcache.go b/resolver/rrcache.go index 9411e9db..5a277987 100644 --- a/resolver/rrcache.go +++ b/resolver/rrcache.go @@ -24,7 +24,7 @@ type RRCache struct { Question dns.Type // constant RCode int // constant - Answer []dns.RR // constant + Answer []dns.RR // mutable (mixing, non-standard formats) Ns []dns.RR // constant Extra []dns.RR // constant TTL int64 // constant @@ -273,25 +273,33 @@ func (rrCache *RRCache) ShallowCopy() *RRCache { } } +// ReplaceAnswerNames is a helper function that replaces all answer names, that +// match the query domain, with another value. This is used to support handling +// non-standard query names, which are resolved normalized, but have to be +// reverted back for the origin non-standard query name in order for the +// clients to recognize the response. +func (rrCache *RRCache) ReplaceAnswerNames(fqdn string) { + for _, answer := range rrCache.Answer { + if answer.Header().Name == rrCache.Domain { + answer.Header().Name = fqdn + } + } +} + // 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, rrCache.RCode) + reply.Answer = rrCache.Answer reply.Ns = rrCache.Ns reply.Extra = rrCache.Extra - if len(rrCache.Answer) > 0 { - // Copy answers, as we randomize their order a little. - reply.Answer = make([]dns.RR, len(rrCache.Answer)) - copy(reply.Answer, rrCache.Answer) - - // Randomize the order of the answer records a little to allow dumb clients - // (who only look at the first record) to reliably connect. - for i := range reply.Answer { - j := rand.Intn(i + 1) - reply.Answer[i], reply.Answer[j] = reply.Answer[j], reply.Answer[i] - } + // Randomize the order of the answer records a little to allow dumb clients + // (who only look at the first record) to reliably connect. + for i := range reply.Answer { + j := rand.Intn(i + 1) + reply.Answer[i], reply.Answer[j] = reply.Answer[j], reply.Answer[i] } return reply From 7d9454f29a4b13cd75dbd0c4b730a20ed24d0307 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 30 Sep 2020 17:16:45 +0200 Subject: [PATCH 2/2] Remove RRCache locking and clean up attributes --- resolver/resolve.go | 6 +-- resolver/resolver-mdns.go | 4 +- resolver/rrcache.go | 90 ++++++++++++++++++--------------------- 3 files changed, 46 insertions(+), 54 deletions(-) diff --git a/resolver/resolve.go b/resolver/resolve.go index 67b95ec6..b917968a 100644 --- a/resolver/resolve.go +++ b/resolver/resolve.go @@ -215,9 +215,7 @@ func checkCache(ctx context.Context, q *Query) *RRCache { // Check if the cache will expire soon and start an async request. if rrCache.ExpiresSoon() { // Set flag that we are refreshing this entry. - rrCache.Lock() - rrCache.requestingNew = true - rrCache.Unlock() + rrCache.RequestingNew = true log.Tracer(ctx).Tracef( "resolver: cache for %s will expire in %s, refreshing async now", @@ -397,7 +395,7 @@ resolveLoop: // Check if we want to use an older cache instead. if oldCache != nil { - oldCache.isBackup = true + oldCache.IsBackup = true switch { case err != nil: diff --git a/resolver/resolver-mdns.go b/resolver/resolver-mdns.go index 21a91a87..166cebfb 100644 --- a/resolver/resolver-mdns.go +++ b/resolver/resolver-mdns.go @@ -196,9 +196,9 @@ func handleMDNSMessages(ctx context.Context, messages chan *dns.Msg) error { if saveFullRequest { // get from database rrCache, err = GetRRCache(question.Name, dns.Type(question.Qtype)) - // if we have no cached entry, or it has been updated less more than two seconds ago, or if it expired: + // if we have no cached entry, or it has been updated more than two seconds ago, or if it expired: // create new and do not append - if err != nil || rrCache.updated < time.Now().Add(-2*time.Second).Unix() || rrCache.TTL < time.Now().Unix() { + if err != nil || rrCache.Modified < time.Now().Add(-2*time.Second).Unix() || rrCache.Expired() { rrCache = &RRCache{ Domain: question.Name, Question: dns.Type(question.Qtype), diff --git a/resolver/rrcache.go b/resolver/rrcache.go index 5a277987..1eee7b4a 100644 --- a/resolver/rrcache.go +++ b/resolver/rrcache.go @@ -5,7 +5,6 @@ import ( "fmt" "math/rand" "net" - "sync" "time" "github.com/safing/portbase/log" @@ -15,31 +14,37 @@ import ( "github.com/miekg/dns" ) -// RRCache is used to cache DNS data +// RRCache is a single-use structure to hold a DNS response. +// Persistence is handled through NameRecords because of a limitation of the +// underlying dns library. //nolint:maligned // TODO type RRCache struct { - sync.Mutex + // Respnse Header + Domain string + Question dns.Type + RCode int - Domain string // constant - Question dns.Type // constant - RCode int // constant + // Response Content + Answer []dns.RR + Ns []dns.RR + Extra []dns.RR + TTL int64 - Answer []dns.RR // mutable (mixing, non-standard formats) - Ns []dns.RR // constant - Extra []dns.RR // constant - TTL int64 // constant + // Source Information + Server string + ServerScope int8 + ServerInfo string - Server string // constant - ServerScope int8 // constant - ServerInfo string // constant + // Metadata about the request and handling + ServedFromCache bool + RequestingNew bool + IsBackup bool + Filtered bool + FilteredEntries []string - servedFromCache bool // mutable - requestingNew bool // mutable - isBackup bool // mutable - Filtered bool // mutable - FilteredEntries []string // mutable - - updated int64 // mutable + // Modified holds when this entry was last changed, ie. saved to database. + // This field is only populated when the entry comes from the cache. + Modified int64 } // ID returns the ID of the RRCache consisting of the domain and question type. @@ -59,9 +64,6 @@ func (rrCache *RRCache) ExpiresSoon() bool { // Clean sets all TTLs to 17 and sets cache expiry with specified minimum. func (rrCache *RRCache) Clean(minExpires uint32) { - rrCache.Lock() - defer rrCache.Unlock() - var lowestTTL uint32 = 0xFFFFFFFF var header *dns.RR_Header @@ -200,7 +202,8 @@ func GetRRCache(domain string, question dns.Type) (*RRCache, error) { rrCache.Server = nameRecord.Server rrCache.ServerScope = nameRecord.ServerScope rrCache.ServerInfo = nameRecord.ServerInfo - rrCache.servedFromCache = true + rrCache.ServedFromCache = true + rrCache.Modified = nameRecord.Meta().Modified return rrCache, nil } @@ -217,26 +220,16 @@ func parseRR(section []dns.RR, entry string) []dns.RR { return section } -// ServedFromCache marks the RRCache as served from cache. -func (rrCache *RRCache) ServedFromCache() bool { - return rrCache.servedFromCache -} - -// RequestingNew informs that it has expired and new RRs are being fetched. -func (rrCache *RRCache) RequestingNew() bool { - return rrCache.requestingNew -} - // Flags formats ServedFromCache and RequestingNew to a condensed, flag-like format. func (rrCache *RRCache) Flags() string { var s string - if rrCache.servedFromCache { + if rrCache.ServedFromCache { s += "C" } - if rrCache.requestingNew { + if rrCache.RequestingNew { s += "R" } - if rrCache.isBackup { + if rrCache.IsBackup { s += "B" } if rrCache.Filtered { @@ -255,21 +248,22 @@ func (rrCache *RRCache) ShallowCopy() *RRCache { Domain: rrCache.Domain, Question: rrCache.Question, RCode: rrCache.RCode, - Answer: rrCache.Answer, - Ns: rrCache.Ns, - Extra: rrCache.Extra, - TTL: rrCache.TTL, + + Answer: rrCache.Answer, + Ns: rrCache.Ns, + Extra: rrCache.Extra, + TTL: rrCache.TTL, Server: rrCache.Server, ServerScope: rrCache.ServerScope, ServerInfo: rrCache.ServerInfo, - updated: rrCache.updated, - servedFromCache: rrCache.servedFromCache, - requestingNew: rrCache.requestingNew, - isBackup: rrCache.isBackup, + ServedFromCache: rrCache.ServedFromCache, + RequestingNew: rrCache.RequestingNew, + IsBackup: rrCache.IsBackup, Filtered: rrCache.Filtered, FilteredEntries: rrCache.FilteredEntries, + Modified: rrCache.Modified, } } @@ -308,7 +302,7 @@ func (rrCache *RRCache) ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns // 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 { + if rrCache.ServedFromCache { extra = addExtra(ctx, extra, "served from cache, resolved by "+rrCache.ServerInfo) } else { extra = addExtra(ctx, extra, "freshly resolved by "+rrCache.ServerInfo) @@ -320,10 +314,10 @@ func (rrCache *RRCache) GetExtraRRs(ctx context.Context, query *dns.Msg) (extra } else { extra = addExtra(ctx, extra, fmt.Sprintf("record valid for %s", time.Until(time.Unix(rrCache.TTL, 0)).Round(time.Second))) } - if rrCache.requestingNew { + if rrCache.RequestingNew { extra = addExtra(ctx, extra, "async request to refresh the cache has been started") } - if rrCache.isBackup { + if rrCache.IsBackup { extra = addExtra(ctx, extra, "this record is served because a fresh request failed") }