Remove obsolete turn implementations
This commit is contained in:
parent
50e298a4f4
commit
82e21b0c21
|
@ -94,7 +94,6 @@ currentReportingKey.salt: AAAAAAAAAAA=
|
||||||
|
|
||||||
registrationService.collationKeySalt: AAAAAAAAAAA=
|
registrationService.collationKeySalt: AAAAAAAAAAA=
|
||||||
|
|
||||||
turn.secret: AAAAAAAAAAA=
|
|
||||||
turn.cloudflare.apiToken: ABCDEFGHIJKLM
|
turn.cloudflare.apiToken: ABCDEFGHIJKLM
|
||||||
|
|
||||||
linkDevice.secret: AAAAAAAAAAA=
|
linkDevice.secret: AAAAAAAAAAA=
|
||||||
|
|
|
@ -473,7 +473,6 @@ keyTransparencyService:
|
||||||
clientPrivateKey: secret://keyTransparencyService.clientPrivateKey
|
clientPrivateKey: secret://keyTransparencyService.clientPrivateKey
|
||||||
|
|
||||||
turn:
|
turn:
|
||||||
secret: secret://turn.secret
|
|
||||||
cloudflare:
|
cloudflare:
|
||||||
apiToken: secret://turn.cloudflare.apiToken
|
apiToken: secret://turn.cloudflare.apiToken
|
||||||
endpoint: https://rtc.live.cloudflare.com/v1/turn/keys/LMNOP/credentials/generate
|
endpoint: https://rtc.live.cloudflare.com/v1/turn/keys/LMNOP/credentials/generate
|
||||||
|
@ -490,30 +489,6 @@ turn:
|
||||||
linkDevice:
|
linkDevice:
|
||||||
secret: secret://linkDevice.secret
|
secret: secret://linkDevice.secret
|
||||||
|
|
||||||
maxmindCityDatabase:
|
|
||||||
s3Region: a-region
|
|
||||||
s3Bucket: a-bucket
|
|
||||||
objectKey: an-object.tar.gz
|
|
||||||
maxSize: 32777216
|
|
||||||
|
|
||||||
callingTurnDnsRecords:
|
|
||||||
s3Region: a-region
|
|
||||||
s3Bucket: a-bucket
|
|
||||||
objectKey: an-object.tar.gz
|
|
||||||
maxSize: 32777216
|
|
||||||
|
|
||||||
callingTurnPerformanceTable:
|
|
||||||
s3Region: a-region
|
|
||||||
s3Bucket: a-bucket
|
|
||||||
objectKey: an-object.tar.gz
|
|
||||||
maxSize: 32777216
|
|
||||||
|
|
||||||
callingTurnManualTable:
|
|
||||||
s3Region: a-region
|
|
||||||
s3Bucket: a-bucket
|
|
||||||
objectKey: an-object.tar.gz
|
|
||||||
maxSize: 32777216
|
|
||||||
|
|
||||||
noiseTunnel:
|
noiseTunnel:
|
||||||
port: 8443
|
port: 8443
|
||||||
tlsKeyStoreFile: /path/to/file.p12
|
tlsKeyStoreFile: /path/to/file.p12
|
||||||
|
|
|
@ -232,12 +232,6 @@
|
||||||
<version>1.27.1</version>
|
<version>1.27.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.maxmind.geoip2</groupId>
|
|
||||||
<artifactId>geoip2</artifactId>
|
|
||||||
<version>4.2.1</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.cloud</groupId>
|
<groupId>com.google.cloud</groupId>
|
||||||
<artifactId>google-cloud-pubsub</artifactId>
|
<artifactId>google-cloud-pubsub</artifactId>
|
||||||
|
|
|
@ -301,27 +301,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private VirtualThreadConfiguration virtualThread = new VirtualThreadConfiguration(Duration.ofMillis(1));
|
private VirtualThreadConfiguration virtualThread = new VirtualThreadConfiguration(Duration.ofMillis(1));
|
||||||
|
|
||||||
|
|
||||||
@Valid
|
|
||||||
@NotNull
|
|
||||||
@JsonProperty
|
|
||||||
private S3ObjectMonitorFactory maxmindCityDatabase;
|
|
||||||
|
|
||||||
@Valid
|
|
||||||
@NotNull
|
|
||||||
@JsonProperty
|
|
||||||
private S3ObjectMonitorFactory callingTurnDnsRecords;
|
|
||||||
|
|
||||||
@Valid
|
|
||||||
@NotNull
|
|
||||||
@JsonProperty
|
|
||||||
private S3ObjectMonitorFactory callingTurnPerformanceTable;
|
|
||||||
|
|
||||||
@Valid
|
|
||||||
@NotNull
|
|
||||||
@JsonProperty
|
|
||||||
private S3ObjectMonitorFactory callingTurnManualTable;
|
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
@NotNull
|
@NotNull
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
|
@ -539,22 +518,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
return virtualThread;
|
return virtualThread;
|
||||||
}
|
}
|
||||||
|
|
||||||
public S3ObjectMonitorFactory getMaxmindCityDatabase() {
|
|
||||||
return maxmindCityDatabase;
|
|
||||||
}
|
|
||||||
|
|
||||||
public S3ObjectMonitorFactory getCallingTurnDnsRecords() {
|
|
||||||
return callingTurnDnsRecords;
|
|
||||||
}
|
|
||||||
|
|
||||||
public S3ObjectMonitorFactory getCallingTurnPerformanceTable() {
|
|
||||||
return callingTurnPerformanceTable;
|
|
||||||
}
|
|
||||||
|
|
||||||
public S3ObjectMonitorFactory getCallingTurnManualTable() {
|
|
||||||
return callingTurnManualTable;
|
|
||||||
}
|
|
||||||
|
|
||||||
public NoiseWebSocketTunnelConfiguration getNoiseWebSocketTunnelConfiguration() {
|
public NoiseWebSocketTunnelConfiguration getNoiseWebSocketTunnelConfiguration() {
|
||||||
return noiseTunnel;
|
return noiseTunnel;
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,6 @@ import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator
|
||||||
import org.whispersystems.textsecuregcm.auth.IdlePrimaryDeviceAuthenticatedWebSocketUpgradeFilter;
|
import org.whispersystems.textsecuregcm.auth.IdlePrimaryDeviceAuthenticatedWebSocketUpgradeFilter;
|
||||||
import org.whispersystems.textsecuregcm.auth.PhoneVerificationTokenManager;
|
import org.whispersystems.textsecuregcm.auth.PhoneVerificationTokenManager;
|
||||||
import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager;
|
import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager;
|
||||||
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
|
||||||
import org.whispersystems.textsecuregcm.auth.WebsocketRefreshApplicationEventListener;
|
import org.whispersystems.textsecuregcm.auth.WebsocketRefreshApplicationEventListener;
|
||||||
import org.whispersystems.textsecuregcm.auth.grpc.ProhibitAuthenticationInterceptor;
|
import org.whispersystems.textsecuregcm.auth.grpc.ProhibitAuthenticationInterceptor;
|
||||||
import org.whispersystems.textsecuregcm.auth.grpc.RequireAuthenticationInterceptor;
|
import org.whispersystems.textsecuregcm.auth.grpc.RequireAuthenticationInterceptor;
|
||||||
|
@ -667,7 +666,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
|
|
||||||
final MessageSender messageSender = new MessageSender(messagesManager, pushNotificationManager);
|
final MessageSender messageSender = new MessageSender(messagesManager, pushNotificationManager);
|
||||||
final ReceiptSender receiptSender = new ReceiptSender(accountsManager, messageSender, receiptSenderExecutor);
|
final ReceiptSender receiptSender = new ReceiptSender(accountsManager, messageSender, receiptSenderExecutor);
|
||||||
final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(config.getTurnConfiguration().secret().value());
|
|
||||||
final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager = new CloudflareTurnCredentialsManager(
|
final CloudflareTurnCredentialsManager cloudflareTurnCredentialsManager = new CloudflareTurnCredentialsManager(
|
||||||
config.getTurnConfiguration().cloudflare().apiToken().value(),
|
config.getTurnConfiguration().cloudflare().apiToken().value(),
|
||||||
config.getTurnConfiguration().cloudflare().endpoint(),
|
config.getTurnConfiguration().cloudflare().endpoint(),
|
||||||
|
|
|
@ -15,8 +15,8 @@ import java.net.Inet6Address;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
@ -127,11 +127,11 @@ public class CloudflareTurnCredentialsManager {
|
||||||
final CloudflareTurnResponse cloudflareTurnResponse = SystemMapper.jsonMapper()
|
final CloudflareTurnResponse cloudflareTurnResponse = SystemMapper.jsonMapper()
|
||||||
.readValue(response.body(), CloudflareTurnResponse.class);
|
.readValue(response.body(), CloudflareTurnResponse.class);
|
||||||
|
|
||||||
return TurnTokenGenerator.from(
|
return new TurnToken(
|
||||||
cloudflareTurnResponse.iceServers().username(),
|
cloudflareTurnResponse.iceServers().username(),
|
||||||
cloudflareTurnResponse.iceServers().credential(),
|
cloudflareTurnResponse.iceServers().credential(),
|
||||||
Optional.ofNullable(cloudflareTurnUrls),
|
cloudflareTurnUrls == null ? Collections.emptyList() : cloudflareTurnUrls,
|
||||||
Optional.ofNullable(cloudflareTurnComposedUrls),
|
cloudflareTurnComposedUrls,
|
||||||
cloudflareTurnHostname
|
cloudflareTurnHostname
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,87 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2013-2020 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.auth;
|
|
||||||
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import javax.crypto.Mac;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
import org.whispersystems.textsecuregcm.calls.routing.TurnServerOptions;
|
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
|
||||||
|
|
||||||
public class TurnTokenGenerator {
|
|
||||||
|
|
||||||
private final byte[] turnSecret;
|
|
||||||
|
|
||||||
private static final String ALGORITHM = "HmacSHA1";
|
|
||||||
|
|
||||||
private static final String WithUrlsProtocol = "00";
|
|
||||||
|
|
||||||
private static final String WithIpsProtocol = "01";
|
|
||||||
|
|
||||||
public TurnTokenGenerator(final byte[] turnSecret) {
|
|
||||||
this.turnSecret = turnSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TurnToken generateWithTurnServerOptions(TurnServerOptions options) {
|
|
||||||
return generateToken(options.hostname(), options.urlsWithIps(), options.urlsWithHostname());
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
|
||||||
private TurnToken generateToken(
|
|
||||||
String hostname,
|
|
||||||
Optional<List<String>> urlsWithIps,
|
|
||||||
Optional<List<String>> urlsWithHostname
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
final Mac mac = Mac.getInstance(ALGORITHM);
|
|
||||||
final long validUntilSeconds = Instant.now().plus(Duration.ofDays(1)).getEpochSecond();
|
|
||||||
final long user = Util.ensureNonNegativeInt(new SecureRandom().nextInt());
|
|
||||||
final String userTime = validUntilSeconds + ":" + user;
|
|
||||||
final String protocol = urlsWithIps.isEmpty() || urlsWithIps.get().isEmpty()
|
|
||||||
? WithUrlsProtocol
|
|
||||||
: WithIpsProtocol;
|
|
||||||
final String protocolUserTime = userTime + "#" + protocol;
|
|
||||||
|
|
||||||
mac.init(new SecretKeySpec(turnSecret, ALGORITHM));
|
|
||||||
final String password = Base64.getEncoder().encodeToString(mac.doFinal(protocolUserTime.getBytes()));
|
|
||||||
|
|
||||||
return from(
|
|
||||||
protocolUserTime,
|
|
||||||
password,
|
|
||||||
urlsWithHostname,
|
|
||||||
urlsWithIps,
|
|
||||||
hostname
|
|
||||||
);
|
|
||||||
} catch (final NoSuchAlgorithmException | InvalidKeyException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
|
||||||
public static TurnToken from(
|
|
||||||
String username,
|
|
||||||
String password,
|
|
||||||
Optional<List<String>> urls,
|
|
||||||
Optional<List<String>> urlsWithIps,
|
|
||||||
String hostname
|
|
||||||
) {
|
|
||||||
return new TurnToken(
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
urls.orElse(Collections.emptyList()),
|
|
||||||
urlsWithIps.orElse(Collections.emptyList()),
|
|
||||||
hostname
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.calls.routing;
|
|
||||||
|
|
||||||
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 java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Random;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
/** Uses DynamicConfig to help route a turn request */
|
|
||||||
public class DynamicConfigTurnRouter {
|
|
||||||
|
|
||||||
private static final Random rng = new Random();
|
|
||||||
|
|
||||||
public static final long RANDOMIZE_RATE_BASIS = 100_000;
|
|
||||||
|
|
||||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
|
||||||
|
|
||||||
public DynamicConfigTurnRouter(final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
|
|
||||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> targetedUrls(final UUID aci) {
|
|
||||||
final DynamicTurnConfiguration turnConfig = dynamicConfigurationManager.getConfiguration().getTurnConfiguration();
|
|
||||||
|
|
||||||
final Optional<TurnUriConfiguration> enrolled = turnConfig.getUriConfigs().stream()
|
|
||||||
.filter(config -> config.getEnrolledAcis().contains(aci))
|
|
||||||
.findFirst();
|
|
||||||
|
|
||||||
return enrolled
|
|
||||||
.map(turnUriConfiguration -> turnUriConfiguration.getUris().stream().toList())
|
|
||||||
.orElse(Collections.emptyList());
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> randomUrls() {
|
|
||||||
final DynamicTurnConfiguration turnConfig = dynamicConfigurationManager.getConfiguration().getTurnConfiguration();
|
|
||||||
|
|
||||||
// select from turn server sets by weighted choice
|
|
||||||
return WeightedRandomSelect.select(turnConfig
|
|
||||||
.getUriConfigs()
|
|
||||||
.stream()
|
|
||||||
.map(c -> new Pair<>(c.getUris(), c.getWeight())).toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getHostname() {
|
|
||||||
final DynamicTurnConfiguration turnConfig = dynamicConfigurationManager.getConfiguration().getTurnConfiguration();
|
|
||||||
return turnConfig.getHostname();
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getRandomizeRate() {
|
|
||||||
final DynamicTurnConfiguration turnConfig = dynamicConfigurationManager.getConfiguration().getTurnConfiguration();
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,190 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.calls.routing;
|
|
||||||
|
|
||||||
import com.maxmind.geoip2.DatabaseReader;
|
|
||||||
import com.maxmind.geoip2.exception.GeoIp2Exception;
|
|
||||||
import com.maxmind.geoip2.model.CityResponse;
|
|
||||||
import org.apache.commons.lang3.tuple.Triple;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns routes based on performance tables, manually routing tables, and target routing. Falls back to a random Turn
|
|
||||||
* instance that the server knows about.
|
|
||||||
*/
|
|
||||||
public class TurnCallRouter {
|
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(TurnCallRouter.class);
|
|
||||||
|
|
||||||
private final Supplier<CallDnsRecords> callDnsRecords;
|
|
||||||
private final Supplier<CallRoutingTable> performanceRouting;
|
|
||||||
private final Supplier<CallRoutingTable> manualRouting;
|
|
||||||
private final DynamicConfigTurnRouter configTurnRouter;
|
|
||||||
private final Supplier<DatabaseReader> geoIp;
|
|
||||||
// controls whether instance IPs are shuffled. using if & boolean is ~5x faster than a function pointer
|
|
||||||
private final boolean stableSelect;
|
|
||||||
|
|
||||||
public TurnCallRouter(
|
|
||||||
@Nonnull Supplier<CallDnsRecords> callDnsRecords,
|
|
||||||
@Nonnull Supplier<CallRoutingTable> performanceRouting,
|
|
||||||
@Nonnull Supplier<CallRoutingTable> manualRouting,
|
|
||||||
@Nonnull DynamicConfigTurnRouter configTurnRouter,
|
|
||||||
@Nonnull Supplier<DatabaseReader> geoIp,
|
|
||||||
boolean stableSelect
|
|
||||||
) {
|
|
||||||
this.performanceRouting = performanceRouting;
|
|
||||||
this.callDnsRecords = callDnsRecords;
|
|
||||||
this.manualRouting = manualRouting;
|
|
||||||
this.configTurnRouter = configTurnRouter;
|
|
||||||
this.geoIp = geoIp;
|
|
||||||
this.stableSelect = stableSelect;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TurnServerOptions getRoutingFor(
|
|
||||||
@Nonnull final UUID aci,
|
|
||||||
@Nonnull final Optional<InetAddress> 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
|
|
||||||
* of the other.
|
|
||||||
* @param aci aci of client
|
|
||||||
* @param clientAddress IP address to base routing on
|
|
||||||
* @param instanceLimit max instances to return options for
|
|
||||||
* @return Up to two * instanceLimit options, half in ipv4, half in ipv6
|
|
||||||
*/
|
|
||||||
public TurnServerOptions getRoutingFor(
|
|
||||||
@Nonnull final UUID aci,
|
|
||||||
@Nonnull final Optional<InetAddress> clientAddress,
|
|
||||||
final int instanceLimit
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
return getRoutingForInner(aci, clientAddress, instanceLimit);
|
|
||||||
} catch(Exception e) {
|
|
||||||
logger.error("Failed to perform routing", e);
|
|
||||||
return new TurnServerOptions(this.configTurnRouter.getHostname(), null, Optional.of(this.configTurnRouter.randomUrls()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TurnServerOptions getRoutingForInner(
|
|
||||||
@Nonnull final UUID aci,
|
|
||||||
@Nonnull final Optional<InetAddress> clientAddress,
|
|
||||||
final int instanceLimit
|
|
||||||
) {
|
|
||||||
String hostname = this.configTurnRouter.getHostname();
|
|
||||||
|
|
||||||
List<String> targetedUrls = this.configTurnRouter.targetedUrls(aci);
|
|
||||||
if(!targetedUrls.isEmpty()) {
|
|
||||||
return new TurnServerOptions(hostname, Optional.empty(), Optional.ofNullable(targetedUrls));
|
|
||||||
}
|
|
||||||
|
|
||||||
if(clientAddress.isEmpty() || this.configTurnRouter.shouldRandomize() || instanceLimit < 1) {
|
|
||||||
return new TurnServerOptions(hostname, Optional.empty(), Optional.ofNullable(this.configTurnRouter.randomUrls()));
|
|
||||||
}
|
|
||||||
|
|
||||||
CityResponse geoInfo;
|
|
||||||
try {
|
|
||||||
geoInfo = geoIp.get().city(clientAddress.get());
|
|
||||||
} catch (IOException | GeoIp2Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
Optional<String> subdivision = !geoInfo.getSubdivisions().isEmpty()
|
|
||||||
? Optional.of(geoInfo.getSubdivisions().getFirst().getIsoCode())
|
|
||||||
: Optional.empty();
|
|
||||||
|
|
||||||
List<String> datacenters = this.manualRouting.get().getDatacentersFor(
|
|
||||||
clientAddress.get(),
|
|
||||||
geoInfo.getContinent().getCode(),
|
|
||||||
geoInfo.getCountry().getIsoCode(),
|
|
||||||
subdivision
|
|
||||||
);
|
|
||||||
|
|
||||||
if (datacenters.isEmpty()){
|
|
||||||
datacenters = this.performanceRouting.get().getDatacentersFor(
|
|
||||||
clientAddress.get(),
|
|
||||||
geoInfo.getContinent().getCode(),
|
|
||||||
geoInfo.getCountry().getIsoCode(),
|
|
||||||
subdivision
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> urlsWithIps = getUrlsForInstances(
|
|
||||||
selectInstances(
|
|
||||||
datacenters,
|
|
||||||
instanceLimit
|
|
||||||
));
|
|
||||||
return new TurnServerOptions(hostname, Optional.of(urlsWithIps), Optional.of(minimalRandomUrls()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Includes only the udp options in the randomUrls
|
|
||||||
private List<String> minimalRandomUrls(){
|
|
||||||
return this.configTurnRouter.randomUrls().stream()
|
|
||||||
.filter(s -> s.startsWith("turn:") && !s.endsWith("transport=tcp"))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns balanced number of instances across provided datacenters, prioritizing the datacenters earlier in the list
|
|
||||||
private List<String> selectInstances(List<String> datacenters, int instanceLimit) {
|
|
||||||
if(datacenters.isEmpty() || instanceLimit == 0) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
CallDnsRecords dnsRecords = this.callDnsRecords.get();
|
|
||||||
List<List<InetAddress>> ipv4Options = datacenters.stream()
|
|
||||||
.map(dc -> randomNOf(dnsRecords.aByRegion().get(dc), instanceLimit, stableSelect))
|
|
||||||
.toList();
|
|
||||||
List<List<InetAddress>> ipv6Options = datacenters.stream()
|
|
||||||
.map(dc -> randomNOf(dnsRecords.aaaaByRegion().get(dc), instanceLimit, stableSelect))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
List<InetAddress> ipv4Selection = selectFromOptions(ipv4Options, instanceLimit);
|
|
||||||
List<InetAddress> ipv6Selection = selectFromOptions(ipv6Options, instanceLimit);
|
|
||||||
|
|
||||||
return Stream.concat(
|
|
||||||
ipv4Selection.stream().map(InetAddress::getHostAddress),
|
|
||||||
// map ipv6 to RFC3986 format i.e. surrounded by brackets
|
|
||||||
ipv6Selection.stream().map(i -> String.format("[%s]", i.getHostAddress()))
|
|
||||||
).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<InetAddress> selectFromOptions(List<List<InetAddress>> recordsByDc, int instanceLimit) {
|
|
||||||
return IntStream.range(0, recordsByDc.size())
|
|
||||||
.mapToObj(dcIndex -> IntStream.range(0, recordsByDc.get(dcIndex).size())
|
|
||||||
.mapToObj(addressIndex -> Triple.of(addressIndex, dcIndex, recordsByDc.get(dcIndex).get(addressIndex))))
|
|
||||||
.flatMap(i -> i)
|
|
||||||
.sorted(Comparator.comparingInt((Triple<Integer, Integer, InetAddress> t) -> t.getLeft())
|
|
||||||
.thenComparingInt(Triple::getMiddle))
|
|
||||||
.limit(instanceLimit)
|
|
||||||
.sorted(Comparator.comparingInt(Triple::getMiddle))
|
|
||||||
.map(Triple::getRight)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <E> List<E> randomNOf(List<E> values, int n, boolean stableSelect) {
|
|
||||||
return stableSelect ? Util.randomNOfStable(values, n) : Util.randomNOfShuffled(values, n);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<String> getUrlsForInstances(List<String> instanceIps) {
|
|
||||||
return instanceIps.stream().flatMap(ip -> Stream.of(
|
|
||||||
String.format("turn:%s", ip),
|
|
||||||
String.format("turn:%s:80?transport=tcp", ip),
|
|
||||||
String.format("turns:%s:443?transport=tcp", ip)
|
|
||||||
)
|
|
||||||
).toList();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.configuration;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import jakarta.validation.constraints.Positive;
|
import jakarta.validation.constraints.Positive;
|
||||||
|
@ -15,8 +16,8 @@ import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
||||||
public record CloudflareTurnConfiguration(@NotNull SecretString apiToken,
|
public record CloudflareTurnConfiguration(@NotNull SecretString apiToken,
|
||||||
@NotBlank String endpoint,
|
@NotBlank String endpoint,
|
||||||
@NotBlank long ttl,
|
@NotBlank long ttl,
|
||||||
@NotBlank List<String> urls,
|
@NotNull @NotEmpty @Valid List<@NotBlank String> urls,
|
||||||
@NotBlank List<String> urlsWithIps,
|
@NotNull @NotEmpty @Valid List<@NotBlank String> urlsWithIps,
|
||||||
@NotNull @Valid CircuitBreakerConfiguration circuitBreaker,
|
@NotNull @Valid CircuitBreakerConfiguration circuitBreaker,
|
||||||
@NotNull @Valid RetryConfiguration retry,
|
@NotNull @Valid RetryConfiguration retry,
|
||||||
@NotBlank String hostname,
|
@NotBlank String hostname,
|
||||||
|
|
|
@ -5,7 +5,5 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.configuration;
|
package org.whispersystems.textsecuregcm.configuration;
|
||||||
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
|
public record TurnConfiguration(CloudflareTurnConfiguration cloudflare) {
|
||||||
|
|
||||||
public record TurnConfiguration(SecretBytes secret, CloudflareTurnConfiguration cloudflare) {
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,10 +42,6 @@ public class DynamicConfiguration {
|
||||||
@Valid
|
@Valid
|
||||||
private DynamicCaptchaConfiguration captcha = new DynamicCaptchaConfiguration();
|
private DynamicCaptchaConfiguration captcha = new DynamicCaptchaConfiguration();
|
||||||
|
|
||||||
@JsonProperty
|
|
||||||
@Valid
|
|
||||||
private DynamicTurnConfiguration turn = new DynamicTurnConfiguration();
|
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
@Valid
|
@Valid
|
||||||
DynamicMessagePersisterConfiguration messagePersister = new DynamicMessagePersisterConfiguration();
|
DynamicMessagePersisterConfiguration messagePersister = new DynamicMessagePersisterConfiguration();
|
||||||
|
@ -104,10 +100,6 @@ public class DynamicConfiguration {
|
||||||
return captcha;
|
return captcha;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DynamicTurnConfiguration getTurnConfiguration() {
|
|
||||||
return turn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DynamicMessagePersisterConfiguration getMessagePersisterConfiguration() {
|
public DynamicMessagePersisterConfiguration getMessagePersisterConfiguration() {
|
||||||
return messagePersister;
|
return messagePersister;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 jakarta.validation.Valid;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.TurnUriConfiguration;
|
|
||||||
|
|
||||||
public class DynamicTurnConfiguration {
|
|
||||||
|
|
||||||
@JsonProperty
|
|
||||||
private String hostname;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rate at which to prioritize a random turn URL to exercise all endpoints.
|
|
||||||
* Based on a 100,000 basis, where 100,000 == 100%.
|
|
||||||
*/
|
|
||||||
@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();
|
|
||||||
|
|
||||||
public List<TurnUriConfiguration> getUriConfigs() {
|
|
||||||
return uriConfigs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getRandomizeRate() {
|
|
||||||
return randomizeRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDefaultInstanceIpCount() {
|
|
||||||
return defaultInstanceIpCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getHostname() {
|
|
||||||
return hostname;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.geo;
|
|
||||||
|
|
||||||
import com.maxmind.db.CHMCache;
|
|
||||||
import com.maxmind.geoip2.DatabaseReader;
|
|
||||||
import io.dropwizard.lifecycle.Managed;
|
|
||||||
import io.micrometer.core.instrument.Metrics;
|
|
||||||
import io.micrometer.core.instrument.Timer;
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
import org.apache.commons.compress.archivers.ArchiveEntry;
|
|
||||||
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
|
|
||||||
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.S3ObjectMonitorFactory;
|
|
||||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
|
||||||
import org.whispersystems.textsecuregcm.s3.S3ObjectMonitor;
|
|
||||||
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
|
||||||
|
|
||||||
public class MaxMindDatabaseManager implements Supplier<DatabaseReader>, Managed {
|
|
||||||
|
|
||||||
private final S3ObjectMonitor databaseMonitor;
|
|
||||||
|
|
||||||
private final AtomicReference<DatabaseReader> databaseReader = new AtomicReference<>();
|
|
||||||
|
|
||||||
private final String databaseTag;
|
|
||||||
|
|
||||||
private final Timer refreshTimer;
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(MaxMindDatabaseManager.class);
|
|
||||||
|
|
||||||
public MaxMindDatabaseManager(final ScheduledExecutorService executorService,
|
|
||||||
final AwsCredentialsProvider awsCredentialsProvider, final S3ObjectMonitorFactory configuration,
|
|
||||||
final String databaseTag) {
|
|
||||||
|
|
||||||
this.databaseMonitor = configuration.build(awsCredentialsProvider, executorService);
|
|
||||||
this.databaseTag = databaseTag;
|
|
||||||
this.refreshTimer = Metrics.timer(MetricsUtil.name(MaxMindDatabaseManager.class, "refresh"), "db", databaseTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleDatabaseChanged(final InputStream inputStream) {
|
|
||||||
refreshTimer.record(() -> {
|
|
||||||
boolean foundDatabaseEntry = false;
|
|
||||||
|
|
||||||
try (final InputStream bufferedInputStream = new BufferedInputStream(inputStream);
|
|
||||||
final GzipCompressorInputStream gzipInputStream = new GzipCompressorInputStream(bufferedInputStream);
|
|
||||||
final TarArchiveInputStream tarInputStream = new TarArchiveInputStream(gzipInputStream)) {
|
|
||||||
|
|
||||||
ArchiveEntry nextEntry;
|
|
||||||
|
|
||||||
while ((nextEntry = tarInputStream.getNextEntry()) != null) {
|
|
||||||
if (nextEntry.getName().toLowerCase().endsWith(".mmdb")) {
|
|
||||||
foundDatabaseEntry = true;
|
|
||||||
|
|
||||||
final DatabaseReader oldReader = databaseReader.getAndSet(
|
|
||||||
new DatabaseReader.Builder(tarInputStream).withCache(new CHMCache()).build()
|
|
||||||
);
|
|
||||||
if (oldReader != null) {
|
|
||||||
oldReader.close();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (final IOException e) {
|
|
||||||
log.error(String.format("Failed to load MaxMind database, tag %s", databaseTag));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundDatabaseEntry) {
|
|
||||||
log.warn(String.format("No .mmdb entry loaded from input stream, tag %s", databaseTag));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() throws Exception {
|
|
||||||
databaseMonitor.start(this::handleDatabaseChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() throws Exception {
|
|
||||||
databaseMonitor.stop();
|
|
||||||
|
|
||||||
final DatabaseReader reader = databaseReader.getAndSet(null);
|
|
||||||
if (reader != null) {
|
|
||||||
reader.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DatabaseReader get() {
|
|
||||||
return this.databaseReader.get();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.calls.routing;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
|
||||||
|
|
||||||
public class CallDnsRecordsManagerTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testParseDnsRecords() throws IOException {
|
|
||||||
var input = """
|
|
||||||
{
|
|
||||||
"aByRegion": {
|
|
||||||
"datacenter-1": [
|
|
||||||
"127.0.0.1"
|
|
||||||
],
|
|
||||||
"datacenter-2": [
|
|
||||||
"127.0.0.2",
|
|
||||||
"127.0.0.3"
|
|
||||||
],
|
|
||||||
"datacenter-3": [
|
|
||||||
"127.0.0.4",
|
|
||||||
"127.0.0.5"
|
|
||||||
],
|
|
||||||
"datacenter-4": [
|
|
||||||
"127.0.0.6",
|
|
||||||
"127.0.0.7"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"aaaaByRegion": {
|
|
||||||
"datacenter-1": [
|
|
||||||
"2600:1111:2222:3333:0:20:0:0",
|
|
||||||
"2600:1111:2222:3333:0:21:0:0",
|
|
||||||
"2600:1111:2222:3333:0:22:0:0"
|
|
||||||
],
|
|
||||||
"datacenter-2": [
|
|
||||||
"2600:1111:2222:3333:0:23:0:0",
|
|
||||||
"2600:1111:2222:3333:0:24:0:0"
|
|
||||||
],
|
|
||||||
"datacenter-3": [
|
|
||||||
"2600:1111:2222:3333:0:25:0:0",
|
|
||||||
"2600:1111:2222:3333:0:26:0:0"
|
|
||||||
],
|
|
||||||
"datacenter-4": [
|
|
||||||
"2600:1111:2222:3333:0:27:0:0"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var actual = CallDnsRecordsManager.parseRecords(new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)));
|
|
||||||
var expected = new CallDnsRecords(
|
|
||||||
Map.of(
|
|
||||||
"datacenter-1", Stream.of("127.0.0.1").map(this::getAddressByName).toList(),
|
|
||||||
"datacenter-2", Stream.of("127.0.0.2", "127.0.0.3").map(this::getAddressByName).toList(),
|
|
||||||
"datacenter-3", Stream.of("127.0.0.4", "127.0.0.5").map(this::getAddressByName).toList(),
|
|
||||||
"datacenter-4", Stream.of("127.0.0.6", "127.0.0.7").map(this::getAddressByName).toList()
|
|
||||||
),
|
|
||||||
Map.of(
|
|
||||||
"datacenter-1", Stream.of(
|
|
||||||
"2600:1111:2222:3333:0:20:0:0",
|
|
||||||
"2600:1111:2222:3333:0:21:0:0",
|
|
||||||
"2600:1111:2222:3333:0:22:0:0"
|
|
||||||
).map(this::getAddressByName).toList(),
|
|
||||||
"datacenter-2", Stream.of(
|
|
||||||
"2600:1111:2222:3333:0:23:0:0",
|
|
||||||
"2600:1111:2222:3333:0:24:0:0")
|
|
||||||
.map(this::getAddressByName).toList(),
|
|
||||||
"datacenter-3", Stream.of(
|
|
||||||
"2600:1111:2222:3333:0:25:0:0",
|
|
||||||
"2600:1111:2222:3333:0:26:0:0")
|
|
||||||
.map(this::getAddressByName).toList(),
|
|
||||||
"datacenter-4", Stream.of(
|
|
||||||
"2600:1111:2222:3333:0:27:0:0"
|
|
||||||
).map(this::getAddressByName).toList()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThat(actual).isEqualTo(expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
InetAddress getAddressByName(String ip) {
|
|
||||||
try {
|
|
||||||
return InetAddress.getByName(ip) ;
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,245 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.calls.routing;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.StringReader;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class CallRoutingTableParserTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testParserSuccess() throws IOException {
|
|
||||||
var input =
|
|
||||||
"""
|
|
||||||
192.1.12.0/24 datacenter-1 datacenter-2 datacenter-3
|
|
||||||
193.123.123.0/24 datacenter-1 datacenter-2
|
|
||||||
1.123.123.0/24 datacenter-1
|
|
||||||
|
|
||||||
2001:db8:b0aa::/48 datacenter-1
|
|
||||||
2001:db8:b0ab::/48 datacenter-3 datacenter-1 datacenter-2
|
|
||||||
2001:db8:b0ac::/48 datacenter-2 datacenter-1
|
|
||||||
|
|
||||||
SA-SR-v4 datacenter-3
|
|
||||||
SA-UY-v4 datacenter-3 datacenter-1 datacenter-2
|
|
||||||
NA-US-VA-v6 datacenter-2 datacenter-1
|
|
||||||
""";
|
|
||||||
var actual = CallRoutingTableParser.fromTsv(new StringReader(input));
|
|
||||||
var expected = new CallRoutingTable(
|
|
||||||
Map.of(
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("192.1.12.0/24"), List.of("datacenter-1", "datacenter-2", "datacenter-3"),
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("193.123.123.0/24"), List.of("datacenter-1", "datacenter-2"),
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("1.123.123.0/24"), List.of("datacenter-1")
|
|
||||||
),
|
|
||||||
Map.of(
|
|
||||||
(CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock("2001:db8:b0aa::/48"), List.of("datacenter-1"),
|
|
||||||
(CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock("2001:db8:b0ab::/48"), List.of("datacenter-3", "datacenter-1", "datacenter-2"),
|
|
||||||
(CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock("2001:db8:b0ac::/48"), List.of("datacenter-2", "datacenter-1")
|
|
||||||
),
|
|
||||||
Map.of(
|
|
||||||
new CallRoutingTable.GeoKey("SA", "SR", Optional.empty(), CallRoutingTable.Protocol.v4), List.of("datacenter-3"),
|
|
||||||
new CallRoutingTable.GeoKey("SA", "UY", Optional.empty(), CallRoutingTable.Protocol.v4), List.of("datacenter-3", "datacenter-1", "datacenter-2"),
|
|
||||||
new CallRoutingTable.GeoKey("NA", "US", Optional.of("VA"), CallRoutingTable.Protocol.v6), List.of("datacenter-2", "datacenter-1")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThat(actual).isEqualTo(expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testParserVariousWhitespaceSuccess() throws IOException {
|
|
||||||
var input =
|
|
||||||
"""
|
|
||||||
|
|
||||||
192.1.12.0/24\t \tdatacenter-1\t\t datacenter-2 datacenter-3
|
|
||||||
\t193.123.123.0/24\tdatacenter-1\tdatacenter-2
|
|
||||||
|
|
||||||
|
|
||||||
1.123.123.0/24\t datacenter-1
|
|
||||||
2001:db8:b0aa::/48\t \tdatacenter-1
|
|
||||||
2001:db8:b0ab::/48 \tdatacenter-3\tdatacenter-1 datacenter-2
|
|
||||||
2001:db8:b0ac::/48\tdatacenter-2\tdatacenter-1
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SA-SR-v4 datacenter-3
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SA-UY-v4\tdatacenter-3\tdatacenter-1\tdatacenter-2
|
|
||||||
NA-US-VA-v6 datacenter-2 \tdatacenter-1
|
|
||||||
""";
|
|
||||||
var actual = CallRoutingTableParser.fromTsv(new StringReader(input));
|
|
||||||
var expected = new CallRoutingTable(
|
|
||||||
Map.of(
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("192.1.12.0/24"), List.of("datacenter-1", "datacenter-2", "datacenter-3"),
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("193.123.123.0/24"), List.of("datacenter-1", "datacenter-2"),
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("1.123.123.0/24"), List.of("datacenter-1")
|
|
||||||
),
|
|
||||||
Map.of(
|
|
||||||
(CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock("2001:db8:b0aa::/48"), List.of("datacenter-1"),
|
|
||||||
(CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock("2001:db8:b0ab::/48"), List.of("datacenter-3", "datacenter-1", "datacenter-2"),
|
|
||||||
(CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock("2001:db8:b0ac::/48"), List.of("datacenter-2", "datacenter-1")
|
|
||||||
),
|
|
||||||
Map.of(
|
|
||||||
new CallRoutingTable.GeoKey("SA", "SR", Optional.empty(), CallRoutingTable.Protocol.v4), List.of("datacenter-3"),
|
|
||||||
new CallRoutingTable.GeoKey("SA", "UY", Optional.empty(), CallRoutingTable.Protocol.v4), List.of("datacenter-3", "datacenter-1", "datacenter-2"),
|
|
||||||
new CallRoutingTable.GeoKey("NA", "US", Optional.of("VA"), CallRoutingTable.Protocol.v6), List.of("datacenter-2", "datacenter-1")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThat(actual).isEqualTo(expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testParserMissingSection() throws IOException {
|
|
||||||
var input =
|
|
||||||
"""
|
|
||||||
192.1.12.0/24\t \tdatacenter-1\t\t datacenter-2 datacenter-3
|
|
||||||
193.123.123.0/24\tdatacenter-1\tdatacenter-2
|
|
||||||
1.123.123.0/24\t datacenter-1
|
|
||||||
|
|
||||||
SA-SR-v4 datacenter-3
|
|
||||||
SA-UY-v4\tdatacenter-3\tdatacenter-1\tdatacenter-2
|
|
||||||
NA-US-VA-v6 datacenter-2 \tdatacenter-1
|
|
||||||
""";
|
|
||||||
var actual = CallRoutingTableParser.fromTsv(new StringReader(input));
|
|
||||||
var expected = new CallRoutingTable(
|
|
||||||
Map.of(
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("192.1.12.0/24"), List.of("datacenter-1", "datacenter-2", "datacenter-3"),
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("193.123.123.0/24"), List.of("datacenter-1", "datacenter-2"),
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("1.123.123.0/24"), List.of("datacenter-1")
|
|
||||||
),
|
|
||||||
Map.of(),
|
|
||||||
Map.of(
|
|
||||||
new CallRoutingTable.GeoKey("SA", "SR", Optional.empty(), CallRoutingTable.Protocol.v4), List.of("datacenter-3"),
|
|
||||||
new CallRoutingTable.GeoKey("SA", "UY", Optional.empty(), CallRoutingTable.Protocol.v4), List.of("datacenter-3", "datacenter-1", "datacenter-2"),
|
|
||||||
new CallRoutingTable.GeoKey("NA", "US", Optional.of("VA"), CallRoutingTable.Protocol.v6), List.of("datacenter-2", "datacenter-1")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThat(actual).isEqualTo(expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testParserMixedSections() throws IOException {
|
|
||||||
var input =
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
1.123.123.0/24\t datacenter-1
|
|
||||||
2001:db8:b0aa::/48\t \tdatacenter-1
|
|
||||||
2001:db8:b0ab::/48 \tdatacenter-3\tdatacenter-1 datacenter-2
|
|
||||||
2001:db8:b0ac::/48\tdatacenter-2\tdatacenter-1
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
192.1.12.0/24\t \tdatacenter-1\t\t datacenter-2 datacenter-3
|
|
||||||
193.123.123.0/24\tdatacenter-1\tdatacenter-2
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SA-SR-v4 datacenter-3
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SA-UY-v4\tdatacenter-3\tdatacenter-1\tdatacenter-2
|
|
||||||
NA-US-VA-v6 datacenter-2 \tdatacenter-1
|
|
||||||
""";
|
|
||||||
var actual = CallRoutingTableParser.fromTsv(new StringReader(input));
|
|
||||||
var expected = new CallRoutingTable(
|
|
||||||
Map.of(
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("1.123.123.0/24"), List.of("datacenter-1"),
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("192.1.12.0/24"), List.of("datacenter-1", "datacenter-2", "datacenter-3"),
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("193.123.123.0/24"), List.of("datacenter-1", "datacenter-2")
|
|
||||||
),
|
|
||||||
Map.of(
|
|
||||||
(CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock("2001:db8:b0aa::/48"), List.of("datacenter-1"),
|
|
||||||
(CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock("2001:db8:b0ab::/48"), List.of("datacenter-3", "datacenter-1", "datacenter-2"),
|
|
||||||
(CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock("2001:db8:b0ac::/48"), List.of("datacenter-2", "datacenter-1")
|
|
||||||
),
|
|
||||||
Map.of(
|
|
||||||
new CallRoutingTable.GeoKey("SA", "SR", Optional.empty(), CallRoutingTable.Protocol.v4), List.of("datacenter-3"),
|
|
||||||
new CallRoutingTable.GeoKey("SA", "UY", Optional.empty(), CallRoutingTable.Protocol.v4), List.of("datacenter-3", "datacenter-1", "datacenter-2"),
|
|
||||||
new CallRoutingTable.GeoKey("NA", "US", Optional.of("VA"), CallRoutingTable.Protocol.v6), List.of("datacenter-2", "datacenter-1")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThat(actual).isEqualTo(expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testJsonParserSuccess() throws IOException {
|
|
||||||
var input =
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"ipv4GeoToDataCenters": {
|
|
||||||
"SA-SR": ["datacenter-3"],
|
|
||||||
"SA-UY": ["datacenter-3", "datacenter-1", "datacenter-2"]
|
|
||||||
},
|
|
||||||
"ipv6GeoToDataCenters": {
|
|
||||||
"NA-US-VA": ["datacenter-2", "datacenter-1"]
|
|
||||||
},
|
|
||||||
"ipv4SubnetsToDatacenters": {
|
|
||||||
"192.1.12.0": ["datacenter-1", "datacenter-2", "datacenter-3"],
|
|
||||||
"193.123.123.0": ["datacenter-1", "datacenter-2"],
|
|
||||||
"1.123.123.0": ["datacenter-1"]
|
|
||||||
},
|
|
||||||
"ipv6SubnetsToDatacenters": {
|
|
||||||
"2001:db8:b0aa::": ["datacenter-1"],
|
|
||||||
"2001:db8:b0ab::": ["datacenter-3", "datacenter-1", "datacenter-2"],
|
|
||||||
"2001:db8:b0ac::": ["datacenter-2", "datacenter-1"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
var actual = CallRoutingTableParser.fromJson(new StringReader(input));
|
|
||||||
var expected = new CallRoutingTable(
|
|
||||||
Map.of(
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("192.1.12.0/24"), List.of("datacenter-1", "datacenter-2", "datacenter-3"),
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("193.123.123.0/24"), List.of("datacenter-1", "datacenter-2"),
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("1.123.123.0/24"), List.of("datacenter-1")
|
|
||||||
),
|
|
||||||
Map.of(
|
|
||||||
(CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock("2001:db8:b0aa::/48"), List.of("datacenter-1"),
|
|
||||||
(CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock("2001:db8:b0ab::/48"), List.of("datacenter-3", "datacenter-1", "datacenter-2"),
|
|
||||||
(CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock("2001:db8:b0ac::/48"), List.of("datacenter-2", "datacenter-1")
|
|
||||||
),
|
|
||||||
Map.of(
|
|
||||||
new CallRoutingTable.GeoKey("SA", "SR", Optional.empty(), CallRoutingTable.Protocol.v4), List.of("datacenter-3"),
|
|
||||||
new CallRoutingTable.GeoKey("SA", "UY", Optional.empty(), CallRoutingTable.Protocol.v4), List.of("datacenter-3", "datacenter-1", "datacenter-2"),
|
|
||||||
new CallRoutingTable.GeoKey("NA", "US", Optional.of("VA"), CallRoutingTable.Protocol.v6), List.of("datacenter-2", "datacenter-1")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThat(actual).isEqualTo(expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testParseVariousEdgeCases() throws IOException {
|
|
||||||
var input =
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"ipv4GeoToDataCenters": {},
|
|
||||||
"ipv6GeoToDataCenters": {},
|
|
||||||
"ipv4SubnetsToDatacenters": {},
|
|
||||||
"ipv6SubnetsToDatacenters": {}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
assertThat(CallRoutingTableParser.fromJson(new StringReader(input))).isEqualTo(CallRoutingTable.empty());
|
|
||||||
assertThat(CallRoutingTableParser.fromJson(new StringReader("{}"))).isEqualTo(CallRoutingTable.empty());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,139 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.calls.routing;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
|
||||||
|
|
||||||
import java.net.Inet4Address;
|
|
||||||
import java.net.Inet6Address;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
public class CallRoutingTableTest {
|
|
||||||
|
|
||||||
static final CallRoutingTable basicTable = new CallRoutingTable(
|
|
||||||
Map.of(
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("192.1.12.0/24"), List.of("datacenter-1", "datacenter-2", "datacenter-3"),
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("193.123.123.0/24"), List.of("datacenter-1", "datacenter-2"),
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("1.123.123.0/24"), List.of("datacenter-4")
|
|
||||||
),
|
|
||||||
Map.of(
|
|
||||||
(CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock("2001:db8:b0aa::/48"), List.of("datacenter-1"),
|
|
||||||
(CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock("2001:db8:b0ab::/48"), List.of("datacenter-3", "datacenter-1", "datacenter-2"),
|
|
||||||
(CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock("2001:db8:b0ac::/48"), List.of("datacenter-2", "datacenter-1")
|
|
||||||
),
|
|
||||||
Map.of(
|
|
||||||
new CallRoutingTable.GeoKey("SA", "SR", Optional.empty(), CallRoutingTable.Protocol.v4), List.of("datacenter-3"),
|
|
||||||
new CallRoutingTable.GeoKey("SA", "UY", Optional.empty(), CallRoutingTable.Protocol.v4), List.of("datacenter-3", "datacenter-1", "datacenter-2"),
|
|
||||||
new CallRoutingTable.GeoKey("NA", "US", Optional.of("VA"), CallRoutingTable.Protocol.v6), List.of("datacenter-2", "datacenter-1"),
|
|
||||||
new CallRoutingTable.GeoKey("NA", "US", Optional.empty(), CallRoutingTable.Protocol.v6), List.of("datacenter-3", "datacenter-4")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// has overlapping subnets
|
|
||||||
static final CallRoutingTable overlappingTable = new CallRoutingTable(
|
|
||||||
Map.of(
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("192.1.12.0/24"), List.of("datacenter-1", "datacenter-2", "datacenter-3"),
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("1.123.123.0/24"), List.of("datacenter-4"),
|
|
||||||
(CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock("1.123.0.0/16"), List.of("datacenter-1")
|
|
||||||
),
|
|
||||||
Map.of(
|
|
||||||
(CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock("2001:db8:b0aa::/48"), List.of("datacenter-1"),
|
|
||||||
(CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock("2001:db8:b0ac::/48"), List.of("datacenter-3", "datacenter-1", "datacenter-2"),
|
|
||||||
(CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock("2001:db8:b0a0::/44"), List.of("datacenter-2", "datacenter-1")
|
|
||||||
),
|
|
||||||
Map.of(
|
|
||||||
new CallRoutingTable.GeoKey("SA", "SR", Optional.empty(), CallRoutingTable.Protocol.v4), List.of("datacenter-3"),
|
|
||||||
new CallRoutingTable.GeoKey("SA", "UY", Optional.empty(), CallRoutingTable.Protocol.v4), List.of("datacenter-3", "datacenter-1", "datacenter-2"),
|
|
||||||
new CallRoutingTable.GeoKey("NA", "US", Optional.of("VA"), CallRoutingTable.Protocol.v6), List.of("datacenter-2", "datacenter-1")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetFastestDataCentersBySubnet() throws UnknownHostException {
|
|
||||||
var v4address = Inet4Address.getByName("1.123.123.1");
|
|
||||||
var actualV4 = basicTable.getDatacentersBySubnet(v4address);
|
|
||||||
assertThat(actualV4).isEqualTo(List.of("datacenter-4"));
|
|
||||||
|
|
||||||
var v6address = Inet6Address.getByName("2001:db8:b0ac:aaaa:aaaa:aaaa:aaaa:0001");
|
|
||||||
var actualV6 = basicTable.getDatacentersBySubnet(v6address);
|
|
||||||
assertThat(actualV6).isEqualTo(List.of("datacenter-2", "datacenter-1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetFastestDataCentersBySubnetOverlappingTable() throws UnknownHostException {
|
|
||||||
var v4address = Inet4Address.getByName("1.123.123.1");
|
|
||||||
var actualV4 = overlappingTable.getDatacentersBySubnet(v4address);
|
|
||||||
assertThat(actualV4).isEqualTo(List.of("datacenter-4"));
|
|
||||||
|
|
||||||
var v6address = Inet6Address.getByName("2001:db8:b0ac:aaaa:aaaa:aaaa:aaaa:0001");
|
|
||||||
var actualV6 = overlappingTable.getDatacentersBySubnet(v6address);
|
|
||||||
assertThat(actualV6).isEqualTo(List.of("datacenter-3", "datacenter-1", "datacenter-2"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetFastestDataCentersByGeo() {
|
|
||||||
var actual = basicTable.getDatacentersByGeo("SA", "SR", Optional.empty());
|
|
||||||
assertThat(actual).isEqualTo(List.of("datacenter-3"));
|
|
||||||
|
|
||||||
var actualWithSubdvision = basicTable.getDatacentersByGeo("NA", "US", Optional.of("VA"));
|
|
||||||
assertThat(actualWithSubdvision).isEqualTo(List.of("datacenter-2", "datacenter-1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetFastestDataCentersByGeoFallback() {
|
|
||||||
var actualExactMatch = basicTable.getDatacentersByGeo("NA", "US", Optional.of("VA"));
|
|
||||||
assertThat(actualExactMatch).isEqualTo(List.of("datacenter-2", "datacenter-1"));
|
|
||||||
|
|
||||||
var actualApproximateMatch = basicTable.getDatacentersByGeo("NA", "US", Optional.of("MD"));
|
|
||||||
assertThat(actualApproximateMatch).isEqualTo(List.of("datacenter-3", "datacenter-4"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetFastestDatacentersPrioritizesSubnet() throws UnknownHostException {
|
|
||||||
var v4address = Inet4Address.getByName("1.123.123.1");
|
|
||||||
var actual = basicTable.getDatacentersFor(v4address, "NA", "US", Optional.of("VA"));
|
|
||||||
assertThat(actual).isEqualTo(List.of("datacenter-4", "datacenter-2", "datacenter-1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetFastestDatacentersEmptySubnet() throws UnknownHostException {
|
|
||||||
var v4address = Inet4Address.getByName("200.200.123.1");
|
|
||||||
var actual = basicTable.getDatacentersFor(v4address, "NA", "US", Optional.of("VA"));
|
|
||||||
assertThat(actual).isEqualTo(List.of("datacenter-2", "datacenter-1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetFastestDatacentersEmptySubnetTakesExtraFromGeo() throws UnknownHostException {
|
|
||||||
var v4address = Inet4Address.getByName("200.200.123.1");
|
|
||||||
var actual = basicTable.getDatacentersFor(v4address, "SA", "UY", Optional.empty());
|
|
||||||
assertThat(actual).isEqualTo(List.of("datacenter-3", "datacenter-1", "datacenter-2"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetFastestDatacentersEmptyGeoResults() throws UnknownHostException {
|
|
||||||
var v4address = Inet4Address.getByName("1.123.123.1");
|
|
||||||
var actual = basicTable.getDatacentersFor(v4address, "ZZ", "AA", Optional.empty());
|
|
||||||
assertThat(actual).isEqualTo(List.of("datacenter-4"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetFastestDatacentersEmptyGeoTakesFromSubnet() throws UnknownHostException {
|
|
||||||
var v4address = Inet4Address.getByName("192.1.12.1");
|
|
||||||
var actual = basicTable.getDatacentersFor(v4address, "ZZ", "AA", Optional.empty());
|
|
||||||
assertThat(actual).isEqualTo(List.of("datacenter-1", "datacenter-2", "datacenter-3"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGetFastestDatacentersDistinct() throws UnknownHostException {
|
|
||||||
var v6address = Inet6Address.getByName("2001:db8:b0ac:aaaa:aaaa:aaaa:aaaa:0001");
|
|
||||||
var actual = basicTable.getDatacentersFor(v6address, "NA", "US", Optional.of("VA"));
|
|
||||||
assertThat(actual).isEqualTo(List.of("datacenter-2", "datacenter-1"));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.calls.routing;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.HexFormat;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
public class CidrBlockTest {
|
|
||||||
|
|
||||||
private HexFormat hex = HexFormat.ofDelimiter(":").withLowerCase();
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testIPv4CidrBlockParseSuccess() {
|
|
||||||
var actual = CidrBlock.parseCidrBlock("255.32.15.0/24");
|
|
||||||
var expected = new CidrBlock.IpV4CidrBlock(0xFF_20_0F_00, 0xFFFFFF00, 24);
|
|
||||||
|
|
||||||
assertThat(actual).isInstanceOf(CidrBlock.IpV4CidrBlock.class);
|
|
||||||
assertThat(actual).isEqualTo(expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testIPv6CidrBlockParseSuccess() {
|
|
||||||
var actual = CidrBlock.parseCidrBlock("2001:db8:b0aa::/48");
|
|
||||||
var expected = new CidrBlock.IpV6CidrBlock(
|
|
||||||
new BigInteger(hex.parseHex("20:01:0d:b8:b0:aa:00:00:00:00:00:00:00:00:00:00")),
|
|
||||||
new BigInteger(hex.parseHex("FF:FF:FF:FF:FF:FF:00:00:00:00:00:00:00:00:00:00")),
|
|
||||||
48
|
|
||||||
);
|
|
||||||
|
|
||||||
assertThat(actual).isInstanceOf(CidrBlock.IpV6CidrBlock.class);
|
|
||||||
assertThat(actual).isEqualTo(expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testIPv4InBlock() throws UnknownHostException {
|
|
||||||
var block = CidrBlock.parseCidrBlock("255.32.15.0/24");
|
|
||||||
|
|
||||||
assertThat(block.ipInBlock(InetAddress.getByName("255.32.15.123"))).isTrue();
|
|
||||||
assertThat(block.ipInBlock(InetAddress.getByName("255.32.15.0"))).isTrue();
|
|
||||||
assertThat(block.ipInBlock(InetAddress.getByName("255.32.16.0"))).isFalse();
|
|
||||||
assertThat(block.ipInBlock(InetAddress.getByName("255.33.15.0"))).isFalse();
|
|
||||||
assertThat(block.ipInBlock(InetAddress.getByName("254.33.15.0"))).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testIPv6InBlock() throws UnknownHostException {
|
|
||||||
var block = CidrBlock.parseCidrBlock("2001:db8:b0aa::/48");
|
|
||||||
|
|
||||||
assertThat(block.ipInBlock(InetAddress.getByName("2001:db8:b0aa:1:1::"))).isTrue();
|
|
||||||
assertThat(block.ipInBlock(InetAddress.getByName("2001:db8:b0aa:0:0::"))).isTrue();
|
|
||||||
assertThat(block.ipInBlock(InetAddress.getByName("2001:db8:b0ab:1:1::"))).isFalse();
|
|
||||||
assertThat(block.ipInBlock(InetAddress.getByName("2001:da8:b0aa:1:1::"))).isFalse();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,137 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.calls.routing;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
public class DynamicConfigTurnRouterTest {
|
|
||||||
@Test
|
|
||||||
public void testAlwaysSelectFirst() throws JsonProcessingException {
|
|
||||||
final String configString = """
|
|
||||||
captcha:
|
|
||||||
scoreFloor: 1.0
|
|
||||||
turn:
|
|
||||||
uriConfigs:
|
|
||||||
- uris:
|
|
||||||
- always1.org
|
|
||||||
- always2.org
|
|
||||||
- uris:
|
|
||||||
- never.org
|
|
||||||
weight: 0
|
|
||||||
""";
|
|
||||||
DynamicConfiguration config = DynamicConfigurationManager
|
|
||||||
.parseConfiguration(configString, DynamicConfiguration.class)
|
|
||||||
.orElseThrow();
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
DynamicConfigurationManager<DynamicConfiguration> mockDynamicConfigManager = mock(
|
|
||||||
DynamicConfigurationManager.class);
|
|
||||||
|
|
||||||
when(mockDynamicConfigManager.getConfiguration()).thenReturn(config);
|
|
||||||
|
|
||||||
final DynamicConfigTurnRouter configTurnRouter = new DynamicConfigTurnRouter(mockDynamicConfigManager);
|
|
||||||
|
|
||||||
final long COUNT = 1000;
|
|
||||||
|
|
||||||
final Map<String, Long> urlCounts = Stream
|
|
||||||
.generate(configTurnRouter::randomUrls)
|
|
||||||
.limit(COUNT)
|
|
||||||
.flatMap(Collection::stream)
|
|
||||||
.collect(Collectors.groupingBy(i -> i, Collectors.counting()));
|
|
||||||
|
|
||||||
assertThat(urlCounts.get("always1.org")).isEqualTo(COUNT);
|
|
||||||
assertThat(urlCounts.get("always2.org")).isEqualTo(COUNT);
|
|
||||||
assertThat(urlCounts).doesNotContainKey("never.org");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testProbabilisticUrls() throws JsonProcessingException {
|
|
||||||
final String configString = """
|
|
||||||
captcha:
|
|
||||||
scoreFloor: 1.0
|
|
||||||
turn:
|
|
||||||
uriConfigs:
|
|
||||||
- uris:
|
|
||||||
- always.org
|
|
||||||
- sometimes1.org
|
|
||||||
weight: 5
|
|
||||||
- uris:
|
|
||||||
- always.org
|
|
||||||
- sometimes2.org
|
|
||||||
weight: 5
|
|
||||||
""";
|
|
||||||
DynamicConfiguration config = DynamicConfigurationManager
|
|
||||||
.parseConfiguration(configString, DynamicConfiguration.class)
|
|
||||||
.orElseThrow();
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
DynamicConfigurationManager<DynamicConfiguration> mockDynamicConfigManager = mock(
|
|
||||||
DynamicConfigurationManager.class);
|
|
||||||
|
|
||||||
when(mockDynamicConfigManager.getConfiguration()).thenReturn(config);
|
|
||||||
final DynamicConfigTurnRouter configTurnRouter = new DynamicConfigTurnRouter(mockDynamicConfigManager);
|
|
||||||
|
|
||||||
final long COUNT = 1000;
|
|
||||||
|
|
||||||
final Map<String, Long> urlCounts = Stream
|
|
||||||
.generate(configTurnRouter::randomUrls)
|
|
||||||
.limit(COUNT)
|
|
||||||
.flatMap(Collection::stream)
|
|
||||||
.collect(Collectors.groupingBy(i -> i, Collectors.counting()));
|
|
||||||
|
|
||||||
assertThat(urlCounts.get("always.org")).isEqualTo(COUNT);
|
|
||||||
assertThat(urlCounts.get("sometimes1.org")).isGreaterThan(0);
|
|
||||||
assertThat(urlCounts.get("sometimes2.org")).isGreaterThan(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testExplicitEnrollment() throws JsonProcessingException {
|
|
||||||
final String configString = """
|
|
||||||
captcha:
|
|
||||||
scoreFloor: 1.0
|
|
||||||
turn:
|
|
||||||
secret: bloop
|
|
||||||
uriConfigs:
|
|
||||||
- uris:
|
|
||||||
- enrolled.org
|
|
||||||
weight: 0
|
|
||||||
enrolledAcis:
|
|
||||||
- 732506d7-d04f-43a4-b1d7-8a3a91ebe8a6
|
|
||||||
- uris:
|
|
||||||
- unenrolled.org
|
|
||||||
weight: 1
|
|
||||||
""";
|
|
||||||
DynamicConfiguration config = DynamicConfigurationManager
|
|
||||||
.parseConfiguration(configString, DynamicConfiguration.class)
|
|
||||||
.orElseThrow();
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
DynamicConfigurationManager<DynamicConfiguration> mockDynamicConfigManager = mock(
|
|
||||||
DynamicConfigurationManager.class);
|
|
||||||
|
|
||||||
when(mockDynamicConfigManager.getConfiguration()).thenReturn(config);
|
|
||||||
final DynamicConfigTurnRouter configTurnRouter = new DynamicConfigTurnRouter(mockDynamicConfigManager);
|
|
||||||
|
|
||||||
List<String> urls = configTurnRouter.targetedUrls(UUID.fromString("732506d7-d04f-43a4-b1d7-8a3a91ebe8a6"));
|
|
||||||
assertThat(urls.getFirst()).isEqualTo("enrolled.org");
|
|
||||||
urls = configTurnRouter.targetedUrls(UUID.randomUUID());
|
|
||||||
assertTrue(urls.isEmpty());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,335 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.calls.routing;
|
|
||||||
|
|
||||||
import com.maxmind.geoip2.DatabaseReader;
|
|
||||||
import com.maxmind.geoip2.exception.GeoIp2Exception;
|
|
||||||
import com.maxmind.geoip2.model.CityResponse;
|
|
||||||
import com.maxmind.geoip2.record.Continent;
|
|
||||||
import com.maxmind.geoip2.record.Country;
|
|
||||||
import com.maxmind.geoip2.record.Subdivision;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
public class TurnCallRouterTest {
|
|
||||||
|
|
||||||
private final static String TEST_HOSTNAME = "subdomain.example.org";
|
|
||||||
private final static List<String> TEST_URLS_WITH_HOSTS = List.of(
|
|
||||||
"stun:one.example.com",
|
|
||||||
"turn:two.example.com",
|
|
||||||
"turn:three.example.com?transport=tcp"
|
|
||||||
);
|
|
||||||
private final static List<String> EXPECTED_TEST_URLS_WITH_HOSTS = List.of(
|
|
||||||
"turn:two.example.com"
|
|
||||||
);
|
|
||||||
|
|
||||||
private CallRoutingTable performanceTable;
|
|
||||||
private CallRoutingTable manualTable;
|
|
||||||
private DynamicConfigTurnRouter configTurnRouter;
|
|
||||||
private DatabaseReader geoIp;
|
|
||||||
private Country country;
|
|
||||||
private Continent continent;
|
|
||||||
private CallDnsRecords callDnsRecords;
|
|
||||||
private Subdivision subdivision;
|
|
||||||
private UUID aci = UUID.randomUUID();
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setup() throws IOException, GeoIp2Exception {
|
|
||||||
performanceTable = mock(CallRoutingTable.class);
|
|
||||||
manualTable = mock(CallRoutingTable.class);
|
|
||||||
configTurnRouter = mock(DynamicConfigTurnRouter.class);
|
|
||||||
geoIp = mock(DatabaseReader.class);
|
|
||||||
continent = mock(Continent.class);
|
|
||||||
country = mock(Country.class);
|
|
||||||
subdivision = mock(Subdivision.class);
|
|
||||||
ArrayList<Subdivision> subdivisions = new ArrayList<>();
|
|
||||||
subdivisions.add(subdivision);
|
|
||||||
|
|
||||||
when(geoIp.city(any())).thenReturn(new CityResponse(null, continent, country, null, null, null, null, null, subdivisions, null));
|
|
||||||
setupDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setupDefault() {
|
|
||||||
when(configTurnRouter.targetedUrls(any())).thenReturn(Collections.emptyList());
|
|
||||||
when(configTurnRouter.randomUrls()).thenReturn(TEST_URLS_WITH_HOSTS);
|
|
||||||
when(configTurnRouter.getHostname()).thenReturn(TEST_HOSTNAME);
|
|
||||||
when(configTurnRouter.shouldRandomize()).thenReturn(false);
|
|
||||||
when(manualTable.getDatacentersFor(any(), any(), any(), any())).thenReturn(Collections.emptyList());
|
|
||||||
when(continent.getCode()).thenReturn("NA");
|
|
||||||
when(country.getIsoCode()).thenReturn("US");
|
|
||||||
when(subdivision.getIsoCode()).thenReturn("VA");
|
|
||||||
try {
|
|
||||||
callDnsRecords = new CallDnsRecords(
|
|
||||||
Map.of(
|
|
||||||
"dc-manual", List.of(InetAddress.getByName("1.1.1.1")),
|
|
||||||
"dc-performance1", List.of(
|
|
||||||
InetAddress.getByName("9.9.9.1"),
|
|
||||||
InetAddress.getByName("9.9.9.2")
|
|
||||||
),
|
|
||||||
"dc-performance2", List.of(InetAddress.getByName("9.9.9.3")),
|
|
||||||
"dc-performance3", List.of(InetAddress.getByName("9.9.9.4")),
|
|
||||||
"dc-performance4", List.of(
|
|
||||||
InetAddress.getByName("9.9.9.5"),
|
|
||||||
InetAddress.getByName("9.9.9.6"),
|
|
||||||
InetAddress.getByName("9.9.9.7")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Map.of(
|
|
||||||
"dc-manual", List.of(InetAddress.getByName("2222:1111:0:dead::")),
|
|
||||||
"dc-performance1", List.of(
|
|
||||||
InetAddress.getByName("2222:1111:0:abc0::"),
|
|
||||||
InetAddress.getByName("2222:1111:0:abc1::")
|
|
||||||
),
|
|
||||||
"dc-performance2", List.of(InetAddress.getByName("2222:1111:0:abc2::")),
|
|
||||||
"dc-performance3", List.of(InetAddress.getByName("2222:1111:0:abc3::")),
|
|
||||||
"dc-performance4", List.of(
|
|
||||||
InetAddress.getByName("2222:1111:0:abc4::"),
|
|
||||||
InetAddress.getByName("2222:1111:0:abc5::"),
|
|
||||||
InetAddress.getByName("2222:1111:0:abc6::")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private TurnCallRouter router() {
|
|
||||||
return new TurnCallRouter(
|
|
||||||
() -> callDnsRecords,
|
|
||||||
() -> performanceTable,
|
|
||||||
() -> manualTable,
|
|
||||||
configTurnRouter,
|
|
||||||
() -> geoIp,
|
|
||||||
// set to true so the return values are predictable
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TurnServerOptions optionsWithUrls(List<String> urls) {
|
|
||||||
return new TurnServerOptions(
|
|
||||||
TEST_HOSTNAME,
|
|
||||||
Optional.of(urls),
|
|
||||||
Optional.of(EXPECTED_TEST_URLS_WITH_HOSTS)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testPrioritizesTargetedUrls() throws UnknownHostException {
|
|
||||||
List<String> targetedUrls = List.of(
|
|
||||||
"targeted1.example.com",
|
|
||||||
"targeted.example.com"
|
|
||||||
);
|
|
||||||
when(configTurnRouter.targetedUrls(any()))
|
|
||||||
.thenReturn(targetedUrls);
|
|
||||||
|
|
||||||
assertThat(router().getRoutingFor(aci, Optional.of(InetAddress.getByName("0.0.0.1")), 10))
|
|
||||||
.isEqualTo(new TurnServerOptions(
|
|
||||||
TEST_HOSTNAME,
|
|
||||||
Optional.empty(),
|
|
||||||
Optional.of(targetedUrls)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRandomizes() throws UnknownHostException {
|
|
||||||
when(configTurnRouter.shouldRandomize())
|
|
||||||
.thenReturn(true);
|
|
||||||
|
|
||||||
assertThat(router().getRoutingFor(aci, Optional.of(InetAddress.getByName("0.0.0.1")), 10))
|
|
||||||
.isEqualTo(new TurnServerOptions(
|
|
||||||
TEST_HOSTNAME,
|
|
||||||
Optional.empty(),
|
|
||||||
Optional.of(TEST_URLS_WITH_HOSTS)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
@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,
|
|
||||||
Optional.empty(),
|
|
||||||
Optional.of(TEST_URLS_WITH_HOSTS)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testOrderedByPerformance() throws UnknownHostException {
|
|
||||||
when(performanceTable.getDatacentersFor(any(), any(), any(), any()))
|
|
||||||
.thenReturn(List.of("dc-performance2", "dc-performance1"));
|
|
||||||
|
|
||||||
assertThat(router().getRoutingFor(aci, Optional.of(InetAddress.getByName("0.0.0.1")), 10))
|
|
||||||
.isEqualTo(optionsWithUrls(List.of(
|
|
||||||
"turn:9.9.9.3",
|
|
||||||
"turn:9.9.9.3:80?transport=tcp",
|
|
||||||
"turns:9.9.9.3:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:9.9.9.1",
|
|
||||||
"turn:9.9.9.1:80?transport=tcp",
|
|
||||||
"turns:9.9.9.1:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:9.9.9.2",
|
|
||||||
"turn:9.9.9.2:80?transport=tcp",
|
|
||||||
"turns:9.9.9.2:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:[2222:1111:0:abc2:0:0:0:0]",
|
|
||||||
"turn:[2222:1111:0:abc2:0:0:0:0]:80?transport=tcp",
|
|
||||||
"turns:[2222:1111:0:abc2:0:0:0:0]:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:[2222:1111:0:abc0:0:0:0:0]",
|
|
||||||
"turn:[2222:1111:0:abc0:0:0:0:0]:80?transport=tcp",
|
|
||||||
"turns:[2222:1111:0:abc0:0:0:0:0]:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:[2222:1111:0:abc1:0:0:0:0]",
|
|
||||||
"turn:[2222:1111:0:abc1:0:0:0:0]:80?transport=tcp",
|
|
||||||
"turns:[2222:1111:0:abc1:0:0:0:0]:443?transport=tcp"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testPrioritizesManualRecords() throws UnknownHostException {
|
|
||||||
when(performanceTable.getDatacentersFor(any(), any(), any(), any()))
|
|
||||||
.thenReturn(List.of("dc-performance1"));
|
|
||||||
when(manualTable.getDatacentersFor(any(), any(), any(), any()))
|
|
||||||
.thenReturn(List.of("dc-manual"));
|
|
||||||
|
|
||||||
assertThat(router().getRoutingFor(aci, Optional.of(InetAddress.getByName("0.0.0.1")), 10))
|
|
||||||
.isEqualTo(optionsWithUrls(List.of(
|
|
||||||
"turn:1.1.1.1",
|
|
||||||
"turn:1.1.1.1:80?transport=tcp",
|
|
||||||
"turns:1.1.1.1:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:[2222:1111:0:dead:0:0:0:0]",
|
|
||||||
"turn:[2222:1111:0:dead:0:0:0:0]:80?transport=tcp",
|
|
||||||
"turns:[2222:1111:0:dead:0:0:0:0]:443?transport=tcp"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testLimitPrioritizesBestDataCenters() throws UnknownHostException {
|
|
||||||
when(performanceTable.getDatacentersFor(any(), any(), any(), any()))
|
|
||||||
.thenReturn(List.of("dc-performance3", "dc-performance2", "dc-performance3"));
|
|
||||||
|
|
||||||
// gets one instance from best two datacenters
|
|
||||||
assertThat(router().getRoutingFor(aci, Optional.of(InetAddress.getByName("0.0.0.1")), 2))
|
|
||||||
.isEqualTo(optionsWithUrls(List.of(
|
|
||||||
"turn:9.9.9.4",
|
|
||||||
"turn:9.9.9.4:80?transport=tcp",
|
|
||||||
"turns:9.9.9.4:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:9.9.9.3",
|
|
||||||
"turn:9.9.9.3:80?transport=tcp",
|
|
||||||
"turns:9.9.9.3:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:[2222:1111:0:abc3:0:0:0:0]",
|
|
||||||
"turn:[2222:1111:0:abc3:0:0:0:0]:80?transport=tcp",
|
|
||||||
"turns:[2222:1111:0:abc3:0:0:0:0]:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:[2222:1111:0:abc2:0:0:0:0]",
|
|
||||||
"turn:[2222:1111:0:abc2:0:0:0:0]:80?transport=tcp",
|
|
||||||
"turns:[2222:1111:0:abc2:0:0:0:0]:443?transport=tcp"
|
|
||||||
)));
|
|
||||||
|
|
||||||
assertThat(router().getRoutingFor(aci, Optional.of(InetAddress.getByName("2222:1111:0:abc2:0:0:0:1")), 1))
|
|
||||||
.isEqualTo(optionsWithUrls(List.of(
|
|
||||||
"turn:9.9.9.4",
|
|
||||||
"turn:9.9.9.4:80?transport=tcp",
|
|
||||||
"turns:9.9.9.4:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:[2222:1111:0:abc3:0:0:0:0]",
|
|
||||||
"turn:[2222:1111:0:abc3:0:0:0:0]:80?transport=tcp",
|
|
||||||
"turns:[2222:1111:0:abc3:0:0:0:0]:443?transport=tcp"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testBackFillsUpToLimit() throws UnknownHostException {
|
|
||||||
when(performanceTable.getDatacentersFor(any(), any(), any(), any()))
|
|
||||||
.thenReturn(List.of("dc-performance4", "dc-performance2", "dc-performance3"));
|
|
||||||
|
|
||||||
assertThat(router().getRoutingFor(aci, Optional.of(InetAddress.getByName("0.0.0.1")), 5))
|
|
||||||
.isEqualTo(optionsWithUrls(List.of(
|
|
||||||
"turn:9.9.9.5",
|
|
||||||
"turn:9.9.9.5:80?transport=tcp",
|
|
||||||
"turns:9.9.9.5:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:9.9.9.6",
|
|
||||||
"turn:9.9.9.6:80?transport=tcp",
|
|
||||||
"turns:9.9.9.6:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:9.9.9.7",
|
|
||||||
"turn:9.9.9.7:80?transport=tcp",
|
|
||||||
"turns:9.9.9.7:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:9.9.9.3",
|
|
||||||
"turn:9.9.9.3:80?transport=tcp",
|
|
||||||
"turns:9.9.9.3:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:9.9.9.4",
|
|
||||||
"turn:9.9.9.4:80?transport=tcp",
|
|
||||||
"turns:9.9.9.4:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:[2222:1111:0:abc4:0:0:0:0]",
|
|
||||||
"turn:[2222:1111:0:abc4:0:0:0:0]:80?transport=tcp",
|
|
||||||
"turns:[2222:1111:0:abc4:0:0:0:0]:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:[2222:1111:0:abc5:0:0:0:0]",
|
|
||||||
"turn:[2222:1111:0:abc5:0:0:0:0]:80?transport=tcp",
|
|
||||||
"turns:[2222:1111:0:abc5:0:0:0:0]:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:[2222:1111:0:abc6:0:0:0:0]",
|
|
||||||
"turn:[2222:1111:0:abc6:0:0:0:0]:80?transport=tcp",
|
|
||||||
"turns:[2222:1111:0:abc6:0:0:0:0]:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:[2222:1111:0:abc2:0:0:0:0]",
|
|
||||||
"turn:[2222:1111:0:abc2:0:0:0:0]:80?transport=tcp",
|
|
||||||
"turns:[2222:1111:0:abc2:0:0:0:0]:443?transport=tcp",
|
|
||||||
|
|
||||||
"turn:[2222:1111:0:abc3:0:0:0:0]",
|
|
||||||
"turn:[2222:1111:0:abc3:0:0:0:0]:80?transport=tcp",
|
|
||||||
"turns:[2222:1111:0:abc3:0:0:0:0]:443?transport=tcp"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testNoDatacentersMatched() throws UnknownHostException {
|
|
||||||
when(performanceTable.getDatacentersFor(any(), any(), any(), any()))
|
|
||||||
.thenReturn(List.of());
|
|
||||||
|
|
||||||
assertThat(router().getRoutingFor(aci, Optional.of(InetAddress.getByName("0.0.0.1")), 10))
|
|
||||||
.isEqualTo(optionsWithUrls(List.of()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testHandlesDatacenterNotInDnsRecords() throws UnknownHostException {
|
|
||||||
when(performanceTable.getDatacentersFor(any(), any(), any(), any()))
|
|
||||||
.thenReturn(List.of("unsynced-datacenter"));
|
|
||||||
|
|
||||||
assertThat(router().getRoutingFor(aci, Optional.of(InetAddress.getByName("0.0.0.1")), 10))
|
|
||||||
.isEqualTo(optionsWithUrls(List.of()));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -335,67 +335,6 @@ class DynamicConfigurationTest {
|
||||||
assertThat(resetRateLimiterConfig.permitRegenerationDuration()).isEqualTo(Duration.ofNanos(4_000));
|
assertThat(resetRateLimiterConfig.permitRegenerationDuration()).isEqualTo(Duration.ofNanos(4_000));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void testParseTurnConfig() throws JsonProcessingException {
|
|
||||||
{
|
|
||||||
final String config = REQUIRED_CONFIG.concat("""
|
|
||||||
turn:
|
|
||||||
secret: bloop
|
|
||||||
uriConfigs:
|
|
||||||
- uris:
|
|
||||||
- turn:test.org
|
|
||||||
weight: -1
|
|
||||||
""");
|
|
||||||
assertThat(DynamicConfigurationManager.parseConfiguration(config, DynamicConfiguration.class)).isEmpty();
|
|
||||||
}
|
|
||||||
{
|
|
||||||
final String config = REQUIRED_CONFIG.concat("""
|
|
||||||
turn:
|
|
||||||
uriConfigs:
|
|
||||||
- uris:
|
|
||||||
- turn:test0.org
|
|
||||||
- turn:test1.org
|
|
||||||
- uris:
|
|
||||||
- turn:test2.org
|
|
||||||
weight: 2
|
|
||||||
enrolledAcis:
|
|
||||||
- 732506d7-d04f-43a4-b1d7-8a3a91ebe8a6
|
|
||||||
randomizeRate: 100_000
|
|
||||||
hostname: test.domain.org
|
|
||||||
""");
|
|
||||||
DynamicTurnConfiguration turnConfiguration = DynamicConfigurationManager
|
|
||||||
.parseConfiguration(config, DynamicConfiguration.class)
|
|
||||||
.orElseThrow()
|
|
||||||
.getTurnConfiguration();
|
|
||||||
assertThat(turnConfiguration.getUriConfigs().get(0).getUris()).hasSize(2);
|
|
||||||
assertThat(turnConfiguration.getUriConfigs().get(1).getUris()).hasSize(1);
|
|
||||||
assertThat(turnConfiguration.getUriConfigs().get(0).getWeight()).isEqualTo(1);
|
|
||||||
assertThat(turnConfiguration.getUriConfigs().get(1).getWeight()).isEqualTo(2);
|
|
||||||
assertThat(turnConfiguration.getUriConfigs().get(1).getEnrolledAcis())
|
|
||||||
.containsExactly(UUID.fromString("732506d7-d04f-43a4-b1d7-8a3a91ebe8a6"));
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testMessagePersister() throws JsonProcessingException {
|
void testMessagePersister() throws JsonProcessingException {
|
||||||
{
|
{
|
||||||
|
|
|
@ -59,7 +59,6 @@ import org.signal.libsignal.usernames.BaseUsernameException;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||||
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
|
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
|
||||||
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
|
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
|
||||||
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountIdentifierResponse;
|
import org.whispersystems.textsecuregcm.entities.AccountIdentifierResponse;
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
|
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
|
||||||
|
@ -130,7 +129,6 @@ class AccountControllerTest {
|
||||||
private static final RateLimiter usernameReserveLimiter = mock(RateLimiter.class);
|
private static final RateLimiter usernameReserveLimiter = mock(RateLimiter.class);
|
||||||
private static final RateLimiter usernameLookupLimiter = mock(RateLimiter.class);
|
private static final RateLimiter usernameLookupLimiter = mock(RateLimiter.class);
|
||||||
private static final RateLimiter checkAccountExistence = mock(RateLimiter.class);
|
private static final RateLimiter checkAccountExistence = mock(RateLimiter.class);
|
||||||
private static final TurnTokenGenerator turnTokenGenerator = mock(TurnTokenGenerator.class);
|
|
||||||
private static final Account senderPinAccount = mock(Account.class);
|
private static final Account senderPinAccount = mock(Account.class);
|
||||||
private static final Account senderRegLockAccount = mock(Account.class);
|
private static final Account senderRegLockAccount = mock(Account.class);
|
||||||
private static final Account senderHasStorage = mock(Account.class);
|
private static final Account senderHasStorage = mock(Account.class);
|
||||||
|
@ -234,7 +232,6 @@ class AccountControllerTest {
|
||||||
usernameSetLimiter,
|
usernameSetLimiter,
|
||||||
usernameReserveLimiter,
|
usernameReserveLimiter,
|
||||||
usernameLookupLimiter,
|
usernameLookupLimiter,
|
||||||
turnTokenGenerator,
|
|
||||||
senderPinAccount,
|
senderPinAccount,
|
||||||
senderRegLockAccount,
|
senderRegLockAccount,
|
||||||
senderHasStorage,
|
senderHasStorage,
|
||||||
|
|
|
@ -164,7 +164,6 @@ currentReportingKey.salt: AAAAAAAAAAA=
|
||||||
|
|
||||||
registrationService.collationKeySalt: AAAAAAAAAAA=
|
registrationService.collationKeySalt: AAAAAAAAAAA=
|
||||||
|
|
||||||
turn.secret: AAAAAAAAAAA=
|
|
||||||
turn.cloudflare.apiToken: ABCDEFGHIJKLM
|
turn.cloudflare.apiToken: ABCDEFGHIJKLM
|
||||||
|
|
||||||
linkDevice.secret: AAAAAAAAAAA=
|
linkDevice.secret: AAAAAAAAAAA=
|
||||||
|
|
|
@ -467,7 +467,6 @@ keyTransparencyService:
|
||||||
clientPrivateKey: secret://keyTransparencyService.clientPrivateKey
|
clientPrivateKey: secret://keyTransparencyService.clientPrivateKey
|
||||||
|
|
||||||
turn:
|
turn:
|
||||||
secret: secret://turn.secret
|
|
||||||
cloudflare:
|
cloudflare:
|
||||||
apiToken: secret://turn.cloudflare.apiToken
|
apiToken: secret://turn.cloudflare.apiToken
|
||||||
endpoint: https://rtc.live.cloudflare.com/v1/turn/keys/LMNOP/credentials/generate
|
endpoint: https://rtc.live.cloudflare.com/v1/turn/keys/LMNOP/credentials/generate
|
||||||
|
@ -484,18 +483,6 @@ turn:
|
||||||
linkDevice:
|
linkDevice:
|
||||||
secret: secret://linkDevice.secret
|
secret: secret://linkDevice.secret
|
||||||
|
|
||||||
maxmindCityDatabase:
|
|
||||||
type: static
|
|
||||||
|
|
||||||
callingTurnDnsRecords:
|
|
||||||
type: static
|
|
||||||
|
|
||||||
callingTurnPerformanceTable:
|
|
||||||
type: static
|
|
||||||
|
|
||||||
callingTurnManualTable:
|
|
||||||
type: static
|
|
||||||
|
|
||||||
noiseTunnel:
|
noiseTunnel:
|
||||||
port: 8443
|
port: 8443
|
||||||
noiseStaticPrivateKey: secret://noiseTunnel.noiseStaticPrivateKey
|
noiseStaticPrivateKey: secret://noiseTunnel.noiseStaticPrivateKey
|
||||||
|
|
Loading…
Reference in New Issue