istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/networking/core/httproute.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 "fmt" 19 "net" 20 "sort" 21 "strconv" 22 "strings" 23 24 route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 25 statefulsession "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/stateful_session/v3" 26 hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" 27 discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 28 anypb "google.golang.org/protobuf/types/known/anypb" 29 "google.golang.org/protobuf/types/known/durationpb" 30 wrappers "google.golang.org/protobuf/types/known/wrapperspb" 31 32 meshconfig "istio.io/api/mesh/v1alpha1" 33 networking "istio.io/api/networking/v1alpha3" 34 "istio.io/istio/pilot/pkg/features" 35 "istio.io/istio/pilot/pkg/model" 36 istionetworking "istio.io/istio/pilot/pkg/networking" 37 "istio.io/istio/pilot/pkg/networking/core/envoyfilter" 38 istio_route "istio.io/istio/pilot/pkg/networking/core/route" 39 "istio.io/istio/pilot/pkg/networking/telemetry" 40 "istio.io/istio/pilot/pkg/networking/util" 41 "istio.io/istio/pilot/pkg/serviceregistry/provider" 42 "istio.io/istio/pilot/pkg/util/protoconv" 43 "istio.io/istio/pkg/config" 44 "istio.io/istio/pkg/config/constants" 45 "istio.io/istio/pkg/config/host" 46 "istio.io/istio/pkg/config/protocol" 47 "istio.io/istio/pkg/proto" 48 "istio.io/istio/pkg/slices" 49 "istio.io/istio/pkg/util/sets" 50 ) 51 52 const ( 53 wildcardDomainPrefix = "*." 54 inboundVirtualHostPrefix = string(model.TrafficDirectionInbound) + "|http|" 55 ) 56 57 // BuildHTTPRoutes produces a list of routes for the proxy 58 func (configgen *ConfigGeneratorImpl) BuildHTTPRoutes( 59 node *model.Proxy, 60 req *model.PushRequest, 61 routeNames []string, 62 ) ([]*discovery.Resource, model.XdsLogDetails) { 63 var routeConfigurations model.Resources 64 65 efw := req.Push.EnvoyFilters(node) 66 hit, miss := 0, 0 67 switch node.Type { 68 case model.SidecarProxy, model.Waypoint: 69 vHostCache := make(map[int][]*route.VirtualHost) 70 // dependent envoyfilters' key, calculate in front once to prevent calc for each route. 71 envoyfilterKeys := efw.KeysApplyingTo( 72 networking.EnvoyFilter_ROUTE_CONFIGURATION, 73 networking.EnvoyFilter_VIRTUAL_HOST, 74 networking.EnvoyFilter_HTTP_ROUTE, 75 ) 76 for _, routeName := range routeNames { 77 rc, cached := configgen.buildSidecarOutboundHTTPRouteConfig(node, req, routeName, vHostCache, efw, envoyfilterKeys) 78 if cached && !features.EnableUnsafeAssertions { 79 hit++ 80 } else { 81 miss++ 82 } 83 if rc == nil { 84 emptyRoute := &route.RouteConfiguration{ 85 Name: routeName, 86 VirtualHosts: []*route.VirtualHost{}, 87 ValidateClusters: proto.BoolFalse, 88 } 89 rc = &discovery.Resource{ 90 Name: routeName, 91 Resource: protoconv.MessageToAny(emptyRoute), 92 } 93 } 94 routeConfigurations = append(routeConfigurations, rc) 95 } 96 case model.Router: 97 for _, routeName := range routeNames { 98 rc := configgen.buildGatewayHTTPRouteConfig(node, req.Push, routeName) 99 if rc != nil { 100 rc = envoyfilter.ApplyRouteConfigurationPatches(networking.EnvoyFilter_GATEWAY, node, efw, rc) 101 resource := &discovery.Resource{ 102 Name: routeName, 103 Resource: protoconv.MessageToAny(rc), 104 } 105 routeConfigurations = append(routeConfigurations, resource) 106 } 107 } 108 } 109 if !features.EnableRDSCaching { 110 return routeConfigurations, model.DefaultXdsLogDetails 111 } 112 return routeConfigurations, model.XdsLogDetails{AdditionalInfo: fmt.Sprintf("cached:%v/%v", hit, hit+miss)} 113 } 114 115 // buildSidecarInboundHTTPRouteConfig builds the route config with a single wildcard virtual host on the inbound path 116 // TODO: trace decorators, inbound timeouts 117 func buildSidecarInboundHTTPRouteConfig(lb *ListenerBuilder, cc inboundChainConfig) *route.RouteConfiguration { 118 traceOperation := telemetry.TraceOperation(string(cc.telemetryMetadata.InstanceHostname), cc.port.Port) 119 defaultRoute := istio_route.BuildDefaultHTTPInboundRoute(cc.clusterName, traceOperation) 120 121 inboundVHost := &route.VirtualHost{ 122 Name: inboundVirtualHostPrefix + strconv.Itoa(cc.port.Port), // Format: "inbound|http|%d" 123 Domains: []string{"*"}, 124 Routes: []*route.Route{defaultRoute}, 125 } 126 127 r := &route.RouteConfiguration{ 128 Name: cc.clusterName, 129 VirtualHosts: []*route.VirtualHost{inboundVHost}, 130 ValidateClusters: proto.BoolFalse, 131 } 132 efw := lb.push.EnvoyFilters(lb.node) 133 r = envoyfilter.ApplyRouteConfigurationPatches(networking.EnvoyFilter_SIDECAR_INBOUND, lb.node, efw, r) 134 return r 135 } 136 137 // buildSidecarOutboundHTTPRouteConfig builds an outbound HTTP Route for sidecar. 138 // Based on port, will determine all virtual hosts that listen on the port. 139 func (configgen *ConfigGeneratorImpl) buildSidecarOutboundHTTPRouteConfig( 140 node *model.Proxy, 141 req *model.PushRequest, 142 routeName string, 143 vHostCache map[int][]*route.VirtualHost, 144 efw *model.EnvoyFilterWrapper, 145 efKeys []string, 146 ) (*discovery.Resource, bool) { 147 listenerPort, useSniffing, err := extractListenerPort(routeName) 148 if err != nil && routeName != model.RDSHttpProxy && !strings.HasPrefix(routeName, model.UnixAddressPrefix) { 149 // TODO: This is potentially one place where envoyFilter ADD operation can be helpful if the 150 // user wants to ship a custom RDS. But at this point, the match semantics are murky. We have no 151 // object to match upon. This needs more thought. For now, we will continue to return nil for 152 // unknown routes 153 return nil, false 154 } 155 156 var virtualHosts []*route.VirtualHost 157 var routeCache *istio_route.Cache 158 var resource *discovery.Resource 159 160 cacheHit := false 161 if useSniffing && listenerPort != 0 { 162 // Check if we have already computed the list of all virtual hosts for this port 163 // If so, then we simply have to return only the relevant virtual hosts for 164 // this listener's host:port 165 if vhosts, exists := vHostCache[listenerPort]; exists { 166 virtualHosts = getVirtualHostsForSniffedServicePort(vhosts, routeName) 167 cacheHit = true 168 } 169 } 170 if !cacheHit { 171 virtualHosts, resource, routeCache = BuildSidecarOutboundVirtualHosts(node, req.Push, routeName, listenerPort, efKeys, configgen.Cache) 172 if resource != nil { 173 return resource, true 174 } 175 if listenerPort > 0 { 176 // only cache for tcp ports and not for uds 177 vHostCache[listenerPort] = virtualHosts 178 } 179 180 // FIXME: This will ignore virtual services with hostnames that do not match any service in the registry 181 // per api spec, these hostnames + routes should appear in the virtual hosts (think bookinfo.com and 182 // productpage.ns1.svc.cluster.local). See the TODO in BuildSidecarOutboundVirtualHosts for the right solution 183 if useSniffing { 184 virtualHosts = getVirtualHostsForSniffedServicePort(virtualHosts, routeName) 185 } 186 } 187 188 util.SortVirtualHosts(virtualHosts) 189 190 if !useSniffing { 191 includeRequestAttemptCount := GetProxyHeaders(node, req.Push, istionetworking.ListenerClassSidecarOutbound).IncludeRequestAttemptCount 192 virtualHosts = append(virtualHosts, buildCatchAllVirtualHost(node, includeRequestAttemptCount)) 193 } 194 195 out := &route.RouteConfiguration{ 196 Name: routeName, 197 VirtualHosts: virtualHosts, 198 ValidateClusters: proto.BoolFalse, 199 MaxDirectResponseBodySizeBytes: istio_route.DefaultMaxDirectResponseBodySizeBytes, 200 IgnorePortInHostMatching: true, 201 } 202 203 // apply envoy filter patches 204 out = envoyfilter.ApplyRouteConfigurationPatches(networking.EnvoyFilter_SIDECAR_OUTBOUND, node, efw, out) 205 206 resource = &discovery.Resource{ 207 Name: out.Name, 208 Resource: protoconv.MessageToAny(out), 209 } 210 211 if features.EnableRDSCaching && routeCache != nil { 212 configgen.Cache.Add(routeCache, req, resource) 213 } 214 215 return resource, false 216 } 217 218 func extractListenerPort(routeName string) (int, bool, error) { 219 hasPrefix := strings.HasPrefix(routeName, model.UnixAddressPrefix) 220 index := strings.IndexRune(routeName, ':') 221 if !hasPrefix { 222 routeName = routeName[index+1:] 223 } 224 225 listenerPort, err := strconv.Atoi(routeName) 226 useSniffing := !hasPrefix && index != -1 227 return listenerPort, useSniffing, err 228 } 229 230 // TODO: merge with IstioEgressListenerWrapper.selectVirtualServices 231 // selectVirtualServices selects the virtual services by matching given services' host names. 232 func selectVirtualServices(virtualServices []config.Config, servicesByName map[host.Name]*model.Service) []config.Config { 233 out := make([]config.Config, 0) 234 // As a performance optimization, find out wildcard service hosts first, so that 235 // if non wildcard vs hosts can't be looked up directly in the service map, only need to 236 // loop through wildcard service hosts instead of all. 237 wcSvcHosts := []host.Name{} 238 for svcHost := range servicesByName { 239 if svcHost.IsWildCarded() { 240 wcSvcHosts = append(wcSvcHosts, svcHost) 241 } 242 } 243 244 for i := range virtualServices { 245 rule := virtualServices[i].Spec.(*networking.VirtualService) 246 var match bool 247 248 // Selection algorithm: 249 // virtualservices have a list of hosts in the API spec 250 // if any host in the list matches one service hostname, select the virtual service 251 // and break out of the loop. 252 for _, h := range rule.Hosts { 253 // TODO: This is a bug. VirtualServices can have many hosts 254 // while the user might be importing only a single host 255 // We need to generate a new VirtualService with just the matched host 256 if servicesByName[host.Name(h)] != nil { 257 match = true 258 break 259 } 260 261 if host.Name(h).IsWildCarded() { 262 // Process wildcard vs host as it need to follow the slow path of 263 // looping through all services in the map. 264 for svcHost := range servicesByName { 265 if host.Name(h).Matches(svcHost) { 266 match = true 267 break 268 } 269 } 270 } else { 271 // If non wildcard vs host isn't be found in service map, only loop through 272 // wildcard service hosts to avoid repeated matching. 273 for _, svcHost := range wcSvcHosts { 274 if host.Name(h).Matches(svcHost) { 275 match = true 276 break 277 } 278 } 279 } 280 281 if match { 282 break 283 } 284 } 285 286 if match { 287 out = append(out, virtualServices[i]) 288 } 289 } 290 291 return out 292 } 293 294 type ProxyHeaders struct { 295 ServerName string 296 ServerHeaderTransformation hcm.HttpConnectionManager_ServerHeaderTransformation 297 ForwardedClientCert hcm.HttpConnectionManager_ForwardClientCertDetails 298 IncludeRequestAttemptCount bool 299 GenerateRequestID *wrappers.BoolValue 300 SuppressDebugHeaders bool 301 SkipIstioMXHeaders bool 302 } 303 304 func GetProxyHeaders(node *model.Proxy, push *model.PushContext, class istionetworking.ListenerClass) ProxyHeaders { 305 pc := node.Metadata.ProxyConfigOrDefault(push.Mesh.DefaultConfig) 306 return GetProxyHeadersFromProxyConfig(pc, class) 307 } 308 309 func GetProxyHeadersFromProxyConfig(pc *meshconfig.ProxyConfig, class istionetworking.ListenerClass) ProxyHeaders { 310 base := ProxyHeaders{ 311 ServerName: EnvoyServerName, 312 ServerHeaderTransformation: hcm.HttpConnectionManager_OVERWRITE, 313 ForwardedClientCert: hcm.HttpConnectionManager_APPEND_FORWARD, 314 IncludeRequestAttemptCount: true, 315 SuppressDebugHeaders: false, 316 GenerateRequestID: nil, // Envoy default is to enable them, so set nil 317 SkipIstioMXHeaders: false, 318 } 319 if class == istionetworking.ListenerClassSidecarOutbound { 320 // Likely due to a mistake, outbound uses "envoy" while inbound uses "istio-envoy". Bummer. 321 // We keep it for backwards compatibility. 322 base.ServerName = "" // Envoy default is "envoy" so no need to set it explicitly. 323 } 324 ph := pc.GetProxyHeaders() 325 if ph == nil { 326 return base 327 } 328 if ph.AttemptCount.GetDisabled().GetValue() { 329 base.IncludeRequestAttemptCount = false 330 } 331 if ph.ForwardedClientCert != meshconfig.ForwardClientCertDetails_UNDEFINED { 332 base.ForwardedClientCert = util.MeshConfigToEnvoyForwardClientCertDetails(ph.ForwardedClientCert) 333 } 334 if ph.Server != nil { 335 if ph.Server.Disabled.GetValue() { 336 base.ServerName = "" 337 base.ServerHeaderTransformation = hcm.HttpConnectionManager_PASS_THROUGH 338 } else if ph.Server.Value != "" { 339 base.ServerName = ph.Server.Value 340 } 341 } 342 if ph.RequestId.GetDisabled().GetValue() { 343 base.GenerateRequestID = proto.BoolFalse 344 } 345 if ph.EnvoyDebugHeaders.GetDisabled().GetValue() { 346 base.SuppressDebugHeaders = true 347 } 348 if ph.MetadataExchangeHeaders != nil && ph.MetadataExchangeHeaders.GetMode() == meshconfig.ProxyConfig_ProxyHeaders_IN_MESH { 349 base.SkipIstioMXHeaders = true 350 } 351 return base 352 } 353 354 func BuildSidecarOutboundVirtualHosts(node *model.Proxy, push *model.PushContext, 355 routeName string, 356 listenerPort int, 357 efKeys []string, 358 xdsCache model.XdsCache, 359 ) ([]*route.VirtualHost, *discovery.Resource, *istio_route.Cache) { 360 var virtualServices []config.Config 361 var services []*model.Service 362 363 // Get the services from the egress listener. When sniffing is enabled, we send 364 // route name as foo.bar.com:8080 which is going to match against the wildcard 365 // egress listener only. A route with sniffing would not have been generated if there 366 // was a sidecar with explicit port (and hence protocol declaration). A route with 367 // sniffing is generated only in the case of the catch all egress listener. 368 egressListener := node.SidecarScope.GetEgressListenerForRDS(listenerPort, routeName) 369 // We should never be getting a nil egress listener because the code that setup this RDS 370 // call obviously saw an egress listener 371 if egressListener == nil { 372 return nil, nil, nil 373 } 374 375 services = egressListener.Services() 376 // To maintain correctness, we should only use the virtualservices for 377 // this listener and not all virtual services accessible to this proxy. 378 virtualServices = egressListener.VirtualServices() 379 380 // When generating RDS for ports created via the SidecarScope, we treat ports as HTTP proxy style ports 381 // if ports protocol is HTTP_PROXY. 382 if egressListener.IstioListener != nil && egressListener.IstioListener.Port != nil && 383 protocol.Parse(egressListener.IstioListener.Port.Protocol) == protocol.HTTP_PROXY { 384 listenerPort = 0 385 } 386 387 includeRequestAttemptCount := GetProxyHeaders(node, push, istionetworking.ListenerClassSidecarOutbound).IncludeRequestAttemptCount 388 389 servicesByName := make(map[host.Name]*model.Service) 390 for _, svc := range services { 391 if svc.Resolution == model.Alias { 392 // Will be handled by the service it is an alias for 393 continue 394 } 395 if listenerPort == 0 { 396 // Take all ports when listen port is 0 (http_proxy or uds) 397 // Expect virtualServices to resolve to right port 398 servicesByName[svc.Hostname] = svc 399 } else if svcPort, exists := svc.Ports.GetByPort(listenerPort); exists { 400 servicesByName[svc.Hostname] = &model.Service{ 401 Hostname: svc.Hostname, 402 DefaultAddress: svc.GetAddressForProxy(node), 403 MeshExternal: svc.MeshExternal, 404 Resolution: svc.Resolution, 405 Ports: []*model.Port{svcPort}, 406 Attributes: model.ServiceAttributes{ 407 Namespace: svc.Attributes.Namespace, 408 ServiceRegistry: svc.Attributes.ServiceRegistry, 409 Labels: svc.Attributes.Labels, 410 Aliases: svc.Attributes.Aliases, 411 K8sAttributes: svc.Attributes.K8sAttributes, 412 }, 413 } 414 if features.EnableDualStack { 415 // cannot correctly build virtualHost domains for dual stack without ClusterVIPs 416 servicesByName[svc.Hostname].ClusterVIPs = *svc.ClusterVIPs.DeepCopy() 417 } 418 } 419 } 420 421 var routeCache *istio_route.Cache 422 if listenerPort > 0 && features.EnableRDSCaching { 423 // sort services, ensure that routeCache calculation result is stable 424 services = make([]*model.Service, 0, len(servicesByName)) 425 for _, svc := range servicesByName { 426 services = append(services, svc) 427 } 428 sort.SliceStable(services, func(i, j int) bool { 429 return services[i].Hostname <= services[j].Hostname 430 }) 431 routeCache = &istio_route.Cache{ 432 RouteName: routeName, 433 ProxyVersion: node.Metadata.IstioVersion, 434 ClusterID: string(node.Metadata.ClusterID), 435 DNSDomain: node.DNSDomain, 436 DNSCapture: bool(node.Metadata.DNSCapture), 437 DNSAutoAllocate: bool(node.Metadata.DNSAutoAllocate), 438 AllowAny: util.IsAllowAnyOutbound(node), 439 ListenerPort: listenerPort, 440 Services: services, 441 VirtualServices: virtualServices, 442 DelegateVirtualServices: push.DelegateVirtualServices(virtualServices), 443 EnvoyFilterKeys: efKeys, 444 } 445 } 446 447 // This is hack to keep consistent with previous behavior. 448 if listenerPort != 80 { 449 // only select virtualServices that matches a service 450 virtualServices = selectVirtualServices(virtualServices, servicesByName) 451 } 452 453 mostSpecificWildcardVsIndex := egressListener.MostSpecificWildcardVirtualServiceIndex() 454 // Get list of virtual services bound to the mesh gateway 455 virtualHostWrappers := istio_route.BuildSidecarVirtualHostWrapper(routeCache, node, push, 456 servicesByName, virtualServices, listenerPort, mostSpecificWildcardVsIndex, 457 ) 458 459 if features.EnableRDSCaching { 460 resource := xdsCache.Get(routeCache) 461 if resource != nil && !features.EnableUnsafeAssertions { 462 return nil, resource, routeCache 463 } 464 } 465 466 vHostPortMap := make(map[int][]*route.VirtualHost) 467 vhosts := sets.String{} 468 vhdomains := sets.String{} 469 knownFQDN := sets.String{} 470 471 buildVirtualHost := func(hostname string, vhwrapper istio_route.VirtualHostWrapper, svc *model.Service) *route.VirtualHost { 472 name := util.DomainName(hostname, vhwrapper.Port) 473 if vhosts.InsertContains(name) { 474 // This means this virtual host has caused duplicate virtual host name. 475 var msg string 476 if svc == nil { 477 msg = fmt.Sprintf("duplicate domain from virtual service: %s", name) 478 } else { 479 msg = fmt.Sprintf("duplicate domain from service: %s", name) 480 } 481 push.AddMetric(model.DuplicatedDomains, name, node.ID, msg) 482 return nil 483 } 484 var domains []string 485 var altHosts []string 486 if svc == nil { 487 if SidecarIgnorePort(node) { 488 domains = []string{util.IPv6Compliant(hostname)} 489 } else { 490 domains = []string{util.IPv6Compliant(hostname), name} 491 } 492 } else { 493 domains, altHosts = generateVirtualHostDomains(svc, listenerPort, vhwrapper.Port, node) 494 } 495 dl := len(domains) 496 domains = dedupeDomains(domains, vhdomains, altHosts, knownFQDN) 497 if dl != len(domains) { 498 var msg string 499 if svc == nil { 500 msg = fmt.Sprintf("duplicate domain from virtual service: %s", name) 501 } else { 502 msg = fmt.Sprintf("duplicate domain from service: %s", name) 503 } 504 // This means this virtual host has caused duplicate virtual host domain. 505 push.AddMetric(model.DuplicatedDomains, name, node.ID, msg) 506 } 507 if len(domains) > 0 { 508 pervirtualHostFilters := map[string]*anypb.Any{} 509 if statefulConfig := util.MaybeBuildStatefulSessionFilterConfig(svc); statefulConfig != nil { 510 perRouteStatefulSession := &statefulsession.StatefulSessionPerRoute{ 511 Override: &statefulsession.StatefulSessionPerRoute_StatefulSession{ 512 StatefulSession: statefulConfig, 513 }, 514 } 515 pervirtualHostFilters[util.StatefulSessionFilter] = protoconv.MessageToAny(perRouteStatefulSession) 516 } 517 return &route.VirtualHost{ 518 Name: name, 519 Domains: domains, 520 Routes: vhwrapper.Routes, 521 IncludeRequestAttemptCount: includeRequestAttemptCount, 522 TypedPerFilterConfig: pervirtualHostFilters, 523 } 524 } 525 526 return nil 527 } 528 529 for _, virtualHostWrapper := range virtualHostWrappers { 530 for _, svc := range virtualHostWrapper.Services { 531 name := util.DomainName(string(svc.Hostname), virtualHostWrapper.Port) 532 knownFQDN.InsertAll(name, string(svc.Hostname)) 533 } 534 } 535 536 for _, virtualHostWrapper := range virtualHostWrappers { 537 // If none of the routes matched by source, skip this virtual host 538 if len(virtualHostWrapper.Routes) == 0 { 539 continue 540 } 541 virtualHosts := make([]*route.VirtualHost, 0, len(virtualHostWrapper.VirtualServiceHosts)+len(virtualHostWrapper.Services)) 542 543 for _, hostname := range virtualHostWrapper.VirtualServiceHosts { 544 if vhost := buildVirtualHost(hostname, virtualHostWrapper, nil); vhost != nil { 545 virtualHosts = append(virtualHosts, vhost) 546 } 547 } 548 549 for _, svc := range virtualHostWrapper.Services { 550 if vhost := buildVirtualHost(string(svc.Hostname), virtualHostWrapper, svc); vhost != nil { 551 virtualHosts = append(virtualHosts, vhost) 552 } 553 } 554 vHostPortMap[virtualHostWrapper.Port] = append(vHostPortMap[virtualHostWrapper.Port], virtualHosts...) 555 } 556 557 var out []*route.VirtualHost 558 if listenerPort == 0 { 559 out = mergeAllVirtualHosts(vHostPortMap) 560 } else { 561 out = vHostPortMap[listenerPort] 562 } 563 564 return out, nil, routeCache 565 } 566 567 // dedupeDomains removes the duplicate domains from the passed in domains. 568 func dedupeDomains(domains []string, vhdomains sets.String, expandedHosts []string, knownFQDNs sets.String) []string { 569 temp := domains[:0] 570 for _, d := range domains { 571 if vhdomains.Contains(strings.ToLower(d)) { 572 continue 573 } 574 // Check if the domain is an "expanded" host, and its also a known FQDN 575 // This prevents a case where a domain like "foo.com.cluster.local" gets expanded to "foo.com", overwriting 576 // the real "foo.com" 577 // This works by providing a list of domains that were added as expanding the DNS domain as part of expandedHosts, 578 // and a list of known unexpanded FQDNs to compare against 579 if slices.Contains(expandedHosts, d) && knownFQDNs.Contains(d) { // O(n) search, but n is at most 10 580 continue 581 } 582 temp = append(temp, d) 583 vhdomains.Insert(strings.ToLower(d)) 584 } 585 return temp 586 } 587 588 // Returns the set of virtual hosts that correspond to the listener that has HTTP protocol detection 589 // setup. This listener should only get the virtual hosts that correspond to this service+port and not 590 // all virtual hosts that are usually supplied for 0.0.0.0:PORT. 591 func getVirtualHostsForSniffedServicePort(vhosts []*route.VirtualHost, routeName string) []*route.VirtualHost { 592 nameWithoutPort, _, _ := net.SplitHostPort(routeName) 593 var virtualHosts []*route.VirtualHost 594 for _, vh := range vhosts { 595 for _, domain := range vh.Domains { 596 if domain == routeName || domain == nameWithoutPort { 597 virtualHosts = append(virtualHosts, vh) 598 break 599 } 600 } 601 } 602 603 if len(virtualHosts) == 0 { 604 return virtualHosts 605 } 606 if len(virtualHosts) == 1 { 607 virtualHosts[0].Domains = []string{"*"} 608 return virtualHosts 609 } 610 if features.EnableUnsafeAssertions { 611 panic(fmt.Sprintf("unexpectedly matched multiple virtual hosts for %v: %v", routeName, virtualHosts)) 612 } 613 return virtualHosts 614 } 615 616 func SidecarIgnorePort(node *model.Proxy) bool { 617 return !node.IsProxylessGrpc() 618 } 619 620 // generateVirtualHostDomains generates the set of domain matches for a service being accessed from 621 // a proxy node 622 func generateVirtualHostDomains(service *model.Service, listenerPort int, port int, node *model.Proxy) ([]string, []string) { 623 if SidecarIgnorePort(node) && listenerPort != 0 { 624 // Indicate we do not need port, as we will set IgnorePortInHostMatching 625 port = portNoAppendPortSuffix 626 } 627 domains := []string{} 628 allAltHosts := []string{} 629 all := []string{string(service.Hostname)} 630 for _, a := range service.Attributes.Aliases { 631 all = append(all, a.Hostname.String()) 632 } 633 for _, s := range all { 634 altHosts := GenerateAltVirtualHosts(s, port, node.DNSDomain) 635 domains = appendDomainPort(domains, s, port) 636 domains = append(domains, altHosts...) 637 allAltHosts = append(allAltHosts, altHosts...) 638 } 639 640 if service.Resolution == model.Passthrough && 641 service.Attributes.ServiceRegistry == provider.Kubernetes { 642 for _, domain := range domains { 643 domains = append(domains, wildcardDomainPrefix+domain) 644 } 645 } 646 647 svcAddr := service.GetAddressForProxy(node) 648 if len(svcAddr) > 0 && svcAddr != constants.UnspecifiedIP { 649 domains = appendDomainPort(domains, svcAddr, port) 650 } 651 652 // handle dual stack's extra address when generating the virtualHost domains 653 // assumes that conversion is stripping out the DefaultAddress from ClusterVIPs 654 extraAddr := service.GetExtraAddressesForProxy(node) 655 for _, addr := range extraAddr { 656 domains = appendDomainPort(domains, addr, port) 657 } 658 659 return domains, allAltHosts 660 } 661 662 // appendDomainPort appends `domain` and `domain:port` to `domains`. The `domain:port` variant is skipped 663 // if port is unset. 664 func appendDomainPort(domains []string, domain string, port int) []string { 665 if port == portNoAppendPortSuffix { 666 return append(domains, util.IPv6Compliant(domain)) 667 } 668 return append(domains, util.IPv6Compliant(domain), util.DomainName(domain, port)) 669 } 670 671 // GenerateAltVirtualHosts given a service and a port, generates all possible HTTP Host headers. 672 // For example, a service of the form foo.local.campus.net on port 80, with local domain "local.campus.net" 673 // could be accessed as http://foo:80 within the .local network, as http://foo.local:80 (by other clients 674 // in the campus.net domain), as http://foo.local.campus:80, etc. 675 // NOTE: When a sidecar in remote.campus.net domain is talking to foo.local.campus.net, 676 // we should only generate foo.local, foo.local.campus, etc (and never just "foo"). 677 // 678 // - Given foo.local.campus.net on proxy domain local.campus.net, this function generates 679 // foo:80, foo.local:80, foo.local.campus:80, with and without ports. It will not generate 680 // foo.local.campus.net (full hostname) since its already added elsewhere. 681 // 682 // - Given foo.local.campus.net on proxy domain remote.campus.net, this function generates 683 // foo.local:80, foo.local.campus:80 684 // 685 // - Given foo.local.campus.net on proxy domain "" or proxy domain example.com, this 686 // function returns nil 687 func GenerateAltVirtualHosts(hostname string, port int, proxyDomain string) []string { 688 // If the dns/proxy domain contains `.svc`, only services following the <ns>.svc.<suffix> 689 // naming convention and that share a suffix with the domain should be expanded. 690 if strings.Contains(proxyDomain, ".svc.") { 691 692 if strings.HasSuffix(hostname, removeSvcNamespace(proxyDomain)) { 693 return generateAltVirtualHostsForKubernetesService(hostname, port, proxyDomain) 694 } 695 696 // Hostname is not a kube service. It is not safe to expand the 697 // hostname as non-fully-qualified names could conflict with expansion of other kube service 698 // hostnames 699 return nil 700 } 701 702 var vhosts []string 703 uniqueHostnameParts, sharedDNSDomainParts := getUniqueAndSharedDNSDomain(hostname, proxyDomain) 704 705 // If there is no shared DNS name (e.g., foobar.com service on local.net proxy domain) 706 // do not generate any alternate virtual host representations 707 if len(sharedDNSDomainParts) == 0 { 708 return nil 709 } 710 711 uniqueHostname := strings.Join(uniqueHostnameParts, ".") 712 713 // Add the uniqueHost. 714 vhosts = appendDomainPort(vhosts, uniqueHostname, port) 715 if len(uniqueHostnameParts) == 2 { 716 // This is the case of uniqHostname having namespace already. 717 dnsHostName := uniqueHostname + "." + sharedDNSDomainParts[0] 718 vhosts = appendDomainPort(vhosts, dnsHostName, port) 719 } 720 return vhosts 721 } 722 723 // portNoAppendPortSuffix is a signal to not append port to vhost 724 const portNoAppendPortSuffix = 0 725 726 func generateAltVirtualHostsForKubernetesService(hostname string, port int, proxyDomain string) []string { 727 id := strings.Index(proxyDomain, ".svc.") 728 ih := strings.Index(hostname, ".svc.") 729 if ih > 0 { // Proxy and service hostname are in kube 730 ns := strings.Index(hostname, ".") 731 if ns+1 >= len(hostname) || ns+1 > ih { 732 // Invalid domain 733 return nil 734 } 735 if hostname[ns+1:ih] == proxyDomain[:id] { 736 // Same namespace 737 if port == portNoAppendPortSuffix { 738 return []string{ 739 hostname[:ns], 740 hostname[:ih] + ".svc", 741 hostname[:ih], 742 } 743 } 744 return []string{ 745 hostname[:ns], 746 util.DomainName(hostname[:ns], port), 747 hostname[:ih] + ".svc", 748 util.DomainName(hostname[:ih]+".svc", port), 749 hostname[:ih], 750 util.DomainName(hostname[:ih], port), 751 } 752 } 753 // Different namespace 754 if port == portNoAppendPortSuffix { 755 return []string{ 756 hostname[:ih], 757 hostname[:ih] + ".svc", 758 } 759 } 760 return []string{ 761 hostname[:ih], 762 util.DomainName(hostname[:ih], port), 763 hostname[:ih] + ".svc", 764 util.DomainName(hostname[:ih]+".svc", port), 765 } 766 } 767 // Proxy is in k8s, but service isn't. No alt hosts 768 return nil 769 } 770 771 // mergeAllVirtualHosts across all ports. On routes for ports other than port 80, 772 // virtual hosts without an explicit port suffix (IP:PORT) should not be added 773 func mergeAllVirtualHosts(vHostPortMap map[int][]*route.VirtualHost) []*route.VirtualHost { 774 var virtualHosts []*route.VirtualHost 775 for p, vhosts := range vHostPortMap { 776 if p == 80 { 777 virtualHosts = append(virtualHosts, vhosts...) 778 } else { 779 for _, vhost := range vhosts { 780 vhost.Domains = slices.FilterInPlace(vhost.Domains, func(domain string) bool { 781 return strings.Contains(domain, ":") 782 }) 783 if len(vhost.Domains) > 0 { 784 virtualHosts = append(virtualHosts, vhost) 785 } 786 } 787 } 788 } 789 return virtualHosts 790 } 791 792 func min(a, b int) int { 793 if a < b { 794 return a 795 } 796 return b 797 } 798 799 // getUniqueAndSharedDNSDomain computes the unique and shared DNS suffix from a FQDN service name and 800 // the proxy's local domain with namespace. This is especially useful in Kubernetes environments, where 801 // a two services can have same name in different namespaces (e.g., foo.ns1.svc.cluster.local, 802 // foo.ns2.svc.cluster.local). In this case, if the proxy is in ns2.svc.cluster.local, then while 803 // generating alt virtual hosts for service foo.ns1 for the sidecars in ns2 namespace, we should generate 804 // foo.ns1, foo.ns1.svc, foo.ns1.svc.cluster.local and should not generate a virtual host called "foo" for 805 // foo.ns1 service. 806 // So given foo.ns1.svc.cluster.local and ns2.svc.cluster.local, this function will return 807 // foo.ns1, and svc.cluster.local. 808 // When given foo.ns2.svc.cluster.local and ns2.svc.cluster.local, this function will return 809 // foo, ns2.svc.cluster.local. 810 func getUniqueAndSharedDNSDomain(fqdnHostname, proxyDomain string) (partsUnique []string, partsShared []string) { 811 // split them by the dot and reverse the arrays, so that we can 812 // start collecting the shared bits of DNS suffix. 813 // E.g., foo.ns1.svc.cluster.local -> local,cluster,svc,ns1,foo 814 // ns2.svc.cluster.local -> local,cluster,svc,ns2 815 partsFQDN := strings.Split(fqdnHostname, ".") 816 partsProxyDomain := strings.Split(proxyDomain, ".") 817 partsFQDNInReverse := slices.Reverse(partsFQDN) 818 partsProxyDomainInReverse := slices.Reverse(partsProxyDomain) 819 var sharedSuffixesInReverse []string // pieces shared between proxy and svc. e.g., local,cluster,svc 820 821 for i := 0; i < min(len(partsFQDNInReverse), len(partsProxyDomainInReverse)); i++ { 822 if partsFQDNInReverse[i] == partsProxyDomainInReverse[i] { 823 sharedSuffixesInReverse = append(sharedSuffixesInReverse, partsFQDNInReverse[i]) 824 } else { 825 break 826 } 827 } 828 829 if len(sharedSuffixesInReverse) == 0 { 830 partsUnique = partsFQDN 831 } else { 832 // get the non shared pieces (ns1, foo) and reverse Array 833 partsUnique = slices.Reverse(partsFQDNInReverse[len(sharedSuffixesInReverse):]) 834 partsShared = slices.Reverse(sharedSuffixesInReverse) 835 } 836 return 837 } 838 839 func buildCatchAllVirtualHost(node *model.Proxy, includeRequestAttemptCount bool) *route.VirtualHost { 840 if util.IsAllowAnyOutbound(node) { 841 egressCluster := util.PassthroughCluster 842 notimeout := durationpb.New(0) 843 844 // no need to check for nil value as the previous if check has checked 845 if node.SidecarScope.OutboundTrafficPolicy.EgressProxy != nil { 846 // user has provided an explicit destination for all the unknown traffic. 847 // build a cluster out of this destination 848 egressCluster = istio_route.GetDestinationCluster(node.SidecarScope.OutboundTrafficPolicy.EgressProxy, 849 nil, 0) 850 } 851 852 routeAction := &route.RouteAction{ 853 ClusterSpecifier: &route.RouteAction_Cluster{Cluster: egressCluster}, 854 // Disable timeout instead of assuming some defaults. 855 Timeout: notimeout, 856 // Use deprecated value for now as the replacement MaxStreamDuration has some regressions. 857 // nolint: staticcheck 858 MaxGrpcTimeout: notimeout, 859 } 860 861 return &route.VirtualHost{ 862 Name: util.Passthrough, 863 Domains: []string{"*"}, 864 Routes: []*route.Route{ 865 { 866 Name: util.Passthrough, 867 Match: &route.RouteMatch{ 868 PathSpecifier: &route.RouteMatch_Prefix{Prefix: "/"}, 869 }, 870 Action: &route.Route_Route{ 871 Route: routeAction, 872 }, 873 }, 874 }, 875 IncludeRequestAttemptCount: includeRequestAttemptCount, 876 } 877 } 878 879 return &route.VirtualHost{ 880 Name: util.BlackHole, 881 Domains: []string{"*"}, 882 Routes: []*route.Route{ 883 { 884 Name: util.BlackHole, 885 Match: &route.RouteMatch{ 886 PathSpecifier: &route.RouteMatch_Prefix{Prefix: "/"}, 887 }, 888 Action: &route.Route_DirectResponse{ 889 DirectResponse: &route.DirectResponseAction{ 890 Status: 502, 891 }, 892 }, 893 }, 894 }, 895 IncludeRequestAttemptCount: includeRequestAttemptCount, 896 } 897 } 898 899 // Simply removes everything before .svc, if present 900 func removeSvcNamespace(domain string) string { 901 if idx := strings.Index(domain, ".svc."); idx > 0 { 902 return domain[idx:] 903 } 904 905 return domain 906 }