istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/networking/core/tls.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 core
    16  
    17  import (
    18  	"sort"
    19  	"strings"
    20  
    21  	"istio.io/api/networking/v1alpha3"
    22  	"istio.io/istio/pilot/pkg/model"
    23  	"istio.io/istio/pilot/pkg/networking/core/tunnelingconfig"
    24  	"istio.io/istio/pilot/pkg/networking/telemetry"
    25  	"istio.io/istio/pilot/pkg/networking/util"
    26  	"istio.io/istio/pilot/pkg/serviceregistry/provider"
    27  	"istio.io/istio/pkg/config"
    28  	"istio.io/istio/pkg/config/constants"
    29  	"istio.io/istio/pkg/config/host"
    30  	"istio.io/istio/pkg/config/labels"
    31  	"istio.io/istio/pkg/log"
    32  	"istio.io/istio/pkg/slices"
    33  	"istio.io/istio/pkg/util/sets"
    34  )
    35  
    36  // Match by source labels, the listener port where traffic comes in, the gateway on which the rule is being
    37  // bound, etc. All these can be checked statically, since we are generating the configuration for a proxy
    38  // with predefined labels, on a specific port.
    39  func matchTLS(match *v1alpha3.TLSMatchAttributes, proxyLabels labels.Instance, gateways sets.String, port int, proxyNamespace string) bool {
    40  	if match == nil {
    41  		return true
    42  	}
    43  
    44  	gatewayMatch := len(match.Gateways) == 0
    45  	for _, gateway := range match.Gateways {
    46  		gatewayMatch = gatewayMatch || gateways.Contains(gateway)
    47  	}
    48  
    49  	labelMatch := labels.Instance(match.SourceLabels).SubsetOf(proxyLabels)
    50  
    51  	portMatch := match.Port == 0 || match.Port == uint32(port)
    52  
    53  	nsMatch := match.SourceNamespace == "" || match.SourceNamespace == proxyNamespace
    54  
    55  	return gatewayMatch && labelMatch && portMatch && nsMatch
    56  }
    57  
    58  // Match by source labels, the listener port where traffic comes in, the gateway on which the rule is being
    59  // bound, etc. All these can be checked statically, since we are generating the configuration for a proxy
    60  // with predefined labels, on a specific port.
    61  func matchTCP(match *v1alpha3.L4MatchAttributes, proxyLabels labels.Instance, gateways sets.String, port int, proxyNamespace string) bool {
    62  	if match == nil {
    63  		return true
    64  	}
    65  
    66  	gatewayMatch := len(match.Gateways) == 0
    67  	for _, gateway := range match.Gateways {
    68  		gatewayMatch = gatewayMatch || gateways.Contains(gateway)
    69  	}
    70  
    71  	labelMatch := labels.Instance(match.SourceLabels).SubsetOf(proxyLabels)
    72  
    73  	portMatch := match.Port == 0 || match.Port == uint32(port)
    74  
    75  	nsMatch := match.SourceNamespace == "" || match.SourceNamespace == proxyNamespace
    76  
    77  	return gatewayMatch && labelMatch && portMatch && nsMatch
    78  }
    79  
    80  // Select the config pertaining to the service being processed.
    81  func getConfigsForHost(filterNamespace string, hostname host.Name, configs []config.Config) []config.Config {
    82  	svcConfigs := make([]config.Config, 0)
    83  	for _, cfg := range configs {
    84  		virtualService := cfg.Spec.(*v1alpha3.VirtualService)
    85  		for _, vsHost := range virtualService.Hosts {
    86  			if filterNamespace != "" && filterNamespace != cfg.Namespace {
    87  				continue
    88  			}
    89  			if host.Name(vsHost).Matches(hostname) {
    90  				svcConfigs = append(svcConfigs, cfg)
    91  				break
    92  			}
    93  		}
    94  	}
    95  	return svcConfigs
    96  }
    97  
    98  // hashRuntimeTLSMatchPredicates hashes runtime predicates of a TLS match
    99  func hashRuntimeTLSMatchPredicates(match *v1alpha3.TLSMatchAttributes) string {
   100  	return strings.Join(match.SniHosts, ",") + "|" + strings.Join(match.DestinationSubnets, ",")
   101  }
   102  
   103  func buildSidecarOutboundTLSFilterChainOpts(node *model.Proxy, push *model.PushContext, destinationCIDR string,
   104  	service *model.Service, bind string, listenPort *model.Port,
   105  	gateways sets.String, configs []config.Config,
   106  ) []*filterChainOpts {
   107  	if !listenPort.Protocol.IsTLS() {
   108  		return nil
   109  	}
   110  	actualWildcard, _ := getActualWildcardAndLocalHost(node)
   111  	// TLS matches are composed of runtime and static predicates.
   112  	// Static predicates can be evaluated during the generation of the config. Examples: gateway, source labels, etc.
   113  	// Runtime predicates cannot be evaluated during config generation. Instead the proxy must be configured to
   114  	// evaluate them. Examples: SNI hosts, source/destination subnets, etc.
   115  	//
   116  	// A list of matches may contain duplicate runtime matches, but different static matches. For example:
   117  	//
   118  	// {sni_hosts: A, sourceLabels: X} => destination M
   119  	// {sni_hosts: A, sourceLabels: *} => destination N
   120  	//
   121  	// For a proxy with labels X, we can evaluate the static predicates to get:
   122  	// {sni_hosts: A} => destination M
   123  	// {sni_hosts: A} => destination N
   124  	//
   125  	// The matches have the same runtime predicates. Since the second match can never be reached, we only
   126  	// want to generate config for the first match.
   127  	//
   128  	// To achieve this in this function we keep track of which runtime matches we have already generated config for
   129  	// and only add config if the we have not already generated config for that set of runtime predicates.
   130  	matchHasBeenHandled := sets.New[string]() // Runtime predicate set -> have we generated config for this set?
   131  
   132  	// Is there a virtual service with a TLS block that matches us?
   133  	hasTLSMatch := false
   134  
   135  	lb := &ListenerBuilder{node: node, push: push}
   136  	out := make([]*filterChainOpts, 0)
   137  	for _, cfg := range configs {
   138  		virtualService := cfg.Spec.(*v1alpha3.VirtualService)
   139  		for _, tls := range virtualService.Tls {
   140  			for _, match := range tls.Match {
   141  				if matchTLS(match, node.Labels, gateways, listenPort.Port, node.Metadata.Namespace) {
   142  					// Use the service's CIDRs.
   143  					// But if a virtual service overrides it with its own destination subnet match
   144  					// give preference to the user provided one
   145  					// destinationCIDR will be empty for services with VIPs
   146  					var destinationCIDRs []string
   147  					if destinationCIDR != "" {
   148  						destinationCIDRs = []string{destinationCIDR}
   149  					}
   150  					// Only set CIDR match if the listener is bound to an IP.
   151  					// If its bound to a unix domain socket, then ignore the CIDR matches
   152  					// Unix domain socket bound ports have Port value set to 0
   153  					if len(match.DestinationSubnets) > 0 && listenPort.Port > 0 {
   154  						destinationCIDRs = match.DestinationSubnets
   155  					}
   156  					matchHash := hashRuntimeTLSMatchPredicates(match)
   157  					if !matchHasBeenHandled.Contains(matchHash) {
   158  						out = append(out, &filterChainOpts{
   159  							metadata:         util.BuildConfigInfoMetadata(cfg.Meta),
   160  							sniHosts:         match.SniHosts,
   161  							destinationCIDRs: destinationCIDRs,
   162  							networkFilters:   lb.buildOutboundNetworkFilters(tls.Route, listenPort, cfg.Meta, false),
   163  						})
   164  						hasTLSMatch = true
   165  					}
   166  					matchHasBeenHandled.Insert(matchHash)
   167  				}
   168  			}
   169  		}
   170  	}
   171  
   172  	// HTTPS or TLS ports without associated virtual service
   173  	if !hasTLSMatch {
   174  		var sniHosts []string
   175  
   176  		// In case of a sidecar config with user defined port, if the user specified port is not the same as the
   177  		// service's port, then pick the service port if and only if the service has only one port. If service
   178  		// has multiple ports, then route to a cluster with the listener port (i.e. sidecar defined port) - the
   179  		// traffic will most likely blackhole.
   180  		port := listenPort.Port
   181  		if len(service.Ports) == 1 {
   182  			port = service.Ports[0].Port
   183  		}
   184  
   185  		clusterName := model.BuildSubsetKey(model.TrafficDirectionOutbound, "", service.Hostname, port)
   186  		statPrefix := clusterName
   187  		// If stat name is configured, use it to build the stat prefix.
   188  		if len(push.Mesh.OutboundClusterStatName) != 0 {
   189  			statPrefix = telemetry.BuildStatPrefix(push.Mesh.OutboundClusterStatName, string(service.Hostname), "", &model.Port{Port: port}, 0, &service.Attributes)
   190  		}
   191  		// Use the hostname as the SNI value if and only:
   192  		// 1) if the destination is a CIDR;
   193  		// 2) or if we have an empty destination VIP (i.e. which we should never get in case some platform adapter improper handlings);
   194  		// 3) or if the destination is a wildcard destination VIP with the listener bound to the wildcard as well.
   195  		// In the above cited cases, the listener will be bound to 0.0.0.0. So SNI match is the only way to distinguish different
   196  		// target services. If we have a VIP, then we know the destination. Or if we do not have an VIP, but have
   197  		// `PILOT_ENABLE_HEADLESS_SERVICE_POD_LISTENERS` enabled (by default) and applicable to all that's needed, pilot will generate
   198  		// an outbound listener for each pod in a headless service. There is thus no need to do a SNI match. It saves us from having to
   199  		// generate expensive permutations of the host name just like RDS does..
   200  		// NOTE that we cannot have two services with the same VIP as our listener build logic will treat it as a collision and
   201  		// ignore one of the services.
   202  		svcListenAddress := service.GetAddressForProxy(node)
   203  		if strings.Contains(svcListenAddress, "/") {
   204  			// Address is a CIDR, already captured by destinationCIDR parameter.
   205  			svcListenAddress = ""
   206  		}
   207  
   208  		if service.Attributes.ServiceRegistry == provider.External && node.IsIPv6() && svcListenAddress == constants.UnspecifiedIP {
   209  			svcListenAddress = constants.UnspecifiedIPv6
   210  		}
   211  
   212  		if len(destinationCIDR) > 0 || len(svcListenAddress) == 0 || (svcListenAddress == actualWildcard && bind == actualWildcard) {
   213  			sniHosts = []string{string(service.Hostname)}
   214  			for _, a := range service.Attributes.Aliases {
   215  				alt := GenerateAltVirtualHosts(a.Hostname.String(), 0, node.DNSDomain)
   216  				sniHosts = append(sniHosts, a.Hostname.String())
   217  				sniHosts = append(sniHosts, alt...)
   218  			}
   219  		}
   220  		destinationRule := CastDestinationRule(node.SidecarScope.DestinationRule(
   221  			model.TrafficDirectionOutbound, node, service.Hostname).GetRule())
   222  		var destinationCIDRs []string
   223  		if destinationCIDR != "" {
   224  			destinationCIDRs = []string{destinationCIDR}
   225  		}
   226  		out = append(out, &filterChainOpts{
   227  			sniHosts:         sniHosts,
   228  			destinationCIDRs: destinationCIDRs,
   229  			networkFilters: lb.buildOutboundNetworkFiltersWithSingleDestination(statPrefix, clusterName, "",
   230  				listenPort, destinationRule, tunnelingconfig.Apply, false),
   231  		})
   232  	}
   233  
   234  	return out
   235  }
   236  
   237  func buildSidecarOutboundTCPFilterChainOpts(node *model.Proxy, push *model.PushContext, destinationCIDR string,
   238  	service *model.Service, listenPort *model.Port,
   239  	gateways sets.String, configs []config.Config,
   240  ) []*filterChainOpts {
   241  	if listenPort.Protocol.IsTLS() {
   242  		return nil
   243  	}
   244  
   245  	out := make([]*filterChainOpts, 0)
   246  
   247  	lb := &ListenerBuilder{node: node, push: push}
   248  	// very basic TCP
   249  	// break as soon as we add one network filter with no destination addresses to match
   250  	// This is the terminating condition in the filter chain match list
   251  	defaultRouteAdded := false
   252  TcpLoop:
   253  	for _, cfg := range configs {
   254  		virtualService := cfg.Spec.(*v1alpha3.VirtualService)
   255  		for _, tcp := range virtualService.Tcp {
   256  			var destinationCIDRs []string
   257  			if destinationCIDR != "" {
   258  				destinationCIDRs = []string{destinationCIDR}
   259  			}
   260  			if len(tcp.Match) == 0 {
   261  				// implicit match
   262  				out = append(out, &filterChainOpts{
   263  					metadata:         util.BuildConfigInfoMetadata(cfg.Meta),
   264  					destinationCIDRs: destinationCIDRs,
   265  					networkFilters:   lb.buildOutboundNetworkFilters(tcp.Route, listenPort, cfg.Meta, false),
   266  				})
   267  				defaultRouteAdded = true
   268  				break TcpLoop
   269  			}
   270  
   271  			// Use the service's virtual address first.
   272  			// But if a virtual service overrides it with its own destination subnet match
   273  			// give preference to the user provided one
   274  			virtualServiceDestinationSubnets := make([]string, 0)
   275  
   276  			for _, match := range tcp.Match {
   277  				if matchTCP(match, node.Labels, gateways, listenPort.Port, node.Metadata.Namespace) {
   278  					// Scan all the match blocks
   279  					// if we find any match block without a runtime destination subnet match
   280  					// i.e. match any destination address, then we treat it as the terminal match/catch all match
   281  					// and break out of the loop. We also treat it as a terminal match if the listener is bound
   282  					// to a unix domain socket.
   283  					// But if we find only runtime destination subnet matches in all match blocks, collect them
   284  					// (this is similar to virtual hosts in http) and create filter chain match accordingly.
   285  					if len(match.DestinationSubnets) == 0 || listenPort.Port == 0 {
   286  						out = append(out, &filterChainOpts{
   287  							metadata:         util.BuildConfigInfoMetadata(cfg.Meta),
   288  							destinationCIDRs: destinationCIDRs,
   289  							networkFilters:   lb.buildOutboundNetworkFilters(tcp.Route, listenPort, cfg.Meta, false),
   290  						})
   291  						defaultRouteAdded = true
   292  						break TcpLoop
   293  					}
   294  					virtualServiceDestinationSubnets = append(virtualServiceDestinationSubnets, match.DestinationSubnets...)
   295  				}
   296  			}
   297  
   298  			if len(virtualServiceDestinationSubnets) > 0 {
   299  				out = append(out, &filterChainOpts{
   300  					destinationCIDRs: virtualServiceDestinationSubnets,
   301  					networkFilters:   lb.buildOutboundNetworkFilters(tcp.Route, listenPort, cfg.Meta, false),
   302  				})
   303  
   304  				// If at this point there is a filter chain generated with the same CIDR match as the
   305  				// one that may be generated for the service as the default route, do not generate it.
   306  				// Otherwise, Envoy will complain about having filter chains with identical matches
   307  				// and will reject the config.
   308  				sort.Strings(virtualServiceDestinationSubnets)
   309  				sort.Strings(destinationCIDRs)
   310  				if slices.Equal(virtualServiceDestinationSubnets, destinationCIDRs) {
   311  					log.Warnf("Existing filter chain with same matching CIDR: %v.", destinationCIDRs)
   312  					defaultRouteAdded = true
   313  				}
   314  			}
   315  		}
   316  	}
   317  
   318  	if !defaultRouteAdded {
   319  		// In case of a sidecar config with user defined port, if the user specified port is not the same as the
   320  		// service's port, then pick the service port if and only if the service has only one port. If service
   321  		// has multiple ports, then route to a cluster with the listener port (i.e. sidecar defined port) - the
   322  		// traffic will most likely blackhole.
   323  		port := listenPort.Port
   324  		if len(service.Ports) == 1 {
   325  			port = service.Ports[0].Port
   326  		}
   327  
   328  		clusterName := model.BuildSubsetKey(model.TrafficDirectionOutbound, "", service.Hostname, port)
   329  		statPrefix := clusterName
   330  		destinationRule := CastDestinationRule(node.SidecarScope.DestinationRule(
   331  			model.TrafficDirectionOutbound, node, service.Hostname).GetRule())
   332  		// If stat name is configured, use it to build the stat prefix.
   333  		if len(push.Mesh.OutboundClusterStatName) != 0 {
   334  			statPrefix = telemetry.BuildStatPrefix(push.Mesh.OutboundClusterStatName, string(service.Hostname), "", &model.Port{Port: port}, 0, &service.Attributes)
   335  		}
   336  		var destinationCIDRs []string
   337  		if destinationCIDR != "" {
   338  			destinationCIDRs = []string{destinationCIDR}
   339  		}
   340  		out = append(out, &filterChainOpts{
   341  			destinationCIDRs: destinationCIDRs,
   342  			networkFilters: lb.buildOutboundNetworkFiltersWithSingleDestination(statPrefix, clusterName, "",
   343  				listenPort, destinationRule, tunnelingconfig.Apply, false),
   344  		})
   345  	}
   346  
   347  	return out
   348  }