Remove latency based 1:1 call routing

This commit is contained in:
Adel Lahlou 2025-06-25 15:08:11 -07:00 committed by Ameya Lokare
parent 7260a9d5b4
commit d4322a2ed4
7 changed files with 0 additions and 718 deletions

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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
}
}

View File

@ -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));
}
}
}

View File

@ -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
) {
}