Add support to trial Cloudflare TURN beta

This commit is contained in:
Chris Eager
2024-04-24 18:49:53 -05:00
committed by Chris Eager
parent 0986ce12e6
commit 4a28ab6317
14 changed files with 158 additions and 58 deletions

View File

@@ -57,7 +57,7 @@ import org.whispersystems.textsecuregcm.configuration.SpamFilterConfiguration;
import org.whispersystems.textsecuregcm.configuration.StripeConfiguration;
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
import org.whispersystems.textsecuregcm.configuration.TlsKeyStoreConfiguration;
import org.whispersystems.textsecuregcm.configuration.TurnSecretConfiguration;
import org.whispersystems.textsecuregcm.configuration.TurnConfiguration;
import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration;
import org.whispersystems.textsecuregcm.configuration.VirtualThreadConfiguration;
import org.whispersystems.textsecuregcm.configuration.ZkConfig;
@@ -288,7 +288,7 @@ public class WhisperServerConfiguration extends Configuration {
@Valid
@NotNull
@JsonProperty
private TurnSecretConfiguration turn;
private TurnConfiguration turn;
@Valid
@NotNull
@@ -529,7 +529,7 @@ public class WhisperServerConfiguration extends Configuration {
return registrationService;
}
public TurnSecretConfiguration getTurnSecretConfiguration() {
public TurnConfiguration getTurnConfiguration() {
return turn;
}

View File

@@ -605,7 +605,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
pushLatencyManager);
final ReceiptSender receiptSender = new ReceiptSender(accountsManager, messageSender, receiptSenderExecutor);
final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager,
config.getTurnSecretConfiguration().secret().value());
config.getTurnConfiguration().secret().value(), config.getTurnConfiguration().cloudflare());
final CardinalityEstimator messageByteLimitCardinalityEstimator = new CardinalityEstimator(
rateLimitersCluster,
@@ -938,7 +938,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
new AttachmentControllerV4(rateLimiters, gcsAttachmentGenerator, tusAttachmentGenerator,
experimentEnrollmentManager),
new ArchiveController(backupAuthManager, backupManager),
new CallRoutingController(rateLimiters, callRouter, turnTokenGenerator),
new CallRoutingController(rateLimiters, callRouter, turnTokenGenerator, experimentEnrollmentManager),
new CallLinkController(rateLimiters, callingGenericZkSecretParams),
new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().certificate().value(),
config.getDeliveryCertificate().ecPrivateKey(), config.getDeliveryCertificate().expiresDays()),

View File

