diff --git a/apis/projectcontour/v1alpha1/contourconfig.go b/apis/projectcontour/v1alpha1/contourconfig.go index 5699d5e1f30..d231d19bbaa 100644 --- a/apis/projectcontour/v1alpha1/contourconfig.go +++ b/apis/projectcontour/v1alpha1/contourconfig.go @@ -391,6 +391,27 @@ type EnvoyListenerConfig struct { // Single set of options are applied to all listeners. // +optional SocketOptions *SocketOptions `json:"socketOptions,omitempty"` + + // Defines the limit on number of HTTP requests that Envoy will process from a single + // connection in a single I/O cycle. Requests over this limit are processed in subsequent + // I/O cycles. Can be used as a mitigation for CVE-2023-44487 when abusive traffic is + // detected. Configures the http.max_requests_per_io_cycle Envoy runtime setting. The default + // value when this is not set is no limit. + // + // +kubebuilder:validation:Minimum=1 + // +optional + MaxRequestsPerIOCycle *uint32 `json:"maxRequestsPerIOCycle,omitempty"` + + // Defines the value for SETTINGS_MAX_CONCURRENT_STREAMS Envoy will advertise in the + // SETTINGS frame in HTTP/2 connections and the limit for concurrent streams allowed + // for a peer on a single HTTP/2 connection. It is recommended to not set this lower + // than 100 but this field can be used to bound resource usage by HTTP/2 connections + // and mitigate attacks like CVE-2023-44487. The default value when this is not set is + // unlimited. + // + // +kubebuilder:validation:Minimum=1 + // +optional + HTTP2MaxConcurrentStreams *uint32 `json:"httpMaxConcurrentStreams,omitempty"` } // SocketOptions defines configurable socket options for Envoy listeners. diff --git a/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go b/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go index d889c429aa5..ff4f84765b9 100644 --- a/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go +++ b/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go @@ -582,6 +582,16 @@ func (in *EnvoyListenerConfig) DeepCopyInto(out *EnvoyListenerConfig) { *out = new(SocketOptions) **out = **in } + if in.MaxRequestsPerIOCycle != nil { + in, out := &in.MaxRequestsPerIOCycle, &out.MaxRequestsPerIOCycle + *out = new(uint32) + **out = **in + } + if in.HTTP2MaxConcurrentStreams != nil { + in, out := &in.HTTP2MaxConcurrentStreams, &out.HTTP2MaxConcurrentStreams + *out = new(uint32) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvoyListenerConfig. diff --git a/changelogs/unreleased/5827-sunjayBhatia-minor.md b/changelogs/unreleased/5827-sunjayBhatia-minor.md new file mode 100644 index 00000000000..93010ffb61a --- /dev/null +++ b/changelogs/unreleased/5827-sunjayBhatia-minor.md @@ -0,0 +1,16 @@ +## Max HTTP requests per IO cycle is configurable as an additional mitigation for HTTP/2 CVE-2023-44487 + +Envoy v1.27.1 mitigates CVE-2023-44487 with some default runtime settings, however the `http.max_requests_per_io_cycle` does not have a default value. +This change allows configuring this runtime setting via Contour configuration to allow administrators of Contour to prevent abusive connections from starving resources from other valid connections. +The default is left as the existing behavior (no limit) so as not to impact existing valid traffic. + +The Contour ConfigMap can be modified similar to the following (and Contour restarted) to set this value: + +``` +listener: + max-requests-per-io-cycle: 10 +``` + +(Note this can be used in addition to the existing Listener configuration field `listener.max-requests-per-connection` which is used primarily for HTTP/1.1 connections and is an approximate limit for HTTP/2) + +See the [Envoy release notes](https://www.envoyproxy.io/docs/envoy/v1.27.1/version_history/v1.27/v1.27.1) for more details. diff --git a/changelogs/unreleased/5850-sunjayBhatia-minor.md b/changelogs/unreleased/5850-sunjayBhatia-minor.md new file mode 100644 index 00000000000..32e38a6c494 --- /dev/null +++ b/changelogs/unreleased/5850-sunjayBhatia-minor.md @@ -0,0 +1,11 @@ +## HTTP/2 max concurrent streams is configurable + +This field can be used to limit the number of concurrent streams Envoy will allow on a single connection from a downstream peer. +It can be used to tune resource usage and as a mitigation for DOS attacks arising from vulnerabilities like CVE-2023-44487. + +The Contour ConfigMap can be modified similar to the following (and Contour restarted) to set this value: + +``` +listener: + http2-max-concurrent-streams: 50 +``` diff --git a/cmd/contour/serve.go b/cmd/contour/serve.go index 1d9a41a713d..6114545aacb 100644 --- a/cmd/contour/serve.go +++ b/cmd/contour/serve.go @@ -430,6 +430,7 @@ func (s *Server) doServe() error { XffNumTrustedHops: *contourConfiguration.Envoy.Network.XffNumTrustedHops, ConnectionBalancer: contourConfiguration.Envoy.Listener.ConnectionBalancer, MaxRequestsPerConnection: contourConfiguration.Envoy.Listener.MaxRequestsPerConnection, + HTTP2MaxConcurrentStreams: contourConfiguration.Envoy.Listener.HTTP2MaxConcurrentStreams, PerConnectionBufferLimitBytes: contourConfiguration.Envoy.Listener.PerConnectionBufferLimitBytes, SocketOptions: contourConfiguration.Envoy.Listener.SocketOptions, } @@ -458,7 +459,9 @@ func (s *Server) doServe() error { &xdscache_v3.RouteCache{}, &xdscache_v3.ClusterCache{}, endpointHandler, - &xdscache_v3.RuntimeCache{}, + xdscache_v3.NewRuntimeCache(xdscache_v3.ConfigurableRuntimeSettings{ + MaxRequestsPerIOCycle: contourConfiguration.Envoy.Listener.MaxRequestsPerIOCycle, + }), } // snapshotHandler is used to produce new snapshots when the internal state changes for any xDS resource. diff --git a/cmd/contour/servecontext.go b/cmd/contour/servecontext.go index 629ef75280f..7d3f72312ad 100644 --- a/cmd/contour/servecontext.go +++ b/cmd/contour/servecontext.go @@ -528,6 +528,8 @@ func (ctx *serveContext) convertToContourConfigurationSpec() contour_api_v1alpha ConnectionBalancer: ctx.Config.Listener.ConnectionBalancer, PerConnectionBufferLimitBytes: ctx.Config.Listener.PerConnectionBufferLimitBytes, MaxRequestsPerConnection: ctx.Config.Listener.MaxRequestsPerConnection, + MaxRequestsPerIOCycle: ctx.Config.Listener.MaxRequestsPerIOCycle, + HTTP2MaxConcurrentStreams: ctx.Config.Listener.HTTP2MaxConcurrentStreams, TLS: &contour_api_v1alpha1.EnvoyTLS{ MinimumProtocolVersion: ctx.Config.TLS.MinimumProtocolVersion, MaximumProtocolVersion: ctx.Config.TLS.MaximumProtocolVersion, diff --git a/cmd/contour/servecontext_test.go b/cmd/contour/servecontext_test.go index 1e7cc21db2a..dd8d3bf681c 100644 --- a/cmd/contour/servecontext_test.go +++ b/cmd/contour/servecontext_test.go @@ -874,6 +874,18 @@ func TestConvertServeContext(t *testing.T) { return cfg }, }, + "envoy listener settings": { + getServeContext: func(ctx *serveContext) *serveContext { + ctx.Config.Listener.MaxRequestsPerIOCycle = ref.To(uint32(10)) + ctx.Config.Listener.HTTP2MaxConcurrentStreams = ref.To(uint32(30)) + return ctx + }, + getContourConfiguration: func(cfg contour_api_v1alpha1.ContourConfigurationSpec) contour_api_v1alpha1.ContourConfigurationSpec { + cfg.Envoy.Listener.MaxRequestsPerIOCycle = ref.To(uint32(10)) + cfg.Envoy.Listener.HTTP2MaxConcurrentStreams = ref.To(uint32(30)) + return cfg + }, + }, } for name, tc := range cases { diff --git a/examples/contour/01-crds.yaml b/examples/contour/01-crds.yaml index 9fceb5c1458..909909afb98 100644 --- a/examples/contour/01-crds.yaml +++ b/examples/contour/01-crds.yaml @@ -196,6 +196,18 @@ spec: slashes from request URL paths. \n Contour's default is false." type: boolean + httpMaxConcurrentStreams: + description: Defines the value for SETTINGS_MAX_CONCURRENT_STREAMS + Envoy will advertise in the SETTINGS frame in HTTP/2 connections + and the limit for concurrent streams allowed for a peer + on a single HTTP/2 connection. It is recommended to not + set this lower than 100 but this field can be used to bound + resource usage by HTTP/2 connections and mitigate attacks + like CVE-2023-44487. The default value when this is not + set is unlimited. + format: int32 + minimum: 1 + type: integer maxRequestsPerConnection: description: Defines the maximum requests for downstream connections. If not specified, there is no limit. see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-msg-config-core-v3-httpprotocoloptions @@ -203,6 +215,17 @@ spec: format: int32 minimum: 1 type: integer + maxRequestsPerIOCycle: + description: Defines the limit on number of HTTP requests + that Envoy will process from a single connection in a single + I/O cycle. Requests over this limit are processed in subsequent + I/O cycles. Can be used as a mitigation for CVE-2023-44487 + when abusive traffic is detected. Configures the http.max_requests_per_io_cycle + Envoy runtime setting. The default value when this is not + set is no limit. + format: int32 + minimum: 1 + type: integer per-connection-buffer-limit-bytes: description: Defines the soft limit on size of the listener’s new connection read and write buffers in bytes. If unspecified, @@ -3626,6 +3649,18 @@ spec: duplicate slashes from request URL paths. \n Contour's default is false." type: boolean + httpMaxConcurrentStreams: + description: Defines the value for SETTINGS_MAX_CONCURRENT_STREAMS + Envoy will advertise in the SETTINGS frame in HTTP/2 + connections and the limit for concurrent streams allowed + for a peer on a single HTTP/2 connection. It is recommended + to not set this lower than 100 but this field can be + used to bound resource usage by HTTP/2 connections and + mitigate attacks like CVE-2023-44487. The default value + when this is not set is unlimited. + format: int32 + minimum: 1 + type: integer maxRequestsPerConnection: description: Defines the maximum requests for downstream connections. If not specified, there is no limit. see @@ -3634,6 +3669,18 @@ spec: format: int32 minimum: 1 type: integer + maxRequestsPerIOCycle: + description: Defines the limit on number of HTTP requests + that Envoy will process from a single connection in + a single I/O cycle. Requests over this limit are processed + in subsequent I/O cycles. Can be used as a mitigation + for CVE-2023-44487 when abusive traffic is detected. + Configures the http.max_requests_per_io_cycle Envoy + runtime setting. The default value when this is not + set is no limit. + format: int32 + minimum: 1 + type: integer per-connection-buffer-limit-bytes: description: Defines the soft limit on size of the listener’s new connection read and write buffers in bytes. If unspecified, diff --git a/examples/render/contour-deployment.yaml b/examples/render/contour-deployment.yaml index 9b529994350..f07bb8fa342 100644 --- a/examples/render/contour-deployment.yaml +++ b/examples/render/contour-deployment.yaml @@ -415,6 +415,18 @@ spec: slashes from request URL paths. \n Contour's default is false." type: boolean + httpMaxConcurrentStreams: + description: Defines the value for SETTINGS_MAX_CONCURRENT_STREAMS + Envoy will advertise in the SETTINGS frame in HTTP/2 connections + and the limit for concurrent streams allowed for a peer + on a single HTTP/2 connection. It is recommended to not + set this lower than 100 but this field can be used to bound + resource usage by HTTP/2 connections and mitigate attacks + like CVE-2023-44487. The default value when this is not + set is unlimited. + format: int32 + minimum: 1 + type: integer maxRequestsPerConnection: description: Defines the maximum requests for downstream connections. If not specified, there is no limit. see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-msg-config-core-v3-httpprotocoloptions @@ -422,6 +434,17 @@ spec: format: int32 minimum: 1 type: integer + maxRequestsPerIOCycle: + description: Defines the limit on number of HTTP requests + that Envoy will process from a single connection in a single + I/O cycle. Requests over this limit are processed in subsequent + I/O cycles. Can be used as a mitigation for CVE-2023-44487 + when abusive traffic is detected. Configures the http.max_requests_per_io_cycle + Envoy runtime setting. The default value when this is not + set is no limit. + format: int32 + minimum: 1 + type: integer per-connection-buffer-limit-bytes: description: Defines the soft limit on size of the listener’s new connection read and write buffers in bytes. If unspecified, @@ -3845,6 +3868,18 @@ spec: duplicate slashes from request URL paths. \n Contour's default is false." type: boolean + httpMaxConcurrentStreams: + description: Defines the value for SETTINGS_MAX_CONCURRENT_STREAMS + Envoy will advertise in the SETTINGS frame in HTTP/2 + connections and the limit for concurrent streams allowed + for a peer on a single HTTP/2 connection. It is recommended + to not set this lower than 100 but this field can be + used to bound resource usage by HTTP/2 connections and + mitigate attacks like CVE-2023-44487. The default value + when this is not set is unlimited. + format: int32 + minimum: 1 + type: integer maxRequestsPerConnection: description: Defines the maximum requests for downstream connections. If not specified, there is no limit. see @@ -3853,6 +3888,18 @@ spec: format: int32 minimum: 1 type: integer + maxRequestsPerIOCycle: + description: Defines the limit on number of HTTP requests + that Envoy will process from a single connection in + a single I/O cycle. Requests over this limit are processed + in subsequent I/O cycles. Can be used as a mitigation + for CVE-2023-44487 when abusive traffic is detected. + Configures the http.max_requests_per_io_cycle Envoy + runtime setting. The default value when this is not + set is no limit. + format: int32 + minimum: 1 + type: integer per-connection-buffer-limit-bytes: description: Defines the soft limit on size of the listener’s new connection read and write buffers in bytes. If unspecified, diff --git a/examples/render/contour-gateway-provisioner.yaml b/examples/render/contour-gateway-provisioner.yaml index 342b97f023b..203da65e01c 100644 --- a/examples/render/contour-gateway-provisioner.yaml +++ b/examples/render/contour-gateway-provisioner.yaml @@ -210,6 +210,18 @@ spec: slashes from request URL paths. \n Contour's default is false." type: boolean + httpMaxConcurrentStreams: + description: Defines the value for SETTINGS_MAX_CONCURRENT_STREAMS + Envoy will advertise in the SETTINGS frame in HTTP/2 connections + and the limit for concurrent streams allowed for a peer + on a single HTTP/2 connection. It is recommended to not + set this lower than 100 but this field can be used to bound + resource usage by HTTP/2 connections and mitigate attacks + like CVE-2023-44487. The default value when this is not + set is unlimited. + format: int32 + minimum: 1 + type: integer maxRequestsPerConnection: description: Defines the maximum requests for downstream connections. If not specified, there is no limit. see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-msg-config-core-v3-httpprotocoloptions @@ -217,6 +229,17 @@ spec: format: int32 minimum: 1 type: integer + maxRequestsPerIOCycle: + description: Defines the limit on number of HTTP requests + that Envoy will process from a single connection in a single + I/O cycle. Requests over this limit are processed in subsequent + I/O cycles. Can be used as a mitigation for CVE-2023-44487 + when abusive traffic is detected. Configures the http.max_requests_per_io_cycle + Envoy runtime setting. The default value when this is not + set is no limit. + format: int32 + minimum: 1 + type: integer per-connection-buffer-limit-bytes: description: Defines the soft limit on size of the listener’s new connection read and write buffers in bytes. If unspecified, @@ -3640,6 +3663,18 @@ spec: duplicate slashes from request URL paths. \n Contour's default is false." type: boolean + httpMaxConcurrentStreams: + description: Defines the value for SETTINGS_MAX_CONCURRENT_STREAMS + Envoy will advertise in the SETTINGS frame in HTTP/2 + connections and the limit for concurrent streams allowed + for a peer on a single HTTP/2 connection. It is recommended + to not set this lower than 100 but this field can be + used to bound resource usage by HTTP/2 connections and + mitigate attacks like CVE-2023-44487. The default value + when this is not set is unlimited. + format: int32 + minimum: 1 + type: integer maxRequestsPerConnection: description: Defines the maximum requests for downstream connections. If not specified, there is no limit. see @@ -3648,6 +3683,18 @@ spec: format: int32 minimum: 1 type: integer + maxRequestsPerIOCycle: + description: Defines the limit on number of HTTP requests + that Envoy will process from a single connection in + a single I/O cycle. Requests over this limit are processed + in subsequent I/O cycles. Can be used as a mitigation + for CVE-2023-44487 when abusive traffic is detected. + Configures the http.max_requests_per_io_cycle Envoy + runtime setting. The default value when this is not + set is no limit. + format: int32 + minimum: 1 + type: integer per-connection-buffer-limit-bytes: description: Defines the soft limit on size of the listener’s new connection read and write buffers in bytes. If unspecified, diff --git a/examples/render/contour-gateway.yaml b/examples/render/contour-gateway.yaml index cd987fa0b4a..b1bbeb6df5f 100644 --- a/examples/render/contour-gateway.yaml +++ b/examples/render/contour-gateway.yaml @@ -421,6 +421,18 @@ spec: slashes from request URL paths. \n Contour's default is false." type: boolean + httpMaxConcurrentStreams: + description: Defines the value for SETTINGS_MAX_CONCURRENT_STREAMS + Envoy will advertise in the SETTINGS frame in HTTP/2 connections + and the limit for concurrent streams allowed for a peer + on a single HTTP/2 connection. It is recommended to not + set this lower than 100 but this field can be used to bound + resource usage by HTTP/2 connections and mitigate attacks + like CVE-2023-44487. The default value when this is not + set is unlimited. + format: int32 + minimum: 1 + type: integer maxRequestsPerConnection: description: Defines the maximum requests for downstream connections. If not specified, there is no limit. see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-msg-config-core-v3-httpprotocoloptions @@ -428,6 +440,17 @@ spec: format: int32 minimum: 1 type: integer + maxRequestsPerIOCycle: + description: Defines the limit on number of HTTP requests + that Envoy will process from a single connection in a single + I/O cycle. Requests over this limit are processed in subsequent + I/O cycles. Can be used as a mitigation for CVE-2023-44487 + when abusive traffic is detected. Configures the http.max_requests_per_io_cycle + Envoy runtime setting. The default value when this is not + set is no limit. + format: int32 + minimum: 1 + type: integer per-connection-buffer-limit-bytes: description: Defines the soft limit on size of the listener’s new connection read and write buffers in bytes. If unspecified, @@ -3851,6 +3874,18 @@ spec: duplicate slashes from request URL paths. \n Contour's default is false." type: boolean + httpMaxConcurrentStreams: + description: Defines the value for SETTINGS_MAX_CONCURRENT_STREAMS + Envoy will advertise in the SETTINGS frame in HTTP/2 + connections and the limit for concurrent streams allowed + for a peer on a single HTTP/2 connection. It is recommended + to not set this lower than 100 but this field can be + used to bound resource usage by HTTP/2 connections and + mitigate attacks like CVE-2023-44487. The default value + when this is not set is unlimited. + format: int32 + minimum: 1 + type: integer maxRequestsPerConnection: description: Defines the maximum requests for downstream connections. If not specified, there is no limit. see @@ -3859,6 +3894,18 @@ spec: format: int32 minimum: 1 type: integer + maxRequestsPerIOCycle: + description: Defines the limit on number of HTTP requests + that Envoy will process from a single connection in + a single I/O cycle. Requests over this limit are processed + in subsequent I/O cycles. Can be used as a mitigation + for CVE-2023-44487 when abusive traffic is detected. + Configures the http.max_requests_per_io_cycle Envoy + runtime setting. The default value when this is not + set is no limit. + format: int32 + minimum: 1 + type: integer per-connection-buffer-limit-bytes: description: Defines the soft limit on size of the listener’s new connection read and write buffers in bytes. If unspecified, diff --git a/examples/render/contour.yaml b/examples/render/contour.yaml index 4eb1e802b11..8e2d63364e6 100644 --- a/examples/render/contour.yaml +++ b/examples/render/contour.yaml @@ -415,6 +415,18 @@ spec: slashes from request URL paths. \n Contour's default is false." type: boolean + httpMaxConcurrentStreams: + description: Defines the value for SETTINGS_MAX_CONCURRENT_STREAMS + Envoy will advertise in the SETTINGS frame in HTTP/2 connections + and the limit for concurrent streams allowed for a peer + on a single HTTP/2 connection. It is recommended to not + set this lower than 100 but this field can be used to bound + resource usage by HTTP/2 connections and mitigate attacks + like CVE-2023-44487. The default value when this is not + set is unlimited. + format: int32 + minimum: 1 + type: integer maxRequestsPerConnection: description: Defines the maximum requests for downstream connections. If not specified, there is no limit. see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto#envoy-v3-api-msg-config-core-v3-httpprotocoloptions @@ -422,6 +434,17 @@ spec: format: int32 minimum: 1 type: integer + maxRequestsPerIOCycle: + description: Defines the limit on number of HTTP requests + that Envoy will process from a single connection in a single + I/O cycle. Requests over this limit are processed in subsequent + I/O cycles. Can be used as a mitigation for CVE-2023-44487 + when abusive traffic is detected. Configures the http.max_requests_per_io_cycle + Envoy runtime setting. The default value when this is not + set is no limit. + format: int32 + minimum: 1 + type: integer per-connection-buffer-limit-bytes: description: Defines the soft limit on size of the listener’s new connection read and write buffers in bytes. If unspecified, @@ -3845,6 +3868,18 @@ spec: duplicate slashes from request URL paths. \n Contour's default is false." type: boolean + httpMaxConcurrentStreams: + description: Defines the value for SETTINGS_MAX_CONCURRENT_STREAMS + Envoy will advertise in the SETTINGS frame in HTTP/2 + connections and the limit for concurrent streams allowed + for a peer on a single HTTP/2 connection. It is recommended + to not set this lower than 100 but this field can be + used to bound resource usage by HTTP/2 connections and + mitigate attacks like CVE-2023-44487. The default value + when this is not set is unlimited. + format: int32 + minimum: 1 + type: integer maxRequestsPerConnection: description: Defines the maximum requests for downstream connections. If not specified, there is no limit. see @@ -3853,6 +3888,18 @@ spec: format: int32 minimum: 1 type: integer + maxRequestsPerIOCycle: + description: Defines the limit on number of HTTP requests + that Envoy will process from a single connection in + a single I/O cycle. Requests over this limit are processed + in subsequent I/O cycles. Can be used as a mitigation + for CVE-2023-44487 when abusive traffic is detected. + Configures the http.max_requests_per_io_cycle Envoy + runtime setting. The default value when this is not + set is no limit. + format: int32 + minimum: 1 + type: integer per-connection-buffer-limit-bytes: description: Defines the soft limit on size of the listener’s new connection read and write buffers in bytes. If unspecified, diff --git a/internal/contourconfig/contourconfiguration_test.go b/internal/contourconfig/contourconfiguration_test.go index 0d455d6e8d4..4106ea9a1d7 100644 --- a/internal/contourconfig/contourconfiguration_test.go +++ b/internal/contourconfig/contourconfiguration_test.go @@ -57,6 +57,7 @@ func TestOverlayOnDefaults(t *testing.T) { DisableAllowChunkedLength: ref.To(true), DisableMergeSlashes: ref.To(true), MaxRequestsPerConnection: ref.To(uint32(1)), + HTTP2MaxConcurrentStreams: ref.To(uint32(10)), ServerHeaderTransformation: contour_api_v1alpha1.PassThroughServerHeader, ConnectionBalancer: "yesplease", TLS: &contour_api_v1alpha1.EnvoyTLS{ diff --git a/internal/envoy/v3/listener.go b/internal/envoy/v3/listener.go index 6948cba9a41..5ac6e971598 100644 --- a/internal/envoy/v3/listener.go +++ b/internal/envoy/v3/listener.go @@ -172,6 +172,7 @@ type httpConnectionManagerBuilder struct { numTrustedHops uint32 tracingConfig *http.HttpConnectionManager_Tracing maxRequestsPerConnection *uint32 + http2MaxConcurrentStreams *uint32 enableWebsockets bool } @@ -284,6 +285,11 @@ func (b *httpConnectionManagerBuilder) MaxRequestsPerConnection(maxRequestsPerCo return b } +func (b *httpConnectionManagerBuilder) HTTP2MaxConcurrentStreams(http2MaxConcurrentStreams *uint32) *httpConnectionManagerBuilder { + b.http2MaxConcurrentStreams = http2MaxConcurrentStreams + return b +} + func (b *httpConnectionManagerBuilder) DefaultFilters() *httpConnectionManagerBuilder { // Add a default set of ordered http filters. @@ -538,6 +544,12 @@ func (b *httpConnectionManagerBuilder) Get() *envoy_listener_v3.Filter { cm.CommonHttpProtocolOptions.MaxRequestsPerConnection = wrapperspb.UInt32(*b.maxRequestsPerConnection) } + if b.http2MaxConcurrentStreams != nil { + cm.Http2ProtocolOptions = &envoy_core_v3.Http2ProtocolOptions{ + MaxConcurrentStreams: wrapperspb.UInt32(*b.http2MaxConcurrentStreams), + } + } + if b.enableWebsockets { cm.UpgradeConfigs = append(cm.UpgradeConfigs, &http.HttpConnectionManager_UpgradeConfig{ diff --git a/internal/envoy/v3/listener_test.go b/internal/envoy/v3/listener_test.go index 4a9d8046e56..12bccde3262 100644 --- a/internal/envoy/v3/listener_test.go +++ b/internal/envoy/v3/listener_test.go @@ -652,6 +652,7 @@ func TestHTTPConnectionManager(t *testing.T) { forwardClientCertificate *dag.ClientCertificateDetails xffNumTrustedHops uint32 maxRequestsPerConnection *uint32 + http2MaxConcurrentStreams *uint32 want *envoy_listener_v3.Filter }{ "default": { @@ -1396,6 +1397,56 @@ func TestHTTPConnectionManager(t *testing.T) { }, }, }, + "http2MaxConcurrentStreams set": { + routename: "default/kuard", + accesslogger: FileAccessLogEnvoy("/dev/stdout", "", nil, v1alpha1.LogLevelInfo), + http2MaxConcurrentStreams: ref.To(uint32(50)), + want: &envoy_listener_v3.Filter{ + Name: wellknown.HTTPConnectionManager, + ConfigType: &envoy_listener_v3.Filter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&http.HttpConnectionManager{ + StatPrefix: "default/kuard", + RouteSpecifier: &http.HttpConnectionManager_Rds{ + Rds: &http.Rds{ + RouteConfigName: "default/kuard", + ConfigSource: &envoy_core_v3.ConfigSource{ + ResourceApiVersion: envoy_core_v3.ApiVersion_V3, + ConfigSourceSpecifier: &envoy_core_v3.ConfigSource_ApiConfigSource{ + ApiConfigSource: &envoy_core_v3.ApiConfigSource{ + ApiType: envoy_core_v3.ApiConfigSource_GRPC, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + GrpcServices: []*envoy_core_v3.GrpcService{{ + TargetSpecifier: &envoy_core_v3.GrpcService_EnvoyGrpc_{ + EnvoyGrpc: &envoy_core_v3.GrpcService_EnvoyGrpc{ + ClusterName: "contour", + Authority: "contour", + }, + }, + }}, + }, + }, + }, + }, + }, + HttpFilters: defaultHTTPFilters, + HttpProtocolOptions: &envoy_core_v3.Http1ProtocolOptions{ + // Enable support for HTTP/1.0 requests that carry + // a Host: header. See #537. + AcceptHttp_10: true, + }, + CommonHttpProtocolOptions: &envoy_core_v3.HttpProtocolOptions{}, + Http2ProtocolOptions: &envoy_core_v3.Http2ProtocolOptions{ + MaxConcurrentStreams: wrapperspb.UInt32(50), + }, + AccessLog: FileAccessLogEnvoy("/dev/stdout", "", nil, v1alpha1.LogLevelInfo), + UseRemoteAddress: wrapperspb.Bool(true), + NormalizePath: wrapperspb.Bool(true), + PreserveExternalRequestId: true, + MergeSlashes: false, + }), + }, + }, + }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { @@ -1415,6 +1466,7 @@ func TestHTTPConnectionManager(t *testing.T) { NumTrustedHops(tc.xffNumTrustedHops). ForwardClientCertificate(tc.forwardClientCertificate). MaxRequestsPerConnection(tc.maxRequestsPerConnection). + HTTP2MaxConcurrentStreams(tc.http2MaxConcurrentStreams). DefaultFilters(). Get() diff --git a/internal/envoy/v3/runtime.go b/internal/envoy/v3/runtime.go index 8e2a121e06b..135eddab56b 100644 --- a/internal/envoy/v3/runtime.go +++ b/internal/envoy/v3/runtime.go @@ -24,11 +24,15 @@ const ( maxRegexProgramSizeWarn = 1000 ) -func RuntimeLayers() []*envoy_service_runtime_v3.Runtime { +func RuntimeLayers(configurableRuntimeFields map[string]*structpb.Value) []*envoy_service_runtime_v3.Runtime { + baseLayer := baseRuntimeLayer() + for k, v := range configurableRuntimeFields { + baseLayer.Fields[k] = v + } return []*envoy_service_runtime_v3.Runtime{ { Name: DynamicRuntimeLayerName, - Layer: baseRuntimeLayer(), + Layer: baseLayer, }, } } @@ -36,8 +40,8 @@ func RuntimeLayers() []*envoy_service_runtime_v3.Runtime { func baseRuntimeLayer() *structpb.Struct { return &structpb.Struct{ Fields: map[string]*structpb.Value{ - "re2.max_program_size.error_level": {Kind: &structpb.Value_NumberValue{NumberValue: maxRegexProgramSizeError}}, - "re2.max_program_size.warn_level": {Kind: &structpb.Value_NumberValue{NumberValue: maxRegexProgramSizeWarn}}, + "re2.max_program_size.error_level": structpb.NewNumberValue(maxRegexProgramSizeError), + "re2.max_program_size.warn_level": structpb.NewNumberValue(maxRegexProgramSizeWarn), }, } } diff --git a/internal/envoy/v3/runtime_test.go b/internal/envoy/v3/runtime_test.go index a0ca20d3cf0..9e84d136b65 100644 --- a/internal/envoy/v3/runtime_test.go +++ b/internal/envoy/v3/runtime_test.go @@ -22,15 +22,38 @@ import ( ) func TestRuntimeLayers(t *testing.T) { - require.Equal(t, []*envoy_service_runtime_v3.Runtime{ - { - Name: "dynamic", - Layer: &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "re2.max_program_size.error_level": {Kind: &structpb.Value_NumberValue{NumberValue: 1 << 20}}, - "re2.max_program_size.warn_level": {Kind: &structpb.Value_NumberValue{NumberValue: 1000}}, - }, + testCases := map[string]struct { + configurableFields map[string]*structpb.Value + }{ + "nil configurable fields": {}, + "empty configurable fields": { + configurableFields: map[string]*structpb.Value{}, + }, + "some configurable fields": { + configurableFields: map[string]*structpb.Value{ + "some.value1": structpb.NewBoolValue(true), + "some.value2": structpb.NewNumberValue(1000), }, }, - }, RuntimeLayers()) + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + expectedFields := map[string]*structpb.Value{ + "re2.max_program_size.error_level": structpb.NewNumberValue(1 << 20), + "re2.max_program_size.warn_level": structpb.NewNumberValue(1000), + } + for k, v := range tc.configurableFields { + expectedFields[k] = v + } + layers := RuntimeLayers(tc.configurableFields) + require.Equal(t, []*envoy_service_runtime_v3.Runtime{ + { + Name: "dynamic", + Layer: &structpb.Struct{ + Fields: expectedFields, + }, + }, + }, layers) + }) + } } diff --git a/internal/xdscache/v3/listener.go b/internal/xdscache/v3/listener.go index c9cf6d04aaf..01e99745689 100644 --- a/internal/xdscache/v3/listener.go +++ b/internal/xdscache/v3/listener.go @@ -129,6 +129,8 @@ type ListenerConfig struct { // if not specified there is no limit set. MaxRequestsPerConnection *uint32 + HTTP2MaxConcurrentStreams *uint32 + // PerConnectionBufferLimitBytes defines the soft limit on size of the listener’s new connection read and write buffers // If unspecified, an implementation defined default is applied (1MiB). PerConnectionBufferLimitBytes *uint32 @@ -420,6 +422,7 @@ func (c *ListenerCache) OnChange(root *dag.DAG) { ServerHeaderTransformation(cfg.ServerHeaderTransformation). NumTrustedHops(cfg.XffNumTrustedHops). MaxRequestsPerConnection(cfg.MaxRequestsPerConnection). + HTTP2MaxConcurrentStreams(cfg.HTTP2MaxConcurrentStreams). AddFilter(httpGlobalExternalAuthConfig(cfg.GlobalExternalAuthConfig)). Tracing(envoy_v3.TracingConfig(envoyTracingConfig(cfg.TracingConfig))). AddFilter(envoy_v3.GlobalRateLimitFilter(envoyGlobalRateLimitConfig(cfg.RateLimitConfig))). @@ -497,6 +500,7 @@ func (c *ListenerCache) OnChange(root *dag.DAG) { AddFilter(envoy_v3.GlobalRateLimitFilter(envoyGlobalRateLimitConfig(cfg.RateLimitConfig))). ForwardClientCertificate(forwardClientCertificate). MaxRequestsPerConnection(cfg.MaxRequestsPerConnection). + HTTP2MaxConcurrentStreams(cfg.HTTP2MaxConcurrentStreams). EnableWebsockets(listener.EnableWebsockets). Get() @@ -571,6 +575,7 @@ func (c *ListenerCache) OnChange(root *dag.DAG) { AddFilter(envoy_v3.GlobalRateLimitFilter(envoyGlobalRateLimitConfig(cfg.RateLimitConfig))). ForwardClientCertificate(forwardClientCertificate). MaxRequestsPerConnection(cfg.MaxRequestsPerConnection). + HTTP2MaxConcurrentStreams(cfg.HTTP2MaxConcurrentStreams). EnableWebsockets(listener.EnableWebsockets). Get() diff --git a/internal/xdscache/v3/listener_test.go b/internal/xdscache/v3/listener_test.go index 186215e39c7..0c64fc3030f 100644 --- a/internal/xdscache/v3/listener_test.go +++ b/internal/xdscache/v3/listener_test.go @@ -3684,6 +3684,142 @@ func TestListenerVisit(t *testing.T) { SocketOptions: envoy_v3.NewSocketOptions().TCPKeepalive().Build(), }), }, + "httpproxy with HTTP2MaxConcurrentStreams set in listener config": { + ListenerConfig: ListenerConfig{ + HTTP2MaxConcurrentStreams: ref.To(uint32(100)), + }, + objs: []any{ + &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple", + Namespace: "default", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "www.example.com", + }, + Routes: []contour_api_v1.Route{{ + Conditions: []contour_api_v1.MatchCondition{{ + Prefix: "/", + }}, + Services: []contour_api_v1.Service{{ + Name: "backend", + Port: 80, + }}, + }}, + }, + }, + &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backend", + Namespace: "default", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{{ + Name: "http", + Protocol: "TCP", + Port: 80, + }}, + }, + }, + }, + want: listenermap(&envoy_listener_v3.Listener{ + Name: ENVOY_HTTP_LISTENER, + Address: envoy_v3.SocketAddress("0.0.0.0", 8080), + FilterChains: envoy_v3.FilterChains( + envoy_v3.HTTPConnectionManagerBuilder(). + RouteConfigName(ENVOY_HTTP_LISTENER). + MetricsPrefix(ENVOY_HTTP_LISTENER). + AccessLoggers(envoy_v3.FileAccessLogEnvoy(DEFAULT_HTTP_ACCESS_LOG, "", nil, v1alpha1.LogLevelInfo)). + DefaultFilters(). + HTTP2MaxConcurrentStreams(ref.To(uint32(100))). + Get(), + ), + SocketOptions: envoy_v3.NewSocketOptions().TCPKeepalive().Build(), + }), + }, + "httpsproxy with HTTP2MaxConcurrentStreams set in listener config": { + ListenerConfig: ListenerConfig{ + HTTP2MaxConcurrentStreams: ref.To(uint32(101)), + }, + objs: []any{ + &contour_api_v1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple", + Namespace: "default", + }, + Spec: contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "www.example.com", + TLS: &contour_api_v1.TLS{ + SecretName: "secret", + }, + }, + Routes: []contour_api_v1.Route{{ + Services: []contour_api_v1.Service{{ + Name: "backend", + Port: 80, + }}, + }}, + }, + }, + &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "default", + }, + Type: "kubernetes.io/tls", + Data: secretdata(CERTIFICATE, RSA_PRIVATE_KEY), + }, + &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backend", + Namespace: "default", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{{ + Name: "http", + Protocol: "TCP", + Port: 80, + }}, + }, + }, + }, + want: listenermap(&envoy_listener_v3.Listener{ + Name: ENVOY_HTTP_LISTENER, + Address: envoy_v3.SocketAddress("0.0.0.0", 8080), + FilterChains: envoy_v3.FilterChains(envoy_v3.HTTPConnectionManagerBuilder(). + RouteConfigName(ENVOY_HTTP_LISTENER). + MetricsPrefix(ENVOY_HTTP_LISTENER). + AccessLoggers(envoy_v3.FileAccessLogEnvoy(DEFAULT_HTTP_ACCESS_LOG, "", nil, v1alpha1.LogLevelInfo)). + DefaultFilters(). + HTTP2MaxConcurrentStreams(ref.To(uint32(101))). + Get(), + ), + SocketOptions: envoy_v3.NewSocketOptions().TCPKeepalive().Build(), + }, &envoy_listener_v3.Listener{ + Name: ENVOY_HTTPS_LISTENER, + Address: envoy_v3.SocketAddress("0.0.0.0", 8443), + FilterChains: []*envoy_listener_v3.FilterChain{{ + FilterChainMatch: &envoy_listener_v3.FilterChainMatch{ + ServerNames: []string{"www.example.com"}, + }, + TransportSocket: transportSocket("secret", envoy_tls_v3.TlsParameters_TLSv1_2, envoy_tls_v3.TlsParameters_TLSv1_3, nil, "h2", "http/1.1"), + Filters: envoy_v3.Filters(envoy_v3.HTTPConnectionManagerBuilder(). + AddFilter(envoy_v3.FilterMisdirectedRequests("www.example.com")). + DefaultFilters(). + MetricsPrefix(ENVOY_HTTPS_LISTENER). + RouteConfigName(path.Join("https", "www.example.com")). + AccessLoggers(envoy_v3.FileAccessLogEnvoy(DEFAULT_HTTP_ACCESS_LOG, "", nil, v1alpha1.LogLevelInfo)). + HTTP2MaxConcurrentStreams(ref.To(uint32(101))). + Get()), + }}, + ListenerFilters: envoy_v3.ListenerFilters( + envoy_v3.TLSInspector(), + ), + SocketOptions: envoy_v3.NewSocketOptions().TCPKeepalive().Build(), + }), + }, "httpproxy with PerConnectionBufferLimitBytes set in listener config": { ListenerConfig: ListenerConfig{ PerConnectionBufferLimitBytes: ref.To(uint32(32768)), diff --git a/internal/xdscache/v3/runtime.go b/internal/xdscache/v3/runtime.go index a163350804f..b4124bfa217 100644 --- a/internal/xdscache/v3/runtime.go +++ b/internal/xdscache/v3/runtime.go @@ -20,30 +20,46 @@ import ( envoy_v3 "github.com/projectcontour/contour/internal/envoy/v3" "github.com/projectcontour/contour/internal/protobuf" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" ) +type ConfigurableRuntimeSettings struct { + MaxRequestsPerIOCycle *uint32 +} + // RuntimeCache manages the contents of the gRPC RTDS cache. -type RuntimeCache struct { +type runtimeCache struct { contour.Cond + runtimeKV map[string]*structpb.Value +} + +// NewRuntimeCache builds a RuntimeCache with the provided runtime +// settings that will be set in the runtime layer configured by Contour. +func NewRuntimeCache(runtimeSettings ConfigurableRuntimeSettings) *runtimeCache { + runtimeKV := make(map[string]*structpb.Value) + if runtimeSettings.MaxRequestsPerIOCycle != nil && *runtimeSettings.MaxRequestsPerIOCycle > 0 { + runtimeKV["http.max_requests_per_io_cycle"] = structpb.NewNumberValue(float64(*runtimeSettings.MaxRequestsPerIOCycle)) + } + return &runtimeCache{runtimeKV: runtimeKV} } // Contents returns all Runtime layers. -func (c *RuntimeCache) Contents() []proto.Message { - return protobuf.AsMessages(envoy_v3.RuntimeLayers()) +func (c *runtimeCache) Contents() []proto.Message { + return protobuf.AsMessages(envoy_v3.RuntimeLayers(c.runtimeKV)) } // Query returns only the "dynamic" layer if requested, otherwise empty. -func (c *RuntimeCache) Query(names []string) []proto.Message { +func (c *runtimeCache) Query(names []string) []proto.Message { for _, name := range names { if name == envoy_v3.DynamicRuntimeLayerName { - return protobuf.AsMessages(envoy_v3.RuntimeLayers()) + return protobuf.AsMessages(envoy_v3.RuntimeLayers(c.runtimeKV)) } } return []proto.Message{} } -func (*RuntimeCache) TypeURL() string { return resource.RuntimeType } +func (*runtimeCache) TypeURL() string { return resource.RuntimeType } -func (c *RuntimeCache) OnChange(root *dag.DAG) { +func (c *runtimeCache) OnChange(root *dag.DAG) { // DAG changes do not affect runtime layers at the moment. } diff --git a/internal/xdscache/v3/runtime_test.go b/internal/xdscache/v3/runtime_test.go index 71793f0c40d..b7aa25815d2 100644 --- a/internal/xdscache/v3/runtime_test.go +++ b/internal/xdscache/v3/runtime_test.go @@ -18,16 +18,72 @@ import ( envoy_service_runtime_v3 "github.com/envoyproxy/go-control-plane/envoy/service/runtime/v3" "github.com/projectcontour/contour/internal/protobuf" + "github.com/projectcontour/contour/internal/ref" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" ) func TestRuntimeCacheContents(t *testing.T) { - rc := &RuntimeCache{} - protobuf.ExpectEqual(t, runtimeLayers(), rc.Contents()) + testCases := map[string]struct { + runtimeSettings ConfigurableRuntimeSettings + additionalFields map[string]*structpb.Value + }{ + "no values set": { + runtimeSettings: ConfigurableRuntimeSettings{}, + }, + "http max requests per io cycle set": { + runtimeSettings: ConfigurableRuntimeSettings{ + MaxRequestsPerIOCycle: ref.To(uint32(1)), + }, + additionalFields: map[string]*structpb.Value{ + "http.max_requests_per_io_cycle": structpb.NewNumberValue(1), + }, + }, + "http max requests per io cycle set invalid": { + runtimeSettings: ConfigurableRuntimeSettings{ + MaxRequestsPerIOCycle: ref.To(uint32(0)), + }, + }, + "http max requests per io cycle set nil": { + runtimeSettings: ConfigurableRuntimeSettings{ + MaxRequestsPerIOCycle: nil, + }, + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + rc := NewRuntimeCache(tc.runtimeSettings) + fields := map[string]*structpb.Value{ + "re2.max_program_size.error_level": structpb.NewNumberValue(1 << 20), + "re2.max_program_size.warn_level": structpb.NewNumberValue(1000), + } + for k, v := range tc.additionalFields { + fields[k] = v + } + protobuf.ExpectEqual(t, []proto.Message{ + &envoy_service_runtime_v3.Runtime{ + Name: "dynamic", + Layer: &structpb.Struct{ + Fields: fields, + }, + }, + }, rc.Contents()) + }) + } } func TestRuntimeCacheQuery(t *testing.T) { + baseRuntimeLayers := []proto.Message{ + &envoy_service_runtime_v3.Runtime{ + Name: "dynamic", + Layer: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "re2.max_program_size.error_level": structpb.NewNumberValue(1 << 20), + "re2.max_program_size.warn_level": structpb.NewNumberValue(1000), + }, + }, + }, + } testCases := map[string]struct { names []string expected []proto.Message @@ -38,7 +94,7 @@ func TestRuntimeCacheQuery(t *testing.T) { }, "names include dynamic": { names: []string{"foo", "dynamic", "bar"}, - expected: runtimeLayers(), + expected: baseRuntimeLayers, }, "names excludes dynamic": { names: []string{"foo", "bar", "baz"}, @@ -47,22 +103,8 @@ func TestRuntimeCacheQuery(t *testing.T) { } for name, tc := range testCases { t.Run(name, func(t *testing.T) { - rc := &RuntimeCache{} + rc := NewRuntimeCache(ConfigurableRuntimeSettings{}) protobuf.ExpectEqual(t, tc.expected, rc.Query(tc.names)) }) } } - -func runtimeLayers() []proto.Message { - return []proto.Message{ - &envoy_service_runtime_v3.Runtime{ - Name: "dynamic", - Layer: &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "re2.max_program_size.error_level": {Kind: &structpb.Value_NumberValue{NumberValue: 1 << 20}}, - "re2.max_program_size.warn_level": {Kind: &structpb.Value_NumberValue{NumberValue: 1000}}, - }, - }, - }, - } -} diff --git a/internal/xdscache/v3/server_test.go b/internal/xdscache/v3/server_test.go index 54d845e06f0..ee1451b931e 100644 --- a/internal/xdscache/v3/server_test.go +++ b/internal/xdscache/v3/server_test.go @@ -207,7 +207,7 @@ func TestGRPC(t *testing.T) { &RouteCache{}, &ClusterCache{}, et, - &RuntimeCache{}, + NewRuntimeCache(ConfigurableRuntimeSettings{}), } eh = contour.NewEventHandler(contour.EventHandlerConfig{ diff --git a/pkg/config/parameters.go b/pkg/config/parameters.go index 462361d226c..9046c6485b9 100644 --- a/pkg/config/parameters.go +++ b/pkg/config/parameters.go @@ -484,6 +484,21 @@ type ListenerParameters struct { // SocketOptions is used to set socket options for listeners. SocketOptions SocketOptions `yaml:"socket-options"` + + // Defines the limit on number of HTTP requests that Envoy will process from a single + // connection in a single I/O cycle. Requests over this limit are processed in subsequent + // I/O cycles. Can be used as a mitigation for CVE-2023-44487 when abusive traffic is + // detected. Configures the http.max_requests_per_io_cycle Envoy runtime setting. The default + // value when this is not set is no limit. + MaxRequestsPerIOCycle *uint32 `yaml:"max-requests-per-io-cycle,omitempty"` + + // Defines the value for SETTINGS_MAX_CONCURRENT_STREAMS Envoy will advertise in the + // SETTINGS frame in HTTP/2 connections and the limit for concurrent streams allowed + // for a peer on a single HTTP/2 connection. It is recommended to not set this lower + // than 100 but this field can be used to bound resource usage by HTTP/2 connections + // and mitigate attacks like CVE-2023-44487. The default value when this is not set is + // unlimited. + HTTP2MaxConcurrentStreams *uint32 `yaml:"http2-max-concurrent-streams,omitempty"` } func (p *ListenerParameters) Validate() error { @@ -503,6 +518,14 @@ func (p *ListenerParameters) Validate() error { return fmt.Errorf("invalid per connections buffer limit bytes value %q set on listener, minimum value is 1", *p.PerConnectionBufferLimitBytes) } + if p.MaxRequestsPerIOCycle != nil && *p.MaxRequestsPerIOCycle < 1 { + return fmt.Errorf("invalid max connections per IO cycle value %q set on listener, minimum value is 1", *p.MaxRequestsPerIOCycle) + } + + if p.HTTP2MaxConcurrentStreams != nil && *p.HTTP2MaxConcurrentStreams < 1 { + return fmt.Errorf("invalid max HTTP/2 concurrent streams value %q set on listener, minimum value is 1", *p.HTTP2MaxConcurrentStreams) + } + return p.SocketOptions.Validate() } diff --git a/pkg/config/parameters_test.go b/pkg/config/parameters_test.go index bcd46957a99..31467990952 100644 --- a/pkg/config/parameters_test.go +++ b/pkg/config/parameters_test.go @@ -461,6 +461,13 @@ listener: max-requests-per-connection: 1 `) + check(func(t *testing.T, conf *Parameters) { + assert.Equal(t, ref.To(uint32(10)), conf.Listener.HTTP2MaxConcurrentStreams) + }, ` +listener: + http2-max-concurrent-streams: 10 +`) + check(func(t *testing.T, conf *Parameters) { assert.Equal(t, ref.To(uint32(1)), conf.Listener.PerConnectionBufferLimitBytes) }, ` @@ -468,6 +475,13 @@ listener: per-connection-buffer-limit-bytes: 1 `) + check(func(t *testing.T, conf *Parameters) { + assert.Equal(t, ref.To(uint32(1)), conf.Listener.MaxRequestsPerIOCycle) + }, ` +listener: + max-requests-per-io-cycle: 1 +`) + check(func(t *testing.T, conf *Parameters) { assert.Equal(t, ref.To(uint32(1)), conf.Cluster.MaxRequestsPerConnection) }, ` @@ -564,6 +578,22 @@ func TestListenerValidation(t *testing.T) { PerConnectionBufferLimitBytes: ref.To(uint32(0)), } require.Error(t, l.Validate()) + l = &ListenerParameters{ + MaxRequestsPerIOCycle: ref.To(uint32(1)), + } + require.NoError(t, l.Validate()) + l = &ListenerParameters{ + MaxRequestsPerIOCycle: ref.To(uint32(0)), + } + require.Error(t, l.Validate()) + l = &ListenerParameters{ + HTTP2MaxConcurrentStreams: ref.To(uint32(1)), + } + require.NoError(t, l.Validate()) + l = &ListenerParameters{ + HTTP2MaxConcurrentStreams: ref.To(uint32(0)), + } + require.Error(t, l.Validate()) l = &ListenerParameters{ SocketOptions: SocketOptions{ TOS: 64, diff --git a/site/content/docs/main/config/api-reference.html b/site/content/docs/main/config/api-reference.html index 172f69733e9..0fe36d4b559 100644 --- a/site/content/docs/main/config/api-reference.html +++ b/site/content/docs/main/config/api-reference.html @@ -6776,6 +6776,41 @@
maxRequestsPerIOCycle
+Defines the limit on number of HTTP requests that Envoy will process from a single +connection in a single I/O cycle. Requests over this limit are processed in subsequent +I/O cycles. Can be used as a mitigation for CVE-2023-44487 when abusive traffic is +detected. Configures the http.max_requests_per_io_cycle Envoy runtime setting. The default +value when this is not set is no limit.
+httpMaxConcurrentStreams
+Defines the value for SETTINGS_MAX_CONCURRENT_STREAMS Envoy will advertise in the +SETTINGS frame in HTTP/2 connections and the limit for concurrent streams allowed +for a peer on a single HTTP/2 connection. It is recommended to not set this lower +than 100 but this field can be used to bound resource usage by HTTP/2 connections +and mitigate attacks like CVE-2023-44487. The default value when this is not set is +unlimited.
+