remove performance based turn routing from CallRoutingControllerV2

This commit is contained in:
Adel Lahlou 2025-02-27 12:17:06 -08:00 committed by Jon Chambers
parent b248b6bc12
commit 886984861f
3 changed files with 12 additions and 189 deletions

View File

@ -94,10 +94,6 @@ import org.whispersystems.textsecuregcm.backup.BackupsDb;
import org.whispersystems.textsecuregcm.backup.Cdn3BackupCredentialGenerator; import org.whispersystems.textsecuregcm.backup.Cdn3BackupCredentialGenerator;
import org.whispersystems.textsecuregcm.backup.Cdn3RemoteStorageManager; import org.whispersystems.textsecuregcm.backup.Cdn3RemoteStorageManager;
import org.whispersystems.textsecuregcm.badges.ConfiguredProfileBadgeConverter; import org.whispersystems.textsecuregcm.badges.ConfiguredProfileBadgeConverter;
import org.whispersystems.textsecuregcm.calls.routing.CallDnsRecordsManager;
import org.whispersystems.textsecuregcm.calls.routing.CallRoutingTableManager;
import org.whispersystems.textsecuregcm.calls.routing.DynamicConfigTurnRouter;
import org.whispersystems.textsecuregcm.calls.routing.TurnCallRouter;
import org.whispersystems.textsecuregcm.captcha.CaptchaChecker; import org.whispersystems.textsecuregcm.captcha.CaptchaChecker;
import org.whispersystems.textsecuregcm.captcha.CaptchaClient; import org.whispersystems.textsecuregcm.captcha.CaptchaClient;
import org.whispersystems.textsecuregcm.captcha.RegistrationCaptchaManager; import org.whispersystems.textsecuregcm.captcha.RegistrationCaptchaManager;
@ -142,7 +138,6 @@ import org.whispersystems.textsecuregcm.filters.RemoteDeprecationFilter;
import org.whispersystems.textsecuregcm.filters.RequestStatisticsFilter; import org.whispersystems.textsecuregcm.filters.RequestStatisticsFilter;
import org.whispersystems.textsecuregcm.filters.RestDeprecationFilter; import org.whispersystems.textsecuregcm.filters.RestDeprecationFilter;
import org.whispersystems.textsecuregcm.filters.TimestampResponseFilter; import org.whispersystems.textsecuregcm.filters.TimestampResponseFilter;
import org.whispersystems.textsecuregcm.geo.MaxMindDatabaseManager;
import org.whispersystems.textsecuregcm.grpc.AccountsAnonymousGrpcService; import org.whispersystems.textsecuregcm.grpc.AccountsAnonymousGrpcService;
import org.whispersystems.textsecuregcm.grpc.AccountsGrpcService; import org.whispersystems.textsecuregcm.grpc.AccountsGrpcService;
import org.whispersystems.textsecuregcm.grpc.ErrorMappingInterceptor; import org.whispersystems.textsecuregcm.grpc.ErrorMappingInterceptor;
@ -813,45 +808,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
config.getAppleDeviceCheck().teamId(), config.getAppleDeviceCheck().teamId(),
config.getAppleDeviceCheck().bundleId()); config.getAppleDeviceCheck().bundleId());
final DynamicConfigTurnRouter configTurnRouter = new DynamicConfigTurnRouter(dynamicConfigurationManager);
MaxMindDatabaseManager geoIpCityDatabaseManager = new MaxMindDatabaseManager(
recurringConfigSyncExecutor,
awsCredentialsProvider,
config.getMaxmindCityDatabase(),
"city"
);
environment.lifecycle().manage(geoIpCityDatabaseManager);
CallDnsRecordsManager callDnsRecordsManager = new CallDnsRecordsManager(
recurringConfigSyncExecutor,
awsCredentialsProvider,
config.getCallingTurnDnsRecords()
);
environment.lifecycle().manage(callDnsRecordsManager);
CallRoutingTableManager callRoutingTableManager = new CallRoutingTableManager(
recurringConfigSyncExecutor,
awsCredentialsProvider,
config.getCallingTurnPerformanceTable(),
"Performance"
);
environment.lifecycle().manage(callRoutingTableManager);
CallRoutingTableManager manualCallRoutingTableManager = new CallRoutingTableManager(
recurringConfigSyncExecutor,
awsCredentialsProvider,
config.getCallingTurnManualTable(),
"Manual"
);
environment.lifecycle().manage(manualCallRoutingTableManager);
TurnCallRouter callRouter = new TurnCallRouter(
callDnsRecordsManager,
callRoutingTableManager,
manualCallRoutingTableManager,
configTurnRouter,
geoIpCityDatabaseManager,
false
);
final GrpcClientConnectionManager grpcClientConnectionManager = new GrpcClientConnectionManager(); final GrpcClientConnectionManager grpcClientConnectionManager = new GrpcClientConnectionManager();
disconnectionRequestManager.addListener(grpcClientConnectionManager); disconnectionRequestManager.addListener(grpcClientConnectionManager);
@ -1117,7 +1073,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
new AttachmentControllerV4(rateLimiters, gcsAttachmentGenerator, tusAttachmentGenerator, new AttachmentControllerV4(rateLimiters, gcsAttachmentGenerator, tusAttachmentGenerator,
experimentEnrollmentManager), experimentEnrollmentManager),
new ArchiveController(backupAuthManager, backupManager), new ArchiveController(backupAuthManager, backupManager),
new CallRoutingControllerV2(rateLimiters, callRouter, turnTokenGenerator, experimentEnrollmentManager, cloudflareTurnCredentialsManager), new CallRoutingControllerV2(rateLimiters, cloudflareTurnCredentialsManager),
new CallLinkController(rateLimiters, callingGenericZkSecretParams), new CallLinkController(rateLimiters, callingGenericZkSecretParams),
new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().certificate().value(), new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().certificate().value(),
config.getDeliveryCertificate().ecPrivateKey(), config.getDeliveryCertificate().expiresDays()), config.getDeliveryCertificate().ecPrivateKey(), config.getDeliveryCertificate().expiresDays()),

