github.com/cilium/cilium@v1.16.2/pkg/ciliumenvoyconfig/cec_resource_parser.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package ciliumenvoyconfig
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"net"
    10  
    11  	"github.com/cilium/hive/cell"
    12  	cilium "github.com/cilium/proxy/go/cilium/api"
    13  	envoy_config_cluster "github.com/cilium/proxy/go/envoy/config/cluster/v3"
    14  	envoy_config_core "github.com/cilium/proxy/go/envoy/config/core/v3"
    15  	envoy_config_endpoint "github.com/cilium/proxy/go/envoy/config/endpoint/v3"
    16  	envoy_config_listener "github.com/cilium/proxy/go/envoy/config/listener/v3"
    17  	envoy_config_route "github.com/cilium/proxy/go/envoy/config/route/v3"
    18  	envoy_config_healthcheck "github.com/cilium/proxy/go/envoy/extensions/filters/http/health_check/v3"
    19  	envoy_config_http "github.com/cilium/proxy/go/envoy/extensions/filters/network/http_connection_manager/v3"
    20  	envoy_config_tcp "github.com/cilium/proxy/go/envoy/extensions/filters/network/tcp_proxy/v3"
    21  	envoy_config_tls "github.com/cilium/proxy/go/envoy/extensions/transport_sockets/tls/v3"
    22  	envoy_config_upstream "github.com/cilium/proxy/go/envoy/extensions/upstreams/http/v3"
    23  	envoy_config_types "github.com/cilium/proxy/go/envoy/type/v3"
    24  	"github.com/sirupsen/logrus"
    25  	"google.golang.org/protobuf/proto"
    26  	"google.golang.org/protobuf/types/known/anypb"
    27  	"google.golang.org/protobuf/types/known/wrapperspb"
    28  
    29  	"github.com/cilium/cilium/pkg/bpf"
    30  	"github.com/cilium/cilium/pkg/envoy"
    31  	cilium_v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
    32  	"github.com/cilium/cilium/pkg/logging/logfields"
    33  	"github.com/cilium/cilium/pkg/node"
    34  	"github.com/cilium/cilium/pkg/option"
    35  	"github.com/cilium/cilium/pkg/policy/api"
    36  	"github.com/cilium/cilium/pkg/proxy"
    37  )
    38  
    39  const (
    40  	ciliumBPFMetadataListenerFilterName = "cilium.bpf_metadata"
    41  	ciliumNetworkFilterName             = "cilium.network"
    42  	ciliumL7FilterName                  = "cilium.l7policy"
    43  	envoyRouterFilterName               = "envoy.filters.http.router"
    44  
    45  	httpProtocolOptionsType = "envoy.extensions.upstreams.http.v3.HttpProtocolOptions"
    46  
    47  	upstreamCodecFilterTypeURL   = "type.googleapis.com/envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec"
    48  	quicUpstreamTransportTypeURL = "type.googleapis.com/envoy.extensions.transport_sockets.quic.v3.QuicUpstreamTransport"
    49  	upstreamTlsContextTypeURL    = "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext"
    50  	ciliumL7FilterTypeURL        = "type.googleapis.com/cilium.L7Policy"
    51  )
    52  
    53  type cecResourceParser struct {
    54  	logger        logrus.FieldLogger
    55  	portAllocator PortAllocator
    56  
    57  	ingressIPv4 net.IP
    58  	ingressIPv6 net.IP
    59  }
    60  
    61  type parserParams struct {
    62  	cell.In
    63  
    64  	Logger    logrus.FieldLogger
    65  	Lifecycle cell.Lifecycle
    66  
    67  	Proxy          *proxy.Proxy
    68  	LocalNodeStore *node.LocalNodeStore
    69  }
    70  
    71  func newCECResourceParser(params parserParams) *cecResourceParser {
    72  	parser := &cecResourceParser{
    73  		logger:        params.Logger,
    74  		portAllocator: params.Proxy,
    75  	}
    76  
    77  	// Retrieve Ingress IPs from local Node.
    78  	// It's assumed that these don't change.
    79  	params.Lifecycle.Append(cell.Hook{
    80  		OnStart: func(ctx cell.HookContext) error {
    81  			localNode, err := params.LocalNodeStore.Get(ctx)
    82  			if err != nil {
    83  				return fmt.Errorf("failed to get LocalNodeStore: %w", err)
    84  			}
    85  
    86  			parser.ingressIPv4 = localNode.IPv4IngressIP
    87  			parser.ingressIPv6 = localNode.IPv6IngressIP
    88  
    89  			params.Logger.
    90  				WithField(logfields.V4IngressIP, localNode.IPv4IngressIP).
    91  				WithField(logfields.V6IngressIP, localNode.IPv6IngressIP).
    92  				Debug("Retrieved Ingress IPs from Node")
    93  
    94  			return nil
    95  		},
    96  	})
    97  
    98  	return parser
    99  }
   100  
   101  type PortAllocator interface {
   102  	AllocateCRDProxyPort(name string) (uint16, error)
   103  	AckProxyPort(ctx context.Context, name string) error
   104  	ReleaseProxyPort(name string) error
   105  }
   106  
   107  // parseResources parses all supported Envoy resource types from CiliumEnvoyConfig CRD to the internal type `envoy.Resources`.
   108  //
   109  // - Qualify names by prepending the namespace and name of the origin CEC to the Envoy resource names.
   110  // - Validate resources
   111  // - Inject Cilium specifics into the Listeners (BPF Metadata listener filter, Network filter & L7 filter)
   112  // - Assign a random proxy port to Listeners that don't have an explicit address specified.
   113  //
   114  // Parameters:
   115  //   - `cecNamespace` and `cecName` will be prepended to the Envoy resource names.
   116  //   - `xdsResources` are the resources from the CiliumEnvoyConfig or CiliumClusterwideEnvoyConfig.
   117  //   - `isL7LB` defines whether these resources are used for L7 loadbalancing. If `true`, the Envoy Cilium Network- and L7 filters are always
   118  //     added to all non-internal Listeners. In addition, the info gets passed to the Envoy Cilium BPF Metadata listener filter on all Listeners.
   119  //   - `useOriginalSourceAddr` is passed to the Envoy Cilium BPF Metadata listener filter on all Listeners.
   120  //   - `newResources` is passed as `true` when parsing resources that are being added or are the new version of the resources being updated,
   121  //     and as `false` if the resources are being removed or are the old version of the resources being updated. Only 'new' resources are validated.
   122  func (r *cecResourceParser) parseResources(cecNamespace string, cecName string, xdsResources []cilium_v2.XDSResource, isL7LB bool, useOriginalSourceAddr bool, newResources bool) (envoy.Resources, error) {
   123  	// only validate new  resources - old ones are already applied
   124  	validate := newResources
   125  
   126  	// upstream filters are injected if any non-internal listener is L7 LB
   127  	// and downstream filters were injected.
   128  	injectCiliumUpstreamFilters := false
   129  
   130  	resources := envoy.Resources{}
   131  	for _, res := range xdsResources {
   132  		// Skip empty TypeURLs, which are left behind when Unmarshaling resource JSON fails
   133  		if res.TypeUrl == "" {
   134  			continue
   135  		}
   136  		message, err := res.UnmarshalNew()
   137  		if err != nil {
   138  			return envoy.Resources{}, err
   139  		}
   140  		typeURL := res.GetTypeUrl()
   141  		switch typeURL {
   142  		case envoy.ListenerTypeURL:
   143  			listener, ok := message.(*envoy_config_listener.Listener)
   144  			if !ok {
   145  				return envoy.Resources{}, fmt.Errorf("invalid type for Listener: %T", message)
   146  			}
   147  			// Check that a listener name is provided
   148  			if listener.Name == "" {
   149  				return envoy.Resources{}, fmt.Errorf("unspecified Listener name")
   150  			}
   151  
   152  			if option.Config.EnableBPFTProxy {
   153  				// Envoy since 1.20.0 uses SO_REUSEPORT on listeners by default.
   154  				// BPF TPROXY is currently not compatible with SO_REUSEPORT, so
   155  				// disable it.  Note that this may degrade Envoy performance.
   156  				listener.EnableReusePort = &wrapperspb.BoolValue{Value: false}
   157  			}
   158  
   159  			// Only inject Cilium filters if all of the following conditions are fulfilled
   160  			// * Cilium allocates listener address or it's a listener for a L7 loadbalancer
   161  			// * It's not an internal listener
   162  			injectCiliumFilters := (listener.GetAddress() == nil || isL7LB) && listener.GetInternalListener() == nil
   163  
   164  			// Fill in SDS & RDS config source if unset
   165  			for _, fc := range listener.FilterChains {
   166  				fillInTransportSocketXDS(cecNamespace, cecName, fc.TransportSocket)
   167  				foundCiliumNetworkFilter := false
   168  				for i, filter := range fc.Filters {
   169  					if filter.Name == ciliumNetworkFilterName {
   170  						foundCiliumNetworkFilter = true
   171  					}
   172  					tc := filter.GetTypedConfig()
   173  					if tc == nil {
   174  						continue
   175  					}
   176  					switch tc.GetTypeUrl() {
   177  					case envoy.HttpConnectionManagerTypeURL:
   178  						any, err := tc.UnmarshalNew()
   179  						if err != nil {
   180  							continue
   181  						}
   182  						hcmConfig, ok := any.(*envoy_config_http.HttpConnectionManager)
   183  						if !ok {
   184  							continue
   185  						}
   186  						updated := false
   187  						if rds := hcmConfig.GetRds(); rds != nil {
   188  							// Since we are prepending CEC namespace and name to Routes name,
   189  							// we must do the same here to point to the correct Route resource.
   190  							if rds.RouteConfigName != "" {
   191  								rds.RouteConfigName, updated = api.ResourceQualifiedName(cecNamespace, cecName, rds.RouteConfigName, api.ForceNamespace)
   192  							}
   193  							if rds.ConfigSource == nil {
   194  								rds.ConfigSource = envoy.CiliumXDSConfigSource
   195  								updated = true
   196  							}
   197  						}
   198  						if routeConfig := hcmConfig.GetRouteConfig(); routeConfig != nil {
   199  							if qualifyRouteConfigurationResourceNames(cecNamespace, cecName, routeConfig) {
   200  								updated = true
   201  							}
   202  						}
   203  						if injectCiliumFilters {
   204  							l7FilterUpdated := injectCiliumL7Filter(hcmConfig)
   205  							updated = updated || l7FilterUpdated
   206  
   207  							// Also inject upstream filters for L7 LB when injecting the downstream
   208  							// HTTP enforcement filter
   209  							if isL7LB {
   210  								injectCiliumUpstreamFilters = true
   211  							}
   212  						}
   213  
   214  						httpFiltersUpdated := qualifyHttpFilters(cecNamespace, cecName, hcmConfig)
   215  						updated = updated || httpFiltersUpdated
   216  
   217  						if updated {
   218  							filter.ConfigType = &envoy_config_listener.Filter_TypedConfig{
   219  								TypedConfig: toAny(hcmConfig),
   220  							}
   221  						}
   222  					case envoy.TCPProxyTypeURL:
   223  						any, err := tc.UnmarshalNew()
   224  						if err != nil {
   225  							continue
   226  						}
   227  						tcpProxy, ok := any.(*envoy_config_tcp.TcpProxy)
   228  						if !ok {
   229  							continue
   230  						}
   231  
   232  						if qualifyTcpProxyResourceNames(cecNamespace, cecName, tcpProxy) {
   233  							filter.ConfigType = &envoy_config_listener.Filter_TypedConfig{
   234  								TypedConfig: toAny(tcpProxy),
   235  							}
   236  						}
   237  					default:
   238  						continue
   239  					}
   240  					if injectCiliumFilters && !foundCiliumNetworkFilter {
   241  						// Inject Cilium network filter just before the HTTP Connection Manager or TCPProxy filter
   242  						fc.Filters = append(fc.Filters[:i+1], fc.Filters[i:]...)
   243  						fc.Filters[i] = &envoy_config_listener.Filter{
   244  							Name: ciliumNetworkFilterName,
   245  							ConfigType: &envoy_config_listener.Filter_TypedConfig{
   246  								TypedConfig: toAny(&cilium.NetworkFilter{}),
   247  							},
   248  						}
   249  					}
   250  					break // Done with this filter chain
   251  				}
   252  			}
   253  
   254  			name := listener.Name
   255  			listener.Name, _ = api.ResourceQualifiedName(cecNamespace, cecName, listener.Name, api.ForceNamespace)
   256  
   257  			// Check for duplicate after the name has been qualified
   258  			for i := range resources.Listeners {
   259  				if listener.Name == resources.Listeners[i].Name {
   260  					return envoy.Resources{}, fmt.Errorf("duplicate Listener name %q", listener.Name)
   261  				}
   262  			}
   263  
   264  			if validate {
   265  				if err := listener.Validate(); err != nil {
   266  					return envoy.Resources{}, fmt.Errorf("failed to validate Listener (%w): %s", err, listener.String())
   267  				}
   268  			}
   269  			resources.Listeners = append(resources.Listeners, listener)
   270  
   271  			r.logger.Debugf("ParseResources: Parsed listener %q: %v", name, listener)
   272  
   273  		case envoy.RouteTypeURL:
   274  			route, ok := message.(*envoy_config_route.RouteConfiguration)
   275  			if !ok {
   276  				return envoy.Resources{}, fmt.Errorf("invalid type for Route: %T", message)
   277  			}
   278  			// Check that a Route name is provided
   279  			if route.Name == "" {
   280  				return envoy.Resources{}, fmt.Errorf("unspecified RouteConfiguration name")
   281  			}
   282  
   283  			qualifyRouteConfigurationResourceNames(cecNamespace, cecName, route)
   284  
   285  			name := route.Name
   286  			route.Name, _ = api.ResourceQualifiedName(cecNamespace, cecName, name, api.ForceNamespace)
   287  
   288  			// Check for duplicate after the name has been qualified
   289  			for i := range resources.Routes {
   290  				if route.Name == resources.Routes[i].Name {
   291  					return envoy.Resources{}, fmt.Errorf("duplicate Route name %q", route.Name)
   292  				}
   293  			}
   294  
   295  			if validate {
   296  				if err := route.Validate(); err != nil {
   297  					return envoy.Resources{}, fmt.Errorf("failed to validate RouteConfiguration (%w): %s", err, route.String())
   298  				}
   299  			}
   300  			resources.Routes = append(resources.Routes, route)
   301  
   302  			r.logger.Debugf("ParseResources: Parsed route %q: %v", name, route)
   303  
   304  		case envoy.ClusterTypeURL:
   305  			cluster, ok := message.(*envoy_config_cluster.Cluster)
   306  			if !ok {
   307  				return envoy.Resources{}, fmt.Errorf("invalid type for Route: %T", message)
   308  			}
   309  			// Check that a Cluster name is provided
   310  			if cluster.Name == "" {
   311  				return envoy.Resources{}, fmt.Errorf("unspecified Cluster name")
   312  			}
   313  
   314  			fillInTransportSocketXDS(cecNamespace, cecName, cluster.TransportSocket)
   315  
   316  			// Fill in EDS config source if unset
   317  			if enum := cluster.GetType(); enum == envoy_config_cluster.Cluster_EDS {
   318  				if cluster.EdsClusterConfig == nil {
   319  					cluster.EdsClusterConfig = &envoy_config_cluster.Cluster_EdsClusterConfig{}
   320  				}
   321  				if cluster.EdsClusterConfig.EdsConfig == nil {
   322  					cluster.EdsClusterConfig.EdsConfig = envoy.CiliumXDSConfigSource
   323  				}
   324  			}
   325  
   326  			if cluster.LoadAssignment != nil {
   327  				qualifyEDSEndpoints(cecNamespace, cecName, cluster.LoadAssignment)
   328  			}
   329  
   330  			name := cluster.Name
   331  			cluster.Name, _ = api.ResourceQualifiedName(cecNamespace, cecName, name)
   332  
   333  			// Check for duplicate after the name has been qualified
   334  			for i := range resources.Clusters {
   335  				if cluster.Name == resources.Clusters[i].Name {
   336  					return envoy.Resources{}, fmt.Errorf("duplicate Cluster name %q", cluster.Name)
   337  				}
   338  			}
   339  
   340  			if validate {
   341  				if err := cluster.Validate(); err != nil {
   342  					return envoy.Resources{}, fmt.Errorf("failed to validate Cluster %q (%w): %s", cluster.Name, err, cluster.String())
   343  				}
   344  			}
   345  			resources.Clusters = append(resources.Clusters, cluster)
   346  
   347  			r.logger.Debugf("ParseResources: Parsed cluster %q: %v", name, cluster)
   348  
   349  		case envoy.EndpointTypeURL:
   350  			endpoints, ok := message.(*envoy_config_endpoint.ClusterLoadAssignment)
   351  			if !ok {
   352  				return envoy.Resources{}, fmt.Errorf("invalid type for Route: %T", message)
   353  			}
   354  			// Check that a Cluster name is provided
   355  			if endpoints.ClusterName == "" {
   356  				return envoy.Resources{}, fmt.Errorf("unspecified ClusterLoadAssignment cluster_name")
   357  			}
   358  
   359  			name := endpoints.ClusterName
   360  			qualifyEDSEndpoints(cecNamespace, cecName, endpoints)
   361  
   362  			// Check for duplicate after the name has been qualified
   363  			for i := range resources.Endpoints {
   364  				if endpoints.ClusterName == resources.Endpoints[i].ClusterName {
   365  					return envoy.Resources{}, fmt.Errorf("duplicate cluster_name %q", endpoints.ClusterName)
   366  				}
   367  			}
   368  
   369  			if validate {
   370  				if err := endpoints.Validate(); err != nil {
   371  					return envoy.Resources{}, fmt.Errorf("failed to validate ClusterLoadAssignment for cluster %q (%w): %s", endpoints.ClusterName, err, endpoints.String())
   372  				}
   373  			}
   374  			resources.Endpoints = append(resources.Endpoints, endpoints)
   375  
   376  			r.logger.Debugf("ParseResources: Parsed endpoints for cluster %q: %v", name, endpoints)
   377  
   378  		case envoy.SecretTypeURL:
   379  			secret, ok := message.(*envoy_config_tls.Secret)
   380  			if !ok {
   381  				return envoy.Resources{}, fmt.Errorf("invalid type for Secret: %T", message)
   382  			}
   383  			// Check that a Secret name is provided
   384  			if secret.Name == "" {
   385  				return envoy.Resources{}, fmt.Errorf("unspecified Secret name")
   386  			}
   387  
   388  			name := secret.Name
   389  			secret.Name, _ = api.ResourceQualifiedName(cecNamespace, cecName, name)
   390  
   391  			// Check for duplicate after the name has been qualified
   392  			for i := range resources.Secrets {
   393  				if secret.Name == resources.Secrets[i].Name {
   394  					return envoy.Resources{}, fmt.Errorf("duplicate Secret name %q", secret.Name)
   395  				}
   396  			}
   397  
   398  			if validate {
   399  				if err := secret.Validate(); err != nil {
   400  					return envoy.Resources{}, fmt.Errorf("failed to validate Secret for cluster %q: %w", secret.Name, err)
   401  				}
   402  			}
   403  			resources.Secrets = append(resources.Secrets, secret)
   404  
   405  			r.logger.Debugf("ParseResources: Parsed secret: %s", name)
   406  
   407  		default:
   408  			return envoy.Resources{}, fmt.Errorf("unsupported type: %s", typeURL)
   409  		}
   410  	}
   411  
   412  	// Allocate TPROXY ports for listeners without address.
   413  	// Do this only after all other possible error cases.
   414  	for _, listener := range resources.Listeners {
   415  		// Figure out if this is an internal listener
   416  		isInternalListener := listener.GetInternalListener() != nil
   417  
   418  		if !isInternalListener {
   419  			if listener.GetAddress() == nil {
   420  				listenerName := listener.Name
   421  				port, err := r.portAllocator.AllocateCRDProxyPort(listenerName)
   422  				if err != nil || port == 0 {
   423  					return envoy.Resources{}, fmt.Errorf("listener port allocation for %q failed: %w", listenerName, err)
   424  				}
   425  				if resources.PortAllocationCallbacks == nil {
   426  					resources.PortAllocationCallbacks = make(map[string]func(context.Context) error)
   427  				}
   428  				if newResources {
   429  					resources.PortAllocationCallbacks[listenerName] = func(ctx context.Context) error { return r.portAllocator.AckProxyPort(ctx, listenerName) }
   430  				} else {
   431  					resources.PortAllocationCallbacks[listenerName] = func(_ context.Context) error { return r.portAllocator.ReleaseProxyPort(listenerName) }
   432  				}
   433  
   434  				listener.Address, listener.AdditionalAddresses = envoy.GetLocalListenerAddresses(port, option.Config.IPv4Enabled(), option.Config.IPv6Enabled())
   435  			}
   436  
   437  			// Inject Cilium bpf metadata listener filter, if not already present.
   438  			// This must be done after listener address/port is already set.
   439  			found := false
   440  			for _, lf := range listener.ListenerFilters {
   441  				if lf.Name == ciliumBPFMetadataListenerFilterName {
   442  					found = true
   443  					break
   444  				}
   445  			}
   446  			if !found {
   447  				// Get the listener port from the listener's (main) address
   448  				port := uint16(listener.GetAddress().GetSocketAddress().GetPortValue())
   449  
   450  				listener.ListenerFilters = append(listener.ListenerFilters, r.getBPFMetadataListenerFilter(useOriginalSourceAddr, isL7LB, port))
   451  			}
   452  		}
   453  
   454  		if validate {
   455  			if err := listener.Validate(); err != nil {
   456  				return envoy.Resources{}, fmt.Errorf("failed to validate Listener %q (%w): %s", listener.Name, err, listener.String())
   457  			}
   458  		}
   459  	}
   460  
   461  	// Validate that internal listeners exist
   462  	for _, cluster := range resources.Clusters {
   463  		if cluster.LoadAssignment != nil {
   464  			if err := validateEDSEndpoints(cecNamespace, cecName, cluster.LoadAssignment, resources.Listeners); err != nil {
   465  				return envoy.Resources{}, fmt.Errorf("ParseResources: Cluster refers to missing internal listener %q (%w): %s", cluster.Name, err, cluster.String())
   466  			}
   467  		}
   468  
   469  	}
   470  	for _, endpoints := range resources.Endpoints {
   471  		if err := validateEDSEndpoints(cecNamespace, cecName, endpoints, resources.Listeners); err != nil {
   472  			return envoy.Resources{}, fmt.Errorf("ParseResources: Endpoint refers to missing internal listener %q (%w): %s", endpoints.ClusterName, err, endpoints.String())
   473  		}
   474  	}
   475  
   476  	if injectCiliumUpstreamFilters {
   477  		for _, cluster := range resources.Clusters {
   478  			opts := &envoy_config_upstream.HttpProtocolOptions{}
   479  
   480  			if cluster.TypedExtensionProtocolOptions == nil {
   481  				cluster.TypedExtensionProtocolOptions = make(map[string]*anypb.Any)
   482  			}
   483  
   484  			a := cluster.TypedExtensionProtocolOptions[httpProtocolOptionsType]
   485  			if a != nil {
   486  				if err := a.UnmarshalTo(opts); err != nil {
   487  					return envoy.Resources{}, fmt.Errorf("failed to unmarshal HttpProtocolOptions: %w", err)
   488  				}
   489  			}
   490  
   491  			// Figure out if upstream supports ALPN so that we can add missing upstream
   492  			// protocol config
   493  			supportsALPN := false
   494  			ts := cluster.GetTransportSocket()
   495  			if ts != nil {
   496  				tc := ts.GetTypedConfig()
   497  				if tc == nil {
   498  					return envoy.Resources{}, fmt.Errorf("Transport socket has no type: %s", ts.String())
   499  				}
   500  				switch tc.GetTypeUrl() {
   501  				case upstreamTlsContextTypeURL, quicUpstreamTransportTypeURL:
   502  					supportsALPN = true
   503  				}
   504  			}
   505  			injected, err := injectCiliumUpstreamL7Filter(opts, supportsALPN)
   506  			if err != nil {
   507  				return envoy.Resources{}, fmt.Errorf("failed to inject upstream filters for cluster %q: %w", cluster.Name, err)
   508  			}
   509  			if injected {
   510  				cluster.TypedExtensionProtocolOptions[httpProtocolOptionsType] = toAny(opts)
   511  				if validate {
   512  					if err := cluster.Validate(); err != nil {
   513  						return envoy.Resources{}, fmt.Errorf("failed to validate Cluster %q after injecting Cilium upstream filters (%s): %w", cluster.Name, cluster.String(), err)
   514  					}
   515  				}
   516  			}
   517  		}
   518  	}
   519  
   520  	return resources, nil
   521  }
   522  
   523  // 'l7lb' triggers the upstream mark to embed source pod EndpointID instead of source security ID
   524  func (r *cecResourceParser) getBPFMetadataListenerFilter(useOriginalSourceAddr bool, l7lb bool, proxyPort uint16) *envoy_config_listener.ListenerFilter {
   525  	conf := &cilium.BpfMetadata{
   526  		IsIngress:                false,
   527  		UseOriginalSourceAddress: useOriginalSourceAddr,
   528  		BpfRoot:                  bpf.BPFFSRoot(),
   529  		IsL7Lb:                   l7lb,
   530  		ProxyId:                  uint32(proxyPort),
   531  	}
   532  
   533  	// Set Ingress source addresses if configuring for L7 LB.  One of these will be used when
   534  	// useOriginalSourceAddr is false, or when the source is known to not be from the local node
   535  	// (in such a case use of the original source address would lead to broken routing for the
   536  	// return traffic, as it would not be sent to the this node where upstream connection
   537  	// originates from).
   538  	//
   539  	// Note: This means that all non-local traffic will be identified by the destination to be
   540  	// coming from/via "Ingress", even if the listener is not an Ingress listener.
   541  	// We could refrain from using these ingress addresses in such cases, but then the upstream
   542  	// traffic would come from an (other) host IP, which is even worse.
   543  	//
   544  	// One solution to this dilemma would be to never configure these addresses if
   545  	// useOriginalSourceAddr is true and let such traffic fail.
   546  	if l7lb {
   547  		if r.ingressIPv4 != nil {
   548  			conf.Ipv4SourceAddress = r.ingressIPv4.String()
   549  			// Enforce ingress policy for Ingress
   550  			conf.EnforcePolicyOnL7Lb = true
   551  		}
   552  		if r.ingressIPv6 != nil {
   553  			conf.Ipv6SourceAddress = r.ingressIPv6.String()
   554  			// Enforce ingress policy for Ingress
   555  			conf.EnforcePolicyOnL7Lb = true
   556  		}
   557  		r.logger.Debugf("%s: ipv4_source_address: %s", ciliumBPFMetadataListenerFilterName, conf.GetIpv4SourceAddress())
   558  		r.logger.Debugf("%s: ipv6_source_address: %s", ciliumBPFMetadataListenerFilterName, conf.GetIpv6SourceAddress())
   559  	}
   560  
   561  	return &envoy_config_listener.ListenerFilter{
   562  		Name: ciliumBPFMetadataListenerFilterName,
   563  		ConfigType: &envoy_config_listener.ListenerFilter_TypedConfig{
   564  			TypedConfig: toAny(conf),
   565  		},
   566  	}
   567  }
   568  
   569  // qualifyAddress finds if there is a ServerListenerName in the address and qualifies it
   570  func qualifyAddress(namespace, name string, address *envoy_config_core.Address) {
   571  	internalAddress := address.GetEnvoyInternalAddress()
   572  	if internalAddress != nil {
   573  		if x, ok := internalAddress.GetAddressNameSpecifier().(*envoy_config_core.EnvoyInternalAddress_ServerListenerName); ok && x.ServerListenerName != "" {
   574  			x.ServerListenerName, _ =
   575  				api.ResourceQualifiedName(namespace, name, x.ServerListenerName)
   576  		}
   577  	}
   578  }
   579  
   580  // qualifyEDSEndpoints qualifies resource names in a ClusterLoadAssignment (aka EDS endpoint)
   581  func qualifyEDSEndpoints(namespace, name string, eds *envoy_config_endpoint.ClusterLoadAssignment) {
   582  	eds.ClusterName, _ = api.ResourceQualifiedName(namespace, name, eds.ClusterName)
   583  
   584  	for _, cla := range eds.Endpoints {
   585  		for _, lbe := range cla.LbEndpoints {
   586  			endpoint := lbe.GetEndpoint()
   587  			if endpoint != nil {
   588  				qualifyAddress(namespace, name, endpoint.Address)
   589  			}
   590  			for i := range endpoint.AdditionalAddresses {
   591  				qualifyAddress(namespace, name, endpoint.AdditionalAddresses[i].Address)
   592  			}
   593  		}
   594  	}
   595  }
   596  
   597  // validateAddress checks that the referred to internal listener is specified, if it is in the same CRD
   598  func validateAddress(namespace, name string, address *envoy_config_core.Address,
   599  	listeners []*envoy_config_listener.Listener) error {
   600  	internalAddress := address.GetEnvoyInternalAddress()
   601  	if internalAddress != nil {
   602  		if x, ok := internalAddress.GetAddressNameSpecifier().(*envoy_config_core.EnvoyInternalAddress_ServerListenerName); ok && x.ServerListenerName != "" {
   603  
   604  			internalNamespace, internalName, listenerName := api.ParseQualifiedName(x.ServerListenerName)
   605  			if internalNamespace == namespace && internalName == name {
   606  				found := false
   607  				// Check that the listener exists and is an internal listener
   608  				for i := range listeners {
   609  					if x.ServerListenerName == listeners[i].Name &&
   610  						listeners[i].GetInternalListener() != nil {
   611  						found = true
   612  						break
   613  					}
   614  				}
   615  				if !found {
   616  					return fmt.Errorf("missing internal listener: %s", listenerName)
   617  				}
   618  			}
   619  		}
   620  	}
   621  	return nil
   622  }
   623  
   624  // validateEDSEndpoints checks internal listener references, if any
   625  func validateEDSEndpoints(namespace, name string, eds *envoy_config_endpoint.ClusterLoadAssignment,
   626  	listeners []*envoy_config_listener.Listener) error {
   627  	for _, cla := range eds.Endpoints {
   628  		for _, lbe := range cla.LbEndpoints {
   629  			endpoint := lbe.GetEndpoint()
   630  			if endpoint != nil {
   631  				err := validateAddress(namespace, name, endpoint.Address, listeners)
   632  				if err != nil {
   633  					return err
   634  				}
   635  			}
   636  			for i := range endpoint.AdditionalAddresses {
   637  				err := validateAddress(namespace, name, endpoint.AdditionalAddresses[i].Address, listeners)
   638  				if err != nil {
   639  					return err
   640  				}
   641  			}
   642  		}
   643  	}
   644  	return nil
   645  }
   646  
   647  func qualifyTcpProxyResourceNames(namespace, name string, tcpProxy *envoy_config_tcp.TcpProxy) (updated bool) {
   648  	switch c := tcpProxy.GetClusterSpecifier().(type) {
   649  	case *envoy_config_tcp.TcpProxy_Cluster:
   650  		if c != nil {
   651  			c.Cluster, updated = api.ResourceQualifiedName(namespace, name, c.Cluster)
   652  		}
   653  	case *envoy_config_tcp.TcpProxy_WeightedClusters:
   654  		if c != nil {
   655  			for _, wc := range c.WeightedClusters.Clusters {
   656  				var nameUpdated bool
   657  				wc.Name, nameUpdated = api.ResourceQualifiedName(namespace, name, wc.Name)
   658  				if nameUpdated {
   659  					updated = true
   660  				}
   661  			}
   662  		}
   663  	}
   664  	return updated
   665  }
   666  
   667  func qualifyRouteConfigurationResourceNames(namespace, name string, routeConfig *envoy_config_route.RouteConfiguration) (updated bool) {
   668  	// Strictly not a reference, and may be an empty string
   669  	routeConfig.Name, updated = api.ResourceQualifiedName(namespace, name, routeConfig.Name, api.ForceNamespace)
   670  
   671  	for _, vhost := range routeConfig.VirtualHosts {
   672  		var nameUpdated bool
   673  		vhost.Name, nameUpdated = api.ResourceQualifiedName(namespace, name, vhost.Name, api.ForceNamespace)
   674  		if nameUpdated {
   675  			updated = true
   676  		}
   677  		for _, rt := range vhost.Routes {
   678  			if action := rt.GetRoute(); action != nil {
   679  				if clusterName := action.GetCluster(); clusterName != "" {
   680  					action.GetClusterSpecifier().(*envoy_config_route.RouteAction_Cluster).Cluster, nameUpdated = api.ResourceQualifiedName(namespace, name, clusterName)
   681  					if nameUpdated {
   682  						updated = true
   683  					}
   684  				}
   685  				for _, r := range action.GetRequestMirrorPolicies() {
   686  					if clusterName := r.GetCluster(); clusterName != "" {
   687  						r.Cluster, nameUpdated = api.ResourceQualifiedName(namespace, name, clusterName)
   688  						if nameUpdated {
   689  							updated = true
   690  						}
   691  					}
   692  				}
   693  				if weightedClusters := action.GetWeightedClusters(); weightedClusters != nil {
   694  					for _, cluster := range weightedClusters.GetClusters() {
   695  						cluster.Name, nameUpdated = api.ResourceQualifiedName(namespace, name, cluster.Name)
   696  						if nameUpdated {
   697  							updated = true
   698  						}
   699  					}
   700  				}
   701  			}
   702  		}
   703  	}
   704  	return updated
   705  }
   706  
   707  // injectCiliumL7Filter injects the Cilium HTTP filter just before the HTTP Router filter
   708  func injectCiliumL7Filter(hcmConfig *envoy_config_http.HttpConnectionManager) bool {
   709  	foundCiliumL7Filter := false
   710  
   711  	for j, httpFilter := range hcmConfig.HttpFilters {
   712  		switch httpFilter.Name {
   713  		case ciliumL7FilterName:
   714  			foundCiliumL7Filter = true
   715  		case envoyRouterFilterName:
   716  			if !foundCiliumL7Filter {
   717  				hcmConfig.HttpFilters = append(hcmConfig.HttpFilters[:j+1], hcmConfig.HttpFilters[j:]...)
   718  				hcmConfig.HttpFilters[j] = envoy.GetCiliumHttpFilter()
   719  				return true
   720  			}
   721  		}
   722  	}
   723  
   724  	return false
   725  }
   726  
   727  func qualifyHttpFilters(cecNamespace string, cecName string, hcmConfig *envoy_config_http.HttpConnectionManager) bool {
   728  	updated := false
   729  
   730  	for _, httpFilter := range hcmConfig.HttpFilters {
   731  		switch h := httpFilter.ConfigType.(type) {
   732  		case *envoy_config_http.HttpFilter_TypedConfig:
   733  			any, err := h.TypedConfig.UnmarshalNew()
   734  			if err != nil {
   735  				continue
   736  			}
   737  
   738  			switch httpFilterConfig := any.(type) {
   739  			case *envoy_config_healthcheck.HealthCheck:
   740  				clusters := map[string]*envoy_config_types.Percent{}
   741  				for c, p := range httpFilterConfig.ClusterMinHealthyPercentages {
   742  					clusterName := c
   743  					updatedClusterName, nameUpdated := api.ResourceQualifiedName(cecNamespace, cecName, c)
   744  					if nameUpdated {
   745  						updated = true
   746  						clusterName = updatedClusterName
   747  					}
   748  
   749  					clusters[clusterName] = p
   750  				}
   751  
   752  				if updated {
   753  					httpFilterConfig.ClusterMinHealthyPercentages = clusters
   754  					h.TypedConfig = toAny(httpFilterConfig)
   755  				}
   756  			}
   757  		}
   758  	}
   759  
   760  	return updated
   761  }
   762  
   763  // injectCiliumUpstreamL7Filter injects the Cilium HTTP filter just before the Upstream Codec filter
   764  func injectCiliumUpstreamL7Filter(opts *envoy_config_upstream.HttpProtocolOptions, supportsALPN bool) (bool, error) {
   765  	filters := opts.GetHttpFilters()
   766  	if filters == nil {
   767  		filters = make([]*envoy_config_http.HttpFilter, 0, 2)
   768  	}
   769  
   770  	foundCiliumL7Filter := false
   771  	codecFilterIndex := -1
   772  	for j, filter := range filters {
   773  		// no filters allowed after upstream codec filter
   774  		if codecFilterIndex >= 0 {
   775  			return false, fmt.Errorf("filter after codec filter: %s", filter.String())
   776  		}
   777  		if len(filter.Name) == 0 {
   778  			return false, fmt.Errorf("filter has no name: %s", filter.String())
   779  		}
   780  		tc := filter.GetTypedConfig()
   781  		if tc == nil {
   782  			return false, fmt.Errorf("filter has no type: %s", filter.String())
   783  		}
   784  		switch tc.GetTypeUrl() {
   785  		case ciliumL7FilterTypeURL:
   786  			foundCiliumL7Filter = true
   787  		case upstreamCodecFilterTypeURL:
   788  			codecFilterIndex = j
   789  		}
   790  	}
   791  	changed := false
   792  	if !foundCiliumL7Filter {
   793  		j := codecFilterIndex
   794  		if j >= 0 {
   795  			filters = append(filters[:j+1], filters[j:]...)
   796  			filters[j] = envoy.GetCiliumHttpFilter()
   797  		} else {
   798  			filters = append(filters, envoy.GetCiliumHttpFilter())
   799  		}
   800  		changed = true
   801  	}
   802  
   803  	// Add CodecFilter if missing
   804  	if codecFilterIndex < 0 {
   805  		filters = append(filters, envoy.GetUpstreamCodecFilter())
   806  		changed = true
   807  	}
   808  
   809  	if changed {
   810  		opts.HttpFilters = filters
   811  	}
   812  
   813  	// Add required HttpProtocolOptions fields
   814  	if opts.GetUpstreamProtocolOptions() == nil {
   815  		if supportsALPN {
   816  			// Use auto config for upstream transports that support ALPN
   817  			opts.UpstreamProtocolOptions = &envoy_config_upstream.HttpProtocolOptions_AutoConfig{
   818  				AutoConfig: &envoy_config_upstream.HttpProtocolOptions_AutoHttpConfig{},
   819  			}
   820  		} else {
   821  			// Use downstream protocol for upstream transports that do not support ALPN
   822  			opts.UpstreamProtocolOptions = &envoy_config_upstream.HttpProtocolOptions_UseDownstreamProtocolConfig{
   823  				UseDownstreamProtocolConfig: &envoy_config_upstream.HttpProtocolOptions_UseDownstreamHttpConfig{
   824  					Http2ProtocolOptions: &envoy_config_core.Http2ProtocolOptions{},
   825  				},
   826  			}
   827  		}
   828  		changed = true
   829  	}
   830  
   831  	if err := opts.Validate(); err != nil {
   832  		return false, fmt.Errorf("failed to validate HttpProtocolOptions after injecting Cilium upstream filters (%s): %w", opts.String(), err)
   833  	}
   834  
   835  	return changed, nil
   836  }
   837  
   838  func fillInTlsContextXDS(cecNamespace string, cecName string, tls *envoy_config_tls.CommonTlsContext) (updated bool) {
   839  	qualify := func(sc *envoy_config_tls.SdsSecretConfig) {
   840  		if sc.SdsConfig == nil {
   841  			sc.SdsConfig = envoy.CiliumXDSConfigSource
   842  			updated = true
   843  		}
   844  		var nameUpdated bool
   845  		sc.Name, nameUpdated = api.ResourceQualifiedName(cecNamespace, cecName, sc.Name)
   846  		if nameUpdated {
   847  			updated = true
   848  		}
   849  	}
   850  
   851  	if tls != nil {
   852  		for _, sc := range tls.TlsCertificateSdsSecretConfigs {
   853  			qualify(sc)
   854  		}
   855  		if sc := tls.GetValidationContextSdsSecretConfig(); sc != nil {
   856  			qualify(sc)
   857  		}
   858  	}
   859  	return updated
   860  }
   861  
   862  func fillInTransportSocketXDS(cecNamespace string, cecName string, ts *envoy_config_core.TransportSocket) {
   863  	if ts != nil {
   864  		if tc := ts.GetTypedConfig(); tc != nil {
   865  			any, err := tc.UnmarshalNew()
   866  			if err != nil {
   867  				return
   868  			}
   869  			var updated *anypb.Any
   870  			switch tls := any.(type) {
   871  			case *envoy_config_tls.DownstreamTlsContext:
   872  				if fillInTlsContextXDS(cecNamespace, cecName, tls.CommonTlsContext) {
   873  					updated = toAny(tls)
   874  				}
   875  			case *envoy_config_tls.UpstreamTlsContext:
   876  				if fillInTlsContextXDS(cecNamespace, cecName, tls.CommonTlsContext) {
   877  					updated = toAny(tls)
   878  				}
   879  			}
   880  			if updated != nil {
   881  				ts.ConfigType = &envoy_config_core.TransportSocket_TypedConfig{
   882  					TypedConfig: updated,
   883  				}
   884  			}
   885  		}
   886  	}
   887  }
   888  
   889  func toAny(message proto.Message) *anypb.Any {
   890  	a, err := anypb.New(message)
   891  	if err != nil {
   892  		return nil
   893  	}
   894  	return a
   895  }