Make TURN configuration dynamic

Also enables conditionally including more TURN servers for gradual
rollouts
This commit is contained in:
Ravi Khadiwala
2022-03-22 10:37:20 -05:00
committed by ravi-signal
parent 8541360bf3
commit c70d7535b9
14 changed files with 391 additions and 57 deletions

View File

@@ -43,7 +43,6 @@ import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfig
import org.whispersystems.textsecuregcm.configuration.StripeConfiguration;
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
import org.whispersystems.textsecuregcm.configuration.TurnConfiguration;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration;
import org.whispersystems.textsecuregcm.configuration.VoiceVerificationConfiguration;
@@ -168,11 +167,6 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private WebSocketConfiguration webSocket = new WebSocketConfiguration();
@Valid
@NotNull
@JsonProperty
private TurnConfiguration turn;
@Valid
@NotNull
@JsonProperty
@@ -345,10 +339,6 @@ public class WhisperServerConfiguration extends Configuration {
return limits;
}
public TurnConfiguration getTurnConfiguration() {
return turn;
}
public GcmConfiguration getGcmConfiguration() {
return gcm;
}

View File

@@ -484,7 +484,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
SmsSender smsSender = new SmsSender(twilioSmsSender);
MessageSender messageSender = new MessageSender(apnFallbackManager, clientPresenceManager, messagesManager, gcmSender, apnSender, pushLatencyManager);
ReceiptSender receiptSender = new ReceiptSender(accountsManager, messageSender, receiptSenderExecutor);
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(config.getTurnConfiguration());
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager);
RecaptchaClient recaptchaClient = new RecaptchaClient(
config.getRecaptchaConfiguration().getProjectPath(),
config.getRecaptchaConfiguration().getCredentialConfigurationJson(),

View File

@@ -6,6 +6,7 @@
package org.whispersystems.textsecuregcm.auth;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import java.util.List;
@@ -25,4 +26,9 @@ public class TurnToken {
this.password = password;
this.urls = urls;
}
@VisibleForTesting
List<String> getUrls() {
return urls;
}
}

View File

