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 }