@@ -5,9 +5,11 @@
package org.whispersystems.textsecuregcm.auth;
import javax.annotation.Nullable;
import java.util.List;
public record TurnToken(String username, String password, List<String> urls, List<String> urlsWithIps, String hostname) {
public record TurnToken(String username, String password, List<String> urls, @Nullable List<String> urlsWithIps,
@Nullable String hostname) {
public TurnToken(String username, String password, List<String> urls) {
this(username, password, urls, null, null);
}

View File

@@ -5,17 +5,6 @@
package org.whispersystems.textsecuregcm.auth;
import org.whispersystems.textsecuregcm.calls.routing.TurnServerOptions;
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.Util;
import org.whispersystems.textsecuregcm.util.WeightedRandomSelect;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
@@ -25,6 +14,17 @@ import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.whispersystems.textsecuregcm.calls.routing.TurnServerOptions;
import org.whispersystems.textsecuregcm.configuration.CloudflareTurnConfiguration;
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.Util;
import org.whispersystems.textsecuregcm.util.WeightedRandomSelect;
public class TurnTokenGenerator {
@@ -38,13 +38,22 @@ public class TurnTokenGenerator {
private static final String WithIpsProtocol = "01";
private final String cloudflareTurnUsername;
private final String cloudflareTurnPassword;
private final List<String> cloudflareTurnUrls;
public TurnTokenGenerator(final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
final byte[] turnSecret) {
final byte[] turnSecret, final CloudflareTurnConfiguration cloudflareTurnConfiguration) {
this.dynamicConfigurationManager = dynamicConfigurationManager;
this.turnSecret = turnSecret;
this.cloudflareTurnUsername = cloudflareTurnConfiguration.username().value();
this.cloudflareTurnPassword = cloudflareTurnConfiguration.password().value();
this.cloudflareTurnUrls = cloudflareTurnConfiguration.urls();
}
@Deprecated
public TurnToken generate(final UUID aci) {
return generateToken(null, null, urls(aci));
}
@@ -53,6 +62,10 @@ public class TurnTokenGenerator {
return generateToken(options.hostname(), options.urlsWithIps(), options.urlsWithHostname());
}
public TurnToken generateForCloudflareBeta() {
return new TurnToken(cloudflareTurnUsername, cloudflareTurnPassword, cloudflareTurnUrls);
}
private TurnToken generateToken(String hostname, List<String> urlsWithIps, List<String> urlsWithHostname) {
try {
final Mac mac = Mac.getInstance(ALGORITHM);

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
public record CloudflareTurnConfiguration(@NotNull SecretString username, @NotNull SecretString password,
@Valid @NotNull List<@NotBlank String> urls) {
}

View File

@@ -7,5 +7,5 @@ package org.whispersystems.textsecuregcm.configuration;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
public record TurnSecretConfiguration(SecretBytes secret) {
public record TurnConfiguration(SecretBytes secret, CloudflareTurnConfiguration cloudflare) {
}

View File

@@ -95,7 +95,8 @@ public class AccountController {
this.usernameHashZkProofVerifier = usernameHashZkProofVerifier;
}
@Deprecated
// may be removed after 2024-07-16
@Deprecated(forRemoval = true)
@GET
@Path("/turn/")
@Produces(MediaType.APPLICATION_JSON)

View File

@@ -1,11 +1,12 @@
package org.whispersystems.textsecuregcm.controllers;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Metrics;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Optional;
@@ -21,14 +22,13 @@ import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.TurnToken;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.calls.routing.TurnServerOptions;
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.websocket.auth.ReadOnly;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
@Path("/v1/calling")
@io.swagger.v3.oas.annotations.tags.Tag(name = "Calling")
public class CallRoutingController {
@@ -39,15 +39,18 @@ public class CallRoutingController {
private final RateLimiters rateLimiters;
private final TurnCallRouter turnCallRouter;
private final TurnTokenGenerator tokenGenerator;
private final ExperimentEnrollmentManager experimentEnrollmentManager;
public CallRoutingController(
final RateLimiters rateLimiters,
final TurnCallRouter turnCallRouter,
final TurnTokenGenerator tokenGenerator
final TurnTokenGenerator tokenGenerator,
final ExperimentEnrollmentManager experimentEnrollmentManager
) {
this.rateLimiters = rateLimiters;
this.turnCallRouter = turnCallRouter;
this.tokenGenerator = tokenGenerator;
this.experimentEnrollmentManager = experimentEnrollmentManager;
}
@GET
@@ -63,7 +66,7 @@ public class CallRoutingController {
@ApiResponse(responseCode = "400", description = "Invalid get call endpoint request.")
@ApiResponse(responseCode = "401", description = "Account authentication check failed.")
@ApiResponse(responseCode = "422", description = "Invalid request format.")
@ApiResponse(responseCode = "429", description = "Ratelimited.")
@ApiResponse(responseCode = "429", description = "Rate limited.")
public TurnToken getCallingRelays(
final @ReadOnly @Auth AuthenticatedAccount auth,
@Context ContainerRequestContext requestContext
@@ -71,6 +74,10 @@ public class CallRoutingController {
UUID aci = auth.getAccount().getUuid();
rateLimiters.getCallEndpointLimiter().validate(aci);
if (experimentEnrollmentManager.isEnrolled(aci, "cloudflareTurn")) {
return tokenGenerator.generateForCloudflareBeta();
}
Optional<InetAddress> address = Optional.empty();
try {
final String remoteAddress = (String) requestContext.getProperty(