diff --git a/api/v1alpha1/clienttrafficpolicy_types.go b/api/v1alpha1/clienttrafficpolicy_types.go
index d87f568b00..5c9a31cf5e 100644
--- a/api/v1alpha1/clienttrafficpolicy_types.go
+++ b/api/v1alpha1/clienttrafficpolicy_types.go
@@ -98,7 +98,7 @@ type ClientTrafficPolicySpec struct {
// HTTP2 provides HTTP/2 configuration on the listener.
//
// +optional
- HTTP2 *HTTP2Settings `json:"http2,omitempty"`
+ HTTP2 *HTTP2ClientSettings `json:"http2,omitempty"`
// HTTP3 provides HTTP/3 configuration on the listener.
//
// +optional
@@ -440,6 +440,42 @@ const (
SchemeHeaderTransformMatchBackend SchemeHeaderTransform = "MatchBackend"
)
+// HTTP2ClientSettings provides HTTP/2 configuration for client connections to the listener.
+type HTTP2ClientSettings struct {
+ // Embed shared HTTP/2 settings.
+ HTTP2Settings `json:",inline"`
+
+ // ConnectionKeepalive configures HTTP/2 connection keepalive using PING frames.
+ // +optional
+ ConnectionKeepalive *HTTP2ConnectionKeepalive `json:"connectionKeepalive,omitempty"`
+}
+
+// HTTP2ConnectionKeepalive configures HTTP/2 PING-based keepalive settings.
+type HTTP2ConnectionKeepalive struct {
+ // Interval specifies how often to send HTTP/2 PING frames to keep the connection alive.
+ // If not set, PING frames will not be sent periodically.
+ // +optional
+ Interval *gwapiv1.Duration `json:"interval,omitempty"`
+
+ // Timeout specifies how long to wait for a PING response before considering the connection dead.
+ // If not set, a default timeout is used.
+ // +optional
+ Timeout *gwapiv1.Duration `json:"timeout,omitempty"`
+
+ // IntervalJitter specifies a random jitter percentage added to each interval.
+ // Defaults to 15% if not specified.
+ // +kubebuilder:validation:Minimum=0
+ // +kubebuilder:validation:Maximum=100
+ // +optional
+ IntervalJitter *uint32 `json:"intervalJitter,omitempty"`
+
+ // ConnectionIdleInterval specifies how long a connection must be idle before a PING is sent
+ // to check if the connection is still alive.
+ // If not set, idle connection checks are not performed.
+ // +optional
+ ConnectionIdleInterval *gwapiv1.Duration `json:"connectionIdleInterval,omitempty"`
+}
+
func init() {
localSchemeBuilder.Register(&ClientTrafficPolicy{}, &ClientTrafficPolicyList{})
}
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index fb1eedf645..740c1fea1f 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -1196,7 +1196,7 @@ func (in *ClientTrafficPolicySpec) DeepCopyInto(out *ClientTrafficPolicySpec) {
}
if in.HTTP2 != nil {
in, out := &in.HTTP2, &out.HTTP2
- *out = new(HTTP2Settings)
+ *out = new(HTTP2ClientSettings)
(*in).DeepCopyInto(*out)
}
if in.HTTP3 != nil {
@@ -3491,6 +3491,62 @@ func (in *HTTP1Settings) DeepCopy() *HTTP1Settings {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HTTP2ClientSettings) DeepCopyInto(out *HTTP2ClientSettings) {
+ *out = *in
+ in.HTTP2Settings.DeepCopyInto(&out.HTTP2Settings)
+ if in.ConnectionKeepalive != nil {
+ in, out := &in.ConnectionKeepalive, &out.ConnectionKeepalive
+ *out = new(HTTP2ConnectionKeepalive)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTP2ClientSettings.
+func (in *HTTP2ClientSettings) DeepCopy() *HTTP2ClientSettings {
+ if in == nil {
+ return nil
+ }
+ out := new(HTTP2ClientSettings)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HTTP2ConnectionKeepalive) DeepCopyInto(out *HTTP2ConnectionKeepalive) {
+ *out = *in
+ if in.Interval != nil {
+ in, out := &in.Interval, &out.Interval
+ *out = new(v1.Duration)
+ **out = **in
+ }
+ if in.Timeout != nil {
+ in, out := &in.Timeout, &out.Timeout
+ *out = new(v1.Duration)
+ **out = **in
+ }
+ if in.IntervalJitter != nil {
+ in, out := &in.IntervalJitter, &out.IntervalJitter
+ *out = new(uint32)
+ **out = **in
+ }
+ if in.ConnectionIdleInterval != nil {
+ in, out := &in.ConnectionIdleInterval, &out.ConnectionIdleInterval
+ *out = new(v1.Duration)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTP2ConnectionKeepalive.
+func (in *HTTP2ConnectionKeepalive) DeepCopy() *HTTP2ConnectionKeepalive {
+ if in == nil {
+ return nil
+ }
+ out := new(HTTP2ConnectionKeepalive)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HTTP2Settings) DeepCopyInto(out *HTTP2Settings) {
*out = *in
diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
index f86e2c2eca..6d2a254080 100644
--- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
+++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
@@ -799,6 +799,38 @@ spec:
http2:
description: HTTP2 provides HTTP/2 configuration on the listener.
properties:
+ connectionKeepalive:
+ description: ConnectionKeepalive configures HTTP/2 connection
+ keepalive using PING frames.
+ properties:
+ connectionIdleInterval:
+ description: |-
+ ConnectionIdleInterval specifies how long a connection must be idle before a PING is sent
+ to check if the connection is still alive.
+ If not set, idle connection checks are not performed.
+ pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$
+ type: string
+ interval:
+ description: |-
+ Interval specifies how often to send HTTP/2 PING frames to keep the connection alive.
+ If not set, PING frames will not be sent periodically.
+ pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$
+ type: string
+ intervalJitter:
+ description: |-
+ IntervalJitter specifies a random jitter percentage added to each interval.
+ Defaults to 15% if not specified.
+ format: int32
+ maximum: 100
+ minimum: 0
+ type: integer
+ timeout:
+ description: |-
+ Timeout specifies how long to wait for a PING response before considering the connection dead.
+ If not set, a default timeout is used.
+ pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$
+ type: string
+ type: object
initialConnectionWindowSize:
allOf:
- pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
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 0a83915dbe..d32d718008 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml
@@ -798,6 +798,38 @@ spec:
http2:
description: HTTP2 provides HTTP/2 configuration on the listener.
properties:
+ connectionKeepalive:
+ description: ConnectionKeepalive configures HTTP/2 connection
+ keepalive using PING frames.
+ properties:
+ connectionIdleInterval:
+ description: |-
+ ConnectionIdleInterval specifies how long a connection must be idle before a PING is sent
+ to check if the connection is still alive.
+ If not set, idle connection checks are not performed.
+ pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$
+ type: string
+ interval:
+ description: |-
+ Interval specifies how often to send HTTP/2 PING frames to keep the connection alive.
+ If not set, PING frames will not be sent periodically.
+ pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$
+ type: string
+ intervalJitter:
+ description: |-
+ IntervalJitter specifies a random jitter percentage added to each interval.
+ Defaults to 15% if not specified.
+ format: int32
+ maximum: 100
+ minimum: 0
+ type: integer
+ timeout:
+ description: |-
+ Timeout specifies how long to wait for a PING response before considering the connection dead.
+ If not set, a default timeout is used.
+ pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$
+ type: string
+ type: object
initialConnectionWindowSize:
allOf:
- pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
diff --git a/internal/gatewayapi/clienttrafficpolicy.go b/internal/gatewayapi/clienttrafficpolicy.go
index 076f4e779e..54cb71e25f 100644
--- a/internal/gatewayapi/clienttrafficpolicy.go
+++ b/internal/gatewayapi/clienttrafficpolicy.go
@@ -470,7 +470,7 @@ func (t *Translator) translateClientTrafficPolicyForListener(
}
// Translate HTTP2 Settings
- if h2, err := buildIRHTTP2Settings(policy.Spec.HTTP2); err != nil {
+ if h2, err := buildIRHTTP2ClientSettings(policy.Spec.HTTP2); err != nil {
err = perr.WithMessage(err, "HTTP2")
errs = errors.Join(errs, err)
} else {
diff --git a/internal/gatewayapi/http.go b/internal/gatewayapi/http.go
index e54b3f761d..f8316c7522 100644
--- a/internal/gatewayapi/http.go
+++ b/internal/gatewayapi/http.go
@@ -8,6 +8,7 @@ package gatewayapi
import (
"errors"
"fmt"
+ "time"
"k8s.io/utils/ptr"
@@ -74,3 +75,52 @@ func buildIRHTTP2Settings(http2Settings *egv1a1.HTTP2Settings) (*ir.HTTP2Setting
return http2, errs
}
+
+func buildIRHTTP2ClientSettings(http2Settings *egv1a1.HTTP2ClientSettings) (*ir.HTTP2Settings, error) {
+ if http2Settings == nil {
+ return nil, nil
+ }
+
+ // Reuse shared builder for common fields
+ http2, errs := buildIRHTTP2Settings(&http2Settings.HTTP2Settings)
+ if http2 == nil {
+ http2 = &ir.HTTP2Settings{}
+ }
+
+ if http2Settings.ConnectionKeepalive != nil {
+ keepalive := &ir.HTTP2ConnectionKeepalive{}
+ if http2Settings.ConnectionKeepalive.Interval != nil {
+ d, err := time.ParseDuration(string(*http2Settings.ConnectionKeepalive.Interval))
+ if err != nil {
+ errs = errors.Join(errs, fmt.Errorf("invalid ConnectionKeepalive.Interval: %w", err))
+ } else {
+ keepalive.Interval = ptr.To(uint32(d.Seconds()))
+ }
+ }
+ if http2Settings.ConnectionKeepalive.Timeout != nil {
+ d, err := time.ParseDuration(string(*http2Settings.ConnectionKeepalive.Timeout))
+ if err != nil {
+ errs = errors.Join(errs, fmt.Errorf("invalid ConnectionKeepalive.Timeout: %w", err))
+ } else {
+ keepalive.Timeout = ptr.To(uint32(d.Seconds()))
+ }
+ }
+ if keepalive.Interval != nil && keepalive.Timeout != nil && *keepalive.Timeout >= *keepalive.Interval {
+ errs = errors.Join(errs, fmt.Errorf("ConnectionKeepalive.Timeout must be less than Interval"))
+ }
+ if http2Settings.ConnectionKeepalive.IntervalJitter != nil {
+ keepalive.IntervalJitter = http2Settings.ConnectionKeepalive.IntervalJitter
+ }
+ if http2Settings.ConnectionKeepalive.ConnectionIdleInterval != nil {
+ d, err := time.ParseDuration(string(*http2Settings.ConnectionKeepalive.ConnectionIdleInterval))
+ if err != nil {
+ errs = errors.Join(errs, fmt.Errorf("invalid ConnectionKeepalive.ConnectionIdleInterval: %w", err))
+ } else {
+ keepalive.ConnectionIdleInterval = ptr.To(uint32(d.Seconds()))
+ }
+ }
+ http2.ConnectionKeepalive = keepalive
+ }
+
+ return http2, errs
+}
diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-http2-keepalive-invalid-timeout.in.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-http2-keepalive-invalid-timeout.in.yaml
new file mode 100644
index 0000000000..af370c367c
--- /dev/null
+++ b/internal/gatewayapi/testdata/clienttrafficpolicy-http2-keepalive-invalid-timeout.in.yaml
@@ -0,0 +1,31 @@
+clientTrafficPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: ClientTrafficPolicy
+ metadata:
+ namespace: envoy-gateway
+ name: target-gateway-1-section-http-1
+ spec:
+ http2:
+ connectionKeepalive:
+ interval: 10s
+ timeout: 60s
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ sectionName: http-1
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ namespace: envoy-gateway
+ name: gateway-1
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - name: http-1
+ protocol: HTTP
+ port: 80
+ allowedRoutes:
+ namespaces:
+ from: Same
diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-http2-keepalive-invalid-timeout.out.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-http2-keepalive-invalid-timeout.out.yaml
new file mode 100644
index 0000000000..0d3a5ea81d
--- /dev/null
+++ b/internal/gatewayapi/testdata/clienttrafficpolicy-http2-keepalive-invalid-timeout.out.yaml
@@ -0,0 +1,138 @@
+clientTrafficPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: ClientTrafficPolicy
+ metadata:
+ name: target-gateway-1-section-http-1
+ namespace: envoy-gateway
+ spec:
+ http2:
+ connectionKeepalive:
+ interval: 10s
+ timeout: 60s
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ sectionName: http-1
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http-1
+ conditions:
+ - lastTransitionTime: null
+ message: 'HTTP2: ConnectionKeepalive.Timeout must be less than Interval.'
+ reason: Invalid
+ status: "False"
+ type: Accepted
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ name: gateway-1
+ namespace: envoy-gateway
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - allowedRoutes:
+ namespaces:
+ from: Same
+ name: http-1
+ port: 80
+ protocol: HTTP
+ status:
+ listeners:
+ - attachedRoutes: 0
+ conditions:
+ - lastTransitionTime: null
+ message: Sending translated listener configuration to the data plane
+ reason: Programmed
+ status: "True"
+ type: Programmed
+ - lastTransitionTime: null
+ message: Listener has been successfully translated
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Listener references have been resolved
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ name: http-1
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ - group: gateway.networking.k8s.io
+ kind: GRPCRoute
+infraIR:
+ envoy-gateway/gateway-1:
+ proxy:
+ listeners:
+ - address: null
+ name: envoy-gateway/gateway-1/http-1
+ ports:
+ - containerPort: 10080
+ name: http-80
+ protocol: HTTP
+ servicePort: 80
+ metadata:
+ labels:
+ gateway.envoyproxy.io/owning-gateway-name: gateway-1
+ gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+ ownerReference:
+ kind: GatewayClass
+ name: envoy-gateway-class
+ name: envoy-gateway/gateway-1
+ namespace: envoy-gateway-system
+xdsIR:
+ envoy-gateway/gateway-1:
+ accessLog:
+ json:
+ - path: /dev/stdout
+ globalResources:
+ proxyServiceCluster:
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-1-196ae069
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-1
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.6.5.4
+ port: 8080
+ zone: zone1
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-1-196ae069
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-1
+ protocol: TCP
+ http:
+ - address: 0.0.0.0
+ externalPort: 80
+ hostnames:
+ - '*'
+ isHTTP2: false
+ metadata:
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http-1
+ name: envoy-gateway/gateway-1/http-1
+ path:
+ escapedSlashesAction: UnescapeAndRedirect
+ mergeSlashes: true
+ port: 10080
+ readyListener:
+ address: 0.0.0.0
+ ipFamily: IPv4
+ path: /ready
+ port: 19003
diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-http2-keepalive.in.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-http2-keepalive.in.yaml
new file mode 100644
index 0000000000..1fe936bce7
--- /dev/null
+++ b/internal/gatewayapi/testdata/clienttrafficpolicy-http2-keepalive.in.yaml
@@ -0,0 +1,33 @@
+clientTrafficPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: ClientTrafficPolicy
+ metadata:
+ namespace: envoy-gateway
+ name: target-gateway-1-section-http-1
+ spec:
+ http2:
+ connectionKeepalive:
+ interval: 60s
+ timeout: 10s
+ intervalJitter: 20
+ connectionIdleInterval: 30s
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ sectionName: http-1
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ namespace: envoy-gateway
+ name: gateway-1
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - name: http-1
+ protocol: HTTP
+ port: 80
+ allowedRoutes:
+ namespaces:
+ from: Same
diff --git a/internal/gatewayapi/testdata/clienttrafficpolicy-http2-keepalive.out.yaml b/internal/gatewayapi/testdata/clienttrafficpolicy-http2-keepalive.out.yaml
new file mode 100644
index 0000000000..4a7902d104
--- /dev/null
+++ b/internal/gatewayapi/testdata/clienttrafficpolicy-http2-keepalive.out.yaml
@@ -0,0 +1,146 @@
+clientTrafficPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: ClientTrafficPolicy
+ metadata:
+ name: target-gateway-1-section-http-1
+ namespace: envoy-gateway
+ spec:
+ http2:
+ connectionKeepalive:
+ connectionIdleInterval: 30s
+ interval: 60s
+ intervalJitter: 20
+ timeout: 10s
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ sectionName: http-1
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http-1
+ conditions:
+ - lastTransitionTime: null
+ message: Policy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ name: gateway-1
+ namespace: envoy-gateway
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - allowedRoutes:
+ namespaces:
+ from: Same
+ name: http-1
+ port: 80
+ protocol: HTTP
+ status:
+ listeners:
+ - attachedRoutes: 0
+ conditions:
+ - lastTransitionTime: null
+ message: Sending translated listener configuration to the data plane
+ reason: Programmed
+ status: "True"
+ type: Programmed
+ - lastTransitionTime: null
+ message: Listener has been successfully translated
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Listener references have been resolved
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ name: http-1
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ - group: gateway.networking.k8s.io
+ kind: GRPCRoute
+infraIR:
+ envoy-gateway/gateway-1:
+ proxy:
+ listeners:
+ - address: null
+ name: envoy-gateway/gateway-1/http-1
+ ports:
+ - containerPort: 10080
+ name: http-80
+ protocol: HTTP
+ servicePort: 80
+ metadata:
+ labels:
+ gateway.envoyproxy.io/owning-gateway-name: gateway-1
+ gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+ ownerReference:
+ kind: GatewayClass
+ name: envoy-gateway-class
+ name: envoy-gateway/gateway-1
+ namespace: envoy-gateway-system
+xdsIR:
+ envoy-gateway/gateway-1:
+ accessLog:
+ json:
+ - path: /dev/stdout
+ globalResources:
+ proxyServiceCluster:
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-1-196ae069
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-1
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.6.5.4
+ port: 8080
+ zone: zone1
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-1-196ae069
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-1
+ protocol: TCP
+ http:
+ - address: 0.0.0.0
+ externalPort: 80
+ hostnames:
+ - '*'
+ http2:
+ connectionKeepalive:
+ connectionIdleInterval: 30
+ interval: 60
+ intervalJitter: 20
+ timeout: 10
+ isHTTP2: false
+ metadata:
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http-1
+ name: envoy-gateway/gateway-1/http-1
+ path:
+ escapedSlashesAction: UnescapeAndRedirect
+ mergeSlashes: true
+ port: 10080
+ readyListener:
+ address: 0.0.0.0
+ ipFamily: IPv4
+ path: /ready
+ port: 19003
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index 7b8f10eb1f..a902416923 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -616,6 +616,21 @@ type HTTP2Settings struct {
MaxConcurrentStreams *uint32 `json:"maxConcurrentStreams,omitempty" yaml:"maxConcurrentStreams,omitempty"`
// ResetStreamOnError determines if a stream or connection is reset on messaging error.
ResetStreamOnError *bool `json:"resetStreamOnError,omitempty" yaml:"resetStreamOnError,omitempty"`
+ // ConnectionKeepalive configures HTTP/2 PING-based keepalive settings.
+ ConnectionKeepalive *HTTP2ConnectionKeepalive `json:"connectionKeepalive,omitempty" yaml:"connectionKeepalive,omitempty"`
+}
+
+// HTTP2ConnectionKeepalive configures HTTP/2 PING-based keepalive settings.
+// +k8s:deepcopy-gen=true
+type HTTP2ConnectionKeepalive struct {
+ // Interval specifies how often to send HTTP/2 PING frames (in seconds).
+ Interval *uint32 `json:"interval,omitempty" yaml:"interval,omitempty"`
+ // Timeout specifies how long to wait for a PING response (in seconds).
+ Timeout *uint32 `json:"timeout,omitempty" yaml:"timeout,omitempty"`
+ // IntervalJitter specifies a random jitter percentage added to each interval (0-100).
+ IntervalJitter *uint32 `json:"intervalJitter,omitempty" yaml:"intervalJitter,omitempty"`
+ // ConnectionIdleInterval specifies idle time before sending a PING (in seconds).
+ ConnectionIdleInterval *uint32 `json:"connectionIdleInterval,omitempty" yaml:"connectionIdleInterval,omitempty"`
}
// ResponseOverride defines the configuration to override specific responses with a custom one.
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index dc2b458f0c..130ac7b8c2 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -1702,6 +1702,41 @@ func (in *HTTP1Settings) DeepCopy() *HTTP1Settings {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HTTP2ConnectionKeepalive) DeepCopyInto(out *HTTP2ConnectionKeepalive) {
+ *out = *in
+ if in.Interval != nil {
+ in, out := &in.Interval, &out.Interval
+ *out = new(uint32)
+ **out = **in
+ }
+ if in.Timeout != nil {
+ in, out := &in.Timeout, &out.Timeout
+ *out = new(uint32)
+ **out = **in
+ }
+ if in.IntervalJitter != nil {
+ in, out := &in.IntervalJitter, &out.IntervalJitter
+ *out = new(uint32)
+ **out = **in
+ }
+ if in.ConnectionIdleInterval != nil {
+ in, out := &in.ConnectionIdleInterval, &out.ConnectionIdleInterval
+ *out = new(uint32)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTP2ConnectionKeepalive.
+func (in *HTTP2ConnectionKeepalive) DeepCopy() *HTTP2ConnectionKeepalive {
+ if in == nil {
+ return nil
+ }
+ out := new(HTTP2ConnectionKeepalive)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HTTP2Settings) DeepCopyInto(out *HTTP2Settings) {
*out = *in
@@ -1725,6 +1760,11 @@ func (in *HTTP2Settings) DeepCopyInto(out *HTTP2Settings) {
*out = new(bool)
**out = **in
}
+ if in.ConnectionKeepalive != nil {
+ in, out := &in.ConnectionKeepalive, &out.ConnectionKeepalive
+ *out = new(HTTP2ConnectionKeepalive)
+ (*in).DeepCopyInto(*out)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTP2Settings.
diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go
index 8e4be35e29..f866caf0ed 100644
--- a/internal/xds/translator/listener.go
+++ b/internal/xds/translator/listener.go
@@ -11,6 +11,7 @@ import (
"net"
"strconv"
"strings"
+ "time"
xdscore "github.com/cncf/xds/go/xds/core/v3"
matcher "github.com/cncf/xds/go/xds/type/matcher/v3"
@@ -110,6 +111,23 @@ func http2ProtocolOptions(opts *ir.HTTP2Settings) *corev3.Http2ProtocolOptions {
}
}
+ if opts.ConnectionKeepalive != nil {
+ keepalive := &corev3.KeepaliveSettings{}
+ if opts.ConnectionKeepalive.Interval != nil {
+ keepalive.Interval = durationpb.New(time.Duration(*opts.ConnectionKeepalive.Interval) * time.Second)
+ }
+ if opts.ConnectionKeepalive.Timeout != nil {
+ keepalive.Timeout = durationpb.New(time.Duration(*opts.ConnectionKeepalive.Timeout) * time.Second)
+ }
+ if opts.ConnectionKeepalive.IntervalJitter != nil {
+ keepalive.IntervalJitter = &typev3.Percent{Value: float64(*opts.ConnectionKeepalive.IntervalJitter)}
+ }
+ if opts.ConnectionKeepalive.ConnectionIdleInterval != nil {
+ keepalive.ConnectionIdleInterval = durationpb.New(time.Duration(*opts.ConnectionKeepalive.ConnectionIdleInterval) * time.Second)
+ }
+ out.ConnectionKeepalive = keepalive
+ }
+
return out
}
diff --git a/internal/xds/translator/testdata/in/xds-ir/http2-keepalive.yaml b/internal/xds/translator/testdata/in/xds-ir/http2-keepalive.yaml
new file mode 100644
index 0000000000..b539ec8d5b
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/http2-keepalive.yaml
@@ -0,0 +1,28 @@
+http:
+- name: "first-listener"
+ address: "::"
+ port: 10080
+ hostnames:
+ - "foo.com"
+ path:
+ mergeSlashes: true
+ escapedSlashesAction: UnescapeAndRedirect
+ http2:
+ initialConnectionWindowSize: 65536
+ initialStreamWindowSize: 33554432
+ maxConcurrentStreams: 200
+ connectionKeepalive:
+ interval: 60
+ timeout: 10
+ intervalJitter: 20
+ connectionIdleInterval: 30
+ routes:
+ - name: "first-route"
+ hostname: "*"
+ destination:
+ name: "first-route-dest"
+ settings:
+ - endpoints:
+ - host: "1.2.3.4"
+ port: 50000
+ name: "first-route-dest/backend/0"
diff --git a/internal/xds/translator/testdata/out/xds-ir/http2-keepalive.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http2-keepalive.clusters.yaml
new file mode 100644
index 0000000000..054f90bb71
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http2-keepalive.clusters.yaml
@@ -0,0 +1,24 @@
+- circuitBreakers:
+ thresholds:
+ - maxRetries: 1024
+ commonLbConfig: {}
+ connectTimeout: 10s
+ dnsLookupFamily: V4_PREFERRED
+ edsClusterConfig:
+ edsConfig:
+ ads: {}
+ resourceApiVersion: V3
+ serviceName: first-route-dest
+ ignoreHealthOnHostRemoval: true
+ lbPolicy: LEAST_REQUEST
+ loadBalancingPolicy:
+ policies:
+ - typedExtensionConfig:
+ name: envoy.load_balancing_policies.least_request
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+ localityLbConfig:
+ localityWeightedLbConfig: {}
+ name: first-route-dest
+ perConnectionBufferLimitBytes: 32768
+ type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/http2-keepalive.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http2-keepalive.endpoints.yaml
new file mode 100644
index 0000000000..3b3f2d0907
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http2-keepalive.endpoints.yaml
@@ -0,0 +1,12 @@
+- clusterName: first-route-dest
+ endpoints:
+ - lbEndpoints:
+ - endpoint:
+ address:
+ socketAddress:
+ address: 1.2.3.4
+ portValue: 50000
+ loadBalancingWeight: 1
+ loadBalancingWeight: 1
+ locality:
+ region: first-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http2-keepalive.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http2-keepalive.listeners.yaml
new file mode 100644
index 0000000000..00964415dd
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http2-keepalive.listeners.yaml
@@ -0,0 +1,41 @@
+- address:
+ socketAddress:
+ address: '::'
+ portValue: 10080
+ 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:
+ connectionKeepalive:
+ connectionIdleInterval: 30s
+ interval: 60s
+ intervalJitter:
+ value: 20
+ timeout: 10s
+ initialConnectionWindowSize: 33554432
+ initialStreamWindowSize: 65536
+ maxConcurrentStreams: 200
+ httpFilters:
+ - name: envoy.filters.http.router
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+ suppressEnvoyHeaders: true
+ mergeSlashes: true
+ normalizePath: true
+ pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT
+ rds:
+ configSource:
+ ads: {}
+ resourceApiVersion: V3
+ routeConfigName: first-listener
+ serverHeaderTransformation: PASS_THROUGH
+ statPrefix: http-10080
+ useRemoteAddress: true
+ name: first-listener
+ maxConnectionsToAcceptPerSocketEvent: 1
+ name: first-listener
+ perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/http2-keepalive.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http2-keepalive.routes.yaml
new file mode 100644
index 0000000000..0b5b4bee7b
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http2-keepalive.routes.yaml
@@ -0,0 +1,14 @@
+- ignorePortInHostMatching: true
+ name: first-listener
+ virtualHosts:
+ - domains:
+ - '*'
+ name: first-listener/*
+ routes:
+ - match:
+ prefix: /
+ name: first-route
+ route:
+ cluster: first-route-dest
+ upgradeConfigs:
+ - upgradeType: websocket
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index 1faf4c4c4c..368d776c6c 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -9,6 +9,7 @@ security updates: |
# New features or capabilities added in this release.
new features: |
Added support for configuring optional health check configuration.
+ Added HTTP/2 connection keepalive support to ClientTrafficPolicy.
bug fixes: |
Rejected ClientTrafficPolicy if invalid TLS cipher suites are configured.
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index b4e5822d70..29c6e848db 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -800,7 +800,7 @@ _Appears in:_
| `timeout` | _[ClientTimeout](#clienttimeout)_ | false | | Timeout settings for the client connections. |
| `connection` | _[ClientConnection](#clientconnection)_ | false | | Connection includes client connection settings. |
| `http1` | _[HTTP1Settings](#http1settings)_ | false | | HTTP1 provides HTTP/1 configuration on the listener. |
-| `http2` | _[HTTP2Settings](#http2settings)_ | false | | HTTP2 provides HTTP/2 configuration on the listener. |
+| `http2` | _[HTTP2ClientSettings](#http2clientsettings)_ | false | | HTTP2 provides HTTP/2 configuration on the listener. |
| `http3` | _[HTTP3Settings](#http3settings)_ | false | | HTTP3 provides HTTP/3 configuration on the listener. |
| `healthCheck` | _[HealthCheckSettings](#healthchecksettings)_ | false | | HealthCheck provides configuration for determining whether the HTTP/HTTPS listener is healthy. |
| `scheme` | _[SchemeHeaderTransform](#schemeheadertransform)_ | false | | Scheme configures how the :scheme pseudo-header is set for requests forwarded to backends.
- Preserve (default): Preserves the :scheme from the original client request.
Use this when backends need to know the original client scheme for URL generation or redirects.
- MatchBackend: Sets the :scheme to match the backend transport protocol.
If the backend uses TLS, the scheme is "https", otherwise "http".
Use this when backends require the scheme to match the actual transport protocol,
such as strictly HTTPS services that validate the :scheme header. |
@@ -2386,6 +2386,41 @@ _Appears in:_
| `disableSafeMaxConnectionDuration` | _boolean_ | false | | DisableSafeMaxConnectionDuration controls the close behavior for HTTP/1 connections.
By default, connection closure is delayed until the next request arrives after maxConnectionDuration is exceeded.
It then adds a Connection: close header and gracefully closes the connection after the response completes.
When set to true (disabled), Envoy uses its default drain behavior, closing the connection shortly after maxConnectionDuration elapses.
Has no effect unless maxConnectionDuration is set. |
+#### HTTP2ClientSettings
+
+
+
+HTTP2ClientSettings provides HTTP/2 configuration for client connections to the listener.
+
+_Appears in:_
+- [ClientTrafficPolicySpec](#clienttrafficpolicyspec)
+
+| Field | Type | Required | Default | Description |
+| --- | --- | --- | --- | --- |
+| `initialStreamWindowSize` | _[Quantity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#quantity-resource-api)_ | false | | InitialStreamWindowSize sets the initial window size for HTTP/2 streams.
If not set, the default value is 64 KiB(64*1024). |
+| `initialConnectionWindowSize` | _[Quantity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#quantity-resource-api)_ | false | | InitialConnectionWindowSize sets the initial window size for HTTP/2 connections.
If not set, the default value is 1 MiB. |
+| `maxConcurrentStreams` | _integer_ | false | | MaxConcurrentStreams sets the maximum number of concurrent streams allowed per connection.
If not set, the default value is 100. |
+| `onInvalidMessage` | _[InvalidMessageAction](#invalidmessageaction)_ | false | | OnInvalidMessage determines if Envoy will terminate the connection or just the offending stream in the event of HTTP messaging error
It's recommended for L2 Envoy deployments to set this value to TerminateStream.
https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/level_two
Default: TerminateConnection |
+| `connectionKeepalive` | _[HTTP2ConnectionKeepalive](#http2connectionkeepalive)_ | false | | ConnectionKeepalive configures HTTP/2 connection keepalive using PING frames. |
+
+
+#### HTTP2ConnectionKeepalive
+
+
+
+HTTP2ConnectionKeepalive configures HTTP/2 PING-based keepalive settings.
+
+_Appears in:_
+- [HTTP2ClientSettings](#http2clientsettings)
+
+| Field | Type | Required | Default | Description |
+| --- | --- | --- | --- | --- |
+| `interval` | _[Duration](https://gateway-api.sigs.k8s.io/reference/1.4/spec/#duration)_ | false | | Interval specifies how often to send HTTP/2 PING frames to keep the connection alive.
If not set, PING frames will not be sent periodically. |
+| `timeout` | _[Duration](https://gateway-api.sigs.k8s.io/reference/1.4/spec/#duration)_ | false | | Timeout specifies how long to wait for a PING response before considering the connection dead.
If not set, a default timeout is used. |
+| `intervalJitter` | _integer_ | false | | IntervalJitter specifies a random jitter percentage added to each interval.
Defaults to 15% if not specified. |
+| `connectionIdleInterval` | _[Duration](https://gateway-api.sigs.k8s.io/reference/1.4/spec/#duration)_ | false | | ConnectionIdleInterval specifies how long a connection must be idle before a PING is sent
to check if the connection is still alive.
If not set, idle connection checks are not performed. |
+
+
#### HTTP2Settings
@@ -2394,8 +2429,8 @@ HTTP2Settings provides HTTP/2 configuration for listeners and backends.
_Appears in:_
- [BackendTrafficPolicySpec](#backendtrafficpolicyspec)
-- [ClientTrafficPolicySpec](#clienttrafficpolicyspec)
- [ClusterSettings](#clustersettings)
+- [HTTP2ClientSettings](#http2clientsettings)
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
@@ -2925,6 +2960,7 @@ _Underlying type:_ _string_
_Appears in:_
+- [HTTP2ClientSettings](#http2clientsettings)
- [HTTP2Settings](#http2settings)
| Value | Description |
diff --git a/test/cel-validation/clienttrafficpolicy_test.go b/test/cel-validation/clienttrafficpolicy_test.go
index ab28d5142c..97e2fab549 100644
--- a/test/cel-validation/clienttrafficpolicy_test.go
+++ b/test/cel-validation/clienttrafficpolicy_test.go
@@ -465,8 +465,10 @@ func TestClientTrafficPolicyTarget(t *testing.T) {
},
},
},
- HTTP2: &egv1a1.HTTP2Settings{
- InitialStreamWindowSize: ptr.To(resource.MustParse("15m")),
+ HTTP2: &egv1a1.HTTP2ClientSettings{
+ HTTP2Settings: egv1a1.HTTP2Settings{
+ InitialStreamWindowSize: ptr.To(resource.MustParse("15m")),
+ },
},
}
},
@@ -487,8 +489,10 @@ func TestClientTrafficPolicyTarget(t *testing.T) {
},
},
},
- HTTP2: &egv1a1.HTTP2Settings{
- InitialConnectionWindowSize: ptr.To(resource.MustParse("15m")),
+ HTTP2: &egv1a1.HTTP2ClientSettings{
+ HTTP2Settings: egv1a1.HTTP2Settings{
+ InitialConnectionWindowSize: ptr.To(resource.MustParse("15m")),
+ },
},
}
},
diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml
index 8ad08a02bb..ecf0ffcc49 100644
--- a/test/helm/gateway-crds-helm/all.out.yaml
+++ b/test/helm/gateway-crds-helm/all.out.yaml
@@ -25095,6 +25095,38 @@ spec:
http2:
description: HTTP2 provides HTTP/2 configuration on the listener.
properties:
+ connectionKeepalive:
+ description: ConnectionKeepalive configures HTTP/2 connection
+ keepalive using PING frames.
+ properties:
+ connectionIdleInterval:
+ description: |-
+ ConnectionIdleInterval specifies how long a connection must be idle before a PING is sent
+ to check if the connection is still alive.
+ If not set, idle connection checks are not performed.
+ pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$
+ type: string
+ interval:
+ description: |-
+ Interval specifies how often to send HTTP/2 PING frames to keep the connection alive.
+ If not set, PING frames will not be sent periodically.
+ pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$
+ type: string
+ intervalJitter:
+ description: |-
+ IntervalJitter specifies a random jitter percentage added to each interval.
+ Defaults to 15% if not specified.
+ format: int32
+ maximum: 100
+ minimum: 0
+ type: integer
+ timeout:
+ description: |-
+ Timeout specifies how long to wait for a PING response before considering the connection dead.
+ If not set, a default timeout is used.
+ pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$
+ type: string
+ type: object
initialConnectionWindowSize:
allOf:
- pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
index ebb016d68c..951cd52181 100644
--- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
+++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
@@ -4275,6 +4275,38 @@ spec:
http2:
description: HTTP2 provides HTTP/2 configuration on the listener.
properties:
+ connectionKeepalive:
+ description: ConnectionKeepalive configures HTTP/2 connection
+ keepalive using PING frames.
+ properties:
+ connectionIdleInterval:
+ description: |-
+ ConnectionIdleInterval specifies how long a connection must be idle before a PING is sent
+ to check if the connection is still alive.
+ If not set, idle connection checks are not performed.
+ pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$
+ type: string
+ interval:
+ description: |-
+ Interval specifies how often to send HTTP/2 PING frames to keep the connection alive.
+ If not set, PING frames will not be sent periodically.
+ pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$
+ type: string
+ intervalJitter:
+ description: |-
+ IntervalJitter specifies a random jitter percentage added to each interval.
+ Defaults to 15% if not specified.
+ format: int32
+ maximum: 100
+ minimum: 0
+ type: integer
+ timeout:
+ description: |-
+ Timeout specifies how long to wait for a PING response before considering the connection dead.
+ If not set, a default timeout is used.
+ pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$
+ type: string
+ type: object
initialConnectionWindowSize:
allOf:
- pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$