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  }