diff --git a/apis/projectcontour/v1alpha1/contourconfig.go b/apis/projectcontour/v1alpha1/contourconfig.go index e057ea9fcdb..e4395346210 100644 --- a/apis/projectcontour/v1alpha1/contourconfig.go +++ b/apis/projectcontour/v1alpha1/contourconfig.go @@ -378,7 +378,18 @@ type EnvoyListenerConfig struct { // // +kubebuilder:validation:Minimum=1 // +optional - MaxRequestsPerIOCycle *uint32 `json:"maxRequestsPerIOCycle"` + 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"` } // EnvoyTLS describes tls parameters for Envoy listneners. diff --git a/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go b/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go index 8b331a576d6..a118813dd13 100644 --- a/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go +++ b/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go @@ -556,6 +556,11 @@ func (in *EnvoyListenerConfig) DeepCopyInto(out *EnvoyListenerConfig) { *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/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 c69103f6e1e..e06fb61ede2 100644 --- a/cmd/contour/serve.go +++ b/cmd/contour/serve.go @@ -390,6 +390,7 @@ func (s *Server) doServe() error { ServerHeaderTransformation: contourConfiguration.Envoy.Listener.ServerHeaderTransformation, XffNumTrustedHops: *contourConfiguration.Envoy.Network.XffNumTrustedHops, ConnectionBalancer: contourConfiguration.Envoy.Listener.ConnectionBalancer, + HTTP2MaxConcurrentStreams: contourConfiguration.Envoy.Listener.HTTP2MaxConcurrentStreams, } if listenerConfig.TracingConfig, err = s.setupTracingService(contourConfiguration.Tracing); err != nil { diff --git a/cmd/contour/servecontext.go b/cmd/contour/servecontext.go index 080b548151f..08edae46598 100644 --- a/cmd/contour/servecontext.go +++ b/cmd/contour/servecontext.go @@ -510,6 +510,7 @@ func (ctx *serveContext) convertToContourConfigurationSpec() contour_api_v1alpha ServerHeaderTransformation: serverHeaderTransformation, ConnectionBalancer: ctx.Config.Listener.ConnectionBalancer, MaxRequestsPerIOCycle: ctx.Config.Listener.MaxRequestsPerIOCycle, + HTTP2MaxConcurrentStreams: ctx.Config.Listener.HTTP2MaxConcurrentStreams, TLS: &contour_api_v1alpha1.EnvoyTLS{ MinimumProtocolVersion: ctx.Config.TLS.MinimumProtocolVersion, CipherSuites: cipherSuites, diff --git a/cmd/contour/servecontext_test.go b/cmd/contour/servecontext_test.go index 1359305a76f..5a6bcc02c5c 100644 --- a/cmd/contour/servecontext_test.go +++ b/cmd/contour/servecontext_test.go @@ -804,10 +804,12 @@ func TestConvertServeContext(t *testing.T) { "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 }, }, diff --git a/examples/contour/01-crds.yaml b/examples/contour/01-crds.yaml index 606e2a41e18..6002c90e55d 100644 --- a/examples/contour/01-crds.yaml +++ b/examples/contour/01-crds.yaml @@ -180,6 +180,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 maxRequestsPerIOCycle: description: Defines the limit on number of HTTP requests that Envoy will process from a single connection in a single @@ -3352,6 +3364,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 maxRequestsPerIOCycle: description: Defines the limit on number of HTTP requests that Envoy will process from a single connection in diff --git a/examples/render/contour-deployment.yaml b/examples/render/contour-deployment.yaml index de13ff68670..107ec64fad1 100644 --- a/examples/render/contour-deployment.yaml +++ b/examples/render/contour-deployment.yaml @@ -393,6 +393,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 maxRequestsPerIOCycle: description: Defines the limit on number of HTTP requests that Envoy will process from a single connection in a single @@ -3565,6 +3577,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 maxRequestsPerIOCycle: description: Defines the limit on number of HTTP requests that Envoy will process from a single connection in diff --git a/examples/render/contour-gateway-provisioner.yaml b/examples/render/contour-gateway-provisioner.yaml index 3f236144e9d..ab33d4168e0 100644 --- a/examples/render/contour-gateway-provisioner.yaml +++ b/examples/render/contour-gateway-provisioner.yaml @@ -194,6 +194,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 maxRequestsPerIOCycle: description: Defines the limit on number of HTTP requests that Envoy will process from a single connection in a single @@ -3366,6 +3378,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 maxRequestsPerIOCycle: description: Defines the limit on number of HTTP requests that Envoy will process from a single connection in diff --git a/examples/render/contour-gateway.yaml b/examples/render/contour-gateway.yaml index c2d9287f1ac..e81b82c48e8 100644 --- a/examples/render/contour-gateway.yaml +++ b/examples/render/contour-gateway.yaml @@ -399,6 +399,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 maxRequestsPerIOCycle: description: Defines the limit on number of HTTP requests that Envoy will process from a single connection in a single @@ -3571,6 +3583,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 maxRequestsPerIOCycle: description: Defines the limit on number of HTTP requests that Envoy will process from a single connection in diff --git a/examples/render/contour.yaml b/examples/render/contour.yaml index 20e489f0aa3..c371650979f 100644 --- a/examples/render/contour.yaml +++ b/examples/render/contour.yaml @@ -393,6 +393,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 maxRequestsPerIOCycle: description: Defines the limit on number of HTTP requests that Envoy will process from a single connection in a single @@ -3565,6 +3577,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 maxRequestsPerIOCycle: description: Defines the limit on number of HTTP requests that Envoy will process from a single connection in diff --git a/internal/contourconfig/contourconfiguration_test.go b/internal/contourconfig/contourconfiguration_test.go index 2581dd58e03..6ac0b535641 100644 --- a/internal/contourconfig/contourconfiguration_test.go +++ b/internal/contourconfig/contourconfiguration_test.go @@ -55,6 +55,7 @@ func TestOverlayOnDefaults(t *testing.T) { UseProxyProto: ref.To(true), DisableAllowChunkedLength: ref.To(true), DisableMergeSlashes: ref.To(true), + 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 84906e5664e..fc0ae562a31 100644 --- a/internal/envoy/v3/listener.go +++ b/internal/envoy/v3/listener.go @@ -166,6 +166,7 @@ type httpConnectionManagerBuilder struct { forwardClientCertificate *dag.ClientCertificateDetails numTrustedHops uint32 tracingConfig *http.HttpConnectionManager_Tracing + http2MaxConcurrentStreams *uint32 } // RouteConfigName sets the name of the RDS element that contains @@ -266,6 +267,11 @@ func (b *httpConnectionManagerBuilder) NumTrustedHops(num uint32) *httpConnectio 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. @@ -523,6 +529,12 @@ func (b *httpConnectionManagerBuilder) Get() *envoy_listener_v3.Filter { } } + if b.http2MaxConcurrentStreams != nil { + cm.Http2ProtocolOptions = &envoy_core_v3.Http2ProtocolOptions{ + MaxConcurrentStreams: wrapperspb.UInt32(*b.http2MaxConcurrentStreams), + } + } + return &envoy_listener_v3.Filter{ Name: wellknown.HTTPConnectionManager, ConfigType: &envoy_listener_v3.Filter_TypedConfig{ diff --git a/internal/envoy/v3/listener_test.go b/internal/envoy/v3/listener_test.go index a38d70b9d5c..54a155ee1c8 100644 --- a/internal/envoy/v3/listener_test.go +++ b/internal/envoy/v3/listener_test.go @@ -39,6 +39,7 @@ import ( "github.com/projectcontour/contour/internal/dag" "github.com/projectcontour/contour/internal/envoy" "github.com/projectcontour/contour/internal/protobuf" + "github.com/projectcontour/contour/internal/ref" "github.com/projectcontour/contour/internal/timeout" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -613,6 +614,7 @@ func TestHTTPConnectionManager(t *testing.T) { serverHeaderTranformation v1alpha1.ServerHeaderTransformationType forwardClientCertificate *dag.ClientCertificateDetails xffNumTrustedHops uint32 + http2MaxConcurrentStreams *uint32 want *envoy_listener_v3.Filter }{ "default": { @@ -1350,6 +1352,59 @@ 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), + StripPortMode: &http.HttpConnectionManager_StripAnyHostPort{ + StripAnyHostPort: true, + }, + PreserveExternalRequestId: true, + MergeSlashes: false, + }), + }, + }, + }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { @@ -1368,6 +1423,7 @@ func TestHTTPConnectionManager(t *testing.T) { ServerHeaderTransformation(tc.serverHeaderTranformation). NumTrustedHops(tc.xffNumTrustedHops). ForwardClientCertificate(tc.forwardClientCertificate). + HTTP2MaxConcurrentStreams(tc.http2MaxConcurrentStreams). DefaultFilters(). Get() diff --git a/internal/xdscache/v3/listener.go b/internal/xdscache/v3/listener.go index cc986830120..ffc88d720a4 100644 --- a/internal/xdscache/v3/listener.go +++ b/internal/xdscache/v3/listener.go @@ -121,6 +121,9 @@ type ListenerConfig struct { // If no configuration is specified, Envoy will not attempt to balance active connections between worker threads // If specified, the listener will use the exact connection balancer. ConnectionBalancer string + + HTTP2MaxConcurrentStreams *uint32 + // RateLimitConfig optionally configures the global Rate Limit Service to be // used. RateLimitConfig *RateLimitConfig @@ -367,6 +370,7 @@ func (c *ListenerCache) OnChange(root *dag.DAG) { MergeSlashes(cfg.MergeSlashes). ServerHeaderTransformation(cfg.ServerHeaderTransformation). NumTrustedHops(cfg.XffNumTrustedHops). + HTTP2MaxConcurrentStreams(cfg.HTTP2MaxConcurrentStreams). Tracing(envoy_v3.TracingConfig(envoyTracingConfig(cfg.TracingConfig))). AddFilter(envoy_v3.GlobalRateLimitFilter(envoyGlobalRateLimitConfig(cfg.RateLimitConfig))). AddFilter(httpGlobalExternalAuthConfig(cfg.GlobalExternalAuthConfig)). @@ -438,6 +442,7 @@ func (c *ListenerCache) OnChange(root *dag.DAG) { Tracing(envoy_v3.TracingConfig(envoyTracingConfig(cfg.TracingConfig))). AddFilter(envoy_v3.GlobalRateLimitFilter(envoyGlobalRateLimitConfig(cfg.RateLimitConfig))). ForwardClientCertificate(forwardClientCertificate). + HTTP2MaxConcurrentStreams(cfg.HTTP2MaxConcurrentStreams). Get() filters = envoy_v3.Filters(cm) @@ -502,6 +507,7 @@ func (c *ListenerCache) OnChange(root *dag.DAG) { Tracing(envoy_v3.TracingConfig(envoyTracingConfig(cfg.TracingConfig))). AddFilter(envoy_v3.GlobalRateLimitFilter(envoyGlobalRateLimitConfig(cfg.RateLimitConfig))). ForwardClientCertificate(forwardClientCertificate). + HTTP2MaxConcurrentStreams(cfg.HTTP2MaxConcurrentStreams). Get() // Default filter chain diff --git a/internal/xdscache/v3/listener_test.go b/internal/xdscache/v3/listener_test.go index ded08b22a41..1d2575686e2 100644 --- a/internal/xdscache/v3/listener_test.go +++ b/internal/xdscache/v3/listener_test.go @@ -32,6 +32,7 @@ import ( envoy_v3 "github.com/projectcontour/contour/internal/envoy/v3" "github.com/projectcontour/contour/internal/k8s" "github.com/projectcontour/contour/internal/protobuf" + "github.com/projectcontour/contour/internal/ref" "github.com/projectcontour/contour/internal/timeout" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" @@ -3395,6 +3396,142 @@ func TestListenerVisit(t *testing.T) { SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), }), }, + "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.TCPKeepaliveSocketOptions(), + }), + }, + "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.TCPKeepaliveSocketOptions(), + }, &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, 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.TCPKeepaliveSocketOptions(), + }), + }, } for name, tc := range tests { diff --git a/pkg/config/parameters.go b/pkg/config/parameters.go index 2c852916ae2..5faefc4693f 100644 --- a/pkg/config/parameters.go +++ b/pkg/config/parameters.go @@ -442,7 +442,15 @@ type ListenerParameters struct { // 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"` + 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 { @@ -458,6 +466,10 @@ func (p *ListenerParameters) Validate() error { 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 nil } diff --git a/pkg/config/parameters_test.go b/pkg/config/parameters_test.go index 24aec842390..7536b2dcc1b 100644 --- a/pkg/config/parameters_test.go +++ b/pkg/config/parameters_test.go @@ -419,6 +419,13 @@ network: admin-port: 9001 `) + 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.MaxRequestsPerIOCycle) }, ` @@ -507,6 +514,14 @@ func TestListenerValidation(t *testing.T) { 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()) } func TestTracingConfigValidation(t *testing.T) { diff --git a/site/content/docs/main/config/api-reference.html b/site/content/docs/main/config/api-reference.html index 9d5e08d977b..baa46a026f0 100644 --- a/site/content/docs/main/config/api-reference.html +++ b/site/content/docs/main/config/api-reference.html @@ -6532,6 +6532,24 @@

EnvoyListenerConfig value when this is not set is no limit.

+ + +httpMaxConcurrentStreams +
+ +uint32 + + + +(Optional) +

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.

+ +

EnvoyLogging diff --git a/site/content/docs/main/configuration.md b/site/content/docs/main/configuration.md index f3d1795e538..c3794e39a37 100644 --- a/site/content/docs/main/configuration.md +++ b/site/content/docs/main/configuration.md @@ -187,6 +187,7 @@ The listener configuration block can be used to configure various parameters for | ------------------- | ------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | connection-balancer | string | `""` | This field specifies the listener connection balancer. If the value is `exact`, the listener will use the exact connection balancer to balance connections between threads in a single Envoy process. See [the Envoy documentation][14] for more information. | | max-requests-per-io-cycle | int | none | 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. | +| http2-max-concurrent-streams | int | none | 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. | _This is Envoy's default setting value and is not explicitly configured by Contour._