Repository: envoyproxy/gateway
Pull request #3388 (2024-05-13 16:44:01)

bug: Route extension filters with different types but the same name and namespace aren't correctly cached
**What this PR does / why we need it**:
The provider caches `extensionRef` instances based on their name and namespace. But the actual object stored is an Unstructured instance, which means that if two instances of two different objects have the same name and namespace, then the cache would override the first with the second.

This PR adds a unit test to include this situation and fixes the current cache to also consider the extensionRef's type when caching it.

liorokman: /retest
liorokman: /retest

liorokman: /retest
liorokman: /retest

Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 2f37284cf..c7a5bee84 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -20,8 +20,6 @@ updates:
     ignore:
       # skip to update retest, because it won't work with the latest version
       - dependency-name: "envoyproxy/toolshed/gh-actions/retest"
-      # skip until https://github.com/codecov/feedback/issues/112 is resolved
-      - dependency-name: "codecov/codecov-action"
   - package-ecosystem: github-actions
     directory: /tools/github-actions/setup-deps
     schedule:
diff --git a/api/v1alpha1/clienttrafficpolicy_types.go b/api/v1alpha1/clienttrafficpolicy_types.go
index 440c01c6c..ea9d79018 100644
--- a/api/v1alpha1/clienttrafficpolicy_types.go
+++ b/api/v1alpha1/clienttrafficpolicy_types.go
@@ -108,6 +108,13 @@ type HeaderSettings struct {
 	// is encountered. The default action is to reject the request.
 	// +optional
 	WithUnderscoresAction *WithUnderscoresAction `json:"withUnderscoresAction,omitempty"`
+
+	// PreserveXRequestID configures Envoy to keep the X-Request-ID header if passed for a request that is edge
+	// (Edge request is the request from external clients to front Envoy) and not reset it, which is the current Envoy behaviour.
+	// It defaults to false.
+	//
+	// +optional
+	PreserveXRequestID *bool `json:"preserveXRequestID,omitempty"`
 }
 
 // WithUnderscoresAction configures the action to take when an HTTP header with underscores
diff --git a/api/v1alpha1/envoyproxy_helpers.go b/api/v1alpha1/envoyproxy_helpers.go
index 2bcc086cb..03f50d9c9 100644
--- a/api/v1alpha1/envoyproxy_helpers.go
+++ b/api/v1alpha1/envoyproxy_helpers.go
@@ -57,6 +57,23 @@ func DefaultEnvoyProxyHpaMetrics() []autoscalingv2.MetricSpec {
 	}
 }
 
+// NeedToSwitchPorts returns true if the EnvoyProxy needs to switch ports.
+func (e *EnvoyProxy) NeedToSwitchPorts() bool {
+	if e.Spec.Provider == nil {
+		return true
+	}
+
+	if e.Spec.Provider.Kubernetes == nil {
+		return true
+	}
+
+	if e.Spec.Provider.Kubernetes.UseListenerPortAsContainerPort == nil {
+		return true
+	}
+
+	return !*e.Spec.Provider.Kubernetes.UseListenerPortAsContainerPort
+}
+
 // GetEnvoyProxyKubeProvider returns the EnvoyProxyKubernetesProvider of EnvoyProxyProvider or
 // a default EnvoyProxyKubernetesProvider if unspecified. If EnvoyProxyProvider is not of
 // type "Kubernetes", a nil EnvoyProxyKubernetesProvider is returned.
diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go
index d8afe76ca..8ccf37460 100644
--- a/api/v1alpha1/envoyproxy_types.go
+++ b/api/v1alpha1/envoyproxy_types.go
@@ -264,6 +264,13 @@ type EnvoyProxyKubernetesProvider struct {
 	//
 	// +optional
 	EnvoyHpa *KubernetesHorizontalPodAutoscalerSpec `json:"envoyHpa,omitempty"`
+
+	// UseListenerPortAsContainerPort disables the port shifting feature in the Envoy Proxy.
+	// When set to false (default value), if the service port is a privileged port (1-1023), add a constant to the value converting it into an ephemeral port.
+	// This allows the container to bind to the port without needing a CAP_NET_BIND_SERVICE capability.
+	//
+	// +optional
+	UseListenerPortAsContainerPort *bool `json:"useListenerPortAsContainerPort,omitempty"`
 }
 
 // ProxyLogging defines logging parameters for managed proxies.
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index fd607c16a..1456fde63 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -1646,6 +1646,11 @@ func (in *EnvoyProxyKubernetesProvider) DeepCopyInto(out *EnvoyProxyKubernetesPr
 		*out = new(KubernetesHorizontalPodAutoscalerSpec)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.UseListenerPortAsContainerPort != nil {
+		in, out := &in.UseListenerPortAsContainerPort, &out.UseListenerPortAsContainerPort
+		*out = new(bool)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvoyProxyKubernetesProvider.
@@ -2473,6 +2478,11 @@ func (in *HeaderSettings) DeepCopyInto(out *HeaderSettings) {
 		*out = new(WithUnderscoresAction)
 		**out = **in
 	}
+	if in.PreserveXRequestID != nil {
+		in, out := &in.PreserveXRequestID, &out.PreserveXRequestID
+		*out = new(bool)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HeaderSettings.
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
index a9d3fb95f..6d4805ca0 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
@@ -147,6 +147,12 @@ spec:
                       EnableEnvoyHeaders configures Envoy Proxy to add the "X-Envoy-" headers to requests
                       and responses.
                     type: boolean
+                  preserveXRequestID:
+                    description: |-
+                      PreserveXRequestID configures Envoy to keep the X-Request-ID header if passed for a request that is edge
+                      (Edge request is the request from external clients to front Envoy) and not reset it, which is the current Envoy behaviour.
+                      It defaults to false.
+                    type: boolean
                   withUnderscoresAction:
                     description: |-
                       WithUnderscoresAction configures the action to take when an HTTP header with underscores
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml
index 0282aed70..08042e02d 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml
@@ -9980,6 +9980,12 @@ spec:
                         - message: loadBalancerIP can only be set for LoadBalancer
                             type
                           rule: '!has(self.loadBalancerIP) || self.type == ''LoadBalancer'''
+                      useListenerPortAsContainerPort:
+                        description: |-
+                          UseListenerPortAsContainerPort disables the port shifting feature in the Envoy Proxy.
+                          When set to false (default value), if the service port is a privileged port (1-1023), add a constant to the value converting it into an ephemeral port.
+                          This allows the container to bind to the port without needing a CAP_NET_BIND_SERVICE capability.
+                        type: boolean
                     type: object
                     x-kubernetes-validations:
                     - message: only one of envoyDeployment or envoyDaemonSet can be
diff --git a/internal/gatewayapi/clienttrafficpolicy.go b/internal/gatewayapi/clienttrafficpolicy.go
index 8cc054dee..f5465fcd7 100644
--- a/internal/gatewayapi/clienttrafficpolicy.go
+++ b/internal/gatewayapi/clienttrafficpolicy.go
@@ -547,6 +547,7 @@ func translateListenerHeaderSettings(headerSettings *egv1a1.HeaderSettings, http
 	httpIR.Headers = &ir.HeaderSettings{
 		EnableEnvoyHeaders:    ptr.Deref(headerSettings.EnableEnvoyHeaders, false),
 		WithUnderscoresAction: ir.WithUnderscoresAction(ptr.Deref(headerSettings.WithUnderscoresAction, egv1a1.WithUnderscoresActionRejectRequest)),
+		PreserveXRequestID:    ptr.Deref(headerSettings.PreserveXRequestID, false),
 	}
 }
 
diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go
index 2dfc04a18..0e2b6b45a 100644
--- a/internal/gatewayapi/helpers.go
+++ b/internal/gatewayapi/helpers.go
@@ -17,6 +17,7 @@ import (
 	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
 	"sigs.k8s.io/gateway-api/apis/v1alpha2"
 
+	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
 	"github.com/envoyproxy/gateway/internal/ir"
 	"github.com/envoyproxy/gateway/internal/utils"
 )
@@ -228,7 +229,13 @@ func GatewayClassOwnerLabel(name string) map[string]string {
 
 // servicePortToContainerPort translates a service port into an ephemeral
 // container port.
-func servicePortToContainerPort(servicePort int32) int32 {
+func servicePortToContainerPort(servicePort int32, envoyProxy *egv1a1.EnvoyProxy) int32 {
+	if envoyProxy != nil {
+		if !envoyProxy.NeedToSwitchPorts() {
+			return servicePort
+		}
+	}
+
 	// If the service port is a privileged port (1-1023)
 	// add a constant to the value converting it into an ephemeral port.
 	// This allows the container to bind to the port without needing a
diff --git a/internal/gatewayapi/listener.go b/internal/gatewayapi/listener.go
index 62d60b75f..0eb383c90 100644
--- a/internal/gatewayapi/listener.go
+++ b/internal/gatewayapi/listener.go
@@ -95,7 +95,7 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap
 			}
 			// Add the listener to the Xds IR
 			servicePort := &protocolPort{protocol: listener.Protocol, port: int32(listener.Port)}
-			containerPort := servicePortToContainerPort(servicePort.port)
+			containerPort := servicePortToContainerPort(int32(listener.Port), resources.EnvoyProxy)
 			switch listener.Protocol {
 			case gwapiv1.HTTPProtocolType, gwapiv1.HTTPSProtocolType:
 				irListener := &ir.HTTPListener{
@@ -136,7 +136,7 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap
 			// Add the listener to the Infra IR. Infra IR ports must have a unique port number per layer-4 protocol
 			// (TCP or UDP).
 			if !containsPort(foundPorts[irKey], servicePort) {
-				t.processInfraIRListener(listener, infraIR, irKey, servicePort)
+				t.processInfraIRListener(listener, infraIR, irKey, servicePort, containerPort)
 				foundPorts[irKey] = append(foundPorts[irKey], servicePort)
 			}
 		}
@@ -149,7 +149,7 @@ func (t *Translator) processProxyObservability(gw *gwapiv1.Gateway, xdsIR *ir.Xd
 	xdsIR.Metrics = processMetrics(envoyProxy)
 }
 
-func (t *Translator) processInfraIRListener(listener *ListenerContext, infraIR InfraIRMap, irKey string, servicePort *protocolPort) {
+func (t *Translator) processInfraIRListener(listener *ListenerContext, infraIR InfraIRMap, irKey string, servicePort *protocolPort, containerPort int32) {
 	var proto ir.ProtocolType
 	switch listener.Protocol {
 	case gwapiv1.HTTPProtocolType:
@@ -168,7 +168,7 @@ func (t *Translator) processInfraIRListener(listener *ListenerContext, infraIR I
 		Name:          irListenerPortName(proto, servicePort.port),
 		Protocol:      proto,
 		ServicePort:   servicePort.port,
-		ContainerPort: servicePortToContainerPort(servicePort.port),
+		ContainerPort: containerPort,
 	}
 
 	proxyListener := &ir.ProxyListener{
diff --git a/internal/ir/infra.go b/internal/ir/infra.go
index 60a26bddc..52672be64 100644
--- a/internal/ir/infra.go
+++ b/internal/ir/infra.go
@@ -222,8 +222,8 @@ func (p *ProxyInfra) Validate() error {
 				if listener.Ports[j].ServicePort < 1 || listener.Ports[j].ServicePort > 65353 {
 					errs = append(errs, errors.New("listener service port must be a valid port number"))
 				}
-				if listener.Ports[j].ContainerPort < 1024 || listener.Ports[j].ContainerPort > 65353 {
-					errs = append(errs, errors.New("listener container port must be a valid ephemeral port number"))
+				if listener.Ports[j].ContainerPort < 1 || listener.Ports[j].ContainerPort > 65353 {
+					errs = append(errs, errors.New("listener container port must be a valid port number"))
 				}
 			}
 		}
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index b77d26cf7..5b8af16c9 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -423,6 +423,11 @@ type HeaderSettings struct {
 	// is encountered. The default action is to reject the request.
 	// Refer to https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-enum-config-core-v3-httpprotocoloptions-headerswithunderscoresaction
 	WithUnderscoresAction WithUnderscoresAction `json:"withUnderscoresAction,omitempty" yaml:"withUnderscoresAction,omitempty"`
+
+	// PreserveXRequestID configures whether Envoy will keep the x-request-id header if passed for a request that is edge
+	// (Edge request is the request from external clients to front Envoy) and not reset it, which is the current Envoy behaviour.
+	// It defaults to false.
+	PreserveXRequestID bool `json:"preserveXRequestID,omitempty" yaml:"preserveXRequestID,omitempty"`
 }
 
 // ClientTimeout sets the timeout configuration for downstream connections
diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go
index 1852384de..0c33873c1 100644
--- a/internal/provider/kubernetes/controller.go
+++ b/internal/provider/kubernetes/controller.go
@@ -129,9 +129,9 @@ type resourceMappings struct {
 	// Map for storing backendRefs' NamespaceNames referred by various Route objects.
 	allAssociatedBackendRefs map[gwapiv1.BackendObjectReference]struct{}
 	// extensionRefFilters is a map of filters managed by an extension.
-	// The key is the namespaced name of the filter and the value is the
+	// The key is the namespaced name, group and kind of the filter and the value is the
 	// unstructured form of the resource.
-	extensionRefFilters map[types.NamespacedName]unstructured.Unstructured
+	extensionRefFilters map[utils.NamespacedNameWithGroupKind]unstructured.Unstructured
 }
 
 func newResourceMapping() *resourceMappings {
@@ -143,7 +143,7 @@ func newResourceMapping() *resourceMappings {
 		allAssociatedTCPRoutes:   map[string]struct{}{},
 		allAssociatedUDPRoutes:   map[string]struct{}{},
 		allAssociatedBackendRefs: map[gwapiv1.BackendObjectReference]struct{}{},
-		extensionRefFilters:      map[types.NamespacedName]unstructured.Unstructured{},
+		extensionRefFilters:      map[utils.NamespacedNameWithGroupKind]unstructured.Unstructured{},
 	}
 }
 
diff --git a/internal/provider/kubernetes/routes.go b/internal/provider/kubernetes/routes.go
index f72d1b2cf..76991be99 100644
--- a/internal/provider/kubernetes/routes.go
+++ b/internal/provider/kubernetes/routes.go
@@ -185,10 +185,17 @@ func (r *gatewayAPIReconciler) processGRPCRoutes(ctx context.Context, gatewayNam
 				if filter.Type == gwapiv1.GRPCRouteFilterExtensionRef {
 					// NOTE: filters must be in the same namespace as the GRPCRoute
 					// Check if it's a Kind managed by an extension and add to resourceTree
-					key := types.NamespacedName{
-						Namespace: grpcRoute.Namespace,
-						Name:      string(filter.ExtensionRef.Name),
+					key := utils.NamespacedNameWithGroupKind{
+						NamespacedName: types.NamespacedName{
+							Namespace: grpcRoute.Namespace,
+							Name:      string(filter.ExtensionRef.Name),
+						},
+						GroupKind: schema.GroupKind{
+							Group: string(filter.ExtensionRef.Group),
+							Kind:  string(filter.ExtensionRef.Kind),
+						},
 					}
+
 					extRefFilter, ok := resourceMap.extensionRefFilters[key]
 					if !ok {
 						r.log.Error(
@@ -229,7 +236,7 @@ func (r *gatewayAPIReconciler) processHTTPRoutes(ctx context.Context, gatewayNam
 	}
 	for i := range extensionRefFilters {
 		filter := extensionRefFilters[i]
-		resourceMap.extensionRefFilters[utils.NamespacedName(&filter)] = filter
+		resourceMap.extensionRefFilters[utils.GetNamespacedNameWithGroupKind(&filter)] = filter
 	}
 
 	if err := r.client.List(ctx, httpRouteList, &client.ListOptions{
@@ -367,9 +374,15 @@ func (r *gatewayAPIReconciler) processHTTPRoutes(ctx context.Context, gatewayNam
 				} else if filter.Type == gwapiv1.HTTPRouteFilterExtensionRef {
 					// NOTE: filters must be in the same namespace as the HTTPRoute
 					// Check if it's a Kind managed by an extension and add to resourceTree
-					key := types.NamespacedName{
-						Namespace: httpRoute.Namespace,
-						Name:      string(filter.ExtensionRef.Name),
+					key := utils.NamespacedNameWithGroupKind{
+						NamespacedName: types.NamespacedName{
+							Namespace: httpRoute.Namespace,
+							Name:      string(filter.ExtensionRef.Name),
+						},
+						GroupKind: schema.GroupKind{
+							Group: string(filter.ExtensionRef.Group),
+							Kind:  string(filter.ExtensionRef.Kind),
+						},
 					}
 					extRefFilter, ok := resourceMap.extensionRefFilters[key]
 					if !ok {
diff --git a/internal/utils/misc.go b/internal/utils/misc.go
index 6e434660d..84043ecdc 100644
--- a/internal/utils/misc.go
+++ b/internal/utils/misc.go
@@ -11,10 +11,30 @@ import (
 	"hash/fnv"
 	"strings"
 
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
+type NamespacedNameWithGroupKind struct {
+	types.NamespacedName
+	schema.GroupKind
+}
+
+// GetNamespacedNameWithGroupKind creates and returns object's NamespacedNameWithGroupKind.
+func GetNamespacedNameWithGroupKind(obj client.Object) NamespacedNameWithGroupKind {
+	return NamespacedNameWithGroupKind{
+		NamespacedName: types.NamespacedName{
+			Namespace: obj.GetNamespace(),
+			Name:      obj.GetName(),
+		},
+		GroupKind: schema.GroupKind{
+			Group: obj.GetObjectKind().GroupVersionKind().GroupKind().Group,
+			Kind:  obj.GetObjectKind().GroupVersionKind().GroupKind().Kind,
+		},
+	}
+}
+
 // NamespacedName creates and returns object's NamespacedName.
 func NamespacedName(obj client.Object) types.NamespacedName {
 	return types.NamespacedName{
diff --git a/internal/xds/cache/metrics.go b/internal/xds/cache/metrics.go
new file mode 100644
index 000000000..3480b9c40
--- /dev/null
+++ b/internal/xds/cache/metrics.go
@@ -0,0 +1,23 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+package cache
+
+import "github.com/envoyproxy/gateway/internal/metrics"
+
+var (
+	xdsSnapshotCreationTotal   = metrics.NewCounter("xds_snapshot_creation_total", "Total number of xds snapshot cache creation.")
+	xdsSnapshotCreationFailed  = metrics.NewCounter("xds_snapshot_creation_failed", "Number of xds snapshot cache creation that failed.")
+	xdsSnapshotCreationSuccess = metrics.NewCounter("xds_snapshot_creation_success", "Number of xds snapshot cache creation that succeeded.")
+
+	xdsSnapshotUpdateTotal   = metrics.NewCounter("xds_snapshot_update_total", "Total number of xds snapshot cache updates by node id.")
+	xdsSnapshotUpdateFailed  = metrics.NewCounter("xds_snapshot_update_failed", "Number of xds snapshot cache updates that failed by node id.")
+	xdsSnapshotUpdateSuccess = metrics.NewCounter("xds_snapshot_update_success", "Number of xds snapshot cache updates that succeeded by node id.")
+	nodeIDLabel              = metrics.NewLabel("nodeID")
+
+	xdsStreamDurationSeconds      = metrics.NewHistogram("xds_stream_duration_seconds", "How long a xds stream takes to finish.", []float64{0.1, 10, 50, 100, 1000, 10000})
+	xdsDeltaStreamDurationSeconds = metrics.NewHistogram("xds_delta_stream_duration_seconds", "How long a xds delta stream takes to finish.", []float64{0.1, 10, 50, 100, 1000, 10000})
+	streamIDLabel                 = metrics.NewLabel("streamID")
+)
diff --git a/internal/xds/cache/snapshotcache.go b/internal/xds/cache/snapshotcache.go
index bd3b22fb2..383aad836 100644
--- a/internal/xds/cache/snapshotcache.go
+++ b/internal/xds/cache/snapshotcache.go
@@ -19,6 +19,7 @@ import (
 	"math"
 	"strconv"
 	"sync"
+	"time"
 
 	corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
 	discoveryv3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
@@ -51,13 +52,17 @@ type snapshotMap map[string]*cachev3.Snapshot
 
 type nodeInfoMap map[int64]*corev3.Node
 
+type streamDurationMap map[int64]time.Time
+
 type snapshotCache struct {
 	cachev3.SnapshotCache
-	streamIDNodeInfo nodeInfoMap
-	snapshotVersion  int64
-	lastSnapshot     snapshotMap
-	log              *zap.SugaredLogger
-	mu               sync.Mutex
+	streamIDNodeInfo    nodeInfoMap
+	streamDuration      streamDurationMap
+	deltaStreamDuration streamDurationMap
+	snapshotVersion     int64
+	lastSnapshot        snapshotMap
+	log                 *zap.SugaredLogger
+	mu                  sync.Mutex
 }
 
 // GenerateNewSnapshot takes a table of resources (the output from the IR->xDS
@@ -73,17 +78,26 @@ func (s *snapshotCache) GenerateNewSnapshot(irKey string, resources types.XdsRes
 		version,
 		resources,
 	)
+
+	xdsSnapshotCreationTotal.Increment()
 	if err != nil {
+		xdsSnapshotCreationFailed.Increment()
 		return err
+	} else {
+		xdsSnapshotCreationSuccess.Increment()
 	}
 
 	s.lastSnapshot[irKey] = snapshot
 
 	for _, node := range s.getNodeIDs(irKey) {
 		s.log.Debugf("Generating a snapshot with Node %s", node)
-		err := s.SetSnapshot(context.TODO(), node, snapshot)
-		if err != nil {
+		xdsSnapshotUpdateTotal.With(nodeIDLabel.Value(node)).Increment()
+
+		if err = s.SetSnapshot(context.TODO(), node, snapshot); err != nil {
+			xdsSnapshotUpdateFailed.With(nodeIDLabel.Value(node)).Increment()
 			return err
+		} else {
+			xdsSnapshotUpdateSuccess.With(nodeIDLabel.Value(node)).Increment()
 		}
 	}
 
@@ -110,10 +124,12 @@ func NewSnapshotCache(ads bool, logger logging.Logger) SnapshotCacheWithCallback
 	// Set up the nasty wrapper hack.
 	wrappedLogger := logger.Sugar()
 	return &snapshotCache{
-		SnapshotCache:    cachev3.NewSnapshotCache(ads, &Hash, wrappedLogger),
-		log:              wrappedLogger,
-		lastSnapshot:     make(snapshotMap),
-		streamIDNodeInfo: make(nodeInfoMap),
+		SnapshotCache:       cachev3.NewSnapshotCache(ads, &Hash, wrappedLogger),
+		log:                 wrappedLogger,
+		lastSnapshot:        make(snapshotMap),
+		streamIDNodeInfo:    make(nodeInfoMap),
+		streamDuration:      make(streamDurationMap),
+		deltaStreamDuration: make(streamDurationMap),
 	}
 }
 
@@ -137,16 +153,26 @@ func (s *snapshotCache) OnStreamOpen(_ context.Context, streamID int64, _ string
 	defer s.mu.Unlock()
 
 	s.streamIDNodeInfo[streamID] = nil
+	s.streamDuration[streamID] = time.Now()
 
 	return nil
 }
 
-func (s *snapshotCache) OnStreamClosed(streamID int64, _ *corev3.Node) {
+func (s *snapshotCache) OnStreamClosed(streamID int64, node *corev3.Node) {
 	// TODO: something with the node?
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
+	if startTime, ok := s.streamDuration[streamID]; ok {
+		streamDuration := time.Since(startTime)
+		xdsStreamDurationSeconds.With(
+			streamIDLabel.Value(strconv.FormatInt(streamID, 10)),
+			nodeIDLabel.Value(node.Id),
+		).Record(streamDuration.Seconds())
+	}
+
 	delete(s.streamIDNodeInfo, streamID)
+	delete(s.streamDuration, streamID)
 }
 
 func (s *snapshotCache) OnStreamRequest(streamID int64, req *discoveryv3.DiscoveryRequest) error {
@@ -229,16 +255,26 @@ func (s *snapshotCache) OnDeltaStreamOpen(_ context.Context, streamID int64, _ s
 
 	// Ensure that we're adding the streamID to the Node ID list.
 	s.streamIDNodeInfo[streamID] = nil
+	s.deltaStreamDuration[streamID] = time.Now()
 
 	return nil
 }
 
-func (s *snapshotCache) OnDeltaStreamClosed(streamID int64, _ *corev3.Node) {
+func (s *snapshotCache) OnDeltaStreamClosed(streamID int64, node *corev3.Node) {
 	// TODO: something with the node?
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
+	if startTime, ok := s.deltaStreamDuration[streamID]; ok {
+		deltaStreamDuration := time.Since(startTime)
+		xdsDeltaStreamDurationSeconds.With(
+			streamIDLabel.Value(strconv.FormatInt(streamID, 10)),
+			nodeIDLabel.Value(node.Id),
+		).Record(deltaStreamDuration.Seconds())
+	}
+
 	delete(s.streamIDNodeInfo, streamID)
+	delete(s.deltaStreamDuration, streamID)
 }
 
 func (s *snapshotCache) OnStreamDeltaRequest(streamID int64, req *discoveryv3.DeltaDiscoveryRequest) error {
diff --git a/internal/xds/server/runner/runner.go b/internal/xds/server/runner/runner.go
index 28c203a86..f08127525 100644
--- a/internal/xds/server/runner/runner.go
+++ b/internal/xds/server/runner/runner.go
@@ -16,8 +16,6 @@ import (
 	"strconv"
 	"time"
 
-	"google.golang.org/grpc/keepalive"
-
 	clusterv3 "github.com/envoyproxy/go-control-plane/envoy/service/cluster/v3"
 	discoveryv3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
 	endpointv3 "github.com/envoyproxy/go-control-plane/envoy/service/endpoint/v3"
@@ -28,6 +26,7 @@ import (
 	serverv3 "github.com/envoyproxy/go-control-plane/pkg/server/v3"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials"
+	"google.golang.org/grpc/keepalive"
 
 	"github.com/envoyproxy/gateway/api/v1alpha1"
 	"github.com/envoyproxy/gateway/internal/envoygateway/config"
diff --git a/internal/xds/translator/extauth.go b/internal/xds/translator/extauth.go
index 0d8edfce2..46ce5df80 100644
--- a/internal/xds/translator/extauth.go
+++ b/internal/xds/translator/extauth.go
@@ -113,6 +113,7 @@ func extAuthConfig(extAuth *ir.ExtAuth) *extauthv3.ExtAuthz {
 			MatchPattern: &matcherv3.StringMatcher_Exact{
 				Exact: header,
 			},
+			IgnoreCase: true,
 		})
 	}
 
@@ -178,6 +179,7 @@ func httpService(http *ir.HTTPExtAuthService) *extauthv3.HttpService {
 			MatchPattern: &matcherv3.StringMatcher_Exact{
 				Exact: header,
 			},
+			IgnoreCase: true,
 		})
 	}
 
diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go
index a86bff57f..ada6ee454 100644
--- a/internal/xds/translator/listener.go
+++ b/internal/xds/translator/listener.go
@@ -260,7 +260,8 @@ func (t *Translator) addHCMToXDSListener(xdsListener *listenerv3.Listener, irLis
 		CommonHttpProtocolOptions: &corev3.HttpProtocolOptions{
 			HeadersWithUnderscoresAction: buildHeadersWithUnderscoresAction(irListener.Headers),
 		},
-		Tracing: hcmTracing,
+		Tracing:                   hcmTracing,
+		PreserveExternalRequestId: ptr.Deref(irListener.Headers, ir.HeaderSettings{}).PreserveXRequestID,
 	}
 
 	if irListener.Timeout != nil && irListener.Timeout.HTTP != nil {


Test patch:
diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml
index e812062f3..d5714c531 100644
--- a/.github/workflows/build_and_test.yaml
+++ b/.github/workflows/build_and_test.yaml
@@ -44,6 +44,9 @@ jobs:
 
   coverage-test:
     runs-on: ubuntu-latest
+    permissions:
+      contents: read   #  for actions/checkout
+      id-token: write  #  for fetching OIDC token
     steps:
     - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b  # v4.1.5
     - uses: ./tools/github-actions/setup-deps
@@ -52,12 +55,14 @@ jobs:
     - name: Run Coverage Tests
       run: make go.test.coverage
     - name: Upload coverage to Codecov
-      uses: codecov/codecov-action@4fe8c5f003fae66aa5ebb77cfd3e7bfbbda0b6b0  # v3.1.5
+      uses: codecov/codecov-action@6d798873df2b1b8e5846dba6fb86631229fbcb17  # v4.4.0
       with:
-        fail_ci_if_error: false
+        fail_ci_if_error: true
         files: ./coverage.xml
         name: codecov-envoy-gateway
         verbose: true
+        # TODO change to `use_oidc: true` once https://github.com/codecov/codecov-action/issues/1435 is resolved.
+        use_oidc: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) }}
 
   build:
     runs-on: ubuntu-latest
diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-headers.in.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-headers.in.yaml
index c21285612..6d73bee1a 100644
--- a/internal/gatewayapi/testdata/clienttrafficpolicy-headers.in.yaml
+++ b/internal/gatewayapi/testdata/clienttrafficpolicy-headers.in.yaml
@@ -8,6 +8,7 @@ clientTrafficPolicies:
     headers:
       enableEnvoyHeaders: true
       withUnderscoresAction: Allow
+      preserveXRequestID: true
     targetRef:
       group: gateway.networking.k8s.io
       kind: Gateway
diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-headers.out.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-headers.out.yaml
index 8fa939ac3..8b184cbba 100644
--- a/internal/gatewayapi/testdata/clienttrafficpolicy-headers.out.yaml
+++ b/internal/gatewayapi/testdata/clienttrafficpolicy-headers.out.yaml
@@ -8,6 +8,7 @@ clientTrafficPolicies:
   spec:
     headers:
       enableEnvoyHeaders: true
+      preserveXRequestID: true
       withUnderscoresAction: Allow
     targetRef:
       group: gateway.networking.k8s.io
@@ -129,6 +130,7 @@ xdsIR:
     - address: 0.0.0.0
       headers:
         enableEnvoyHeaders: true
+        preserveXRequestID: true
         withUnderscoresAction: Allow
       hostnames:
       - '*'
@@ -141,6 +143,7 @@ xdsIR:
     - address: 0.0.0.0
       headers:
         enableEnvoyHeaders: true
+        preserveXRequestID: true
         withUnderscoresAction: Allow
       hostnames:
       - '*'
diff --git a/internal/gatewayapi/translator_test.go b/internal/gatewayapi/translator_test.go
index 23ade13da..1a200555b 100644
--- a/internal/gatewayapi/translator_test.go
+++ b/internal/gatewayapi/translator_test.go
@@ -679,27 +679,71 @@ func TestServicePortToContainerPort(t *testing.T) {
 	testCases := []struct {
 		servicePort   int32
 		containerPort int32
+		envoyProxy    *egv1a1.EnvoyProxy
 	}{
 		{
 			servicePort:   99,
 			containerPort: 10099,
+			envoyProxy:    nil,
 		},
 		{
 			servicePort:   1023,
 			containerPort: 11023,
+			envoyProxy:    nil,
 		},
 		{
 			servicePort:   1024,
 			containerPort: 1024,
+			envoyProxy:    nil,
 		},
 		{
 			servicePort:   8080,
 			containerPort: 8080,
+			envoyProxy:    nil,
+		},
+		{
+			servicePort:   99,
+			containerPort: 10099,
+			envoyProxy: &egv1a1.EnvoyProxy{
+				Spec: egv1a1.EnvoyProxySpec{
+					Provider: &egv1a1.EnvoyProxyProvider{
+						Type: egv1a1.ProviderTypeKubernetes,
+					},
+				},
+			},
+		},
+		{
+			servicePort:   99,
+			containerPort: 10099,
+			envoyProxy: &egv1a1.EnvoyProxy{
+				Spec: egv1a1.EnvoyProxySpec{
+					Provider: &egv1a1.EnvoyProxyProvider{
+						Type: egv1a1.ProviderTypeKubernetes,
+						Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
+							UseListenerPortAsContainerPort: ptr.To(false),
+						},
+					},
+				},
+			},
+		},
+		{
+			servicePort:   99,
+			containerPort: 99,
+			envoyProxy: &egv1a1.EnvoyProxy{
+				Spec: egv1a1.EnvoyProxySpec{
+					Provider: &egv1a1.EnvoyProxyProvider{
+						Type: egv1a1.ProviderTypeKubernetes,
+						Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
+							UseListenerPortAsContainerPort: ptr.To(true),
+						},
+					},
+				},
+			},
 		},
 	}
 
 	for _, tc := range testCases {
-		got := servicePortToContainerPort(tc.servicePort)
+		got := servicePortToContainerPort(tc.servicePort, tc.envoyProxy)
 		assert.Equal(t, tc.containerPort, got)
 	}
 }
diff --git a/internal/provider/kubernetes/routes_test.go b/internal/provider/kubernetes/routes_test.go
index 9a230e682..be6769589 100644
--- a/internal/provider/kubernetes/routes_test.go
+++ b/internal/provider/kubernetes/routes_test.go
@@ -120,6 +120,102 @@ func TestProcessHTTPRoutes(t *testing.T) {
 			},
 			expected: true,
 		},
+		{
+			name: "httproute with extension filter multiple types same name",
+			routes: []*gwapiv1.HTTPRoute{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: httpRouteNS,
+						Name:      "test",
+					},
+					Spec: gwapiv1.HTTPRouteSpec{
+						CommonRouteSpec: gwapiv1.CommonRouteSpec{
+							ParentRefs: []gwapiv1.ParentReference{
+								{
+									Name: "test",
+								},
+							},
+						},
+						Rules: []gwapiv1.HTTPRouteRule{
+							{
+								Matches: []gwapiv1.HTTPRouteMatch{
+									{
+										Path: &gwapiv1.HTTPPathMatch{
+											Type:  ptr.To(gwapiv1.PathMatchPathPrefix),
+											Value: ptr.To("/"),
+										},
+									},
+								},
+								Filters: []gwapiv1.HTTPRouteFilter{
+									{
+										Type: gwapiv1.HTTPRouteFilterExtensionRef,
+										ExtensionRef: &gwapiv1.LocalObjectReference{
+											Group: gwapiv1.Group("gateway.example.io"),
+											Kind:  gwapiv1.Kind("Bar"),
+											Name:  gwapiv1.ObjectName("test"),
+										},
+									},
+									{
+										Type: gwapiv1.HTTPRouteFilterExtensionRef,
+										ExtensionRef: &gwapiv1.LocalObjectReference{
+											Group: gwapiv1.Group("gateway.example.io"),
+											Kind:  gwapiv1.Kind("Foo"),
+											Name:  gwapiv1.ObjectName("test"),
+										},
+									},
+								},
+								BackendRefs: []gwapiv1.HTTPBackendRef{
+									{
+										BackendRef: gwapiv1.BackendRef{
+											BackendObjectReference: gwapiv1.BackendObjectReference{
+												Group: gatewayapi.GroupPtr(corev1.GroupName),
+												Kind:  gatewayapi.KindPtr(gatewayapi.KindService),
+												Name:  "test",
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			extensionFilters: []*unstructured.Unstructured{
+				{
+					Object: map[string]interface{}{
+						"apiVersion": "gateway.example.io/v1alpha1",
+						"kind":       "Bar",
+						"metadata": map[string]interface{}{
+							"name":      "test",
+							"namespace": httpRouteNS,
+						},
+					},
+				},
+				{
+					Object: map[string]interface{}{
+						"apiVersion": "gateway.example.io/v1alpha1",
+						"kind":       "Foo",
+						"metadata": map[string]interface{}{
+							"name":      "test",
+							"namespace": httpRouteNS,
+						},
+					},
+				},
+			},
+			extensionAPIGroups: []schema.GroupVersionKind{
+				{
+					Group:   "gateway.example.io",
+					Version: "v1alpha1",
+					Kind:    "Bar",
+				},
+				{
+					Group:   "gateway.example.io",
+					Version: "v1alpha1",
+					Kind:    "Foo",
+				},
+			},
+			expected: true,
+		},
 		{
 			name: "httproute with one filter_from_extension",
 			routes: []*gwapiv1.HTTPRoute{
@@ -294,10 +390,16 @@ func TestProcessHTTPRoutes(t *testing.T) {
 				// Ensure the resource tree and map are as expected.
 				require.Equal(t, tc.routes, resourceTree.HTTPRoutes)
 				if tc.extensionFilters != nil {
-					for i, filter := range tc.extensionFilters {
-						key := types.NamespacedName{
-							Namespace: tc.routes[i].Namespace,
-							Name:      filter.GetName(),
+					for _, filter := range tc.extensionFilters {
+						key := utils.NamespacedNameWithGroupKind{
+							NamespacedName: types.NamespacedName{
+								Namespace: tc.routes[0].Namespace,
+								Name:      filter.GetName(),
+							},
+							GroupKind: schema.GroupKind{
+								Group: filter.GroupVersionKind().Group,
+								Kind:  filter.GroupVersionKind().Kind,
+							},
 						}
 						require.Equal(t, *filter, resourceMap.extensionRefFilters[key])
 					}
diff --git a/internal/xds/translator/testdata/in/xds-ir/headers-with-preserve-x-request-id.yaml b/internal/xds/translator/testdata/in/xds-ir/headers-with-preserve-x-request-id.yaml
new file mode 100644
index 000000000..1376be42e
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/headers-with-preserve-x-request-id.yaml
@@ -0,0 +1,32 @@
+http:
+- name: "first-listener"
+  address: "0.0.0.0"
+  port: 8081
+  hostnames:
+  - "*"
+  routes:
+  - name: "first-route"
+    hostname: "*"
+    destination:
+      name: "first-route-dest"
+      settings:
+      - endpoints:
+        - host: "1.1.1.1"
+          port: 8081
+  headers:
+    preserveXRequestID: true
+- name: "second-listener"
+  address: "0.0.0.0"
+  port: 8082
+  hostnames:
+  - "*"
+  routes:
+  - name: "second-route"
+    hostname: "*"
+    destination:
+      name: "second-route-dest"
+      settings:
+      - endpoints:
+        - host: "2.2.2.2"
+          port: 8082
+
diff --git a/internal/xds/translator/testdata/out/xds-ir/ext-auth.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/ext-auth.listeners.yaml
index 083a4a6f3..1c2ed9d00 100644
--- a/internal/xds/translator/testdata/out/xds-ir/ext-auth.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/ext-auth.listeners.yaml
@@ -21,7 +21,9 @@
             allowedHeaders:
               patterns:
               - exact: header1
+                ignoreCase: true
               - exact: header2
+                ignoreCase: true
             grpcService:
               envoyGrpc:
                 authority: grpc-backend.default:9000
@@ -38,7 +40,9 @@
                 allowedUpstreamHeaders:
                   patterns:
                   - exact: header1
+                    ignoreCase: true
                   - exact: header2
+                    ignoreCase: true
               pathPrefix: /auth
               serverUri:
                 cluster: securitypolicy/default/policy-for-gateway-1/envoy-gateway/http-backend
diff --git a/internal/xds/translator/testdata/out/xds-ir/headers-with-preserve-x-request-id.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/headers-with-preserve-x-request-id.clusters.yaml
new file mode 100755
index 000000000..d65e267ad
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/headers-with-preserve-x-request-id.clusters.yaml
@@ -0,0 +1,34 @@
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_ONLY
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: first-route-dest
+  lbPolicy: LEAST_REQUEST
+  name: first-route-dest
+  outlierDetection: {}
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
+- circuitBreakers:
+    thresholds:
+    - maxRetries: 1024
+  commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 10s
+  dnsLookupFamily: V4_ONLY
+  edsClusterConfig:
+    edsConfig:
+      ads: {}
+      resourceApiVersion: V3
+    serviceName: second-route-dest
+  lbPolicy: LEAST_REQUEST
+  name: second-route-dest
+  outlierDetection: {}
+  perConnectionBufferLimitBytes: 32768
+  type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/headers-with-preserve-x-request-id.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/headers-with-preserve-x-request-id.endpoints.yaml
new file mode 100755
index 000000000..9ad09dd91
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/headers-with-preserve-x-request-id.endpoints.yaml
@@ -0,0 +1,24 @@
+- clusterName: first-route-dest
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 1.1.1.1
+            portValue: 8081
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: first-route-dest/backend/0
+- clusterName: second-route-dest
+  endpoints:
+  - lbEndpoints:
+    - endpoint:
+        address:
+          socketAddress:
+            address: 2.2.2.2
+            portValue: 8082
+      loadBalancingWeight: 1
+    loadBalancingWeight: 1
+    locality:
+      region: second-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/headers-with-preserve-x-request-id.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/headers-with-preserve-x-request-id.listeners.yaml
new file mode 100644
index 000000000..880644d46
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/headers-with-preserve-x-request-id.listeners.yaml
@@ -0,0 +1,65 @@
+- address:
+    socketAddress:
+      address: 0.0.0.0
+      portValue: 8081
+  defaultFilterChain:
+    filters:
+    - name: envoy.filters.network.http_connection_manager
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+        commonHttpProtocolOptions:
+          headersWithUnderscoresAction: REJECT_REQUEST
+        http2ProtocolOptions:
+          initialConnectionWindowSize: 1048576
+          initialStreamWindowSize: 65536
+          maxConcurrentStreams: 100
+        httpFilters:
+        - name: envoy.filters.http.router
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+            suppressEnvoyHeaders: true
+        normalizePath: true
+        preserveExternalRequestId: true
+        rds:
+          configSource:
+            ads: {}
+            resourceApiVersion: V3
+          routeConfigName: first-listener
+        serverHeaderTransformation: PASS_THROUGH
+        statPrefix: http
+        useRemoteAddress: true
+  drainType: MODIFY_ONLY
+  name: first-listener
+  perConnectionBufferLimitBytes: 32768
+- address:
+    socketAddress:
+      address: 0.0.0.0
+      portValue: 8082
+  defaultFilterChain:
+    filters:
+    - name: envoy.filters.network.http_connection_manager
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+        commonHttpProtocolOptions:
+          headersWithUnderscoresAction: REJECT_REQUEST
+        http2ProtocolOptions:
+          initialConnectionWindowSize: 1048576
+          initialStreamWindowSize: 65536
+          maxConcurrentStreams: 100
+        httpFilters:
+        - name: envoy.filters.http.router
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+            suppressEnvoyHeaders: true
+        normalizePath: true
+        rds:
+          configSource:
+            ads: {}
+            resourceApiVersion: V3
+          routeConfigName: second-listener
+        serverHeaderTransformation: PASS_THROUGH
+        statPrefix: http
+        useRemoteAddress: true
+  drainType: MODIFY_ONLY
+  name: second-listener
+  perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/headers-with-preserve-x-request-id.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/headers-with-preserve-x-request-id.routes.yaml
new file mode 100755
index 000000000..ff93cfff3
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/headers-with-preserve-x-request-id.routes.yaml
@@ -0,0 +1,28 @@
+- ignorePortInHostMatching: true
+  name: first-listener
+  virtualHosts:
+  - domains:
+    - '*'
+    name: first-listener/*
+    routes:
+    - match:
+        prefix: /
+      name: first-route
+      route:
+        cluster: first-route-dest
+        upgradeConfigs:
+        - upgradeType: websocket
+- ignorePortInHostMatching: true
+  name: second-listener
+  virtualHosts:
+  - domains:
+    - '*'
+    name: second-listener/*
+    routes:
+    - match:
+        prefix: /
+      name: second-route
+      route:
+        cluster: second-route-dest
+        upgradeConfigs:
+        - upgradeType: websocket
diff --git a/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.listeners.yaml
index cec7fd427..1365c290c 100755
--- a/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.listeners.yaml
@@ -27,7 +27,9 @@
                 allowedUpstreamHeaders:
                   patterns:
                   - exact: header1
+                    ignoreCase: true
                   - exact: header2
+                    ignoreCase: true
               pathPrefix: /auth
               serverUri:
                 cluster: securitypolicy/default/policy-for-http-route-2/envoy-gateway/http-backend
@@ -86,7 +88,9 @@
                 allowedUpstreamHeaders:
                   patterns:
                   - exact: header1
+                    ignoreCase: true
                   - exact: header2
+                    ignoreCase: true
               pathPrefix: /auth
               serverUri:
                 cluster: securitypolicy/default/policy-for-http-route-2/envoy-gateway/http-backend
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index 6ac4fe6b1..1e3391760 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -1233,6 +1233,7 @@ _Appears in:_
 | `envoyDaemonSet` | _[KubernetesDaemonSetSpec](#kubernetesdaemonsetspec)_ |  false  | EnvoyDaemonSet defines the desired state of the Envoy daemonset resource.<br />Disabled by default, a deployment resource is used instead to provision the Envoy Proxy fleet |
 | `envoyService` | _[KubernetesServiceSpec](#kubernetesservicespec)_ |  false  | EnvoyService defines the desired state of the Envoy service resource.<br />If unspecified, default settings for the managed Envoy service resource<br />are applied. |
 | `envoyHpa` | _[KubernetesHorizontalPodAutoscalerSpec](#kuberneteshorizontalpodautoscalerspec)_ |  false  | EnvoyHpa defines the Horizontal Pod Autoscaler settings for Envoy Proxy Deployment.<br />Once the HPA is being set, Replicas field from EnvoyDeployment will be ignored. |
+| `useListenerPortAsContainerPort` | _boolean_ |  false  | UseListenerPortAsContainerPort disables the port shifting feature in the Envoy Proxy.<br />When set to false (default value), if the service port is a privileged port (1-1023), add a constant to the value converting it into an ephemeral port.<br />This allows the container to bind to the port without needing a CAP_NET_BIND_SERVICE capability. |
 
 
 #### EnvoyProxyProvider
@@ -1797,6 +1798,7 @@ _Appears in:_
 | ---   | ---  | ---      | ---         |
 | `enableEnvoyHeaders` | _boolean_ |  false  | EnableEnvoyHeaders configures Envoy Proxy to add the "X-Envoy-" headers to requests<br />and responses. |
 | `withUnderscoresAction` | _[WithUnderscoresAction](#withunderscoresaction)_ |  false  | WithUnderscoresAction configures the action to take when an HTTP header with underscores<br />is encountered. The default action is to reject the request. |
+| `preserveXRequestID` | _boolean_ |  false  | PreserveXRequestID configures Envoy to keep the X-Request-ID header if passed for a request that is edge<br />(Edge request is the request from external clients to front Envoy) and not reset it, which is the current Envoy behaviour.<br />It defaults to false. |
 
 
 #### HealthCheck
diff --git a/site/content/zh/latest/releases/_index.md b/site/content/zh/latest/releases/_index.md
new file mode 100644
index 000000000..6a684ff59
--- /dev/null
+++ b/site/content/zh/latest/releases/_index.md
@@ -0,0 +1,5 @@
+---
+title: "版本"
+weight: 90
+description: 本节内容包含 Envoy Gateway 的版本。
+---
diff --git a/site/content/zh/latest/releases/v1.0.0.md b/site/content/zh/latest/releases/v1.0.0.md
new file mode 100644
index 000000000..f09638f64
--- /dev/null
+++ b/site/content/zh/latest/releases/v1.0.0.md
@@ -0,0 +1,179 @@
+---
+title: "v1.0.0"
+publishdate: 2024-03-13
+---
+
+日期：2024 年 3 月 13 日
+
+## 文档 {#documentation}
+
+- 新增了本地限流（Local Ratelimit）的用户指南
+- 新增了熔断（Circuit Breaker）的用户指南
+- 新增了故障注入（Fault Injection）的用户指南
+- 新增了 EnvoyProxy extraArgs 的用户指南
+- 新增了在 ClientTrafficPolicy 中超时的用户指南
+- 新增了基础路由的 JWT Claim 用户指南
+- 新增了 HTTP 超时的用户指南
+- 新增了在 BackendTrafficPolicy 中重试的用户指南
+- 新增了基础身份验证（Basic Auth）的用户指南
+- 新增了 OIDC 的用户指南
+- 新增了 ClientTrafficPolicy 的用户指南
+- 新增了 BackendTrafficPolicy 的用户指南
+- 新增了使用 HTTPS 的基础身份验证（Basic Auth）的用户指南
+- 新增外部鉴权（External Authorization）的用户指南
+- 新增了 Kubernetes 外部路由的用户指南
+- 新增了 BackendTLSPolicy 的用户指南
+- 新增了从外部客户端到网关的双向 TLS 的用户指南
+- 新增了使用自定义证书进行控制平面身份验证的用户指南
+- 新增了多 GatewayClass 和合并 Gateway 部署模式的用户指南
+- 为 CRD API 添加了 `Type` 和 `required` 的文档
+- 重构了用户指南文档的结构
+- 将设计文档移动到“参与”下并重构
+- 将 crd-ref-docs 更新至 0.0.10
+- 将 Envoy Proxy 镜像更新为 main 中的 envoy:distroless-dev
+
+## 安装 {#installation}
+
+- 新增了对从私有仓库中提取 envoyGateway 镜像的支持
+- 新增了为 certgen Job 配置资源的支持
+- 新增了为 EnvoyGateway Pod 配置亲和力的支持
+
+## API {#api}
+
+- 在 ClientTrafficPolicy CRD 中添加了对下游 QUIC/HTTP3 的支持
+- 在 ClientTrafficPolicy CRD 中添加了对下游 MTLS 的支持
+- 在 ClientTrafficPolicy CRD 中添加了对 EnvoyHeaders 的启用支持
+- 在 ClientTrafficPolicy CRD 中添加了对 DisableMergeSlash 和 escapedSlashesAction 的支持
+- 在 ClientTrafficPolicy CRD 中添加了对 HTTP/1.1 中的 EnableTrailers 的支持
+- 在 ClientTrafficPolicy CRD 中添加了对 HTTP/1 上保留标头字母大小写的支持
+- 在 ClientTrafficPolicy CRD 中添加了对 HTTP/1.0 和 HTTP/0.9 启用的支持
+- 在 ClientTrafficPolicy CRD 中添加了对使用 XFF 进行客户端 IP 检测的支持
+- 在 ClientTrafficPolicy CRD 中添加了对使用自定义标头进行客户端 IP 检测的支持
+- 在 ClientTrafficPolicy CRD 中添加了对连接超时的支持
+- 在 ClientTrafficPolicy CRD 中添加了对常见 TLS 配置属性的支持
+- 在 ClientTrafficPolicy CRD 中添加了对代理协议的支持
+- 在 ClientTrafficPolicy CRD 中添加了对 TCPKeepAlive 的支持
+- 在 BackendTrafficPolicy CRD 中添加了对本地限流（Local Ratelimit）的支持
+- 在 BackendTrafficPolicy CRD 中添加了对熔断（Circuit Breaker）的支持
+- 在 BackendTrafficPolicy CRD 中添加了对故障注入（Fault Injection）的支持
+- 在 BackendTrafficPolicy CRD 中添加了对被动健康检查（Passive Health Check）的支持
+- 在 BackendTrafficPolicy CRD 中添加了对主动健康检查（Active Health Check）的支持
+- 在 BackendTrafficPolicy CRD 中添加了对连接超时（Connection Timeout）的支持
+- 在 BackendTrafficPolicy CRD 中添加了对压缩器/解压缩器（Compressor/Decompressor）的支持
+- 在 BackendTrafficPolicy CRD 中添加了对重试（Retry）的支持
+- 在 BackendTrafficPolicy CRD 中添加了对慢启动模式的支持
+- 在 BackendTrafficPolicy CRD 中添加了对代理协议的支持
+- 在 BackendTrafficPolicy CRD 中添加了对 TCPKeepAlive 的支持
+- 在 BackendTrafficPolicy CRD 中添加了对 PolicyStatus 的支持
+- 在 ClientTrafficPolicy CRD 中添加了对 PolicyStatus 的支持
+- 在 SecurityPolicy CRD 中添加了对 PolicyStatus 的支持
+- 在 SecurityPolicy CRD 中添加了对 OIDC 的支持
+- 在 SecurityPolicy CRD 中添加了对基础身份验证（Basic Auth）的支持
+- 在 SecurityPolicy CRD 中添加了对 OIDC 的 RedirectURL 和 signoutPath 的支持
+- 在 SecurityPolicy CRD 中添加了对 ExtractFrom 标头和参数到 JWT 的支持
+- 在 SecurityPolicy CRD 中添加了对外部鉴权（External Authorization）的支持
+- 在 SecurityPolicy CRD 中添加了对 JWT 的 RecomputeRoute 字段的支持
+- 在 SecurityPolicy CRD 中添加了对 CORS 设置的 AllowCredentials 旋钮的支持
+- 在 SecurityPolicy CRD 中添加了对从不同标识符提取到 JWT 的支持
+- 在 EnvoyPatchPolicy CRD 中添加了对 Secret 资源的支持
+- 在 EnvoyPatchPolicy CRD 中添加了对 JSONPatchOperation 的值可选的支持
+- 在 EnvoyPatchPolicy CRD 中添加了对 JSONPatchOperation 中 `From` 字段的支持
+- 在 EnvoyPatchPolicy CRD 中添加了对 MergeGateways 的支持
+- 通过实施 BackendTLSPolicy CRD 添加了对上游 TLS 的支持
+- 在 EnvoyGateway 配置中添加了对 NamespaceSelectors 的 LabelSelector 类型的支持
+- 在 EnvoyGateway 配置中添加了对 Ratelimit Prometheus 的支持
+- 在 EnvoyProxy CRD 中添加了对 Pod 终止时 Envoy 关闭之前优雅地耗尽侦听器的支持
+- 在 EnvoyProxy CRD 中添加了对 Envoy 服务配置 externalTrafficPolicy 的支持
+- 在 EnvoyProxy CRD 中添加了对 Envoy 额外参数的支持
+- 在 EnvoyProxy CRD 中添加了对 Mergepatch 到 envoyproxy/ratelimit 部署的支持
+- 在 EnvoyProxy CRD 中添加了对 Mergepatch 到 envoyproxy 服务的支持
+- 在 EnvoyProxy CRD 中添加了对 NodeSelector 到 PodSpec 的支持
+- 在 EnvoyProxy CRD 中添加了对 HorizontalPodAutoscaler 的支持
+- 在 EnvoyProxy CRD 中添加了对 PodSpec 的 TopologySpreadConstraints 支持
+- 在 EnvoyProxy CRD 中添加了对 PodSpec 的 ImagePullSecrets 的支持
+
+### 重大变化 {#breaking-changes}
+
+- 使用通配符将 AllowOrigins 与 SecurityPolicy CRD 中的 CORS 进行匹配
+- 删除 EnvoyProxy CRD 中的主机网络支持
+
+## 一致性 {#conformance}
+
+- 将后端镜像从 gcr.io/k8s-staging-ingressconformance/echoserver 替换为 gcr.io/k8s-staging-gateway-api/echo-basic
+
+## 测试 {#testing}
+
+- 添加了标头中大小写保留的 e2e 测试
+- 添加了在 ClientTrafficPolicy 中超时的 e2e 测试
+- 添加了 JWT Claim 基础路由的 e2e 测试
+- 添加了 OIDC 的 e2e 测试
+- 添加了 BackendTrafficPolicy 重试（Retry）的 e2e 测试
+- 添加了后端升级的 e2e 测试
+- 添加了外部鉴权（External Authorization）的 e2e 测试
+- 添加了后端 TLS 策略的 e2e 测试
+- 添加了 Envoy Gateway 版本升级的 e2e 测试
+- 添加了加权后端的 e2e 测试
+- 添加了对 LoadBalancerIP 的验证以防止拖尾周期
+
+## 转换器 {#translator}
+
+- 修复了前缀匹配，以防止具有相同前缀的路由匹配错误的问题
+- 通过为 ir.Infra 实现类似接口来修复多重调谐的问题
+- 修复了具有空条件 {} 的 EndpointSlice 问题
+- 修复了解析 http 请求超时时的错误处理
+- 修复了 EnvoyPatchPolicy 被禁用时无状态的问题
+- 修复了 xDS 和 infra IR 的可打印问题
+- 修复了跳过 backendRef 并将权重设置为 0 的问题
+- 修复了限流中的 AND 标头匹配不起作用的问题
+- 修复了不存在 GatewayClass 时的删除逻辑
+- 修复了 ClientTrafficPolicy 的 mergedGateway irKey 匹配问题
+- 修复了策略应仅被应用到 mergeGateway 为 true 的 Gateway 的问题
+- 修复了启用 mergeGateway 时不会显示网关侦听器状态的问题
+- 通过将 Web 套接字升级配置从 hcm 移动到路由，修复了 GRPCroute websocket 无法工作的问题
+- 修复了在 HTTPRoute 上设置超时时配置空闲超时的问题
+- 修复了 OIDC 令牌端点的放宽 HTTPS 限制
+- 修复了将路由转换到空的后端时出现的 Panic 问题
+- 修复了 xDS 转换应以最高效的方式完成
+- 修复了从监测中删除未被使用的状态键的问题
+- 修复了比较 Envoy Proxy 服务时忽略终结器的问题
+- 修复了如果 HTTP/3 被启用后不会覆盖 ALPN 数组的问题
+- 修复了启用 HTTP/3 时默认添加 h3 ALPN 的问题
+- 修复了 SecurityPolicy/BackendTrafficPolicy 的 Merge 行为，改为 Replace
+- 修复了启用 HTTP/3 时在 alt-svc 标头中使用服务端口的问题
+- 修复了防止针对同一端口上的非 TLS 侦听器的策略发生冲突
+- 修复了跳过所有策略的 ReasonTargetNotFound
+- 修复了跳过所有策略的空发布状态
+- 添加了在发送到 Envoy 之前正则表达式验证支持
+- 添加了当服务类型为 ClusterIP 时将 spec.addresses.value 设置为 ClusterIP 的支持
+- 为 BackendRef 中的过滤器添加了不支持的状态条件
+- 为 Provider 资源添加了 List 替代 Map，以保证稳定性
+- 添加了 OAuth Cookie 的后缀，以防止多个 OAuth 过滤器覆盖彼此的 Cookie
+- 添加了对 BackendTrafficPolicy 和 SecurityPolicy 覆盖条件的支持
+- 添加了对默认重试预计量和重试主机判断的支持
+- 添加了对 gateway.spec.infrastructural 实现的支持
+- 添加了对多个后端的上游 TLS 的支持
+- 添加了在 ClientTrafficPolicy 中对 CA 证书验证的支持
+
+## Provider {#providers}
+
+- 添加了对每个控制器多个 GatewayClass 的支持
+- 在 Kubernetes Provider 中添加了 SecurityPolicyIndexers
+- 添加了对在 CertGen  Job 中生成 HMAC 密钥的支持
+- 修复了删除 GatewayClass 时的 Finalizer 逻辑
+- 修复了重新启动控制平面时 MergeGateway 出现 Panic 的问题
+
+## xDS {#xds}
+
+- 添加了对 EDS 缓存的支持
+- 添加了对 ADS 缓存的支持，以保证规则顺序
+- 修复了使用 RequestHeaderModifier 过滤器时出现的已弃用字段错误
+- 修复了 Envoy 在运行时拒绝 XDS 并在重新启动时丢失所有路由的问题
+- 修复了与定义的路由不匹配的请求触发每个路由过滤器的问题
+- 将 go-control-plane 升级至 v0.12.0
+
+## Cli {#cli}
+
+- 添加了对 egctl x status 命令的支持
+- 添加了对 egctl experimental dashboard envoy-proxy 命令的支持
+- 添加了对 egctl config ratelimit 命令的支持
+- 添加了对从 gateway-api 资源到 IR 的 gctl translate from gateway-api 命令的支持
diff --git a/site/content/zh/latest/releases/v1.0.1.md b/site/content/zh/latest/releases/v1.0.1.md
new file mode 100644
index 000000000..d164d23d3
--- /dev/null
+++ b/site/content/zh/latest/releases/v1.0.1.md
@@ -0,0 +1,29 @@
+---
+title: "v1.0.1"
+publishdate: 2024-04-09
+---
+
+日期：2024 年 4 月 9 日
+
+## 安装 {#installation}
+
+- 将 EnvoyProxy 版本更新至 v1.29.3
+- 修复了 Certgen 以支持在升级期间创建 hmac 密钥
+
+## 转换器 {#translator}
+
+- 修复了 ResourceVersionTable 中的 nil Secret 问题
+- 当启用 ClientTrafficPolicy 和 MergeGateway 时，将缺少的 HTTP 过滤器添加到 HTTP 过滤器链
+- 启用 URL 重写时允许 WebSocket
+- 设置 HTTP 健康检查器的 Host 标头
+- 修复了重定向 URL 中的双斜杠
+- 允许 ClientTrafficPolicy 附加到同一网关内的多个 HTTP（非 HTTPS）侦听器
+- 为 HTTP Ext Auth 服务的路径设置前缀
+- 设置路由匹配优先顺序为 Exact > RegularExpression > PathPrefix
+- 修复了被合并网关的 infraIR 重复端口转换
+- 将 SpawnUpstreamSpan 设置为 true
+- 允许限流与多个监听器一起使用
+
+## Infra-manager
+
+- 当 InfraIR 具有空侦听器时跳过创建基础设施资源