Add optimistic DNS cache
This commit is contained in:
@@ -12,9 +12,11 @@ import (
|
||||
"github.com/sagernet/bbolt"
|
||||
bboltErrors "github.com/sagernet/bbolt/errors"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/service/filemanager"
|
||||
)
|
||||
|
||||
@@ -30,6 +32,7 @@ var (
|
||||
string(bucketMode),
|
||||
string(bucketRuleSet),
|
||||
string(bucketRDRC),
|
||||
string(bucketDNSCache),
|
||||
}
|
||||
|
||||
cacheIDDefault = []byte("default")
|
||||
@@ -38,30 +41,43 @@ var (
|
||||
var _ adapter.CacheFile = (*CacheFile)(nil)
|
||||
|
||||
type CacheFile struct {
|
||||
ctx context.Context
|
||||
path string
|
||||
cacheID []byte
|
||||
storeFakeIP bool
|
||||
storeRDRC bool
|
||||
rdrcTimeout time.Duration
|
||||
DB *bbolt.DB
|
||||
resetAccess sync.Mutex
|
||||
saveMetadataTimer *time.Timer
|
||||
saveFakeIPAccess sync.RWMutex
|
||||
saveDomain map[netip.Addr]string
|
||||
saveAddress4 map[string]netip.Addr
|
||||
saveAddress6 map[string]netip.Addr
|
||||
saveRDRCAccess sync.RWMutex
|
||||
saveRDRC map[saveRDRCCacheKey]bool
|
||||
ctx context.Context
|
||||
logger logger.Logger
|
||||
path string
|
||||
cacheID []byte
|
||||
storeFakeIP bool
|
||||
storeRDRC bool
|
||||
storeDNS bool
|
||||
disableExpire bool
|
||||
rdrcTimeout time.Duration
|
||||
optimisticTimeout time.Duration
|
||||
DB *bbolt.DB
|
||||
resetAccess sync.Mutex
|
||||
saveMetadataTimer *time.Timer
|
||||
saveFakeIPAccess sync.RWMutex
|
||||
saveDomain map[netip.Addr]string
|
||||
saveAddress4 map[string]netip.Addr
|
||||
saveAddress6 map[string]netip.Addr
|
||||
saveRDRCAccess sync.RWMutex
|
||||
saveRDRC map[saveCacheKey]bool
|
||||
saveDNSCacheAccess sync.RWMutex
|
||||
saveDNSCache map[saveCacheKey]saveDNSCacheEntry
|
||||
}
|
||||
|
||||
type saveRDRCCacheKey struct {
|
||||
type saveCacheKey struct {
|
||||
TransportName string
|
||||
QuestionName string
|
||||
QType uint16
|
||||
}
|
||||
|
||||
func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
|
||||
type saveDNSCacheEntry struct {
|
||||
rawMessage []byte
|
||||
expireAt time.Time
|
||||
sequence uint64
|
||||
saving bool
|
||||
}
|
||||
|
||||
func New(ctx context.Context, logger logger.Logger, options option.CacheFileOptions) *CacheFile {
|
||||
var path string
|
||||
if options.Path != "" {
|
||||
path = options.Path
|
||||
@@ -72,6 +88,9 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
|
||||
if options.CacheID != "" {
|
||||
cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...)
|
||||
}
|
||||
if options.StoreRDRC {
|
||||
deprecated.Report(ctx, deprecated.OptionStoreRDRC)
|
||||
}
|
||||
var rdrcTimeout time.Duration
|
||||
if options.StoreRDRC {
|
||||
if options.RDRCTimeout > 0 {
|
||||
@@ -82,15 +101,18 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
|
||||
}
|
||||
return &CacheFile{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
path: filemanager.BasePath(ctx, path),
|
||||
cacheID: cacheIDBytes,
|
||||
storeFakeIP: options.StoreFakeIP,
|
||||
storeRDRC: options.StoreRDRC,
|
||||
storeDNS: options.StoreDNS,
|
||||
rdrcTimeout: rdrcTimeout,
|
||||
saveDomain: make(map[netip.Addr]string),
|
||||
saveAddress4: make(map[string]netip.Addr),
|
||||
saveAddress6: make(map[string]netip.Addr),
|
||||
saveRDRC: make(map[saveRDRCCacheKey]bool),
|
||||
saveRDRC: make(map[saveCacheKey]bool),
|
||||
saveDNSCache: make(map[saveCacheKey]saveDNSCacheEntry),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,10 +124,44 @@ func (c *CacheFile) Dependencies() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CacheFile) SetOptimisticTimeout(timeout time.Duration) {
|
||||
c.optimisticTimeout = timeout
|
||||
}
|
||||
|
||||
func (c *CacheFile) SetDisableExpire(disableExpire bool) {
|
||||
c.disableExpire = disableExpire
|
||||
}
|
||||
|
||||
func (c *CacheFile) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateInitialize {
|
||||
return nil
|
||||
switch stage {
|
||||
case adapter.StartStateInitialize:
|
||||
return c.start()
|
||||
case adapter.StartStateStart:
|
||||
c.startCacheCleanup()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CacheFile) startCacheCleanup() {
|
||||
if c.storeDNS {
|
||||
c.clearRDRC()
|
||||
c.cleanupDNSCache()
|
||||
interval := c.optimisticTimeout / 2
|
||||
if interval <= 0 {
|
||||
interval = time.Hour
|
||||
}
|
||||
go c.loopCacheCleanup(interval, c.cleanupDNSCache)
|
||||
} else if c.storeRDRC {
|
||||
c.cleanupRDRC()
|
||||
interval := c.rdrcTimeout / 2
|
||||
if interval <= 0 {
|
||||
interval = time.Hour
|
||||
}
|
||||
go c.loopCacheCleanup(interval, c.cleanupRDRC)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CacheFile) start() error {
|
||||
const fileMode = 0o666
|
||||
options := bbolt.Options{Timeout: time.Second}
|
||||
var (
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
package cachefile
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/bbolt"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
var bucketDNSCache = []byte("dns_cache")
|
||||
|
||||
func (c *CacheFile) StoreDNS() bool {
|
||||
return c.storeDNS
|
||||
}
|
||||
|
||||
func (c *CacheFile) LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool) {
|
||||
c.saveDNSCacheAccess.RLock()
|
||||
entry, cached := c.saveDNSCache[saveCacheKey{transportName, qName, qType}]
|
||||
c.saveDNSCacheAccess.RUnlock()
|
||||
if cached {
|
||||
return entry.rawMessage, entry.expireAt, true
|
||||
}
|
||||
key := buf.Get(2 + len(qName))
|
||||
binary.BigEndian.PutUint16(key, qType)
|
||||
copy(key[2:], qName)
|
||||
defer buf.Put(key)
|
||||
err := c.view(func(tx *bbolt.Tx) error {
|
||||
bucket := c.bucket(tx, bucketDNSCache)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
bucket = bucket.Bucket([]byte(transportName))
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
content := bucket.Get(key)
|
||||
if len(content) < 8 {
|
||||
return nil
|
||||
}
|
||||
expireAt = time.Unix(int64(binary.BigEndian.Uint64(content[:8])), 0)
|
||||
rawMessage = make([]byte, len(content)-8)
|
||||
copy(rawMessage, content[8:])
|
||||
loaded = true
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, time.Time{}, false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *CacheFile) SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error {
|
||||
return c.batch(func(tx *bbolt.Tx) error {
|
||||
bucket, err := c.createBucket(tx, bucketDNSCache)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bucket, err = bucket.CreateBucketIfNotExists([]byte(transportName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := buf.Get(2 + len(qName))
|
||||
binary.BigEndian.PutUint16(key, qType)
|
||||
copy(key[2:], qName)
|
||||
defer buf.Put(key)
|
||||
value := buf.Get(8 + len(rawMessage))
|
||||
defer buf.Put(value)
|
||||
binary.BigEndian.PutUint64(value[:8], uint64(expireAt.Unix()))
|
||||
copy(value[8:], rawMessage)
|
||||
return bucket.Put(key, value)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *CacheFile) SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger) {
|
||||
saveKey := saveCacheKey{transportName, qName, qType}
|
||||
if !c.queueDNSCacheSave(saveKey, rawMessage, expireAt) {
|
||||
return
|
||||
}
|
||||
go c.flushPendingDNSCache(saveKey, logger)
|
||||
}
|
||||
|
||||
func (c *CacheFile) queueDNSCacheSave(saveKey saveCacheKey, rawMessage []byte, expireAt time.Time) bool {
|
||||
c.saveDNSCacheAccess.Lock()
|
||||
defer c.saveDNSCacheAccess.Unlock()
|
||||
entry := c.saveDNSCache[saveKey]
|
||||
entry.rawMessage = append([]byte(nil), rawMessage...)
|
||||
entry.expireAt = expireAt
|
||||
entry.sequence++
|
||||
startFlush := !entry.saving
|
||||
entry.saving = true
|
||||
c.saveDNSCache[saveKey] = entry
|
||||
return startFlush
|
||||
}
|
||||
|
||||
func (c *CacheFile) flushPendingDNSCache(saveKey saveCacheKey, logger logger.Logger) {
|
||||
c.flushPendingDNSCacheWith(saveKey, logger, func(entry saveDNSCacheEntry) error {
|
||||
return c.SaveDNSCache(saveKey.TransportName, saveKey.QuestionName, saveKey.QType, entry.rawMessage, entry.expireAt)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *CacheFile) flushPendingDNSCacheWith(saveKey saveCacheKey, logger logger.Logger, save func(saveDNSCacheEntry) error) {
|
||||
for {
|
||||
c.saveDNSCacheAccess.RLock()
|
||||
entry, loaded := c.saveDNSCache[saveKey]
|
||||
c.saveDNSCacheAccess.RUnlock()
|
||||
if !loaded {
|
||||
return
|
||||
}
|
||||
err := save(entry)
|
||||
if err != nil {
|
||||
logger.Warn("save DNS cache: ", err)
|
||||
}
|
||||
c.saveDNSCacheAccess.Lock()
|
||||
currentEntry, loaded := c.saveDNSCache[saveKey]
|
||||
if !loaded {
|
||||
c.saveDNSCacheAccess.Unlock()
|
||||
return
|
||||
}
|
||||
if currentEntry.sequence != entry.sequence {
|
||||
c.saveDNSCacheAccess.Unlock()
|
||||
continue
|
||||
}
|
||||
delete(c.saveDNSCache, saveKey)
|
||||
c.saveDNSCacheAccess.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CacheFile) ClearDNSCache() error {
|
||||
c.saveDNSCacheAccess.Lock()
|
||||
clear(c.saveDNSCache)
|
||||
c.saveDNSCacheAccess.Unlock()
|
||||
return c.batch(func(tx *bbolt.Tx) error {
|
||||
if c.cacheID == nil {
|
||||
bucket := tx.Bucket(bucketDNSCache)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
return tx.DeleteBucket(bucketDNSCache)
|
||||
}
|
||||
bucket := tx.Bucket(c.cacheID)
|
||||
if bucket == nil || bucket.Bucket(bucketDNSCache) == nil {
|
||||
return nil
|
||||
}
|
||||
return bucket.DeleteBucket(bucketDNSCache)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *CacheFile) loopCacheCleanup(interval time.Duration, cleanupFunc func()) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
cleanupFunc()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CacheFile) cleanupDNSCache() {
|
||||
now := time.Now()
|
||||
err := c.batch(func(tx *bbolt.Tx) error {
|
||||
bucket := c.bucket(tx, bucketDNSCache)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
var emptyTransports [][]byte
|
||||
err := bucket.ForEachBucket(func(transportName []byte) error {
|
||||
transportBucket := bucket.Bucket(transportName)
|
||||
if transportBucket == nil {
|
||||
return nil
|
||||
}
|
||||
var expiredKeys [][]byte
|
||||
err := transportBucket.ForEach(func(key, value []byte) error {
|
||||
if len(value) < 8 {
|
||||
expiredKeys = append(expiredKeys, append([]byte(nil), key...))
|
||||
return nil
|
||||
}
|
||||
if c.disableExpire {
|
||||
return nil
|
||||
}
|
||||
expireAt := time.Unix(int64(binary.BigEndian.Uint64(value[:8])), 0)
|
||||
if now.After(expireAt.Add(c.optimisticTimeout)) {
|
||||
expiredKeys = append(expiredKeys, append([]byte(nil), key...))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, key := range expiredKeys {
|
||||
err = transportBucket.Delete(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
first, _ := transportBucket.Cursor().First()
|
||||
if first == nil {
|
||||
emptyTransports = append(emptyTransports, append([]byte(nil), transportName...))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, name := range emptyTransports {
|
||||
err = bucket.DeleteBucket(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Warn("cleanup DNS cache: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CacheFile) clearRDRC() {
|
||||
c.saveRDRCAccess.Lock()
|
||||
clear(c.saveRDRC)
|
||||
c.saveRDRCAccess.Unlock()
|
||||
err := c.batch(func(tx *bbolt.Tx) error {
|
||||
if c.cacheID == nil {
|
||||
if tx.Bucket(bucketRDRC) == nil {
|
||||
return nil
|
||||
}
|
||||
return tx.DeleteBucket(bucketRDRC)
|
||||
}
|
||||
bucket := tx.Bucket(c.cacheID)
|
||||
if bucket == nil || bucket.Bucket(bucketRDRC) == nil {
|
||||
return nil
|
||||
}
|
||||
return bucket.DeleteBucket(bucketRDRC)
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Warn("clear RDRC: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CacheFile) cleanupRDRC() {
|
||||
now := time.Now()
|
||||
err := c.batch(func(tx *bbolt.Tx) error {
|
||||
bucket := c.bucket(tx, bucketRDRC)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
var emptyTransports [][]byte
|
||||
err := bucket.ForEachBucket(func(transportName []byte) error {
|
||||
transportBucket := bucket.Bucket(transportName)
|
||||
if transportBucket == nil {
|
||||
return nil
|
||||
}
|
||||
var expiredKeys [][]byte
|
||||
err := transportBucket.ForEach(func(key, value []byte) error {
|
||||
if len(value) < 8 {
|
||||
expiredKeys = append(expiredKeys, append([]byte(nil), key...))
|
||||
return nil
|
||||
}
|
||||
expiresAt := time.Unix(int64(binary.BigEndian.Uint64(value)), 0)
|
||||
if now.After(expiresAt) {
|
||||
expiredKeys = append(expiredKeys, append([]byte(nil), key...))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, key := range expiredKeys {
|
||||
err = transportBucket.Delete(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
first, _ := transportBucket.Cursor().First()
|
||||
if first == nil {
|
||||
emptyTransports = append(emptyTransports, append([]byte(nil), transportName...))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, name := range emptyTransports {
|
||||
err = bucket.DeleteBucket(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Warn("cleanup RDRC: ", err)
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ func (c *CacheFile) RDRCTimeout() time.Duration {
|
||||
|
||||
func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (rejected bool) {
|
||||
c.saveRDRCAccess.RLock()
|
||||
rejected, cached := c.saveRDRC[saveRDRCCacheKey{transportName, qName, qType}]
|
||||
rejected, cached := c.saveRDRC[saveCacheKey{transportName, qName, qType}]
|
||||
c.saveRDRCAccess.RUnlock()
|
||||
if cached {
|
||||
return
|
||||
@@ -93,7 +93,7 @@ func (c *CacheFile) SaveRDRC(transportName string, qName string, qType uint16) e
|
||||
}
|
||||
|
||||
func (c *CacheFile) SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger) {
|
||||
saveKey := saveRDRCCacheKey{transportName, qName, qType}
|
||||
saveKey := saveCacheKey{transportName, qName, qType}
|
||||
c.saveRDRCAccess.Lock()
|
||||
c.saveRDRC[saveKey] = true
|
||||
c.saveRDRCAccess.Unlock()
|
||||
|
||||
Reference in New Issue
Block a user