Merge branch 'develop' into feature/ui-revamp

This commit is contained in:
Patrick Pacher
2020-09-29 09:19:15 +02:00
26 changed files with 731 additions and 390 deletions

View File

@@ -32,8 +32,8 @@ var (
// `dot://1.0.0.2:853?verify=cloudflare-dns.com&name=Cloudflare&blockedif=zeroip`,
// AdGuard (encrypted DNS, default flavor)
// `dot://176.103.130.130:853?verify=dns.adguard.com&name=AdGuard&blockedif=zeroip`,
// `dot://176.103.130.131:853?verify=dns.adguard.com&name=AdGuard&blockedif=zeroip`,
// `dot://94.140.14.14:853?verify=dns.adguard.com&name=AdGuard&blockedif=zeroip`,
// `dot://94.140.15.15:853?verify=dns.adguard.com&name=AdGuard&blockedif=zeroip`,
// Foundation for Applied Privacy (encrypted DNS)
// `dot://94.130.106.88:853?verify=dot1.applied-privacy.net&name=AppliedPrivacy`,
@@ -48,8 +48,8 @@ var (
// `dns://1.0.0.2:53?name=Cloudflare&blockedif=zeroip`,
// AdGuard (plain DNS, default flavor)
// `dns://176.103.130.130&name=AdGuard&blockedif=zeroip`,
// `dns://176.103.130.131&name=AdGuard&blockedif=zeroip`,
// `dns://94.140.14.14&name=AdGuard&blockedif=zeroip`,
// `dns://94.140.15.15&name=AdGuard&blockedif=zeroip`,
}
CfgOptionNameServersKey = "dns/nameservers"
@@ -96,7 +96,7 @@ IP:
always use the IP address and _not_ the domain name!
Port:
always add the port!
optionally define a custom port
Parameters:
name: give your DNS Server a name that is used for messages and logs

View File

@@ -28,6 +28,7 @@ type NameRecord struct {
Domain string
Question string
RCode int
Answer []string
Ns []string
Extra []string
@@ -35,6 +36,7 @@ type NameRecord struct {
Server string
ServerScope int8
ServerInfo string
}
func makeNameRecordKey(domain string, question string) string {
@@ -85,48 +87,13 @@ func (rec *NameRecord) Save() error {
return recordDatabase.PutNew(rec)
}
func clearNameCache(_ context.Context, _ interface{}) error {
log.Debugf("resolver: name cache clearing started...")
for {
done, err := removeNameEntries(10000)
if err != nil {
return err
}
if done {
return nil
}
}
}
func removeNameEntries(batchSize int) (bool, error) {
iter, err := recordDatabase.Query(query.New(nameRecordsKeyPrefix))
func clearNameCache(ctx context.Context, _ interface{}) error {
log.Debugf("resolver: dns cache clearing started...")
n, err := recordDatabase.Purge(ctx, query.New(nameRecordsKeyPrefix))
if err != nil {
return false, err
return err
}
keys := make([]string, 0, batchSize)
var cnt int
for r := range iter.Next {
cnt++
keys = append(keys, r.Key())
if cnt == batchSize {
break
}
}
iter.Cancel()
for _, key := range keys {
if err := recordDatabase.Delete(key); err != nil {
log.Warningf("resolver: failed to remove name cache entry %q: %s", key, err)
}
}
log.Debugf("resolver: successfully removed %d name cache entries", cnt)
// if we removed less entries that the batch size we
// are done and no more entries exist
return cnt < batchSize, nil
log.Debugf("resolver: cleared %d entries in dns cache", n)
return nil
}

View File

@@ -46,7 +46,8 @@ var (
)
const (
minTTL = 60 // 1 Minute
minTTL = 60 // 1 Minute
refreshTTL = minTTL / 2
minMDnsTTL = 60 // 1 Minute
maxTTL = 24 * 60 * 60 // 24 hours
)
@@ -120,6 +121,9 @@ func Resolve(ctx context.Context, q *Query) (rrCache *RRCache, err error) {
}
// log
// try adding a context tracer
ctx, tracer := log.AddTracer(ctx)
defer tracer.Submit()
log.Tracer(ctx).Tracef("resolver: resolving %s%s", q.FQDN, q.QType)
// check query compliance
@@ -130,8 +134,7 @@ func Resolve(ctx context.Context, q *Query) (rrCache *RRCache, err error) {
// check the cache
if !q.NoCaching {
rrCache = checkCache(ctx, q)
if rrCache != nil {
rrCache.MixAnswers()
if rrCache != nil && !rrCache.Expired() {
return rrCache, nil
}
@@ -140,8 +143,7 @@ func Resolve(ctx context.Context, q *Query) (rrCache *RRCache, err error) {
if markRequestFinished == nil {
// we waited for another request, recheck the cache!
rrCache = checkCache(ctx, q)
if rrCache != nil {
rrCache.MixAnswers()
if rrCache != nil && !rrCache.Expired() {
return rrCache, nil
}
log.Tracer(ctx).Debugf("resolver: waited for another %s%s query, but cache missed!", q.FQDN, q.QType)
@@ -149,17 +151,22 @@ func Resolve(ctx context.Context, q *Query) (rrCache *RRCache, err error) {
} else {
// we are the first!
defer markRequestFinished()
}
}
return resolveAndCache(ctx, q)
return resolveAndCache(ctx, q, rrCache)
}
func checkCache(ctx context.Context, q *Query) *RRCache {
// Never ask cache for connectivity domains.
if netenv.IsConnectivityDomain(q.FQDN) {
return nil
}
// Get data from cache.
rrCache, err := GetRRCache(q.FQDN, q.QType)
// failed to get from cache
// Return if entry is not in cache.
if err != nil {
if err != database.ErrNotFound {
log.Tracer(ctx).Warningf("resolver: getting RRCache %s%s from database failed: %s", q.FQDN, q.QType.String(), err)
@@ -167,21 +174,21 @@ func checkCache(ctx context.Context, q *Query) *RRCache {
return nil
}
// get resolver that rrCache was resolved with
// Get the resolver that the rrCache was resolved with.
resolver := getActiveResolverByIDWithLocking(rrCache.Server)
if resolver == nil {
log.Tracer(ctx).Debugf("resolver: ignoring RRCache %s%s because source server %s has been removed", q.FQDN, q.QType.String(), rrCache.Server)
return nil
}
// check compliance of resolver
// Check compliance of the resolver, return if non-compliant.
err = resolver.checkCompliance(ctx, q)
if err != nil {
log.Tracer(ctx).Debugf("resolver: cached entry for %s%s does not comply to query parameters: %s", q.FQDN, q.QType.String(), err)
return nil
}
// check if we want to reset the cache
// Check if we want to reset the cache for this entry.
if shouldResetCache(q) {
err := DeleteNameRecord(q.FQDN, q.QType.String())
switch {
@@ -195,27 +202,39 @@ func checkCache(ctx context.Context, q *Query) *RRCache {
return nil
}
// check if expired
// Check if the cache has already expired.
// We still return the cache, if it isn't NXDomain, as it will be used if the
// new query fails.
if rrCache.Expired() {
if netenv.IsConnectivityDomain(rrCache.Domain) {
// do not use cache, resolve immediately
return nil
if rrCache.RCode == dns.RcodeSuccess {
return rrCache
}
return nil
}
// 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()
log.Tracer(ctx).Tracef(
"resolver: using expired RR from cache (since %s), refreshing async now",
time.Since(time.Unix(rrCache.TTL, 0)),
"resolver: cache for %s will expire in %s, refreshing async now",
q.ID(),
time.Until(time.Unix(rrCache.TTL, 0)).Round(time.Second),
)
// resolve async
module.StartWorker("resolve async", func(ctx context.Context) error {
_, err := resolveAndCache(ctx, q)
ctx, tracer := log.AddTracer(ctx)
defer tracer.Submit()
tracer.Debugf("resolver: resolving %s async", q.ID())
_, err := resolveAndCache(ctx, q, nil)
if err != nil {
log.Warningf("resolver: async query for %s%s failed: %s", q.FQDN, q.QType, err)
tracer.Warningf("resolver: async query for %s failed: %s", q.ID(), err)
} else {
tracer.Debugf("resolver: async query for %s succeeded", q.ID())
}
return nil
})
@@ -225,7 +244,7 @@ func checkCache(ctx context.Context, q *Query) *RRCache {
log.Tracer(ctx).Tracef(
"resolver: using cached RR (expires in %s)",
time.Until(time.Unix(rrCache.TTL, 0)),
time.Until(time.Unix(rrCache.TTL, 0)).Round(time.Second),
)
return rrCache
}
@@ -290,7 +309,7 @@ retry:
}
}
func resolveAndCache(ctx context.Context, q *Query) (rrCache *RRCache, err error) { //nolint:gocognit
func resolveAndCache(ctx context.Context, q *Query, oldCache *RRCache) (rrCache *RRCache, err error) { //nolint:gocognit,gocyclo
// get resolvers
resolvers, tryAll := GetResolversInScope(ctx, q)
if len(resolvers) == 0 {
@@ -358,31 +377,51 @@ resolveLoop:
// Defensive: This should normally not happen.
continue
}
// Check if request succeeded and whether we should try another resolver.
if rrCache.RCode != dns.RcodeSuccess && tryAll {
continue
}
break resolveLoop
}
}
// check for error
// Post-process errors
if err != nil {
// tried all resolvers, possibly twice
if i > 1 {
return nil, fmt.Errorf("all %d query-compliant resolvers failed, last error: %s", len(resolvers), err)
err = fmt.Errorf("all %d query-compliant resolvers failed, last error: %s", len(resolvers), err)
}
} else if rrCache == nil /* defensive */ {
err = ErrNotFound
}
// Check if we want to use an older cache instead.
if oldCache != nil {
oldCache.isBackup = true
switch {
case err != nil:
// There was an error during resolving, return the old cache entry instead.
log.Tracer(ctx).Debugf("resolver: serving backup cache of %s because query failed: %s", q.ID(), err)
return oldCache, nil
case !rrCache.Cacheable():
// The new result is NXDomain, return the old cache entry instead.
log.Tracer(ctx).Debugf("resolver: serving backup cache of %s because fresh response is NXDomain", q.ID())
return oldCache, nil
}
}
// Return error, if there is one.
if err != nil {
return nil, err
}
// check for result
if rrCache == nil /* defensive */ {
return nil, ErrNotFound
}
// cache if enabled
if !q.NoCaching {
// persist to database
// Save the new entry if cache is enabled.
if !q.NoCaching && rrCache.Cacheable() {
rrCache.Clean(minTTL)
err = rrCache.Save()
if err != nil {
log.Warningf("resolver: failed to cache RR for %s%s: %s", q.FQDN, q.QType.String(), err)
log.Tracer(ctx).Warningf("resolver: failed to cache RR for %s: %s", q.ID(), err)
}
}

View File

@@ -23,6 +23,7 @@ var (
Server: ServerSourceEnv,
ServerType: ServerTypeEnv,
ServerIPScope: netutils.SiteLocal,
ServerInfo: "Portmaster environment",
Source: ServerSourceEnv,
Conn: &envResolverConn{},
}
@@ -110,10 +111,12 @@ func (er *envResolverConn) makeRRCache(q *Query, answers []dns.RR) *RRCache {
return &RRCache{
Domain: q.FQDN,
Question: q.QType,
RCode: dns.RcodeSuccess,
Answer: answers,
Extra: []dns.RR{internalSpecialUseComment}, // Always add comment about this TLD.
Server: envResolver.Server,
ServerScope: envResolver.ServerIPScope,
ServerInfo: envResolver.ServerInfo,
}
}

View File

@@ -34,6 +34,7 @@ var (
Server: ServerSourceMDNS,
ServerType: ServerTypeDNS,
ServerIPScope: netutils.SiteLocal,
ServerInfo: "mDNS resolver",
Source: ServerSourceMDNS,
Conn: &mDNSResolverConn{},
}
@@ -201,8 +202,10 @@ func handleMDNSMessages(ctx context.Context, messages chan *dns.Msg) error {
rrCache = &RRCache{
Domain: question.Name,
Question: dns.Type(question.Qtype),
RCode: dns.RcodeSuccess,
Server: mDNSResolver.Server,
ServerScope: mDNSResolver.ServerIPScope,
ServerInfo: mDNSResolver.ServerInfo,
}
}
}
@@ -301,9 +304,11 @@ func handleMDNSMessages(ctx context.Context, messages chan *dns.Msg) error {
rrCache = &RRCache{
Domain: v.Header().Name,
Question: dns.Type(v.Header().Class),
RCode: dns.RcodeSuccess,
Answer: []dns.RR{v},
Server: mDNSResolver.Server,
ServerScope: mDNSResolver.ServerIPScope,
ServerInfo: mDNSResolver.ServerInfo,
}
rrCache.Clean(minMDnsTTL)
err := rrCache.Save()
@@ -416,7 +421,15 @@ func queryMulticastDNS(ctx context.Context, q *Query) (*RRCache, error) {
}
}
return nil, ErrNotFound
// Respond with NXDomain.
return &RRCache{
Domain: q.FQDN,
Question: q.QType,
RCode: dns.RcodeNameError,
Server: mDNSResolver.Server,
ServerScope: mDNSResolver.ServerIPScope,
ServerInfo: mDNSResolver.ServerInfo,
}, nil
}
func cleanSavedQuestions() {

View File

@@ -81,11 +81,13 @@ func (pr *PlainResolver) Query(ctx context.Context, q *Query) (*RRCache, error)
newRecord := &RRCache{
Domain: q.FQDN,
Question: q.QType,
RCode: reply.Rcode,
Answer: reply.Answer,
Ns: reply.Ns,
Extra: reply.Extra,
Server: pr.resolver.Server,
ServerScope: pr.resolver.ServerIPScope,
ServerInfo: pr.resolver.ServerInfo,
}
// TODO: check if reply.Answer is valid

View File

@@ -3,6 +3,8 @@ package resolver
import (
"context"
"crypto/tls"
"errors"
"io"
"net"
"sync/atomic"
"time"
@@ -26,6 +28,8 @@ type TCPResolver struct {
dnsClient *dns.Client
clientStarted *abool.AtomicBool
clientHeartbeat chan struct{}
clientCancel func()
connInstanceID *uint32
queries chan *dns.Msg
inFlightQueries map[uint16]*InFlightQuery
@@ -46,11 +50,13 @@ func (ifq *InFlightQuery) MakeCacheRecord(reply *dns.Msg) *RRCache {
return &RRCache{
Domain: ifq.Query.FQDN,
Question: ifq.Query.QType,
RCode: reply.Rcode,
Answer: reply.Answer,
Ns: reply.Ns,
Extra: reply.Extra,
Server: ifq.Resolver.Server,
ServerScope: ifq.Resolver.ServerIPScope,
ServerInfo: ifq.Resolver.ServerInfo,
}
}
@@ -67,10 +73,12 @@ func NewTCPResolver(resolver *Resolver) *TCPResolver {
Timeout: defaultConnectTimeout,
WriteTimeout: tcpWriteTimeout,
},
clientStarted: abool.New(),
clientHeartbeat: make(chan struct{}),
clientCancel: func() {},
connInstanceID: &instanceID,
queries: make(chan *dns.Msg, 100),
inFlightQueries: make(map[uint16]*InFlightQuery),
clientStarted: abool.New(),
}
}
@@ -145,6 +153,7 @@ func (tr *TCPResolver) Query(ctx context.Context, q *Query) (*RRCache, error) {
// submit to client
inFlight := tr.submitQuery(ctx, q)
if inFlight == nil {
tr.checkClientStatus()
return nil, ErrTimeout
}
@@ -152,6 +161,7 @@ func (tr *TCPResolver) Query(ctx context.Context, q *Query) (*RRCache, error) {
select {
case reply = <-inFlight.Response:
case <-time.After(defaultRequestTimeout):
tr.checkClientStatus()
return nil, ErrTimeout
}
@@ -167,6 +177,22 @@ func (tr *TCPResolver) Query(ctx context.Context, q *Query) (*RRCache, error) {
return inFlight.MakeCacheRecord(reply), nil
}
func (tr *TCPResolver) checkClientStatus() {
// Get client cancel function before waiting in order to not immediately
// cancel a new client.
tr.Lock()
cancelClient := tr.clientCancel
tr.Unlock()
// Check if the client is alive with the heartbeat, if not shut it down.
select {
case tr.clientHeartbeat <- struct{}{}:
case <-time.After(defaultRequestTimeout):
log.Warningf("resolver: heartbeat failed for %s dns client, stopping", tr.resolver.GetName())
cancelClient()
}
}
type tcpResolverConnMgr struct {
tr *TCPResolver
responses chan *dns.Msg
@@ -184,8 +210,14 @@ func (tr *TCPResolver) startClient() {
}
func (mgr *tcpResolverConnMgr) run(workerCtx context.Context) error {
mgr.tr.clientStarted.Set()
defer mgr.shutdown()
mgr.tr.clientStarted.Set()
// Create additional cancel function for this worker.
workerCtx, cancelWorker := context.WithCancel(workerCtx)
mgr.tr.Lock()
mgr.tr.clientCancel = cancelWorker
mgr.tr.Unlock()
// connection lifecycle loop
for {
@@ -208,7 +240,7 @@ func (mgr *tcpResolverConnMgr) run(workerCtx context.Context) error {
}
// create connection
conn, connClosing, connCtx, cancelConnCtx := mgr.establishConnection(workerCtx)
conn, connClosing, connCtx, cancelConnCtx := mgr.establishConnection()
if conn == nil {
mgr.failCnt++
continue
@@ -293,7 +325,7 @@ func (mgr *tcpResolverConnMgr) waitForWork(workerCtx context.Context) (proceed b
return true
}
func (mgr *tcpResolverConnMgr) establishConnection(workerCtx context.Context) (
func (mgr *tcpResolverConnMgr) establishConnection() (
conn *dns.Conn,
connClosing *abool.AtomicBool,
connCtx context.Context,
@@ -313,10 +345,21 @@ func (mgr *tcpResolverConnMgr) establishConnection(workerCtx context.Context) (
log.Debugf("resolver: failed to connect to %s (%s)", mgr.tr.resolver.GetName(), mgr.tr.resolver.ServerAddress)
return nil, nil, nil, nil
}
connCtx, cancelConnCtx = context.WithCancel(workerCtx)
connCtx, cancelConnCtx = context.WithCancel(context.Background())
connClosing = abool.New()
log.Debugf("resolver: connected to %s (%s)", mgr.tr.resolver.GetName(), conn.RemoteAddr())
// Get amount of in waiting queries.
mgr.tr.Lock()
waitingQueries := len(mgr.tr.inFlightQueries)
mgr.tr.Unlock()
// Log that a connection to the resolver was established.
log.Debugf(
"resolver: connected to %s (%s) with %d queries waiting",
mgr.tr.resolver.GetName(),
conn.RemoteAddr(),
waitingQueries,
)
// start reader
module.StartServiceWorker("dns client reader", 10*time.Millisecond, func(workerCtx context.Context) error {
@@ -348,6 +391,9 @@ func (mgr *tcpResolverConnMgr) queryHandler( //nolint:golint // context.Context
for {
select {
case <-mgr.tr.clientHeartbeat:
// respond to alive checks
case <-workerCtx.Done():
// module shutdown
return false
@@ -372,9 +418,7 @@ func (mgr *tcpResolverConnMgr) queryHandler( //nolint:golint // context.Context
_ = conn.SetWriteDeadline(time.Now().Add(mgr.tr.dnsClient.WriteTimeout))
err := conn.WriteMsg(msg)
if err != nil {
if connClosing.SetToIf(false, true) {
log.Warningf("resolver: write error to %s (%s): %s", mgr.tr.resolver.GetName(), conn.RemoteAddr(), err)
}
mgr.logConnectionError(err, conn, connClosing)
return true
}
@@ -434,6 +478,10 @@ func (mgr *tcpResolverConnMgr) handleQueryResponse(conn *dns.Conn, msg *dns.Msg)
// persist to database
rrCache := inFlight.MakeCacheRecord(msg)
if !rrCache.Cacheable() {
return
}
rrCache.Clean(minTTL)
err := rrCache.Save()
if err != nil {
@@ -455,11 +503,37 @@ func (mgr *tcpResolverConnMgr) msgReader(
for {
msg, err := conn.ReadMsg()
if err != nil {
if connClosing.SetToIf(false, true) {
log.Warningf("resolver: read error from %s (%s): %s", mgr.tr.resolver.GetName(), conn.RemoteAddr(), err)
}
mgr.logConnectionError(err, conn, connClosing)
return nil
}
mgr.responses <- msg
}
}
func (mgr *tcpResolverConnMgr) logConnectionError(err error, conn *dns.Conn, connClosing *abool.AtomicBool) {
// Check if we are the first to see an error.
if connClosing.SetToIf(false, true) {
// Get amount of in flight queries.
mgr.tr.Lock()
inFlightQueries := len(mgr.tr.inFlightQueries)
mgr.tr.Unlock()
// Log error.
if errors.Is(err, io.EOF) {
log.Debugf(
"resolver: connection to %s (%s) was closed with %d in-flight queries",
mgr.tr.resolver.GetName(),
conn.RemoteAddr(),
inFlightQueries,
)
} else {
log.Warningf(
"resolver: write error to %s (%s) with %d in-flight queries: %s",
mgr.tr.resolver.GetName(),
conn.RemoteAddr(),
inFlightQueries,
err,
)
}
}
}

View File

@@ -60,6 +60,7 @@ type Resolver struct {
ServerIP net.IP
ServerIPScope int8
ServerPort uint16
ServerInfo string
// Special Options
VerifyDomain string

View File

@@ -90,6 +90,16 @@ func createResolver(resolverURL, source string) (*Resolver, bool, error) {
return nil, false, fmt.Errorf("invalid resolver IP")
}
// Add default port for scheme if it is missing.
if u.Port() == "" {
switch u.Scheme {
case ServerTypeDNS, ServerTypeTCP:
u.Host += ":53"
case ServerTypeDoT:
u.Host += ":853"
}
}
scope := netutils.ClassifyIP(ip)
if scope == netutils.HostLocal {
return nil, true, nil // skip
@@ -128,6 +138,13 @@ func createResolver(resolverURL, source string) (*Resolver, bool, error) {
UpstreamBlockDetection: blockType,
}
u.RawQuery = "" // Remove options from parsed URL
if new.Name != "" {
new.ServerInfo = fmt.Sprintf("%s (%s, from %s)", new.Name, u, source)
} else {
new.ServerInfo = fmt.Sprintf("%s (from %s)", u, source)
}
new.Conn = resolverConnFactory(new)
return new, false, nil
}

View File

@@ -14,7 +14,7 @@ func ResolveIPAndValidate(ctx context.Context, ip string, securityLevel uint8) (
// get reversed DNS address
reverseIP, err := dns.ReverseAddr(ip)
if err != nil {
log.Tracef("resolver: failed to get reverse address of %s: %s", ip, err)
log.Tracer(ctx).Tracef("resolver: failed to get reverse address of %s: %s", ip, err)
return "", ErrInvalid
}

View File

@@ -1,6 +1,7 @@
package resolver
import (
"context"
"fmt"
"math/rand"
"net"
@@ -8,6 +9,7 @@ import (
"time"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/nameserver/nsutil"
"github.com/safing/portmaster/netenv"
"github.com/miekg/dns"
@@ -20,17 +22,20 @@ type RRCache struct {
Domain string // constant
Question dns.Type // constant
RCode int // constant
Answer []dns.RR // might be mixed
Answer []dns.RR // constant
Ns []dns.RR // constant
Extra []dns.RR // constant
TTL int64 // constant
Server string // constant
ServerScope int8 // constant
ServerInfo string // constant
servedFromCache bool // mutable
requestingNew bool // mutable
isBackup bool // mutable
Filtered bool // mutable
FilteredEntries []string // mutable
@@ -47,19 +52,16 @@ func (rrCache *RRCache) Expired() bool {
return rrCache.TTL <= time.Now().Unix()
}
// MixAnswers randomizes the answer records to allow dumb clients (who only look at the first record) to reliably connect.
func (rrCache *RRCache) MixAnswers() {
rrCache.Lock()
defer rrCache.Unlock()
for i := range rrCache.Answer {
j := rand.Intn(i + 1)
rrCache.Answer[i], rrCache.Answer[j] = rrCache.Answer[j], rrCache.Answer[i]
}
// ExpiresSoon returns whether the record will expire soon and should already be refreshed.
func (rrCache *RRCache) ExpiresSoon() bool {
return rrCache.TTL <= time.Now().Unix()+refreshTTL
}
// 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
@@ -83,8 +85,8 @@ func (rrCache *RRCache) Clean(minExpires uint32) {
// shorten caching
switch {
case rrCache.IsNXDomain():
// NXDomain
case rrCache.RCode != dns.RcodeSuccess:
// Any sort of error.
lowestTTL = 10
case netenv.IsConnectivityDomain(rrCache.Domain):
// Responses from these domains might change very quickly depending on the environment.
@@ -126,9 +128,11 @@ func (rrCache *RRCache) ToNameRecord() *NameRecord {
new := &NameRecord{
Domain: rrCache.Domain,
Question: rrCache.Question.String(),
RCode: rrCache.RCode,
TTL: rrCache.TTL,
Server: rrCache.Server,
ServerScope: rrCache.ServerScope,
ServerInfo: rrCache.ServerInfo,
}
// stringify RR entries
@@ -145,8 +149,27 @@ func (rrCache *RRCache) ToNameRecord() *NameRecord {
return new
}
// rcodeIsCacheable returns whether a record with the given RCode should be cached.
func rcodeIsCacheable(rCode int) bool {
switch rCode {
case dns.RcodeSuccess, dns.RcodeNameError, dns.RcodeRefused:
return true
default:
return false
}
}
// Cacheable returns whether the record should be cached.
func (rrCache *RRCache) Cacheable() bool {
return rcodeIsCacheable(rrCache.RCode)
}
// Save saves the RRCache to the database as a NameRecord.
func (rrCache *RRCache) Save() error {
if !rrCache.Cacheable() {
return nil
}
return rrCache.ToNameRecord().Save()
}
@@ -162,6 +185,7 @@ func GetRRCache(domain string, question dns.Type) (*RRCache, error) {
return nil, err
}
rrCache.RCode = nameRecord.RCode
rrCache.TTL = nameRecord.TTL
for _, entry := range nameRecord.Answer {
rrCache.Answer = parseRR(rrCache.Answer, entry)
@@ -175,6 +199,7 @@ func GetRRCache(domain string, question dns.Type) (*RRCache, error) {
rrCache.Server = nameRecord.Server
rrCache.ServerScope = nameRecord.ServerScope
rrCache.ServerInfo = nameRecord.ServerInfo
rrCache.servedFromCache = true
return rrCache, nil
}
@@ -211,6 +236,9 @@ func (rrCache *RRCache) Flags() string {
if rrCache.requestingNew {
s += "R"
}
if rrCache.isBackup {
s += "B"
}
if rrCache.Filtered {
s += "F"
}
@@ -221,16 +249,12 @@ func (rrCache *RRCache) Flags() string {
return ""
}
// IsNXDomain returnes whether the result is nxdomain.
func (rrCache *RRCache) IsNXDomain() bool {
return len(rrCache.Answer) == 0
}
// ShallowCopy returns a shallow copy of the cache. slices are not copied, but referenced.
func (rrCache *RRCache) ShallowCopy() *RRCache {
return &RRCache{
Domain: rrCache.Domain,
Question: rrCache.Question,
RCode: rrCache.RCode,
Answer: rrCache.Answer,
Ns: rrCache.Ns,
Extra: rrCache.Extra,
@@ -238,11 +262,81 @@ func (rrCache *RRCache) ShallowCopy() *RRCache {
Server: rrCache.Server,
ServerScope: rrCache.ServerScope,
ServerInfo: rrCache.ServerInfo,
updated: rrCache.updated,
servedFromCache: rrCache.servedFromCache,
requestingNew: rrCache.requestingNew,
isBackup: rrCache.isBackup,
Filtered: rrCache.Filtered,
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, rrCache.RCode)
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]
}
}
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, "served from cache, resolved by "+rrCache.ServerInfo)
} else {
extra = addExtra(ctx, extra, "freshly resolved by "+rrCache.ServerInfo)
}
// Add expiry and cache information.
if rrCache.Expired() {
extra = addExtra(ctx, extra, fmt.Sprintf("record expired since %s", time.Since(time.Unix(rrCache.TTL, 0)).Round(time.Second)))
} else {
extra = addExtra(ctx, extra, fmt.Sprintf("record valid for %s", time.Until(time.Unix(rrCache.TTL, 0)).Round(time.Second)))
}
if rrCache.requestingNew {
extra = addExtra(ctx, extra, "async request to refresh the cache has been started")
}
if rrCache.isBackup {
extra = addExtra(ctx, extra, "this record is served because a fresh request failed")
}
// Add information about filtered entries.
if rrCache.Filtered {
if len(rrCache.FilteredEntries) > 1 {
extra = addExtra(ctx, extra, fmt.Sprintf("%d records have been filtered", len(rrCache.FilteredEntries)))
} else {
extra = addExtra(ctx, extra, fmt.Sprintf("%d record has been filtered", len(rrCache.FilteredEntries)))
}
}
return extra
}
func addExtra(ctx context.Context, extra []dns.RR, msg string) []dns.RR {
rr, err := nsutil.MakeMessageRecord(log.InfoLevel, 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
}

View File

@@ -158,7 +158,7 @@ addNextResolver:
for _, resolver := range addResolvers {
// check for compliance
if err := resolver.checkCompliance(ctx, q); err != nil {
log.Tracef("skipping non-compliant resolver %s: %s", resolver.GetName(), err)
log.Tracer(ctx).Tracef("skipping non-compliant resolver %s: %s", resolver.GetName(), err)
continue
}