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