diff --git a/resolver/main.go b/resolver/main.go index 57370c55..a68bd76e 100644 --- a/resolver/main.go +++ b/resolver/main.go @@ -14,11 +14,15 @@ import ( ) var ( + // ClearNameCacheEvent is a triggerable event that clears the name record cache. + ClearNameCacheEvent = "clear name cache" + module *modules.Module ) func init() { module = modules.Register("resolver", prep, start, nil, "base", "netenv") + module.RegisterEvent(ClearNameCacheEvent) } func prep() error { @@ -71,6 +75,17 @@ func start() error { return err } + // cache clearing + err = module.RegisterEventHook( + "resolver", + ClearNameCacheEvent, + ClearNameCacheEvent, + clearNameCache, + ) + if err != nil { + return err + } + module.StartServiceWorker( "mdns handler", 5*time.Second, diff --git a/resolver/namerecord.go b/resolver/namerecord.go index 1a594e8f..8c9b888d 100644 --- a/resolver/namerecord.go +++ b/resolver/namerecord.go @@ -1,12 +1,15 @@ package resolver import ( + "context" "errors" "fmt" "sync" "github.com/safing/portbase/database" + "github.com/safing/portbase/database/query" "github.com/safing/portbase/database/record" + "github.com/safing/portbase/log" ) var ( @@ -14,6 +17,8 @@ var ( AlwaysSetRelativateExpiry: 2592000, // 30 days CacheSize: 256, }) + + nameRecordsKeyPrefix = "cache:intel/nameRecord/" ) // NameRecord is helper struct to RRCache to better save data to the database. @@ -33,11 +38,11 @@ type NameRecord struct { } func makeNameRecordKey(domain string, question string) string { - return fmt.Sprintf("cache:intel/nameRecord/%s%s", domain, question) + return nameRecordsKeyPrefix + domain + question } // GetNameRecord gets a NameRecord from the database. -func GetNameRecord(domain string, question string) (*NameRecord, error) { +func GetNameRecord(domain, question string) (*NameRecord, error) { key := makeNameRecordKey(domain, question) r, err := recordDatabase.Get(key) @@ -64,6 +69,12 @@ func GetNameRecord(domain string, question string) (*NameRecord, error) { return new, nil } +// DeleteNameRecord deletes a NameRecord from the database. +func DeleteNameRecord(domain, question string) error { + key := makeNameRecordKey(domain, question) + return recordDatabase.Delete(key) +} + // Save saves the NameRecord to the database. func (rec *NameRecord) Save() error { if rec.Domain == "" || rec.Question == "" { @@ -73,3 +84,49 @@ func (rec *NameRecord) Save() error { rec.SetKey(makeNameRecordKey(rec.Domain, rec.Question)) 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)) + if err != nil { + return false, 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 +} diff --git a/resolver/resolve.go b/resolver/resolve.go index e3a24dcf..719ca8ee 100644 --- a/resolver/resolve.go +++ b/resolver/resolve.go @@ -73,6 +73,11 @@ type Query struct { dotPrefixedFQDN string } +// ID returns the ID of the query consisting of the domain and question type. +func (q *Query) ID() string { + return q.FQDN + q.QType.String() +} + // check runs sanity checks and does some initialization. Returns whether the query passed the basic checks. func (q *Query) check() (ok bool) { if q.FQDN == "" { @@ -159,6 +164,20 @@ func checkCache(ctx context.Context, q *Query) *RRCache { return nil } + // check if we want to reset the cache + if shouldResetCache(q) { + err := DeleteNameRecord(q.FQDN, q.QType.String()) + switch { + case err == nil: + log.Tracer(ctx).Tracef("resolver: cache for %s%s was reset", q.FQDN, q.QType) + case errors.Is(err, database.ErrNotFound): + log.Tracer(ctx).Tracef("resolver: cache for %s%s was already reset (is empty)", q.FQDN, q.QType) + default: + log.Tracer(ctx).Warningf("resolver: failed to reset cache for %s%s: %s", q.FQDN, q.QType, err) + } + return nil + } + // check if expired if rrCache.Expired() { rrCache.Lock() @@ -169,7 +188,10 @@ func checkCache(ctx context.Context, q *Query) *RRCache { // resolve async module.StartWorker("resolve async", func(ctx context.Context) error { - _, _ = resolveAndCache(ctx, q) + _, err := resolveAndCache(ctx, q) + if err != nil { + log.Warningf("resolver: async query for %s%s failed: %s", q.FQDN, q.QType, err) + } return nil }) } @@ -180,7 +202,7 @@ func checkCache(ctx context.Context, q *Query) *RRCache { func deduplicateRequest(ctx context.Context, q *Query) (finishRequest func()) { // create identifier key - dupKey := fmt.Sprintf("%s%s", q.FQDN, q.QType.String()) + dupKey := q.ID() dupReqLock.Lock() @@ -282,13 +304,12 @@ resolveLoop: } } - // tried all resolvers, possibly twice - if i > 1 { - return nil, fmt.Errorf("all %d query-compliant resolvers failed, last error: %s", len(resolvers), err) - } - // check for error 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) + } return nil, err } @@ -309,3 +330,31 @@ resolveLoop: return rrCache, nil } + +var ( + cacheResetLock sync.Mutex + cacheResetID string + cacheResetSeenCnt int +) + +func shouldResetCache(q *Query) (reset bool) { + cacheResetLock.Lock() + defer cacheResetLock.Unlock() + + // reset to new domain + qID := q.ID() + if qID != cacheResetID { + cacheResetID = qID + cacheResetSeenCnt = 1 + return false + } + + // increase and check if threshold is reached + cacheResetSeenCnt++ + if cacheResetSeenCnt >= 3 { // 3 to trigger reset + cacheResetSeenCnt = -7 // 10 for follow-up resets + return true + } + + return false +} diff --git a/resolver/rrcache.go b/resolver/rrcache.go index cf0ff51f..1f929264 100644 --- a/resolver/rrcache.go +++ b/resolver/rrcache.go @@ -34,6 +34,11 @@ type RRCache struct { updated int64 // mutable } +// ID returns the ID of the RRCache consisting of the domain and question type. +func (rrCache *RRCache) ID() string { + return rrCache.Domain + rrCache.Question.String() +} + // Expired returns whether the record has expired. func (rrCache *RRCache) Expired() bool { return rrCache.TTL <= time.Now().Unix() @@ -70,6 +75,11 @@ func (rrCache *RRCache) Clean(minExpires uint32) { lowestTTL = minExpires } + // shorten NXDomain caching + if len(rrCache.Answer) == 0 { + lowestTTL = 10 + } + // log.Tracef("lowest TTL is %d", lowestTTL) rrCache.TTL = time.Now().Unix() + int64(lowestTTL) }