diff --git a/service/pom.xml b/service/pom.xml
index 035b54a75..7d6d230c2 100644
--- a/service/pom.xml
+++ b/service/pom.xml
@@ -163,6 +163,12 @@
3.12.0
+
+ org.apache.commons
+ commons-csv
+ 1.8
+
+
org.glassfish.jersey.test-framework.providers
jersey-test-framework-provider-grizzly2
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/util/AsnTable.java b/service/src/main/java/org/whispersystems/textsecuregcm/util/AsnTable.java
new file mode 100644
index 000000000..b3387565d
--- /dev/null
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/util/AsnTable.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2021 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.util;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.IOException;
+import java.io.Reader;
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+import java.util.NavigableMap;
+import java.util.Optional;
+import java.util.TreeMap;
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVParser;
+import org.apache.commons.csv.CSVRecord;
+
+/**
+ * Allows IP->ASN lookup operations using data from https://iptoasn.com/.
+ */
+class AsnTable {
+ private final NavigableMap asnBlocksByFirstIp;
+
+ private static class AsnRange {
+ private final long rangeStart;
+ private final long rangeEnd;
+
+ private final long asn;
+
+ private AsnRange(long rangeStart, long rangeEnd, long asn) {
+ this.rangeStart = rangeStart;
+ this.rangeEnd = rangeEnd;
+ this.asn = asn;
+ }
+
+ boolean contains(final long address) {
+ return address >= rangeStart && address <= rangeEnd;
+ }
+
+ long getAsn() {
+ return asn;
+ }
+ }
+
+ public AsnTable(final Reader tsvReader) throws IOException {
+ final TreeMap treeMap = new TreeMap<>();
+
+ try (final CSVParser csvParser = CSVFormat.TDF.parse(tsvReader)) {
+ for (final CSVRecord record : csvParser) {
+ final long start = Long.parseLong(record.get(0), 10);
+ final long end = Long.parseLong(record.get(1), 10);
+ final long asn = Long.parseLong(record.get(2), 10);
+
+ treeMap.put(start, new AsnRange(start, end, asn));
+ }
+ }
+
+ asnBlocksByFirstIp = treeMap;
+ }
+
+ public Optional getAsn(final Inet4Address address) {
+ final long addressAsLong = ipToLong(address);
+
+ return Optional.ofNullable(asnBlocksByFirstIp.floorEntry(addressAsLong))
+ .filter(entry -> entry.getValue().contains(addressAsLong))
+ .map(entry -> entry.getValue().getAsn());
+ }
+
+ @VisibleForTesting
+ static long ipToLong(final Inet4Address address) {
+ final ByteBuffer buffer = ByteBuffer.allocate(8);
+ buffer.position(4);
+ buffer.put(address.getAddress());
+
+ buffer.flip();
+ return buffer.getLong();
+ }
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/util/AsnTableTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/util/AsnTableTest.java
new file mode 100644
index 000000000..edf19385b
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/util/AsnTableTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.util;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.Inet4Address;
+import java.net.UnknownHostException;
+import java.util.Optional;
+import java.util.zip.GZIPInputStream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AsnTableTest {
+
+ @Test
+ void getAsn() throws IOException {
+ try (final InputStreamReader reader = new InputStreamReader(getClass().getResourceAsStream("ip2asn-test.tsv"))) {
+ final AsnTable asnTable = new AsnTable(reader);
+
+ assertEquals(Optional.of(7922L), asnTable.getAsn((Inet4Address) Inet4Address.getByName("50.79.54.1")));
+ assertEquals(Optional.of(7552L), asnTable.getAsn((Inet4Address) Inet4Address.getByName("27.79.32.1")));
+ assertEquals(Optional.empty(), asnTable.getAsn((Inet4Address) Inet4Address.getByName("32.79.117.1")));
+ assertEquals(Optional.empty(), asnTable.getAsn((Inet4Address) Inet4Address.getByName("10.0.0.1")));
+ }
+ }
+
+ @Test
+ void ipToLong() throws UnknownHostException {
+ assertEquals(0x00000000ffffffffL, AsnTable.ipToLong((Inet4Address) Inet4Address.getByName("255.255.255.255")));
+ assertEquals(0x0000000000000001L, AsnTable.ipToLong((Inet4Address) Inet4Address.getByName("0.0.0.1")));
+ }
+}
diff --git a/service/src/test/resources/org/whispersystems/textsecuregcm/util/ip2asn-test.tsv b/service/src/test/resources/org/whispersystems/textsecuregcm/util/ip2asn-test.tsv
new file mode 100644
index 000000000..d248be4fd
--- /dev/null
+++ b/service/src/test/resources/org/whispersystems/textsecuregcm/util/ip2asn-test.tsv
@@ -0,0 +1,2 @@
+458051584 458227711 7552 VN VIETEL-AS-AP Viettel Group
+843841536 844103679 7922 US COMCAST-7922 - Comcast Cable Communications, LLC