From 7e616a4056ac318caabef210a91666add2f64340 Mon Sep 17 00:00:00 2001 From: adel-signal Date: Fri, 24 Jan 2025 10:46:32 -0800 Subject: [PATCH] Update calling routing to return urls only, no instance IPs --- .../calls/routing/DynamicConfigTurnRouter.java | 5 +++++ .../calls/routing/TurnCallRouter.java | 13 ++++++++----- .../dynamic/DynamicTurnConfiguration.java | 10 ++++++++++ .../controllers/CallRoutingControllerV2.java | 16 ++++++++++------ .../calls/routing/TurnCallRouterTest.java | 17 ++++++++++++++++- .../dynamic/DynamicConfigurationTest.java | 17 +++++++++++++++++ .../CallRoutingControllerV2Test.java | 8 ++------ 7 files changed, 68 insertions(+), 18 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/calls/routing/DynamicConfigTurnRouter.java b/service/src/main/java/org/whispersystems/textsecuregcm/calls/routing/DynamicConfigTurnRouter.java index b8223067b..d579ae486 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/calls/routing/DynamicConfigTurnRouter.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/calls/routing/DynamicConfigTurnRouter.java @@ -62,6 +62,11 @@ public class DynamicConfigTurnRouter { return turnConfig.getRandomizeRate(); } + public int getDefaultInstanceIpCount() { + final DynamicTurnConfiguration turnConfig = dynamicConfigurationManager.getConfiguration().getTurnConfiguration(); + return turnConfig.getDefaultInstanceIpCount(); + } + public boolean shouldRandomize() { long rate = getRandomizeRate(); return rate >= RANDOMIZE_RATE_BASIS || rng.nextLong(0, DynamicConfigTurnRouter.RANDOMIZE_RATE_BASIS) < rate; diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/calls/routing/TurnCallRouter.java b/service/src/main/java/org/whispersystems/textsecuregcm/calls/routing/TurnCallRouter.java index f5a9422b7..91a98c606 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/calls/routing/TurnCallRouter.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/calls/routing/TurnCallRouter.java @@ -52,6 +52,13 @@ public class TurnCallRouter { this.stableSelect = stableSelect; } + public TurnServerOptions getRoutingFor( + @Nonnull final UUID aci, + @Nonnull final Optional clientAddress + ) { + return getRoutingFor(aci, clientAddress, this.configTurnRouter.getDefaultInstanceIpCount()); + } + /** * Gets Turn Instance addresses. Returns both the IPv4 and IPv6 addresses. Prefers to match the IP protocol of the * client address in datacenter selection. Returns 2 instance options of the preferred protocol for every one instance @@ -79,10 +86,6 @@ public class TurnCallRouter { @Nonnull final Optional clientAddress, final int instanceLimit ) { - if (instanceLimit < 1) { - throw new IllegalArgumentException("Limit cannot be less than one"); - } - String hostname = this.configTurnRouter.getHostname(); List targetedUrls = this.configTurnRouter.targetedUrls(aci); @@ -90,7 +93,7 @@ public class TurnCallRouter { return new TurnServerOptions(hostname, null, targetedUrls); } - if(clientAddress.isEmpty() || this.configTurnRouter.shouldRandomize()) { + if(clientAddress.isEmpty() || this.configTurnRouter.shouldRandomize() || instanceLimit < 1) { return new TurnServerOptions(hostname, null, this.configTurnRouter.randomUrls()); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicTurnConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicTurnConfiguration.java index a0032791b..678883ab4 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicTurnConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicTurnConfiguration.java @@ -23,6 +23,12 @@ public class DynamicTurnConfiguration { @JsonProperty private long randomizeRate = 5_000; + /** + * Number of instance ips to return in TURN routing request + */ + @JsonProperty + private int defaultInstanceIpCount = 0; + @JsonProperty private List<@Valid TurnUriConfiguration> uriConfigs = Collections.emptyList(); @@ -34,6 +40,10 @@ public class DynamicTurnConfiguration { return randomizeRate; } + public int getDefaultInstanceIpCount() { + return defaultInstanceIpCount; + } + public String getHostname() { return hostname; } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerV2.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerV2.java index 15326f3f0..f3f325895 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerV2.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerV2.java @@ -18,7 +18,6 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; -import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; @@ -40,8 +39,8 @@ import org.whispersystems.websocket.auth.ReadOnly; @Path("/v2/calling") public class CallRoutingControllerV2 { - private static final int TURN_INSTANCE_LIMIT = 2; private static final Counter INVALID_IP_COUNTER = Metrics.counter(name(CallRoutingControllerV2.class, "invalidIP")); + private static final Counter CLOUDFLARE_TURN_ERROR_COUNTER = Metrics.counter(name(CallRoutingController.class, "cloudflareTurnError")); private final RateLimiters rateLimiters; private final TurnCallRouter turnCallRouter; private final TurnTokenGenerator tokenGenerator; @@ -79,13 +78,18 @@ public class CallRoutingControllerV2 { public GetCallingRelaysResponse getCallingRelays( final @ReadOnly @Auth AuthenticatedDevice auth, @Context ContainerRequestContext requestContext - ) throws RateLimitExceededException, IOException { + ) throws RateLimitExceededException { UUID aci = auth.getAccount().getUuid(); rateLimiters.getCallEndpointLimiter().validate(aci); List tokens = new ArrayList<>(); - if (experimentEnrollmentManager.isEnrolled(auth.getAccount().getNumber(), aci, "cloudflareTurn")) { - tokens.add(cloudflareTurnCredentialsManager.retrieveFromCloudflare()); + try { + if (experimentEnrollmentManager.isEnrolled(auth.getAccount().getNumber(), aci, "cloudflareTurn")) { + tokens.add(cloudflareTurnCredentialsManager.retrieveFromCloudflare()); + } + } catch (Exception e) { + // emit counter, rely on Signal URL fallback + CallRoutingControllerV2.CLOUDFLARE_TURN_ERROR_COUNTER.increment(); } Optional address = Optional.empty(); @@ -97,7 +101,7 @@ public class CallRoutingControllerV2 { INVALID_IP_COUNTER.increment(); } - TurnServerOptions options = turnCallRouter.getRoutingFor(aci, address, TURN_INSTANCE_LIMIT); + TurnServerOptions options = turnCallRouter.getRoutingFor(aci, address); tokens.add(tokenGenerator.generateWithTurnServerOptions(options)); return new GetCallingRelaysResponse(tokens); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/calls/routing/TurnCallRouterTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/calls/routing/TurnCallRouterTest.java index ab71087d3..58d5676cb 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/calls/routing/TurnCallRouterTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/calls/routing/TurnCallRouterTest.java @@ -117,7 +117,7 @@ public class TurnCallRouterTest { () -> callDnsRecords, () -> performanceTable, () -> manualTable, - configTurnRouter, + configTurnRouter, () -> geoIp, // set to true so the return values are predictable true @@ -162,6 +162,21 @@ public class TurnCallRouterTest { )); } + @Test + public void testUrlsOnlyNoInstanceIps() throws UnknownHostException { + when(performanceTable.getDatacentersFor(any(), any(), any(), any())) + .thenReturn(List.of("dc-performance2", "dc-performance1")); + when(configTurnRouter.shouldRandomize()) + .thenReturn(false); + + assertThat(router().getRoutingFor(aci, Optional.of(InetAddress.getByName("0.0.0.1")), 0)) + .isEqualTo(new TurnServerOptions( + TEST_HOSTNAME, + null, + TEST_URLS_WITH_HOSTS + )); + } + @Test public void testOrderedByPerformance() throws UnknownHostException { when(performanceTable.getDatacentersFor(any(), any(), any(), any())) diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java index 5342415cf..ac7b7eb70 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java @@ -376,6 +376,23 @@ class DynamicConfigurationTest { assertThat(turnConfiguration.getHostname()).isEqualTo("test.domain.org"); assertThat(turnConfiguration.getRandomizeRate()).isEqualTo(100_000L); + assertThat(turnConfiguration.getDefaultInstanceIpCount()).isEqualTo(0); + } + + { + final String config = REQUIRED_CONFIG.concat(""" + turn: + uriConfigs: + - uris: + - turn:test0.org + - turn:test1.org + defaultInstanceIpCount: 5 + """); + DynamicTurnConfiguration turnConfiguration = DynamicConfigurationManager + .parseConfiguration(config, DynamicConfiguration.class) + .orElseThrow() + .getTurnConfiguration(); + assertThat(turnConfiguration.getDefaultInstanceIpCount()).isEqualTo(5); } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerV2Test.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerV2Test.java index 261bea77a..dc0e410a1 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerV2Test.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/CallRoutingControllerV2Test.java @@ -34,12 +34,10 @@ 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.configuration.dynamic.DynamicConfiguration; import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; -import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.util.SystemMapper; import org.whispersystems.textsecuregcm.util.TestRemoteAddressFilterProvider; @@ -96,8 +94,7 @@ class CallRoutingControllerV2Test { try { when(turnCallRouter.getRoutingFor( eq(AuthHelper.VALID_UUID), - eq(Optional.of(InetAddress.getByName(REMOTE_ADDRESS))), - anyInt()) + eq(Optional.of(InetAddress.getByName(REMOTE_ADDRESS)))) ).thenReturn(options); } catch (UnknownHostException ignored) { } @@ -177,8 +174,7 @@ class CallRoutingControllerV2Test { when(turnCallRouter.getRoutingFor( eq(AuthHelper.VALID_UUID), - eq(Optional.of(InetAddress.getByName(REMOTE_ADDRESS))), - anyInt()) + eq(Optional.of(InetAddress.getByName(REMOTE_ADDRESS)))) ).thenReturn(options); try (Response rawResponse = resources.getJerseyTest() .target(GET_CALL_RELAYS_PATH)