diff --git a/aws-greengrass-testing-features/aws-greengrass-testing-features-api/src/main/java/com/aws/greengrass/testing/features/ConnectivitySteps.java b/aws-greengrass-testing-features/aws-greengrass-testing-features-api/src/main/java/com/aws/greengrass/testing/features/ConnectivitySteps.java
new file mode 100644
index 00000000..c0b675e0
--- /dev/null
+++ b/aws-greengrass-testing-features/aws-greengrass-testing-features-api/src/main/java/com/aws/greengrass/testing/features/ConnectivitySteps.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.aws.greengrass.testing.features;
+
+import com.aws.greengrass.testing.platform.Platform;
+import io.cucumber.guice.ScenarioScoped;
+import io.cucumber.java.After;
+import io.cucumber.java.en.When;
+
+import java.io.IOException;
+import javax.inject.Inject;
+
+@ScenarioScoped
+public class ConnectivitySteps {
+ private final Platform platform;
+ private boolean offline = false;
+
+ @Inject
+ @SuppressWarnings("MissingJavadocMethod")
+ public ConnectivitySteps(Platform platform) {
+ this.platform = platform;
+ }
+
+ /**
+ * Blocks the traffic on ports 443, 8888, 8889 when the connectivity parameter is "offline" and
+ * re-enables traffic on the ports when it is "online".
+ *
+ * @param connectivity desired connectivity status ("offline", "online")
+ * @throws IOException {throws IOException}
+ * @throws InterruptedException {throws IInterruptedException}
+ * @throws UnsupportedOperationException {throws UnsupportedOperationException}
+ */
+ @When("the device network connectivity is {word}")
+ public void setDeviceNetwork(final String connectivity) throws IOException, InterruptedException {
+ switch (connectivity.toLowerCase()) {
+ case "offline":
+ platform.getNetworkUtils().disconnectNetwork();
+ break;
+ case "online":
+ platform.getNetworkUtils().recoverNetwork();
+ break;
+ default:
+ throw new UnsupportedOperationException("Connectivity " + connectivity + " is not supported");
+ }
+
+ offline = "offline".equalsIgnoreCase(connectivity);
+ }
+
+ @After
+ private void teardown() throws IOException, InterruptedException {
+ if (offline) {
+ platform.getNetworkUtils().recoverNetwork();
+ }
+ }
+}
+
diff --git a/aws-greengrass-testing-features/aws-greengrass-testing-features-api/src/main/java/com/aws/greengrass/testing/features/DeploymentSteps.java b/aws-greengrass-testing-features/aws-greengrass-testing-features-api/src/main/java/com/aws/greengrass/testing/features/DeploymentSteps.java
index fbf47d4b..546e1165 100644
--- a/aws-greengrass-testing-features/aws-greengrass-testing-features-api/src/main/java/com/aws/greengrass/testing/features/DeploymentSteps.java
+++ b/aws-greengrass-testing-features/aws-greengrass-testing-features-api/src/main/java/com/aws/greengrass/testing/features/DeploymentSteps.java
@@ -397,4 +397,5 @@ void checkADeploymentReachesCompleted(GreengrassV2Lifecycle ggv2, String deploym
Thread.currentThread().interrupt();
}
}
+
}
diff --git a/aws-greengrass-testing-platform/aws-greengrass-testing-platform-api/pom.xml b/aws-greengrass-testing-platform/aws-greengrass-testing-platform-api/pom.xml
index 88441f84..90fb8c4d 100644
--- a/aws-greengrass-testing-platform/aws-greengrass-testing-platform-api/pom.xml
+++ b/aws-greengrass-testing-platform/aws-greengrass-testing-platform-api/pom.xml
@@ -142,6 +142,10 @@
2.13.0
test
+
+ software.amazon.awssdk
+ utils
+
\ No newline at end of file
diff --git a/aws-greengrass-testing-platform/aws-greengrass-testing-platform-api/src/main/java/com/aws/greengrass/testing/platform/NetworkUtils.java b/aws-greengrass-testing-platform/aws-greengrass-testing-platform-api/src/main/java/com/aws/greengrass/testing/platform/NetworkUtils.java
new file mode 100644
index 00000000..840e9d4b
--- /dev/null
+++ b/aws-greengrass-testing-platform/aws-greengrass-testing-platform-api/src/main/java/com/aws/greengrass/testing/platform/NetworkUtils.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.aws.greengrass.testing.platform;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class NetworkUtils {
+ protected static final String[] MQTT_PORTS = {"8883", "443"};
+ // 8888 and 8889 are used by the squid proxy which runs on a remote DUT
+ // and need to disable access to test offline proxy scenarios
+ protected static final String[] NETWORK_PORTS = {"443", "8888", "8889"};
+ protected static final int[] GG_UPSTREAM_PORTS = {8883, 8443, 443};
+ protected static final int SSH_PORT = 22;
+ protected final List blockedPorts = new ArrayList<>();
+
+ public abstract void disconnectNetwork() throws InterruptedException, IOException;
+
+ public abstract void recoverNetwork() throws InterruptedException, IOException;
+
+}
diff --git a/aws-greengrass-testing-platform/aws-greengrass-testing-platform-api/src/main/java/com/aws/greengrass/testing/platform/Platform.java b/aws-greengrass-testing-platform/aws-greengrass-testing-platform-api/src/main/java/com/aws/greengrass/testing/platform/Platform.java
index 7eb5a0cd..04e2a7fd 100644
--- a/aws-greengrass-testing-platform/aws-greengrass-testing-platform-api/src/main/java/com/aws/greengrass/testing/platform/Platform.java
+++ b/aws-greengrass-testing-platform/aws-greengrass-testing-platform-api/src/main/java/com/aws/greengrass/testing/platform/Platform.java
@@ -9,4 +9,10 @@ public interface Platform {
Commands commands();
PlatformFiles files();
+
+ default NetworkUtils getNetworkUtils() {
+ throw new UnsupportedOperationException("Not yet implemented");
+ }
+
}
+
diff --git a/aws-greengrass-testing-platform/aws-greengrass-testing-platform-api/src/main/java/com/aws/greengrass/testing/platform/linux/LinuxPlatform.java b/aws-greengrass-testing-platform/aws-greengrass-testing-platform-api/src/main/java/com/aws/greengrass/testing/platform/linux/LinuxPlatform.java
index 19db0728..1cf4ddb9 100644
--- a/aws-greengrass-testing-platform/aws-greengrass-testing-platform-api/src/main/java/com/aws/greengrass/testing/platform/linux/LinuxPlatform.java
+++ b/aws-greengrass-testing-platform/aws-greengrass-testing-platform-api/src/main/java/com/aws/greengrass/testing/platform/linux/LinuxPlatform.java
@@ -8,14 +8,24 @@
import com.aws.greengrass.testing.api.device.Device;
import com.aws.greengrass.testing.api.model.PillboxContext;
import com.aws.greengrass.testing.platform.AbstractPlatform;
+import com.aws.greengrass.testing.platform.NetworkUtils;
public class LinuxPlatform extends AbstractPlatform {
+
+ private final NetworkUtilsLinux networkUtilsLinux;
+
public LinuxPlatform(final Device device, final PillboxContext pillboxContext) {
super(device, pillboxContext);
+ networkUtilsLinux = new NetworkUtilsLinux();
}
@Override
public LinuxCommands commands() {
return new LinuxCommands(device, pillboxContext);
}
+
+ @Override
+ public NetworkUtils getNetworkUtils() {
+ return this.networkUtilsLinux;
+ }
}
diff --git a/aws-greengrass-testing-platform/aws-greengrass-testing-platform-api/src/main/java/com/aws/greengrass/testing/platform/linux/NetworkUtilsLinux.java b/aws-greengrass-testing-platform/aws-greengrass-testing-platform-api/src/main/java/com/aws/greengrass/testing/platform/linux/NetworkUtilsLinux.java
new file mode 100644
index 00000000..a805cc01
--- /dev/null
+++ b/aws-greengrass-testing-platform/aws-greengrass-testing-platform-api/src/main/java/com/aws/greengrass/testing/platform/linux/NetworkUtilsLinux.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.aws.greengrass.testing.platform.linux;
+
+import com.aws.greengrass.testing.platform.NetworkUtils;
+import software.amazon.awssdk.utils.IoUtils;
+
+import java.io.IOException;
+import java.net.NetworkInterface;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+
+public class NetworkUtilsLinux extends NetworkUtils {
+ private static final String ENABLE_OPTION = "--insert";
+ private static final String DISABLE_OPTION = "--delete";
+ private static final String APPEND_OPTION = "-A";
+ private static final String IPTABLE_COMMAND_BLOCK_INGRESS_STR =
+ "sudo iptables %s INPUT -p tcp --sport %s -j REJECT";
+ private static final String IPTABLE_COMMAND_STR = "sudo iptables %s OUTPUT -p tcp --dport %s -j REJECT && "
+ + "sudo iptables %s INPUT -p tcp --sport %s -j REJECT";
+ private static final String IPTABLES_DROP_DPORT_EXTERNAL_ONLY_COMMAND_STR =
+ "sudo iptables %s INPUT -p tcp -s localhost --dport %s -j ACCEPT && "
+ +
+ "sudo iptables %s INPUT -p tcp --dport %s -j DROP && "
+ +
+ "sudo iptables %s OUTPUT -p tcp -d localhost --dport %s -j ACCEPT && "
+ +
+ "sudo iptables %s OUTPUT -p tcp --dport %s -j DROP";
+ private static final String IPTABLE_SAFELIST_COMMAND_STR
+ = "sudo iptables %s OUTPUT -p tcp -d %s --dport %d -j ACCEPT && "
+ +
+ "sudo iptables %s INPUT -p tcp -s %s --sport %d -j ACCEPT";
+ private static final String GET_IPTABLES_RULES = "sudo iptables -S";
+
+ // The string we are looking for to verify that there is an iptables rule to reject a port
+ // We only need to look for sport because sport only gets created if dport is successful
+ private static final String IPTABLES_RULE = "-m tcp --sport %s -j REJECT";
+
+ private static final AtomicBoolean bandwidthSetup = new AtomicBoolean(false);
+
+
+ private void modifyMqttConnection(String action) throws IOException, InterruptedException {
+ for (String port : MQTT_PORTS) {
+ new ProcessBuilder().command(
+ "sh", "-c", String.format(IPTABLES_DROP_DPORT_EXTERNAL_ONLY_COMMAND_STR,
+ action, port, action, port, action, port, action, port)
+ ).start().waitFor(2, TimeUnit.SECONDS);
+ }
+ }
+
+ private void filterPortOnInterface(String iface, int port) throws IOException, InterruptedException {
+ // Filtering SSH traffic impacts test execution, so we explicitly disallow it
+ if (port == SSH_PORT) {
+ return;
+ }
+ List filterSourcePortCommand = Stream.of("sudo", "tc", "filter", "add", "dev",
+ iface, "parent", "1:", "protocol", "ip", "prio", "1", "u32", "match",
+ "ip", "sport", Integer.toString(port), "0xffff", "flowid", "1:2").collect(Collectors.toList());
+ executeCommand(filterSourcePortCommand);
+
+ List filterDestPortCommand = Stream.of("sudo", "tc", "filter", "add", "dev", iface,
+ "parent", "1:", "protocol", "ip", "prio", "1", "u32", "match",
+ "ip", "dport", Integer.toString(port), "0xffff", "flowid", "1:2").collect(Collectors.toList());
+ executeCommand(filterDestPortCommand);
+ }
+
+ private void deleteRootNetemQdiscOnInterface() throws InterruptedException, IOException {
+ Enumeration nets = NetworkInterface.getNetworkInterfaces();
+ for (NetworkInterface netint : Collections.list(nets)) {
+ if (netint.isPointToPoint() || netint.isLoopback()) {
+ continue;
+ }
+ executeCommand(Stream.of("sudo", "tc", "qdisc", "del", "dev", netint.getName(), "root")
+ .collect(Collectors.toList()));
+ }
+ }
+
+ private void createRootNetemQdiscOnInterface(String iface, int netemRateKbps)
+ throws InterruptedException, IOException {
+ // TODO: Add support for setting packet loss and delay
+ int netemDelayMs = 750;
+ List addQdiscCommand = Stream.of("sudo", "tc", "qdisc", "add", "dev", iface, "root", "handle",
+ "1:", "prio", "bands", "2", "priomap", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0",
+ "0", "0", "0", "0", "0").collect(Collectors.toList());
+ executeCommand(addQdiscCommand);
+
+ List netemCommand =
+ Stream.of("sudo", "tc", "qdisc", "add", "dev", iface, "parent", "1:2", "netem", "delay",
+ String.format("%dms", netemDelayMs), "rate", String.format("%dkbit", netemRateKbps))
+ .collect(Collectors.toList());
+ executeCommand(netemCommand);
+ }
+
+ private String executeCommand(List command) throws IOException, InterruptedException {
+ Process proc = new ProcessBuilder().command(command).start();
+ proc.waitFor(2, TimeUnit.SECONDS);
+ if (proc.exitValue() != 0) {
+ throw new IOException("CLI command " + command + " failed with error "
+ + new String(IoUtils.toByteArray(proc.getErrorStream()), StandardCharsets.UTF_8));
+ }
+ return new String(IoUtils.toByteArray(proc.getInputStream()), StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public void disconnectNetwork() throws InterruptedException, IOException {
+ interfacepolicy(IPTABLE_COMMAND_STR, ENABLE_OPTION, "connection-loss", NETWORK_PORTS);
+ }
+
+ @Override
+ public void recoverNetwork() throws InterruptedException, IOException {
+ interfacepolicy(IPTABLE_COMMAND_STR, DISABLE_OPTION, "connection-recover", NETWORK_PORTS);
+
+ if (bandwidthSetup.get()) {
+ deleteRootNetemQdiscOnInterface();
+ bandwidthSetup.set(false);
+ }
+ }
+
+ private void interfacepolicy(String iptableCommandString, String option, String eventName, String... ports)
+ throws InterruptedException, IOException {
+ for (String port : ports) {
+ new ProcessBuilder().command("sh", "-c", String.format(iptableCommandString, option, port, option, port))
+ .start().waitFor(2, TimeUnit.SECONDS);
+ }
+ }
+}