istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/networking/core/route/route.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 route 16 17 import ( 18 "fmt" 19 "regexp" 20 "sort" 21 "strconv" 22 "strings" 23 24 core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 25 route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 26 xdsfault "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/common/fault/v3" 27 cors "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/cors/v3" 28 xdshttpfault "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/fault/v3" 29 statefulsession "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/stateful_session/v3" 30 matcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" 31 xdstype "github.com/envoyproxy/go-control-plane/envoy/type/v3" 32 "google.golang.org/protobuf/types/known/anypb" 33 "google.golang.org/protobuf/types/known/durationpb" 34 "google.golang.org/protobuf/types/known/wrapperspb" 35 "k8s.io/apimachinery/pkg/types" 36 37 meshconfig "istio.io/api/mesh/v1alpha1" 38 networking "istio.io/api/networking/v1alpha3" 39 "istio.io/istio/pilot/pkg/features" 40 "istio.io/istio/pilot/pkg/model" 41 "istio.io/istio/pilot/pkg/networking/core/route/retry" 42 "istio.io/istio/pilot/pkg/networking/telemetry" 43 "istio.io/istio/pilot/pkg/networking/util" 44 authz "istio.io/istio/pilot/pkg/security/authz/model" 45 "istio.io/istio/pilot/pkg/util/protoconv" 46 "istio.io/istio/pkg/config" 47 "istio.io/istio/pkg/config/constants" 48 "istio.io/istio/pkg/config/host" 49 "istio.io/istio/pkg/config/labels" 50 "istio.io/istio/pkg/jwt" 51 "istio.io/istio/pkg/log" 52 "istio.io/istio/pkg/util/grpc" 53 "istio.io/istio/pkg/util/sets" 54 "istio.io/istio/pkg/wellknown" 55 ) 56 57 // Headers with special meaning in Envoy 58 const ( 59 HeaderMethod = ":method" 60 HeaderAuthority = ":authority" 61 HeaderScheme = ":scheme" 62 ) 63 64 // DefaultRouteName is the name assigned to a route generated by default in absence of a virtual service. 65 const DefaultRouteName = "default" 66 67 var Notimeout = durationpb.New(0) 68 69 // DefaultMaxDirectResponseBodySizeBytes is 1mb, the same limit the control plane validates via webhook. Set this to increase from envoy default of 4k 70 var DefaultMaxDirectResponseBodySizeBytes = wrapperspb.UInt32(1024 * 1024) 71 72 type DestinationHashMap map[*networking.HTTPRouteDestination]*networking.LoadBalancerSettings_ConsistentHashLB 73 74 // VirtualHostWrapper is a context-dependent virtual host entry with guarded routes. 75 // Note: Currently we are not fully utilizing this structure. We could invoke this logic 76 // once for all sidecars in the cluster to compute all RDS for inside the mesh and arrange 77 // it by listener port. However to properly use such an optimization, we need to have an 78 // eventing subsystem to invalidate the computed routes if any service changes/virtual Services change. 79 type VirtualHostWrapper struct { 80 // Port is the listener port for outbound sidecar (e.g. service port) 81 Port int 82 83 // Services are the Services from the registry. Each service 84 // in this list should have a virtual host entry 85 Services []*model.Service 86 87 // VirtualServiceHosts is a list of hosts defined in the virtual service 88 // if virtual service hostname is same as a the service registry host, then 89 // the host would appear in Services as we need to generate all variants of the 90 // service's hostname within a platform (e.g., foo, foo.default, foo.default.svc, etc.) 91 VirtualServiceHosts []string 92 93 // Routes in the virtual host 94 Routes []*route.Route 95 } 96 97 // BuildSidecarVirtualHostWrapper creates virtual hosts from the given set of virtual Services 98 // and a list of Services from the service registry. Services are indexed by FQDN hostnames. 99 // The list of Services is also passed to allow maintaining consistent ordering. 100 func BuildSidecarVirtualHostWrapper(routeCache *Cache, node *model.Proxy, push *model.PushContext, serviceRegistry map[host.Name]*model.Service, 101 virtualServices []config.Config, listenPort int, mostSpecificWildcardVsIndex map[host.Name]types.NamespacedName, 102 ) []VirtualHostWrapper { 103 out := make([]VirtualHostWrapper, 0) 104 105 // dependentDestinationRules includes all the destinationrules referenced by 106 // the virtualservices, which have consistent hash policy. 107 dependentDestinationRules := []*model.ConsolidatedDestRule{} 108 109 // First build virtual host wrappers for services that have virtual services. 110 for _, virtualService := range virtualServices { 111 hashByDestination, destinationRules := hashForVirtualService(push, node, virtualService) 112 dependentDestinationRules = append(dependentDestinationRules, destinationRules...) 113 wrappers := buildSidecarVirtualHostsForVirtualService( 114 node, virtualService, serviceRegistry, hashByDestination, listenPort, push.Mesh, mostSpecificWildcardVsIndex, 115 ) 116 out = append(out, wrappers...) 117 } 118 119 // Now exclude the services that have virtual services. 120 for _, wrapper := range out { 121 for _, service := range wrapper.Services { 122 delete(serviceRegistry, service.Hostname) 123 } 124 } 125 126 for _, svc := range serviceRegistry { 127 for _, port := range svc.Ports { 128 if port.Protocol.IsHTTPOrSniffed() { 129 hash, destinationRule := hashForService(push, node, svc, port) 130 if hash != nil { 131 dependentDestinationRules = append(dependentDestinationRules, destinationRule) 132 } 133 // append default hosts for the service missing virtual Services. 134 out = append(out, buildSidecarVirtualHostForService(svc, port, hash, push.Mesh)) 135 } 136 } 137 } 138 139 if routeCache != nil { 140 routeCache.DestinationRules = dependentDestinationRules 141 } 142 143 return out 144 } 145 146 // separateVSHostsAndServices splits the virtual service hosts into Services (if they are found in the registry) and 147 // plain non-registry hostnames 148 func separateVSHostsAndServices(virtualService config.Config, 149 serviceRegistry map[host.Name]*model.Service, 150 mostSpecificWildcardVsIndex map[host.Name]types.NamespacedName, 151 ) ([]string, []*model.Service) { 152 // TODO: A further optimization would be to completely rely on the index and not do the loop below 153 // However, that requires assuming that serviceRegistry never got filtered after the 154 // egressListener was created. 155 rule := virtualService.Spec.(*networking.VirtualService) 156 // Stores VS hosts that don't correspond to services in the registry 157 // Currently, the only use for this list is to enable VirtualService configuration to affect 158 // traffic to hosts outside of the service registry (e.g. google.com) on port 80 159 nonServiceRegistryHosts := make([]string, 0) 160 // Stores services for this VirtualService that are in the registry (based on hostname) 161 matchingRegistryServices := make([]*model.Service, 0) 162 wchosts := make([]host.Name, 0) 163 164 // As a performance optimization, process non wildcard hosts first, so that they can be 165 // looked up directly in the service registry map. 166 for _, hostname := range rule.Hosts { 167 vshost := host.Name(hostname) 168 if vshost.IsWildCarded() { 169 // We'll process wild card hosts later 170 wchosts = append(wchosts, vshost) 171 continue 172 } 173 if svc, exists := serviceRegistry[vshost]; exists { 174 matchingRegistryServices = append(matchingRegistryServices, svc) 175 } else { 176 nonServiceRegistryHosts = append(nonServiceRegistryHosts, hostname) 177 } 178 } 179 180 // Now process wild card hosts as they need to follow the slow path of looping through all Services in the registry. 181 for _, hostname := range wchosts { 182 if model.UseGatewaySemantics(virtualService) { 183 nonServiceRegistryHosts = append(nonServiceRegistryHosts, string(hostname)) 184 continue 185 } 186 // foundSvcMatch's only purpose is to make sure we don't add hosts that correspond to services 187 // to the list of non-serviceregistry hosts 188 foundSvcMatch := false 189 for svcHost, svc := range serviceRegistry { 190 // First, check if this service matches the VS host. 191 // If it does, then we never want to add it to the nonServiceRegistryHosts list. 192 // The result is OR'd so we don't overwrite a previously true value 193 // localMatch is tracking the match found within this iteration of the loop 194 localMatch := svcHost.Matches(hostname) 195 // foundSvcMatch is tracking in the wider context whether or not ANY match was found during an iteration 196 foundSvcMatch = foundSvcMatch || localMatch 197 if !localMatch { 198 // If the wildcard doesn't even match this service, it won't be in the index 199 continue 200 } 201 // The mostSpecificWildcardVsIndex ensures that each VirtualService host is only associated with 202 // a single service in the registry. This is generally results in the most specific wildcard match for 203 // a given wildcard host (unless PERSIST_OLDEST_FIRST_HEURISTIC_FOR_VIRTUAL_SERVICE_HOST_MATCHING is true). 204 vs, ok := mostSpecificWildcardVsIndex[svcHost] 205 if !ok { 206 // This service doesn't have a virtualService that matches it. 207 continue 208 } 209 if vs != virtualService.NamespacedName() { 210 // This virtual service is not the most specific wildcard match for this service. 211 // So we don't add it to the list of services in this virtual service so as 212 // to avoid duplicates 213 continue 214 } 215 matchingRegistryServices = append(matchingRegistryServices, svc) 216 } 217 218 // If we never found a match for this hostname in the service registry, add it to the list of non-service hosts 219 if !foundSvcMatch { 220 nonServiceRegistryHosts = append(nonServiceRegistryHosts, string(hostname)) 221 } 222 } 223 224 return nonServiceRegistryHosts, matchingRegistryServices 225 } 226 227 // buildSidecarVirtualHostsForVirtualService creates virtual hosts corresponding to a virtual service. 228 // Called for each port to determine the list of vhosts on the given port. 229 // It may return an empty list if no VirtualService rule has a matching service. 230 func buildSidecarVirtualHostsForVirtualService( 231 node *model.Proxy, 232 virtualService config.Config, 233 serviceRegistry map[host.Name]*model.Service, 234 hashByDestination DestinationHashMap, 235 listenPort int, 236 mesh *meshconfig.MeshConfig, 237 mostSpecificWildcardVsIndex map[host.Name]types.NamespacedName, 238 ) []VirtualHostWrapper { 239 meshGateway := sets.New(constants.IstioMeshGateway) 240 opts := RouteOptions{ 241 // Sidecar is never terminating TLS 242 IsTLS: false, 243 // Sidecar is never doing H3 (yet) 244 IsHTTP3AltSvcHeaderNeeded: false, 245 Mesh: mesh, 246 } 247 routes, err := BuildHTTPRoutesForVirtualService(node, virtualService, serviceRegistry, hashByDestination, 248 listenPort, meshGateway, opts) 249 if err != nil || len(routes) == 0 { 250 return nil 251 } 252 253 hosts, matchingRegistryServices := separateVSHostsAndServices(virtualService, serviceRegistry, mostSpecificWildcardVsIndex) 254 255 // Gateway allows only routes from the namespace of the proxy, or namespace of the destination. 256 if model.UseGatewaySemantics(virtualService) { 257 res := make([]*model.Service, 0, len(matchingRegistryServices)) 258 for _, s := range matchingRegistryServices { 259 if s.Attributes.Namespace != virtualService.Namespace && node.ConfigNamespace != virtualService.Namespace { 260 continue 261 } 262 res = append(res, s) 263 } 264 if len(res) == 0 { 265 return nil 266 } 267 } 268 269 // Now group these Services by port so that we can infer the destination.port if the user 270 // doesn't specify any port for a multiport service. We need to know the destination port in 271 // order to build the cluster name (outbound|<port>|<subset>|<serviceFQDN>) 272 // If the destination service is being accessed on port X, we set that as the default 273 // destination port 274 serviceByPort := make(map[int][]*model.Service) 275 for _, svc := range matchingRegistryServices { 276 for _, port := range svc.Ports { 277 if port.Protocol.IsHTTPOrSniffed() { 278 serviceByPort[port.Port] = append(serviceByPort[port.Port], svc) 279 } 280 } 281 } 282 283 if len(serviceByPort) == 0 { 284 if listenPort == 80 { 285 // TODO: This is a gross HACK. Fix me. Its a much bigger surgery though, due to the way 286 // the current code is written. 287 serviceByPort[80] = nil 288 } 289 } 290 291 out := make([]VirtualHostWrapper, 0, len(serviceByPort)) 292 for port, services := range serviceByPort { 293 out = append(out, VirtualHostWrapper{ 294 Port: port, 295 Services: services, 296 VirtualServiceHosts: hosts, 297 Routes: routes, 298 }) 299 } 300 301 return out 302 } 303 304 func buildSidecarVirtualHostForService(svc *model.Service, 305 port *model.Port, 306 hash *networking.LoadBalancerSettings_ConsistentHashLB, 307 mesh *meshconfig.MeshConfig, 308 ) VirtualHostWrapper { 309 cluster := model.BuildSubsetKey(model.TrafficDirectionOutbound, "", svc.Hostname, port.Port) 310 traceOperation := telemetry.TraceOperation(string(svc.Hostname), port.Port) 311 httpRoute := BuildDefaultHTTPOutboundRoute(cluster, traceOperation, mesh) 312 313 // if this host has no virtualservice, the consistentHash on its destinationRule will be useless 314 hashPolicy := consistentHashToHashPolicy(hash) 315 if hashPolicy != nil { 316 httpRoute.GetRoute().HashPolicy = []*route.RouteAction_HashPolicy{hashPolicy} 317 } 318 return VirtualHostWrapper{ 319 Port: port.Port, 320 Services: []*model.Service{svc}, 321 Routes: []*route.Route{httpRoute}, 322 } 323 } 324 325 // GetDestinationCluster generates a cluster name for the route, or error if no cluster 326 // can be found. Called by translateRule to determine if 327 func GetDestinationCluster(destination *networking.Destination, service *model.Service, listenerPort int) string { 328 if len(destination.GetHost()) == 0 { 329 // only happens when the gateway-api BackendRef is invalid 330 return "UnknownService" 331 } 332 h := host.Name(destination.Host) 333 // If this is an Alias, point to the concrete service 334 // TODO: this will not work if we have Alias -> Alias -> Concrete service. 335 if features.EnableExternalNameAlias && service != nil && service.Attributes.K8sAttributes.ExternalName != "" { 336 h = host.Name(service.Attributes.K8sAttributes.ExternalName) 337 } 338 port := listenerPort 339 if destination.GetPort() != nil { 340 port = int(destination.GetPort().GetNumber()) 341 } else if service != nil && len(service.Ports) == 1 { 342 // if service only has one port defined, use that as the port, otherwise use default listenerPort 343 port = service.Ports[0].Port 344 345 // Do not return blackhole cluster for service==nil case as there is a legitimate use case for 346 // calling this function with nil service: to route to a pre-defined statically configured cluster 347 // declared as part of the bootstrap. 348 // If blackhole cluster is needed, do the check on the caller side. See gateway and tls.go for examples. 349 } 350 351 return model.BuildSubsetKey(model.TrafficDirectionOutbound, destination.Subset, h, port) 352 } 353 354 type RouteOptions struct { 355 // IsTLS indicates if the route is intended for a TLS listener 356 IsTLS bool 357 // IsHTTP3AltSvcHeaderNeeded indicates if HTTP3 alt-svc header needs to be inserted 358 IsHTTP3AltSvcHeaderNeeded bool 359 Mesh *meshconfig.MeshConfig 360 } 361 362 // BuildHTTPRoutesForVirtualService creates data plane HTTP routes from the virtual service spec. 363 // The rule should be adapted to destination names (outbound clusters). 364 // Each rule is guarded by source labels. 365 // 366 // This is called for each port to compute virtual hosts. 367 // Each VirtualService is tried, with a list of Services that listen on the port. 368 // Error indicates the given virtualService can't be used on the port. 369 // This function is used by both the gateway and the sidecar 370 func BuildHTTPRoutesForVirtualService( 371 node *model.Proxy, 372 virtualService config.Config, 373 serviceRegistry map[host.Name]*model.Service, 374 hashByDestination DestinationHashMap, 375 listenPort int, 376 gatewayNames sets.String, 377 opts RouteOptions, 378 ) ([]*route.Route, error) { 379 vs, ok := virtualService.Spec.(*networking.VirtualService) 380 if !ok { // should never happen 381 return nil, fmt.Errorf("in not a virtual service: %#v", virtualService) 382 } 383 384 out := make([]*route.Route, 0, len(vs.Http)) 385 386 catchall := false 387 for _, http := range vs.Http { 388 if len(http.Match) == 0 { 389 if r := translateRoute(node, http, nil, listenPort, virtualService, serviceRegistry, 390 hashByDestination, gatewayNames, opts); r != nil { 391 out = append(out, r) 392 } 393 catchall = true 394 } else { 395 for _, match := range http.Match { 396 if r := translateRoute(node, http, match, listenPort, virtualService, serviceRegistry, 397 hashByDestination, gatewayNames, opts); r != nil { 398 out = append(out, r) 399 // This is a catch all path. Routes are matched in order, so we will never go beyond this match 400 // As an optimization, we can just stop sending any more routes here. 401 if IsCatchAllRoute(r) { 402 catchall = true 403 break 404 } 405 } 406 } 407 } 408 if catchall { 409 break 410 } 411 } 412 413 if len(out) == 0 { 414 return nil, fmt.Errorf("no routes matched") 415 } 416 return out, nil 417 } 418 419 // sourceMatchHttp checks if the sourceLabels or the gateways in a match condition match with the 420 // labels for the proxy or the gateway name for which we are generating a route 421 func sourceMatchHTTP(match *networking.HTTPMatchRequest, proxyLabels labels.Instance, gatewayNames sets.String, proxyNamespace string) bool { 422 if match == nil { 423 return true 424 } 425 426 // Trim by source labels or mesh gateway 427 if len(match.Gateways) > 0 { 428 for _, g := range match.Gateways { 429 if gatewayNames.Contains(g) { 430 return true 431 } 432 } 433 } else if labels.Instance(match.GetSourceLabels()).SubsetOf(proxyLabels) { 434 return match.SourceNamespace == "" || match.SourceNamespace == proxyNamespace 435 } 436 437 return false 438 } 439 440 // translateRoute translates HTTP routes 441 func translateRoute( 442 node *model.Proxy, 443 in *networking.HTTPRoute, 444 match *networking.HTTPMatchRequest, 445 listenPort int, 446 virtualService config.Config, 447 serviceRegistry map[host.Name]*model.Service, 448 hashByDestination DestinationHashMap, 449 gatewayNames sets.String, 450 opts RouteOptions, 451 ) *route.Route { 452 // When building routes, it's okay if the target cluster cannot be 453 // resolved. Traffic to such clusters will blackhole. 454 455 // Match by the destination port specified in the match condition 456 if match != nil && match.Port != 0 && match.Port != uint32(listenPort) { 457 return nil 458 } 459 // Match by source labels/gateway names inside the match condition 460 if !sourceMatchHTTP(match, node.Labels, gatewayNames, node.Metadata.Namespace) { 461 return nil 462 } 463 464 routeName := in.Name 465 if match != nil && match.Name != "" { 466 routeName = routeName + "." + match.Name 467 } 468 469 out := &route.Route{ 470 Name: routeName, 471 Match: TranslateRouteMatch(virtualService, match, node.SupportsEnvoyExtendedJwt()), 472 Metadata: util.BuildConfigInfoMetadata(virtualService.Meta), 473 } 474 475 if match != nil && match.StatPrefix != "" { 476 out.StatPrefix = match.StatPrefix 477 } 478 479 authority := "" 480 if in.Headers != nil { 481 operations := TranslateHeadersOperations(in.Headers) 482 out.RequestHeadersToAdd = operations.RequestHeadersToAdd 483 out.ResponseHeadersToAdd = operations.ResponseHeadersToAdd 484 out.RequestHeadersToRemove = operations.RequestHeadersToRemove 485 out.ResponseHeadersToRemove = operations.ResponseHeadersToRemove 486 authority = operations.Authority 487 } 488 489 var hostnames []host.Name 490 if in.Redirect != nil { 491 ApplyRedirect(out, in.Redirect, listenPort, opts.IsTLS, model.UseGatewaySemantics(virtualService)) 492 } else if in.DirectResponse != nil { 493 ApplyDirectResponse(out, in.DirectResponse) 494 } else { 495 hostnames = applyHTTPRouteDestination(out, node, virtualService, in, opts.Mesh, authority, serviceRegistry, listenPort, hashByDestination) 496 } 497 498 out.Decorator = &route.Decorator{ 499 Operation: GetRouteOperation(out, virtualService.Name, listenPort), 500 } 501 if in.Fault != nil || in.CorsPolicy != nil { 502 out.TypedPerFilterConfig = make(map[string]*anypb.Any) 503 } 504 if in.Fault != nil { 505 out.TypedPerFilterConfig[wellknown.Fault] = protoconv.MessageToAny(TranslateFault(in.Fault)) 506 } 507 if in.CorsPolicy != nil { 508 out.TypedPerFilterConfig[wellknown.CORS] = protoconv.MessageToAny(TranslateCORSPolicy(node, in.CorsPolicy)) 509 } 510 var statefulConfig *statefulsession.StatefulSession 511 for _, hostname := range hostnames { 512 perSvcStatefulConfig := util.MaybeBuildStatefulSessionFilterConfig(serviceRegistry[hostname]) 513 // This means we have more than one stateful config for the same route because of weighed destinations. 514 // We should just pick the first and give a warning. 515 if perSvcStatefulConfig != nil && statefulConfig != nil { 516 log.Warnf("More than one stateful config for the same route %s. Picking the first one.", routeName) 517 break 518 } 519 statefulConfig = perSvcStatefulConfig 520 } 521 // Build stateful set config if the svc has appropriate labels attached. 522 if statefulConfig != nil { 523 if out.TypedPerFilterConfig == nil { 524 out.TypedPerFilterConfig = make(map[string]*anypb.Any) 525 } 526 perRouteStatefulSession := &statefulsession.StatefulSessionPerRoute{ 527 Override: &statefulsession.StatefulSessionPerRoute_StatefulSession{ 528 StatefulSession: statefulConfig, 529 }, 530 } 531 out.TypedPerFilterConfig[util.StatefulSessionFilter] = protoconv.MessageToAny(perRouteStatefulSession) 532 } 533 534 if opts.IsHTTP3AltSvcHeaderNeeded { 535 http3AltSvcHeader := buildHTTP3AltSvcHeader(listenPort, util.ALPNHttp3OverQUIC) 536 if out.ResponseHeadersToAdd == nil { 537 out.ResponseHeadersToAdd = make([]*core.HeaderValueOption, 0) 538 } 539 out.ResponseHeadersToAdd = append(out.ResponseHeadersToAdd, http3AltSvcHeader) 540 } 541 542 return out 543 } 544 545 func applyHTTPRouteDestination( 546 out *route.Route, 547 node *model.Proxy, 548 vs config.Config, 549 in *networking.HTTPRoute, 550 mesh *meshconfig.MeshConfig, 551 authority string, 552 serviceRegistry map[host.Name]*model.Service, 553 listenerPort int, 554 hashByDestination DestinationHashMap, 555 ) []host.Name { 556 action := &route.RouteAction{} 557 558 setTimeout(action, in.Timeout, node) 559 560 if model.UseGatewaySemantics(vs) { 561 // return 500 for invalid backends 562 // https://github.com/kubernetes-sigs/gateway-api/blob/cea484e38e078a2c1997d8c7a62f410a1540f519/apis/v1beta1/httproute_types.go#L204 563 action.ClusterNotFoundResponseCode = route.RouteAction_INTERNAL_SERVER_ERROR 564 } 565 566 out.Action = &route.Route_Route{Route: action} 567 568 if in.Rewrite != nil { 569 action.ClusterSpecifier = &route.RouteAction_Cluster{ 570 Cluster: in.Name, 571 } 572 573 if regexRewrite := in.Rewrite.GetUriRegexRewrite(); regexRewrite != nil { 574 action.RegexRewrite = &matcher.RegexMatchAndSubstitute{ 575 Pattern: &matcher.RegexMatcher{ 576 Regex: regexRewrite.Match, 577 }, 578 Substitution: regexRewrite.Rewrite, 579 } 580 } else if uri := in.Rewrite.GetUri(); uri != "" { 581 if model.UseGatewaySemantics(vs) && uri == "/" { 582 // remove the prefix 583 action.RegexRewrite = &matcher.RegexMatchAndSubstitute{ 584 Pattern: &matcher.RegexMatcher{ 585 Regex: fmt.Sprintf(`^%s(/?)(.*)`, regexp.QuoteMeta(out.Match.GetPathSeparatedPrefix())), 586 }, 587 // hold `/` in case the entire path is removed 588 Substitution: `/\2`, 589 } 590 } else { 591 action.PrefixRewrite = uri 592 } 593 } 594 if in.Rewrite.GetAuthority() != "" { 595 authority = in.Rewrite.GetAuthority() 596 } 597 } 598 if authority != "" { 599 action.HostRewriteSpecifier = &route.RouteAction_HostRewriteLiteral{ 600 HostRewriteLiteral: authority, 601 } 602 } 603 604 if in.Mirror != nil { 605 if mp := MirrorPercent(in); mp != nil { 606 action.RequestMirrorPolicies = append(action.RequestMirrorPolicies, 607 TranslateRequestMirrorPolicy(in.Mirror, serviceRegistry[host.Name(in.Mirror.Host)], listenerPort, mp)) 608 } 609 } 610 for _, mirror := range in.Mirrors { 611 if mp := MirrorPercentByPolicy(mirror); mp != nil && mirror.Destination != nil { 612 action.RequestMirrorPolicies = append(action.RequestMirrorPolicies, 613 TranslateRequestMirrorPolicy(mirror.Destination, serviceRegistry[host.Name(mirror.Destination.Host)], listenerPort, mp)) 614 } 615 } 616 617 var hostnames []host.Name 618 policy := in.Retries 619 if policy == nil { 620 // No VS policy set, use mesh defaults 621 policy = mesh.GetDefaultHttpRetryPolicy() 622 } 623 consistentHash := false 624 if len(in.Route) == 1 { 625 hostnames = append(hostnames, processDestination(in.Route[0], serviceRegistry, listenerPort, hashByDestination, out, action)) 626 hash := hashByDestination[in.Route[0]] 627 consistentHash = hash != nil 628 } else { 629 weighted := make([]*route.WeightedCluster_ClusterWeight, 0) 630 for _, dst := range in.Route { 631 if dst.Weight == 0 { 632 // Ignore 0 weighted clusters if there are other clusters in the route. 633 continue 634 } 635 destinationweight, hostname := processWeightedDestination(dst, serviceRegistry, listenerPort, hashByDestination, action) 636 weighted = append(weighted, destinationweight) 637 hostnames = append(hostnames, hostname) 638 } 639 action.ClusterSpecifier = &route.RouteAction_WeightedClusters{ 640 WeightedClusters: &route.WeightedCluster{ 641 Clusters: weighted, 642 }, 643 } 644 } 645 action.RetryPolicy = retry.ConvertPolicy(policy, consistentHash) 646 return hostnames 647 } 648 649 // processDestination processes a single destination in a route. It specifies to which cluster the route should 650 // be routed to. It also sets the headers and hash policy if specified. 651 // Returns the hostname of the destination. 652 func processDestination(dst *networking.HTTPRouteDestination, serviceRegistry map[host.Name]*model.Service, 653 listenerPort int, 654 hashByDestination DestinationHashMap, 655 out *route.Route, 656 action *route.RouteAction, 657 ) host.Name { 658 hostname := host.Name(dst.GetDestination().GetHost()) 659 action.ClusterSpecifier = &route.RouteAction_Cluster{ 660 Cluster: GetDestinationCluster(dst.Destination, serviceRegistry[hostname], listenerPort), 661 } 662 if dst.Headers != nil { 663 operations := TranslateHeadersOperations(dst.Headers) 664 out.RequestHeadersToAdd = append(out.RequestHeadersToAdd, operations.RequestHeadersToAdd...) 665 out.RequestHeadersToRemove = append(out.RequestHeadersToRemove, operations.RequestHeadersToRemove...) 666 out.ResponseHeadersToAdd = append(out.ResponseHeadersToAdd, operations.ResponseHeadersToAdd...) 667 out.ResponseHeadersToRemove = append(out.ResponseHeadersToRemove, operations.ResponseHeadersToRemove...) 668 if operations.Authority != "" && action.HostRewriteSpecifier == nil { 669 // Ideally, if the weighted cluster overwrites authority, it has precedence. This mirrors behavior of headers, 670 // because for headers we append the weighted last which allows it to Set and wipe out previous Adds. 671 // However, Envoy behavior is different when we set at both cluster level and route level, and we want 672 // behavior to be consistent with a single cluster and multiple clusters. 673 // As a result, we only override if the top level rewrite is not set 674 action.HostRewriteSpecifier = &route.RouteAction_HostRewriteLiteral{ 675 HostRewriteLiteral: operations.Authority, 676 } 677 } 678 } 679 hash := hashByDestination[dst] 680 hashPolicy := consistentHashToHashPolicy(hash) 681 if hashPolicy != nil { 682 action.HashPolicy = append(action.HashPolicy, hashPolicy) 683 } 684 return hostname 685 } 686 687 // processWeightedDestination processes a weighted destination in a route. It specifies to which cluster the route should 688 // be routed to. It also sets the headers and hash policy if specified. 689 // Returns the hostname of the destination along with its weight. 690 func processWeightedDestination(dst *networking.HTTPRouteDestination, serviceRegistry map[host.Name]*model.Service, 691 listenerPort int, 692 hashByDestination DestinationHashMap, 693 action *route.RouteAction, 694 ) (*route.WeightedCluster_ClusterWeight, host.Name) { 695 hostname := host.Name(dst.GetDestination().GetHost()) 696 clusterWeight := &route.WeightedCluster_ClusterWeight{ 697 Name: GetDestinationCluster(dst.Destination, serviceRegistry[hostname], listenerPort), 698 Weight: &wrapperspb.UInt32Value{Value: uint32(dst.Weight)}, 699 } 700 if dst.Headers != nil { 701 operations := TranslateHeadersOperations(dst.Headers) 702 // If weighted destination has headers, we need to set them on the cluster weight. 703 clusterWeight.RequestHeadersToAdd = operations.RequestHeadersToAdd 704 clusterWeight.RequestHeadersToRemove = operations.RequestHeadersToRemove 705 clusterWeight.ResponseHeadersToAdd = operations.ResponseHeadersToAdd 706 clusterWeight.ResponseHeadersToRemove = operations.ResponseHeadersToRemove 707 if operations.Authority != "" { 708 clusterWeight.HostRewriteSpecifier = &route.WeightedCluster_ClusterWeight_HostRewriteLiteral{ 709 HostRewriteLiteral: operations.Authority, 710 } 711 } 712 } 713 hash := hashByDestination[dst] 714 hashPolicy := consistentHashToHashPolicy(hash) 715 if hashPolicy != nil { 716 action.HashPolicy = append(action.HashPolicy, hashPolicy) 717 } 718 return clusterWeight, hostname 719 } 720 721 func ApplyRedirect(out *route.Route, redirect *networking.HTTPRedirect, port int, isTLS bool, useGatewaySemantics bool) { 722 action := &route.Route_Redirect{ 723 Redirect: &route.RedirectAction{ 724 HostRedirect: redirect.Authority, 725 PathRewriteSpecifier: &route.RedirectAction_PathRedirect{ 726 PathRedirect: redirect.Uri, 727 }, 728 }, 729 } 730 731 if useGatewaySemantics { 732 if uri, isPrefixReplace := cutPrefix(redirect.Uri, "%PREFIX()%"); isPrefixReplace { 733 action.Redirect.PathRewriteSpecifier = &route.RedirectAction_PrefixRewrite{ 734 PrefixRewrite: uri, 735 } 736 } 737 } 738 739 if redirect.Scheme != "" { 740 action.Redirect.SchemeRewriteSpecifier = &route.RedirectAction_SchemeRedirect{SchemeRedirect: redirect.Scheme} 741 } 742 743 if redirect.RedirectPort != nil { 744 switch rp := redirect.RedirectPort.(type) { 745 case *networking.HTTPRedirect_DerivePort: 746 if rp.DerivePort == networking.HTTPRedirect_FROM_REQUEST_PORT { 747 // Envoy doesn't actually support deriving the port from the request dynamically. However, 748 // we always generate routes in the context of a specific request port. As a result, we can just 749 // use that port 750 action.Redirect.PortRedirect = uint32(port) 751 } 752 // Otherwise, no port needed; HTTPRedirect_FROM_PROTOCOL_DEFAULT is Envoy's default behavior 753 case *networking.HTTPRedirect_Port: 754 action.Redirect.PortRedirect = rp.Port 755 } 756 scheme := redirect.Scheme 757 if scheme == "" { 758 if isTLS { 759 scheme = "https" 760 } else { 761 scheme = "http" 762 } 763 } 764 // Do not put explicit :80 or :443 when its http/https 765 if action.Redirect.PortRedirect == 80 && scheme == "http" { 766 action.Redirect.PortRedirect = 0 767 } 768 if action.Redirect.PortRedirect == 443 && scheme == "https" { 769 action.Redirect.PortRedirect = 0 770 } 771 } 772 773 switch redirect.RedirectCode { 774 case 0, 301: 775 action.Redirect.ResponseCode = route.RedirectAction_MOVED_PERMANENTLY 776 case 302: 777 action.Redirect.ResponseCode = route.RedirectAction_FOUND 778 case 303: 779 action.Redirect.ResponseCode = route.RedirectAction_SEE_OTHER 780 case 307: 781 action.Redirect.ResponseCode = route.RedirectAction_TEMPORARY_REDIRECT 782 case 308: 783 action.Redirect.ResponseCode = route.RedirectAction_PERMANENT_REDIRECT 784 default: 785 log.Warnf("Redirect Code %d is not yet supported", redirect.RedirectCode) 786 action = nil 787 } 788 789 out.Action = action 790 } 791 792 func ApplyDirectResponse(out *route.Route, directResponse *networking.HTTPDirectResponse) { 793 action := &route.Route_DirectResponse{ 794 DirectResponse: &route.DirectResponseAction{ 795 Status: directResponse.Status, 796 }, 797 } 798 799 if directResponse.Body != nil { 800 switch op := directResponse.Body.Specifier.(type) { 801 case *networking.HTTPBody_String_: 802 action.DirectResponse.Body = &core.DataSource{ 803 Specifier: &core.DataSource_InlineString{ 804 InlineString: op.String_, 805 }, 806 } 807 case *networking.HTTPBody_Bytes: 808 action.DirectResponse.Body = &core.DataSource{ 809 Specifier: &core.DataSource_InlineBytes{ 810 InlineBytes: op.Bytes, 811 }, 812 } 813 } 814 } 815 816 out.Action = action 817 } 818 819 func buildHTTP3AltSvcHeader(port int, h3Alpns []string) *core.HeaderValueOption { 820 // For example, www.cloudflare.com returns the following 821 // alt-svc: h3-27=":443"; ma=86400, h3-28=":443"; ma=86400, h3-29=":443"; ma=86400, h3=":443"; ma=86400 822 valParts := make([]string, 0, len(h3Alpns)) 823 for _, alpn := range h3Alpns { 824 // Max-age is hardcoded to 1 day for now. 825 valParts = append(valParts, fmt.Sprintf(`%s=":%d"; ma=86400`, alpn, port)) 826 } 827 headerVal := strings.Join(valParts, ", ") 828 return &core.HeaderValueOption{ 829 AppendAction: core.HeaderValueOption_APPEND_IF_EXISTS_OR_ADD, 830 Header: &core.HeaderValue{ 831 Key: util.AltSvcHeader, 832 Value: headerVal, 833 }, 834 } 835 } 836 837 // SortHeaderValueOption type and the functions below (Len, Less and Swap) are for sort.Stable for type HeaderValueOption 838 type SortHeaderValueOption []*core.HeaderValueOption 839 840 // MirrorPercent computes the mirror percent to be used based on "Mirror" data in route. 841 func MirrorPercent(in *networking.HTTPRoute) *core.RuntimeFractionalPercent { 842 switch { 843 case in.MirrorPercentage != nil: 844 if in.MirrorPercentage.GetValue() > 0 { 845 return &core.RuntimeFractionalPercent{ 846 DefaultValue: translatePercentToFractionalPercent(in.MirrorPercentage), 847 } 848 } 849 // If zero percent is provided explicitly, we should not mirror. 850 return nil 851 // nolint: staticcheck 852 case in.MirrorPercent != nil: 853 if in.MirrorPercent.GetValue() > 0 { 854 return &core.RuntimeFractionalPercent{ 855 DefaultValue: translateIntegerToFractionalPercent((int32(in.MirrorPercent.GetValue()))), 856 } 857 } 858 // If zero percent is provided explicitly, we should not mirror. 859 return nil 860 default: 861 // Default to 100 percent if percent is not given. 862 return &core.RuntimeFractionalPercent{ 863 DefaultValue: translateIntegerToFractionalPercent(100), 864 } 865 } 866 } 867 868 // MirrorPercentByPolicy computes the mirror percent to be used based on HTTPMirrorPolicy. 869 func MirrorPercentByPolicy(mirror *networking.HTTPMirrorPolicy) *core.RuntimeFractionalPercent { 870 switch { 871 case mirror.Percentage != nil: 872 if mirror.Percentage.GetValue() > 0 { 873 return &core.RuntimeFractionalPercent{ 874 DefaultValue: translatePercentToFractionalPercent(mirror.Percentage), 875 } 876 } 877 // If zero percent is provided explicitly, we should not mirror. 878 return nil 879 default: 880 // Default to 100 percent if percent is not given. 881 return &core.RuntimeFractionalPercent{ 882 DefaultValue: translateIntegerToFractionalPercent(100), 883 } 884 } 885 } 886 887 // Len is i the sort.Interface for SortHeaderValueOption 888 func (b SortHeaderValueOption) Len() int { 889 return len(b) 890 } 891 892 // Less is in the sort.Interface for SortHeaderValueOption 893 func (b SortHeaderValueOption) Less(i, j int) bool { 894 if b[i] == nil || b[i].Header == nil { 895 return false 896 } else if b[j] == nil || b[j].Header == nil { 897 return true 898 } 899 return strings.Compare(b[i].Header.Key, b[j].Header.Key) < 0 900 } 901 902 // Swap is in the sort.Interface for SortHeaderValueOption 903 func (b SortHeaderValueOption) Swap(i, j int) { 904 b[i], b[j] = b[j], b[i] 905 } 906 907 // translateAppendHeaders translates headers 908 func translateAppendHeaders(headers map[string]string, appendFlag bool) ([]*core.HeaderValueOption, string) { 909 if len(headers) == 0 { 910 return nil, "" 911 } 912 authority := "" 913 headerValueOptionList := make([]*core.HeaderValueOption, 0, len(headers)) 914 for key, value := range headers { 915 if isAuthorityHeader(key) { 916 // If there are multiple, last one wins; validation will reject 917 authority = value 918 } 919 if isInternalHeader(key) { 920 continue 921 } 922 headerValueOption := &core.HeaderValueOption{ 923 Header: &core.HeaderValue{ 924 Key: key, 925 Value: value, 926 }, 927 } 928 if appendFlag { 929 headerValueOption.AppendAction = core.HeaderValueOption_APPEND_IF_EXISTS_OR_ADD 930 } else { 931 headerValueOption.AppendAction = core.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD 932 } 933 headerValueOptionList = append(headerValueOptionList, headerValueOption) 934 } 935 sort.Stable(SortHeaderValueOption(headerValueOptionList)) 936 return headerValueOptionList, authority 937 } 938 939 type HeadersOperations struct { 940 RequestHeadersToAdd []*core.HeaderValueOption 941 ResponseHeadersToAdd []*core.HeaderValueOption 942 RequestHeadersToRemove []string 943 ResponseHeadersToRemove []string 944 Authority string 945 } 946 947 // isInternalHeader returns true if a header refers to an internal value that cannot be modified by Envoy 948 func isInternalHeader(headerKey string) bool { 949 return strings.HasPrefix(headerKey, ":") || strings.EqualFold(headerKey, "host") 950 } 951 952 // isAuthorityHeader returns true if a header refers to the authority header 953 func isAuthorityHeader(headerKey string) bool { 954 return strings.EqualFold(headerKey, ":authority") || strings.EqualFold(headerKey, "host") 955 } 956 957 func dropInternal(keys []string) []string { 958 result := make([]string, 0, len(keys)) 959 for _, k := range keys { 960 if isInternalHeader(k) { 961 continue 962 } 963 result = append(result, k) 964 } 965 return result 966 } 967 968 // TranslateHeadersOperations translates headers operations 969 func TranslateHeadersOperations(headers *networking.Headers) HeadersOperations { 970 req := headers.GetRequest() 971 resp := headers.GetResponse() 972 973 requestHeadersToAdd, setAuthority := translateAppendHeaders(req.GetSet(), false) 974 reqAdd, addAuthority := translateAppendHeaders(req.GetAdd(), true) 975 requestHeadersToAdd = append(requestHeadersToAdd, reqAdd...) 976 977 responseHeadersToAdd, _ := translateAppendHeaders(resp.GetSet(), false) 978 respAdd, _ := translateAppendHeaders(resp.GetAdd(), true) 979 responseHeadersToAdd = append(responseHeadersToAdd, respAdd...) 980 981 auth := addAuthority 982 if setAuthority != "" { 983 // If authority is set in 'add' and 'set', pick the one from 'set' 984 auth = setAuthority 985 } 986 return HeadersOperations{ 987 RequestHeadersToAdd: requestHeadersToAdd, 988 ResponseHeadersToAdd: responseHeadersToAdd, 989 RequestHeadersToRemove: dropInternal(req.GetRemove()), 990 ResponseHeadersToRemove: dropInternal(resp.GetRemove()), 991 Authority: auth, 992 } 993 } 994 995 // TranslateRouteMatch translates match condition 996 func TranslateRouteMatch(vs config.Config, in *networking.HTTPMatchRequest, useExtendedJwt bool) *route.RouteMatch { 997 out := &route.RouteMatch{PathSpecifier: &route.RouteMatch_Prefix{Prefix: "/"}} 998 if in == nil { 999 return out 1000 } 1001 1002 for name, stringMatch := range in.Headers { 1003 // The metadata matcher takes precedence over the header matcher. 1004 if metadataMatcher := translateMetadataMatch(name, stringMatch, useExtendedJwt); metadataMatcher != nil { 1005 out.DynamicMetadata = append(out.DynamicMetadata, metadataMatcher) 1006 } else { 1007 matcher := translateHeaderMatch(name, stringMatch) 1008 out.Headers = append(out.Headers, matcher) 1009 } 1010 } 1011 1012 for name, stringMatch := range in.WithoutHeaders { 1013 if metadataMatcher := translateMetadataMatch(name, stringMatch, useExtendedJwt); metadataMatcher != nil { 1014 metadataMatcher.Invert = true 1015 out.DynamicMetadata = append(out.DynamicMetadata, metadataMatcher) 1016 } else { 1017 matcher := translateHeaderMatch(name, stringMatch) 1018 matcher.InvertMatch = true 1019 // treat_missing_header_as_empty conflict with present_match 1020 if !canBeConvertedToPresentMatch(stringMatch) { 1021 matcher.TreatMissingHeaderAsEmpty = true 1022 } 1023 out.Headers = append(out.Headers, matcher) 1024 } 1025 } 1026 1027 // guarantee ordering of headers 1028 sort.Slice(out.Headers, func(i, j int) bool { 1029 return out.Headers[i].Name < out.Headers[j].Name 1030 }) 1031 1032 if in.Uri != nil { 1033 switch m := in.Uri.MatchType.(type) { 1034 case *networking.StringMatch_Exact: 1035 out.PathSpecifier = &route.RouteMatch_Path{Path: m.Exact} 1036 case *networking.StringMatch_Prefix: 1037 if (model.UseIngressSemantics(vs) || model.UseGatewaySemantics(vs)) && m.Prefix != "/" { 1038 path := strings.TrimSuffix(m.Prefix, "/") 1039 out.PathSpecifier = &route.RouteMatch_PathSeparatedPrefix{PathSeparatedPrefix: path} 1040 } else { 1041 out.PathSpecifier = &route.RouteMatch_Prefix{Prefix: m.Prefix} 1042 } 1043 case *networking.StringMatch_Regex: 1044 out.PathSpecifier = &route.RouteMatch_SafeRegex{ 1045 SafeRegex: &matcher.RegexMatcher{ 1046 Regex: m.Regex, 1047 }, 1048 } 1049 } 1050 } 1051 1052 out.CaseSensitive = &wrapperspb.BoolValue{Value: !in.IgnoreUriCase} 1053 1054 if in.Method != nil { 1055 matcher := translateHeaderMatch(HeaderMethod, in.Method) 1056 out.Headers = append(out.Headers, matcher) 1057 } 1058 1059 if in.Authority != nil { 1060 matcher := translateHeaderMatch(HeaderAuthority, in.Authority) 1061 out.Headers = append(out.Headers, matcher) 1062 } 1063 1064 if in.Scheme != nil { 1065 matcher := translateHeaderMatch(HeaderScheme, in.Scheme) 1066 out.Headers = append(out.Headers, matcher) 1067 } 1068 1069 for name, stringMatch := range in.QueryParams { 1070 matcher := translateQueryParamMatch(name, stringMatch) 1071 out.QueryParameters = append(out.QueryParameters, matcher) 1072 } 1073 1074 return out 1075 } 1076 1077 // translateQueryParamMatch translates a StringMatch to a QueryParameterMatcher. 1078 func translateQueryParamMatch(name string, in *networking.StringMatch) *route.QueryParameterMatcher { 1079 out := &route.QueryParameterMatcher{ 1080 Name: name, 1081 } 1082 1083 if canBeConvertedToPresentMatch(in) { 1084 out.QueryParameterMatchSpecifier = &route.QueryParameterMatcher_PresentMatch{ 1085 PresentMatch: true, 1086 } 1087 return out 1088 } 1089 1090 if em := util.ConvertToEnvoyMatch(in); em != nil { 1091 out.QueryParameterMatchSpecifier = &route.QueryParameterMatcher_StringMatch{ 1092 StringMatch: em, 1093 } 1094 } 1095 1096 return out 1097 } 1098 1099 // canBeConvertedToPresentMatch determines if the given matcher can be converted to present_match or not. 1100 // Currently, if the regex is "*" value, it returns true 1101 func canBeConvertedToPresentMatch(in *networking.StringMatch) bool { 1102 if in == nil || in.MatchType == nil { 1103 return true 1104 } 1105 1106 catchall := false 1107 1108 switch m := in.MatchType.(type) { 1109 case *networking.StringMatch_Regex: 1110 // `*` is NOT a RE2 style regex, it's a metacharacter. 1111 // It will be translated as present_match, rather than matching "any string". 1112 // see https://github.com/istio/istio/pull/20629 1113 catchall = m.Regex == "*" 1114 } 1115 1116 return catchall 1117 } 1118 1119 // translateMetadataMatch translates a header match to dynamic metadata matcher. Returns nil if the header is not supported 1120 // or the header format is invalid for generating metadata matcher. 1121 // 1122 // The currently only supported header is @request.auth.claims for JWT claims matching. Claims of type string or list of string 1123 // are supported and nested claims are also supported using `.` or `[]` as a separator for claim names, `[]` is recommended. 1124 // 1125 // Examples using `.` as a separator: 1126 // - `@request.auth.claims.admin` matches the claim "admin". 1127 // - `@request.auth.claims.group.id` matches the nested claims "group" and "id". 1128 // 1129 // Examples using `[]` as a separator: 1130 // - `@request.auth.claims[admin]` matches the claim "admin". 1131 // - `@request.auth.claims[group][id]` matches the nested claims "group" and "id". 1132 func translateMetadataMatch(name string, in *networking.StringMatch, useExtendedJwt bool) *matcher.MetadataMatcher { 1133 rc := jwt.ToRoutingClaim(name) 1134 if !rc.Match { 1135 return nil 1136 } 1137 return authz.MetadataMatcherForJWTClaims(rc.Claims, util.ConvertToEnvoyMatch(in), useExtendedJwt) 1138 } 1139 1140 // translateHeaderMatch translates to HeaderMatcher 1141 func translateHeaderMatch(name string, in *networking.StringMatch) *route.HeaderMatcher { 1142 out := &route.HeaderMatcher{ 1143 Name: name, 1144 } 1145 1146 if canBeConvertedToPresentMatch(in) { 1147 out.HeaderMatchSpecifier = &route.HeaderMatcher_PresentMatch{PresentMatch: true} 1148 return out 1149 } 1150 1151 if em := util.ConvertToEnvoyMatch(in); em != nil { 1152 out.HeaderMatchSpecifier = &route.HeaderMatcher_StringMatch{ 1153 StringMatch: em, 1154 } 1155 } 1156 1157 return out 1158 } 1159 1160 func forwardNotMatchingPreflights(cors *networking.CorsPolicy) *wrapperspb.BoolValue { 1161 if cors.GetUnmatchedPreflights() == networking.CorsPolicy_IGNORE { 1162 return wrapperspb.Bool(false) 1163 } 1164 1165 // This is the default behavior before envoy 1.30. 1166 return wrapperspb.Bool(true) 1167 } 1168 1169 // TranslateCORSPolicy translates CORS policy 1170 func TranslateCORSPolicy(proxy *model.Proxy, in *networking.CorsPolicy) *cors.CorsPolicy { 1171 if in == nil { 1172 return nil 1173 } 1174 1175 // CORS filter is enabled by default 1176 out := cors.CorsPolicy{} 1177 // Start from Envoy 1.30(istio 1.22), cors filter will not forward preflight requests to upstream by default. 1178 // Istio start support this feature from 1.23. 1179 if proxy.VersionGreaterAndEqual(&model.IstioVersion{Major: 1, Minor: 23, Patch: -1}) { 1180 out.ForwardNotMatchingPreflights = forwardNotMatchingPreflights(in) 1181 } 1182 1183 // nolint: staticcheck 1184 if in.AllowOrigins != nil { 1185 out.AllowOriginStringMatch = util.ConvertToEnvoyMatches(in.AllowOrigins) 1186 } else if in.AllowOrigin != nil { 1187 out.AllowOriginStringMatch = util.StringToExactMatch(in.AllowOrigin) 1188 } 1189 1190 out.FilterEnabled = &core.RuntimeFractionalPercent{ 1191 DefaultValue: &xdstype.FractionalPercent{ 1192 Numerator: 100, 1193 Denominator: xdstype.FractionalPercent_HUNDRED, 1194 }, 1195 } 1196 1197 out.AllowCredentials = in.AllowCredentials 1198 out.AllowHeaders = strings.Join(in.AllowHeaders, ",") 1199 out.AllowMethods = strings.Join(in.AllowMethods, ",") 1200 out.ExposeHeaders = strings.Join(in.ExposeHeaders, ",") 1201 if in.MaxAge != nil { 1202 out.MaxAge = strconv.FormatInt(in.MaxAge.GetSeconds(), 10) 1203 } 1204 return &out 1205 } 1206 1207 // GetRouteOperation returns readable route description for trace. 1208 func GetRouteOperation(in *route.Route, vsName string, port int) string { 1209 path := "/*" 1210 m := in.GetMatch() 1211 ps := m.GetPathSpecifier() 1212 if ps != nil { 1213 switch ps.(type) { 1214 case *route.RouteMatch_Prefix: 1215 path = m.GetPrefix() + "*" 1216 case *route.RouteMatch_Path: 1217 path = m.GetPath() 1218 case *route.RouteMatch_SafeRegex: 1219 path = m.GetSafeRegex().GetRegex() 1220 } 1221 } 1222 1223 // If there is only one destination cluster in route, return host:port/uri as description of route. 1224 // Otherwise there are multiple destination clusters and destination host is not clear. For that case 1225 // return virtual service name:port/uri as substitute. 1226 if c := in.GetRoute().GetCluster(); model.IsValidSubsetKey(c) { 1227 // Parse host and port from cluster name. 1228 _, _, h, p := model.ParseSubsetKey(c) 1229 return string(h) + ":" + strconv.Itoa(p) + path 1230 } 1231 return vsName + ":" + strconv.Itoa(port) + path 1232 } 1233 1234 // BuildDefaultHTTPInboundRoute builds a default inbound route. 1235 func BuildDefaultHTTPInboundRoute(clusterName string, operation string) *route.Route { 1236 out := buildDefaultHTTPRoute(clusterName, operation) 1237 // For inbound, configure with notimeout. 1238 out.GetRoute().Timeout = Notimeout 1239 out.GetRoute().MaxStreamDuration = &route.RouteAction_MaxStreamDuration{ 1240 MaxStreamDuration: Notimeout, 1241 // If not configured at all, the grpc-timeout header is not used and 1242 // gRPC requests time out like any other requests using timeout or its default. 1243 GrpcTimeoutHeaderMax: Notimeout, 1244 } 1245 return out 1246 } 1247 1248 func buildDefaultHTTPRoute(clusterName string, operation string) *route.Route { 1249 routeAction := &route.RouteAction{ 1250 ClusterSpecifier: &route.RouteAction_Cluster{Cluster: clusterName}, 1251 } 1252 val := &route.Route{ 1253 Match: TranslateRouteMatch(config.Config{}, nil, true), 1254 Decorator: &route.Decorator{ 1255 Operation: operation, 1256 }, 1257 Action: &route.Route_Route{ 1258 Route: routeAction, 1259 }, 1260 } 1261 1262 val.Name = DefaultRouteName 1263 return val 1264 } 1265 1266 // setTimeout sets timeout for a route. 1267 func setTimeout(action *route.RouteAction, vsTimeout *durationpb.Duration, node *model.Proxy) { 1268 // Configure timeouts specified by Virtual Service if they are provided, otherwise set it to defaults. 1269 action.Timeout = Notimeout 1270 if vsTimeout != nil { 1271 action.Timeout = vsTimeout 1272 } 1273 if node != nil && node.IsProxylessGrpc() { 1274 // TODO(stevenctl) merge these paths; grpc's xDS impl will not read the deprecated value 1275 action.MaxStreamDuration = &route.RouteAction_MaxStreamDuration{ 1276 MaxStreamDuration: action.Timeout, 1277 } 1278 } else { 1279 // If not configured at all, the grpc-timeout header is not used and 1280 // gRPC requests time out like any other requests using timeout or its default. 1281 // Use deprecated value for now as the replacement MaxStreamDuration has some regressions. 1282 // nolint: staticcheck 1283 if action.Timeout.AsDuration().Nanoseconds() == 0 { 1284 action.MaxGrpcTimeout = Notimeout 1285 } else { 1286 action.MaxGrpcTimeout = action.Timeout 1287 } 1288 } 1289 } 1290 1291 // BuildDefaultHTTPOutboundRoute builds a default outbound route, including a retry policy. 1292 func BuildDefaultHTTPOutboundRoute(clusterName string, operation string, mesh *meshconfig.MeshConfig) *route.Route { 1293 out := buildDefaultHTTPRoute(clusterName, operation) 1294 // Add a default retry policy for outbound routes. 1295 out.GetRoute().RetryPolicy = retry.ConvertPolicy(mesh.GetDefaultHttpRetryPolicy(), false) 1296 setTimeout(out.GetRoute(), nil, nil) 1297 return out 1298 } 1299 1300 // translatePercentToFractionalPercent translates an v1alpha3 Percent instance 1301 // to an envoy.type.FractionalPercent instance. 1302 func translatePercentToFractionalPercent(p *networking.Percent) *xdstype.FractionalPercent { 1303 return &xdstype.FractionalPercent{ 1304 Numerator: uint32(p.Value * 10000), 1305 Denominator: xdstype.FractionalPercent_MILLION, 1306 } 1307 } 1308 1309 // translateIntegerToFractionalPercent translates an int32 instance to an 1310 // envoy.type.FractionalPercent instance. 1311 func translateIntegerToFractionalPercent(p int32) *xdstype.FractionalPercent { 1312 return &xdstype.FractionalPercent{ 1313 Numerator: uint32(p), 1314 Denominator: xdstype.FractionalPercent_HUNDRED, 1315 } 1316 } 1317 1318 // TranslateFault translates networking.HTTPFaultInjection into Envoy's HTTPFault 1319 func TranslateFault(in *networking.HTTPFaultInjection) *xdshttpfault.HTTPFault { 1320 if in == nil { 1321 return nil 1322 } 1323 1324 out := xdshttpfault.HTTPFault{} 1325 if in.Delay != nil { 1326 out.Delay = &xdsfault.FaultDelay{} 1327 if in.Delay.Percentage != nil { 1328 out.Delay.Percentage = translatePercentToFractionalPercent(in.Delay.Percentage) 1329 } else { 1330 out.Delay.Percentage = translateIntegerToFractionalPercent(in.Delay.Percent) // nolint: staticcheck 1331 } 1332 switch d := in.Delay.HttpDelayType.(type) { 1333 case *networking.HTTPFaultInjection_Delay_FixedDelay: 1334 out.Delay.FaultDelaySecifier = &xdsfault.FaultDelay_FixedDelay{ 1335 FixedDelay: d.FixedDelay, 1336 } 1337 default: 1338 log.Warnf("Exponential faults are not yet supported") 1339 out.Delay = nil 1340 } 1341 } 1342 1343 if in.Abort != nil { 1344 out.Abort = &xdshttpfault.FaultAbort{} 1345 if in.Abort.Percentage != nil { 1346 out.Abort.Percentage = translatePercentToFractionalPercent(in.Abort.Percentage) 1347 } 1348 switch a := in.Abort.ErrorType.(type) { 1349 case *networking.HTTPFaultInjection_Abort_HttpStatus: 1350 out.Abort.ErrorType = &xdshttpfault.FaultAbort_HttpStatus{ 1351 HttpStatus: uint32(a.HttpStatus), 1352 } 1353 case *networking.HTTPFaultInjection_Abort_GrpcStatus: 1354 // We wouldn't have an unknown gRPC code here. This is because 1355 // the validation webhook would have already caught the invalid 1356 // code and we wouldn't reach here. 1357 out.Abort.ErrorType = &xdshttpfault.FaultAbort_GrpcStatus{ 1358 GrpcStatus: uint32(grpc.SupportedGRPCStatus[a.GrpcStatus]), 1359 } 1360 default: 1361 log.Warnf("Only HTTP and gRPC type abort faults are supported") 1362 out.Abort = nil 1363 } 1364 } 1365 1366 if out.Delay == nil && out.Abort == nil { 1367 return nil 1368 } 1369 1370 return &out 1371 } 1372 1373 func TranslateRequestMirrorPolicy(dst *networking.Destination, service *model.Service, 1374 listenerPort int, mp *core.RuntimeFractionalPercent, 1375 ) *route.RouteAction_RequestMirrorPolicy { 1376 return &route.RouteAction_RequestMirrorPolicy{ 1377 Cluster: GetDestinationCluster(dst, service, listenerPort), 1378 RuntimeFraction: mp, 1379 TraceSampled: &wrapperspb.BoolValue{Value: false}, 1380 } 1381 } 1382 1383 func portLevelSettingsConsistentHash(dst *networking.Destination, 1384 pls []*networking.TrafficPolicy_PortTrafficPolicy, 1385 ) *networking.LoadBalancerSettings_ConsistentHashLB { 1386 if dst.Port != nil { 1387 portNumber := dst.GetPort().GetNumber() 1388 for _, setting := range pls { 1389 number := setting.GetPort().GetNumber() 1390 if number == portNumber { 1391 return setting.GetLoadBalancer().GetConsistentHash() 1392 } 1393 } 1394 } 1395 1396 return nil 1397 } 1398 1399 func consistentHashToHashPolicy(consistentHash *networking.LoadBalancerSettings_ConsistentHashLB) *route.RouteAction_HashPolicy { 1400 switch consistentHash.GetHashKey().(type) { 1401 case *networking.LoadBalancerSettings_ConsistentHashLB_HttpHeaderName: 1402 return &route.RouteAction_HashPolicy{ 1403 PolicySpecifier: &route.RouteAction_HashPolicy_Header_{ 1404 Header: &route.RouteAction_HashPolicy_Header{ 1405 HeaderName: consistentHash.GetHttpHeaderName(), 1406 }, 1407 }, 1408 } 1409 case *networking.LoadBalancerSettings_ConsistentHashLB_HttpCookie: 1410 cookie := consistentHash.GetHttpCookie() 1411 var ttl *durationpb.Duration 1412 if cookie.GetTtl() != nil { 1413 ttl = cookie.GetTtl() 1414 } 1415 return &route.RouteAction_HashPolicy{ 1416 PolicySpecifier: &route.RouteAction_HashPolicy_Cookie_{ 1417 Cookie: &route.RouteAction_HashPolicy_Cookie{ 1418 Name: cookie.GetName(), 1419 Ttl: ttl, 1420 Path: cookie.GetPath(), 1421 }, 1422 }, 1423 } 1424 case *networking.LoadBalancerSettings_ConsistentHashLB_UseSourceIp: 1425 return &route.RouteAction_HashPolicy{ 1426 PolicySpecifier: &route.RouteAction_HashPolicy_ConnectionProperties_{ 1427 ConnectionProperties: &route.RouteAction_HashPolicy_ConnectionProperties{ 1428 SourceIp: consistentHash.GetUseSourceIp(), 1429 }, 1430 }, 1431 } 1432 case *networking.LoadBalancerSettings_ConsistentHashLB_HttpQueryParameterName: 1433 return &route.RouteAction_HashPolicy{ 1434 PolicySpecifier: &route.RouteAction_HashPolicy_QueryParameter_{ 1435 QueryParameter: &route.RouteAction_HashPolicy_QueryParameter{ 1436 Name: consistentHash.GetHttpQueryParameterName(), 1437 }, 1438 }, 1439 } 1440 } 1441 return nil 1442 } 1443 1444 func hashForService(push *model.PushContext, 1445 node *model.Proxy, 1446 svc *model.Service, 1447 port *model.Port, 1448 ) (*networking.LoadBalancerSettings_ConsistentHashLB, *model.ConsolidatedDestRule) { 1449 if push == nil { 1450 return nil, nil 1451 } 1452 mergedDR := node.SidecarScope.DestinationRule(model.TrafficDirectionOutbound, node, svc.Hostname) 1453 destinationRule := mergedDR.GetRule() 1454 if destinationRule == nil { 1455 return nil, nil 1456 } 1457 rule := destinationRule.Spec.(*networking.DestinationRule) 1458 consistentHash := rule.GetTrafficPolicy().GetLoadBalancer().GetConsistentHash() 1459 portLevelSettings := rule.GetTrafficPolicy().GetPortLevelSettings() 1460 for _, setting := range portLevelSettings { 1461 number := setting.GetPort().GetNumber() 1462 if int(number) == port.Port { 1463 if setting.GetLoadBalancer().GetConsistentHash() != nil { 1464 consistentHash = setting.GetLoadBalancer().GetConsistentHash() 1465 } 1466 break 1467 } 1468 } 1469 1470 return consistentHash, mergedDR 1471 } 1472 1473 func hashForVirtualService(push *model.PushContext, 1474 node *model.Proxy, 1475 virtualService config.Config, 1476 ) (DestinationHashMap, []*model.ConsolidatedDestRule) { 1477 hashByDestination := DestinationHashMap{} 1478 destinationRules := make([]*model.ConsolidatedDestRule, 0) 1479 for _, httpRoute := range virtualService.Spec.(*networking.VirtualService).Http { 1480 for _, destination := range httpRoute.Route { 1481 hash, dr := hashForHTTPDestination(push, node, destination) 1482 if hash != nil { 1483 hashByDestination[destination] = hash 1484 destinationRules = append(destinationRules, dr) 1485 } 1486 } 1487 } 1488 return hashByDestination, destinationRules 1489 } 1490 1491 func GetConsistentHashForVirtualService(push *model.PushContext, node *model.Proxy, virtualService config.Config) DestinationHashMap { 1492 hashByDestination, _ := hashForVirtualService(push, node, virtualService) 1493 return hashByDestination 1494 } 1495 1496 // hashForHTTPDestination return the ConsistentHashLB and the DestinationRule associated with HTTP route destination. 1497 func hashForHTTPDestination(push *model.PushContext, node *model.Proxy, 1498 dst *networking.HTTPRouteDestination, 1499 ) (*networking.LoadBalancerSettings_ConsistentHashLB, *model.ConsolidatedDestRule) { 1500 if push == nil { 1501 return nil, nil 1502 } 1503 1504 destination := dst.GetDestination() 1505 mergedDR := node.SidecarScope.DestinationRule(model.TrafficDirectionOutbound, node, host.Name(destination.Host)) 1506 destinationRule := mergedDR.GetRule() 1507 if destinationRule == nil { 1508 return nil, nil 1509 } 1510 1511 rule := destinationRule.Spec.(*networking.DestinationRule) 1512 1513 consistentHash := rule.GetTrafficPolicy().GetLoadBalancer().GetConsistentHash() 1514 portLevelSettings := rule.GetTrafficPolicy().GetPortLevelSettings() 1515 plsHash := portLevelSettingsConsistentHash(destination, portLevelSettings) 1516 1517 var subsetHash, subsetPLSHash *networking.LoadBalancerSettings_ConsistentHashLB 1518 for _, subset := range rule.GetSubsets() { 1519 if subset.GetName() == destination.GetSubset() { 1520 subsetPortLevelSettings := subset.GetTrafficPolicy().GetPortLevelSettings() 1521 subsetHash = subset.GetTrafficPolicy().GetLoadBalancer().GetConsistentHash() 1522 subsetPLSHash = portLevelSettingsConsistentHash(destination, subsetPortLevelSettings) 1523 break 1524 } 1525 } 1526 1527 switch { 1528 case subsetPLSHash != nil: 1529 consistentHash = subsetPLSHash 1530 case subsetHash != nil: 1531 consistentHash = subsetHash 1532 case plsHash != nil: 1533 consistentHash = plsHash 1534 } 1535 return consistentHash, mergedDR 1536 } 1537 1538 // SortVHostRoutes moves the catch all routes alone to the end, while retaining 1539 // the relative order of other routes in the slice. 1540 func SortVHostRoutes(routes []*route.Route) []*route.Route { 1541 allroutes := make([]*route.Route, 0, len(routes)) 1542 catchAllRoutes := make([]*route.Route, 0) 1543 for _, r := range routes { 1544 if IsCatchAllRoute(r) { 1545 catchAllRoutes = append(catchAllRoutes, r) 1546 } else { 1547 allroutes = append(allroutes, r) 1548 } 1549 } 1550 return append(allroutes, catchAllRoutes...) 1551 } 1552 1553 // IsCatchAllRoute returns true if an Envoy route is a catchall route otherwise false. 1554 func IsCatchAllRoute(r *route.Route) bool { 1555 catchall := false 1556 // A Match is catch all if and only if it has no header/query param match 1557 // and URI has a prefix `/` or regex `.*`. 1558 switch ir := r.Match.PathSpecifier.(type) { 1559 case *route.RouteMatch_Prefix: 1560 catchall = ir.Prefix == "/" 1561 case *route.RouteMatch_PathSeparatedPrefix: 1562 catchall = ir.PathSeparatedPrefix == "/" 1563 case *route.RouteMatch_SafeRegex: 1564 catchall = ir.SafeRegex.GetRegex() == ".*" 1565 } 1566 1567 return catchall && len(r.Match.Headers) == 0 && len(r.Match.QueryParameters) == 0 && len(r.Match.DynamicMetadata) == 0 1568 } 1569 1570 func cutPrefix(s, prefix string) (after string, found bool) { 1571 if !strings.HasPrefix(s, prefix) { 1572 return s, false 1573 } 1574 return s[len(prefix):], true 1575 }