diff --git a/cmd/contour/serve.go b/cmd/contour/serve.go index fa52a3d19f1..79d876858cf 100644 --- a/cmd/contour/serve.go +++ b/cmd/contour/serve.go @@ -350,10 +350,11 @@ func doServe(log logrus.FieldLogger, ctx *serveContext) error { } listenerConfig.RateLimitConfig = &xdscache_v3.RateLimitConfig{ - ExtensionService: namespacedName, - Domain: ctx.Config.RateLimitService.Domain, - Timeout: responseTimeout, - FailOpen: ctx.Config.RateLimitService.FailOpen, + ExtensionService: namespacedName, + Domain: ctx.Config.RateLimitService.Domain, + Timeout: responseTimeout, + FailOpen: ctx.Config.RateLimitService.FailOpen, + EnableXRateLimitHeaders: ctx.Config.RateLimitService.EnableXRateLimitHeaders, } } diff --git a/examples/contour/01-contour-config.yaml b/examples/contour/01-contour-config.yaml index b7bf34b6195..f6b304d16a2 100644 --- a/examples/contour/01-contour-config.yaml +++ b/examples/contour/01-contour-config.yaml @@ -120,6 +120,12 @@ data: # service fails to respond with a valid rate limit decision within # the timeout defined on the extension service. # failOpen: false + # Defines whether to include the X-RateLimit headers X-RateLimit-Limit, + # X-RateLimit-Remaining, and X-RateLimit-Reset (as defined by the IETF + # Internet-Draft linked below), on responses to clients when the Rate + # Limit Service is consulted for a request. + # ref. https://tools.ietf.org/id/draft-polli-ratelimit-headers-03.html + # enableXRateLimitHeaders: false # # Global Policy settings. # policy: diff --git a/examples/render/contour.yaml b/examples/render/contour.yaml index d3823888295..4fb364b15bf 100644 --- a/examples/render/contour.yaml +++ b/examples/render/contour.yaml @@ -154,6 +154,12 @@ data: # service fails to respond with a valid rate limit decision within # the timeout defined on the extension service. # failOpen: false + # Defines whether to include the X-RateLimit headers X-RateLimit-Limit, + # X-RateLimit-Remaining, and X-RateLimit-Reset (as defined by the IETF + # Internet-Draft linked below), on responses to clients when the Rate + # Limit Service is consulted for a request. + # ref. https://tools.ietf.org/id/draft-polli-ratelimit-headers-03.html + # enableXRateLimitHeaders: false # # Global Policy settings. # policy: diff --git a/internal/envoy/v3/ratelimit.go b/internal/envoy/v3/ratelimit.go index 8d570a28658..133a1b6ae8c 100644 --- a/internal/envoy/v3/ratelimit.go +++ b/internal/envoy/v3/ratelimit.go @@ -111,10 +111,11 @@ func GlobalRateLimits(descriptors []*dag.RateLimitDescriptor) []*envoy_route_v3. // GlobalRateLimitConfig stores configuration for // an HTTP global rate limiting filter. type GlobalRateLimitConfig struct { - ExtensionService types.NamespacedName - FailOpen bool - Timeout timeout.Setting - Domain string + ExtensionService types.NamespacedName + FailOpen bool + Timeout timeout.Setting + Domain string + EnableXRateLimitHeaders bool } // GlobalRateLimitFilter returns a configured HTTP global rate limit filter, @@ -141,7 +142,15 @@ func GlobalRateLimitFilter(config *GlobalRateLimitConfig) *http.HttpFilter { }, TransportApiVersion: envoy_core_v3.ApiVersion_V3, }, + EnableXRatelimitHeaders: enableXRateLimitHeaders(config.EnableXRateLimitHeaders), }), }, } } + +func enableXRateLimitHeaders(enable bool) ratelimit_filter_v3.RateLimit_XRateLimitHeadersRFCVersion { + if enable { + return ratelimit_filter_v3.RateLimit_DRAFT_VERSION_03 + } + return ratelimit_filter_v3.RateLimit_OFF +} diff --git a/internal/envoy/v3/ratelimit_test.go b/internal/envoy/v3/ratelimit_test.go index 446c1a5f7f7..13ecc45dac4 100644 --- a/internal/envoy/v3/ratelimit_test.go +++ b/internal/envoy/v3/ratelimit_test.go @@ -180,35 +180,105 @@ func TestGlobalRateLimits(t *testing.T) { } func TestGlobalRateLimitFilter(t *testing.T) { - assert.Nil(t, GlobalRateLimitFilter(nil)) - - want := &http.HttpFilter{ - Name: wellknown.HTTPRateLimit, - ConfigType: &http.HttpFilter_TypedConfig{ - TypedConfig: protobuf.MustMarshalAny(&ratelimit_filter_v3.RateLimit{ - Domain: "domain", - Timeout: protobuf.Duration(time.Second), - FailureModeDeny: false, - RateLimitService: &ratelimit_config_v3.RateLimitServiceConfig{ - GrpcService: &envoy_core_v3.GrpcService{ - TargetSpecifier: &envoy_core_v3.GrpcService_EnvoyGrpc_{ - EnvoyGrpc: &envoy_core_v3.GrpcService_EnvoyGrpc{ - ClusterName: "extension/projectcontour/ratelimit", + tests := map[string]struct { + cfg *GlobalRateLimitConfig + want *http.HttpFilter + }{ + "nil config produces nil filter": { + cfg: nil, + want: nil, + }, + "all fields configured correctly with FailOpen=false": { + cfg: &GlobalRateLimitConfig{ + ExtensionService: k8s.NamespacedNameFrom("projectcontour/ratelimit"), + Timeout: timeout.DurationSetting(7 * time.Second), + Domain: "domain", + FailOpen: false, + }, + want: &http.HttpFilter{ + Name: wellknown.HTTPRateLimit, + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&ratelimit_filter_v3.RateLimit{ + Domain: "domain", + Timeout: protobuf.Duration(7 * time.Second), + FailureModeDeny: true, + RateLimitService: &ratelimit_config_v3.RateLimitServiceConfig{ + GrpcService: &envoy_core_v3.GrpcService{ + TargetSpecifier: &envoy_core_v3.GrpcService_EnvoyGrpc_{ + EnvoyGrpc: &envoy_core_v3.GrpcService_EnvoyGrpc{ + ClusterName: "extension/projectcontour/ratelimit", + }, + }, }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, }, - }, - TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }), + }, + }, + }, + "all fields configured correctly with FailOpen=true": { + cfg: &GlobalRateLimitConfig{ + ExtensionService: k8s.NamespacedNameFrom("projectcontour/ratelimit"), + Timeout: timeout.DurationSetting(7 * time.Second), + Domain: "domain", + FailOpen: true, + }, + want: &http.HttpFilter{ + Name: wellknown.HTTPRateLimit, + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&ratelimit_filter_v3.RateLimit{ + Domain: "domain", + Timeout: protobuf.Duration(7 * time.Second), + FailureModeDeny: false, + RateLimitService: &ratelimit_config_v3.RateLimitServiceConfig{ + GrpcService: &envoy_core_v3.GrpcService{ + TargetSpecifier: &envoy_core_v3.GrpcService_EnvoyGrpc_{ + EnvoyGrpc: &envoy_core_v3.GrpcService_EnvoyGrpc{ + ClusterName: "extension/projectcontour/ratelimit", + }, + }, + }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }, + }), }, - }), + }, + }, + "EnableXRateLimitHeaders=true is configured correctly": { + cfg: &GlobalRateLimitConfig{ + ExtensionService: k8s.NamespacedNameFrom("projectcontour/ratelimit"), + Timeout: timeout.DurationSetting(7 * time.Second), + Domain: "domain", + FailOpen: true, + EnableXRateLimitHeaders: true, + }, + want: &http.HttpFilter{ + Name: wellknown.HTTPRateLimit, + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&ratelimit_filter_v3.RateLimit{ + Domain: "domain", + Timeout: protobuf.Duration(7 * time.Second), + FailureModeDeny: false, + RateLimitService: &ratelimit_config_v3.RateLimitServiceConfig{ + GrpcService: &envoy_core_v3.GrpcService{ + TargetSpecifier: &envoy_core_v3.GrpcService_EnvoyGrpc_{ + EnvoyGrpc: &envoy_core_v3.GrpcService_EnvoyGrpc{ + ClusterName: "extension/projectcontour/ratelimit", + }, + }, + }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }, + EnableXRatelimitHeaders: ratelimit_filter_v3.RateLimit_DRAFT_VERSION_03, + }), + }, + }, }, } - cfg := &GlobalRateLimitConfig{ - ExtensionService: k8s.NamespacedNameFrom("projectcontour/ratelimit"), - FailOpen: true, - Timeout: timeout.DurationSetting(time.Second), - Domain: "domain", + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tc.want, GlobalRateLimitFilter(tc.cfg)) + }) } - - assert.Equal(t, want, GlobalRateLimitFilter(cfg)) } diff --git a/internal/xdscache/v3/listener.go b/internal/xdscache/v3/listener.go index 5fde1cdccf0..2755ef668f9 100644 --- a/internal/xdscache/v3/listener.go +++ b/internal/xdscache/v3/listener.go @@ -141,10 +141,11 @@ type ListenerConfig struct { } type RateLimitConfig struct { - ExtensionService types.NamespacedName - Domain string - Timeout timeout.Setting - FailOpen bool + ExtensionService types.NamespacedName + Domain string + Timeout timeout.Setting + FailOpen bool + EnableXRateLimitHeaders bool } // httpAddress returns the port for the HTTP (non TLS) @@ -406,10 +407,11 @@ func envoyGlobalRateLimitConfig(config *RateLimitConfig) *envoy_v3.GlobalRateLim } return &envoy_v3.GlobalRateLimitConfig{ - ExtensionService: config.ExtensionService, - FailOpen: config.FailOpen, - Timeout: config.Timeout, - Domain: config.Domain, + ExtensionService: config.ExtensionService, + FailOpen: config.FailOpen, + Timeout: config.Timeout, + Domain: config.Domain, + EnableXRateLimitHeaders: config.EnableXRateLimitHeaders, } } diff --git a/internal/xdscache/v3/listener_test.go b/internal/xdscache/v3/listener_test.go index ae2ce0d9bb5..15f40031dc6 100644 --- a/internal/xdscache/v3/listener_test.go +++ b/internal/xdscache/v3/listener_test.go @@ -20,11 +20,16 @@ import ( envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + ratelimit_config_v3 "github.com/envoyproxy/go-control-plane/envoy/config/ratelimit/v3" + ratelimit_filter_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ratelimit/v3" + http "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" + "github.com/envoyproxy/go-control-plane/pkg/wellknown" "github.com/golang/protobuf/proto" contour_api_v1 "github.com/projectcontour/contour/apis/projectcontour/v1" "github.com/projectcontour/contour/internal/dag" 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/timeout" v1 "k8s.io/api/core/v1" @@ -2917,6 +2922,391 @@ func TestListenerVisit(t *testing.T) { SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), }), }, + "insecure httpproxy with rate limit config": { + ListenerConfig: ListenerConfig{ + RateLimitConfig: &RateLimitConfig{ + ExtensionService: types.NamespacedName{Namespace: "projectcontour", Name: "ratelimit"}, + Domain: "contour", + Timeout: timeout.DurationSetting(7 * time.Second), + FailOpen: false, + EnableXRateLimitHeaders: true, + }, + }, + objs: []interface{}{ + &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("ingress_http"). + MetricsPrefix("ingress_http"). + AccessLoggers(envoy_v3.FileAccessLogEnvoy("/dev/stdout")). + DefaultFilters(). + AddFilter(&http.HttpFilter{ + Name: wellknown.HTTPRateLimit, + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&ratelimit_filter_v3.RateLimit{ + Domain: "contour", + FailureModeDeny: true, + Timeout: protobuf.Duration(7 * time.Second), + RateLimitService: &ratelimit_config_v3.RateLimitServiceConfig{ + GrpcService: &envoy_core_v3.GrpcService{ + TargetSpecifier: &envoy_core_v3.GrpcService_EnvoyGrpc_{ + EnvoyGrpc: &envoy_core_v3.GrpcService_EnvoyGrpc{ + ClusterName: dag.ExtensionClusterName(k8s.NamespacedNameFrom("projectcontour/ratelimit")), + }, + }, + }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }, + EnableXRatelimitHeaders: ratelimit_filter_v3.RateLimit_DRAFT_VERSION_03, + }), + }, + }).Get()), + SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), + }), + }, + "secure httpproxy with rate limit config": { + ListenerConfig: ListenerConfig{ + RateLimitConfig: &RateLimitConfig{ + ExtensionService: types.NamespacedName{Namespace: "projectcontour", Name: "ratelimit"}, + Domain: "contour", + Timeout: timeout.DurationSetting(7 * time.Second), + FailOpen: false, + EnableXRateLimitHeaders: true, + }, + }, + objs: []interface{}{ + &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)). + DefaultFilters(). + AddFilter(&http.HttpFilter{ + Name: wellknown.HTTPRateLimit, + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&ratelimit_filter_v3.RateLimit{ + Domain: "contour", + FailureModeDeny: true, + Timeout: protobuf.Duration(7 * time.Second), + RateLimitService: &ratelimit_config_v3.RateLimitServiceConfig{ + GrpcService: &envoy_core_v3.GrpcService{ + TargetSpecifier: &envoy_core_v3.GrpcService_EnvoyGrpc_{ + EnvoyGrpc: &envoy_core_v3.GrpcService_EnvoyGrpc{ + ClusterName: dag.ExtensionClusterName(k8s.NamespacedNameFrom("projectcontour/ratelimit")), + }, + }, + }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }, + EnableXRatelimitHeaders: ratelimit_filter_v3.RateLimit_DRAFT_VERSION_03, + }), + }, + }). + 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)). + AddFilter(&http.HttpFilter{ + Name: wellknown.HTTPRateLimit, + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&ratelimit_filter_v3.RateLimit{ + Domain: "contour", + FailureModeDeny: true, + Timeout: protobuf.Duration(7 * time.Second), + RateLimitService: &ratelimit_config_v3.RateLimitServiceConfig{ + GrpcService: &envoy_core_v3.GrpcService{ + TargetSpecifier: &envoy_core_v3.GrpcService_EnvoyGrpc_{ + EnvoyGrpc: &envoy_core_v3.GrpcService_EnvoyGrpc{ + ClusterName: dag.ExtensionClusterName(k8s.NamespacedNameFrom("projectcontour/ratelimit")), + }, + }, + }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }, + EnableXRatelimitHeaders: ratelimit_filter_v3.RateLimit_DRAFT_VERSION_03, + }), + }, + }). + Get()), + }}, + ListenerFilters: envoy_v3.ListenerFilters( + envoy_v3.TLSInspector(), + ), + SocketOptions: envoy_v3.TCPKeepaliveSocketOptions(), + }), + }, + "secure httpproxy using fallback certificate with rate limit config": { + fallbackCertificate: &types.NamespacedName{ + Name: "fallbacksecret", + Namespace: "default", + }, + ListenerConfig: ListenerConfig{ + RateLimitConfig: &RateLimitConfig{ + ExtensionService: types.NamespacedName{Namespace: "projectcontour", Name: "ratelimit"}, + Domain: "contour", + Timeout: timeout.DurationSetting(7 * time.Second), + FailOpen: false, + EnableXRateLimitHeaders: true, + }, + }, + objs: []interface{}{ + &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", + EnableFallbackCertificate: true, + }, + }, + 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.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fallbacksecret", + 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). + AccessLoggers(envoy_v3.FileAccessLogEnvoy(DEFAULT_HTTP_ACCESS_LOG)). + DefaultFilters(). + AddFilter(&http.HttpFilter{ + Name: wellknown.HTTPRateLimit, + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&ratelimit_filter_v3.RateLimit{ + Domain: "contour", + FailureModeDeny: true, + Timeout: protobuf.Duration(7 * time.Second), + RateLimitService: &ratelimit_config_v3.RateLimitServiceConfig{ + GrpcService: &envoy_core_v3.GrpcService{ + TargetSpecifier: &envoy_core_v3.GrpcService_EnvoyGrpc_{ + EnvoyGrpc: &envoy_core_v3.GrpcService_EnvoyGrpc{ + ClusterName: dag.ExtensionClusterName(k8s.NamespacedNameFrom("projectcontour/ratelimit")), + }, + }, + }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }, + EnableXRatelimitHeaders: ratelimit_filter_v3.RateLimit_DRAFT_VERSION_03, + }), + }, + }). + 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)). + AddFilter(&http.HttpFilter{ + Name: wellknown.HTTPRateLimit, + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&ratelimit_filter_v3.RateLimit{ + Domain: "contour", + FailureModeDeny: true, + Timeout: protobuf.Duration(7 * time.Second), + RateLimitService: &ratelimit_config_v3.RateLimitServiceConfig{ + GrpcService: &envoy_core_v3.GrpcService{ + TargetSpecifier: &envoy_core_v3.GrpcService_EnvoyGrpc_{ + EnvoyGrpc: &envoy_core_v3.GrpcService_EnvoyGrpc{ + ClusterName: dag.ExtensionClusterName(k8s.NamespacedNameFrom("projectcontour/ratelimit")), + }, + }, + }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }, + EnableXRatelimitHeaders: ratelimit_filter_v3.RateLimit_DRAFT_VERSION_03, + }), + }, + }). + Get(), + ), + }, { + FilterChainMatch: &envoy_listener_v3.FilterChainMatch{ + TransportProtocol: "tls", + }, + TransportSocket: transportSocket("fallbacksecret", envoy_tls_v3.TlsParameters_TLSv1_2, nil, "h2", "http/1.1"), + Filters: envoy_v3.Filters(envoy_v3.HTTPConnectionManagerBuilder(). + DefaultFilters(). + MetricsPrefix(ENVOY_HTTPS_LISTENER). + RouteConfigName(ENVOY_FALLBACK_ROUTECONFIG). + AccessLoggers(envoy_v3.FileAccessLogEnvoy(DEFAULT_HTTP_ACCESS_LOG)). + AddFilter(&http.HttpFilter{ + Name: wellknown.HTTPRateLimit, + ConfigType: &http.HttpFilter_TypedConfig{ + TypedConfig: protobuf.MustMarshalAny(&ratelimit_filter_v3.RateLimit{ + Domain: "contour", + FailureModeDeny: true, + Timeout: protobuf.Duration(7 * time.Second), + RateLimitService: &ratelimit_config_v3.RateLimitServiceConfig{ + GrpcService: &envoy_core_v3.GrpcService{ + TargetSpecifier: &envoy_core_v3.GrpcService_EnvoyGrpc_{ + EnvoyGrpc: &envoy_core_v3.GrpcService_EnvoyGrpc{ + ClusterName: dag.ExtensionClusterName(k8s.NamespacedNameFrom("projectcontour/ratelimit")), + }, + }, + }, + TransportApiVersion: envoy_core_v3.ApiVersion_V3, + }, + EnableXRatelimitHeaders: ratelimit_filter_v3.RateLimit_DRAFT_VERSION_03, + }), + }, + }). + Get(), + ), + Name: "fallback-certificate", + }}, + 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 ff8273d2bd3..fbd13865cf8 100644 --- a/pkg/config/parameters.go +++ b/pkg/config/parameters.go @@ -567,6 +567,14 @@ type RateLimitService struct { // Rate Limit Service fails to respond with a valid rate limit // decision within the timeout defined on the extension service. FailOpen bool `yaml:"failOpen,omitempty"` + + // EnableXRateLimitHeaders defines whether to include the X-RateLimit + // headers X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset + // (as defined by the IETF Internet-Draft linked below), on responses + // to clients when the Rate Limit Service is consulted for a request. + // + // ref. https://tools.ietf.org/id/draft-polli-ratelimit-headers-03.html + EnableXRateLimitHeaders bool `yaml:"enableXRateLimitHeaders,omitempty"` } // Validate verifies that the parameter values do not have any syntax errors. diff --git a/site/docs/main/configuration.md b/site/docs/main/configuration.md index 2f8d9c0b263..200be0bd77c 100644 --- a/site/docs/main/configuration.md +++ b/site/docs/main/configuration.md @@ -241,6 +241,7 @@ The rate limit service configuration block is used to configure an optional glob | extensionService | string | | This field identifies the extension service defining the rate limit service, formatted as /. | | domain | string | contour | This field defines the rate limit domain value to pass to the rate limit service. Acts as a container for a set of rate limit definitions within the RLS. | | failOpen | bool | false | This field defines whether to allow requests to proceed when the rate limit service fails to respond with a valid rate limit decision within the timeout defined on the extension service. | +| enableXRateLimitHeaders | bool | false | This field defines whether to include the X-RateLimit headers X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (as defined by the IETF Internet-Draft https://tools.ietf.org/id/draft-polli-ratelimit-headers-03.html), on responses to clients when the Rate Limit Service is consulted for a request. | {: class="table thead-dark table-bordered"}
@@ -355,10 +356,25 @@ data: # right side of the x-forwarded-for HTTP header to trust. # num-trusted-hops: 0 # + # Configure an optional global rate limit service. # rateLimitService: + # Identifies the extension service defining the rate limit service, + # formatted as /. # extensionService: projectcontour/ratelimit + # Defines the rate limit domain to pass to the rate limit service. + # Acts as a container for a set of rate limit definitions within + # the RLS. # domain: contour + # Defines whether to allow requests to proceed when the rate limit + # service fails to respond with a valid rate limit decision within + # the timeout defined on the extension service. # failOpen: false + # Defines whether to include the X-RateLimit headers X-RateLimit-Limit, + # X-RateLimit-Remaining, and X-RateLimit-Reset (as defined by the IETF + # Internet-Draft linked below), on responses to clients when the Rate + # Limit Service is consulted for a request. + # ref. https://tools.ietf.org/id/draft-polli-ratelimit-headers-03.html + # enableXRateLimitHeaders: false # # Global Policy settings. # policy: