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 }