Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions docs/docfx/articles/rate-limiting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Rate Limiting

## Introduction
The reverse proxy can be used to rate-limit requests before they are proxied to the destination servers. This can reduce load on the destination servers, add a layer of protection, and ensure consistent policies are implemented across your applications.

> This feature is only available when using .NET 7.0 or later

## Defaults

No rate limiting is performed on requests unless enabled in the route or application configuration. However, the Rate Limiting middleware (`app.UseRateLimiter()`) can apply a default limiter applied to all routes, and this doesn't require any opt-in from the config.

Example:
```c#
builder.Services.AddRateLimiter(options => options.GlobalLimiter = globalLimiter);
```

## Configuration
Rate Limiter policies can be specified per route via [RouteConfig.RateLimiterPolicy](xref:Yarp.ReverseProxy.Configuration.RouteConfig) and can be bound from the `Routes` sections of the config file. As with other route properties, this can be modified and reloaded without restarting the proxy. Policy names are case insensitive.

Example:
```JSON
{
"ReverseProxy": {
"Routes": {
"route1" : {
"ClusterId": "cluster1",
"RateLimiterPolicy": "customPolicy",
"Match": {
"Hosts": [ "localhost" ]
},
}
},
"Clusters": {
"cluster1": {
"Destinations": {
"cluster1/destination1": {
"Address": "https://localhost:10001/"
}
}
}
}
}
}
```

