mirror of
https://github.com/safing/portmaster
synced 2025-09-05 03:59:11 +00:00
Merge pull request #163 from safing/fix/dns-query-case
Support non-standard format on dns query name
This commit is contained in:
commit
f60d239d6f
4 changed files with 84 additions and 70 deletions
|
@ -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
|
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.
|
// 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{
|
q := &resolver.Query{
|
||||||
FQDN: question.Name,
|
FQDN: lowerCaseQuestion,
|
||||||
QType: dns.Type(question.Qtype),
|
QType: dns.Type(originalQuestion.Qtype),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get remote address of request.
|
// Get remote address of request.
|
||||||
|
@ -118,9 +127,9 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the Query Class.
|
// Check the Query Class.
|
||||||
if question.Qclass != dns.ClassINET {
|
if originalQuestion.Qclass != dns.ClassINET {
|
||||||
// we only serve IN records, return nxdomain
|
// 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"))
|
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.
|
// Save dns request as open.
|
||||||
defer network.SaveOpenDNSRequest(conn)
|
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.
|
// Reply with successful response.
|
||||||
tracer.Infof("nameserver: returning %s response for %s to %s", conn.Verdict.Verb(), q.ID(), conn.Process())
|
tracer.Infof("nameserver: returning %s response for %s to %s", conn.Verdict.Verb(), q.ID(), conn.Process())
|
||||||
return reply(rrCache, conn, rrCache)
|
return reply(rrCache, conn, rrCache)
|
||||||
|
|
|
@ -215,9 +215,7 @@ func checkCache(ctx context.Context, q *Query) *RRCache {
|
||||||
// Check if the cache will expire soon and start an async request.
|
// Check if the cache will expire soon and start an async request.
|
||||||
if rrCache.ExpiresSoon() {
|
if rrCache.ExpiresSoon() {
|
||||||
// Set flag that we are refreshing this entry.
|
// Set flag that we are refreshing this entry.
|
||||||
rrCache.Lock()
|
rrCache.RequestingNew = true
|
||||||
rrCache.requestingNew = true
|
|
||||||
rrCache.Unlock()
|
|
||||||
|
|
||||||
log.Tracer(ctx).Tracef(
|
log.Tracer(ctx).Tracef(
|
||||||
"resolver: cache for %s will expire in %s, refreshing async now",
|
"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.
|
// Check if we want to use an older cache instead.
|
||||||
if oldCache != nil {
|
if oldCache != nil {
|
||||||
oldCache.isBackup = true
|
oldCache.IsBackup = true
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
|
|
|
@ -196,9 +196,9 @@ func handleMDNSMessages(ctx context.Context, messages chan *dns.Msg) error {
|
||||||
if saveFullRequest {
|
if saveFullRequest {
|
||||||
// get from database
|
// get from database
|
||||||
rrCache, err = GetRRCache(question.Name, dns.Type(question.Qtype))
|
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
|
// 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{
|
rrCache = &RRCache{
|
||||||
Domain: question.Name,
|
Domain: question.Name,
|
||||||
Question: dns.Type(question.Qtype),
|
Question: dns.Type(question.Qtype),
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
|
@ -15,31 +14,37 @@ import (
|
||||||
"github.com/miekg/dns"
|
"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
|
//nolint:maligned // TODO
|
||||||
type RRCache struct {
|
type RRCache struct {
|
||||||
sync.Mutex
|
// Respnse Header
|
||||||
|
Domain string
|
||||||
|
Question dns.Type
|
||||||
|
RCode int
|
||||||
|
|
||||||
Domain string // constant
|
// Response Content
|
||||||
Question dns.Type // constant
|
Answer []dns.RR
|
||||||
RCode int // constant
|
Ns []dns.RR
|
||||||
|
Extra []dns.RR
|
||||||
|
TTL int64
|
||||||
|
|
||||||
Answer []dns.RR // constant
|
// Source Information
|
||||||
Ns []dns.RR // constant
|
Server string
|
||||||
Extra []dns.RR // constant
|
ServerScope int8
|
||||||
TTL int64 // constant
|
ServerInfo string
|
||||||
|
|
||||||
Server string // constant
|
// Metadata about the request and handling
|
||||||
ServerScope int8 // constant
|
ServedFromCache bool
|
||||||
ServerInfo string // constant
|
RequestingNew bool
|
||||||
|
IsBackup bool
|
||||||
|
Filtered bool
|
||||||
|
FilteredEntries []string
|
||||||
|
|
||||||
servedFromCache bool // mutable
|
// Modified holds when this entry was last changed, ie. saved to database.
|
||||||
requestingNew bool // mutable
|
// This field is only populated when the entry comes from the cache.
|
||||||
isBackup bool // mutable
|
Modified int64
|
||||||
Filtered bool // mutable
|
|
||||||
FilteredEntries []string // mutable
|
|
||||||
|
|
||||||
updated int64 // mutable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ID returns the ID of the RRCache consisting of the domain and question type.
|
// 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.
|
// Clean sets all TTLs to 17 and sets cache expiry with specified minimum.
|
||||||
func (rrCache *RRCache) Clean(minExpires uint32) {
|
func (rrCache *RRCache) Clean(minExpires uint32) {
|
||||||
rrCache.Lock()
|
|
||||||
defer rrCache.Unlock()
|
|
||||||
|
|
||||||
var lowestTTL uint32 = 0xFFFFFFFF
|
var lowestTTL uint32 = 0xFFFFFFFF
|
||||||
var header *dns.RR_Header
|
var header *dns.RR_Header
|
||||||
|
|
||||||
|
@ -200,7 +202,8 @@ func GetRRCache(domain string, question dns.Type) (*RRCache, error) {
|
||||||
rrCache.Server = nameRecord.Server
|
rrCache.Server = nameRecord.Server
|
||||||
rrCache.ServerScope = nameRecord.ServerScope
|
rrCache.ServerScope = nameRecord.ServerScope
|
||||||
rrCache.ServerInfo = nameRecord.ServerInfo
|
rrCache.ServerInfo = nameRecord.ServerInfo
|
||||||
rrCache.servedFromCache = true
|
rrCache.ServedFromCache = true
|
||||||
|
rrCache.Modified = nameRecord.Meta().Modified
|
||||||
return rrCache, nil
|
return rrCache, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,26 +220,16 @@ func parseRR(section []dns.RR, entry string) []dns.RR {
|
||||||
return section
|
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.
|
// Flags formats ServedFromCache and RequestingNew to a condensed, flag-like format.
|
||||||
func (rrCache *RRCache) Flags() string {
|
func (rrCache *RRCache) Flags() string {
|
||||||
var s string
|
var s string
|
||||||
if rrCache.servedFromCache {
|
if rrCache.ServedFromCache {
|
||||||
s += "C"
|
s += "C"
|
||||||
}
|
}
|
||||||
if rrCache.requestingNew {
|
if rrCache.RequestingNew {
|
||||||
s += "R"
|
s += "R"
|
||||||
}
|
}
|
||||||
if rrCache.isBackup {
|
if rrCache.IsBackup {
|
||||||
s += "B"
|
s += "B"
|
||||||
}
|
}
|
||||||
if rrCache.Filtered {
|
if rrCache.Filtered {
|
||||||
|
@ -255,21 +248,35 @@ func (rrCache *RRCache) ShallowCopy() *RRCache {
|
||||||
Domain: rrCache.Domain,
|
Domain: rrCache.Domain,
|
||||||
Question: rrCache.Question,
|
Question: rrCache.Question,
|
||||||
RCode: rrCache.RCode,
|
RCode: rrCache.RCode,
|
||||||
Answer: rrCache.Answer,
|
|
||||||
Ns: rrCache.Ns,
|
Answer: rrCache.Answer,
|
||||||
Extra: rrCache.Extra,
|
Ns: rrCache.Ns,
|
||||||
TTL: rrCache.TTL,
|
Extra: rrCache.Extra,
|
||||||
|
TTL: rrCache.TTL,
|
||||||
|
|
||||||
Server: rrCache.Server,
|
Server: rrCache.Server,
|
||||||
ServerScope: rrCache.ServerScope,
|
ServerScope: rrCache.ServerScope,
|
||||||
ServerInfo: rrCache.ServerInfo,
|
ServerInfo: rrCache.ServerInfo,
|
||||||
|
|
||||||
updated: rrCache.updated,
|
ServedFromCache: rrCache.ServedFromCache,
|
||||||
servedFromCache: rrCache.servedFromCache,
|
RequestingNew: rrCache.RequestingNew,
|
||||||
requestingNew: rrCache.requestingNew,
|
IsBackup: rrCache.IsBackup,
|
||||||
isBackup: rrCache.isBackup,
|
|
||||||
Filtered: rrCache.Filtered,
|
Filtered: rrCache.Filtered,
|
||||||
FilteredEntries: rrCache.FilteredEntries,
|
FilteredEntries: rrCache.FilteredEntries,
|
||||||
|
Modified: rrCache.Modified,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,20 +285,15 @@ func (rrCache *RRCache) ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns
|
||||||
// reply to query
|
// reply to query
|
||||||
reply := new(dns.Msg)
|
reply := new(dns.Msg)
|
||||||
reply.SetRcode(request, rrCache.RCode)
|
reply.SetRcode(request, rrCache.RCode)
|
||||||
|
reply.Answer = rrCache.Answer
|
||||||
reply.Ns = rrCache.Ns
|
reply.Ns = rrCache.Ns
|
||||||
reply.Extra = rrCache.Extra
|
reply.Extra = rrCache.Extra
|
||||||
|
|
||||||
if len(rrCache.Answer) > 0 {
|
// Randomize the order of the answer records a little to allow dumb clients
|
||||||
// Copy answers, as we randomize their order a little.
|
// (who only look at the first record) to reliably connect.
|
||||||
reply.Answer = make([]dns.RR, len(rrCache.Answer))
|
for i := range reply.Answer {
|
||||||
copy(reply.Answer, rrCache.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
|
return reply
|
||||||
|
@ -300,7 +302,7 @@ func (rrCache *RRCache) ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns
|
||||||
// GetExtraRRs returns a slice of RRs with additional informational records.
|
// GetExtraRRs returns a slice of RRs with additional informational records.
|
||||||
func (rrCache *RRCache) GetExtraRRs(ctx context.Context, query *dns.Msg) (extra []dns.RR) {
|
func (rrCache *RRCache) GetExtraRRs(ctx context.Context, query *dns.Msg) (extra []dns.RR) {
|
||||||
// Add cache status and source of data.
|
// Add cache status and source of data.
|
||||||
if rrCache.servedFromCache {
|
if rrCache.ServedFromCache {
|
||||||
extra = addExtra(ctx, extra, "served from cache, resolved by "+rrCache.ServerInfo)
|
extra = addExtra(ctx, extra, "served from cache, resolved by "+rrCache.ServerInfo)
|
||||||
} else {
|
} else {
|
||||||
extra = addExtra(ctx, extra, "freshly resolved by "+rrCache.ServerInfo)
|
extra = addExtra(ctx, extra, "freshly resolved by "+rrCache.ServerInfo)
|
||||||
|
@ -312,10 +314,10 @@ func (rrCache *RRCache) GetExtraRRs(ctx context.Context, query *dns.Msg) (extra
|
||||||
} else {
|
} else {
|
||||||
extra = addExtra(ctx, extra, fmt.Sprintf("record valid for %s", time.Until(time.Unix(rrCache.TTL, 0)).Round(time.Second)))
|
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")
|
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")
|
extra = addExtra(ctx, extra, "this record is served because a fresh request failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue