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  }