Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion client/dns/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dns

import (
"fmt"
"math/rand/v2"
"strings"

"github.com/coocood/freecache"
Expand Down Expand Up @@ -59,6 +60,9 @@ func (c *Cache) Get(name, qtype string, isDirect bool) *dns.Msg {

// Set stores a DNS message in the appropriate cache using DNS TTL.
// Only A and AAAA records are cached. If isDirect is true, the direct cache is used.
// The effective cache lifetime is the base TTL plus a random jitter in
// [0, baseTTL) so that entries with the same base TTL do not expire at the
// same moment, avoiding bursts of concurrent DNS queries.
func (c *Cache) Set(msg *dns.Msg, isDirect bool) error {
if msg == nil || len(msg.Question) == 0 {
return nil
Expand All @@ -70,7 +74,7 @@ func (c *Cache) Set(msg *dns.Msg, isDirect bool) error {
return err
}
key := []byte(q.Name + dns.TypeToString[q.Qtype])
ttl := dnsCacheTTL(msg, c.serverDomain)
ttl := jitterTTL(dnsCacheTTL(msg, c.serverDomain))
if isDirect {
return c.direct.Set(key, v, ttl)
}
Expand Down Expand Up @@ -109,6 +113,18 @@ func dnsCacheTTL(msg *dns.Msg, serverDomain string) int {
return int(ttl)
}

// jitterTTL returns the effective cache lifetime in seconds for a base TTL:
// the base TTL plus a random jitter in [0, ttl). Entries sharing the same
// base TTL therefore expire at scattered moments instead of all at once,
// avoiding a burst of concurrent DNS queries. A TTL of 0 (never expire,
// e.g. the proxy server's own domain) is returned unchanged.
func jitterTTL(ttl int) int {
if ttl <= 0 {
return ttl
}
return ttl + rand.IntN(ttl)
}

// PrePopulate resolves the domain via the given DNS server and stores the
// A and AAAA results in both the direct and proxied caches. This is used
// to pre-seed the cache with the proxy server's IP before TUN routes are
Expand Down
28 changes: 28 additions & 0 deletions client/dns/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,31 @@ func TestCache_ServerDomain_NeverExpires(t *testing.T) {
t.Fatal("regular domain entry expired before minCacheTTL")
}
}

func TestJitterTTL_NeverExpire(t *testing.T) {
// TTL 0(服务器域名永不过期)必须原样返回,不受抖动影响
if ttl := jitterTTL(0); ttl != 0 {
t.Errorf("jitterTTL(0) expected 0, got %d", ttl)
}
// 负数属于防御性边界,不应 panic 也不应被改动
if ttl := jitterTTL(-1); ttl != -1 {
t.Errorf("jitterTTL(-1) expected -1, got %d", ttl)
}
}

func TestJitterTTL_Range(t *testing.T) {
const base = 1800
seen := make(map[int]bool)
for i := 0; i < 1000; i++ {
ttl := jitterTTL(base)
// 结果必须在 [base, 2*base) 区间内
if ttl < base || ttl >= 2*base {
t.Fatalf("jitterTTL(%d) = %d, want in [%d, %d)", base, ttl, base, 2*base)
}
seen[ttl] = true
}
// 1000 次采样下结果应分散,而非固定同一个值
if len(seen) < 2 {
t.Errorf("jitterTTL(%d) always returned %d, expected random jitter", base, base)
}
}
Loading