mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 19:58:03 +01:00
Introduce a hyper-log-log-based cardinality rate limiter
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.limits;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.redis.AbstractRedisClusterTest;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class CardinalityRateLimiterTest extends AbstractRedisClusterTest {
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidate() {
|
||||
final int maxCardinality = 10;
|
||||
final CardinalityRateLimiter rateLimiter = new CardinalityRateLimiter(getRedisCluster(), "test", Duration.ofDays(1), Duration.ofDays(1), maxCardinality);
|
||||
|
||||
final String source = "+18005551234";
|
||||
int validatedAttempts = 0;
|
||||
int blockedAttempts = 0;
|
||||
|
||||
for (int i = 0; i < maxCardinality * 2; i++) {
|
||||
try {
|
||||
rateLimiter.validate(source, String.valueOf(i));
|
||||
validatedAttempts++;
|
||||
} catch (final RateLimitExceededException e) {
|
||||
blockedAttempts++;
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(validatedAttempts >= maxCardinality);
|
||||
assertTrue(blockedAttempts > 0);
|
||||
|
||||
final String secondSource = "+18005554321";
|
||||
|
||||
try {
|
||||
rateLimiter.validate(secondSource, "test");
|
||||
} catch (final RateLimitExceededException e) {
|
||||
fail("New source should not trigger a rate limit exception on first attempted validation");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.StaleDevices;
|
||||
import org.whispersystems.textsecuregcm.limits.CardinalityRateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
||||
@@ -84,7 +85,7 @@ public class MessageControllerTest {
|
||||
private final MessagesManager messagesManager = mock(MessagesManager.class);
|
||||
private final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||
private final RateLimiter rateLimiter = mock(RateLimiter.class);
|
||||
private final RateLimiter unsealedSenderLimiter = mock(RateLimiter.class);
|
||||
private final CardinalityRateLimiter unsealedSenderLimiter = mock(CardinalityRateLimiter.class);
|
||||
private final ApnFallbackManager apnFallbackManager = mock(ApnFallbackManager.class);
|
||||
private final FeatureFlagsManager featureFlagsManager = mock(FeatureFlagsManager.class);
|
||||
|
||||
|
||||
@@ -5,11 +5,14 @@ import org.junit.Test;
|
||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRateLimitsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.limits.CardinalityRateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -34,11 +37,11 @@ public class DynamicRateLimitsTest {
|
||||
public void testUnchangingConfiguration() {
|
||||
RateLimiters rateLimiters = new RateLimiters(new RateLimitsConfiguration(), dynamicConfig, redisCluster);
|
||||
|
||||
RateLimiter limiter = rateLimiters.getUnsealedSenderLimiter();
|
||||
RateLimiter limiter = rateLimiters.getUnsealedIpLimiter();
|
||||
|
||||
assertThat(limiter.getBucketSize()).isEqualTo(dynamicConfig.getConfiguration().getLimits().getUnsealedSenderNumber().getBucketSize());
|
||||
assertThat(limiter.getLeakRatePerMinute()).isEqualTo(dynamicConfig.getConfiguration().getLimits().getUnsealedSenderNumber().getLeakRatePerMinute());
|
||||
assertSame(rateLimiters.getUnsealedSenderLimiter(), limiter);
|
||||
assertThat(limiter.getBucketSize()).isEqualTo(dynamicConfig.getConfiguration().getLimits().getUnsealedSenderIp().getBucketSize());
|
||||
assertThat(limiter.getLeakRatePerMinute()).isEqualTo(dynamicConfig.getConfiguration().getLimits().getUnsealedSenderIp().getLeakRatePerMinute());
|
||||
assertSame(rateLimiters.getUnsealedIpLimiter(), limiter);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -47,25 +50,27 @@ public class DynamicRateLimitsTest {
|
||||
DynamicRateLimitsConfiguration limitsConfiguration = mock(DynamicRateLimitsConfiguration.class);
|
||||
|
||||
when(configuration.getLimits()).thenReturn(limitsConfiguration);
|
||||
when(limitsConfiguration.getUnsealedSenderNumber()).thenReturn(new RateLimitsConfiguration.RateLimitConfiguration(1, 2.0));
|
||||
when(limitsConfiguration.getUnsealedSenderNumber()).thenReturn(new RateLimitsConfiguration.CardinalityRateLimitConfiguration(10, Duration.ofHours(1), Duration.ofMinutes(10)));
|
||||
when(limitsConfiguration.getUnsealedSenderIp()).thenReturn(new RateLimitsConfiguration.RateLimitConfiguration(4, 1.0));
|
||||
|
||||
when(dynamicConfig.getConfiguration()).thenReturn(configuration);
|
||||
|
||||
RateLimiters rateLimiters = new RateLimiters(new RateLimitsConfiguration(), dynamicConfig, redisCluster);
|
||||
|
||||
RateLimiter limiter = rateLimiters.getUnsealedSenderLimiter();
|
||||
CardinalityRateLimiter limiter = rateLimiters.getUnsealedSenderLimiter();
|
||||
|
||||
assertThat(limiter.getBucketSize()).isEqualTo(1);
|
||||
assertThat(limiter.getLeakRatePerMinute()).isEqualTo(2.0);
|
||||
assertThat(limiter.getMaxCardinality()).isEqualTo(10);
|
||||
assertThat(limiter.getTtl()).isEqualTo(Duration.ofHours(1));
|
||||
assertThat(limiter.getTtlJitter()).isEqualTo(Duration.ofMinutes(10));
|
||||
assertSame(rateLimiters.getUnsealedSenderLimiter(), limiter);
|
||||
|
||||
when(limitsConfiguration.getUnsealedSenderNumber()).thenReturn(new RateLimitsConfiguration.RateLimitConfiguration(2, 3.0));
|
||||
when(limitsConfiguration.getUnsealedSenderNumber()).thenReturn(new RateLimitsConfiguration.CardinalityRateLimitConfiguration(20, Duration.ofHours(2), Duration.ofMinutes(7)));
|
||||
|
||||
RateLimiter changed = rateLimiters.getUnsealedSenderLimiter();
|
||||
CardinalityRateLimiter changed = rateLimiters.getUnsealedSenderLimiter();
|
||||
|
||||
assertThat(changed.getBucketSize()).isEqualTo(2);
|
||||
assertThat(changed.getLeakRatePerMinute()).isEqualTo(3.0);
|
||||
assertThat(changed.getMaxCardinality()).isEqualTo(20);
|
||||
assertThat(changed.getTtl()).isEqualTo(Duration.ofHours(2));
|
||||
assertThat(changed.getTtlJitter()).isEqualTo(Duration.ofMinutes(7));
|
||||
assertNotSame(limiter, changed);
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user