[RateLimiter policies](https://learn.microsoft.com/aspnet/core/performance/rate-limit) are an ASP.NET Core concept that the proxy utilizes. The proxy provides the above configuration to specify a policy per route and the rest is handled by existing ASP.NET Core rate limiting middleware.

RateLimiter policies can be configured in Startup.ConfigureServices as follows:
```c#
public void ConfigureServices(IServiceCollection services)
{
services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("customPolicy", opt =>
{
opt.PermitLimit = 4;
opt.Window = TimeSpan.FromSeconds(12);
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
opt.QueueLimit = 2;
});
});
}
```

In Startup.Configure add the RateLimiter middleware between Routing and Endpoints.

```c#
public void Configure(IApplicationBuilder app)
{
app.UseRouting();

app.UseRateLimiter();

app.UseEndpoints(endpoints =>
{
endpoints.MapReverseProxy();
});
}
```

See the [Rate Limiting](https://learn.microsoft.com/aspnet/core/performance/rate-limit) docs for setting up your preferred kind of rate limiting.

### Disable Rate Limiting

Specifying the value `disable` in a route's `RateLimiterPolicy` parameter means the rate limiter middleware will not apply any policies to this route, even the default policy.
2 changes: 2 additions & 0 deletions docs/docfx/articles/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
href: header-routing.md
- name: Authentication and Authorization
href: authn-authz.md
- name: Rate Limiting
href: rate-limiting.md
- name: Cross-Origin Requests (CORS)
href: cors.md
- name: Session Affinity
Expand Down
8 changes: 8 additions & 0 deletions samples/KubernetesIngress.Sample/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ metadata:
namespace: default
annotations:
yarp.ingress.kubernetes.io/authorization-policy: authzpolicy
yarp.ingress.kubernetes.io/rate-limiter-policy: ratelimiterpolicy
yarp.ingress.kubernetes.io/transforms: |
- PathRemovePrefix: "/apis"
yarp.ingress.kubernetes.io/route-headers: |
Expand Down Expand Up @@ -73,6 +74,7 @@ The table below lists the available annotations.
|Annotation|Data Type|
|---|---|
|yarp.ingress.kubernetes.io/authorization-policy|string|
|yarp.ingress.kubernetes.io/rate-limiter-policy|string|
|yarp.ingress.kubernetes.io/backend-protocol|string|
|yarp.ingress.kubernetes.io/cors-policy|string|
|yarp.ingress.kubernetes.io/health-check|[ActivateHealthCheckConfig](https://microsoft.github.io/reverse-proxy/api/Yarp.ReverseProxy.Configuration.ActiveHealthCheckConfig.html)|
Expand All @@ -90,6 +92,12 @@ See https://microsoft.github.io/reverse-proxy/articles/authn-authz.html for a li

`yarp.ingress.kubernetes.io/authorization-policy: anonymous`

#### RateLimiter Policy

See https://microsoft.github.io/reverse-proxy/articles/rate-limiting.html for a list of available policies, or how to add your own custom policies.

`yarp.ingress.kubernetes.io/rate-limiter-policy: mypolicy`

#### Backend Protocol

Specifies the protocol of the backend service. Defaults to http.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ internal sealed class YarpIngressOptions
public bool Https { get; set; }
public List<Dictionary<string, string>> Transforms { get; set; }
public string AuthorizationPolicy { get; set; }
#if NET7_0_OR_GREATER
public string RateLimiterPolicy { get; set; }
#endif
public SessionAffinityConfig SessionAffinity { get; set; }
public HttpClientConfig HttpClientConfig { get; set; }
public string LoadBalancingPolicy { get; set; }
Expand Down Expand Up @@ -38,4 +41,4 @@ public RouteHeader ToRouteHeader()
IsCaseSensitive = IsCaseSensitive
};
}
}
}
13 changes: 11 additions & 2 deletions src/Kubernetes.Controller/Converters/YarpParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ private static void HandleIngressRulePath(YarpIngressContext ingressContext, V1S
RouteId = $"{ingressContext.Ingress.Metadata.Name}.{ingressContext.Ingress.Metadata.NamespaceProperty}:{host}{path.Path}",
Transforms = ingressContext.Options.Transforms,
AuthorizationPolicy = ingressContext.Options.AuthorizationPolicy,
#if NET7_0_OR_GREATER
RateLimiterPolicy = ingressContext.Options.RateLimiterPolicy,
#endif
CorsPolicy = ingressContext.Options.CorsPolicy,
Metadata = ingressContext.Options.RouteMetadata,
Order = ingressContext.Options.RouteOrder,
Expand Down Expand Up @@ -171,16 +174,22 @@ private static YarpIngressOptions HandleAnnotations(YarpIngressContext context,

if (annotations.TryGetValue("yarp.ingress.kubernetes.io/backend-protocol", out var http))
{
options.Https = http.Equals("https", StringComparison.OrdinalIgnoreCase);
options.Https = http.Equals("https", StringComparison.OrdinalIgnoreCase);
}
if (annotations.TryGetValue("yarp.ingress.kubernetes.io/transforms", out var transforms))
{
options.Transforms = YamlDeserializer.Deserialize<List<Dictionary<string,string>>>(transforms);
options.Transforms = YamlDeserializer.Deserialize<List<Dictionary<string, string>>>(transforms);
}
if (annotations.TryGetValue("yarp.ingress.kubernetes.io/authorization-policy", out var authorizationPolicy))
{
options.AuthorizationPolicy = authorizationPolicy;
}
#if NET7_0_OR_GREATER
if (annotations.TryGetValue("yarp.ingress.kubernetes.io/rate-limiter-policy", out var rateLimiterPolicy))
{
options.RateLimiterPolicy = rateLimiterPolicy;
}
#endif
if (annotations.TryGetValue("yarp.ingress.kubernetes.io/cors-policy", out var corsPolicy))
{
options.CorsPolicy = corsPolicy;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ private static RouteConfig CreateRoute(IConfigurationSection section)
MaxRequestBodySize = section.ReadInt64(nameof(RouteConfig.MaxRequestBodySize)),
ClusterId = section[nameof(RouteConfig.ClusterId)],
AuthorizationPolicy = section[nameof(RouteConfig.AuthorizationPolicy)],
#if NET7_0_OR_GREATER
RateLimiterPolicy = section[nameof(RouteConfig.RateLimiterPolicy)],
#endif
CorsPolicy = section[nameof(RouteConfig.CorsPolicy)],
Metadata = section.GetSection(nameof(RouteConfig.Metadata)).ReadStringDictionary(),
Transforms = CreateTransforms(section.GetSection(nameof(RouteConfig.Transforms))),
Expand Down
35 changes: 35 additions & 0 deletions src/ReverseProxy/Configuration/ConfigValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ internal sealed class ConfigValidator : IConfigValidator

private readonly ITransformBuilder _transformBuilder;
private readonly IAuthorizationPolicyProvider _authorizationPolicyProvider;
private readonly IYarpRateLimiterPolicyProvider _rateLimiterPolicyProvider;
private readonly ICorsPolicyProvider _corsPolicyProvider;
private readonly IDictionary<string, ILoadBalancingPolicy> _loadBalancingPolicies;
private readonly IDictionary<string, IAffinityFailurePolicy> _affinityFailurePolicies;
Expand All @@ -40,6 +41,7 @@ internal sealed class ConfigValidator : IConfigValidator

public ConfigValidator(ITransformBuilder transformBuilder,
IAuthorizationPolicyProvider authorizationPolicyProvider,
IYarpRateLimiterPolicyProvider rateLimiterPolicyProvider,
ICorsPolicyProvider corsPolicyProvider,
IEnumerable<ILoadBalancingPolicy> loadBalancingPolicies,
IEnumerable<IAffinityFailurePolicy> affinityFailurePolicies,
Expand All @@ -50,6 +52,7 @@ public ConfigValidator(ITransformBuilder transformBuilder,
{
_transformBuilder = transformBuilder ?? throw new ArgumentNullException(nameof(transformBuilder));
_authorizationPolicyProvider = authorizationPolicyProvider ?? throw new ArgumentNullException(nameof(authorizationPolicyProvider));
_rateLimiterPolicyProvider = rateLimiterPolicyProvider ?? throw new ArgumentNullException(nameof(rateLimiterPolicyProvider));
_corsPolicyProvider = corsPolicyProvider ?? throw new ArgumentNullException(nameof(corsPolicyProvider));
_loadBalancingPolicies = loadBalancingPolicies?.ToDictionaryByUniqueId(p => p.Name) ?? throw new ArgumentNullException(nameof(loadBalancingPolicies));
_affinityFailurePolicies = affinityFailurePolicies?.ToDictionaryByUniqueId(p => p.Name) ?? throw new ArgumentNullException(nameof(affinityFailurePolicies));
Expand All @@ -72,6 +75,9 @@ public async ValueTask<IList<Exception>> ValidateRouteAsync(RouteConfig route)

errors.AddRange(_transformBuilder.ValidateRoute(route));
await ValidateAuthorizationPolicyAsync(errors, route.AuthorizationPolicy, route.RouteId);
#if NET7_0_OR_GREATER
await ValidateRateLimiterPolicyAsync(errors, route.RateLimiterPolicy, route.RouteId);
#endif
await ValidateCorsPolicyAsync(errors, route.CorsPolicy, route.RouteId);

if (route.Match is null)
Expand Down Expand Up @@ -287,6 +293,35 @@ private async ValueTask ValidateAuthorizationPolicyAsync(IList<Exception> errors
}
}

private async ValueTask ValidateRateLimiterPolicyAsync(IList<Exception> errors, string? rateLimiterPolicyName, string routeId)
{
if (string.IsNullOrEmpty(rateLimiterPolicyName))
{
return;
}

try
{
var policy = await _rateLimiterPolicyProvider.GetPolicyAsync(rateLimiterPolicyName);

if (policy is null)
{
errors.Add(new ArgumentException($"RateLimiter policy '{rateLimiterPolicyName}' not found for route '{routeId}'."));
return;
}

if (string.Equals(RateLimitingConstants.Default, rateLimiterPolicyName, StringComparison.OrdinalIgnoreCase)
|| string.Equals(RateLimitingConstants.Disable, rateLimiterPolicyName, StringComparison.OrdinalIgnoreCase))
{
errors.Add(new ArgumentException($"The application has registered a RateLimiter policy named '{rateLimiterPolicyName}' that conflicts with the reserved RateLimiter policy name used on this route. The registered policy name needs to be changed for this route to function."));
}
}
catch (Exception ex)
{
errors.Add(new ArgumentException($"Unable to retrieve the RateLimiter policy '{rateLimiterPolicyName}' for route '{routeId}'.", ex));
}
}

private async ValueTask ValidateCorsPolicyAsync(IList<Exception> errors, string? corsPolicyName, string routeId)
{
if (string.IsNullOrEmpty(corsPolicyName))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#if NET7_0_OR_GREATER
using System;
using System.Collections;
using System.Reflection;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options;
#endif

using System.Threading.Tasks;

namespace Yarp.ReverseProxy.Configuration;

// TODO: update or remove this once AspNetCore provides a mechanism to validate the RateLimiter policies https://github.com/dotnet/aspnetcore/issues/45684


internal interface IYarpRateLimiterPolicyProvider
{
ValueTask<object?> GetPolicyAsync(string policyName);
}

internal class YarpRateLimiterPolicyProvider : IYarpRateLimiterPolicyProvider
{
#if NET7_0_OR_GREATER
private readonly RateLimiterOptions _rateLimiterOptions;

private readonly IDictionary _policyMap, _unactivatedPolicyMap;

public YarpRateLimiterPolicyProvider(IOptions<RateLimiterOptions> rateLimiterOptions)
{
_rateLimiterOptions = rateLimiterOptions?.Value ?? throw new ArgumentNullException(nameof(rateLimiterOptions));

var type = typeof(RateLimiterOptions);
var flags = BindingFlags.Instance | BindingFlags.NonPublic;
_policyMap = type.GetProperty("PolicyMap", flags)?.GetValue(_rateLimiterOptions, null) as IDictionary
?? throw new NotSupportedException("This version of YARP is incompatible with the current version of ASP.NET Core.");
_unactivatedPolicyMap = type.GetProperty("UnactivatedPolicyMap", flags)?.GetValue(_rateLimiterOptions, null) as IDictionary
?? throw new NotSupportedException("This version of YARP is incompatible with the current version of ASP.NET Core.");
}

public ValueTask<object?> GetPolicyAsync(string policyName)
{
return ValueTask.FromResult(_policyMap[policyName] ?? _unactivatedPolicyMap[policyName]);
}
#else
public ValueTask<object?> GetPolicyAsync(string policyName)
{
return default;
}
#endif
}
10 changes: 10 additions & 0 deletions src/ReverseProxy/Configuration/RateLimitingConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Yarp.ReverseProxy.Configuration;

internal static class RateLimitingConstants
{
internal const string Default = "Default";
internal const string Disable = "Disable";
}
35 changes: 26 additions & 9 deletions src/ReverseProxy/Configuration/RouteConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,15 @@ public sealed record RouteConfig
/// Set to "Anonymous" to disable all authorization checks for this route.
/// </summary>
public string? AuthorizationPolicy { get; init; }

#if NET7_0_OR_GREATER
/// <summary>
/// The name of the RateLimiterPolicy to apply to this route.
/// If not set then only the GlobalLimiter will apply.
/// Set to "Disable" to disable rate limiting for this route.
/// Set to "Default" or leave empty to use the global rate limits, if any.
/// </summary>
public string? RateLimiterPolicy { get; init; }
#endif
/// <summary>
/// The name of the CorsPolicy to apply to this route.
/// If not set then the route won't be automatically matched for cors preflight requests.
Expand Down Expand Up @@ -79,6 +87,9 @@ public bool Equals(RouteConfig? other)
&& string.Equals(RouteId, other.RouteId, StringComparison.OrdinalIgnoreCase)
&& string.Equals(ClusterId, other.ClusterId, StringComparison.OrdinalIgnoreCase)
&& string.Equals(AuthorizationPolicy, other.AuthorizationPolicy, StringComparison.OrdinalIgnoreCase)
#if NET7_0_OR_GREATER
&& string.Equals(RateLimiterPolicy, other.RateLimiterPolicy, StringComparison.OrdinalIgnoreCase)
#endif
&& string.Equals(CorsPolicy, other.CorsPolicy, StringComparison.OrdinalIgnoreCase)
&& Match == other.Match
&& CaseSensitiveEqualHelper.Equals(Metadata, other.Metadata)
Expand All @@ -87,13 +98,19 @@ public bool Equals(RouteConfig? other)

public override int GetHashCode()
{
return HashCode.Combine(Order,
RouteId?.GetHashCode(StringComparison.OrdinalIgnoreCase),
ClusterId?.GetHashCode(StringComparison.OrdinalIgnoreCase),
AuthorizationPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase),
CorsPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase),
Match,
CaseSensitiveEqualHelper.GetHashCode(Metadata),
CaseSensitiveEqualHelper.GetHashCode(Transforms));
// HashCode.Combine(...) takes only 8 arguments
var hash = new HashCode();
hash.Add(Order);
hash.Add(RouteId?.GetHashCode(StringComparison.OrdinalIgnoreCase));
hash.Add(ClusterId?.GetHashCode(StringComparison.OrdinalIgnoreCase));
hash.Add(AuthorizationPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase));
#if NET7_0_OR_GREATER
hash.Add(RateLimiterPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase));
#endif
hash.Add(CorsPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase));
hash.Add(Match);
hash.Add(CaseSensitiveEqualHelper.GetHashCode(Metadata));
hash.Add(CaseSensitiveEqualHelper.GetHashCode(Transforms));
return hash.ToHashCode();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal static class IReverseProxyBuilderExtensions
{
public static IReverseProxyBuilder AddConfigBuilder(this IReverseProxyBuilder builder)
{
builder.Services.TryAddSingleton<IYarpRateLimiterPolicyProvider, YarpRateLimiterPolicyProvider>();
builder.Services.TryAddSingleton<IConfigValidator, ConfigValidator>();
builder.Services.TryAddSingleton<IRandomFactory, RandomFactory>();
builder.AddTransformFactory<ForwardedTransformFactory>();
Expand Down
Loading