github.com/cilium/cilium@v1.16.2/pkg/fqdn/name_manager.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package fqdn
     5  
     6  import (
     7  	"context"
     8  	"hash/fnv"
     9  	"net/netip"
    10  	"os"
    11  	"path/filepath"
    12  	"regexp"
    13  	"slices"
    14  
    15  	"github.com/go-openapi/strfmt"
    16  	"github.com/sirupsen/logrus"
    17  	"golang.org/x/sync/errgroup"
    18  	"k8s.io/apimachinery/pkg/util/sets"
    19  
    20  	"github.com/cilium/cilium/api/v1/models"
    21  	"github.com/cilium/cilium/pkg/controller"
    22  	"github.com/cilium/cilium/pkg/ip"
    23  	"github.com/cilium/cilium/pkg/ipcache"
    24  	ipcacheTypes "github.com/cilium/cilium/pkg/ipcache/types"
    25  	"github.com/cilium/cilium/pkg/labels"
    26  	"github.com/cilium/cilium/pkg/lock"
    27  	"github.com/cilium/cilium/pkg/logging/logfields"
    28  	"github.com/cilium/cilium/pkg/metrics"
    29  	"github.com/cilium/cilium/pkg/option"
    30  	"github.com/cilium/cilium/pkg/policy/api"
    31  	"github.com/cilium/cilium/pkg/source"
    32  	"github.com/cilium/cilium/pkg/time"
    33  )
    34  
    35  // NameManager maintains state DNS names, via FQDNSelector or exact match for
    36  // polling, need to be tracked. It is the main structure which relates the FQDN
    37  // subsystem to the policy subsystem for plumbing the relation between a DNS
    38  // name and the corresponding IPs which have been returned via DNS lookups.
    39  // When DNS updates are given to a NameManager it update cached selectors as
    40  // required via UpdateSelectors.
    41  // DNS information is cached, respecting TTL.
    42  type NameManager struct {
    43  	lock.RWMutex
    44  
    45  	// config is a copy from when this instance was initialized.
    46  	// It is read-only once set
    47  	config Config
    48  
    49  	// allSelectors contains all FQDNSelectors which are present in all policy. We
    50  	// use these selectors to map selectors --> IPs.
    51  	allSelectors map[api.FQDNSelector]*regexp.Regexp
    52  
    53  	// cache is a private copy of the pointer from config.
    54  	cache *DNSCache
    55  
    56  	bootstrapCompleted bool
    57  
    58  	// restoredPrefixes contains all prefixes for which we have restored the
    59  	// IPCache metadata from previous Cilium v1.15 installation.
    60  	// Cleared by CompleteBoostrap
    61  	restoredPrefixes sets.Set[netip.Prefix]
    62  
    63  	manager *controller.Manager
    64  
    65  	// list of locks used as coordination points for name updates
    66  	// see LockName() for details.
    67  	nameLocks []*lock.Mutex
    68  }
    69  
    70  // GetModel returns the API model of the NameManager.
    71  func (n *NameManager) GetModel() *models.NameManager {
    72  	n.RWMutex.RLock()
    73  	defer n.RWMutex.RUnlock()
    74  
    75  	allSelectors := make([]*models.SelectorEntry, 0, len(n.allSelectors))
    76  	for fqdnSel, regex := range n.allSelectors {
    77  		pair := &models.SelectorEntry{
    78  			SelectorString: fqdnSel.String(),
    79  			RegexString:    regex.String(),
    80  		}
    81  		allSelectors = append(allSelectors, pair)
    82  	}
    83  
    84  	return &models.NameManager{
    85  		FQDNPolicySelectors: allSelectors,
    86  	}
    87  }
    88  
    89  var (
    90  	DNSSourceLookup     = "lookup"
    91  	DNSSourceConnection = "connection"
    92  )
    93  
    94  type NoEndpointIDMatch struct {
    95  	ID string
    96  }
    97  
    98  func (e NoEndpointIDMatch) Error() string {
    99  	return "unable to find target endpoint ID: " + e.ID
   100  }
   101  
   102  // GetDNSHistoryModel returns API models.DNSLookup copies of DNS data in each
   103  // endpoint's DNSHistory. These are filtered by the specified matchers if
   104  // they are non-empty.
   105  //
   106  // Note that this does *NOT* dump the NameManager's own global DNSCache.
   107  //
   108  // endpointID may be "" in order to get DNS history for all endpoints.
   109  func (n *NameManager) GetDNSHistoryModel(endpointID string, prefixMatcher PrefixMatcherFunc, nameMatcher NameMatcherFunc, source string) (lookups []*models.DNSLookup, err error) {
   110  	eps := n.config.GetEndpointsDNSInfo(endpointID)
   111  	if eps == nil {
   112  		return nil, &NoEndpointIDMatch{ID: endpointID}
   113  	}
   114  	for _, ep := range eps {
   115  		lookupSourceEntries := []*models.DNSLookup{}
   116  		connectionSourceEntries := []*models.DNSLookup{}
   117  		for _, lookup := range ep.DNSHistory.Dump() {
   118  			if !nameMatcher(lookup.Name) {
   119  				continue
   120  			}
   121  
   122  			// The API model needs strings
   123  			IPStrings := make([]string, 0, len(lookup.IPs))
   124  
   125  			// only proceed if any IP matches the prefix selector
   126  			anIPMatches := false
   127  			for _, ip := range lookup.IPs {
   128  				anIPMatches = anIPMatches || prefixMatcher(ip)
   129  				IPStrings = append(IPStrings, ip.String())
   130  			}
   131  			if !anIPMatches {
   132  				continue
   133  			}
   134  
   135  			lookupSourceEntries = append(lookupSourceEntries, &models.DNSLookup{
   136  				Fqdn:           lookup.Name,
   137  				Ips:            IPStrings,
   138  				LookupTime:     strfmt.DateTime(lookup.LookupTime),
   139  				TTL:            int64(lookup.TTL),
   140  				ExpirationTime: strfmt.DateTime(lookup.ExpirationTime),
   141  				EndpointID:     int64(ep.ID64),
   142  				Source:         DNSSourceLookup,
   143  			})
   144  		}
   145  
   146  		for _, delete := range ep.DNSZombies.DumpAlive(prefixMatcher) {
   147  			for _, name := range delete.Names {
   148  				if !nameMatcher(name) {
   149  					continue
   150  				}
   151  
   152  				connectionSourceEntries = append(connectionSourceEntries, &models.DNSLookup{
   153  					Fqdn:           name,
   154  					Ips:            []string{delete.IP.String()},
   155  					LookupTime:     strfmt.DateTime(delete.AliveAt),
   156  					TTL:            0,
   157  					ExpirationTime: strfmt.DateTime(ep.DNSZombies.nextCTGCUpdate),
   158  					EndpointID:     int64(ep.ID64),
   159  					Source:         DNSSourceConnection,
   160  				})
   161  			}
   162  		}
   163  
   164  		switch source {
   165  		case DNSSourceLookup:
   166  			lookups = append(lookups, lookupSourceEntries...)
   167  		case DNSSourceConnection:
   168  			lookups = append(lookups, connectionSourceEntries...)
   169  		default:
   170  			lookups = append(lookups, lookupSourceEntries...)
   171  			lookups = append(lookups, connectionSourceEntries...)
   172  		}
   173  	}
   174  
   175  	return lookups, nil
   176  }
   177  
   178  // RegisterFQDNSelector exposes this FQDNSelector so that the identity labels
   179  // of IPs contained in a DNS response that matches said selector can be
   180  // associated with that selector.
   181  // This function also evaluates if any DNS names in the cache are matched by
   182  // this new selector and updates the labels for those DNS names accordingly.
   183  func (n *NameManager) RegisterFQDNSelector(selector api.FQDNSelector) {
   184  	n.Lock()
   185  	defer n.Unlock()
   186  
   187  	_, exists := n.allSelectors[selector]
   188  	if exists {
   189  		log.WithField("fqdnSelector", selector).Warning("FQDNSelector was already registered for updates.")
   190  	} else {
   191  		// This error should never occur since the FQDNSelector has already been
   192  		// validated, but account for it for good measure.
   193  		regex, err := selector.ToRegex()
   194  		if err != nil {
   195  			log.WithError(err).WithField("fqdnSelector", selector).Error("FQDNSelector did not compile to valid regex")
   196  			return
   197  		}
   198  
   199  		n.allSelectors[selector] = regex
   200  		if metrics.FQDNSelectors.IsEnabled() {
   201  			metrics.FQDNSelectors.Set(float64(len(n.allSelectors)))
   202  		}
   203  	}
   204  
   205  	// The newly added FQDN selector could match DNS Names in the cache. If
   206  	// that is the case, we want to update the IPCache metadata for all
   207  	// associated IPs
   208  	selectedNamesAndIPs := n.mapSelectorsToNamesLocked(selector)
   209  	n.updateMetadata(deriveLabelsForNames(selectedNamesAndIPs, n.allSelectors))
   210  }
   211  
   212  // UnregisterFQDNSelector removes this FQDNSelector from the set of
   213  // IPs which are being tracked by the identityNotifier. The result
   214  // of this is that an IP may be evicted from IPCache if it is no longer
   215  // selected by any other FQDN selector.
   216  func (n *NameManager) UnregisterFQDNSelector(selector api.FQDNSelector) {
   217  	n.Lock()
   218  	defer n.Unlock()
   219  
   220  	// Remove selector
   221  	delete(n.allSelectors, selector)
   222  	if metrics.FQDNSelectors.IsEnabled() {
   223  		metrics.FQDNSelectors.Set(float64(len(n.allSelectors)))
   224  	}
   225  
   226  	// Re-compute labels for affected names and IPs
   227  	selectedNamesAndIPs := n.mapSelectorsToNamesLocked(selector)
   228  	n.updateMetadata(deriveLabelsForNames(selectedNamesAndIPs, n.allSelectors))
   229  }
   230  
   231  // NewNameManager creates an initialized NameManager.
   232  // When config.Cache is nil, the global fqdn.DefaultDNSCache is used.
   233  func NewNameManager(config Config) *NameManager {
   234  
   235  	if config.Cache == nil {
   236  		config.Cache = NewDNSCache(0)
   237  	}
   238  
   239  	if config.GetEndpointsDNSInfo == nil {
   240  		config.GetEndpointsDNSInfo = func(_ string) []EndpointDNSInfo {
   241  			return nil
   242  		}
   243  	}
   244  
   245  	n := &NameManager{
   246  		config:       config,
   247  		allSelectors: make(map[api.FQDNSelector]*regexp.Regexp),
   248  		cache:        config.Cache,
   249  		manager:      controller.NewManager(),
   250  		nameLocks:    make([]*lock.Mutex, option.Config.DNSProxyLockCount),
   251  	}
   252  
   253  	for i := range n.nameLocks {
   254  		n.nameLocks[i] = &lock.Mutex{}
   255  	}
   256  
   257  	return n
   258  }
   259  
   260  // UpdateGenerateDNS inserts the new DNS information into the cache. If the IPs
   261  // have changed for a name they will be reflected in updatedDNSIPs.
   262  func (n *NameManager) UpdateGenerateDNS(ctx context.Context, lookupTime time.Time, updatedDNSIPs map[string]*DNSIPRecords) *errgroup.Group {
   263  	n.RWMutex.Lock()
   264  	defer n.RWMutex.Unlock()
   265  
   266  	// Update IPs in n
   267  	updatedDNSNames, ipcacheRevision := n.updateDNSIPs(lookupTime, updatedDNSIPs)
   268  	for dnsName, IPs := range updatedDNSNames {
   269  		log.WithFields(logrus.Fields{
   270  			"matchName": dnsName,
   271  			"IPs":       IPs,
   272  		}).Debug("Updated FQDN with new IPs")
   273  	}
   274  
   275  	g, ctx := errgroup.WithContext(ctx)
   276  	g.Go(func() error {
   277  		return n.config.IPCache.WaitForRevision(ctx, ipcacheRevision)
   278  	})
   279  	return g
   280  }
   281  
   282  func (n *NameManager) CompleteBootstrap() {
   283  	n.Lock()
   284  	defer n.Unlock()
   285  
   286  	n.bootstrapCompleted = true
   287  	if len(n.restoredPrefixes) > 0 {
   288  		log.WithField("prefixes", len(n.restoredPrefixes)).Debug("Removing restored IPCache labels")
   289  
   290  		// The following logic needs to match the restoration logic in RestoreCaches
   291  		ipcacheUpdates := make([]ipcache.MU, 0, len(n.restoredPrefixes))
   292  		for prefix := range n.restoredPrefixes {
   293  			ipcacheUpdates = append(ipcacheUpdates, ipcache.MU{
   294  				Prefix:   prefix,
   295  				Source:   source.Restored,
   296  				Resource: restorationIPCacheResource,
   297  				Metadata: []ipcache.IPMetadata{
   298  					labels.Labels{}, // remove restored labels
   299  				},
   300  			})
   301  		}
   302  		n.config.IPCache.RemoveMetadataBatch(ipcacheUpdates...)
   303  		n.restoredPrefixes = nil
   304  
   305  		checkpointPath := filepath.Join(option.Config.StateDir, checkpointFile)
   306  		if err := os.Remove(checkpointPath); err != nil {
   307  			log.WithError(err).WithField(logfields.Path, checkpointPath).
   308  				Debug("Failed to remove checkpoint file")
   309  		}
   310  	}
   311  }
   312  
   313  // updateDNSIPs updates the IPs for each DNS name in updatedDNSIPs.
   314  // It returns:
   315  // updatedNames: a map of DNS names to all the valid IPs we store for each.
   316  // ipcacheRevision: a revision number to pass to WaitForRevision()
   317  func (n *NameManager) updateDNSIPs(lookupTime time.Time, updatedDNSIPs map[string]*DNSIPRecords) (updatedNames map[string][]netip.Addr, ipcacheRevision uint64) {
   318  	updatedNames = make(map[string][]netip.Addr, len(updatedDNSIPs))
   319  	updatedMetadata := make(map[string]nameMetadata, len(updatedDNSIPs))
   320  
   321  	for dnsName, lookupIPs := range updatedDNSIPs {
   322  		addrs := ip.MustAddrsFromIPs(lookupIPs.IPs)
   323  		updated := n.updateIPsForName(lookupTime, dnsName, addrs, lookupIPs.TTL)
   324  
   325  		// The IPs didn't change. No more to be done for this dnsName
   326  		if !updated && n.bootstrapCompleted {
   327  			log.WithFields(logrus.Fields{
   328  				"dnsName":   dnsName,
   329  				"lookupIPs": lookupIPs,
   330  			}).Debug("FQDN: IPs didn't change for DNS name")
   331  			continue
   332  		}
   333  
   334  		// record the IPs that were different
   335  		updatedNames[dnsName] = addrs
   336  
   337  		// accumulate the new labels affected by new IPs
   338  		if len(n.allSelectors) == 0 {
   339  			log.WithFields(logrus.Fields{
   340  				"dnsName":   dnsName,
   341  				"lookupIPs": lookupIPs,
   342  			}).Debug("FQDN: No selectors registered for updates")
   343  			continue
   344  		}
   345  
   346  		// derive labels for this DNS name
   347  		nameLabels := deriveLabelsForName(dnsName, n.allSelectors)
   348  		if len(nameLabels) == 0 {
   349  			// If no selectors care about this name, then skip IPCache updates
   350  			// for this name.
   351  			// If any selectors/ are added later, ipcache insertion will happen then.
   352  			continue
   353  		}
   354  
   355  		updatedMetadata[dnsName] = nameMetadata{
   356  			addrs:  addrs,
   357  			labels: nameLabels,
   358  		}
   359  	}
   360  
   361  	// If new IPs were detected, and these IPs are selected by selectors,
   362  	// then ensure they have an identity allocated to them via the ipcache.
   363  	ipcacheRevision = n.updateMetadata(updatedMetadata)
   364  	return updatedNames, ipcacheRevision
   365  }
   366  
   367  // updateIPsName will update the IPs for dnsName. It always retains a copy of
   368  // newIPs.
   369  // updated is true when the new IPs differ from the old IPs
   370  func (n *NameManager) updateIPsForName(lookupTime time.Time, dnsName string, newIPs []netip.Addr, ttl int) (updated bool) {
   371  	oldCacheIPs := n.cache.Lookup(dnsName)
   372  
   373  	if n.config.MinTTL > ttl {
   374  		ttl = n.config.MinTTL
   375  	}
   376  
   377  	changed := n.cache.Update(lookupTime, dnsName, newIPs, ttl)
   378  	if !changed { // Changed may have false positives, but not false negatives
   379  		return false
   380  	}
   381  
   382  	newCacheIPs := n.cache.Lookup(dnsName) // DNSCache returns IPs unsorted
   383  
   384  	// The 0 checks below account for an unlike race condition where this
   385  	// function is called with already expired data and if other cache data
   386  	// from before also expired.
   387  	if len(oldCacheIPs) != len(newCacheIPs) || len(oldCacheIPs) == 0 {
   388  		return true
   389  	}
   390  
   391  	ip.SortAddrList(oldCacheIPs) // sorts in place
   392  	ip.SortAddrList(newCacheIPs)
   393  
   394  	return !slices.Equal(oldCacheIPs, newCacheIPs)
   395  }
   396  
   397  func ipcacheResource(dnsName string) ipcacheTypes.ResourceID {
   398  	return ipcacheTypes.NewResourceID(ipcacheTypes.ResourceKindDaemon, "fqdn-name-manager", dnsName)
   399  }
   400  
   401  // updateMetadata updates (i.e. upserts or removes) the metadata in IPCache for
   402  // each (name, IP) pair provided in nameToMetadata.
   403  func (n *NameManager) updateMetadata(nameToMetadata map[string]nameMetadata) (ipcacheRevision uint64) {
   404  	var ipcacheUpserts, ipcacheRemovals []ipcache.MU
   405  
   406  	for dnsName, metadata := range nameToMetadata {
   407  		var updates []ipcache.MU
   408  		resource := ipcacheResource(dnsName)
   409  
   410  		if option.Config.Debug {
   411  			log.WithFields(logrus.Fields{
   412  				"name":     dnsName,
   413  				"prefixes": metadata.addrs,
   414  				"labels":   metadata.labels,
   415  			}).Debug("Updating prefix labels in IPCache")
   416  		}
   417  
   418  		for _, addr := range metadata.addrs {
   419  			updates = append(updates, ipcache.MU{
   420  				Prefix:   netip.PrefixFrom(addr, addr.BitLen()),
   421  				Source:   source.Generated,
   422  				Resource: resource,
   423  				Metadata: []ipcache.IPMetadata{
   424  					metadata.labels,
   425  				},
   426  			})
   427  		}
   428  
   429  		// If labels are empty (i.e. this domain is no longer selected),
   430  		// then we want to the labels of our resource owner
   431  		if len(metadata.labels) > 0 {
   432  			ipcacheUpserts = append(ipcacheUpserts, updates...)
   433  		} else {
   434  			ipcacheRemovals = append(ipcacheRemovals, updates...)
   435  		}
   436  	}
   437  
   438  	if len(ipcacheUpserts) > 0 {
   439  		ipcacheRevision = n.config.IPCache.UpsertMetadataBatch(ipcacheUpserts...)
   440  	}
   441  	if len(ipcacheRemovals) > 0 {
   442  		ipcacheRevision = n.config.IPCache.RemoveMetadataBatch(ipcacheRemovals...)
   443  	}
   444  
   445  	return ipcacheRevision
   446  }
   447  
   448  // maybeRemoveMetadata removes the ipcache metadata from every (name, IP) pair
   449  // in maybeRemoved, as long as that (name, IP) is not still in the dns cache.
   450  func (n *NameManager) maybeRemoveMetadata(maybeRemoved map[netip.Addr][]string) {
   451  	// Need to take an RLock here so that no DNS updates are processed.
   452  	// Otherwise, we might accidentally remove an IP that is newly inserted.
   453  	n.RWMutex.RLock()
   454  	defer n.RWMutex.RUnlock()
   455  
   456  	n.cache.RLock()
   457  	ipCacheUpdates := make([]ipcache.MU, 0, len(maybeRemoved))
   458  	for ip, names := range maybeRemoved {
   459  		for _, name := range names {
   460  			if !n.cache.entryExistsLocked(name, ip) {
   461  				ipCacheUpdates = append(ipCacheUpdates, ipcache.MU{
   462  					Prefix:   netip.PrefixFrom(ip, ip.BitLen()),
   463  					Source:   source.Generated,
   464  					Resource: ipcacheResource(name),
   465  					Metadata: []ipcache.IPMetadata{
   466  						labels.Labels{}, // remove all labels for this (ip, name) pair
   467  					},
   468  				})
   469  			}
   470  		}
   471  	}
   472  	n.cache.RUnlock()
   473  	n.config.IPCache.RemoveMetadataBatch(ipCacheUpdates...)
   474  }
   475  
   476  // LockName is used to serialize  parallel end-to-end updates to the same name.
   477  //
   478  // It is needed due to some subtleties around NameManager locks and
   479  // policy updates. Specifically, we unlock the NameManager after updates
   480  // are queued to endpoints, but *before* changes are pushed to policy maps.
   481  // So, if a second request comes in during this state, it may encounter
   482  // policy drops until the policy updates are complete.
   483  //
   484  // Serializing on names prevents this.
   485  //
   486  // Rather than having a potentially unbounded set of per-name locks, this
   487  // buckets names in to a set of locks. The lock count is configurable.
   488  func (n *NameManager) LockName(name string) {
   489  	idx := nameLockIndex(name, option.Config.DNSProxyLockCount)
   490  	n.nameLocks[idx].Lock()
   491  }
   492  
   493  // UnlockName releases a lock previously acquired by LockName()
   494  func (n *NameManager) UnlockName(name string) {
   495  	idx := nameLockIndex(name, option.Config.DNSProxyLockCount)
   496  	n.nameLocks[idx].Unlock()
   497  }
   498  
   499  // nameLockIndex hashes the DNS name to a uint32, then returns that
   500  // mod the bucket count.
   501  func nameLockIndex(name string, cnt int) uint32 {
   502  	h := fnv.New32()
   503  	_, _ = h.Write([]byte(name)) // cannot return error
   504  	return h.Sum32() % uint32(cnt)
   505  }
   506  
   507  type nameMetadata struct {
   508  	addrs  []netip.Addr
   509  	labels labels.Labels // if empty, metadata will be removed for this name
   510  }
   511  
   512  // deriveLabelsForName derives what `fqdn:` labels we want to associate with
   513  // IPs for this DNS name, i.e. what selectors match the DNS name.
   514  func deriveLabelsForName(dnsName string, selectors map[api.FQDNSelector]*regexp.Regexp) labels.Labels {
   515  	lbls := labels.Labels{}
   516  	for fqdnSel, fqdnRegex := range selectors {
   517  		matches := fqdnRegex.MatchString(dnsName)
   518  		if matches {
   519  			l := fqdnSel.IdentityLabel()
   520  			lbls[l.Key] = l
   521  		}
   522  	}
   523  	return lbls
   524  }
   525  
   526  // deriveLabelsForNames derives the labels for all names found in nameToIPs
   527  func deriveLabelsForNames(nameToIPs map[string][]netip.Addr, selectors map[api.FQDNSelector]*regexp.Regexp) (namesWithMetadata map[string]nameMetadata) {
   528  	namesWithMetadata = make(map[string]nameMetadata, len(nameToIPs))
   529  	for dnsName, addrs := range nameToIPs {
   530  		namesWithMetadata[dnsName] = nameMetadata{
   531  			addrs:  addrs,
   532  			labels: deriveLabelsForName(dnsName, selectors),
   533  		}
   534  	}
   535  	return namesWithMetadata
   536  }