Remove latency based 1:1 call routing
This commit is contained in:
parent
7260a9d5b4
commit
d4322a2ed4
|
@ -1,34 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.calls.routing;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public record CallDnsRecords(
|
|
||||||
@NotNull
|
|
||||||
Map<String, List<InetAddress>> aByRegion,
|
|
||||||
@NotNull
|
|
||||||
Map<String, List<InetAddress>> aaaaByRegion
|
|
||||||
) {
|
|
||||||
public String getSummary() {
|
|
||||||
int numARecords = aByRegion.values().stream().mapToInt(List::size).sum();
|
|
||||||
int numAAAARecords = aaaaByRegion.values().stream().mapToInt(List::size).sum();
|
|
||||||
return String.format(
|
|
||||||
"(A records, %s regions, %s records), (AAAA records, %s regions, %s records)",
|
|
||||||
aByRegion.size(),
|
|
||||||
numARecords,
|
|
||||||
aaaaByRegion.size(),
|
|
||||||
numAAAARecords
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CallDnsRecords empty() {
|
|
||||||
return new CallDnsRecords(Map.of(), Map.of());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.calls.routing;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.StreamReadFeature;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
|
||||||
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.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 CallDnsRecordsManager implements Supplier<CallDnsRecords>, Managed {
|
|
||||||
|
|
||||||
private final S3ObjectMonitor objectMonitor;
|
|
||||||
|
|
||||||
private final AtomicReference<CallDnsRecords> callDnsRecords = new AtomicReference<>();
|
|
||||||
|
|
||||||
private final Timer refreshTimer;
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(CallDnsRecordsManager.class);
|
|
||||||
|
|
||||||
private static final ObjectMapper objectMapper = JsonMapper.builder()
|
|
||||||
.enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
public CallDnsRecordsManager(final ScheduledExecutorService executorService,
|
|
||||||
final AwsCredentialsProvider awsCredentialsProvider, final S3ObjectMonitorFactory configuration) {
|
|
||||||
|
|
||||||
this.objectMonitor = configuration.build(awsCredentialsProvider, executorService);
|
|
||||||
this.callDnsRecords.set(CallDnsRecords.empty());
|
|
||||||
this.refreshTimer = Metrics.timer(MetricsUtil.name(CallDnsRecordsManager.class, "refresh"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleDatabaseChanged(final InputStream inputStream) {
|
|
||||||
refreshTimer.record(() -> {
|
|
||||||
try (final InputStream bufferedInputStream = new BufferedInputStream(inputStream)) {
|
|
||||||
final CallDnsRecords newRecords = parseRecords(bufferedInputStream);
|
|
||||||
final CallDnsRecords oldRecords = callDnsRecords.getAndSet(newRecords);
|
|
||||||
log.info("Replaced dns records, old summary=[{}], new summary=[{}]", oldRecords != null ? oldRecords.getSummary() : "null", newRecords);
|
|
||||||
} catch (final IOException e) {
|
|
||||||
log.error("Failed to load Call DNS Records");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static CallDnsRecords parseRecords(InputStream inputStream) throws IOException {
|
|
||||||
return objectMapper.readValue(inputStream, CallDnsRecords.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() throws Exception {
|
|
||||||
objectMonitor.start(this::handleDatabaseChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() throws Exception {
|
|
||||||
objectMonitor.stop();
|
|
||||||
callDnsRecords.getAndSet(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CallDnsRecords get() {
|
|
||||||
return this.callDnsRecords.get();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,193 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.calls.routing;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.net.Inet4Address;
|
|
||||||
import java.net.Inet6Address;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.TreeMap;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
public class CallRoutingTable {
|
|
||||||
private final TreeMap<Integer, Map<Integer, List<String>>> ipv4Map;
|
|
||||||
private final TreeMap<Integer, Map<BigInteger, List<String>>> ipv6Map;
|
|
||||||
private final Map<GeoKey, List<String>> geoToDatacenter;
|
|
||||||
|
|
||||||
public CallRoutingTable(
|
|
||||||
Map<CidrBlock.IpV4CidrBlock, List<String>> ipv4SubnetToDatacenter,
|
|
||||||
Map<CidrBlock.IpV6CidrBlock, List<String>> ipv6SubnetToDatacenter,
|
|
||||||
Map<GeoKey, List<String>> geoToDatacenter
|
|
||||||
) {
|
|
||||||
this.ipv4Map = new TreeMap<>();
|
|
||||||
for (Map.Entry<CidrBlock.IpV4CidrBlock, List<String>> t : ipv4SubnetToDatacenter.entrySet()) {
|
|
||||||
if (!this.ipv4Map.containsKey(t.getKey().cidrBlockSize())) {
|
|
||||||
this.ipv4Map.put(t.getKey().cidrBlockSize(), new HashMap<>());
|
|
||||||
}
|
|
||||||
this.ipv4Map
|
|
||||||
.get(t.getKey().cidrBlockSize())
|
|
||||||
.put(t.getKey().subnet(), t.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ipv6Map = new TreeMap<>();
|
|
||||||
for (Map.Entry<CidrBlock.IpV6CidrBlock, List<String>> t : ipv6SubnetToDatacenter.entrySet()) {
|
|
||||||
if (!this.ipv6Map.containsKey(t.getKey().cidrBlockSize())) {
|
|
||||||
this.ipv6Map.put(t.getKey().cidrBlockSize(), new HashMap<>());
|
|
||||||
}
|
|
||||||
this.ipv6Map
|
|
||||||
.get(t.getKey().cidrBlockSize())
|
|
||||||
.put(t.getKey().subnet(), t.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.geoToDatacenter = geoToDatacenter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CallRoutingTable empty() {
|
|
||||||
return new CallRoutingTable(Map.of(), Map.of(), Map.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Protocol {
|
|
||||||
v4,
|
|
||||||
v6
|
|
||||||
}
|
|
||||||
|
|
||||||
public record GeoKey(
|
|
||||||
@NotBlank String continent,
|
|
||||||
@NotBlank String country,
|
|
||||||
@NotNull Optional<String> subdivision,
|
|
||||||
@NotBlank Protocol protocol
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns ordered list of fastest datacenters based on IP & Geo info. Prioritize the results based on subnet.
|
|
||||||
* Returns at most three, 2 by subnet and 1 by geo. Takes more from either bucket to hit 3.
|
|
||||||
*/
|
|
||||||
public List<String> getDatacentersFor(
|
|
||||||
InetAddress address,
|
|
||||||
String continent,
|
|
||||||
String country,
|
|
||||||
Optional<String> subdivision
|
|
||||||
) {
|
|
||||||
final int NUM_DATACENTERS = 3;
|
|
||||||
|
|
||||||
if(this.isEmpty()) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> dcsBySubnet = getDatacentersBySubnet(address);
|
|
||||||
List<String> dcsByGeo = getDatacentersByGeo(continent, country, subdivision).stream()
|
|
||||||
.limit(NUM_DATACENTERS)
|
|
||||||
.filter(dc ->
|
|
||||||
(dcsBySubnet.isEmpty() || !dc.equals(dcsBySubnet.getFirst()))
|
|
||||||
&& (dcsBySubnet.size() < 2 || !dc.equals(dcsBySubnet.get(1)))
|
|
||||||
).toList();
|
|
||||||
|
|
||||||
return Stream.concat(
|
|
||||||
dcsBySubnet.stream().limit(dcsByGeo.isEmpty() ? NUM_DATACENTERS : NUM_DATACENTERS - 1),
|
|
||||||
dcsByGeo.stream())
|
|
||||||
.limit(NUM_DATACENTERS)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEmpty() {
|
|
||||||
return this.ipv4Map.isEmpty() && this.ipv6Map.isEmpty() && this.geoToDatacenter.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns ordered list of fastest datacenters based on ip info. Prioritizes V4 connections.
|
|
||||||
*/
|
|
||||||
public List<String> getDatacentersBySubnet(InetAddress address) throws IllegalArgumentException {
|
|
||||||
if(address instanceof Inet4Address) {
|
|
||||||
for(Map.Entry<Integer, Map<Integer, List<String>>> t: this.ipv4Map.descendingMap().entrySet()) {
|
|
||||||
int maskedIp = CidrBlock.IpV4CidrBlock.maskToSize((Inet4Address) address, t.getKey());
|
|
||||||
if(t.getValue().containsKey(maskedIp)) {
|
|
||||||
return t.getValue().get(maskedIp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (address instanceof Inet6Address) {
|
|
||||||
for(Map.Entry<Integer, Map<BigInteger, List<String>>> t: this.ipv6Map.descendingMap().entrySet()) {
|
|
||||||
BigInteger maskedIp = CidrBlock.IpV6CidrBlock.maskToSize((Inet6Address) address, t.getKey());
|
|
||||||
if(t.getValue().containsKey(maskedIp)) {
|
|
||||||
return t.getValue().get(maskedIp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Expected either an Inet4Address or Inet6Address");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns ordered list of fastest datacenters based on geo info. Attempts to match based on subdivision, falls back
|
|
||||||
* to country based lookup. Does not attempt to look for nearby subdivisions. Prioritizes V4 connections.
|
|
||||||
*/
|
|
||||||
public List<String> getDatacentersByGeo(
|
|
||||||
String continent,
|
|
||||||
String country,
|
|
||||||
Optional<String> subdivision
|
|
||||||
) {
|
|
||||||
GeoKey v4Key = new GeoKey(continent, country, subdivision, Protocol.v4);
|
|
||||||
List<String> v4Options = this.geoToDatacenter.getOrDefault(v4Key, Collections.emptyList());
|
|
||||||
List<String> v4OptionsBackup = v4Options.isEmpty() && subdivision.isPresent() ?
|
|
||||||
this.geoToDatacenter.getOrDefault(
|
|
||||||
new GeoKey(continent, country, Optional.empty(), Protocol.v4),
|
|
||||||
Collections.emptyList())
|
|
||||||
: Collections.emptyList();
|
|
||||||
|
|
||||||
GeoKey v6Key = new GeoKey(continent, country, subdivision, Protocol.v6);
|
|
||||||
List<String> v6Options = this.geoToDatacenter.getOrDefault(v6Key, Collections.emptyList());
|
|
||||||
List<String> v6OptionsBackup = v6Options.isEmpty() && subdivision.isPresent() ?
|
|
||||||
this.geoToDatacenter.getOrDefault(
|
|
||||||
new GeoKey(continent, country, Optional.empty(), Protocol.v6),
|
|
||||||
Collections.emptyList())
|
|
||||||
: Collections.emptyList();
|
|
||||||
|
|
||||||
return Stream.of(
|
|
||||||
v4Options.stream(),
|
|
||||||
v6Options.stream(),
|
|
||||||
v4OptionsBackup.stream(),
|
|
||||||
v6OptionsBackup.stream()
|
|
||||||
)
|
|
||||||
.flatMap(Function.identity())
|
|
||||||
.distinct()
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toSummaryString() {
|
|
||||||
return String.format(
|
|
||||||
"[Ipv4Table=%s rows, Ipv6Table=%s rows, GeoTable=%s rows]",
|
|
||||||
ipv4Map.size(),
|
|
||||||
ipv6Map.size(),
|
|
||||||
geoToDatacenter.size()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(final Object o) {
|
|
||||||
if (this == o)
|
|
||||||
return true;
|
|
||||||
if (o == null || getClass() != o.getClass())
|
|
||||||
return false;
|
|
||||||
CallRoutingTable that = (CallRoutingTable) o;
|
|
||||||
return Objects.equals(ipv4Map, that.ipv4Map) && Objects.equals(ipv6Map, that.ipv6Map) && Objects.equals(
|
|
||||||
geoToDatacenter, that.geoToDatacenter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hash(ipv4Map, ipv6Map, geoToDatacenter);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.calls.routing;
|
|
||||||
|
|
||||||
import io.dropwizard.lifecycle.Managed;
|
|
||||||
import io.micrometer.core.instrument.Metrics;
|
|
||||||
import io.micrometer.core.instrument.Timer;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
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 CallRoutingTableManager implements Supplier<CallRoutingTable>, Managed {
|
|
||||||
|
|
||||||
private final S3ObjectMonitor objectMonitor;
|
|
||||||
|
|
||||||
private final AtomicReference<CallRoutingTable> routingTable = new AtomicReference<>();
|
|
||||||
|
|
||||||
private final String tableTag;
|
|
||||||
|
|
||||||
private final Timer refreshTimer;
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(CallRoutingTableManager.class);
|
|
||||||
|
|
||||||
public CallRoutingTableManager(final ScheduledExecutorService executorService,
|
|
||||||
final AwsCredentialsProvider awsCredentialsProvider, final S3ObjectMonitorFactory configuration,
|
|
||||||
final String tableTag) {
|
|
||||||
|
|
||||||
this.objectMonitor = configuration.build(awsCredentialsProvider, executorService);
|
|
||||||
this.tableTag = tableTag;
|
|
||||||
this.routingTable.set(CallRoutingTable.empty());
|
|
||||||
this.refreshTimer = Metrics.timer(MetricsUtil.name(CallRoutingTableManager.class, tableTag));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleDatabaseChanged(final InputStream inputStream) {
|
|
||||||
refreshTimer.record(() -> {
|
|
||||||
try(InputStreamReader reader = new InputStreamReader(inputStream)) {
|
|
||||||
CallRoutingTable newTable = CallRoutingTableParser.fromJson(reader);
|
|
||||||
this.routingTable.set(newTable);
|
|
||||||
log.info("Replaced {} call routing table: {}", tableTag, newTable.toSummaryString());
|
|
||||||
} catch (final IOException e) {
|
|
||||||
log.error("Failed to parse and update {} call routing table", tableTag);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() throws Exception {
|
|
||||||
objectMonitor.start(this::handleDatabaseChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() throws Exception {
|
|
||||||
objectMonitor.stop();
|
|
||||||
routingTable.getAndSet(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CallRoutingTable get() {
|
|
||||||
return this.routingTable.get();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,185 +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.StreamReadFeature;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.Reader;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
|
|
||||||
final class CallRoutingTableParser {
|
|
||||||
|
|
||||||
private final static int IPV4_DEFAULT_BLOCK_SIZE = 24;
|
|
||||||
private final static int IPV6_DEFAULT_BLOCK_SIZE = 48;
|
|
||||||
private static final ObjectMapper objectMapper = JsonMapper.builder()
|
|
||||||
.enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
/** Used for parsing JSON */
|
|
||||||
private static class RawCallRoutingTable {
|
|
||||||
public Map<String, List<String>> ipv4GeoToDataCenters = Map.of();
|
|
||||||
public Map<String, List<String>> ipv6GeoToDataCenters = Map.of();
|
|
||||||
public Map<String, List<String>> ipv4SubnetsToDatacenters = Map.of();
|
|
||||||
public Map<String, List<String>> ipv6SubnetsToDatacenters = Map.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
private final static String WHITESPACE_REGEX = "\\s+";
|
|
||||||
|
|
||||||
public static CallRoutingTable fromJson(final Reader inputReader) throws IOException {
|
|
||||||
try (final BufferedReader reader = new BufferedReader(inputReader)) {
|
|
||||||
RawCallRoutingTable rawTable = objectMapper.readValue(reader, RawCallRoutingTable.class);
|
|
||||||
|
|
||||||
Map<CidrBlock.IpV4CidrBlock, List<String>> ipv4SubnetToDatacenter = rawTable.ipv4SubnetsToDatacenters
|
|
||||||
.entrySet()
|
|
||||||
.stream()
|
|
||||||
.collect(Collectors.toUnmodifiableMap(
|
|
||||||
e -> (CidrBlock.IpV4CidrBlock) CidrBlock.parseCidrBlock(e.getKey(), IPV4_DEFAULT_BLOCK_SIZE),
|
|
||||||
Map.Entry::getValue
|
|
||||||
));
|
|
||||||
|
|
||||||
Map<CidrBlock.IpV6CidrBlock, List<String>> ipv6SubnetToDatacenter = rawTable.ipv6SubnetsToDatacenters
|
|
||||||
.entrySet()
|
|
||||||
.stream()
|
|
||||||
.collect(Collectors.toUnmodifiableMap(
|
|
||||||
e -> (CidrBlock.IpV6CidrBlock) CidrBlock.parseCidrBlock(e.getKey(), IPV6_DEFAULT_BLOCK_SIZE),
|
|
||||||
Map.Entry::getValue
|
|
||||||
));
|
|
||||||
|
|
||||||
Map<CallRoutingTable.GeoKey, List<String>> geoToDatacenter = Stream.concat(
|
|
||||||
rawTable.ipv4GeoToDataCenters
|
|
||||||
.entrySet()
|
|
||||||
.stream()
|
|
||||||
.map(e -> Map.entry(parseRawGeoKey(e.getKey(), CallRoutingTable.Protocol.v4), e.getValue())),
|
|
||||||
rawTable.ipv6GeoToDataCenters
|
|
||||||
.entrySet()
|
|
||||||
.stream()
|
|
||||||
.map(e -> Map.entry(parseRawGeoKey(e.getKey(), CallRoutingTable.Protocol.v6), e.getValue()))
|
|
||||||
).collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));
|
|
||||||
|
|
||||||
return new CallRoutingTable(
|
|
||||||
ipv4SubnetToDatacenter,
|
|
||||||
ipv6SubnetToDatacenter,
|
|
||||||
geoToDatacenter
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CallRoutingTable.GeoKey parseRawGeoKey(String rawKey, CallRoutingTable.Protocol protocol) {
|
|
||||||
String[] splits = rawKey.split("-");
|
|
||||||
if (splits.length < 2 || splits.length > 3) {
|
|
||||||
throw new IllegalArgumentException("Invalid raw key");
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<String> subdivision = splits.length < 3 ? Optional.empty() : Optional.of(splits[2]);
|
|
||||||
return new CallRoutingTable.GeoKey(splits[0], splits[1], subdivision, protocol);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a call routing table in TSV format. Example below - see tests for more examples:
|
|
||||||
192.0.2.0/24 northamerica-northeast1
|
|
||||||
198.51.100.0/24 us-south1
|
|
||||||
203.0.113.0/24 asia-southeast1
|
|
||||||
|
|
||||||
2001:db8:b0a9::/48 us-east4
|
|
||||||
2001:db8:b0f5::/48 us-central1 northamerica-northeast1 us-east4
|
|
||||||
2001:db8:9406::/48 us-east1 us-central1
|
|
||||||
|
|
||||||
SA-SR-v4 us-east1 us-east4
|
|
||||||
SA-SR-v6 us-east1 us-south1
|
|
||||||
SA-UY-v4 southamerica-west1 southamerica-east1 europe-west3
|
|
||||||
SA-UY-v6 southamerica-west1 europe-west4
|
|
||||||
SA-VE-v4 us-east1 us-east4 us-south1
|
|
||||||
SA-VE-v6 us-east1 northamerica-northeast1 us-east4
|
|
||||||
ZZ-ZZ-v4 asia-south1 europe-southwest1 australia-southeast1
|
|
||||||
*/
|
|
||||||
public static CallRoutingTable fromTsv(final Reader inputReader) throws IOException {
|
|
||||||
try (final BufferedReader reader = new BufferedReader(inputReader)) {
|
|
||||||
// use maps to silently dedupe CidrBlocks
|
|
||||||
Map<CidrBlock.IpV4CidrBlock, List<String>> ipv4Map = new HashMap<>();
|
|
||||||
Map<CidrBlock.IpV6CidrBlock, List<String>> ipv6Map = new HashMap<>();
|
|
||||||
Map<CallRoutingTable.GeoKey, List<String>> ipGeoTable = new HashMap<>();
|
|
||||||
String line;
|
|
||||||
while((line = reader.readLine()) != null) {
|
|
||||||
if(line.isBlank()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> splits = Arrays.stream(line.split(WHITESPACE_REGEX)).filter(s -> !s.isBlank()).toList();
|
|
||||||
if (splits.size() < 2) {
|
|
||||||
throw new IllegalStateException("Invalid row, expected some key and list of values");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> datacenters = splits.subList(1, splits.size());
|
|
||||||
switch (guessLineType(splits)) {
|
|
||||||
case v4 -> {
|
|
||||||
CidrBlock cidrBlock = CidrBlock.parseCidrBlock(splits.getFirst());
|
|
||||||
if(!(cidrBlock instanceof CidrBlock.IpV4CidrBlock)) {
|
|
||||||
throw new IllegalArgumentException("Expected an ipv4 cidr block");
|
|
||||||
}
|
|
||||||
ipv4Map.put((CidrBlock.IpV4CidrBlock) cidrBlock, datacenters);
|
|
||||||
}
|
|
||||||
case v6 -> {
|
|
||||||
CidrBlock cidrBlock = CidrBlock.parseCidrBlock(splits.getFirst());
|
|
||||||
if(!(cidrBlock instanceof CidrBlock.IpV6CidrBlock)) {
|
|
||||||
throw new IllegalArgumentException("Expected an ipv6 cidr block");
|
|
||||||
}
|
|
||||||
ipv6Map.put((CidrBlock.IpV6CidrBlock) cidrBlock, datacenters);
|
|
||||||
}
|
|
||||||
case Geo -> {
|
|
||||||
String[] geo = splits.getFirst().split("-");
|
|
||||||
if(geo.length < 3) {
|
|
||||||
throw new IllegalStateException("Geo row key invalid, expected atleast continent, country, and protocol");
|
|
||||||
}
|
|
||||||
String continent = geo[0];
|
|
||||||
String country = geo[1];
|
|
||||||
Optional<String> subdivision = geo.length > 3 ? Optional.of(geo[2]) : Optional.empty();
|
|
||||||
CallRoutingTable.Protocol protocol = CallRoutingTable.Protocol.valueOf(geo[geo.length - 1].toLowerCase());
|
|
||||||
CallRoutingTable.GeoKey tableKey = new CallRoutingTable.GeoKey(
|
|
||||||
continent,
|
|
||||||
country,
|
|
||||||
subdivision,
|
|
||||||
protocol
|
|
||||||
);
|
|
||||||
ipGeoTable.put(tableKey, datacenters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CallRoutingTable(
|
|
||||||
ipv4Map,
|
|
||||||
ipv6Map,
|
|
||||||
ipGeoTable
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LineType guessLineType(List<String> splits) {
|
|
||||||
String first = splits.getFirst();
|
|
||||||
if (first.contains("-")) {
|
|
||||||
return LineType.Geo;
|
|
||||||
} else if(first.contains(":")) {
|
|
||||||
return LineType.v6;
|
|
||||||
} else if (first.contains(".")) {
|
|
||||||
return LineType.v4;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new IllegalArgumentException(String.format("Invalid line, could not determine type from '%s'", first));
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum LineType {
|
|
||||||
v4, v6, Geo
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,137 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.calls.routing;
|
|
||||||
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.net.Inet4Address;
|
|
||||||
import java.net.Inet6Address;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Can be used to check if an IP is in the CIDR block
|
|
||||||
*/
|
|
||||||
public interface CidrBlock {
|
|
||||||
|
|
||||||
boolean ipInBlock(InetAddress address);
|
|
||||||
|
|
||||||
static CidrBlock parseCidrBlock(String cidrBlock, int defaultBlockSize) {
|
|
||||||
String[] splits = cidrBlock.split("/");
|
|
||||||
if(splits.length > 2) {
|
|
||||||
throw new IllegalArgumentException("Invalid cidr block format, expected {address}/{blocksize}");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
int blockSize = splits.length == 2 ? Integer.parseInt(splits[1]) : defaultBlockSize;
|
|
||||||
return parseCidrBlockInner(splits[0], blockSize);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
throw new IllegalArgumentException(String.format("Invalid block size specified: '%s'", splits[1]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static CidrBlock parseCidrBlock(String cidrBlock) {
|
|
||||||
String[] splits = cidrBlock.split("/");
|
|
||||||
if (splits.length != 2) {
|
|
||||||
throw new IllegalArgumentException("Invalid cidr block format, expected {address}/{blocksize}");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
int blockSize = Integer.parseInt(splits[1]);
|
|
||||||
return parseCidrBlockInner(splits[0], blockSize);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
throw new IllegalArgumentException(String.format("Invalid block size specified: '%s'", splits[1]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CidrBlock parseCidrBlockInner(String rawAddress, int blockSize) {
|
|
||||||
try {
|
|
||||||
InetAddress address = InetAddress.getByName(rawAddress);
|
|
||||||
if(address instanceof Inet4Address) {
|
|
||||||
return IpV4CidrBlock.of((Inet4Address) address, blockSize);
|
|
||||||
} else if (address instanceof Inet6Address) {
|
|
||||||
return IpV6CidrBlock.of((Inet6Address) address, blockSize);
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Must be an ipv4 or ipv6 string");
|
|
||||||
}
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
throw new IllegalArgumentException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
record IpV4CidrBlock(int subnet, int subnetMask, int cidrBlockSize) implements CidrBlock {
|
|
||||||
public static IpV4CidrBlock of(Inet4Address subnet, int cidrBlockSize) {
|
|
||||||
if(cidrBlockSize > 32 || cidrBlockSize < 0) {
|
|
||||||
throw new IllegalArgumentException("Invalid cidrBlockSize");
|
|
||||||
}
|
|
||||||
|
|
||||||
int subnetMask = mask(cidrBlockSize);
|
|
||||||
int maskedIp = ipToInt(subnet) & subnetMask;
|
|
||||||
return new IpV4CidrBlock(maskedIp, subnetMask, cidrBlockSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean ipInBlock(InetAddress address) {
|
|
||||||
if(!(address instanceof Inet4Address)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
int ip = ipToInt((Inet4Address) address);
|
|
||||||
return (ip & subnetMask) == subnet;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int ipToInt(Inet4Address address) {
|
|
||||||
byte[] octets = address.getAddress();
|
|
||||||
return (octets[0] & 0xff) << 24 |
|
|
||||||
(octets[1] & 0xff) << 16 |
|
|
||||||
(octets[2] & 0xff) << 8 |
|
|
||||||
octets[3] & 0xff;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int mask(int cidrBlockSize) {
|
|
||||||
return (int) (-1L << (32 - cidrBlockSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int maskToSize(Inet4Address address, int cidrBlockSize) {
|
|
||||||
return ipToInt(address) & mask(cidrBlockSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
record IpV6CidrBlock(BigInteger subnet, BigInteger subnetMask, int cidrBlockSize) implements CidrBlock {
|
|
||||||
|
|
||||||
private static final BigInteger MINUS_ONE = BigInteger.valueOf(-1);
|
|
||||||
|
|
||||||
public static IpV6CidrBlock of(Inet6Address subnet, int cidrBlockSize) {
|
|
||||||
if(cidrBlockSize > 128 || cidrBlockSize < 0) {
|
|
||||||
throw new IllegalArgumentException("Invalid cidrBlockSize");
|
|
||||||
}
|
|
||||||
|
|
||||||
BigInteger subnetMask = mask(cidrBlockSize);
|
|
||||||
BigInteger maskedIp = ipToInt(subnet).and(subnetMask);
|
|
||||||
return new IpV6CidrBlock(maskedIp, subnetMask, cidrBlockSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean ipInBlock(InetAddress address) {
|
|
||||||
if(!(address instanceof Inet6Address)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
BigInteger ip = ipToInt((Inet6Address) address);
|
|
||||||
return ip.and(subnetMask).equals(subnet);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static BigInteger ipToInt(Inet6Address ipAddress) {
|
|
||||||
byte[] octets = ipAddress.getAddress();
|
|
||||||
assert octets.length == 16;
|
|
||||||
|
|
||||||
return new BigInteger(octets);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static BigInteger mask(int cidrBlockSize) {
|
|
||||||
return MINUS_ONE.shiftLeft(128 - cidrBlockSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static BigInteger maskToSize(Inet6Address address, int cidrBlockSize) {
|
|
||||||
return ipToInt(address).and(mask(cidrBlockSize));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.calls.routing;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public record TurnServerOptions(
|
|
||||||
String hostname,
|
|
||||||
Optional<List<String>> urlsWithIps,
|
|
||||||
Optional<List<String>> urlsWithHostname
|
|
||||||
) {
|
|
||||||
}
|
|
Loading…
Reference in New Issue