istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/dns/server/name_table.go (about)

     1  // Copyright Istio Authors
     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 server
    16  
    17  import (
    18  	"strings"
    19  
    20  	"istio.io/istio/pilot/pkg/model"
    21  	"istio.io/istio/pilot/pkg/serviceregistry/provider"
    22  	"istio.io/istio/pkg/config/constants"
    23  	dnsProto "istio.io/istio/pkg/dns/proto"
    24  	netutil "istio.io/istio/pkg/util/net"
    25  )
    26  
    27  // Config for building the name table.
    28  type Config struct {
    29  	Node *model.Proxy
    30  	Push *model.PushContext
    31  
    32  	// MulticlusterHeadlessEnabled if true, the DNS name table for a headless service will resolve to
    33  	// same-network endpoints in any cluster.
    34  	MulticlusterHeadlessEnabled bool
    35  }
    36  
    37  // BuildNameTable produces a table of hostnames and their associated IPs that can then
    38  // be used by the agent to resolve DNS. This logic is always active. However, local DNS resolution
    39  // will only be effective if DNS capture is enabled in the proxy
    40  func BuildNameTable(cfg Config) *dnsProto.NameTable {
    41  	out := &dnsProto.NameTable{
    42  		Table: make(map[string]*dnsProto.NameTable_NameInfo),
    43  	}
    44  	for _, svc := range cfg.Node.SidecarScope.Services() {
    45  		svcAddress := svc.GetAddressForProxy(cfg.Node)
    46  		var addressList []string
    47  		hostName := svc.Hostname
    48  		if svcAddress != constants.UnspecifiedIP {
    49  			// Filter out things we cannot parse as IP. Generally this means CIDRs, as anything else
    50  			// should be caught in validation.
    51  			if !netutil.IsValidIPAddress(svcAddress) {
    52  				continue
    53  			}
    54  			addressList = append(addressList, svcAddress)
    55  		} else {
    56  			// The IP will be unspecified here if its headless service or if the auto
    57  			// IP allocation logic for service entry was unable to allocate an IP.
    58  			if svc.Resolution == model.Passthrough && len(svc.Ports) > 0 {
    59  				for _, instance := range cfg.Push.ServiceEndpointsByPort(svc, svc.Ports[0].Port, nil) {
    60  					// empty addresses are possible here
    61  					if !netutil.IsValidIPAddress(instance.Address) {
    62  						continue
    63  					}
    64  					// TODO(stevenctl): headless across-networks https://github.com/istio/istio/issues/38327
    65  					sameNetwork := cfg.Node.InNetwork(instance.Network)
    66  					sameCluster := cfg.Node.InCluster(instance.Locality.ClusterID)
    67  					// For all k8s headless services, populate the dns table with the endpoint IPs as k8s does.
    68  					// And for each individual pod, populate the dns table with the endpoint IP with a manufactured host name.
    69  					if instance.SubDomain != "" && sameNetwork {
    70  						// Follow k8s pods dns naming convention of "<hostname>.<subdomain>.<pod namespace>.svc.<cluster domain>"
    71  						// i.e. "mysql-0.mysql.default.svc.cluster.local".
    72  						parts := strings.SplitN(hostName.String(), ".", 2)
    73  						if len(parts) != 2 {
    74  							continue
    75  						}
    76  						address := []string{instance.Address}
    77  						shortName := instance.HostName + "." + instance.SubDomain
    78  						host := shortName + "." + parts[1] // Add cluster domain.
    79  						nameInfo := &dnsProto.NameTable_NameInfo{
    80  							Ips:       address,
    81  							Registry:  string(svc.Attributes.ServiceRegistry),
    82  							Namespace: svc.Attributes.Namespace,
    83  							Shortname: shortName,
    84  						}
    85  
    86  						if _, f := out.Table[host]; !f || sameCluster {
    87  							// We may have the same pod in two clusters (ie mysql-0 deployed in both places).
    88  							// We can only return a single IP for these queries. We should prefer the local cluster,
    89  							// so if the entry already exists only overwrite it if the instance is in our own cluster.
    90  							out.Table[host] = nameInfo
    91  						}
    92  					}
    93  					skipForMulticluster := !cfg.MulticlusterHeadlessEnabled && !sameCluster
    94  					if skipForMulticluster || !sameNetwork {
    95  						// We take only cluster-local endpoints. While this seems contradictory to
    96  						// our logic other parts of the code, where cross-cluster is the default.
    97  						// However, this only impacts the DNS response. If we were to send all
    98  						// endpoints, cross network routing would break, as we do passthrough LB and
    99  						// don't go through the network gateway. While we could, hypothetically, send
   100  						// "network-local" endpoints, this would still make enabling DNS give vastly
   101  						// different load balancing than without, so its probably best to filter.
   102  						// This ends up matching the behavior of Kubernetes DNS.
   103  						continue
   104  					}
   105  					// TODO: should we skip the node's own IP like we do in listener?
   106  					addressList = append(addressList, instance.Address)
   107  				}
   108  			}
   109  			if len(addressList) == 0 {
   110  				// could not reliably determine the addresses of endpoints of headless service
   111  				// or this is not a k8s service
   112  				continue
   113  			}
   114  		}
   115  
   116  		if ni, f := out.Table[hostName.String()]; !f {
   117  			nameInfo := &dnsProto.NameTable_NameInfo{
   118  				Ips:      addressList,
   119  				Registry: string(svc.Attributes.ServiceRegistry),
   120  			}
   121  			if svc.Attributes.ServiceRegistry == provider.Kubernetes &&
   122  				!strings.HasSuffix(hostName.String(), "."+constants.DefaultClusterSetLocalDomain) {
   123  				// The agent will take care of resolving a, a.ns, a.ns.svc, etc.
   124  				// No need to provide a DNS entry for each variant.
   125  				//
   126  				// NOTE: This is not done for Kubernetes Multi-Cluster Services (MCS) hosts, in order
   127  				// to avoid conflicting with the entries for the regular (cluster.local) service.
   128  				nameInfo.Namespace = svc.Attributes.Namespace
   129  				nameInfo.Shortname = svc.Attributes.Name
   130  			}
   131  			out.Table[hostName.String()] = nameInfo
   132  		} else if provider.ID(ni.Registry) != provider.Kubernetes {
   133  			// 2 possible cases:
   134  			// 1. If the SE has multiple addresses(vips) specified, merge the ips
   135  			// 2. If the previous SE is a decorator of the k8s service, give precedence to the k8s service
   136  			if svc.Attributes.ServiceRegistry == provider.Kubernetes {
   137  				ni.Ips = addressList
   138  				ni.Registry = string(provider.Kubernetes)
   139  				if !strings.HasSuffix(hostName.String(), "."+constants.DefaultClusterSetLocalDomain) {
   140  					ni.Namespace = svc.Attributes.Namespace
   141  					ni.Shortname = svc.Attributes.Name
   142  				}
   143  			} else {
   144  				ni.Ips = append(ni.Ips, addressList...)
   145  			}
   146  		}
   147  	}
   148  	return out
   149  }