@@ -5,7 +5,12 @@
package org.whispersystems.textsecuregcm.auth;
import org.whispersystems.textsecuregcm.configuration.TurnConfiguration;
import org.whispersystems.textsecuregcm.configuration.TurnUriConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicTurnConfiguration;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.WeightedRandomSelect;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@@ -14,20 +19,21 @@ import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
public class TurnTokenGenerator {
private final byte[] key;
private final List<String> urls;
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfiguration;
public TurnTokenGenerator(TurnConfiguration configuration) {
this.key = configuration.getSecret().getBytes();
this.urls = configuration.getUris();
public TurnTokenGenerator(final DynamicConfigurationManager<DynamicConfiguration> config) {
this.dynamicConfiguration = config;
}
public TurnToken generate() {
public TurnToken generate(final String e164) {
try {
byte[] key = dynamicConfiguration.getConfiguration().getTurnConfiguration().getSecret().getBytes();
List<String> urls = urls(e164);
Mac mac = Mac.getInstance("HmacSHA1");
long validUntilSeconds = (System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1)) / 1000;
long user = Math.abs(new SecureRandom().nextInt());
@@ -41,4 +47,22 @@ public class TurnTokenGenerator {
throw new AssertionError(e);
}
}
private List<String> urls(final String e164) {
final DynamicTurnConfiguration turnConfig = dynamicConfiguration.getConfiguration().getTurnConfiguration();
// Check if number is enrolled to test out specific turn servers
final Optional<TurnUriConfiguration> enrolled = turnConfig.getUriConfigs().stream()
.filter(config -> config.getEnrolledNumbers().contains(e164))
.findFirst();
if (enrolled.isPresent()) {
return enrolled.get().getUris();
}
// Otherwise, select from turn server sets by weighted choice
return WeightedRandomSelect.select(turnConfig
.getUriConfigs()
.stream()
.map(c -> new Pair<List<String>, Long>(c.getUris(), c.getWeight())).toList());
}
}

View File

@@ -1,30 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public class TurnConfiguration {
@JsonProperty
@NotEmpty
private String secret;
@JsonProperty
@NotNull
private List<String> uris;
public List<String> getUris() {
return uris;
}
public String getSecret() {
return secret;
}
}

View File

@@ -0,0 +1,38 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.util.Collections;
import java.util.List;
import java.util.Set;
public class TurnUriConfiguration {
@JsonProperty
@NotNull
private List<String> uris;
/**
* The weight of this entry for weighted random selection
*/
@JsonProperty
@Min(0)
private long weight = 1;
/**
* Enrolled numbers will always get this uri list
*/
private Set<String> enrolledNumbers = Collections.emptySet();
public List<String> getUris() {
return uris;
}
public long getWeight() {
return weight;
}
public Set<String> getEnrolledNumbers() {
return Collections.unmodifiableSet(enrolledNumbers);
}
}

View File

@@ -56,6 +56,10 @@ public class DynamicConfiguration {
@Valid
private DynamicUakMigrationConfiguration uakMigrationConfiguration = new DynamicUakMigrationConfiguration();
@JsonProperty
@Valid
private DynamicTurnConfiguration turn = new DynamicTurnConfiguration();
public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(
final String experimentName) {
return Optional.ofNullable(experiments.get(experimentName));
@@ -109,4 +113,8 @@ public class DynamicConfiguration {
public DynamicUakMigrationConfiguration getUakMigrationConfiguration() { return uakMigrationConfiguration; }
public DynamicTurnConfiguration getTurnConfiguration() {
return turn;
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.dynamic;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.configuration.TurnUriConfiguration;
import java.util.Collections;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public class DynamicTurnConfiguration {
@JsonProperty
private String secret;
@JsonProperty
private List<@Valid TurnUriConfiguration> uriConfigs = Collections.emptyList();
public List<TurnUriConfiguration> getUriConfigs() {
return uriConfigs;
}
public String getSecret() {
return secret;
}
}

View File

@@ -449,7 +449,7 @@ public class AccountController {
@Produces(MediaType.APPLICATION_JSON)
public TurnToken getTurnToken(@Auth AuthenticatedAccount auth) throws RateLimitExceededException {
rateLimiters.getTurnLimiter().validate(auth.getAccount().getUuid());
return turnTokenGenerator.generate();
return turnTokenGenerator.generate(auth.getAccount().getNumber());
}
@Timed

View File

@@ -0,0 +1,54 @@
package org.whispersystems.textsecuregcm.util;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
/**
* Select a random item according to its weight
*
* @param <T> the type of the objects to select from
*/
public class WeightedRandomSelect<T> {
List<Pair<T, Long>> weightedItems;
long totalWeight;
public WeightedRandomSelect(List<Pair<T, Long>> weightedItems) throws IllegalArgumentException {
this.weightedItems = weightedItems;
this.totalWeight = weightedItems.stream().mapToLong(Pair::second).sum();
weightedItems.stream().map(Pair::second).filter(w -> w < 0).findFirst().ifPresent(invalid -> {
throw new IllegalArgumentException("Illegal selection weight " + invalid);
});
if (weightedItems.isEmpty() || totalWeight == 0) {
throw new IllegalArgumentException("Cannot create an empty weighted random selector");
}
}
public T select() {
if (weightedItems.size() == 1) {
return weightedItems.get(0).first();
}
long select = ThreadLocalRandom.current().nextLong(0, totalWeight);
long current = 0;
for (Pair<T, Long> item : weightedItems) {
/*
Accumulate weights for each item and select the first item whose
cumulative weight exceeds the selected value. nextLong() is exclusive,
so by the last item we're guaranteed to find a value as the
last item's weight is one more than the maximum value of select.
*/
current += item.second();
if (current > select) {
return item.first();
}
}
throw new IllegalStateException("totalWeight " + totalWeight + " exceeds item weights");
}
public static <T> T select(List<Pair<T, Long>> weightedItems) {
return new WeightedRandomSelect<T>(weightedItems).select();
}
}