From e733c8afc5dc90155c6aa87ac7b591d2945f5f77 Mon Sep 17 00:00:00 2001 From: "Huabing (Robin) Zhao" Date: Tue, 16 Jun 2026 12:35:34 +0800 Subject: [PATCH 1/2] support consistent hash for non-route backends Signed-off-by: Huabing (Robin) Zhao --- internal/xds/translator/cluster.go | 12 ++++- internal/xds/translator/cluster_test.go | 48 +++++++++++++++++++ internal/xds/translator/route.go | 8 +++- .../testdata/in/xds-ir/ext-auth-retry.yaml | 6 +++ .../out/xds-ir/ext-auth-retry.clusters.yaml | 9 ++-- .../jwt-with-backend-tls-retry.clusters.yaml | 3 ++ ...idc-backend-cluster-provider.clusters.yaml | 3 ++ ...dc-provider-traffic-features.clusters.yaml | 3 ++ release-notes/current.yaml | 1 + 9 files changed, 87 insertions(+), 6 deletions(-) diff --git a/internal/xds/translator/cluster.go b/internal/xds/translator/cluster.go index 1bd78bd0f6..0982a00a73 100644 --- a/internal/xds/translator/cluster.go +++ b/internal/xds/translator/cluster.go @@ -15,6 +15,7 @@ import ( clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" endpointv3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" + routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" dfpv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/dynamic_forward_proxy/v3" commondfpv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/common/dynamic_forward_proxy/v3" codecv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/upstream_codec/v3" @@ -1049,13 +1050,22 @@ func buildTypedExtensionProtocolOptions(args *xdsClusterArgs, requiresAutoHTTPCo requiresHTTPFilters := (len(args.settings) > 0 && args.settings[0].Filters != nil && args.settings[0].Filters.CredentialInjection != nil) || args.admissionControl != nil + var clusterHashPolicy []*routev3.RouteAction_HashPolicy + if !args.isRoute && args.loadBalancer != nil { + clusterHashPolicy = buildConsistentHashPolicy(args.loadBalancer.ConsistentHash) + } + requiredHTTPProtocolOptions := args.useClientProtocol || requiresAutoHTTPConfig || - requiresCommonHTTPOptions || requiresHTTP1Options || requiresHTTP2Options || requiresHTTPFilters || requiresAutoSNI || forceHTTP1UpstreamProtocol + requiresCommonHTTPOptions || requiresHTTP1Options || requiresHTTP2Options || requiresHTTPFilters || requiresAutoSNI || + forceHTTP1UpstreamProtocol || len(clusterHashPolicy) > 0 if !requiredHTTPProtocolOptions { return nil, nil, nil } protocolOptions := httpv3.HttpProtocolOptions{} + if len(clusterHashPolicy) > 0 { + protocolOptions.HashPolicy = clusterHashPolicy + } if requiresCommonHTTPOptions { protocolOptions.CommonHttpProtocolOptions = &corev3.HttpProtocolOptions{} if args.timeout != nil && args.timeout.HTTP != nil { diff --git a/internal/xds/translator/cluster_test.go b/internal/xds/translator/cluster_test.go index 368ef6cad2..9b5e7b93df 100644 --- a/internal/xds/translator/cluster_test.go +++ b/internal/xds/translator/cluster_test.go @@ -15,6 +15,7 @@ import ( cswrrv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/client_side_weighted_round_robin/v3" override_hostv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/override_host/v3" wrr_localityv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/wrr_locality/v3" + httpv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" "google.golang.org/protobuf/encoding/protojson" @@ -251,6 +252,53 @@ func TestBuildClusterWithBackendUtilization(t *testing.T) { require.Equal(t, "type.googleapis.com/envoy.extensions.load_balancing_policies.client_side_weighted_round_robin.v3.ClientSideWeightedRoundRobin", policy.TypedExtensionConfig.TypedConfig.TypeUrl) } +func TestBuildXdsClusterWithClusterLevelHashPolicy(t *testing.T) { + sourceIP := true + args := &xdsClusterArgs{ + name: "test-cluster-consistent-hash", + endpointType: EndpointTypeStatic, + settings: []*ir.DestinationSetting{{ + Endpoints: []*ir.DestinationEndpoint{{Host: "127.0.0.1", Port: 8080}}, + }}, + loadBalancer: &ir.LoadBalancer{ + ConsistentHash: &ir.ConsistentHash{ + SourceIP: &sourceIP, + }, + }, + } + + result, err := buildXdsCluster(args) + require.NoError(t, err) + + options := &httpv3.HttpProtocolOptions{} + require.NotNil(t, result.cluster.TypedExtensionProtocolOptions) + require.NoError(t, result.cluster.TypedExtensionProtocolOptions[extensionOptionsKey].UnmarshalTo(options)) + require.Len(t, options.HashPolicy, 1) + require.True(t, options.HashPolicy[0].GetConnectionProperties().GetSourceIp()) +} + +func TestBuildXdsRouteClusterWithoutClusterLevelHashPolicy(t *testing.T) { + sourceIP := true + args := &xdsClusterArgs{ + name: "test-route-cluster-consistent-hash", + endpointType: EndpointTypeStatic, + settings: []*ir.DestinationSetting{{ + Endpoints: []*ir.DestinationEndpoint{{Host: "127.0.0.1", Port: 8080}}, + }}, + loadBalancer: &ir.LoadBalancer{ + ConsistentHash: &ir.ConsistentHash{ + SourceIP: &sourceIP, + }, + }, + isRoute: true, + } + + result, err := buildXdsCluster(args) + require.NoError(t, err) + + require.Nil(t, result.cluster.TypedExtensionProtocolOptions) +} + func TestBuildClusterWithBackendUtilizationSlowStart(t *testing.T) { window := 5 * time.Second args := &xdsClusterArgs{ diff --git a/internal/xds/translator/route.go b/internal/xds/translator/route.go index 4426dc2efe..0e2cbc3506 100644 --- a/internal/xds/translator/route.go +++ b/internal/xds/translator/route.go @@ -708,7 +708,13 @@ func buildHashPolicy(httpRoute *ir.HTTPRoute) []*routev3.RouteAction_HashPolicy return nil } - ch := httpRoute.Traffic.LoadBalancer.ConsistentHash + return buildConsistentHashPolicy(httpRoute.Traffic.LoadBalancer.ConsistentHash) +} + +func buildConsistentHashPolicy(ch *ir.ConsistentHash) []*routev3.RouteAction_HashPolicy { + if ch == nil { + return nil + } switch { case ch.Headers != nil: diff --git a/internal/xds/translator/testdata/in/xds-ir/ext-auth-retry.yaml b/internal/xds/translator/testdata/in/xds-ir/ext-auth-retry.yaml index 9b466d0157..4a44b236e1 100644 --- a/internal/xds/translator/testdata/in/xds-ir/ext-auth-retry.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/ext-auth-retry.yaml @@ -29,6 +29,9 @@ http: extAuth: name: securitypolicy/default/policy-for-http-route-1 traffic: + loadBalancer: + consistentHash: + sourceIP: true retry: numRetries: 2 perRetry: @@ -86,6 +89,9 @@ http: extAuth: name: securitypolicy/default/policy-for-http-route-1 traffic: + loadBalancer: + consistentHash: + sourceIP: true retry: numRetries: 2 perRetry: diff --git a/internal/xds/translator/testdata/out/xds-ir/ext-auth-retry.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/ext-auth-retry.clusters.yaml index 5bf2b6b57d..648d9ce187 100644 --- a/internal/xds/translator/testdata/out/xds-ir/ext-auth-retry.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/ext-auth-retry.clusters.yaml @@ -82,11 +82,9 @@ loadBalancingPolicy: policies: - typedExtensionConfig: - name: envoy.load_balancing_policies.least_request + name: envoy.load_balancing_policies.maglev typedConfig: - '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest - localityLbConfig: - localityWeightedLbConfig: {} + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.maglev.v3.Maglev name: securitypolicy/default/policy-for-http-route-1/default/grpc-backend perConnectionBufferLimitBytes: 32768 type: EDS @@ -97,6 +95,9 @@ http2ProtocolOptions: initialConnectionWindowSize: 1048576 initialStreamWindowSize: 65536 + hashPolicy: + - connectionProperties: + sourceIp: true - circuitBreakers: thresholds: - maxRetries: 1024 diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.clusters.yaml index d450036306..ba89cad1c2 100644 --- a/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.clusters.yaml @@ -125,6 +125,9 @@ idleTimeout: 16s maxConnectionDuration: 17s maxRequestsPerConnection: 1 + hashPolicy: + - connectionProperties: + sourceIp: true upstreamConnectionOptions: tcpKeepalive: keepaliveInterval: 60 diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.clusters.yaml index fb91e542dc..ba54932f8b 100644 --- a/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/oidc-backend-cluster-provider.clusters.yaml @@ -125,6 +125,9 @@ idleTimeout: 16s maxConnectionDuration: 17s maxRequestsPerConnection: 1 + hashPolicy: + - connectionProperties: + sourceIp: true upstreamConnectionOptions: tcpKeepalive: keepaliveInterval: 60 diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc-provider-traffic-features.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc-provider-traffic-features.clusters.yaml index dd15877c4b..103a3d6b35 100644 --- a/internal/xds/translator/testdata/out/xds-ir/oidc-provider-traffic-features.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/oidc-provider-traffic-features.clusters.yaml @@ -103,6 +103,9 @@ maxRequestsPerConnection: 1 explicitHttpConfig: httpProtocolOptions: {} + hashPolicy: + - connectionProperties: + sourceIp: true upstreamConnectionOptions: tcpKeepalive: keepaliveInterval: 60 diff --git a/release-notes/current.yaml b/release-notes/current.yaml index 339fc95010..1c5b869333 100644 --- a/release-notes/current.yaml +++ b/release-notes/current.yaml @@ -36,6 +36,7 @@ bug fixes: | Fixed Kubernetes provider namespace-scoped watches to always include the controller namespace so Envoy Gateway can read its own infrastructure resources. Fixed TLS secrets with non-canonical PEM formatting (e.g. unusual line endings) being passed verbatim to Envoy, which could cause BoringSSL errors such as `BAD_END_LINE`. Cert and key PEM data is now re-encoded to a canonical form before being delivered as xDS resources. Fixed `MaxStreamDuration` not being set on `CommonHttpProtocolOptions` for non-route cluster. + Fixed `ConsistentHash` load balancer settings on non-route service clusters, such as SecurityPolicy extAuth backends, by emitting cluster-level HTTP hash policies. Fixed `egctl x status all`/`xroute`/`xpolicy` failing when a Gateway API CRD (e.g. TCPRoute) is not installed in the cluster; missing CRDs are now skipped silently, or reported on stderr with `-v`. Fixed Kubernetes Service and ServiceImport `appProtocol` values `kubernetes.io/ws` and `kubernetes.io/wss` to force HTTP/1.1 upstream connections instead of negotiating HTTP/2, avoiding compatibility issues with WebSocket backends that do not support RFC 8441 extended CONNECT. Fixed an `ExternalName` Service referenced as a route backend producing an invalid xDS cluster (with an empty address) that failed IR validation and stalled config delivery for the whole snapshot. `ExternalName` Services are now explicitly rejected as backends with a `ResolvedRefs: False` route condition; use an Envoy Gateway `Backend` resource with an FQDN endpoint instead. From 279688da11fd913c82a814951d2faedab52f3503 Mon Sep 17 00:00:00 2001 From: "Huabing (Robin) Zhao" Date: Tue, 16 Jun 2026 12:50:32 +0800 Subject: [PATCH 2/2] update e2e tests Signed-off-by: Huabing (Robin) Zhao --- test/e2e/testdata/ext-auth-grpc-securitypolicy.yaml | 5 +++++ test/e2e/testdata/ext-auth-service.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/test/e2e/testdata/ext-auth-grpc-securitypolicy.yaml b/test/e2e/testdata/ext-auth-grpc-securitypolicy.yaml index f7dbc7d06b..9eb26d03c0 100644 --- a/test/e2e/testdata/ext-auth-grpc-securitypolicy.yaml +++ b/test/e2e/testdata/ext-auth-grpc-securitypolicy.yaml @@ -54,6 +54,11 @@ spec: - name: grpc-ext-auth namespace: gateway-conformance-infra port: 9002 + backendSettings: + loadBalancer: + type: ConsistentHash + consistentHash: + type: SourceIP --- apiVersion: gateway.networking.k8s.io/v1 kind: BackendTLSPolicy diff --git a/test/e2e/testdata/ext-auth-service.yaml b/test/e2e/testdata/ext-auth-service.yaml index 0ebce42f38..169801c1b5 100644 --- a/test/e2e/testdata/ext-auth-service.yaml +++ b/test/e2e/testdata/ext-auth-service.yaml @@ -56,7 +56,7 @@ metadata: name: envoy-ext-auth namespace: gateway-conformance-infra spec: - replicas: 1 + replicas: 2 selector: matchLabels: app: envoy-ext-auth