Introduce an ASN-to-IP manager.

This commit is contained in:
Jon Chambers 2021-05-17 18:40:07 -04:00 committed by Jon Chambers
parent 1160af9522
commit f8c623074b
8 changed files with 171 additions and 10 deletions

View File

@ -40,7 +40,7 @@ import org.whispersystems.textsecuregcm.configuration.RemoteConfigConfiguration;
import org.whispersystems.textsecuregcm.configuration.SecureBackupServiceConfiguration;
import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration;
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
import org.whispersystems.textsecuregcm.configuration.TorExitNodeConfiguration;
import org.whispersystems.textsecuregcm.configuration.MonitoredS3ObjectConfiguration;
import org.whispersystems.textsecuregcm.configuration.TurnConfiguration;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration;
@ -259,7 +259,12 @@ public class WhisperServerConfiguration extends Configuration {
@Valid
@NotNull
@JsonProperty
private TorExitNodeConfiguration tor;
private MonitoredS3ObjectConfiguration torExitNodeList;
@Valid
@NotNull
@JsonProperty
private MonitoredS3ObjectConfiguration asnTable;
@Valid
@NotNull
@ -450,8 +455,12 @@ public class WhisperServerConfiguration extends Configuration {
return reportMessageDynamoDb;
}
public TorExitNodeConfiguration getTorExitNodeConfiguration() {
return tor;
public MonitoredS3ObjectConfiguration getTorExitNodeListConfiguration() {
return torExitNodeList;
}
public MonitoredS3ObjectConfiguration getAsnTableConfiguration() {
return asnTable;
}
public DonationConfiguration getDonationConfiguration() {

View File

@ -186,6 +186,7 @@ import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
import org.whispersystems.textsecuregcm.storage.ReservedUsernames;
import org.whispersystems.textsecuregcm.storage.Usernames;
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
import org.whispersystems.textsecuregcm.util.AsnManager;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.TorExitNodeManager;
import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener;
@ -436,7 +437,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
GCMSender gcmSender = new GCMSender(gcmSenderExecutor, accountsManager, config.getGcmConfiguration().getApiKey());
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), dynamicConfigurationManager, rateLimitersCluster);
ProvisioningManager provisioningManager = new ProvisioningManager(pubSubManager);
TorExitNodeManager torExitNodeManager = new TorExitNodeManager(recurringJobExecutor, config.getTorExitNodeConfiguration());
TorExitNodeManager torExitNodeManager = new TorExitNodeManager(recurringJobExecutor, config.getTorExitNodeListConfiguration());
AsnManager asnManager = new AsnManager(recurringJobExecutor, config.getAsnTableConfiguration());
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
DisabledPermittedAccountAuthenticator disabledPermittedAccountAuthenticator = new DisabledPermittedAccountAuthenticator(accountsManager);
@ -489,6 +491,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.lifecycle().manage(clientPresenceManager);
environment.lifecycle().manage(currencyManager);
environment.lifecycle().manage(torExitNodeManager);
environment.lifecycle().manage(asnManager);
AWSCredentials credentials = new BasicAWSCredentials(config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret());
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);

View File

@ -11,7 +11,7 @@ import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import java.time.Duration;
public class TorExitNodeConfiguration {
public class MonitoredS3ObjectConfiguration {
@JsonProperty
@NotBlank

View File

@ -0,0 +1,98 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.util;
import static com.codahale.metrics.MetricRegistry.name;
import com.amazonaws.services.s3.model.S3Object;
import com.google.common.annotations.VisibleForTesting;
import io.dropwizard.lifecycle.Managed;
import io.micrometer.core.instrument.Counter;
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.net.Inet4Address;
import java.net.UnknownHostException;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicReference;
import java.util.zip.GZIPInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.MonitoredS3ObjectConfiguration;
public class AsnManager implements Managed {
private final S3ObjectMonitor asnTableMonitor;
private final AtomicReference<AsnTable> asnTable = new AtomicReference<>(AsnTable.EMPTY);
private static final Timer REFRESH_TIMER = Metrics.timer(name(AsnManager.class, "refresh"));
private static final Counter REFRESH_ERRORS = Metrics.counter(name(AsnManager.class, "refreshErrors"));
private static final Logger log = LoggerFactory.getLogger(AsnManager.class);
public AsnManager(
final ScheduledExecutorService scheduledExecutorService,
final MonitoredS3ObjectConfiguration configuration) {
this.asnTableMonitor = new S3ObjectMonitor(
configuration.getS3Region(),
configuration.getS3Bucket(),
configuration.getObjectKey(),
configuration.getMaxSize(),
scheduledExecutorService,
configuration.getRefreshInterval(),
this::handleAsnTableChanged);
}
@Override
public void start() throws Exception {
try {
handleAsnTableChanged(asnTableMonitor.getObject());
} catch (final Exception e) {
log.warn("Failed to load initial IP-to-ASN map", e);
}
asnTableMonitor.start();
}
@Override
public void stop() throws Exception {
asnTableMonitor.stop();
}
public Optional<Long> getAsn(final String address) {
try {
return asnTable.get().getAsn((Inet4Address) Inet4Address.getByName(address));
} catch (final UnknownHostException e) {
log.warn("Could not parse \"{}\" as an Inet4Address", address);
return Optional.empty();
}
}
private void handleAsnTableChanged(final S3Object asnTableObject) {
REFRESH_TIMER.record(() -> {
try {
handleAsnTableChanged(new GZIPInputStream(asnTableObject.getObjectContent()));
} catch (final IOException e) {
log.error("Retrieved object was not a gzip archive", e);
}
});
}
@VisibleForTesting
void handleAsnTableChanged(final InputStream inputStream) {
try (final InputStreamReader reader = new InputStreamReader(inputStream)) {
asnTable.set(new AsnTable(reader));
} catch (final Exception e) {
REFRESH_ERRORS.increment();
log.warn("Failed to refresh IP-to-ASN table", e);
}
}
}

View File

@ -44,6 +44,8 @@ class AsnTable {
}
}
public static final AsnTable EMPTY = new AsnTable();
public AsnTable(final Reader tsvReader) throws IOException {
final TreeMap<Long, AsnRange> treeMap = new TreeMap<>();
@ -60,6 +62,10 @@ class AsnTable {
asnBlocksByFirstIp = treeMap;
}
private AsnTable() {
asnBlocksByFirstIp = new TreeMap<>();
}
public Optional<Long> getAsn(final Inet4Address address) {
final long addressAsLong = ipToLong(address);

View File

@ -24,7 +24,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.TorExitNodeConfiguration;
import org.whispersystems.textsecuregcm.configuration.MonitoredS3ObjectConfiguration;
/**
* A utility for checking whether IP addresses belong to Tor exit nodes using the "bulk exit list."
@ -44,7 +44,7 @@ public class TorExitNodeManager implements Managed {
public TorExitNodeManager(
final ScheduledExecutorService scheduledExecutorService,
final TorExitNodeConfiguration configuration) {
final MonitoredS3ObjectConfiguration configuration) {
this.exitListMonitor = new S3ObjectMonitor(
configuration.getS3Region(),

View File

@ -0,0 +1,45 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.util;
import org.junit.jupiter.api.Test;
import org.whispersystems.textsecuregcm.configuration.MonitoredS3ObjectConfiguration;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Inet4Address;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
class AsnManagerTest {
@Test
void getAsn() throws IOException {
final MonitoredS3ObjectConfiguration configuration = new MonitoredS3ObjectConfiguration();
configuration.setS3Region("ap-northeast-3");
final AsnManager asnManager = new AsnManager(mock(ScheduledExecutorService.class), configuration);
assertEquals(Optional.empty(), asnManager.getAsn("10.0.0.1"));
try (final InputStream tableInputStream = getClass().getResourceAsStream("ip2asn-test.tsv")) {
asnManager.handleAsnTableChanged(tableInputStream);
}
assertEquals(Optional.of(7922L), asnManager.getAsn("50.79.54.1"));
assertEquals(Optional.of(7552L), asnManager.getAsn("27.79.32.1"));
assertEquals(Optional.empty(), asnManager.getAsn("32.79.117.1"));
assertEquals(Optional.empty(), asnManager.getAsn("10.0.0.1"));
}
}

View File

@ -13,14 +13,14 @@ import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ScheduledExecutorService;
import org.junit.Test;
import org.whispersystems.textsecuregcm.configuration.TorExitNodeConfiguration;
import org.whispersystems.textsecuregcm.configuration.MonitoredS3ObjectConfiguration;
import org.whispersystems.textsecuregcm.redis.AbstractRedisClusterTest;
public class TorExitNodeManagerTest extends AbstractRedisClusterTest {
@Test
public void testIsTorExitNode() {
final TorExitNodeConfiguration configuration = new TorExitNodeConfiguration();
final MonitoredS3ObjectConfiguration configuration = new MonitoredS3ObjectConfiguration();
configuration.setS3Region("ap-northeast-3");
final TorExitNodeManager torExitNodeManager =