github.com/cilium/cilium@v1.16.2/pkg/fqdn/cache_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package fqdn 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "math/rand/v2" 10 "net" 11 "net/netip" 12 "regexp" 13 "sort" 14 "testing" 15 "time" 16 17 "github.com/stretchr/testify/require" 18 "k8s.io/apimachinery/pkg/util/sets" 19 20 "github.com/cilium/cilium/pkg/defaults" 21 "github.com/cilium/cilium/pkg/fqdn/re" 22 "github.com/cilium/cilium/pkg/ip" 23 ) 24 25 func init() { 26 re.InitRegexCompileLRU(defaults.FQDNRegexCompileLRUSize) 27 } 28 29 // TestUpdateLookup tests that we can insert DNS data and retrieve it. We 30 // iterate through time, ensuring that data is expired as appropriate. We also 31 // insert redundant DNS entries that should not change the output. 32 func TestUpdateLookup(t *testing.T) { 33 name := "test.com" 34 now := time.Now() 35 cache := NewDNSCache(0) 36 endTimeSeconds := 4 37 38 // Add 1 new entry "per second", and one with a redundant IP (with ttl/2). 39 // The IP reflects the second in which it will expire, and should show up for 40 // all now+ttl that is less than it. 41 for i := 1; i <= endTimeSeconds; i++ { 42 ttl := i 43 cache.Update(now, 44 name, 45 []netip.Addr{netip.MustParseAddr(fmt.Sprintf("1.1.1.%d", i)), netip.MustParseAddr(fmt.Sprintf("2.2.2.%d", i))}, 46 ttl) 47 48 cache.Update(now, 49 name, 50 []netip.Addr{netip.MustParseAddr(fmt.Sprintf("1.1.1.%d", i))}, 51 ttl/2) 52 } 53 54 // lookup our entries 55 // - no redundant entries (the 1.1.1.x is not repeated) 56 // - with each step of secondsPastNow, fewer entries are returned 57 for secondsPastNow := 1; secondsPastNow <= endTimeSeconds; secondsPastNow++ { 58 ips := cache.lookupByTime(now.Add(time.Duration(secondsPastNow)*time.Second), name) 59 require.Len(t, ips, 2*(endTimeSeconds-secondsPastNow+1), "Incorrect number of IPs returned") 60 61 // This test expects ips sorted 62 ip.SortAddrList(ips) 63 64 // Check that we returned each 1.1.1.x entry where x={1..endTimeSeconds} 65 // These are sorted, and are in the first half of the array 66 // Similarly, check the 2.2.2.x entries in the second half of the array 67 j := secondsPastNow 68 halfIndex := endTimeSeconds - secondsPastNow + 1 69 for _, ip := range ips[:halfIndex] { 70 require.Equalf(t, fmt.Sprintf("1.1.1.%d", j), ip.String(), "Incorrect IP returned (j=%d, secondsPastNow=%d)", j, secondsPastNow) 71 j++ 72 } 73 j = secondsPastNow 74 for _, ip := range ips[halfIndex:] { 75 require.Equalf(t, fmt.Sprintf("2.2.2.%d", j), ip.String(), "Incorrect IP returned (j=%d, secondsPastNow=%d)", j, secondsPastNow) 76 j++ 77 } 78 } 79 } 80 81 // TestDelete tests that we can forcibly clear parts of the cache. 82 func TestDelete(t *testing.T) { 83 names := map[string]netip.Addr{ 84 "test1.com": netip.MustParseAddr("2.2.2.1"), 85 "test2.com": netip.MustParseAddr("2.2.2.2"), 86 "test3.com": netip.MustParseAddr("2.2.2.3")} 87 sharedIP := netip.MustParseAddr("1.1.1.1") 88 now := time.Now() 89 cache := NewDNSCache(0) 90 91 // Insert 3 records with 1 shared IP and 3 with different IPs 92 cache.Update(now, "test1.com", []netip.Addr{sharedIP, names["test1.com"]}, 5) 93 cache.Update(now, "test2.com", []netip.Addr{sharedIP, names["test2.com"]}, 5) 94 cache.Update(now, "test3.com", []netip.Addr{sharedIP, names["test3.com"]}, 5) 95 96 now = now.Add(time.Second) 97 98 // Test that a non-matching ForceExpire doesn't do anything. All data should 99 // still be present. 100 nameMatch, err := regexp.Compile("^notatest.com$") 101 require.Nil(t, err) 102 namesAffected := cache.ForceExpire(now, nameMatch) 103 require.Lenf(t, namesAffected, 0, "Incorrect count of names removed %v", namesAffected) 104 for _, name := range []string{"test1.com", "test2.com", "test3.com"} { 105 ips := cache.lookupByTime(now, name) 106 require.Lenf(t, ips, 2, "Wrong count of IPs returned (%v) for non-deleted name '%s'", ips, name) 107 require.Containsf(t, cache.forward, name, "Expired name '%s' not deleted from forward", name) 108 for _, ip := range ips { 109 require.Containsf(t, cache.reverse, ip, "Expired IP '%s' not deleted from reverse", ip) 110 } 111 } 112 113 // Delete a single name and check that 114 // - It is returned in namesAffected 115 // - Lookups for it show no data, but data remains for other names 116 nameMatch, err = regexp.Compile("^test1.com$") 117 require.Nil(t, err) 118 namesAffected = cache.ForceExpire(now, nameMatch) 119 require.Lenf(t, namesAffected, 1, "Incorrect count of names removed %v", namesAffected) 120 require.Containsf(t, namesAffected, "test1.com", "Incorrect affected name returned on forced expire: %s", namesAffected) 121 ips := cache.lookupByTime(now, "test1.com") 122 require.Lenf(t, ips, 0, "IPs returned (%v) for deleted name 'test1.com'", ips) 123 require.NotContains(t, cache.forward, "test1.com", "Expired name 'test1.com' not deleted from forward") 124 for _, ip := range ips { 125 require.Containsf(t, cache.reverse, ip, "Expired IP '%s' not deleted from reverse", ip) 126 } 127 for _, name := range []string{"test2.com", "test3.com"} { 128 ips = cache.lookupByTime(now, name) 129 require.Lenf(t, ips, 2, "Wrong count of IPs returned (%v) for non-deleted name '%s'", ips, name) 130 require.Containsf(t, cache.forward, name, "Expired name '%s' not deleted from forward", name) 131 for _, ip := range ips { 132 require.Containsf(t, cache.reverse, ip, "Expired IP '%s' not deleted from reverse", ip) 133 } 134 } 135 136 // Delete the whole cache. This should leave no data. 137 namesAffected = cache.ForceExpire(now, nil) 138 require.Lenf(t, namesAffected, 2, "Incorrect count of names removed %v", namesAffected) 139 for _, name := range []string{"test2.com", "test3.com"} { 140 require.Contains(t, namesAffected, name, "Incorrect affected name returned on forced expire") 141 } 142 for name := range names { 143 ips = cache.lookupByTime(now, name) 144 require.Lenf(t, ips, 0, "Returned IP data for %s after the cache was fully cleared: %v", name, ips) 145 } 146 require.Len(t, cache.forward, 0) 147 require.Len(t, cache.reverse, 0) 148 dump := cache.Dump() 149 require.Lenf(t, dump, 0, "Returned cache entries from cache dump after the cache was fully cleared: %v", dump) 150 } 151 152 func Test_forceExpiredByNames(t *testing.T) { 153 names := []string{"test1.com", "test2.com"} 154 cache := NewDNSCache(0) 155 for i := 1; i < 4; i++ { 156 cache.Update( 157 now, 158 fmt.Sprintf("test%d.com", i), 159 []netip.Addr{netip.MustParseAddr(fmt.Sprintf("1.1.1.%d", i))}, 160 5) 161 } 162 163 require.Len(t, cache.forward, 3) 164 cache.forceExpireByNames(time.Now(), names) 165 require.NotNil(t, cache.forward["test3.com"]) 166 } 167 168 func TestReverseUpdateLookup(t *testing.T) { 169 names := map[string]netip.Addr{ 170 "test1.com": netip.MustParseAddr("2.2.2.1"), 171 "test2.com": netip.MustParseAddr("2.2.2.2"), 172 "test3.com": netip.MustParseAddr("2.2.2.3")} 173 sharedIP := netip.MustParseAddr("1.1.1.1") 174 now := time.Now() 175 cache := NewDNSCache(0) 176 177 // insert 2 records, with 1 shared IP 178 cache.Update(now, "test1.com", []netip.Addr{sharedIP, names["test1.com"]}, 2) 179 cache.Update(now, "test2.com", []netip.Addr{sharedIP, names["test2.com"]}, 4) 180 181 // lookup within the TTL for both names should return 2 names for sharedIPs, 182 // and one name for the 2.2.2.* IPs 183 currentTime := now.Add(time.Second) 184 lookupNames := cache.lookupIPByTime(currentTime, sharedIP) 185 require.Len(t, lookupNames, 2, "Incorrect number of names returned") 186 for _, name := range lookupNames { 187 _, found := names[name] 188 require.True(t, found, "Returned a DNS name that doesn't match IP") 189 } 190 191 lookupNames = cache.lookupIPByTime(currentTime, names["test1.com"]) 192 require.Len(t, lookupNames, 1, "Incorrect number of names returned") 193 require.Equal(t, lookupNames[0], "test1.com", "Returned a DNS name that doesn't match IP") 194 195 lookupNames = cache.lookupIPByTime(currentTime, names["test2.com"]) 196 require.Len(t, lookupNames, 1, "Incorrect number of names returned") 197 require.Equal(t, lookupNames[0], "test2.com", "Returned a DNS name that doesn't match IP") 198 199 lookupNames = cache.lookupIPByTime(currentTime, names["test3.com"]) 200 require.Len(t, lookupNames, 0, "Returned names for IP not in cache") 201 202 // lookup between 2-4 seconds later (test1.com has expired) for both names 203 // should return 2 names for sharedIPs, and one name for the 2.2.2.* IPs 204 currentTime = now.Add(3 * time.Second) 205 lookupNames = cache.lookupIPByTime(currentTime, sharedIP) 206 require.Len(t, lookupNames, 1, "Incorrect number of names returned") 207 require.Equal(t, "test2.com", lookupNames[0], "Returned a DNS name that doesn't match IP") 208 209 lookupNames = cache.lookupIPByTime(currentTime, names["test1.com"]) 210 require.Len(t, lookupNames, 0, "Incorrect number of names returned") 211 212 lookupNames = cache.lookupIPByTime(currentTime, names["test2.com"]) 213 require.Len(t, lookupNames, 1, "Incorrect number of names returned") 214 require.Equal(t, lookupNames[0], "test2.com", "Returned a DNS name that doesn't match IP") 215 216 lookupNames = cache.lookupIPByTime(currentTime, names["test3.com"]) 217 require.Len(t, lookupNames, 0, "Returned names for IP not in cache") 218 219 // lookup between after 4 seconds later (all have expired) for both names 220 // should return no names in all cases. 221 currentTime = now.Add(5 * time.Second) 222 lookupNames = cache.lookupIPByTime(currentTime, sharedIP) 223 require.Len(t, lookupNames, 0, "Incorrect number of names returned") 224 225 lookupNames = cache.lookupIPByTime(currentTime, names["test1.com"]) 226 require.Len(t, lookupNames, 0, "Incorrect number of names returned") 227 228 lookupNames = cache.lookupIPByTime(currentTime, names["test2.com"]) 229 require.Len(t, lookupNames, 0, "Incorrect number of names returned") 230 231 lookupNames = cache.lookupIPByTime(currentTime, names["test3.com"]) 232 require.Len(t, lookupNames, 0, "Returned names for IP not in cache") 233 } 234 235 func TestJSONMarshal(t *testing.T) { 236 names := map[string]netip.Addr{ 237 "test1.com": netip.MustParseAddr("2.2.2.1"), 238 "test2.com": netip.MustParseAddr("2.2.2.2"), 239 "test3.com": netip.MustParseAddr("2.2.2.3")} 240 sharedIP := netip.MustParseAddr("1.1.1.1") 241 now := time.Now() 242 cache := NewDNSCache(0) 243 244 // insert 3 records with 1 shared IP and 3 with different IPs 245 cache.Update(now, "test1.com", []netip.Addr{sharedIP}, 5) 246 cache.Update(now, "test2.com", []netip.Addr{sharedIP}, 5) 247 cache.Update(now, "test3.com", []netip.Addr{sharedIP}, 5) 248 cache.Update(now, "test1.com", []netip.Addr{names["test1.com"]}, 5) 249 cache.Update(now, "test2.com", []netip.Addr{names["test2.com"]}, 5) 250 cache.Update(now, "test3.com", []netip.Addr{names["test3.com"]}, 5) 251 252 // Marshal and unmarshal 253 data, err := cache.MarshalJSON() 254 require.Nil(t, err) 255 256 newCache := NewDNSCache(0) 257 err = newCache.UnmarshalJSON(data) 258 require.Nil(t, err) 259 260 // Marshalled data should have no duplicate entries Note: this is tightly 261 // coupled with the implementation of DNSCache.MarshalJSON because the 262 // unmarshalled instance will hide duplicates. We simply check the length 263 // since we control the inserted data, and we test its correctness below. 264 rawList := make([]*cacheEntry, 0) 265 err = json.Unmarshal(data, &rawList) 266 require.Nil(t, err) 267 require.Equal(t, 6, len(rawList)) 268 269 // Check that the unmarshalled instance contains all the data at now 270 currentTime := now 271 for name := range names { 272 IPs := cache.lookupByTime(currentTime, name) 273 ip.SortAddrList(IPs) 274 require.Lenf(t, IPs, 2, "Incorrect number of IPs returned for %s", name) 275 require.Equalf(t, sharedIP.String(), IPs[0].String(), "Returned an IP that doesn't match %s", name) 276 require.Equalf(t, names[name].String(), IPs[1].String(), "Returned an IP name that doesn't match %s", name) 277 } 278 279 // Check that the unmarshalled data expires correctly 280 currentTime = now.Add(10 * time.Second) 281 for name := range names { 282 IPs := cache.lookupByTime(currentTime, name) 283 require.Len(t, IPs, 0, "Returned IPs that should be expired for %s", name) 284 } 285 } 286 287 func TestCountIPs(t *testing.T) { 288 names := map[string]netip.Addr{ 289 "test1.com": netip.MustParseAddr("1.1.1.1"), 290 "test2.com": netip.MustParseAddr("2.2.2.2"), 291 "test3.com": netip.MustParseAddr("3.3.3.3")} 292 sharedIP := netip.MustParseAddr("8.8.8.8") 293 cache := NewDNSCache(0) 294 295 // Insert 3 records all sharing one IP and 1 unique IP. 296 cache.Update(now, "test1.com", []netip.Addr{sharedIP, names["test1.com"]}, 5) 297 cache.Update(now, "test2.com", []netip.Addr{sharedIP, names["test2.com"]}, 5) 298 cache.Update(now, "test3.com", []netip.Addr{sharedIP, names["test3.com"]}, 5) 299 300 fqdns, ips := cache.Count() 301 302 // Dump() returns the deduplicated (or consolidated) list of entries with 303 // length equal to CountFQDNs(), while CountIPs() returns the raw number of 304 // IPs. 305 require.Equal(t, len(names), len(cache.Dump())) 306 require.Equal(t, len(names), int(fqdns)) 307 require.Equal(t, len(names)*2, int(ips)) 308 } 309 310 /* Benchmarks 311 * These are here to help gauge the relative costs of operations in DNSCache. 312 * Note: some are on arrays `size` elements, so the benchmark "op time" is too 313 * large. 314 */ 315 316 var ( 317 now = time.Now() 318 size = uint32(1000) // size of array to operate on 319 entriesOrig = makeEntries(now, 1+size/3, 1+size/3, 1+size/3) 320 ) 321 322 // makeIPs generates count sequential IPv4 IPs 323 func makeIPs(count uint32) []netip.Addr { 324 ips := make([]netip.Addr, 0, count) 325 for i := uint32(0); i < count; i++ { 326 ips = append(ips, netip.AddrFrom4([4]byte{byte(i >> 24), byte(i >> 16), byte(i >> 8), byte(i >> 0)})) 327 } 328 return ips 329 } 330 331 func makeEntries(now time.Time, live, redundant, expired uint32) (entries []*cacheEntry) { 332 liveTTL := 120 333 redundantTTL := 60 334 335 for ; live > 0; live-- { 336 ip := netip.AddrFrom4([4]byte{byte(live >> 24), byte(live >> 16), byte(live >> 8), byte(live >> 0)}) 337 338 entries = append(entries, &cacheEntry{ 339 Name: fmt.Sprintf("live-%s", ip.String()), 340 LookupTime: now, 341 ExpirationTime: now.Add(time.Duration(liveTTL) * time.Second), 342 TTL: liveTTL, 343 IPs: []netip.Addr{ip}}) 344 345 if redundant > 0 { 346 redundant-- 347 entries = append(entries, &cacheEntry{ 348 Name: fmt.Sprintf("redundant-%s", ip.String()), 349 LookupTime: now, 350 ExpirationTime: now.Add(time.Duration(redundantTTL) * time.Second), 351 TTL: redundantTTL, 352 IPs: []netip.Addr{ip}}) 353 } 354 355 if expired > 0 { 356 expired-- 357 entries = append(entries, &cacheEntry{ 358 Name: fmt.Sprintf("expired-%s", ip.String()), 359 LookupTime: now.Add(-time.Duration(liveTTL) * time.Second), 360 ExpirationTime: now.Add(-time.Second), 361 TTL: liveTTL, 362 IPs: []netip.Addr{ip}}) 363 } 364 } 365 366 rand.Shuffle(len(entries), func(i, j int) { 367 entries[i], entries[j] = entries[j], entries[i] 368 }) 369 370 return entries 371 } 372 373 // Note: each "op" works on size things 374 func BenchmarkGetIPs(b *testing.B) { 375 b.StopTimer() 376 now := time.Now() 377 cache := NewDNSCache(0) 378 cache.Update(now, "test.com", []netip.Addr{netip.MustParseAddr("1.2.3.4")}, 60) 379 entries := cache.forward["test.com"] 380 for _, entry := range entriesOrig { 381 cache.updateWithEntryIPs(entries, entry) 382 } 383 b.StartTimer() 384 385 for i := 0; i < b.N; i++ { 386 entries.getIPs(now) 387 } 388 } 389 390 // Note: each "op" works on size things 391 func BenchmarkUpdateIPs(b *testing.B) { 392 for i := 0; i < b.N; i++ { 393 b.StopTimer() 394 now := time.Now() 395 cache := NewDNSCache(0) 396 cache.Update(now, "test.com", []netip.Addr{netip.MustParseAddr("1.2.3.4")}, 60) 397 entries := cache.forward["test.com"] 398 b.StartTimer() 399 400 for _, entry := range entriesOrig { 401 cache.updateWithEntryIPs(entries, entry) 402 cache.removeExpired(entries, now, time.Time{}) 403 } 404 } 405 } 406 407 // JSON Marshal/Unmarshal benchmarks 408 var numIPsPerEntry = 10 // number of IPs to generate in each entry 409 410 func BenchmarkMarshalJSON10(b *testing.B) { benchmarkMarshalJSON(b, 10) } 411 func BenchmarkMarshalJSON100(b *testing.B) { benchmarkMarshalJSON(b, 100) } 412 func BenchmarkMarshalJSON1000(b *testing.B) { benchmarkMarshalJSON(b, 1000) } 413 func BenchmarkMarshalJSON10000(b *testing.B) { benchmarkMarshalJSON(b, 10000) } 414 415 func BenchmarkUnmarshalJSON10(b *testing.B) { benchmarkUnmarshalJSON(b, 10) } 416 func BenchmarkUnmarshalJSON100(b *testing.B) { benchmarkUnmarshalJSON(b, 100) } 417 func BenchmarkUnmarshalJSON1000(b *testing.B) { 418 benchmarkUnmarshalJSON(b, 1000) 419 } 420 func BenchmarkUnmarshalJSON10000(b *testing.B) { 421 benchmarkUnmarshalJSON(b, 10000) 422 } 423 424 // BenchmarkMarshalJSON100Repeat2 tests whether repeating the whole 425 // serialization is notably slower than a single run. 426 func BenchmarkMarshalJSON100Repeat2(b *testing.B) { 427 benchmarkMarshalJSON(b, 50) 428 benchmarkMarshalJSON(b, 50) 429 } 430 431 func BenchmarkMarshalJSON1000Repeat2(b *testing.B) { 432 benchmarkMarshalJSON(b, 500) 433 benchmarkMarshalJSON(b, 500) 434 } 435 436 // benchmarkMarshalJSON benchmarks the cost of creating a json representation 437 // of DNSCache. Each benchmark "op" is on numDNSEntries. 438 // Note: It assumes the JSON only uses data in DNSCache.forward when generating 439 // the data. Changes to the implementation need to also change this benchmark. 440 func benchmarkMarshalJSON(b *testing.B, numDNSEntries int) { 441 b.StopTimer() 442 ips := makeIPs(uint32(numIPsPerEntry)) 443 444 cache := NewDNSCache(0) 445 for i := 0; i < numDNSEntries; i++ { 446 // TTL needs to be far enough in the future that the entry is serialized 447 cache.Update(time.Now(), fmt.Sprintf("domain-%v.com", i), ips, 86400) 448 } 449 b.StartTimer() 450 451 for i := 0; i < b.N; i++ { 452 _, err := cache.MarshalJSON() 453 require.Nil(b, err) 454 } 455 } 456 457 // benchmarkUnmarshalJSON benchmarks the cost of parsing a json representation 458 // of DNSCache. Each benchmark "op" is on numDNSEntries. 459 // Note: It assumes the JSON only uses data in DNSCache.forward when generating 460 // the data. Changes to the implementation need to also change this benchmark. 461 func benchmarkUnmarshalJSON(b *testing.B, numDNSEntries int) { 462 b.StopTimer() 463 ips := makeIPs(uint32(numIPsPerEntry)) 464 465 cache := NewDNSCache(0) 466 for i := 0; i < numDNSEntries; i++ { 467 // TTL needs to be far enough in the future that the entry is serialized 468 cache.Update(time.Now(), fmt.Sprintf("domain-%v.com", i), ips, 86400) 469 } 470 471 data, err := cache.MarshalJSON() 472 require.Nil(b, err) 473 474 emptyCaches := make([]*DNSCache, b.N) 475 for i := 0; i < b.N; i++ { 476 emptyCaches[i] = NewDNSCache(0) 477 } 478 b.StartTimer() 479 480 for i := 0; i < b.N; i++ { 481 err := emptyCaches[i].UnmarshalJSON(data) 482 require.Nil(b, err) 483 } 484 } 485 486 func TestTTLInsertWithMinValue(t *testing.T) { 487 now := time.Now() 488 cache := NewDNSCache(60) 489 cache.Update(now, "test.com", []netip.Addr{netip.MustParseAddr("1.2.3.4")}, 3) 490 491 // Checking just now to validate that is inserted correctly 492 res := cache.lookupByTime(now, "test.com") 493 require.Len(t, res, 1) 494 require.Equal(t, "1.2.3.4", res[0].String()) 495 496 // Checking the latest match 497 res = cache.lookupByTime(now.Add(time.Second*3), "test.com") 498 require.Len(t, res, 1) 499 require.Equal(t, "1.2.3.4", res[0].String()) 500 501 // Validate that in future time the value is correct 502 future := time.Now().Add(time.Second * 70) 503 res = cache.lookupByTime(future, "test.com") 504 require.Len(t, res, 0) 505 } 506 507 func TestTTLInsertWithZeroValue(t *testing.T) { 508 now := time.Now() 509 cache := NewDNSCache(0) 510 cache.Update(now, "test.com", []netip.Addr{netip.MustParseAddr("1.2.3.4")}, 10) 511 512 // Checking just now to validate that is inserted correctly 513 res := cache.lookupByTime(now, "test.com") 514 require.Len(t, res, 1) 515 require.Equal(t, "1.2.3.4", res[0].String()) 516 517 // Checking the latest match 518 res = cache.lookupByTime(now.Add(time.Second*10), "test.com") 519 require.Len(t, res, 1) 520 require.Equal(t, "1.2.3.4", res[0].String()) 521 522 // Checking that expires correctly 523 future := now.Add(time.Second * 11) 524 res = cache.lookupByTime(future, "test.com") 525 require.Len(t, res, 0) 526 } 527 528 func TestTTLCleanupEntries(t *testing.T) { 529 cache := NewDNSCache(0) 530 cache.Update(now, "test.com", []netip.Addr{netip.MustParseAddr("1.2.3.4")}, 3) 531 require.Equal(t, 1, len(cache.cleanup)) 532 entries, _ := cache.cleanupExpiredEntries(time.Now().Add(5 * time.Second)) 533 require.Len(t, entries, 1) 534 require.Len(t, cache.cleanup, 0) 535 require.Len(t, cache.Lookup("test.com"), 0) 536 } 537 538 func TestTTLCleanupWithoutForward(t *testing.T) { 539 cache := NewDNSCache(0) 540 now := time.Now() 541 cache.cleanup[now.Unix()] = []string{"test.com"} 542 // To make sure that all entries are validated correctly 543 cache.lastCleanup = time.Now().Add(-1 * time.Minute) 544 entries, _ := cache.cleanupExpiredEntries(time.Now().Add(5 * time.Second)) 545 require.Len(t, entries, 0) 546 require.Len(t, cache.cleanup, 0) 547 } 548 549 func TestOverlimitEntriesWithValidLimit(t *testing.T) { 550 limit := 5 551 cache := NewDNSCacheWithLimit(0, limit) 552 553 cache.Update(now, "foo.bar", []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 1) 554 cache.Update(now, "bar.foo", []netip.Addr{netip.MustParseAddr("2.1.1.1")}, 1) 555 for i := 1; i < limit+2; i++ { 556 cache.Update(now, "test.com", []netip.Addr{netip.MustParseAddr(fmt.Sprintf("1.1.1.%d", i))}, i) 557 } 558 affectedNames, _ := cache.cleanupOverLimitEntries() 559 require.EqualValues(t, sets.New[string]("test.com"), affectedNames) 560 561 require.Len(t, cache.Lookup("test.com"), limit) 562 require.EqualValues(t, []string{"foo.bar"}, cache.LookupIP(netip.MustParseAddr("1.1.1.1"))) 563 require.Nil(t, cache.forward["test.com"][netip.MustParseAddr("1.1.1.1")]) 564 require.Len(t, cache.Lookup("foo.bar"), 1) 565 require.Len(t, cache.Lookup("bar.foo"), 1) 566 require.Len(t, cache.overLimit, 0) 567 } 568 569 func TestOverlimitEntriesWithoutLimit(t *testing.T) { 570 limit := 0 571 cache := NewDNSCacheWithLimit(0, limit) 572 for i := 0; i < 5; i++ { 573 cache.Update(now, "test.com", []netip.Addr{netip.MustParseAddr(fmt.Sprintf("1.1.1.%d", i))}, i) 574 } 575 affectedNames, _ := cache.cleanupOverLimitEntries() 576 require.Len(t, affectedNames, 0) 577 require.Len(t, cache.Lookup("test.com"), 5) 578 } 579 580 func TestGCOverlimitAfterTTLCleanup(t *testing.T) { 581 limit := 5 582 cache := NewDNSCacheWithLimit(0, limit) 583 584 // Make sure that the cleanup takes all the changes from 1 minute ago. 585 cache.lastCleanup = time.Now().Add(-1 * time.Minute) 586 for i := 1; i < limit+2; i++ { 587 cache.Update(now, "test.com", []netip.Addr{netip.MustParseAddr(fmt.Sprintf("1.1.1.%d", i))}, 1) 588 } 589 590 require.Len(t, cache.Lookup("test.com"), limit+1) 591 require.Len(t, cache.overLimit, 1) 592 593 result, _ := cache.cleanupExpiredEntries(time.Now().Add(5 * time.Second)) 594 require.EqualValues(t, sets.New[string]("test.com"), result) 595 596 // Due all entries are deleted on TTL, the overlimit should return 0 entries. 597 affectedNames, _ := cache.cleanupOverLimitEntries() 598 require.Len(t, affectedNames, 0) 599 } 600 601 func TestOverlimitAfterDeleteForwardEntry(t *testing.T) { 602 // Validate if something delete the forward entry no invalid key access on 603 // CG operation 604 dnsCache := NewDNSCache(0) 605 dnsCache.overLimit["test.com"] = true 606 affectedNames, _ := dnsCache.cleanupOverLimitEntries() 607 require.Len(t, affectedNames, 0) 608 } 609 610 func assertZombiesContain(t *testing.T, zombies []*DNSZombieMapping, expected map[string][]string) { 611 t.Helper() 612 require.Lenf(t, zombies, len(expected), "Different number of zombies than expected: %+v", zombies) 613 614 for _, zombie := range zombies { 615 names, exists := expected[zombie.IP.String()] 616 require.Truef(t, exists, "Unexpected zombie %s in zombies", zombie.IP.String()) 617 618 sort.Strings(zombie.Names) 619 sort.Strings(names) 620 621 require.Len(t, zombie.Names, len(names)) 622 for i := range zombie.Names { 623 require.Equal(t, names[i], zombie.Names[i], "Unexpected name in zombie names list") 624 } 625 } 626 } 627 628 func TestZombiesSiblingsGC(t *testing.T) { 629 now := time.Now() 630 zombies := NewDNSZombieMappings(defaults.ToFQDNsMaxDeferredConnectionDeletes, defaults.ToFQDNsMaxIPsPerHost) 631 632 // Siblings are IPs that resolve to the same name. 633 zombies.Upsert(now, netip.MustParseAddr("1.1.1.1"), "test.com") 634 zombies.Upsert(now, netip.MustParseAddr("1.1.1.2"), "test.com") 635 zombies.Upsert(now, netip.MustParseAddr("3.3.3.3"), "pizza.com") 636 637 // Mark 1.1.1.2 alive which should also keep 1.1.1.1 alive since they 638 // have the same name 639 now = now.Add(5 * time.Minute) 640 next := now.Add(5 * time.Minute) 641 zombies.SetCTGCTime(now, next) 642 now = now.Add(time.Second) 643 zombies.MarkAlive(now.Add(time.Second), netip.MustParseAddr("1.1.1.2")) 644 zombies.SetCTGCTime(now, next) 645 646 alive, dead := zombies.GC() 647 assertZombiesContain(t, alive, map[string][]string{ 648 "1.1.1.1": {"test.com"}, 649 "1.1.1.2": {"test.com"}, 650 }) 651 assertZombiesContain(t, dead, map[string][]string{ 652 "3.3.3.3": {"pizza.com"}, 653 }) 654 } 655 656 func TestZombiesGC(t *testing.T) { 657 now := time.Now() 658 zombies := NewDNSZombieMappings(defaults.ToFQDNsMaxDeferredConnectionDeletes, defaults.ToFQDNsMaxIPsPerHost) 659 660 zombies.Upsert(now, netip.MustParseAddr("1.1.1.1"), "test.com") 661 zombies.Upsert(now, netip.MustParseAddr("2.2.2.2"), "somethingelse.com") 662 663 // Without any MarkAlive or SetCTGCTime, all entries remain alive 664 alive, dead := zombies.GC() 665 require.Len(t, dead, 0) 666 assertZombiesContain(t, alive, map[string][]string{ 667 "1.1.1.1": {"test.com"}, 668 "2.2.2.2": {"somethingelse.com"}, 669 }) 670 671 // Adding another name to 1.1.1.1 keeps it alive and adds the name to the 672 // zombie 673 zombies.Upsert(now, netip.MustParseAddr("1.1.1.1"), "anotherthing.com") 674 alive, dead = zombies.GC() 675 require.Len(t, dead, 0) 676 assertZombiesContain(t, alive, map[string][]string{ 677 "1.1.1.1": {"test.com", "anotherthing.com"}, 678 "2.2.2.2": {"somethingelse.com"}, 679 }) 680 681 // Even when not marking alive, running CT GC the first time is ignored; 682 // we must always complete 2 GC cycles before allowing a name to be dead 683 now = now.Add(5 * time.Minute) 684 next := now.Add(5 * time.Minute) 685 zombies.SetCTGCTime(now, next) 686 alive, dead = zombies.GC() 687 require.Len(t, dead, 0) 688 assertZombiesContain(t, alive, map[string][]string{ 689 "1.1.1.1": {"test.com", "anotherthing.com"}, 690 "2.2.2.2": {"somethingelse.com"}, 691 }) 692 693 // Cause 1.1.1.1 to die by not marking it alive before the second GC 694 //zombies.MarkAlive(now, netip.MustParseAddr("1.1.1.1")) 695 now = now.Add(5 * time.Minute) 696 next = now.Add(5 * time.Minute) 697 // Mark 2.2.2.2 alive with 1 second grace period 698 zombies.MarkAlive(now.Add(time.Second), netip.MustParseAddr("2.2.2.2")) 699 zombies.SetCTGCTime(now, next) 700 701 // alive should contain 2.2.2.2 -> somethingelse.com 702 // dead should contain 1.1.1.1 -> anotherthing.com, test.com 703 alive, dead = zombies.GC() 704 assertZombiesContain(t, alive, map[string][]string{ 705 "2.2.2.2": {"somethingelse.com"}, 706 }) 707 assertZombiesContain(t, dead, map[string][]string{ 708 "1.1.1.1": {"test.com", "anotherthing.com"}, 709 }) 710 711 // A second GC call only returns alive entries 712 alive, dead = zombies.GC() 713 require.Len(t, dead, 0) 714 require.Len(t, alive, 1) 715 716 // Update 2.2.2.2 with a new DNS name. It remains alive. 717 // Add 1.1.1.1 again. It is alive. 718 zombies.Upsert(now, netip.MustParseAddr("2.2.2.2"), "thelastthing.com") 719 zombies.Upsert(now, netip.MustParseAddr("1.1.1.1"), "onemorething.com") 720 721 alive, dead = zombies.GC() 722 require.Len(t, dead, 0) 723 assertZombiesContain(t, alive, map[string][]string{ 724 "1.1.1.1": {"onemorething.com"}, 725 "2.2.2.2": {"somethingelse.com", "thelastthing.com"}, 726 }) 727 728 // Cause all zombies but 2.2.2.2 to die 729 now = now.Add(5 * time.Minute) 730 next = now.Add(5 * time.Minute) 731 zombies.SetCTGCTime(now, next) 732 now = now.Add(5 * time.Minute) 733 next = now.Add(5 * time.Minute) 734 zombies.MarkAlive(now.Add(time.Second), netip.MustParseAddr("2.2.2.2")) 735 zombies.SetCTGCTime(now, next) 736 alive, dead = zombies.GC() 737 require.Len(t, alive, 1) 738 assertZombiesContain(t, alive, map[string][]string{ 739 "2.2.2.2": {"somethingelse.com", "thelastthing.com"}, 740 }) 741 assertZombiesContain(t, dead, map[string][]string{ 742 "1.1.1.1": {"onemorething.com"}, 743 }) 744 745 // Cause all zombies to die 746 now = now.Add(2 * time.Second) 747 zombies.SetCTGCTime(now, next) 748 alive, dead = zombies.GC() 749 require.Len(t, alive, 0) 750 assertZombiesContain(t, dead, map[string][]string{ 751 "2.2.2.2": {"somethingelse.com", "thelastthing.com"}, 752 }) 753 } 754 755 func TestZombiesGCOverLimit(t *testing.T) { 756 now := time.Now() 757 zombies := NewDNSZombieMappings(defaults.ToFQDNsMaxDeferredConnectionDeletes, 1) 758 759 // Limit the total number of IPs to be associated with a specific host 760 // to 1, but associate 'test.com' with multiple IPs. 761 zombies.Upsert(now, netip.MustParseAddr("1.1.1.1"), "test.com") 762 zombies.Upsert(now, netip.MustParseAddr("2.2.2.2"), "somethingelse.com", "test.com") 763 zombies.Upsert(now, netip.MustParseAddr("3.3.3.3"), "anothertest.com") 764 765 // Based on the zombie liveness sorting, the '2.2.2.2' entry is more 766 // important (as it could potentially impact multiple apps connecting 767 // to different domains), so it should be kept alive when sweeping to 768 // enforce the max per-host IP limit for names. 769 alive, dead := zombies.GC() 770 assertZombiesContain(t, dead, map[string][]string{ 771 "1.1.1.1": {"test.com"}, 772 }) 773 assertZombiesContain(t, alive, map[string][]string{ 774 "2.2.2.2": {"somethingelse.com", "test.com"}, 775 "3.3.3.3": {"anothertest.com"}, 776 }) 777 } 778 779 func TestZombiesGCOverLimitWithCTGC(t *testing.T) { 780 now := time.Now() 781 afterNow := now.Add(1 * time.Nanosecond) 782 maxConnections := 3 783 zombies := NewDNSZombieMappings(defaults.ToFQDNsMaxDeferredConnectionDeletes, maxConnections) 784 zombies.SetCTGCTime(now, afterNow) 785 786 // Limit the number of IPs per hostname, but associate 'test.com' with 787 // more IPs. 788 for i := 0; i < maxConnections+1; i++ { 789 zombies.Upsert(now, netip.MustParseAddr(fmt.Sprintf("1.1.1.%d", i+1)), "test.com") 790 } 791 792 // Simulate that CT garbage collection marks some IPs as live, we'll 793 // use the first 'maxConnections' IPs just so we can sort the output 794 // in the test below. 795 for i := 0; i < maxConnections; i++ { 796 zombies.MarkAlive(afterNow, netip.MustParseAddr(fmt.Sprintf("1.1.1.%d", i+1))) 797 } 798 zombies.SetCTGCTime(afterNow, afterNow.Add(5*time.Minute)) 799 800 // Garbage collection should now impose the maxConnections limit on 801 // the name, prioritizing to keep the active IPs live and then marking 802 // the inactive IP as dead (to delete). 803 alive, dead := zombies.GC() 804 assertZombiesContain(t, dead, map[string][]string{ 805 "1.1.1.4": {"test.com"}, 806 }) 807 assertZombiesContain(t, alive, map[string][]string{ 808 "1.1.1.1": {"test.com"}, 809 "1.1.1.2": {"test.com"}, 810 "1.1.1.3": {"test.com"}, 811 }) 812 } 813 814 func TestZombiesGCDeferredDeletes(t *testing.T) { 815 now := time.Now() 816 zombies := NewDNSZombieMappings(defaults.ToFQDNsMaxDeferredConnectionDeletes, defaults.ToFQDNsMaxIPsPerHost) 817 818 zombies.Upsert(now.Add(0*time.Second), netip.MustParseAddr("1.1.1.1"), "test.com") 819 zombies.Upsert(now.Add(1*time.Second), netip.MustParseAddr("2.2.2.2"), "somethingelse.com") 820 zombies.Upsert(now.Add(2*time.Second), netip.MustParseAddr("3.3.3.3"), "onemorething.com") 821 822 // No zombies should be evicted because the limit is high 823 alive, dead := zombies.GC() 824 require.Len(t, dead, 0) 825 assertZombiesContain(t, alive, map[string][]string{ 826 "1.1.1.1": {"test.com"}, 827 "2.2.2.2": {"somethingelse.com"}, 828 "3.3.3.3": {"onemorething.com"}, 829 }) 830 831 zombies = NewDNSZombieMappings(2, defaults.ToFQDNsMaxIPsPerHost) 832 zombies.Upsert(now.Add(0*time.Second), netip.MustParseAddr("1.1.1.1"), "test.com") 833 834 // No zombies should be evicted because we are below the limit 835 alive, dead = zombies.GC() 836 require.Len(t, dead, 0) 837 assertZombiesContain(t, alive, map[string][]string{ 838 "1.1.1.1": {"test.com"}, 839 }) 840 841 // 1.1.1.1 is evicted because it was Upserted earlier in 842 // time, implying an earlier DNS expiry. 843 zombies.Upsert(now.Add(1*time.Second), netip.MustParseAddr("2.2.2.2"), "somethingelse.com") 844 zombies.Upsert(now.Add(2*time.Second), netip.MustParseAddr("3.3.3.3"), "onemorething.com") 845 alive, dead = zombies.GC() 846 assertZombiesContain(t, dead, map[string][]string{ 847 "1.1.1.1": {"test.com"}, 848 }) 849 assertZombiesContain(t, alive, map[string][]string{ 850 "2.2.2.2": {"somethingelse.com"}, 851 "3.3.3.3": {"onemorething.com"}, 852 }) 853 854 // Only 3.3.3.3 is evicted because it is not marked alive, despite having the 855 // latest insert time. 856 zombies.Upsert(now.Add(0*time.Second), netip.MustParseAddr("1.1.1.1"), "test.com") 857 gcTime := now.Add(4 * time.Second) 858 next := now.Add(4 * time.Second) 859 zombies.MarkAlive(gcTime, netip.MustParseAddr("1.1.1.1")) 860 zombies.MarkAlive(gcTime, netip.MustParseAddr("2.2.2.2")) 861 zombies.SetCTGCTime(gcTime, next) 862 863 alive, dead = zombies.GC() 864 assertZombiesContain(t, dead, map[string][]string{ 865 "3.3.3.3": {"onemorething.com"}, 866 }) 867 assertZombiesContain(t, alive, map[string][]string{ 868 "2.2.2.2": {"somethingelse.com"}, 869 "1.1.1.1": {"test.com"}, 870 }) 871 } 872 873 func TestZombiesForceExpire(t *testing.T) { 874 now := time.Now() 875 zombies := NewDNSZombieMappings(defaults.ToFQDNsMaxDeferredConnectionDeletes, defaults.ToFQDNsMaxIPsPerHost) 876 877 zombies.Upsert(now, netip.MustParseAddr("1.1.1.1"), "test.com", "anothertest.com") 878 zombies.Upsert(now, netip.MustParseAddr("2.2.2.2"), "somethingelse.com") 879 880 // Without any MarkAlive or SetCTGCTime, all entries remain alive 881 alive, dead := zombies.GC() 882 require.Len(t, dead, 0) 883 require.Len(t, alive, 2) 884 885 // Expire only 1 name on 1 zombie 886 nameMatch, err := regexp.Compile("^test.com$") 887 require.Nil(t, err) 888 zombies.ForceExpire(time.Time{}, nameMatch) 889 890 alive, dead = zombies.GC() 891 require.Len(t, dead, 0) 892 assertZombiesContain(t, alive, map[string][]string{ 893 "1.1.1.1": {"anothertest.com"}, 894 "2.2.2.2": {"somethingelse.com"}, 895 }) 896 897 // Expire the last name on a zombie. It will be deleted and not returned in a 898 // GC 899 nameMatch, err = regexp.Compile("^anothertest.com$") 900 require.Nil(t, err) 901 zombies.ForceExpire(time.Time{}, nameMatch) 902 alive, dead = zombies.GC() 903 require.Len(t, dead, 0) 904 assertZombiesContain(t, alive, map[string][]string{ 905 "2.2.2.2": {"somethingelse.com"}, 906 }) 907 908 // Setup again with 2 names for test.com 909 zombies.Upsert(now, netip.MustParseAddr("2.2.2.2"), "test.com") 910 911 // Don't expire if the IP doesn't match 912 err = zombies.ForceExpireByNameIP(time.Time{}, "somethingelse.com", net.ParseIP("1.1.1.1")) 913 require.Nil(t, err) 914 alive, dead = zombies.GC() 915 require.Len(t, dead, 0) 916 assertZombiesContain(t, alive, map[string][]string{ 917 "2.2.2.2": {"somethingelse.com", "test.com"}, 918 }) 919 920 // Expire 1 name for this IP but leave other names 921 err = zombies.ForceExpireByNameIP(time.Time{}, "somethingelse.com", net.ParseIP("2.2.2.2")) 922 require.Nil(t, err) 923 alive, dead = zombies.GC() 924 require.Len(t, dead, 0) 925 assertZombiesContain(t, alive, map[string][]string{ 926 "2.2.2.2": {"test.com"}, 927 }) 928 929 // Don't remove if the name doesn't match 930 err = zombies.ForceExpireByNameIP(time.Time{}, "blarg.com", net.ParseIP("2.2.2.2")) 931 require.Nil(t, err) 932 alive, dead = zombies.GC() 933 require.Len(t, dead, 0) 934 assertZombiesContain(t, alive, map[string][]string{ 935 "2.2.2.2": {"test.com"}, 936 }) 937 938 // Clear everything 939 err = zombies.ForceExpireByNameIP(time.Time{}, "test.com", net.ParseIP("2.2.2.2")) 940 require.Nil(t, err) 941 alive, dead = zombies.GC() 942 require.Len(t, dead, 0) 943 require.Len(t, alive, 0) 944 assertZombiesContain(t, alive, map[string][]string{}) 945 } 946 947 func TestCacheToZombiesGCCascade(t *testing.T) { 948 now := time.Now() 949 cache := NewDNSCache(0) 950 zombies := NewDNSZombieMappings(defaults.ToFQDNsMaxDeferredConnectionDeletes, defaults.ToFQDNsMaxIPsPerHost) 951 952 // Add entries that should expire at different times 953 cache.Update(now, "test.com", []netip.Addr{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("2.2.2.2")}, 3) 954 cache.Update(now, "test.com", []netip.Addr{netip.MustParseAddr("3.3.3.3")}, 5) 955 956 // Cascade expirations from cache to zombies. The 3.3.3.3 lookup has not expired 957 now = now.Add(4 * time.Second) 958 expired := cache.GC(now, zombies) 959 require.Equal(t, 1, expired.Len()) // test.com 960 // Not all IPs expired (3.3.3.3 still alive) so we expect test.com to be 961 // present in the cache. 962 require.Contains(t, cache.forward, "test.com") 963 require.Contains(t, cache.reverse, netip.MustParseAddr("3.3.3.3")) 964 require.NotContains(t, cache.reverse, netip.MustParseAddr("1.1.1.1")) 965 require.NotContains(t, cache.reverse, netip.MustParseAddr("2.2.2.2")) 966 alive, dead := zombies.GC() 967 require.Len(t, dead, 0) 968 assertZombiesContain(t, alive, map[string][]string{ 969 "1.1.1.1": {"test.com"}, 970 "2.2.2.2": {"test.com"}, 971 }) 972 973 // Cascade expirations from cache to zombies. The 3.3.3.3 lookup has expired 974 // but the older zombies are still alive. 975 now = now.Add(4 * time.Second) 976 expired = cache.GC(now, zombies) 977 require.Equal(t, 1, expired.Len()) // test.com 978 // Now all IPs expired so we expect test.com to be removed from the cache. 979 require.NotContains(t, cache.forward, "test.com") 980 require.Len(t, cache.forward, 0) 981 require.NotContains(t, cache.reverse, "3.3.3.") 982 require.Len(t, cache.reverse, 0) 983 alive, dead = zombies.GC() 984 require.Len(t, dead, 0) 985 assertZombiesContain(t, alive, map[string][]string{ 986 "1.1.1.1": {"test.com"}, 987 "2.2.2.2": {"test.com"}, 988 "3.3.3.3": {"test.com"}, 989 }) 990 } 991 992 func TestZombiesDumpAlive(t *testing.T) { 993 now := time.Now() 994 zombies := NewDNSZombieMappings(defaults.ToFQDNsMaxDeferredConnectionDeletes, defaults.ToFQDNsMaxIPsPerHost) 995 996 alive := zombies.DumpAlive(nil) 997 require.Len(t, alive, 0) 998 999 zombies.Upsert(now, netip.MustParseAddr("1.1.1.1"), "test.com") 1000 zombies.Upsert(now, netip.MustParseAddr("2.2.2.2"), "example.com") 1001 zombies.Upsert(now, netip.MustParseAddr("3.3.3.3"), "example.org") 1002 1003 alive = zombies.DumpAlive(nil) 1004 assertZombiesContain(t, alive, map[string][]string{ 1005 "1.1.1.1": {"test.com"}, 1006 "2.2.2.2": {"example.com"}, 1007 "3.3.3.3": {"example.org"}, 1008 }) 1009 1010 // Simulate an interleaved CTGC and DNS GC 1011 // Ensure that two GC runs must progress before 1012 // marking zombies dead. 1013 now = now.Add(time.Second) 1014 next := now.Add(5 * time.Minute) 1015 zombies.SetCTGCTime(now, next) 1016 alive = zombies.DumpAlive(nil) 1017 assertZombiesContain(t, alive, map[string][]string{ 1018 "1.1.1.1": {"test.com"}, 1019 "2.2.2.2": {"example.com"}, 1020 "3.3.3.3": {"example.org"}, 1021 }) 1022 1023 now = now.Add(5 * time.Minute) // Need to step the clock 5 minutes ahead here, to account for the grace period 1024 next = now.Add(5 * time.Minute) 1025 zombies.MarkAlive(now, netip.MustParseAddr("1.1.1.1")) 1026 zombies.MarkAlive(now, netip.MustParseAddr("2.2.2.2")) 1027 zombies.SetCTGCTime(now, next) 1028 1029 alive = zombies.DumpAlive(nil) 1030 assertZombiesContain(t, alive, map[string][]string{ 1031 "1.1.1.1": {"test.com"}, 1032 "2.2.2.2": {"example.com"}, 1033 }) 1034 1035 cidrMatcher := func(addr netip.Addr) bool { return false } 1036 alive = zombies.DumpAlive(cidrMatcher) 1037 require.Len(t, alive, 0) 1038 1039 cidrMatcher = func(_ netip.Addr) bool { return true } 1040 alive = zombies.DumpAlive(cidrMatcher) 1041 assertZombiesContain(t, alive, map[string][]string{ 1042 "1.1.1.1": {"test.com"}, 1043 "2.2.2.2": {"example.com"}, 1044 }) 1045 1046 prefix := netip.MustParsePrefix("1.1.1.0/24") 1047 cidrMatcher = func(a netip.Addr) bool { return prefix.Contains(a) } 1048 alive = zombies.DumpAlive(cidrMatcher) 1049 assertZombiesContain(t, alive, map[string][]string{ 1050 "1.1.1.1": {"test.com"}, 1051 }) 1052 1053 zombies.Upsert(now, netip.MustParseAddr("1.1.1.2"), "test2.com") 1054 1055 alive = zombies.DumpAlive(cidrMatcher) 1056 assertZombiesContain(t, alive, map[string][]string{ 1057 "1.1.1.1": {"test.com"}, 1058 "1.1.1.2": {"test2.com"}, 1059 }) 1060 1061 prefix = netip.MustParsePrefix("4.4.0.0/16") 1062 cidrMatcher = func(a netip.Addr) bool { return prefix.Contains(a) } 1063 alive = zombies.DumpAlive(cidrMatcher) 1064 require.Len(t, alive, 0) 1065 } 1066 1067 func TestOverlimitPreferNewerEntries(t *testing.T) { 1068 toFQDNsMinTTL := 100 1069 toFQDNsMaxIPsPerHost := 5 1070 cache := NewDNSCacheWithLimit(toFQDNsMinTTL, toFQDNsMaxIPsPerHost) 1071 1072 toFQDNsMaxDeferredConnectionDeletes := 10 1073 zombies := NewDNSZombieMappings(toFQDNsMaxDeferredConnectionDeletes, toFQDNsMaxIPsPerHost) 1074 1075 name := "test.com" 1076 IPs := []netip.Addr{ 1077 netip.MustParseAddr("1.1.1.1"), 1078 netip.MustParseAddr("1.1.1.2"), 1079 netip.MustParseAddr("1.1.1.3"), 1080 netip.MustParseAddr("1.1.1.4"), 1081 netip.MustParseAddr("1.1.1.5"), 1082 netip.MustParseAddr("1.1.1.6"), 1083 netip.MustParseAddr("1.1.1.7"), 1084 netip.MustParseAddr("1.1.1.8"), 1085 netip.MustParseAddr("1.1.1.9"), 1086 netip.MustParseAddr("1.1.1.10"), 1087 netip.MustParseAddr("1.1.1.11"), 1088 netip.MustParseAddr("1.1.1.12"), 1089 netip.MustParseAddr("1.1.1.13"), 1090 netip.MustParseAddr("1.1.1.14"), 1091 netip.MustParseAddr("1.1.1.15"), 1092 netip.MustParseAddr("1.1.1.16"), 1093 netip.MustParseAddr("1.1.1.17"), 1094 netip.MustParseAddr("1.1.1.18"), 1095 netip.MustParseAddr("1.1.1.19"), 1096 netip.MustParseAddr("1.1.1.20"), 1097 } 1098 ttl := 0 // will be overwritten with toFQDNsMinTTL 1099 1100 now := time.Now() 1101 for i, ip := range IPs { 1102 // Entries with lower values in last IP octet will expire earlier 1103 lookupTime := now.Add(-time.Duration(len(IPs)-i) * time.Second) 1104 cache.Update(lookupTime, name, []netip.Addr{ip}, ttl) 1105 } 1106 1107 affected := cache.GC(time.Now(), zombies) 1108 1109 require.Equal(t, 1, affected.Len()) 1110 require.Equal(t, true, affected.Has(name)) 1111 1112 // No entries have expired, but no more than toFQDNsMaxIPsPerHost can be 1113 // kept in the cache. 1114 // The exceeding ones will be moved to the zombies cache due to overlimit 1115 require.Len(t, cache.forward[name], toFQDNsMaxIPsPerHost) 1116 1117 alive, dead := zombies.GC() 1118 1119 // No more than toFQDNsMaxIPsPerHost entries will be kept 1120 // alive in the zombies cache as well 1121 require.Len(t, alive, toFQDNsMaxIPsPerHost) 1122 1123 // More recent entries (i.e. entries with later expire time) will be kept alive 1124 assertZombiesContain(t, alive, map[string][]string{ 1125 "1.1.1.11": {name}, 1126 "1.1.1.12": {name}, 1127 "1.1.1.13": {name}, 1128 "1.1.1.14": {name}, 1129 "1.1.1.15": {name}, 1130 }) 1131 1132 // Older entries will be evicted 1133 assertZombiesContain(t, dead, map[string][]string{ 1134 "1.1.1.1": {name}, 1135 "1.1.1.2": {name}, 1136 "1.1.1.3": {name}, 1137 "1.1.1.4": {name}, 1138 "1.1.1.5": {name}, 1139 "1.1.1.6": {name}, 1140 "1.1.1.7": {name}, 1141 "1.1.1.8": {name}, 1142 "1.1.1.9": {name}, 1143 "1.1.1.10": {name}, 1144 }) 1145 } 1146 1147 // Define a test-only string representation to make the output below more readable. 1148 func (z *DNSZombieMapping) String() string { 1149 return fmt.Sprintf( 1150 "DNSZombieMapping{AliveAt: %s, DeletePendingAt: %s, Names: %v}", 1151 z.AliveAt, z.DeletePendingAt, z.Names, 1152 ) 1153 } 1154 1155 func validateZombieSort(t *testing.T, zombies []*DNSZombieMapping) { 1156 t.Helper() 1157 sl := len(zombies) 1158 1159 logFailure := func(t *testing.T, zs []*DNSZombieMapping, prop string, i, j int) { 1160 t.Helper() 1161 t.Logf("order property fail %v: want zombie[i] < zombie[j]", prop) 1162 t.Log("zombie[i]: ", zs[i]) 1163 t.Log("zombie[j]: ", zs[j]) 1164 t.Log("all mappings: ") 1165 for i, z := range zs { 1166 t.Log(fmt.Sprintf("%2d", i), z) 1167 } 1168 } 1169 // Don't try to be efficient, just check that the properties we want hold 1170 // for every pair of zombie mappings. 1171 for i := 0; i < sl; i++ { 1172 for j := i + 1; j < sl; j++ { 1173 if zombies[i].AliveAt.Before(zombies[j].AliveAt) { 1174 continue 1175 } else if zombies[i].AliveAt.After(zombies[j].AliveAt) { 1176 logFailure(t, zombies, "AliveAt", i, j) 1177 t.Fatalf("order wrong: AliveAt: %v is after %v", zombies[i].AliveAt, zombies[j].AliveAt) 1178 return 1179 } 1180 1181 if zombies[i].DeletePendingAt.Before(zombies[j].DeletePendingAt) { 1182 continue 1183 } else if zombies[i].DeletePendingAt.After(zombies[j].DeletePendingAt) { 1184 logFailure(t, zombies, "DeletePendingAt", i, j) 1185 t.Fatalf("order wrong: DeletePendingAt: %v is after %v", zombies[i].DeletePendingAt, zombies[j].DeletePendingAt) 1186 return 1187 } 1188 1189 if len(zombies[i].Names) > len(zombies[j].Names) { 1190 logFailure(t, zombies, "len(names)", i, j) 1191 t.Fatalf("order wrong: len(names): %v is longer than %v", zombies[i].Names, zombies[j].Names) 1192 } 1193 } 1194 } 1195 } 1196 1197 func Test_sortZombieMappingSlice(t *testing.T) { 1198 // Create three moments in time, so we can have before, equal and after. 1199 moments := []time.Time{ 1200 time.Date(2001, time.January, 1, 1, 1, 1, 0, time.Local), 1201 time.Date(2002, time.February, 2, 2, 2, 2, 0, time.Local), 1202 time.Date(2003, time.March, 3, 3, 3, 3, 0, time.Local), 1203 } 1204 1205 // Couple of edge cases/hand-picked scenarios. To be complemented by the 1206 // randomly generated ones, below. 1207 type args struct { 1208 zombies []*DNSZombieMapping 1209 } 1210 tests := []struct { 1211 name string 1212 args args 1213 }{ 1214 { 1215 "empty", 1216 args{zombies: nil}, 1217 }, 1218 { 1219 "single", 1220 args{zombies: []*DNSZombieMapping{{ 1221 Names: []string{"test.com"}, 1222 AliveAt: moments[0], 1223 DeletePendingAt: moments[1], 1224 }}}, 1225 }, 1226 { 1227 "swapped alive at", 1228 args{zombies: []*DNSZombieMapping{ 1229 { 1230 AliveAt: moments[2], 1231 }, 1232 { 1233 AliveAt: moments[0], 1234 }, 1235 }}, 1236 }, 1237 { 1238 "equal alive, swapped delete pending at", 1239 args{zombies: []*DNSZombieMapping{ 1240 { 1241 AliveAt: moments[0], 1242 DeletePendingAt: moments[2], 1243 }, 1244 { 1245 AliveAt: moments[0], 1246 DeletePendingAt: moments[1], 1247 }, 1248 }}, 1249 }, 1250 { 1251 "swapped equal times, tiebreaker", 1252 args{zombies: []*DNSZombieMapping{ 1253 { 1254 Names: []string{"test.com", "test2.com"}, 1255 AliveAt: moments[0], 1256 DeletePendingAt: moments[1], 1257 }, 1258 { 1259 Names: []string{"test.com"}, 1260 AliveAt: moments[0], 1261 DeletePendingAt: moments[1], 1262 }, 1263 }}, 1264 }, 1265 } 1266 1267 // Generate zombie mappings which cover all cases of the two times 1268 // being either moment 0, 1 or 2, as well as with 0, 1 or 2 names. 1269 names := []string{"example.org", "test.com"} 1270 nMoments := len(moments) 1271 allMappings := make([]*DNSZombieMapping, 0, nMoments*nMoments*nMoments) 1272 for _, mi := range moments { 1273 for _, mj := range moments { 1274 for k := range names { 1275 m := DNSZombieMapping{ 1276 AliveAt: mi, 1277 DeletePendingAt: mj, 1278 Names: names[:k], 1279 } 1280 allMappings = append(allMappings, &m) 1281 } 1282 } 1283 } 1284 1285 // Five random tests: 1286 for i := 0; i < 5; i++ { 1287 ts := make([]*DNSZombieMapping, len(allMappings)) 1288 copy(ts, allMappings) 1289 rand.Shuffle(len(ts), func(i, j int) { 1290 ts[i], ts[j] = ts[j], ts[i] 1291 }) 1292 tests = append(tests, struct { 1293 name string 1294 args args 1295 }{ 1296 name: "Randomised sorting test", 1297 args: args{ 1298 zombies: ts, 1299 }, 1300 }) 1301 } 1302 1303 for _, tt := range tests { 1304 t.Run(tt.name, func(t *testing.T) { 1305 ol := len(tt.args.zombies) 1306 sortZombieMappingSlice(tt.args.zombies) 1307 if len(tt.args.zombies) != ol { 1308 t.Fatalf("length of slice changed by sorting") 1309 } 1310 validateZombieSort(t, tt.args.zombies) 1311 }) 1312 } 1313 }