View File

@ -18,20 +18,13 @@ import jakarta.ws.rs.Produces;
import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import java.net.InetAddress; import java.io.IOException;
import java.net.UnknownHostException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice; import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.auth.CloudflareTurnCredentialsManager; import org.whispersystems.textsecuregcm.auth.CloudflareTurnCredentialsManager;
import org.whispersystems.textsecuregcm.auth.TurnToken; import org.whispersystems.textsecuregcm.auth.TurnToken;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.calls.routing.TurnCallRouter;
import org.whispersystems.textsecuregcm.calls.routing.TurnServerOptions;
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.filters.RemoteAddressFilter;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.websocket.auth.ReadOnly; import org.whispersystems.websocket.auth.ReadOnly;
@ -39,25 +32,15 @@ import org.whispersystems.websocket.auth.ReadOnly;
@Path("/v2/calling") @Path("/v2/calling")
public class CallRoutingControllerV2 { public class CallRoutingControllerV2 {
private static final Counter INVALID_IP_COUNTER = Metrics.counter(name(CallRoutingControllerV2.class, "invalidIP"));
private static final Counter CLOUDFLARE_TURN_ERROR_COUNTER = Metrics.counter(name(CallRoutingControllerV2.class, "cloudflareTurnError")); private static final Counter CLOUDFLARE_TURN_ERROR_COUNTER = Metrics.counter(name(CallRoutingControllerV2.class, "cloudflareTurnError"));
private final RateLimiters rateLimiters; private final RateLimiters rateLimiters;
private final TurnCallRouter turnCallRouter;
private final TurnTokenGenerator tokenGenerator;
private final ExperimentEnrollmentManager experimentEnrollmentManager;
private final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager; private final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager;
public CallRoutingControllerV2( public CallRoutingControllerV2(
final RateLimiters rateLimiters, final RateLimiters rateLimiters,
final TurnCallRouter turnCallRouter,
final TurnTokenGenerator tokenGenerator,
final ExperimentEnrollmentManager experimentEnrollmentManager,
final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager
) { ) {
this.rateLimiters = rateLimiters; this.rateLimiters = rateLimiters;
this.turnCallRouter = turnCallRouter;
this.tokenGenerator = tokenGenerator;
this.experimentEnrollmentManager = experimentEnrollmentManager;
this.cloudflareTurnCredentialsManager = cloudflareTurnCredentialsManager; this.cloudflareTurnCredentialsManager = cloudflareTurnCredentialsManager;
} }
@ -76,34 +59,19 @@ public class CallRoutingControllerV2 {
@ApiResponse(responseCode = "422", description = "Invalid request format.") @ApiResponse(responseCode = "422", description = "Invalid request format.")
@ApiResponse(responseCode = "429", description = "Rate limited.") @ApiResponse(responseCode = "429", description = "Rate limited.")
public GetCallingRelaysResponse getCallingRelays( public GetCallingRelaysResponse getCallingRelays(
final @ReadOnly @Auth AuthenticatedDevice auth, final @ReadOnly @Auth AuthenticatedDevice auth
@Context ContainerRequestContext requestContext ) throws RateLimitExceededException, IOException {
) throws RateLimitExceededException {
UUID aci = auth.getAccount().getUuid(); UUID aci = auth.getAccount().getUuid();
rateLimiters.getCallEndpointLimiter().validate(aci); rateLimiters.getCallEndpointLimiter().validate(aci);
List<TurnToken> tokens = new ArrayList<>(); List<TurnToken> tokens = new ArrayList<>();
try { try {
if (experimentEnrollmentManager.isEnrolled(auth.getAccount().getNumber(), aci, "cloudflareTurn")) {
tokens.add(cloudflareTurnCredentialsManager.retrieveFromCloudflare()); tokens.add(cloudflareTurnCredentialsManager.retrieveFromCloudflare());
}
} catch (Exception e) { } catch (Exception e) {
// emit counter, rely on Signal URL fallback
CallRoutingControllerV2.CLOUDFLARE_TURN_ERROR_COUNTER.increment(); CallRoutingControllerV2.CLOUDFLARE_TURN_ERROR_COUNTER.increment();
throw e;
} }
Optional<InetAddress> address = Optional.empty();
try {
final String remoteAddress = (String) requestContext.getProperty(
RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
address = Optional.of(InetAddress.getByName(remoteAddress));
} catch (UnknownHostException e) {
INVALID_IP_COUNTER.increment();
}
TurnServerOptions options = turnCallRouter.getRoutingFor(aci, address);
tokens.add(tokenGenerator.generateWithTurnServerOptions(options));
return new GetCallingRelaysResponse(tokens); return new GetCallingRelaysResponse(tokens);
} }

View File

@ -6,8 +6,6 @@
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset; import static org.mockito.Mockito.reset;
@ -18,11 +16,7 @@ import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
import io.dropwizard.testing.junit5.ResourceExtension; import io.dropwizard.testing.junit5.ResourceExtension;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -31,10 +25,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice; import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.auth.CloudflareTurnCredentialsManager; import org.whispersystems.textsecuregcm.auth.CloudflareTurnCredentialsManager;
import org.whispersystems.textsecuregcm.auth.TurnToken; import org.whispersystems.textsecuregcm.auth.TurnToken;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.calls.routing.TurnCallRouter;
import org.whispersystems.textsecuregcm.calls.routing.TurnServerOptions;
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
@ -47,11 +37,6 @@ class CallRoutingControllerV2Test {
private static final String GET_CALL_RELAYS_PATH = "v2/calling/relays"; private static final String GET_CALL_RELAYS_PATH = "v2/calling/relays";
private static final String REMOTE_ADDRESS = "123.123.123.1"; private static final String REMOTE_ADDRESS = "123.123.123.1";
private static final TurnServerOptions TURN_SERVER_OPTIONS = new TurnServerOptions(
"example.domain.org",
Optional.of(List.of("stun:12.34.56.78")),
Optional.of(List.of("stun:example.domain.org"))
);
private static final TurnToken CLOUDFLARE_TURN_TOKEN = new TurnToken( private static final TurnToken CLOUDFLARE_TURN_TOKEN = new TurnToken(
"ABC", "ABC",
"XYZ", "XYZ",
@ -61,12 +46,8 @@ class CallRoutingControllerV2Test {
private static final RateLimiters rateLimiters = mock(RateLimiters.class); private static final RateLimiters rateLimiters = mock(RateLimiters.class);
private static final RateLimiter getCallEndpointLimiter = mock(RateLimiter.class); private static final RateLimiter getCallEndpointLimiter = mock(RateLimiter.class);
private static final ExperimentEnrollmentManager experimentEnrollmentManager = mock(
ExperimentEnrollmentManager.class);
private static final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator("bloop".getBytes(StandardCharsets.UTF_8));
private static final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager = mock( private static final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager = mock(
CloudflareTurnCredentialsManager.class); CloudflareTurnCredentialsManager.class);
private static final TurnCallRouter turnCallRouter = mock(TurnCallRouter.class);
private static final ResourceExtension resources = ResourceExtension.builder() private static final ResourceExtension resources = ResourceExtension.builder()
.addProvider(AuthHelper.getAuthFilter()) .addProvider(AuthHelper.getAuthFilter())
@ -75,8 +56,7 @@ class CallRoutingControllerV2Test {
.addProvider(new TestRemoteAddressFilterProvider(REMOTE_ADDRESS)) .addProvider(new TestRemoteAddressFilterProvider(REMOTE_ADDRESS))
.setMapper(SystemMapper.jsonMapper()) .setMapper(SystemMapper.jsonMapper())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory()) .setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new CallRoutingControllerV2(rateLimiters, turnCallRouter, turnTokenGenerator, .addResource(new CallRoutingControllerV2(rateLimiters, cloudflareTurnCredentialsManager))
experimentEnrollmentManager, cloudflareTurnCredentialsManager))
.build(); .build();
@BeforeEach @BeforeEach
@ -86,58 +66,19 @@ class CallRoutingControllerV2Test {
@AfterEach @AfterEach
void tearDown() { void tearDown() {
reset(experimentEnrollmentManager, rateLimiters, getCallEndpointLimiter, turnCallRouter); reset( rateLimiters, getCallEndpointLimiter);
} }
void initializeMocksWith(Optional<TurnServerOptions> signalTurn, Optional<TurnToken> cloudflare) { void initializeMocksWith(TurnToken cloudflareToken) {
signalTurn.ifPresent(options -> {
try { try {
when(turnCallRouter.getRoutingFor( when(cloudflareTurnCredentialsManager.retrieveFromCloudflare()).thenReturn(cloudflareToken);
eq(AuthHelper.VALID_UUID),
eq(Optional.of(InetAddress.getByName(REMOTE_ADDRESS))))
).thenReturn(options);
} catch (UnknownHostException ignored) {
}
});
cloudflare.ifPresent(token -> {
when(experimentEnrollmentManager.isEnrolled(AuthHelper.VALID_NUMBER, AuthHelper.VALID_UUID, "cloudflareTurn"))
.thenReturn(true);
try {
when(cloudflareTurnCredentialsManager.retrieveFromCloudflare()).thenReturn(token);
} catch (IOException ignored) { } catch (IOException ignored) {
} }
});
}
@Test
void testGetRelaysSignalRoutingOnly() {
TurnServerOptions options = TURN_SERVER_OPTIONS;
initializeMocksWith(Optional.of(options), Optional.empty());
try (Response rawResponse = resources.getJerseyTest()
.target(GET_CALL_RELAYS_PATH)
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get()) {
assertThat(rawResponse.getStatus()).isEqualTo(200);
CallRoutingControllerV2.GetCallingRelaysResponse response = rawResponse.readEntity(
CallRoutingControllerV2.GetCallingRelaysResponse.class);
List<TurnToken> relays = response.relays();
assertThat(relays).hasSize(1);
assertThat(relays.getFirst().username()).isNotEmpty();
assertThat(relays.getFirst().password()).isNotEmpty();
assertThat(relays.getFirst().hostname()).isEqualTo(options.hostname());
assertThat(relays.getFirst().urlsWithIps()).isEqualTo(options.urlsWithIps().get());
assertThat(relays.getFirst().urls()).isEqualTo(options.urlsWithHostname().get());
}
} }
@Test @Test
void testGetRelaysBothRouting() { void testGetRelaysBothRouting() {
TurnServerOptions options = TURN_SERVER_OPTIONS; initializeMocksWith(CLOUDFLARE_TURN_TOKEN);
initializeMocksWith(Optional.of(options), Optional.of(CLOUDFLARE_TURN_TOKEN));
try (Response rawResponse = resources.getJerseyTest() try (Response rawResponse = resources.getJerseyTest()
.target(GET_CALL_RELAYS_PATH) .target(GET_CALL_RELAYS_PATH)
@ -151,49 +92,7 @@ class CallRoutingControllerV2Test {
CallRoutingControllerV2.GetCallingRelaysResponse.class); CallRoutingControllerV2.GetCallingRelaysResponse.class);
List<TurnToken> relays = response.relays(); List<TurnToken> relays = response.relays();
assertThat(relays).hasSize(2); assertThat(relays).isEqualTo(List.of(CLOUDFLARE_TURN_TOKEN));
assertThat(relays.getFirst()).isEqualTo(CLOUDFLARE_TURN_TOKEN);
TurnToken token = relays.get(1);
assertThat(token.username()).isNotEmpty();
assertThat(token.password()).isNotEmpty();
assertThat(token.hostname()).isEqualTo(options.hostname());
assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps().get());
assertThat(token.urls()).isEqualTo(options.urlsWithHostname().get());
}
}
@Test
void testGetRelaysInvalidIpSuccess() throws UnknownHostException {
TurnServerOptions options = new TurnServerOptions(
"example.domain.org",
Optional.of(List.of()),
Optional.of(List.of("stun:example.domain.org"))
);
when(turnCallRouter.getRoutingFor(
eq(AuthHelper.VALID_UUID),
eq(Optional.of(InetAddress.getByName(REMOTE_ADDRESS))))
).thenReturn(options);
try (Response rawResponse = resources.getJerseyTest()
.target(GET_CALL_RELAYS_PATH)
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get()) {
assertThat(rawResponse.getStatus()).isEqualTo(200);
CallRoutingControllerV2.GetCallingRelaysResponse response = rawResponse.readEntity(
CallRoutingControllerV2.GetCallingRelaysResponse.class
);
assertThat(response.relays()).hasSize(1);
TurnToken token = response.relays().getFirst();
assertThat(token.username()).isNotEmpty();
assertThat(token.password()).isNotEmpty();
assertThat(token.hostname()).isEqualTo(options.hostname());
assertThat(token.urlsWithIps()).isEqualTo(options.urlsWithIps().get());
assertThat(token.urls()).isEqualTo(options.urlsWithHostname().get());
} }
} }