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 }