Skip to content

Commit

Permalink
add new dependency commons-net to bom.xml;
Browse files Browse the repository at this point in the history
extend HostValidator to be able to block specific subnets;
extend ConnectionsConfig with blockedSubnets config;
add unit test to HostValidatorTest;

Signed-off-by: Stefan Maute <stefan.maute@bosch.io>
  • Loading branch information
Stefan Maute committed Jul 13, 2021
1 parent 5d5c4db commit 8d08360
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 14 deletions.
7 changes: 7 additions & 0 deletions bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
<reactive-streams.version>1.0.3</reactive-streams.version>
<netty-bom.version>4.1.65.Final</netty-bom.version>
<cloundevents.version>2.0.0</cloundevents.version>
<commons-net.version>3.8.0</commons-net.version>

<slf4j.version>1.7.30</slf4j.version>
<logback.version>1.2.3</logback.version>
Expand Down Expand Up @@ -257,6 +258,12 @@
<version>${cloundevents.version}</version>
</dependency>

<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>${commons-net.version}</version>
</dependency>

<!-- ### Indirect "runtime" dependencies we want to pin to a common version -->
<dependency>
<groupId>org.scala-lang</groupId>
Expand Down
4 changes: 4 additions & 0 deletions connectivity/service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@
<groupId>org.apache.qpid</groupId>
<artifactId>qpid-jms-client</artifactId>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
</dependency>
<dependency>
<groupId>com.hivemq</groupId>
<artifactId>hivemq-mqtt-client</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public interface ConnectionConfig extends WithSupervisorConfig, WithActivityChec
*/
Collection<String> getBlockedHostnames();

/**
* @return the list of blocked subnets to which outgoing connections are prevented.
*/
Collection<String> getBlockedSubnets();

/**
* Returns the config of the connection snapshotting behaviour.
*
Expand Down Expand Up @@ -174,6 +179,11 @@ enum ConnectionConfigValue implements KnownConfigValue {
*/
BLOCKED_HOSTNAMES("blocked-hostnames", ""),

/**
* A comma separated list of blocked subnets to which no http requests will be sent out.
*/
BLOCKED_SUBNETS("blocked-subnets", ""),

/**
* The limitation number of sources within a connection.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public final class DefaultConnectionConfig implements ConnectionConfig {
private final int clientActorRestartsBeforeEscalation;
private final Collection<String> allowedHostnames;
private final Collection<String> blockedHostnames;
private final Collection<String> blockedSubnets;
private final SupervisorConfig supervisorConfig;
private final SnapshotConfig snapshotConfig;
private final DefaultAcknowledgementConfig acknowledgementConfig;
Expand All @@ -64,6 +65,7 @@ private DefaultConnectionConfig(final ConfigWithFallback config) {
config.getPositiveIntOrThrow(ConnectionConfigValue.CLIENT_ACTOR_RESTARTS_BEFORE_ESCALATION);
allowedHostnames = fromCommaSeparatedString(config, ConnectionConfigValue.ALLOWED_HOSTNAMES);
blockedHostnames = fromCommaSeparatedString(config, ConnectionConfigValue.BLOCKED_HOSTNAMES);
blockedSubnets = fromCommaSeparatedString(config, ConnectionConfigValue.BLOCKED_SUBNETS);
supervisorConfig = DefaultSupervisorConfig.of(config);
snapshotConfig = DefaultSnapshotConfig.of(config);
acknowledgementConfig = DefaultAcknowledgementConfig.of(config);
Expand Down Expand Up @@ -122,6 +124,11 @@ public Collection<String> getBlockedHostnames() {
return blockedHostnames;
}

@Override
public Collection<String> getBlockedSubnets() {
return blockedSubnets;
}

@Override
public SupervisorConfig getSupervisorConfig() {
return supervisorConfig;
Expand Down Expand Up @@ -205,6 +212,7 @@ public boolean equals(final Object o) {
Objects.equals(clientActorRestartsBeforeEscalation, that.clientActorRestartsBeforeEscalation) &&
Objects.equals(allowedHostnames, that.allowedHostnames) &&
Objects.equals(blockedHostnames, that.blockedHostnames) &&
Objects.equals(blockedSubnets, that.blockedSubnets) &&
Objects.equals(supervisorConfig, that.supervisorConfig) &&
Objects.equals(snapshotConfig, that.snapshotConfig) &&
Objects.equals(acknowledgementConfig, that.acknowledgementConfig) &&
Expand All @@ -224,7 +232,7 @@ public boolean equals(final Object o) {
@Override
public int hashCode() {
return Objects.hash(clientActorAskTimeout, clientActorRestartsBeforeEscalation, allowedHostnames,
blockedHostnames, supervisorConfig, snapshotConfig, acknowledgementConfig, maxNumberOfTargets,
blockedHostnames, blockedSubnets, supervisorConfig, snapshotConfig, acknowledgementConfig, maxNumberOfTargets,
maxNumberOfSources, activityCheckConfig, amqp10Config, amqp091Config, mqttConfig, kafkaConfig,
httpPushConfig, ackLabelDeclareInterval, priorityUpdateInterval, allClientActorsOnOneNode);
}
Expand All @@ -236,6 +244,7 @@ public String toString() {
", clientActorRestartsBeforeEscalation=" + clientActorRestartsBeforeEscalation +
", allowedHostnames=" + allowedHostnames +
", blockedHostnames=" + blockedHostnames +
", blockedSubnets=" + blockedSubnets +
", supervisorConfig=" + supervisorConfig +
", snapshotConfig=" + snapshotConfig +
", acknowledgementConfig=" + acknowledgementConfig +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.net.util.SubnetUtils;
import org.eclipse.ditto.connectivity.service.config.ConnectivityConfig;

import akka.event.LoggingAdapter;
Expand All @@ -33,12 +34,13 @@ final class DefaultHostValidator implements HostValidator {

private final Collection<String> allowedHostnames;
private final Collection<InetAddress> blockedAddresses;
private final Collection<SubnetUtils.SubnetInfo> blockedSubnets;
private final AddressResolver resolver;

/**
* Creates a new instance of {@link DefaultHostValidator}.
*
* @param connectivityConfig the connectivity config used to load the allow-/blocklist
* @param connectivityConfig the connectivity config used to load the allow-/block-list
* @param loggingAdapter logging adapter
*/
DefaultHostValidator(final ConnectivityConfig connectivityConfig, final LoggingAdapter loggingAdapter) {
Expand All @@ -48,7 +50,7 @@ final class DefaultHostValidator implements HostValidator {
/**
* Creates a new instance of {@link DefaultHostValidator}.
*
* @param connectivityConfig the connectivity config used to load the allow-/blocklist
* @param connectivityConfig the connectivity config used to load the allow-/block-list
* @param loggingAdapter logging adapter
* @param resolver custom resolver (used for tests only)
*/
Expand All @@ -58,18 +60,20 @@ final class DefaultHostValidator implements HostValidator {
this.allowedHostnames = connectivityConfig.getConnectionConfig().getAllowedHostnames();
final Collection<String> blockedHostnames = connectivityConfig.getConnectionConfig().getBlockedHostnames();
this.blockedAddresses = calculateBlockedAddresses(blockedHostnames, loggingAdapter);
final Collection<String> blockedSubnetsList = connectivityConfig.getConnectionConfig().getBlockedSubnets();
this.blockedSubnets = calculateBlockedSubnets(blockedSubnetsList, loggingAdapter);
}

/**
* Validate if connections to a host are allowed by checking (in this order):
* <ul>
* <li>if the blocklist is empty, this completely disables validation, every host is allowed</li>
* <li>if the host is contained in the allowlist, the host is allowed</li>
* <li>if the block-list is empty, this completely disables validation, every host is allowed</li>
* <li>if the host is contained in the allow-list, the host is allowed</li>
* <li>if the host is resolved to a blocked ip (loopback, site-local, multicast, wildcard ip), the host is blocked</li>
* <li>if the host is contained in the blocklist, the host is blocked</li>
* <li>if the host is contained in the block-list, the host is blocked</li>
* </ul>
* Loopback, private, multicast and wildcard addresses are allowed only if the blocklist is empty or explicitly
* contained in allowlist.
* Loopback, private, multicast and wildcard addresses are allowed only if the block-list is empty or explicitly
* contained in allow-list.
*
* @param host the host to check.
* @return whether connections to the host are permitted.
Expand All @@ -96,13 +100,18 @@ public HostValidationResult validateHost(final String host) {
} else if (requestAddress.isAnyLocalAddress()) {
return HostValidationResult.blocked(host, "the hostname resolved to a wildcard address.");
} else if (blockedAddresses.contains(requestAddress)) {
// host is contained in the blocklist --> block
// host is contained in the block-list --> block
return HostValidationResult.blocked(host);
}
for (final SubnetUtils.SubnetInfo subnet : blockedSubnets) {
if (subnet.isInRange(requestAddress.getHostAddress())) {
return HostValidationResult.blocked(host, "the hostname resides in a blocked subnet.");
}
}
}
return HostValidationResult.valid();
} catch (UnknownHostException e) {
final String reason = String.format("The configured host '%s' is invalid: %s", host, e.getMessage());
final var reason = String.format("The configured host '%s' is invalid: %s", host, e.getMessage());
return HostValidationResult.invalid(host, reason);
}
}
Expand All @@ -124,7 +133,32 @@ private Collection<InetAddress> calculateBlockedAddresses(final Collection<Strin
try {
return Stream.of(resolver.resolve(host));
} catch (final UnknownHostException e) {
log.warning("Could not resolve hostname during building blocked hostnames set: <{}>", host);
log.warning("Could not resolve hostname during building blocked hostnames set: <{}>",
host);
return Stream.empty();
}
})
.collect(Collectors.toSet());
}

/**
* Calculate blocked subnets from cidr range strings that should not be accessed.
*
* @param blockedSubnets blocked subnets.
* @param log the logger.
* @return info of blocked subnets.
*/
private Collection<SubnetUtils.SubnetInfo> calculateBlockedSubnets(final Collection<String> blockedSubnets,
final LoggingAdapter log) {

return blockedSubnets.stream()
.filter(blockedSubnet -> !blockedSubnet.isEmpty())
.flatMap(blockedSubnet -> {
try {
return Stream.of(new SubnetUtils(blockedSubnet).getInfo());
} catch (final IllegalArgumentException e) {
log.warning("Could not create subnet info during building blocked subnets set: <{}>",
blockedSubnet);
return Stream.empty();
}
})
Expand All @@ -147,4 +181,5 @@ interface AddressResolver {
*/
InetAddress[] resolve(String host) throws UnknownHostException;
}

}
5 changes: 5 additions & 0 deletions connectivity/service/src/main/resources/connectivity.conf
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ ditto {
#blocked-hostnames = "localhost"
blocked-hostnames = ${?CONNECTIVITY_CONNECTION_BLOCKED_HOSTNAMES}

# A comma separated string of blocked subnets to which no http requests will be sent out.
# Specify subnets to block in CIDR format e.g. "11.1.0.0/16"
blocked-subnets = ""
blocked-subnets = ${?CONNECTIVITY_CONNECTION_BLOCKED_SUBNETS}

# Number of sources per connection
max-source-number = ${?CONNECTION_SOURCE_NUMBER}
# Number of targets per connection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public static void initTestFixture() {
public void assertImmutability() {
assertInstancesOf(DefaultConnectionConfig.class,
areImmutable(),
assumingFields("blockedHostnames", "allowedHostnames")
assumingFields("allowedHostnames", "blockedHostnames", "blockedSubnets")
.areSafelyCopiedUnmodifiableCollectionsWithImmutableElements(),
provided(DefaultSupervisorConfig.class,
SnapshotConfig.class,
Expand Down Expand Up @@ -92,6 +92,10 @@ public void underTestReturnsValuesOfConfigFile() {
.as(ConnectionConfig.ConnectionConfigValue.BLOCKED_HOSTNAMES.getConfigPath())
.containsExactly("localhost");

softly.assertThat(underTest.getBlockedSubnets())
.as(ConnectionConfig.ConnectionConfigValue.BLOCKED_SUBNETS.getConfigPath())
.containsExactly("11.1.0.0/16");

softly.assertThat(underTest.getSupervisorConfig())
.as("supervisorConfig")
.satisfies(supervisorConfig -> softly.assertThat(supervisorConfig.getExponentialBackOffConfig())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public void setUp() {
loggingAdapter = mock(LoggingAdapter.class);

when(connectionConfig.getBlockedHostnames()).thenReturn(List.of("localhost"));
when(connectionConfig.getBlockedSubnets()).thenReturn(List.of("11.1.0.0/16","169.254.0.0/16"));
}

@Test
Expand All @@ -56,7 +57,7 @@ public void testAllowedBlockedHosts() {
final HostValidator underTest =
getHostValidatorWithAllowlist("0.0.0.0", "8.8.8.8", "[::1]", "192.168.0.1", "224.0.1.1");

// check if allowlist works for fixed (not configured) blocked ips
// check if allow-list works for fixed (not configured) blocked ips
assertValid(underTest.validateHost("0.0.0.0"));
assertValid(underTest.validateHost("8.8.8.8"));
assertValid(underTest.validateHost("[::1]"));
Expand All @@ -68,7 +69,9 @@ public void testAllowedBlockedHosts() {
public void expectValidationFailsForInvalidHost() {
final HostValidator underTest = getHostValidatorWithAllowlist();
final HostValidationResult validationResult = underTest.validateHost("ditto");

assertThat(validationResult).extracting(HostValidationResult::isValid).isEqualTo(false);

final ConnectionConfigurationInvalidException exception = validationResult.toException(DITTO_HEADERS);
assertThat(exception.getDittoHeaders()).isEqualTo(DITTO_HEADERS);
assertThat(exception.getMessage()).contains("The configured host 'ditto' is invalid");
Expand All @@ -80,13 +83,15 @@ public void expectConfiguredAllowedAndBlockedHostIsAllowed() {
final HostValidator underTest =
getHostValidatorWithCustomResolver(HostValidatorTest::resolveHost, eclipseOrg);
final HostValidationResult validationResult = underTest.validateHost(eclipseOrg);

assertThat(validationResult.isValid()).isTrue();
}

@Test
public void expectConfiguredBlockedHostIsBlocked() {
final HostValidator underTest = getHostValidatorWithCustomResolver(HostValidatorTest::resolveHost);
final HostValidationResult validationResult = underTest.validateHost("eclipse.org");

assertThat(validationResult.isValid()).isFalse();
assertThat(validationResult.toException(DITTO_HEADERS).getDittoHeaders()).isEqualTo(DITTO_HEADERS);
}
Expand All @@ -96,7 +101,7 @@ public void expectBlockedHostIsBlocked() {
// test if a host that resolves to blocked address (hardcoded e.g. loopback, not configured) is blocked
final String theHost = "eclipse.org";

// required because empty blocklist disables verification
// required because empty block-list disables verification
when(connectionConfig.getBlockedHostnames()).thenReturn(List.of("dummy.org"));

// eclipse.org resolves to loopback which is blocked
Expand All @@ -106,9 +111,20 @@ public void expectBlockedHostIsBlocked() {
final HostValidator underTest = getHostValidatorWithCustomResolver(resolveToLoopback);

final HostValidationResult validationResult = underTest.validateHost(theHost);

assertThat(validationResult.isValid()).isFalse();
}

@Test
public void expectIpsInBlockedSubnetsAreBlocked() {
final HostValidator underTest = getHostValidatorWithAllowlist();

assertThat(underTest.validateHost("11.1.0.1").isValid()).isFalse();
assertThat(underTest.validateHost("11.1.255.254").isValid()).isFalse();
assertThat(underTest.validateHost("169.254.0.1").isValid()).isFalse();
assertThat(underTest.validateHost("169.254.255.254").isValid()).isFalse();
}

private static InetAddress[] resolveHost(final String host, byte... address) throws UnknownHostException {
return new InetAddress[]{
InetAddress.getByAddress(host, address.length != 4 ? new byte[]{1, 2, 3, 4} : address)
Expand All @@ -134,4 +150,5 @@ private HostValidator getHostValidatorWithCustomResolver(final DefaultHostValida
private void assertValid(final HostValidationResult result) {
assertThat(result.isValid()).isTrue();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ connection {

allowed-hostnames = "eclipse.org"
blocked-hostnames = "localhost"
blocked-subnets = "11.1.0.0/16"

supervisor {
exponential-backoff {
Expand Down

0 comments on commit 8d08360

Please sign in to comment.