github.com/elfadel/cilium@v1.6.12/pkg/fqdn/cache.go (about)

     1  // Copyright 2018 Authors of Cilium
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package fqdn
    16  
    17  import (
    18  	"encoding/json"
    19  	"net"
    20  	"regexp"
    21  	"sort"
    22  	"time"
    23  	"unsafe"
    24  
    25  	"github.com/cilium/cilium/pkg/ip"
    26  	"github.com/cilium/cilium/pkg/lock"
    27  )
    28  
    29  // cacheEntry objects hold data passed in via DNSCache.Update, nominally
    30  // equating to a DNS lookup. They are internal to DNSCache and should not be
    31  // returned.
    32  // cacheEntry objects are immutable once created; the address of an instance is
    33  // a unique identifier.
    34  // Note: the JSON names are intended to correlate to field names from
    35  // api/v1/models.DNSLookup to allow dumping the json from
    36  // `cilium fqdn cache list` to a file that can be unmarshalled via
    37  // `--tofqdns-per-cache`
    38  type cacheEntry struct {
    39  	// Name is a DNS name, it my be not fully qualified (e.g. myservice.namespace)
    40  	Name string `json:"fqdn,omitempty"`
    41  
    42  	// LookupTime is when the data begins being valid
    43  	LookupTime time.Time `json:"lookup-time,omitempty"`
    44  
    45  	// ExpirationTime is a calcutated time when the DNS data stops being valid.
    46  	// It is simply LookupTime + TTL
    47  	ExpirationTime time.Time `json:"expiration-time,omitempty"`
    48  
    49  	// TTL represents the number of seconds past LookupTime that this data is
    50  	// valid.
    51  	TTL int `json:"ttl,omitempty"`
    52  
    53  	// IPs are the IPs associated with Name for this cacheEntry.
    54  	IPs []net.IP `json:"ips,omitempty"`
    55  }
    56  
    57  // isExpiredBy returns true if entry is no longer valid at pointInTime
    58  func (entry *cacheEntry) isExpiredBy(pointInTime time.Time) bool {
    59  	return pointInTime.After(entry.ExpirationTime)
    60  }
    61  
    62  // ipEntries maps a unique IP to the cacheEntry that provides it in .IPs.
    63  // Multiple IPs may point to the same cacheEntry, or they may all be different.
    64  // Crucially, an IP may be present in a cacheEntry but the IP in ipEntries
    65  // points to another cacheEntry. This is because the second cacheEntry has a
    66  // later expiration for this specific IP, and may not include the other IPs
    67  // provided by the first entry.
    68  // The DNS name in the entries is not checked, but is assumed to be the same
    69  // for all entries.
    70  // Note: They are guarded by the DNSCache mutex.
    71  type ipEntries map[string]*cacheEntry
    72  
    73  // nameEntries maps a DNS name to the cache entry that inserted it into the
    74  // cache. It used in reverse DNS lookups. It is similar to ipEntries, above,
    75  // but the key is a DNS name.
    76  type nameEntries map[string]*cacheEntry
    77  
    78  // getIPs returns a sorted list of non-expired unique IPs.
    79  // This needs a read-lock
    80  func (s ipEntries) getIPs(now time.Time) []net.IP {
    81  	ips := make([]net.IP, 0, len(s)) // worst case size
    82  	for ip, entry := range s {
    83  		if entry != nil && !entry.isExpiredBy(now) {
    84  			ips = append(ips, net.ParseIP(ip))
    85  		}
    86  	}
    87  
    88  	return ip.KeepUniqueIPs(ips) // sorts IPs
    89  }
    90  
    91  // DNSCache manages DNS data that will expire after a certain TTL. Information
    92  // is tracked per-IP address, retaining the latest-expiring DNS data for each
    93  // address.
    94  // For most real-world DNS data, the entry per name remains small because newer
    95  // lookups replace older ones. Large TTLs may cause entries to grow if many
    96  // unique IPs are returned in separate lookups.
    97  // Redundant or expired entries are removed on insert.
    98  // Lookups check for expired entries.
    99  type DNSCache struct {
   100  	lock.RWMutex
   101  
   102  	// forward DNS lookups name -> IPEntries
   103  	// IPEntries maps IP -> entry that provides it. An entry may provide multiple IPs.
   104  	forward map[string]ipEntries
   105  
   106  	// IP->dnsNames lookup
   107  	// This map is subordinate to forward, above. An IP inserted into forward, or
   108  	// expired in forward, should also be added/removed in reverse.
   109  	reverse map[string]nameEntries
   110  
   111  	// LastCleanup is the last time that the cleanup happens.
   112  	lastCleanup time.Time
   113  
   114  	// cleanup maps the TTL expiration times (in seconds since the epoch) to
   115  	// DNS names that expire in that second. On every new insertion where the
   116  	// new data is actually inserted into the cache (i.e. it expires later than
   117  	// an existing entry) cleanup will be updated. CleanupExpiredEntries cleans
   118  	// up these entries on demand.
   119  	// Note: Lookup functions will not return expired entries, and this is used
   120  	// to proactively enforce expirations.
   121  	// Note: It is important to periodically call CleanupExpiredEntries
   122  	// otherwise this map will grow forever.
   123  	cleanup map[int64][]string
   124  
   125  	// overLimit is a set of DNS names that were over the per-host configured
   126  	// limit when they received an update. The excess IPs will be removed when
   127  	// cleanupOverLimitEntries is called, but will continue to be returned by
   128  	// Lookup until then.
   129  	// Note: It is important to periodically call GC otherwise this map will
   130  	// grow forever (it is very bounded, however).
   131  	overLimit map[string]bool
   132  
   133  	// perHostLimit is the number of maximum number of IP per host.
   134  	perHostLimit int
   135  
   136  	// minTTL is the minimun TTL value that a cache entry can have, if the TTL
   137  	// sent in the Update is lower, the TTL will be owerwritten to this value.
   138  	// Due is only read-only is not protected by the mutex.
   139  	minTTL int
   140  }
   141  
   142  // NewDNSCache returns an initialized DNSCache
   143  func NewDNSCache(minTTL int) *DNSCache {
   144  	c := &DNSCache{
   145  		forward:      make(map[string]ipEntries),
   146  		reverse:      make(map[string]nameEntries),
   147  		lastCleanup:  time.Now(),
   148  		cleanup:      map[int64][]string{},
   149  		overLimit:    map[string]bool{},
   150  		perHostLimit: 0,
   151  		minTTL:       minTTL,
   152  	}
   153  	return c
   154  }
   155  
   156  // NewDNSCache returns an initialized DNSCache and set the max host limit to
   157  // the given argument
   158  func NewDNSCacheWithLimit(minTTL int, limit int) *DNSCache {
   159  	c := NewDNSCache(minTTL)
   160  	c.perHostLimit = limit
   161  	return c
   162  }
   163  
   164  // Update inserts a new entry into the cache.
   165  // After insertion cache entries for name are expired and redundant entries
   166  // evicted. This is O(number of new IPs) for eviction, and O(number of IPs for
   167  // name) for expiration.
   168  // lookupTime is the time the DNS information began being valid. It should be
   169  // in the past.
   170  // name is used as is and may be an unqualified name (e.g. myservice.namespace).
   171  // ips may be an IPv4 or IPv6 IP. Duplicates will be removed.
   172  // ttl is the DNS TTL for ips and is a seconds value.
   173  func (c *DNSCache) Update(lookupTime time.Time, name string, ips []net.IP, ttl int) bool {
   174  	if c.minTTL > ttl {
   175  		ttl = c.minTTL
   176  	}
   177  
   178  	entry := &cacheEntry{
   179  		Name:           name,
   180  		LookupTime:     lookupTime,
   181  		ExpirationTime: lookupTime.Add(time.Duration(ttl) * time.Second),
   182  		TTL:            ttl,
   183  		IPs:            ips,
   184  	}
   185  
   186  	c.Lock()
   187  	defer c.Unlock()
   188  	return c.updateWithEntry(entry)
   189  }
   190  
   191  // updateWithEntry implements the insertion of a cacheEntry. It is used by
   192  // DNSCache.Update and DNSCache.UpdateWithEntry.
   193  // This needs a write lock
   194  func (c *DNSCache) updateWithEntry(entry *cacheEntry) bool {
   195  	changed := false
   196  	entries, exists := c.forward[entry.Name]
   197  	if !exists {
   198  		changed = true
   199  		entries = make(map[string]*cacheEntry)
   200  		c.forward[entry.Name] = entries
   201  	}
   202  
   203  	if c.updateWithEntryIPs(entries, entry) {
   204  		changed = true
   205  	}
   206  
   207  	// When lookupTime is much earlier than time.Now(), we may not expire all
   208  	// entries that should be expired, leaving more work for .Lookup.
   209  	if c.removeExpired(entries, time.Now(), time.Time{}) {
   210  		changed = true
   211  	}
   212  
   213  	if c.perHostLimit > 0 && len(entries) > c.perHostLimit {
   214  		c.overLimit[entry.Name] = true
   215  	}
   216  	return changed
   217  }
   218  
   219  // AddNameToCleanup adds the IP with the given TTL to the the cleanup map to
   220  // delete the entry from the policy when it expires.
   221  // Need to be called with a write lock
   222  func (c *DNSCache) addNameToCleanup(entry *cacheEntry) {
   223  	if entry.ExpirationTime.Before(c.lastCleanup) {
   224  		// ExpirationTime can be before the lastCleanup don't add that value to
   225  		// prevent leaks on the map.
   226  		return
   227  	}
   228  	expiration := entry.ExpirationTime.Unix()
   229  	expiredEntries, exists := c.cleanup[expiration]
   230  	if !exists {
   231  		expiredEntries = []string{}
   232  	}
   233  	c.cleanup[expiration] = append(expiredEntries, entry.Name)
   234  }
   235  
   236  // cleanupExpiredEntries cleans all the expired entries from the lastTime that
   237  // runs to the give time. It will lock the struct until retrieves all the data.
   238  // It returns the list of names that need to be deleted from the policies.
   239  func (c *DNSCache) cleanupExpiredEntries(expires time.Time) ([]string, time.Time) {
   240  	timediff := int(expires.Sub(c.lastCleanup).Seconds())
   241  	startTime := c.lastCleanup
   242  	expiredEntries := []string{}
   243  	for i := 0; i < int(timediff); i++ {
   244  		expiredTime := c.lastCleanup.Add(time.Second)
   245  		key := expiredTime.Unix()
   246  		c.lastCleanup = expiredTime
   247  		entries, exists := c.cleanup[key]
   248  		if !exists {
   249  			continue
   250  		}
   251  		expiredEntries = append(expiredEntries, entries...)
   252  		delete(c.cleanup, key)
   253  	}
   254  
   255  	result := []string{}
   256  	for _, name := range KeepUniqueNames(expiredEntries) {
   257  		if entries, exists := c.forward[name]; exists {
   258  			if !c.removeExpired(entries, c.lastCleanup, time.Time{}) {
   259  				continue
   260  			}
   261  			result = append(result, name)
   262  		}
   263  	}
   264  	return result, startTime
   265  }
   266  
   267  // cleanupOverLimitEntries returns the names that has reached the max number of
   268  // IP per host. Internally the function sort the entries by the expiration
   269  // time.
   270  func (c *DNSCache) cleanupOverLimitEntries() []string {
   271  	type IPEntry struct {
   272  		ip    string
   273  		entry *cacheEntry
   274  	}
   275  	affectedNames := []string{}
   276  
   277  	// For global cache the limit maybe is not used at all.
   278  	if c.perHostLimit == 0 {
   279  		return affectedNames
   280  	}
   281  
   282  	for dnsName := range c.overLimit {
   283  		entries, ok := c.forward[dnsName]
   284  		if !ok {
   285  			continue
   286  		}
   287  		overlimit := len(entries) - c.perHostLimit
   288  		if overlimit <= 0 {
   289  			continue
   290  		}
   291  		sortedEntries := []IPEntry{}
   292  		for ip, entry := range entries {
   293  			sortedEntries = append(sortedEntries, IPEntry{ip, entry})
   294  		}
   295  
   296  		sort.Slice(sortedEntries, func(i, j int) bool {
   297  			return sortedEntries[i].entry.ExpirationTime.Before(sortedEntries[j].entry.ExpirationTime)
   298  		})
   299  
   300  		for i := 0; i < overlimit; i++ {
   301  			key := sortedEntries[i]
   302  			delete(entries, key.ip)
   303  			c.removeReverse(key.ip, key.entry)
   304  		}
   305  		affectedNames = append(affectedNames, dnsName)
   306  	}
   307  	c.overLimit = map[string]bool{}
   308  	return affectedNames
   309  }
   310  
   311  // GC garbage collector function that clean expired entries and return the
   312  // entries that are deleted.
   313  func (c *DNSCache) GC() []string {
   314  	c.Lock()
   315  	expiredEntries, _ := c.cleanupExpiredEntries(time.Now())
   316  	overLimitEntries := c.cleanupOverLimitEntries()
   317  	c.Unlock()
   318  	return KeepUniqueNames(append(expiredEntries, overLimitEntries...))
   319  }
   320  
   321  // UpdateFromCache is a utility function that allows updating a DNSCache
   322  // instance with all the internal entries of another. Latest-Expiration still
   323  // applies, thus the merged outcome is consistent with adding the entries
   324  // individually.
   325  // When namesToUpdate has non-zero length only those names are updated from
   326  // update, otherwise all DNS names in update are used.
   327  func (c *DNSCache) UpdateFromCache(update *DNSCache, namesToUpdate []string) {
   328  	if update == nil {
   329  		return
   330  	}
   331  
   332  	c.Lock()
   333  	defer c.Unlock()
   334  	c.updateFromCache(update, namesToUpdate)
   335  }
   336  
   337  func (c *DNSCache) updateFromCache(update *DNSCache, namesToUpdate []string) {
   338  	update.RLock()
   339  	defer update.RUnlock()
   340  
   341  	if len(namesToUpdate) == 0 {
   342  		for name := range update.forward {
   343  			namesToUpdate = append(namesToUpdate, name)
   344  		}
   345  	}
   346  	for _, name := range namesToUpdate {
   347  		newEntries, exists := update.forward[name]
   348  		if !exists {
   349  			continue
   350  		}
   351  		for _, newEntry := range newEntries {
   352  			c.updateWithEntry(newEntry)
   353  		}
   354  	}
   355  }
   356  
   357  // ReplaceFromCacheByNames operates as an atomic combination of ForceExpire and
   358  // multiple UpdateFromCache invocations. The result is to collect all entries
   359  // for DNS names in namesToUpdate from each DNSCache in updates, replacing the
   360  // current entries for each of those names.
   361  func (c *DNSCache) ReplaceFromCacheByNames(namesToUpdate []string, updates ...*DNSCache) {
   362  	c.Lock()
   363  	defer c.Unlock()
   364  
   365  	// Remove any DNS name in namesToUpdate with a lookup before "now". This
   366  	// effectively deletes all lookups because we're holding the lock.
   367  	c.forceExpireByNames(time.Now(), namesToUpdate)
   368  
   369  	for _, update := range updates {
   370  		c.updateFromCache(update, namesToUpdate)
   371  	}
   372  }
   373  
   374  // Lookup returns a set of unique IPs that are currently unexpired for name, if
   375  // any exist. An empty list indicates no valid records exist. The IPs are
   376  // returned sorted.
   377  func (c *DNSCache) Lookup(name string) (ips []net.IP) {
   378  	c.RLock()
   379  	defer c.RUnlock()
   380  
   381  	return c.lookupByTime(time.Now(), name)
   382  }
   383  
   384  // lookupByTime takes a timestamp for expiration comparisons, and is only
   385  // intended for testing.
   386  func (c *DNSCache) lookupByTime(now time.Time, name string) (ips []net.IP) {
   387  	entries, found := c.forward[name]
   388  	if !found {
   389  		return nil
   390  	}
   391  
   392  	return entries.getIPs(now)
   393  }
   394  
   395  // LookupByRegexp returns all non-expired cache entries that match re as a map
   396  // of name -> IPs
   397  func (c *DNSCache) LookupByRegexp(re *regexp.Regexp) (matches map[string][]net.IP) {
   398  	return c.lookupByRegexpByTime(time.Now(), re)
   399  }
   400  
   401  // lookupByRegexpByTime takes a timestamp for expiration comparisons, and is
   402  // only intended for testing.
   403  func (c *DNSCache) lookupByRegexpByTime(now time.Time, re *regexp.Regexp) (matches map[string][]net.IP) {
   404  	matches = make(map[string][]net.IP)
   405  
   406  	c.RLock()
   407  	defer c.RUnlock()
   408  
   409  	for name, entry := range c.forward {
   410  		if re.MatchString(name) {
   411  			if ips := entry.getIPs(now); len(ips) > 0 {
   412  				matches[name] = append(matches[name], ips...)
   413  			}
   414  		}
   415  	}
   416  
   417  	return matches
   418  }
   419  
   420  // LookupIP returns all DNS names in entries that include that IP. The cache
   421  // maintains the latest-expiring entry per-name per-IP. This means that multiple
   422  // names referrring to the same IP will expire from the cache at different times,
   423  // and only 1 entry for each name-IP pair is internally retained.
   424  func (c *DNSCache) LookupIP(ip net.IP) (names []string) {
   425  	c.RLock()
   426  	defer c.RUnlock()
   427  
   428  	return c.lookupIPByTime(time.Now(), ip)
   429  }
   430  
   431  // lookupIPByTime takes a timestamp for expiration comparisons, and is
   432  // only intended for testing.
   433  func (c *DNSCache) lookupIPByTime(now time.Time, ip net.IP) (names []string) {
   434  	ipKey := ip.String()
   435  	cacheEntries, found := c.reverse[ipKey]
   436  	if !found {
   437  		return nil
   438  	}
   439  
   440  	for name, entry := range cacheEntries {
   441  		if entry != nil && !entry.isExpiredBy(now) {
   442  			names = append(names, name)
   443  		}
   444  	}
   445  
   446  	sort.Strings(names)
   447  	return names
   448  }
   449  
   450  // updateWithEntryIPs adds a mapping for every IP found in `entry` to `ipEntries`
   451  // (which maps IP -> cacheEntry). It will replace existing IP->old mappings in
   452  // `entries` if the current entry expires sooner (or has already expired).
   453  // This needs a write lock
   454  func (c *DNSCache) updateWithEntryIPs(entries ipEntries, entry *cacheEntry) bool {
   455  	added := false
   456  	for _, ip := range entry.IPs {
   457  		ipStr := ip.String()
   458  		old, exists := entries[ipStr]
   459  		if old == nil || !exists || old.isExpiredBy(entry.ExpirationTime) {
   460  			entries[ipStr] = entry
   461  			c.upsertReverse(ipStr, entry)
   462  			c.addNameToCleanup(entry)
   463  			added = true
   464  		}
   465  	}
   466  	return added
   467  
   468  }
   469  
   470  // removeExpired removes expired (or nil) cacheEntry pointers from entries, an
   471  // ipEntries instance for a specific name. It returns a boolean if any entry is
   472  // removed.
   473  // now is the "current time" and entries with ExpirationTime before then are
   474  // removed.
   475  // expireLookupsBefore is an optional parameter. It causes any entry with a
   476  // LookupTime before it to be expired. It is intended for use with cache
   477  // clearing functions like ForceExpire, and does not maintain the cache's
   478  // guarantees.
   479  // This needs a write lock
   480  func (c *DNSCache) removeExpired(entries ipEntries, now time.Time, expireLookupsBefore time.Time) (removed bool) {
   481  	for ip, entry := range entries {
   482  		if entry == nil || entry.isExpiredBy(now) || entry.LookupTime.Before(expireLookupsBefore) {
   483  			delete(entries, ip)
   484  			c.removeReverse(ip, entry)
   485  			removed = true
   486  		}
   487  	}
   488  
   489  	return removed
   490  }
   491  
   492  // upsertReverse updates the reverse DNS cache for ip with entry, if it expires
   493  // later than the already-stored entry.
   494  // It is assumed that entry includes ip.
   495  // This needs a write lock
   496  func (c *DNSCache) upsertReverse(ip string, entry *cacheEntry) {
   497  	entries, exists := c.reverse[ip]
   498  	if entries == nil || !exists {
   499  		entries = make(map[string]*cacheEntry)
   500  		c.reverse[ip] = entries
   501  	}
   502  	entries[entry.Name] = entry
   503  }
   504  
   505  // removeReverse removes the reference between ip and the name stored in entry.
   506  // When no more references from ip to any name exist, the map entry is deleted
   507  // outright.
   508  // It is assumed that entry includes ip.
   509  // This needs a write lock
   510  func (c *DNSCache) removeReverse(ip string, entry *cacheEntry) {
   511  	entries, exists := c.reverse[ip]
   512  	if entries == nil || !exists {
   513  		return
   514  	}
   515  	delete(entries, entry.Name)
   516  	if len(entries) == 0 {
   517  		delete(c.reverse, ip)
   518  	}
   519  }
   520  
   521  // ForceExpire is used to clear entries from the cache before their TTL is
   522  // over. This operation does not keep previous guarantees that, for each IP,
   523  // the most recent lookup to provide that IP is used.
   524  // Note that all parameters must match, if provided. `time.Time{}` is
   525  // considered not-provided for time parameters.
   526  // expireLookupsBefore requires a lookup to have a LookupTime before it in
   527  // order to remove it.
   528  // nameMatch will remove any DNS names that match.
   529  func (c *DNSCache) ForceExpire(expireLookupsBefore time.Time, nameMatch *regexp.Regexp) (namesAffected []string) {
   530  	c.Lock()
   531  	defer c.Unlock()
   532  
   533  	for name, entries := range c.forward {
   534  		// If nameMatch was passed in, we must match it. Otherwise, "match all".
   535  		if nameMatch != nil && !nameMatch.MatchString(name) {
   536  			continue
   537  		}
   538  		// We pass expireLookupsBefore as the `now` parameter but it is redundant
   539  		// because LookupTime must be before ExpirationTime.
   540  		// The second expireLookupsBefore actually matches lookup times, and will
   541  		// delete the entries completely.
   542  		nameNeedsRegen := c.removeExpired(entries, expireLookupsBefore, expireLookupsBefore)
   543  		if nameNeedsRegen {
   544  			namesAffected = append(namesAffected, name)
   545  		}
   546  	}
   547  
   548  	return namesAffected
   549  }
   550  
   551  // ForceExpireByNames is the same function as ForceExpire but uses the exact
   552  // names to delete the entries.
   553  func (c *DNSCache) ForceExpireByNames(expireLookupsBefore time.Time, names []string) (namesAffected []string) {
   554  	c.Lock()
   555  	defer c.Unlock()
   556  
   557  	return c.forceExpireByNames(expireLookupsBefore, names)
   558  }
   559  
   560  func (c *DNSCache) forceExpireByNames(expireLookupsBefore time.Time, names []string) (namesAffected []string) {
   561  	for _, name := range names {
   562  		entries, exists := c.forward[name]
   563  		if !exists {
   564  			continue
   565  		}
   566  
   567  		// We pass expireLookupsBefore as the `now` parameter but it is redundant
   568  		// because LookupTime must be before ExpirationTime.
   569  		// The second expireLookupsBefore actually matches lookup times, and will
   570  		// delete the entries completely.
   571  		nameNeedsRegen := c.removeExpired(entries, expireLookupsBefore, expireLookupsBefore)
   572  		if nameNeedsRegen {
   573  			namesAffected = append(namesAffected, name)
   574  		}
   575  	}
   576  
   577  	return namesAffected
   578  }
   579  
   580  // Dump returns unexpired cache entries in the cache. They are deduplicated,
   581  // but not usefully sorted. These objects should not be modified.
   582  func (c *DNSCache) Dump() (lookups []*cacheEntry) {
   583  	c.RLock()
   584  	defer c.RUnlock()
   585  
   586  	now := time.Now()
   587  
   588  	// Collect all the still-valid entries
   589  	lookups = make([]*cacheEntry, 0, len(c.forward))
   590  	for _, entries := range c.forward {
   591  		for _, entry := range entries {
   592  			if !entry.isExpiredBy(now) {
   593  				lookups = append(lookups, entry)
   594  			}
   595  		}
   596  	}
   597  
   598  	// Dedup the entries. They are created once and are immutable so the address
   599  	// is a unique identifier.
   600  	// We iterate through the list, keeping unique pointers. This is correct
   601  	// because the list is sorted and, if two consecutive entries are the same,
   602  	// it is safe to overwrite the second duplicate.
   603  	sort.Slice(lookups, func(i, j int) bool {
   604  		return uintptr(unsafe.Pointer(lookups[i])) < uintptr(unsafe.Pointer(lookups[j]))
   605  	})
   606  
   607  	deduped := lookups[:0] // len==0 but cap==cap(lookups)
   608  	for readIdx, lookup := range lookups {
   609  		if readIdx == 0 || deduped[len(deduped)-1] != lookups[readIdx] {
   610  			deduped = append(deduped, lookup)
   611  		}
   612  	}
   613  
   614  	return deduped
   615  }
   616  
   617  // MarshalJSON serialises the set of DNS lookup cacheEntries needed to
   618  // reconstruct this cache instance.
   619  // Note: Expiration times are honored and the reconstructed cache instance is
   620  // expected to return the same values as the original at that point in time.
   621  func (c *DNSCache) MarshalJSON() ([]byte, error) {
   622  	lookups := c.Dump()
   623  
   624  	// serialise into a JSON object array
   625  	return json.Marshal(lookups)
   626  }
   627  
   628  // UnmarshalJSON rebuilds a DNSCache from serialized JSON.
   629  // Note: This is destructive to any currect data. Use UpdateFromCache for bulk
   630  // updates.
   631  func (c *DNSCache) UnmarshalJSON(raw []byte) error {
   632  	lookups := make([]*cacheEntry, 0)
   633  	if err := json.Unmarshal(raw, &lookups); err != nil {
   634  		return err
   635  	}
   636  
   637  	c.Lock()
   638  	defer c.Unlock()
   639  
   640  	c.forward = make(map[string]ipEntries)
   641  	c.reverse = make(map[string]nameEntries)
   642  
   643  	for _, newLookup := range lookups {
   644  		c.updateWithEntry(newLookup)
   645  	}
   646  
   647  	return nil
   648  }