mirror of
https://github.com/safing/portmaster
synced 2025-09-01 18:19:12 +00:00
Add dns cache clearing mechanisms
This commit is contained in:
parent
c9ba2a939a
commit
53e9d3dd47
4 changed files with 140 additions and 9 deletions
|
@ -14,11 +14,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
// ClearNameCacheEvent is a triggerable event that clears the name record cache.
|
||||||
|
ClearNameCacheEvent = "clear name cache"
|
||||||
|
|
||||||
module *modules.Module
|
module *modules.Module
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
module = modules.Register("resolver", prep, start, nil, "base", "netenv")
|
module = modules.Register("resolver", prep, start, nil, "base", "netenv")
|
||||||
|
module.RegisterEvent(ClearNameCacheEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func prep() error {
|
func prep() error {
|
||||||
|
@ -71,6 +75,17 @@ func start() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cache clearing
|
||||||
|
err = module.RegisterEventHook(
|
||||||
|
"resolver",
|
||||||
|
ClearNameCacheEvent,
|
||||||
|
ClearNameCacheEvent,
|
||||||
|
clearNameCache,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
module.StartServiceWorker(
|
module.StartServiceWorker(
|
||||||
"mdns handler",
|
"mdns handler",
|
||||||
5*time.Second,
|
5*time.Second,
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
package resolver
|
package resolver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/safing/portbase/database"
|
"github.com/safing/portbase/database"
|
||||||
|
"github.com/safing/portbase/database/query"
|
||||||
"github.com/safing/portbase/database/record"
|
"github.com/safing/portbase/database/record"
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -14,6 +17,8 @@ var (
|
||||||
AlwaysSetRelativateExpiry: 2592000, // 30 days
|
AlwaysSetRelativateExpiry: 2592000, // 30 days
|
||||||
CacheSize: 256,
|
CacheSize: 256,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
nameRecordsKeyPrefix = "cache:intel/nameRecord/"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NameRecord is helper struct to RRCache to better save data to the database.
|
// 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 {
|
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.
|
// 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)
|
key := makeNameRecordKey(domain, question)
|
||||||
|
|
||||||
r, err := recordDatabase.Get(key)
|
r, err := recordDatabase.Get(key)
|
||||||
|
@ -64,6 +69,12 @@ func GetNameRecord(domain string, question string) (*NameRecord, error) {
|
||||||
return new, nil
|
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.
|
// Save saves the NameRecord to the database.
|
||||||
func (rec *NameRecord) Save() error {
|
func (rec *NameRecord) Save() error {
|
||||||
if rec.Domain == "" || rec.Question == "" {
|
if rec.Domain == "" || rec.Question == "" {
|
||||||
|
@ -73,3 +84,49 @@ func (rec *NameRecord) Save() error {
|
||||||
rec.SetKey(makeNameRecordKey(rec.Domain, rec.Question))
|
rec.SetKey(makeNameRecordKey(rec.Domain, rec.Question))
|
||||||
return recordDatabase.PutNew(rec)
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -73,6 +73,11 @@ type Query struct {
|
||||||
dotPrefixedFQDN string
|
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.
|
// check runs sanity checks and does some initialization. Returns whether the query passed the basic checks.
|
||||||
func (q *Query) check() (ok bool) {
|
func (q *Query) check() (ok bool) {
|
||||||
if q.FQDN == "" {
|
if q.FQDN == "" {
|
||||||
|
@ -159,6 +164,20 @@ func checkCache(ctx context.Context, q *Query) *RRCache {
|
||||||
return nil
|
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
|
// check if expired
|
||||||
if rrCache.Expired() {
|
if rrCache.Expired() {
|
||||||
rrCache.Lock()
|
rrCache.Lock()
|
||||||
|
@ -169,7 +188,10 @@ func checkCache(ctx context.Context, q *Query) *RRCache {
|
||||||
|
|
||||||
// resolve async
|
// resolve async
|
||||||
module.StartWorker("resolve async", func(ctx context.Context) error {
|
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
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -180,7 +202,7 @@ func checkCache(ctx context.Context, q *Query) *RRCache {
|
||||||
|
|
||||||
func deduplicateRequest(ctx context.Context, q *Query) (finishRequest func()) {
|
func deduplicateRequest(ctx context.Context, q *Query) (finishRequest func()) {
|
||||||
// create identifier key
|
// create identifier key
|
||||||
dupKey := fmt.Sprintf("%s%s", q.FQDN, q.QType.String())
|
dupKey := q.ID()
|
||||||
|
|
||||||
dupReqLock.Lock()
|
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
|
// check for error
|
||||||
if err != nil {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -309,3 +330,31 @@ resolveLoop:
|
||||||
|
|
||||||
return rrCache, nil
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -34,6 +34,11 @@ type RRCache struct {
|
||||||
updated int64 // mutable
|
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.
|
// Expired returns whether the record has expired.
|
||||||
func (rrCache *RRCache) Expired() bool {
|
func (rrCache *RRCache) Expired() bool {
|
||||||
return rrCache.TTL <= time.Now().Unix()
|
return rrCache.TTL <= time.Now().Unix()
|
||||||
|
@ -70,6 +75,11 @@ func (rrCache *RRCache) Clean(minExpires uint32) {
|
||||||
lowestTTL = minExpires
|
lowestTTL = minExpires
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shorten NXDomain caching
|
||||||
|
if len(rrCache.Answer) == 0 {
|
||||||
|
lowestTTL = 10
|
||||||
|
}
|
||||||
|
|
||||||
// log.Tracef("lowest TTL is %d", lowestTTL)
|
// log.Tracef("lowest TTL is %d", lowestTTL)
|
||||||
rrCache.TTL = time.Now().Unix() + int64(lowestTTL)
|
rrCache.TTL = time.Now().Unix() + int64(lowestTTL)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue