534 lines
22 KiB
C#
534 lines
22 KiB
C#
using System.Net;
|
|
using System.Text.Json;
|
|
using FinalFactory.Rendezvous.Contracts;
|
|
using FinalFactory.Rendezvous.Server.Abuse;
|
|
using FinalFactory.Rendezvous.Server.Http;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.HttpOverrides;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace FinalFactory.Rendezvous.Tests.Server.Abuse;
|
|
|
|
public sealed class AbuseProtectionTests
|
|
{
|
|
[Fact]
|
|
public void Ipv4AndIpv6PrefixesShareBudgetsAndRecoverAfterTheWindow()
|
|
{
|
|
ManualTimeProvider time = new(new DateTimeOffset(2026, 7, 16, 12, 0, 0, TimeSpan.Zero));
|
|
AbuseProtectionOptions options = PermissiveOptions();
|
|
options.HttpIpPrefixRequestsPerWindow = 2;
|
|
AbuseProtectionService protection = new(Options.Create(options), time);
|
|
|
|
AssertAccepted(protection, IPAddress.Parse("198.51.100.10"), "BrowseSessions");
|
|
AssertAccepted(protection, IPAddress.Parse("198.51.100.200"), "BrowseSessions");
|
|
AssertRejected(protection, IPAddress.Parse("198.51.100.99"), "BrowseSessions");
|
|
|
|
time.Advance(TimeSpan.FromSeconds(1));
|
|
AssertAccepted(protection, IPAddress.Parse("198.51.100.99"), "BrowseSessions");
|
|
|
|
options = PermissiveOptions();
|
|
options.HttpIpPrefixRequestsPerWindow = 1;
|
|
protection = new(Options.Create(options), time);
|
|
AssertAccepted(protection, IPAddress.Parse("2606:4700:1234:5600::1"), "GetSession");
|
|
AssertRejected(protection, IPAddress.Parse("2606:4700:1234:56ff::2"), "GetSession");
|
|
AssertAccepted(protection, IPAddress.Parse("2606:4700:1234:5700::2"), "GetSession");
|
|
}
|
|
|
|
[Fact]
|
|
public void PrincipalConcurrencyIsReleasedAndRejectedCallsDoNotConsumeRate()
|
|
{
|
|
AbuseProtectionOptions options = PermissiveOptions();
|
|
options.HttpPrincipalConcurrency = 1;
|
|
options.HttpPrincipalRequestsPerWindow = 2;
|
|
AbuseProtectionService protection = new(Options.Create(options));
|
|
|
|
Assert.True(protection.TryAcquireHttpIdentity(
|
|
"RegisterSession", "game/prod", "publisher-1", null, out var first, out _));
|
|
Assert.False(protection.TryAcquireHttpIdentity(
|
|
"RegisterSession", "game/prod", "publisher-1", null, out _, out _));
|
|
first!.Dispose();
|
|
|
|
Assert.True(protection.TryAcquireHttpIdentity(
|
|
"RegisterSession", "game/prod", "publisher-1", null, out var second, out _));
|
|
second!.Dispose();
|
|
Assert.False(protection.TryAcquireHttpIdentity(
|
|
"RegisterSession", "game/prod", "publisher-1", null, out _, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void TrackerCapacityFailsClosedWithoutGrowingAndAWindowResetRecovers()
|
|
{
|
|
ManualTimeProvider time = new(new DateTimeOffset(2026, 7, 16, 12, 0, 0, TimeSpan.Zero));
|
|
AbuseProtectionOptions options = PermissiveOptions();
|
|
options.MaxTrackedKeys = 10;
|
|
options.UdpTrackedKeyLimit = 0;
|
|
AbuseProtectionService protection = new(Options.Create(options), time);
|
|
|
|
AssertAccepted(protection, IPAddress.Parse("198.51.100.1"), "GetSession");
|
|
AssertRejected(protection, IPAddress.Parse("203.0.113.1"), "GetSession");
|
|
Assert.InRange(protection.TrackedKeyCount, 1, options.MaxTrackedKeys);
|
|
|
|
time.Advance(TimeSpan.FromSeconds(1));
|
|
AssertAccepted(protection, IPAddress.Parse("203.0.113.1"), "GetSession");
|
|
Assert.InRange(protection.TrackedKeyCount, 1, options.MaxTrackedKeys);
|
|
}
|
|
|
|
[Fact]
|
|
public void OptionalTrafficCannotConsumeTheLeaseOperationReserve()
|
|
{
|
|
AbuseProtectionOptions options = PermissiveOptions();
|
|
options.HttpGlobalRequestsPerWindow = 3;
|
|
options.HttpOptionalRequestsPerWindow = 2;
|
|
options.HttpIpPrefixRequestsPerWindow = 3;
|
|
options.HttpOptionalIpPrefixRequestsPerWindow = 2;
|
|
AbuseProtectionService protection = new(Options.Create(options));
|
|
IPAddress source = IPAddress.Parse("198.51.100.10");
|
|
|
|
Assert.True(protection.IsOperatorSourceAllowed(source));
|
|
Assert.True(protection.IsOperatorSourceAllowed(IPAddress.Parse("::ffff:198.51.100.10")));
|
|
Assert.False(protection.IsOperatorSourceAllowed(IPAddress.Parse("198.51.100.11")));
|
|
AssertAccepted(protection, source, "BrowseSessions");
|
|
AssertAccepted(protection, source, "BrowseSessions");
|
|
AssertRejected(protection, source, "BrowseSessions");
|
|
AssertAccepted(protection, source, "RenewSessionLease");
|
|
AssertRejected(protection, source, "RenewSessionLease");
|
|
}
|
|
|
|
[Fact]
|
|
public void PublicSaturationCannotConsumeTheOperatorPartition()
|
|
{
|
|
AbuseProtectionOptions options = PermissiveOptions();
|
|
options.HttpGlobalRequestsPerWindow = 1;
|
|
options.HttpOptionalRequestsPerWindow = 1;
|
|
options.OperatorGlobalRequestsPerWindow = 1;
|
|
AbuseProtectionService protection = new(Options.Create(options));
|
|
IPAddress source = IPAddress.Parse("198.51.100.10");
|
|
|
|
AssertAccepted(protection, source, "BrowseSessions");
|
|
AssertRejected(protection, source, "BrowseSessions");
|
|
Assert.True(protection.TryAcquireOperatorIngress(source, out var lease, out _));
|
|
lease!.Dispose();
|
|
Assert.False(protection.TryAcquireOperatorIngress(source, out _, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeniedOperatorSourcesConsumeTheBoundedPublicPartition()
|
|
{
|
|
AbuseProtectionOptions options = PermissiveOptions();
|
|
options.OperatorAllowedAddresses = ["192.0.2.10"];
|
|
options.HttpGlobalRequestsPerWindow = 1;
|
|
options.HttpOptionalRequestsPerWindow = 1;
|
|
options.HttpIpPrefixRequestsPerWindow = 1;
|
|
options.HttpOptionalIpPrefixRequestsPerWindow = 1;
|
|
AbuseProtectionService protection = new(Options.Create(options));
|
|
bool dispatched = false;
|
|
HttpAbuseProtectionMiddleware middleware = new(
|
|
_ =>
|
|
{
|
|
dispatched = true;
|
|
return Task.CompletedTask;
|
|
},
|
|
protection);
|
|
|
|
DefaultHttpContext first = Context("198.51.100.10");
|
|
first.SetEndpoint(new Endpoint(
|
|
_ => Task.CompletedTask,
|
|
new EndpointMetadataCollection(new EndpointNameMetadata("GetOperatorStatus")),
|
|
"operator-status"));
|
|
await middleware.InvokeAsync(first);
|
|
Assert.Equal(StatusCodes.Status404NotFound, first.Response.StatusCode);
|
|
|
|
DefaultHttpContext repeated = Context("198.51.100.10");
|
|
repeated.SetEndpoint(first.GetEndpoint());
|
|
await middleware.InvokeAsync(repeated);
|
|
Assert.Equal(StatusCodes.Status429TooManyRequests, repeated.Response.StatusCode);
|
|
Assert.False(dispatched);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResourceBudgetsRemainIsolatedAcrossTenantAndPrincipalScopes()
|
|
{
|
|
AbuseProtectionOptions options = PermissiveOptions();
|
|
options.HttpResourceRequestsPerWindow = 1;
|
|
AbuseProtectionService protection = new(Options.Create(options));
|
|
|
|
Assert.True(protection.TryAcquireHttpIdentity(
|
|
"UpdateSession", IPAddress.Parse("198.51.100.10"),
|
|
"game-a/prod", "publisher", "listing", out var first, out _));
|
|
first!.Dispose();
|
|
Assert.False(protection.TryAcquireHttpIdentity(
|
|
"UpdateSession", IPAddress.Parse("198.51.100.10"),
|
|
"game-a/prod", "publisher", "listing", out _, out _));
|
|
Assert.True(protection.TryAcquireHttpIdentity(
|
|
"UpdateSession", IPAddress.Parse("203.0.113.10"),
|
|
"game-b/prod", "publisher", "listing", out var second, out _));
|
|
second!.Dispose();
|
|
Assert.True(protection.TryAcquireHttpIdentity(
|
|
"UpdateSession", IPAddress.Parse("192.0.2.10"),
|
|
"game-a/prod", "other-publisher", "listing", out var third, out _));
|
|
third!.Dispose();
|
|
}
|
|
|
|
[Fact]
|
|
public void RotatingCredentialsCannotBypassIndependentResourceBudgets()
|
|
{
|
|
AbuseProtectionOptions options = PermissiveOptions();
|
|
options.HttpResourceRequestsPerWindow = 2;
|
|
options.UdpResourceDatagramsPerWindow = 2;
|
|
AbuseProtectionService protection = new(Options.Create(options));
|
|
|
|
for (int index = 1; index <= 2; index++)
|
|
{
|
|
Assert.True(protection.TryAcquireHttpIdentity(
|
|
"CancelJoinAttempt", null, $"capability-{index}", "attempt", out var lease, out _));
|
|
lease!.Dispose();
|
|
Assert.True(protection.TryAcceptUdpIdentity(
|
|
"Client", $"capability-{index}", "mediation-handle"));
|
|
}
|
|
|
|
Assert.False(protection.TryAcquireHttpIdentity(
|
|
"CancelJoinAttempt", null, "capability-3", "attempt", out _, out _));
|
|
Assert.False(protection.TryAcceptUdpIdentity(
|
|
"Client", "capability-3", "mediation-handle"));
|
|
}
|
|
|
|
[Fact]
|
|
public void UdpWireOperationsHaveIndependentBoundedIngressBudgets()
|
|
{
|
|
AbuseProtectionOptions options = PermissiveOptions();
|
|
options.UdpOperationDatagramsPerWindow = 1;
|
|
AbuseProtectionService protection = new(Options.Create(options));
|
|
IPAddress source = IPAddress.Parse("198.51.100.10");
|
|
|
|
Assert.True(protection.TryAcceptUdpIngress(source, "frozen"));
|
|
Assert.False(protection.TryAcceptUdpIngress(source, "frozen"));
|
|
Assert.True(protection.TryAcceptUdpIngress(source, "litenet-or-invalid"));
|
|
Assert.False(protection.TryAcceptUdpIngress(source, "litenet-or-invalid"));
|
|
}
|
|
|
|
[Fact]
|
|
public void UdpTrackerExhaustionCannotConsumeTheCriticalHttpKeyReserve()
|
|
{
|
|
AbuseProtectionOptions options = PermissiveOptions();
|
|
options.MaxTrackedKeys = 28;
|
|
options.CriticalTrackedKeyReserve = 16;
|
|
options.UdpTrackedKeyLimit = 12;
|
|
AbuseProtectionService protection = new(Options.Create(options));
|
|
|
|
for (int index = 1; index <= 3; index++)
|
|
{
|
|
IPAddress address = IPAddress.Parse($"198.51.{index}.1");
|
|
_ = protection.TryAcceptUdpIngress(address, "raw");
|
|
_ = protection.TryAcceptUdpIdentity("Client", $"capability-{index}", $"resource-{index}");
|
|
}
|
|
|
|
Assert.InRange(protection.TrackedKeyCount, 1, 12);
|
|
Assert.True(protection.TryAcquireHttpIngress(
|
|
IPAddress.Parse("203.0.113.10"),
|
|
"RenewSessionLease",
|
|
out var ingress,
|
|
out _));
|
|
Assert.True(protection.TryAcquireHttpIdentity(
|
|
"RenewSessionLease",
|
|
"game/prod",
|
|
"publisher",
|
|
"listing",
|
|
out var identity,
|
|
out _));
|
|
identity!.Dispose();
|
|
ingress!.Dispose();
|
|
Assert.InRange(protection.TrackedKeyCount, 1, options.MaxTrackedKeys);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task HttpOverloadIsTypedAndOversizedBodiesAreRejectedBeforeDispatch()
|
|
{
|
|
AbuseProtectionOptions options = PermissiveOptions();
|
|
options.HttpIpPrefixRequestsPerWindow = 1;
|
|
AbuseProtectionService protection = new(Options.Create(options));
|
|
int dispatched = 0;
|
|
HttpAbuseProtectionMiddleware middleware = new(
|
|
_ =>
|
|
{
|
|
dispatched++;
|
|
return Task.CompletedTask;
|
|
},
|
|
protection);
|
|
|
|
DefaultHttpContext accepted = Context("198.51.100.10");
|
|
await middleware.InvokeAsync(accepted);
|
|
Assert.Equal(1, dispatched);
|
|
|
|
DefaultHttpContext limited = Context("198.51.100.11");
|
|
await middleware.InvokeAsync(limited);
|
|
Assert.Equal(StatusCodes.Status429TooManyRequests, limited.Response.StatusCode);
|
|
Assert.Equal("1", limited.Response.Headers.RetryAfter);
|
|
limited.Response.Body.Position = 0;
|
|
ApiError? error = await JsonSerializer.DeserializeAsync<ApiError>(
|
|
limited.Response.Body,
|
|
ContractJson.Options);
|
|
Assert.Equal(RendezvousErrorCode.RateLimited, error?.Code);
|
|
Assert.Equal(1, error?.RetryAfterSeconds);
|
|
Assert.Equal(1, dispatched);
|
|
|
|
DefaultHttpContext oversized = Context("203.0.113.1");
|
|
oversized.Request.ContentLength = ContractLimits.HttpRequestMaxBytes + 1;
|
|
await middleware.InvokeAsync(oversized);
|
|
Assert.Equal(StatusCodes.Status413PayloadTooLarge, oversized.Response.StatusCode);
|
|
Assert.Equal(1, dispatched);
|
|
|
|
DefaultHttpContext repeatedOversized = Context("203.0.113.2");
|
|
repeatedOversized.Request.ContentLength = ContractLimits.HttpRequestMaxBytes + 1;
|
|
await middleware.InvokeAsync(repeatedOversized);
|
|
Assert.Equal(StatusCodes.Status429TooManyRequests, repeatedOversized.Response.StatusCode);
|
|
Assert.Equal(1, dispatched);
|
|
}
|
|
|
|
[Fact]
|
|
public void DeterministicHostileUdpCorpusNeverThrowsOrAcceptsOversizedDatagrams()
|
|
{
|
|
const int seed = 0x15_2026;
|
|
Random random = new(seed);
|
|
for (int iteration = 0; iteration < 10_000; iteration++)
|
|
{
|
|
int length = random.Next(0, ContractLimits.UdpDatagramMaxBytes + 257);
|
|
byte[] payload = new byte[length];
|
|
random.NextBytes(payload);
|
|
|
|
bool decoded = RendezvousUdpCodec.TryDecode(
|
|
payload,
|
|
out PresenceDatagram? datagram,
|
|
out UdpDecodeError error);
|
|
if (length > ContractLimits.UdpDatagramMaxBytes)
|
|
{
|
|
Assert.False(decoded);
|
|
Assert.Null(datagram);
|
|
Assert.Equal(UdpDecodeError.DatagramTooLarge, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ConcurrentAbusiveBurstStaysBoundedAndCannotBlockCriticalHttp()
|
|
{
|
|
AbuseProtectionOptions options = PermissiveOptions();
|
|
options.MaxTrackedKeys = 2_000;
|
|
options.UdpTrackedKeyLimit = 1_000;
|
|
options.CriticalTrackedKeyReserve = 100;
|
|
options.UdpGlobalDatagramsPerWindow = 100_000;
|
|
options.UdpIpPrefixDatagramsPerWindow = 100_000;
|
|
options.UdpOperationDatagramsPerWindow = 100_000;
|
|
AbuseProtectionService protection = new(Options.Create(options));
|
|
IPAddress source = IPAddress.Parse("198.51.100.10");
|
|
|
|
Parallel.For(0, 20_000, index =>
|
|
{
|
|
_ = protection.TryAcceptUdpIngress(source, "raw");
|
|
_ = protection.TryAcceptUdpIdentity(
|
|
"Client",
|
|
$"capability-{index}",
|
|
$"resource-{index}");
|
|
});
|
|
|
|
Assert.InRange(protection.TrackedKeyCount, 1, options.UdpTrackedKeyLimit);
|
|
Assert.True(protection.TryAcquireHttpIngress(
|
|
IPAddress.Parse("203.0.113.10"),
|
|
"RenewSessionLease",
|
|
out var lease,
|
|
out _));
|
|
lease!.Dispose();
|
|
Assert.InRange(protection.TrackedKeyCount, 1, options.MaxTrackedKeys);
|
|
}
|
|
|
|
[Fact]
|
|
public void SteadyStateUdpAdmissionHasABoundedAllocationBudget()
|
|
{
|
|
AbuseProtectionOptions options = PermissiveOptions();
|
|
options.UdpGlobalDatagramsPerWindow = 100_000;
|
|
options.UdpIpPrefixDatagramsPerWindow = 100_000;
|
|
options.UdpOperationDatagramsPerWindow = 100_000;
|
|
options.UdpCapabilityDatagramsPerWindow = 100_000;
|
|
options.UdpResourceDatagramsPerWindow = 100_000;
|
|
AbuseProtectionService protection = new(Options.Create(options));
|
|
IPAddress source = IPAddress.Parse("198.51.100.10");
|
|
_ = protection.TryAcceptUdpIngress(source, "frozen");
|
|
_ = protection.TryAcceptUdpIdentity("Host", source, "capability", "resource");
|
|
|
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
|
for (int iteration = 0; iteration < 10_000; iteration++)
|
|
{
|
|
Assert.True(protection.TryAcceptUdpIngress(source, "frozen"));
|
|
Assert.True(protection.TryAcceptUdpIdentity(
|
|
"Host", source, "capability", "resource"));
|
|
}
|
|
|
|
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
|
Assert.InRange(allocated, 0, 40_000_000);
|
|
}
|
|
|
|
[Fact]
|
|
public void DeterministicHttpAndCredentialParserCorpusHasOnlyTypedRejections()
|
|
{
|
|
const int seed = 0x15_4A50;
|
|
Random random = new(seed);
|
|
for (int iteration = 0; iteration < 5_000; iteration++)
|
|
{
|
|
byte[] bytes = new byte[random.Next(0, 1_025)];
|
|
random.NextBytes(bytes);
|
|
try
|
|
{
|
|
_ = JsonSerializer.Deserialize<RegisterSessionRequest>(bytes, ContractJson.Options);
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
}
|
|
|
|
string token = Convert.ToBase64String(bytes);
|
|
Assert.False(NatPunchRequestTokenCodec.TryDecode(token, out _));
|
|
Assert.False(NatIntroductionTokenCodec.TryDecode(token, out _));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void SecretFingerprintsAreStableBoundedAndDoNotContainHostileInput()
|
|
{
|
|
const string hostile = "<script>steal('token')</script>\r\nAuthorization: secret";
|
|
string fingerprint = AbuseProtectionService.FingerprintSecret(hostile);
|
|
|
|
Assert.Equal(fingerprint, AbuseProtectionService.FingerprintSecret(hostile));
|
|
Assert.Equal(24, fingerprint.Length);
|
|
Assert.DoesNotContain("script", fingerprint, StringComparison.OrdinalIgnoreCase);
|
|
Assert.DoesNotContain("secret", fingerprint, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExceptionResponsesPreservePayloadStatusWithoutEchoingHostileDetails()
|
|
{
|
|
const string canary = "credential-canary <script> endpoint=203.0.113.8:9000";
|
|
DefaultHttpContext context = Context("198.51.100.10");
|
|
RendezvousExceptionHandler handler = new(
|
|
NullLogger<RendezvousExceptionHandler>.Instance);
|
|
|
|
Assert.True(await handler.TryHandleAsync(
|
|
context,
|
|
new BadHttpRequestException(canary, StatusCodes.Status413PayloadTooLarge),
|
|
CancellationToken.None));
|
|
|
|
Assert.Equal(StatusCodes.Status413PayloadTooLarge, context.Response.StatusCode);
|
|
context.Response.Body.Position = 0;
|
|
using StreamReader reader = new(context.Response.Body);
|
|
string body = await reader.ReadToEndAsync();
|
|
Assert.DoesNotContain(canary, body, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("203.0.113.8", body, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("script", body, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ForwardedSourcesAreDefaultDenyExactProxyOnlyAndSingleHop()
|
|
{
|
|
AbuseProtectionOptions disabled = new();
|
|
Assert.False(TrustedProxyForwarding.IsEnabled(disabled));
|
|
|
|
AbuseProtectionOptions enabled = new()
|
|
{
|
|
TrustedProxyAddresses = ["192.0.2.10"],
|
|
};
|
|
Assert.True(TrustedProxyForwarding.IsEnabled(enabled));
|
|
ForwardedHeadersOptions forwarded = new();
|
|
TrustedProxyForwarding.Configure(forwarded, enabled);
|
|
ForwardedHeadersMiddleware middleware = new(
|
|
_ => Task.CompletedTask,
|
|
NullLoggerFactory.Instance,
|
|
Options.Create(forwarded));
|
|
|
|
DefaultHttpContext trusted = Context("192.0.2.10");
|
|
trusted.Request.Headers["X-Forwarded-For"] = "198.51.100.7";
|
|
await middleware.Invoke(trusted);
|
|
Assert.Equal(IPAddress.Parse("198.51.100.7"), trusted.Connection.RemoteIpAddress);
|
|
|
|
DefaultHttpContext untrusted = Context("192.0.2.11");
|
|
untrusted.Request.Headers["X-Forwarded-For"] = "198.51.100.8";
|
|
await middleware.Invoke(untrusted);
|
|
Assert.Equal(IPAddress.Parse("192.0.2.11"), untrusted.Connection.RemoteIpAddress);
|
|
|
|
DefaultHttpContext multiHop = Context("192.0.2.10");
|
|
multiHop.Request.Headers["X-Forwarded-For"] = "198.51.100.9, 203.0.113.9";
|
|
await middleware.Invoke(multiHop);
|
|
Assert.Equal(IPAddress.Parse("203.0.113.9"), multiHop.Connection.RemoteIpAddress);
|
|
}
|
|
|
|
private static DefaultHttpContext Context(string address)
|
|
{
|
|
DefaultHttpContext context = new();
|
|
context.Connection.RemoteIpAddress = IPAddress.Parse(address);
|
|
context.Response.Body = new MemoryStream();
|
|
return context;
|
|
}
|
|
|
|
private static void AssertAccepted(
|
|
AbuseProtectionService protection,
|
|
IPAddress address,
|
|
string operation)
|
|
{
|
|
Assert.True(protection.TryAcquireHttpIngress(
|
|
address, operation, out var lease, out _));
|
|
lease!.Dispose();
|
|
}
|
|
|
|
private static void AssertRejected(
|
|
AbuseProtectionService protection,
|
|
IPAddress address,
|
|
string operation) => Assert.False(protection.TryAcquireHttpIngress(
|
|
address, operation, out _, out _));
|
|
|
|
private static AbuseProtectionOptions PermissiveOptions() => new()
|
|
{
|
|
WindowSeconds = 1,
|
|
MaxTrackedKeys = 10_000,
|
|
CriticalTrackedKeyReserve = 0,
|
|
UdpTrackedKeyLimit = 5_000,
|
|
HealthGlobalRequestsPerWindow = 10_000,
|
|
HealthGlobalConcurrency = 10_000,
|
|
HealthIpPrefixRequestsPerWindow = 10_000,
|
|
HealthIpPrefixConcurrency = 1_000,
|
|
OperatorAllowedAddresses = ["198.51.100.10"],
|
|
OperatorGlobalRequestsPerWindow = 10_000,
|
|
OperatorGlobalConcurrency = 10_000,
|
|
OperatorIpPrefixRequestsPerWindow = 10_000,
|
|
OperatorIpPrefixConcurrency = 1_000,
|
|
HttpGlobalRequestsPerWindow = 10_000,
|
|
HttpOptionalRequestsPerWindow = 9_000,
|
|
HttpIpPrefixRequestsPerWindow = 10_000,
|
|
HttpOptionalIpPrefixRequestsPerWindow = 9_000,
|
|
HttpOperationRequestsPerWindow = 10_000,
|
|
HttpTenantRequestsPerWindow = 10_000,
|
|
HttpPrincipalRequestsPerWindow = 10_000,
|
|
HttpResourceRequestsPerWindow = 10_000,
|
|
HttpGlobalConcurrency = 10_000,
|
|
HttpOptionalConcurrency = 9_000,
|
|
HttpIpPrefixConcurrency = 10_000,
|
|
HttpOptionalIpPrefixConcurrency = 9_000,
|
|
HttpOperationConcurrency = 10_000,
|
|
HttpTenantConcurrency = 10_000,
|
|
HttpPrincipalConcurrency = 10_000,
|
|
HttpResourceConcurrency = 10_000,
|
|
UdpGlobalDatagramsPerWindow = 10_000,
|
|
UdpIpPrefixDatagramsPerWindow = 10_000,
|
|
UdpOperationDatagramsPerWindow = 10_000,
|
|
UdpCapabilityDatagramsPerWindow = 10_000,
|
|
UdpResourceDatagramsPerWindow = 10_000,
|
|
};
|
|
|
|
private sealed class ManualTimeProvider(DateTimeOffset utcNow) : TimeProvider
|
|
{
|
|
private DateTimeOffset _utcNow = utcNow;
|
|
|
|
public override DateTimeOffset GetUtcNow() => _utcNow;
|
|
|
|
public void Advance(TimeSpan duration) => _utcNow += duration;
|
|
}
|
|
}
|