mirror of
https://github.com/v2rayA/v2rayA.git
synced 2026-10-28 21:08:07 +08:00
958 lines
24 KiB
Go
958 lines
24 KiB
Go
package dns
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
// --- Helper functions ---
|
|
|
|
func newTestCacheConfig() *CacheConfig {
|
|
return &CacheConfig{
|
|
Enabled: true,
|
|
Size: 100,
|
|
MinTTL: 60,
|
|
MaxTTL: 86400,
|
|
Prefetch: true,
|
|
PrefetchThreshold: 30,
|
|
NegativeCache: true,
|
|
}
|
|
}
|
|
|
|
func newTestCacheKey(name string, qtype QueryType) CacheKey {
|
|
return CacheKey{
|
|
UpstreamAddr: "8.8.8.8:53",
|
|
ProxyTag: "direct",
|
|
Name: name,
|
|
QType: qtype,
|
|
}
|
|
}
|
|
|
|
func newTestDNSResponse(name string, qtype QueryType, rcode int, ttl uint32) *DnsResponse {
|
|
m := new(dns.Msg)
|
|
m.SetQuestion(dns.Fqdn(name), uint16(qtype))
|
|
m.Response = true
|
|
m.Rcode = rcode
|
|
m.RecursionAvailable = true
|
|
|
|
// Add answer records if success
|
|
if rcode == dns.RcodeSuccess {
|
|
switch qtype {
|
|
case TypeA:
|
|
rr, _ := dns.NewRR(dns.Fqdn(name) + " 300 IN A 1.2.3.4")
|
|
m.Answer = []dns.RR{rr}
|
|
case TypeAAAA:
|
|
rr, _ := dns.NewRR(dns.Fqdn(name) + " 300 IN AAAA ::1")
|
|
m.Answer = []dns.RR{rr}
|
|
}
|
|
// Override TTL
|
|
if ttl > 0 {
|
|
m.Answer[0].Header().Ttl = ttl
|
|
}
|
|
}
|
|
|
|
return &DnsResponse{
|
|
RawMsg: m,
|
|
Rcode: m.Rcode,
|
|
Answer: m.Answer,
|
|
TTL: ttl,
|
|
Upstream: "8.8.8.8:53",
|
|
ProxyTag: "direct",
|
|
}
|
|
}
|
|
|
|
// --- Test: NewDnsCache ---
|
|
|
|
func TestNewDnsCache(t *testing.T) {
|
|
cache := NewDnsCache(newTestCacheConfig())
|
|
|
|
if cache == nil {
|
|
t.Fatal("NewDnsCache returned nil")
|
|
}
|
|
if cache.maxSize != 100 {
|
|
t.Errorf("expected maxSize=100, got %d", cache.maxSize)
|
|
}
|
|
if cache.lruList.Len() != 0 {
|
|
t.Errorf("expected empty cache, got %d items", cache.lruList.Len())
|
|
}
|
|
}
|
|
|
|
func TestNewDnsCacheDefaultSize(t *testing.T) {
|
|
// Zero size should default to 4096
|
|
config := CacheConfig{Enabled: true, Size: 0}
|
|
cache := NewDnsCache(&config)
|
|
if cache.maxSize != 4096 {
|
|
t.Errorf("expected default maxSize=4096, got %d", cache.maxSize)
|
|
}
|
|
|
|
// Negative size should default to 4096
|
|
config = CacheConfig{Enabled: true, Size: -1}
|
|
cache = NewDnsCache(&config)
|
|
if cache.maxSize != 4096 {
|
|
t.Errorf("expected default maxSize=4096 for negative, got %d", cache.maxSize)
|
|
}
|
|
}
|
|
|
|
// --- Test: Set and Get basic ---
|
|
|
|
func TestSetAndGet(t *testing.T) {
|
|
cache := NewDnsCache(newTestCacheConfig())
|
|
key := newTestCacheKey("example.com", TypeA)
|
|
resp := newTestDNSResponse("example.com", TypeA, dns.RcodeSuccess, 300)
|
|
|
|
// Set cache
|
|
cache.Set(key, resp, 300)
|
|
|
|
// Get cache
|
|
got, ok := cache.Get(key)
|
|
if !ok {
|
|
t.Fatal("expected cache hit, got miss")
|
|
}
|
|
if got == nil {
|
|
t.Fatal("expected non-nil response")
|
|
}
|
|
if got.Rcode != dns.RcodeSuccess {
|
|
t.Errorf("expected RcodeSuccess, got %d", got.Rcode)
|
|
}
|
|
if !got.Cached {
|
|
t.Error("expected Cached=true for cache response")
|
|
}
|
|
}
|
|
|
|
func TestGetMiss(t *testing.T) {
|
|
cache := NewDnsCache(newTestCacheConfig())
|
|
key := newTestCacheKey("nonexistent.com", TypeA)
|
|
|
|
got, ok := cache.Get(key)
|
|
if ok {
|
|
t.Error("expected cache miss for non-existent key")
|
|
}
|
|
if got != nil {
|
|
t.Error("expected nil response for cache miss")
|
|
}
|
|
}
|
|
|
|
// --- Test: TTL expiration ---
|
|
|
|
func TestTTLExpiration(t *testing.T) {
|
|
config := newTestCacheConfig()
|
|
config.MinTTL = 1 // 1 second min TTL for fast test
|
|
config.MaxTTL = 2
|
|
cache := NewDnsCache(config)
|
|
|
|
key := newTestCacheKey("example.com", TypeA)
|
|
resp := newTestDNSResponse("example.com", TypeA, dns.RcodeSuccess, 1)
|
|
|
|
cache.Set(key, resp, 1)
|
|
|
|
// Should be immediately available
|
|
if _, ok := cache.Get(key); !ok {
|
|
t.Fatal("expected cache hit immediately after set")
|
|
}
|
|
|
|
// Wait for TTL to expire
|
|
time.Sleep(3 * time.Second)
|
|
|
|
// Should be expired now
|
|
if _, ok := cache.Get(key); ok {
|
|
t.Error("expected cache miss after TTL expiration")
|
|
}
|
|
}
|
|
|
|
// --- Test: TTL clamping (min/max) ---
|
|
|
|
func TestTTLClampingMin(t *testing.T) {
|
|
config := newTestCacheConfig()
|
|
config.MinTTL = 60
|
|
config.MaxTTL = 86400
|
|
cache := NewDnsCache(config)
|
|
|
|
key := newTestCacheKey("example.com", TypeA)
|
|
resp := newTestDNSResponse("example.com", TypeA, dns.RcodeSuccess, 5) // original TTL=5
|
|
|
|
cache.Set(key, resp, 5)
|
|
|
|
// The effective TTL should be clamped to minTTL=60
|
|
if elem, ok := cache.entries[key]; ok {
|
|
entry := elem.Value.(*CacheEntry)
|
|
if entry.EffectiveTTL != 60 {
|
|
t.Errorf("expected effective TTL=60 (clamped from 5), got %d", entry.EffectiveTTL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTTLClampingMax(t *testing.T) {
|
|
config := newTestCacheConfig()
|
|
config.MinTTL = 60
|
|
config.MaxTTL = 600
|
|
cache := NewDnsCache(config)
|
|
|
|
key := newTestCacheKey("example.com", TypeA)
|
|
resp := newTestDNSResponse("example.com", TypeA, dns.RcodeSuccess, 86400) // original TTL=86400
|
|
|
|
cache.Set(key, resp, 86400)
|
|
|
|
// The effective TTL should be clamped to maxTTL=600
|
|
if elem, ok := cache.entries[key]; ok {
|
|
entry := elem.Value.(*CacheEntry)
|
|
if entry.EffectiveTTL != 600 {
|
|
t.Errorf("expected effective TTL=600 (clamped from 86400), got %d", entry.EffectiveTTL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTTLClampingMiddle(t *testing.T) {
|
|
config := newTestCacheConfig()
|
|
config.MinTTL = 60
|
|
config.MaxTTL = 86400
|
|
cache := NewDnsCache(config)
|
|
|
|
key := newTestCacheKey("example.com", TypeA)
|
|
resp := newTestDNSResponse("example.com", TypeA, dns.RcodeSuccess, 300) // original TTL=300
|
|
|
|
cache.Set(key, resp, 300)
|
|
|
|
// The effective TTL should be 300 (within [60, 86400])
|
|
if elem, ok := cache.entries[key]; ok {
|
|
entry := elem.Value.(*CacheEntry)
|
|
if entry.EffectiveTTL != 300 {
|
|
t.Errorf("expected effective TTL=300, got %d", entry.EffectiveTTL)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Test: calculateEffectiveTTL ---
|
|
|
|
func TestCalculateEffectiveTTL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
original uint32
|
|
minTTL int
|
|
maxTTL int
|
|
expected uint32
|
|
}{
|
|
{"within range", 300, 60, 86400, 300},
|
|
{"below min", 5, 60, 86400, 60},
|
|
{"above max", 100000, 60, 86400, 86400},
|
|
{"zero min", 300, 0, 86400, 300}, // min defaults to 60, but 300 > 60
|
|
{"zero max", 300, 60, 0, 300}, // max defaults to 86400, but 300 < 86400
|
|
{"zero both", 300, 0, 0, 300},
|
|
{"exact min", 60, 60, 86400, 60},
|
|
{"exact max", 86400, 60, 86400, 86400},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := calculateEffectiveTTL(tt.original, tt.minTTL, tt.maxTTL)
|
|
if got != tt.expected {
|
|
t.Errorf("calculateEffectiveTTL(%d, %d, %d) = %d, want %d",
|
|
tt.original, tt.minTTL, tt.maxTTL, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- Test: negativeTTL ---
|
|
|
|
func TestNegativeTTL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
rcode int
|
|
expected uint32
|
|
}{
|
|
{"NXDOMAIN", dns.RcodeNameError, 60},
|
|
{"SERVFAIL", dns.RcodeServerFailure, 30},
|
|
{"REFUSED", dns.RcodeRefused, 10},
|
|
{"other error", dns.RcodeNotImplemented, 5},
|
|
{"success", dns.RcodeSuccess, 5}, // Not negative, but function returns default
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := negativeTTL(tt.rcode)
|
|
if got != tt.expected {
|
|
t.Errorf("negativeTTL(%d) = %d, want %d", tt.rcode, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- Test: Negative cache ---
|
|
|
|
func TestNegativeCache(t *testing.T) {
|
|
config := newTestCacheConfig()
|
|
config.MinTTL = 1
|
|
config.MaxTTL = 86400
|
|
cache := NewDnsCache(config)
|
|
|
|
key := newTestCacheKey("nxdomain.example.com", TypeA)
|
|
resp := newTestDNSResponse("nxdomain.example.com", TypeA, dns.RcodeNameError, 0)
|
|
|
|
cache.Set(key, resp, 0)
|
|
|
|
// Verify it's marked as negative
|
|
if elem, ok := cache.entries[key]; ok {
|
|
entry := elem.Value.(*CacheEntry)
|
|
if !entry.Negative {
|
|
t.Error("expected Negative=true for NXDOMAIN response")
|
|
}
|
|
if entry.EffectiveTTL != 60 {
|
|
t.Errorf("expected negative TTL=60 for NXDOMAIN, got %d", entry.EffectiveTTL)
|
|
}
|
|
}
|
|
|
|
// Should be retrievable
|
|
got, ok := cache.Get(key)
|
|
if !ok {
|
|
t.Fatal("expected cache hit for negative entry")
|
|
}
|
|
if got.Rcode != dns.RcodeNameError {
|
|
t.Errorf("expected RcodeNameError, got %d", got.Rcode)
|
|
}
|
|
}
|
|
|
|
func TestNegativeCacheSERVFAIL(t *testing.T) {
|
|
config := newTestCacheConfig()
|
|
cache := NewDnsCache(config)
|
|
|
|
key := newTestCacheKey("servfail.example.com", TypeA)
|
|
resp := newTestDNSResponse("servfail.example.com", TypeA, dns.RcodeServerFailure, 0)
|
|
|
|
cache.Set(key, resp, 0)
|
|
|
|
if elem, ok := cache.entries[key]; ok {
|
|
entry := elem.Value.(*CacheEntry)
|
|
if entry.EffectiveTTL != 30 {
|
|
t.Errorf("expected negative TTL=30 for SERVFAIL, got %d", entry.EffectiveTTL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNegativeCacheREFUSED(t *testing.T) {
|
|
config := newTestCacheConfig()
|
|
cache := NewDnsCache(config)
|
|
|
|
key := newTestCacheKey("refused.example.com", TypeA)
|
|
resp := newTestDNSResponse("refused.example.com", TypeA, dns.RcodeRefused, 0)
|
|
|
|
cache.Set(key, resp, 0)
|
|
|
|
if elem, ok := cache.entries[key]; ok {
|
|
entry := elem.Value.(*CacheEntry)
|
|
if entry.EffectiveTTL != 10 {
|
|
t.Errorf("expected negative TTL=10 for REFUSED, got %d", entry.EffectiveTTL)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Test: LRU eviction ---
|
|
|
|
func TestLRUEviction(t *testing.T) {
|
|
config := newTestCacheConfig()
|
|
config.Size = 3 // Very small cache for easy eviction
|
|
cache := NewDnsCache(config)
|
|
|
|
// Add 3 items (cache full)
|
|
resp1 := newTestDNSResponse("alpha.com", TypeA, dns.RcodeSuccess, 300)
|
|
resp2 := newTestDNSResponse("beta.com", TypeA, dns.RcodeSuccess, 300)
|
|
resp3 := newTestDNSResponse("gamma.com", TypeA, dns.RcodeSuccess, 300)
|
|
|
|
cache.Set(newTestCacheKey("alpha.com", TypeA), resp1, 300)
|
|
cache.Set(newTestCacheKey("beta.com", TypeA), resp2, 300)
|
|
cache.Set(newTestCacheKey("gamma.com", TypeA), resp3, 300)
|
|
|
|
if cache.Len() != 3 {
|
|
t.Errorf("expected 3 items, got %d", cache.Len())
|
|
}
|
|
|
|
// Access alpha to make it recently used
|
|
cache.Get(newTestCacheKey("alpha.com", TypeA))
|
|
|
|
// Add 4th item - should evict the least recently used (beta, since gamma was last added)
|
|
resp4 := newTestDNSResponse("delta.com", TypeA, dns.RcodeSuccess, 300)
|
|
cache.Set(newTestCacheKey("delta.com", TypeA), resp4, 300)
|
|
|
|
if cache.Len() != 3 {
|
|
t.Errorf("expected 3 items after eviction, got %d", cache.Len())
|
|
}
|
|
|
|
// beta should have been evicted (least recently used)
|
|
if _, ok := cache.Get(newTestCacheKey("beta.com", TypeA)); ok {
|
|
t.Error("expected beta to be evicted (LRU)")
|
|
}
|
|
|
|
// alpha and delta should still be present
|
|
if _, ok := cache.Get(newTestCacheKey("alpha.com", TypeA)); !ok {
|
|
t.Error("expected alpha to still be in cache")
|
|
}
|
|
if _, ok := cache.Get(newTestCacheKey("delta.com", TypeA)); !ok {
|
|
t.Error("expected delta to still be in cache")
|
|
}
|
|
|
|
// Check eviction stats
|
|
stats := cache.Stats()
|
|
if stats.Evictions != 1 {
|
|
t.Errorf("expected 1 eviction, got %d", stats.Evictions)
|
|
}
|
|
}
|
|
|
|
// --- Test: Remove ---
|
|
|
|
func TestRemove(t *testing.T) {
|
|
cache := NewDnsCache(newTestCacheConfig())
|
|
key := newTestCacheKey("example.com", TypeA)
|
|
resp := newTestDNSResponse("example.com", TypeA, dns.RcodeSuccess, 300)
|
|
|
|
cache.Set(key, resp, 300)
|
|
|
|
// Verify it's there
|
|
if _, ok := cache.Get(key); !ok {
|
|
t.Fatal("expected cache hit before removal")
|
|
}
|
|
|
|
// Remove it
|
|
cache.Remove(key)
|
|
|
|
// Verify it's gone
|
|
if _, ok := cache.Get(key); ok {
|
|
t.Error("expected cache miss after removal")
|
|
}
|
|
}
|
|
|
|
// --- Test: Clear ---
|
|
|
|
func TestClear(t *testing.T) {
|
|
cache := NewDnsCache(newTestCacheConfig())
|
|
|
|
// Add multiple items
|
|
for i := 0; i < 10; i++ {
|
|
name := fmt.Sprintf("host%d.example.com", i)
|
|
key := newTestCacheKey(name, TypeA)
|
|
resp := newTestDNSResponse(name, TypeA, dns.RcodeSuccess, 300)
|
|
cache.Set(key, resp, 300)
|
|
}
|
|
|
|
if cache.Len() != 10 {
|
|
t.Errorf("expected 10 items, got %d", cache.Len())
|
|
}
|
|
|
|
cache.Clear()
|
|
|
|
if cache.Len() != 0 {
|
|
t.Errorf("expected 0 items after clear, got %d", cache.Len())
|
|
}
|
|
|
|
stats := cache.Stats()
|
|
if stats.Size != 0 {
|
|
t.Errorf("expected stats.Size=0 after clear, got %d", stats.Size)
|
|
}
|
|
}
|
|
|
|
// --- Test: Stats ---
|
|
|
|
func TestStats(t *testing.T) {
|
|
config := newTestCacheConfig()
|
|
config.Size = 3
|
|
cache := NewDnsCache(config)
|
|
|
|
stats := cache.Stats()
|
|
if stats.Size != 0 {
|
|
t.Errorf("expected initial Size=0, got %d", stats.Size)
|
|
}
|
|
if stats.Hits != 0 {
|
|
t.Errorf("expected initial Hits=0, got %d", stats.Hits)
|
|
}
|
|
if stats.Misses != 0 {
|
|
t.Errorf("expected initial Misses=0, got %d", stats.Misses)
|
|
}
|
|
|
|
// Add and hit
|
|
key := newTestCacheKey("example.com", TypeA)
|
|
resp := newTestDNSResponse("example.com", TypeA, dns.RcodeSuccess, 300)
|
|
cache.Set(key, resp, 300)
|
|
cache.Get(key)
|
|
|
|
stats = cache.Stats()
|
|
if stats.Hits != 1 {
|
|
t.Errorf("expected 1 hit, got %d", stats.Hits)
|
|
}
|
|
|
|
// Miss
|
|
cache.Get(newTestCacheKey("nonexistent.com", TypeA))
|
|
stats = cache.Stats()
|
|
if stats.Misses != 1 {
|
|
t.Errorf("expected 1 miss, got %d", stats.Misses)
|
|
}
|
|
|
|
// Hit rate should be 0.5 (1 hit / 2 total)
|
|
if stats.HitRate != 0.5 {
|
|
t.Errorf("expected HitRate=0.5, got %f", stats.HitRate)
|
|
}
|
|
}
|
|
|
|
// --- Test: Concurrent safety ---
|
|
|
|
func TestConcurrentAccess(t *testing.T) {
|
|
config := newTestCacheConfig()
|
|
config.Size = 2000 // Large enough for concurrent writes
|
|
cache := NewDnsCache(config)
|
|
var wg sync.WaitGroup
|
|
numGoroutines := 20
|
|
numOps := 50
|
|
|
|
// Concurrent writes
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < numOps; j++ {
|
|
name := fmt.Sprintf("host%d-%d.example.com", id, j)
|
|
key := newTestCacheKey(name, TypeA)
|
|
resp := newTestDNSResponse(name, TypeA, dns.RcodeSuccess, 300)
|
|
cache.Set(key, resp, 300)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
expectedTotal := numGoroutines * numOps
|
|
if cache.Len() != expectedTotal {
|
|
t.Errorf("expected %d items after concurrent writes, got %d", expectedTotal, cache.Len())
|
|
}
|
|
|
|
// Concurrent reads
|
|
var readWg sync.WaitGroup
|
|
for i := 0; i < numGoroutines; i++ {
|
|
readWg.Add(1)
|
|
go func(id int) {
|
|
defer readWg.Done()
|
|
for j := 0; j < numOps; j++ {
|
|
name := fmt.Sprintf("host%d-%d.example.com", id, j)
|
|
key := newTestCacheKey(name, TypeA)
|
|
cache.Get(key)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
readWg.Wait()
|
|
|
|
stats := cache.Stats()
|
|
if stats.Hits == 0 {
|
|
t.Error("expected some cache hits from concurrent reads")
|
|
}
|
|
|
|
// Concurrent mixed read/write/remove
|
|
var mixedWg sync.WaitGroup
|
|
for i := 0; i < 10; i++ {
|
|
mixedWg.Add(1)
|
|
go func(id int) {
|
|
defer mixedWg.Done()
|
|
for j := 0; j < 20; j++ {
|
|
name := fmt.Sprintf("host%d-%d.example.com", id, j)
|
|
key := newTestCacheKey(name, TypeA)
|
|
switch j % 3 {
|
|
case 0:
|
|
resp := newTestDNSResponse(name, TypeA, dns.RcodeSuccess, 300)
|
|
cache.Set(key, resp, 300)
|
|
case 1:
|
|
cache.Get(key)
|
|
case 2:
|
|
cache.Remove(key)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
mixedWg.Wait()
|
|
|
|
// Should not panic
|
|
_ = cache.Stats()
|
|
_ = cache.Len()
|
|
_ = cache.Keys()
|
|
}
|
|
|
|
// --- Test: Update existing entry ---
|
|
|
|
func TestUpdateExistingEntry(t *testing.T) {
|
|
cache := NewDnsCache(newTestCacheConfig())
|
|
key := newTestCacheKey("example.com", TypeA)
|
|
|
|
// Initial set
|
|
resp1 := newTestDNSResponse("example.com", TypeA, dns.RcodeSuccess, 300)
|
|
cache.Set(key, resp1, 300)
|
|
|
|
// Update with different data
|
|
resp2 := newTestDNSResponse("example.com", TypeA, dns.RcodeSuccess, 600)
|
|
cache.Set(key, resp2, 600)
|
|
|
|
// Verify updated
|
|
if elem, ok := cache.entries[key]; ok {
|
|
entry := elem.Value.(*CacheEntry)
|
|
if entry.OriginalTTL != 600 {
|
|
t.Errorf("expected updated OriginalTTL=600, got %d", entry.OriginalTTL)
|
|
}
|
|
if entry.Prefetching {
|
|
t.Error("expected Prefetching=false after update")
|
|
}
|
|
}
|
|
|
|
// Verify hit count is from the new entry
|
|
got, ok := cache.Get(key)
|
|
if !ok {
|
|
t.Fatal("expected cache hit after update")
|
|
}
|
|
if got.TTL != 600 {
|
|
t.Errorf("expected TTL=600 from updated entry, got %d", got.TTL)
|
|
}
|
|
}
|
|
|
|
// --- Test: Prefetch logic ---
|
|
|
|
func TestShouldPrefetchBasic(t *testing.T) {
|
|
config := newTestCacheConfig()
|
|
config.Prefetch = true
|
|
config.PrefetchThreshold = 30
|
|
cache := NewDnsCache(config)
|
|
|
|
key := newTestCacheKey("example.com", TypeA)
|
|
resp := newTestDNSResponse("example.com", TypeA, dns.RcodeSuccess, 86400) // very long TTL
|
|
cache.Set(key, resp, 86400)
|
|
|
|
// Just after set, should not prefetch (TTL just started)
|
|
shouldPrefetch := cache.ShouldPrefetch(key)
|
|
if shouldPrefetch {
|
|
t.Log("Note: ShouldPrefetch returned true immediately after set (may vary based on timing)")
|
|
}
|
|
}
|
|
|
|
func TestShouldPrefetchAlreadyPrefetching(t *testing.T) {
|
|
config := newTestCacheConfig()
|
|
config.Prefetch = true
|
|
cache := NewDnsCache(config)
|
|
|
|
key := newTestCacheKey("example.com", TypeA)
|
|
resp := newTestDNSResponse("example.com", TypeA, dns.RcodeSuccess, 300)
|
|
cache.Set(key, resp, 300)
|
|
|
|
// Mark as already prefetching
|
|
cache.SetPrefetching(key, true)
|
|
|
|
// Should not trigger prefetch again
|
|
if cache.ShouldPrefetch(key) {
|
|
t.Error("expected ShouldPrefetch=false when already prefetching")
|
|
}
|
|
}
|
|
|
|
func TestShouldPrefetchDisabled(t *testing.T) {
|
|
config := newTestCacheConfig()
|
|
config.Prefetch = false
|
|
cache := NewDnsCache(config)
|
|
|
|
key := newTestCacheKey("example.com", TypeA)
|
|
resp := newTestDNSResponse("example.com", TypeA, dns.RcodeSuccess, 5) // very short TTL
|
|
cache.Set(key, resp, 5)
|
|
|
|
// With prefetch disabled, should never prefetch
|
|
if cache.ShouldPrefetch(key) {
|
|
t.Error("expected ShouldPrefetch=false when prefetch is disabled")
|
|
}
|
|
}
|
|
|
|
func TestSetPrefetching(t *testing.T) {
|
|
cache := NewDnsCache(newTestCacheConfig())
|
|
|
|
key := newTestCacheKey("example.com", TypeA)
|
|
resp := newTestDNSResponse("example.com", TypeA, dns.RcodeSuccess, 300)
|
|
cache.Set(key, resp, 300)
|
|
|
|
// Set prefetching
|
|
cache.SetPrefetching(key, true)
|
|
if elem, ok := cache.entries[key]; ok {
|
|
entry := elem.Value.(*CacheEntry)
|
|
if !entry.Prefetching {
|
|
t.Error("expected Prefetching=true after SetPrefetching(true)")
|
|
}
|
|
}
|
|
|
|
// Unset prefetching
|
|
cache.SetPrefetching(key, false)
|
|
if elem, ok := cache.entries[key]; ok {
|
|
entry := elem.Value.(*CacheEntry)
|
|
if entry.Prefetching {
|
|
t.Error("expected Prefetching=false after SetPrefetching(false)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Test: Prefetch failure backoff ---
|
|
|
|
func TestPrefetchBackoff(t *testing.T) {
|
|
tests := []struct {
|
|
failCount int
|
|
expected time.Duration
|
|
}{
|
|
{0, 0},
|
|
{1, 5 * time.Second},
|
|
{2, 30 * time.Second},
|
|
{3, 120 * time.Second},
|
|
{4, 120 * time.Second}, // capped at 120s
|
|
{10, 120 * time.Second},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := prefetchBackoff(tt.failCount)
|
|
if got != tt.expected {
|
|
t.Errorf("prefetchBackoff(%d) = %v, want %v", tt.failCount, got, tt.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRecordPrefetchFailure(t *testing.T) {
|
|
cache := NewDnsCache(newTestCacheConfig())
|
|
|
|
key := newTestCacheKey("example.com", TypeA)
|
|
resp := newTestDNSResponse("example.com", TypeA, dns.RcodeSuccess, 300)
|
|
cache.Set(key, resp, 300)
|
|
|
|
// Record failures
|
|
cache.RecordPrefetchFailure(key)
|
|
cache.RecordPrefetchFailure(key)
|
|
|
|
if elem, ok := cache.entries[key]; ok {
|
|
entry := elem.Value.(*CacheEntry)
|
|
if entry.PrefetchFailCount != 2 {
|
|
t.Errorf("expected PrefetchFailCount=2, got %d", entry.PrefetchFailCount)
|
|
}
|
|
if entry.PrefetchBackoff != 30*time.Second {
|
|
t.Errorf("expected PrefetchBackoff=30s, got %v", entry.PrefetchBackoff)
|
|
}
|
|
if entry.Prefetching {
|
|
t.Error("expected Prefetching=false after failure")
|
|
}
|
|
}
|
|
|
|
// After 3 failures, ShouldPrefetch should return false
|
|
cache.RecordPrefetchFailure(key)
|
|
if cache.ShouldPrefetch(key) {
|
|
t.Error("expected ShouldPrefetch=false after 3 failures")
|
|
}
|
|
}
|
|
|
|
func TestRecordPrefetchSuccess(t *testing.T) {
|
|
cache := NewDnsCache(newTestCacheConfig())
|
|
|
|
key := newTestCacheKey("example.com", TypeA)
|
|
resp := newTestDNSResponse("example.com", TypeA, dns.RcodeSuccess, 300)
|
|
cache.Set(key, resp, 300)
|
|
|
|
// Fail twice, then succeed
|
|
cache.RecordPrefetchFailure(key)
|
|
cache.RecordPrefetchFailure(key)
|
|
cache.RecordPrefetchSuccess(key)
|
|
|
|
if elem, ok := cache.entries[key]; ok {
|
|
entry := elem.Value.(*CacheEntry)
|
|
if entry.PrefetchFailCount != 0 {
|
|
t.Errorf("expected PrefetchFailCount=0 after success, got %d", entry.PrefetchFailCount)
|
|
}
|
|
if entry.PrefetchBackoff != 0 {
|
|
t.Errorf("expected PrefetchBackoff=0 after success, got %v", entry.PrefetchBackoff)
|
|
}
|
|
if entry.Prefetching {
|
|
t.Error("expected Prefetching=false after success")
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Test: Different query types ---
|
|
|
|
func TestCacheKeyIsolationByType(t *testing.T) {
|
|
cache := NewDnsCache(newTestCacheConfig())
|
|
|
|
keyA := newTestCacheKey("example.com", TypeA)
|
|
keyAAAA := newTestCacheKey("example.com", TypeAAAA)
|
|
|
|
respA := newTestDNSResponse("example.com", TypeA, dns.RcodeSuccess, 300)
|
|
respAAAA := newTestDNSResponse("example.com", TypeAAAA, dns.RcodeSuccess, 300)
|
|
|
|
cache.Set(keyA, respA, 300)
|
|
cache.Set(keyAAAA, respAAAA, 300)
|
|
|
|
// Both should be independently accessible
|
|
if _, ok := cache.Get(keyA); !ok {
|
|
t.Error("expected cache hit for A record")
|
|
}
|
|
if _, ok := cache.Get(keyAAAA); !ok {
|
|
t.Error("expected cache hit for AAAA record")
|
|
}
|
|
}
|
|
|
|
func TestCacheKeyIsolationByUpstream(t *testing.T) {
|
|
cache := NewDnsCache(newTestCacheConfig())
|
|
|
|
key1 := CacheKey{UpstreamAddr: "8.8.8.8:53", ProxyTag: "direct", Name: "example.com.", QType: TypeA}
|
|
key2 := CacheKey{UpstreamAddr: "1.1.1.1:53", ProxyTag: "direct", Name: "example.com.", QType: TypeA}
|
|
|
|
resp := newTestDNSResponse("example.com", TypeA, dns.RcodeSuccess, 300)
|
|
|
|
cache.Set(key1, resp, 300)
|
|
|
|
// Different upstream should not share cache
|
|
if _, ok := cache.Get(key2); ok {
|
|
t.Error("expected cache miss for different upstream")
|
|
}
|
|
}
|
|
|
|
func TestCacheKeyIsolationByProxyTag(t *testing.T) {
|
|
cache := NewDnsCache(newTestCacheConfig())
|
|
|
|
keyDirect := CacheKey{UpstreamAddr: "8.8.8.8:53", ProxyTag: "direct", Name: "example.com.", QType: TypeA}
|
|
keyProxy := CacheKey{UpstreamAddr: "8.8.8.8:53", ProxyTag: "proxy", Name: "example.com.", QType: TypeA}
|
|
|
|
resp := newTestDNSResponse("example.com", TypeA, dns.RcodeSuccess, 300)
|
|
|
|
cache.Set(keyDirect, resp, 300)
|
|
|
|
// Different proxy tag should not share cache
|
|
if _, ok := cache.Get(keyProxy); ok {
|
|
t.Error("expected cache miss for different proxy tag")
|
|
}
|
|
}
|
|
|
|
// --- Test: Empty cache behavior ---
|
|
|
|
func TestEmptyCache(t *testing.T) {
|
|
cache := NewDnsCache(&CacheConfig{})
|
|
|
|
if cache.Len() != 0 {
|
|
t.Errorf("expected empty cache, got %d items", cache.Len())
|
|
}
|
|
|
|
stats := cache.Stats()
|
|
if stats.Size != 0 {
|
|
t.Errorf("expected Size=0, got %d", stats.Size)
|
|
}
|
|
|
|
keys := cache.Keys()
|
|
if len(keys) != 0 {
|
|
t.Errorf("expected 0 keys, got %d", len(keys))
|
|
}
|
|
}
|
|
|
|
// --- Test: Keys method ---
|
|
|
|
func TestKeys(t *testing.T) {
|
|
cache := NewDnsCache(&CacheConfig{})
|
|
|
|
// Add some entries
|
|
names := []string{"alpha.com", "beta.com", "gamma.com"}
|
|
for _, name := range names {
|
|
key := newTestCacheKey(name, TypeA)
|
|
resp := newTestDNSResponse(name, TypeA, dns.RcodeSuccess, 300)
|
|
cache.Set(key, resp, 300)
|
|
}
|
|
|
|
keys := cache.Keys()
|
|
if len(keys) != 3 {
|
|
t.Errorf("expected 3 keys, got %d", len(keys))
|
|
}
|
|
}
|
|
|
|
// --- Test: Concurrent Set/Get/Remove on same key ---
|
|
|
|
func TestConcurrentSameKey(t *testing.T) {
|
|
cache := NewDnsCache(&CacheConfig{})
|
|
key := newTestCacheKey("contested.com", TypeA)
|
|
var wg sync.WaitGroup
|
|
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < 10; j++ {
|
|
resp := newTestDNSResponse("contested.com", TypeA, dns.RcodeSuccess, uint32(100+j))
|
|
cache.Set(key, resp, uint32(100+j))
|
|
cache.Get(key)
|
|
cache.Remove(key)
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
// Should not panic or deadlock
|
|
t.Log("concurrent same-key operations completed without deadlock")
|
|
}
|
|
|
|
// --- Test: buildCacheKey (from handler.go) ---
|
|
|
|
func TestBuildCacheKey(t *testing.T) {
|
|
query := newTestQuery("example.com", TypeA)
|
|
upstream := &UpstreamInstance{
|
|
Addr: "8.8.8.8:53",
|
|
ProxyTag: "proxy",
|
|
}
|
|
|
|
key := buildCacheKey(query, upstream)
|
|
if key.UpstreamAddr != "8.8.8.8:53" {
|
|
t.Errorf("expected UpstreamAddr=8.8.8.8:53, got %s", key.UpstreamAddr)
|
|
}
|
|
if key.ProxyTag != "proxy" {
|
|
t.Errorf("expected ProxyTag=proxy, got %s", key.ProxyTag)
|
|
}
|
|
if key.Name != "example.com" {
|
|
t.Errorf("expected Name=example.com, got %s", key.Name)
|
|
}
|
|
if key.QType != TypeA {
|
|
t.Errorf("expected QType=A, got %d", key.QType)
|
|
}
|
|
}
|
|
|
|
func TestBuildCacheKeyNilUpstream(t *testing.T) {
|
|
query := newTestQuery("example.com", TypeA)
|
|
key := buildCacheKey(query, nil)
|
|
if key.ProxyTag != "direct" {
|
|
t.Errorf("expected ProxyTag=direct for nil upstream, got %s", key.ProxyTag)
|
|
}
|
|
if key.UpstreamAddr != "" {
|
|
t.Errorf("expected empty UpstreamAddr for nil upstream, got %s", key.UpstreamAddr)
|
|
}
|
|
}
|
|
|
|
// --- Test: Large number of entries ---
|
|
|
|
func TestLargeCache(t *testing.T) {
|
|
config := newTestCacheConfig()
|
|
config.Size = 1000
|
|
cache := NewDnsCache(config)
|
|
|
|
// Add 1000 entries
|
|
for i := 0; i < 1000; i++ {
|
|
name := fmt.Sprintf("host%d.example.com", i)
|
|
key := newTestCacheKey(name, TypeA)
|
|
resp := newTestDNSResponse(name, TypeA, dns.RcodeSuccess, 300)
|
|
cache.Set(key, resp, 300)
|
|
}
|
|
|
|
if cache.Len() != 1000 {
|
|
t.Errorf("expected 1000 entries, got %d", cache.Len())
|
|
}
|
|
|
|
// Add one more - should evict one
|
|
extraKey := newTestCacheKey("extra.example.com", TypeA)
|
|
extraResp := newTestDNSResponse("extra.example.com", TypeA, dns.RcodeSuccess, 300)
|
|
cache.Set(extraKey, extraResp, 300)
|
|
|
|
if cache.Len() != 1000 {
|
|
t.Errorf("expected 1000 entries after eviction, got %d", cache.Len())
|
|
}
|
|
|
|
stats := cache.Stats()
|
|
if stats.Evictions != 1 {
|
|
t.Errorf("expected 1 eviction, got %d", stats.Evictions)
|
|
}
|
|
}
|