Skip to content
Snippets Groups Projects
Commit 806f5a1b authored by Handa Wang's avatar Handa Wang
Browse files

implement Thread Border Router e2e integration test

This implements a basic border routing test case (ping) and demonstrates a
valid test environment for Thread Border Router test cases.

Later we can add more packet verifications and more test cases.

Bug: 295843010
Test: atest android.net.ThreadBorderRouterTest

Change-Id: Ifb9b6d45539143a227c2bc0ed69b5c3167ef9acd
parent fe1d851d
No related branches found
No related tags found
No related merge requests found
......@@ -43,6 +43,7 @@ java_sdk_library {
"//packages/modules/Connectivity/staticlibs/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
......
......@@ -186,6 +186,7 @@ java_sdk_library {
"//packages/modules/Connectivity/staticlibs/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
......
......@@ -190,6 +190,7 @@ java_sdk_library {
"//packages/modules/Connectivity/Cronet/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
......
......@@ -165,6 +165,24 @@ public class Ipv6Utils {
(byte) ICMPV6_ROUTER_SOLICITATION /* type */, (byte) 0 /* code */, payload);
}
/**
* Build an ICMPv6 Router Solicitation packet from the required specified parameters without
* ethernet header.
*/
public static ByteBuffer buildRsPacket(
final Inet6Address srcIp, final Inet6Address dstIp, final ByteBuffer... options) {
final RsHeader rsHeader = new RsHeader((int) 0 /* reserved */);
final ByteBuffer[] payload =
buildIcmpv6Payload(
ByteBuffer.wrap(rsHeader.writeToBytes(ByteOrder.BIG_ENDIAN)), options);
return buildIcmpv6Packet(
srcIp,
dstIp,
(byte) ICMPV6_ROUTER_SOLICITATION /* type */,
(byte) 0 /* code */,
payload);
}
/**
* Build an ICMPv6 Echo Request packet from the required specified parameters.
*/
......@@ -176,11 +194,21 @@ public class Ipv6Utils {
}
/**
* Build an ICMPv6 Echo Reply packet without ethernet header.
* Build an ICMPv6 Echo Request packet from the required specified parameters without ethernet
* header.
*/
public static ByteBuffer buildEchoReplyPacket(final Inet6Address srcIp,
public static ByteBuffer buildEchoRequestPacket(final Inet6Address srcIp,
final Inet6Address dstIp) {
final ByteBuffer payload = ByteBuffer.allocate(4); // ID and Sequence number may be zero.
return buildIcmpv6Packet(srcIp, dstIp, (byte) ICMPV6_ECHO_REQUEST_TYPE /* type */,
(byte) 0 /* code */,
payload);
}
/** Build an ICMPv6 Echo Reply packet without ethernet header. */
public static ByteBuffer buildEchoReplyPacket(
final Inet6Address srcIp, final Inet6Address dstIp) {
final ByteBuffer payload = ByteBuffer.allocate(4); // ID and Sequence number may be zero.
return buildIcmpv6Packet(srcIp, dstIp, (byte) ICMPV6_ECHO_REPLY_TYPE /* type */,
(byte) 0 /* code */, payload);
}
......
......@@ -8,5 +8,10 @@
{
"name": "ThreadNetworkUnitTests"
}
],
"postsubmit": [
{
"name": "ThreadNetworkIntegrationTests"
}
]
}
//
// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package {
default_applicable_licenses: ["Android-Apache-2.0"],
}
java_defaults {
name: "ThreadNetworkIntegrationTestsDefaults",
min_sdk_version: "30",
static_libs: [
"androidx.test.rules",
"guava",
"mockito-target-minus-junit4",
"net-tests-utils",
"net-utils-device-common",
"net-utils-device-common-bpf",
"testables",
],
libs: [
"android.test.runner",
"android.test.base",
"android.test.mock",
],
}
android_test {
name: "ThreadNetworkIntegrationTests",
platform_apis: true,
manifest: "AndroidManifest.xml",
defaults: [
"framework-connectivity-test-defaults",
"ThreadNetworkIntegrationTestsDefaults"
],
test_suites: [
"general-tests",
],
srcs: [
"src/**/*.java",
],
compile_multilib: "both",
}
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.thread.tests.integration">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!-- The test need CHANGE_NETWORK_STATE permission to use requestNetwork API to setup test
network. Since R shell application don't have such permission, grant permission to the test
here. TODO: Remove CHANGE_NETWORK_STATE permission here and use adopt shell permission to
obtain CHANGE_NETWORK_STATE for testing once R device is no longer supported. -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.THREAD_NETWORK_PRIVILEGED"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application android:debuggable="true">
<uses-library android:name="android.test.runner" />
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.thread.tests.integration"
android:label="Thread integration tests">
</instrumentation>
</manifest>
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.net.thread;
import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
import static android.net.thread.IntegrationTestUtils.isExpectedIcmpv6Packet;
import static android.net.thread.IntegrationTestUtils.newPacketReader;
import static android.net.thread.IntegrationTestUtils.readPacketFrom;
import static android.net.thread.IntegrationTestUtils.waitFor;
import static android.net.thread.IntegrationTestUtils.waitForStateAnyOf;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.io.BaseEncoding.base16;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import android.content.Context;
import android.net.LinkProperties;
import android.net.MacAddress;
import android.os.Handler;
import android.os.HandlerThread;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.testutils.TapPacketReader;
import com.android.testutils.TestNetworkTracker;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.net.Inet6Address;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/** Integration test cases for Thread Border Routing feature. */
@RunWith(AndroidJUnit4.class)
@LargeTest
public class BorderRoutingTest {
private static final String TAG = BorderRoutingTest.class.getSimpleName();
private final Context mContext = ApplicationProvider.getApplicationContext();
private final ThreadNetworkManager mThreadNetworkManager =
mContext.getSystemService(ThreadNetworkManager.class);
private ThreadNetworkController mThreadNetworkController;
private HandlerThread mHandlerThread;
private Handler mHandler;
private TestNetworkTracker mInfraNetworkTracker;
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
private static final byte[] DEFAULT_DATASET_TLVS =
base16().decode(
"0E080000000000010000000300001335060004001FFFE002"
+ "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ "B9D351B40C0402A0FFF8");
private static final ActiveOperationalDataset DEFAULT_DATASET =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
@Before
public void setUp() throws Exception {
mHandlerThread = new HandlerThread(getClass().getSimpleName());
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
var threadControllers = mThreadNetworkManager.getAllThreadNetworkControllers();
assertEquals(threadControllers.size(), 1);
mThreadNetworkController = threadControllers.get(0);
mInfraNetworkTracker =
runAsShell(
MANAGE_TEST_NETWORKS,
() ->
initTestNetwork(
mContext, new LinkProperties(), 5000 /* timeoutMs */));
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
() -> {
CountDownLatch latch = new CountDownLatch(1);
mThreadNetworkController.setTestNetworkAsUpstream(
mInfraNetworkTracker.getTestIface().getInterfaceName(),
MoreExecutors.directExecutor(),
v -> {
latch.countDown();
});
latch.await();
});
}
@After
public void tearDown() throws Exception {
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
() -> {
CountDownLatch latch = new CountDownLatch(2);
mThreadNetworkController.setTestNetworkAsUpstream(
null, MoreExecutors.directExecutor(), v -> latch.countDown());
mThreadNetworkController.leave(
MoreExecutors.directExecutor(), v -> latch.countDown());
latch.await(10, TimeUnit.SECONDS);
});
runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
mHandlerThread.quitSafely();
mHandlerThread.join();
}
@Test
public void infraDevicePingTheadDeviceOmr_Succeeds() throws Exception {
/*
* <pre>
* Topology:
* infra network Thread
* infra device -------------------- Border Router -------------- Full Thread device
* (Cuttlefish)
* </pre>
*/
// BR forms a network.
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
() -> {
mThreadNetworkController.join(
DEFAULT_DATASET, MoreExecutors.directExecutor(), result -> {});
});
waitForStateAnyOf(
mThreadNetworkController, List.of(DEVICE_ROLE_LEADER), 30 /* timeoutSeconds */);
// Creates a Full Thread Device (FTD) and lets it join the network.
FullThreadDevice ftd = new FullThreadDevice(5 /* node ID */);
ftd.factoryReset();
ftd.joinNetwork(DEFAULT_DATASET);
ftd.waitForStateAnyOf(List.of("router", "child"), 10 /* timeoutSeconds */);
waitFor(() -> ftd.getOmrAddress() != null, 60 /* timeoutSeconds */);
Inet6Address ftdOmr = ftd.getOmrAddress();
assertNotNull(ftdOmr);
// Creates a infra network device.
TapPacketReader infraNetworkReader =
newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
InfraNetworkDevice infraDevice =
new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), infraNetworkReader);
infraDevice.runSlaac(60 /* timeoutSeconds */);
assertNotNull(infraDevice.ipv6Addr);
// Infra device sends an echo request to FTD's OMR.
infraDevice.sendEchoRequest(ftdOmr);
// Infra device receives an echo reply sent by FTD.
assertNotNull(
readPacketFrom(
infraNetworkReader,
p -> isExpectedIcmpv6Packet(p, ICMPV6_ECHO_REPLY_TYPE)));
}
}
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.net.thread;
import static android.net.thread.IntegrationTestUtils.waitFor;
import static com.google.common.io.BaseEncoding.base16;
import static org.junit.Assert.fail;
import android.net.InetAddresses;
import android.net.IpPrefix;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Inet6Address;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* A class that launches and controls a simulation Full Thread Device (FTD).
*
* <p>This class launches an `ot-cli-ftd` process and communicates with it via command line input
* and output. See <a
* href="https://github.com/openthread/openthread/blob/main/src/cli/README.md">this page</a> for
* available commands.
*/
public final class FullThreadDevice {
private final Process mProcess;
private final BufferedReader mReader;
private final BufferedWriter mWriter;
private ActiveOperationalDataset mActiveOperationalDataset;
/**
* Constructs a {@link FullThreadDevice} for the given node ID.
*
* <p>It launches an `ot-cli-ftd` process using the given node ID. The node ID is an integer in
* range [1, OPENTHREAD_SIMULATION_MAX_NETWORK_SIZE]. `OPENTHREAD_SIMULATION_MAX_NETWORK_SIZE`
* is defined in `external/openthread/examples/platforms/simulation/platform-config.h`.
*
* @param nodeId the node ID for the simulation Full Thread Device.
* @throws IllegalStateException the node ID is already occupied by another simulation Thread
* device.
*/
public FullThreadDevice(int nodeId) {
try {
mProcess = Runtime.getRuntime().exec("/system/bin/ot-cli-ftd " + nodeId);
} catch (IOException e) {
throw new IllegalStateException("Failed to start ot-cli-ftd (id=" + nodeId + ")", e);
}
mReader = new BufferedReader(new InputStreamReader(mProcess.getInputStream()));
mWriter = new BufferedWriter(new OutputStreamWriter(mProcess.getOutputStream()));
mActiveOperationalDataset = null;
}
/**
* Returns an OMR (Off-Mesh-Routable) address on this device if any.
*
* <p>This methods goes through all unicast addresses on the device and returns the first
* address which is neither link-local nor mesh-local.
*/
public Inet6Address getOmrAddress() {
List<String> addresses = executeCommand("ipaddr");
IpPrefix meshLocalPrefix = mActiveOperationalDataset.getMeshLocalPrefix();
for (String address : addresses) {
if (address.startsWith("fe80:")) {
continue;
}
Inet6Address addr = (Inet6Address) InetAddresses.parseNumericAddress(address);
if (!meshLocalPrefix.contains(addr)) {
return addr;
}
}
return null;
}
/**
* Joins the Thread network using the given {@link ActiveOperationalDataset}.
*
* @param dataset the Active Operational Dataset
*/
public void joinNetwork(ActiveOperationalDataset dataset) {
mActiveOperationalDataset = dataset;
executeCommand("dataset set active " + base16().lowerCase().encode(dataset.toThreadTlvs()));
executeCommand("ifconfig up");
executeCommand("thread start");
}
/** Stops the Thread network radio. */
public void stopThreadRadio() {
executeCommand("thread stop");
executeCommand("ifconfig down");
}
/**
* Waits for the Thread device to enter the any state of the given {@link List<String>}.
*
* @param states the list of states to wait for. Valid states are "disabled", "detached",
* "child", "router" and "leader".
* @param timeoutSeconds the number of seconds to wait for.
*/
public void waitForStateAnyOf(List<String> states, int timeoutSeconds) throws TimeoutException {
waitFor(() -> states.contains(getState()), timeoutSeconds);
}
/**
* Gets the state of the Thread device.
*
* @return a string representing the state.
*/
public String getState() {
return executeCommand("state").get(0);
}
/** Runs the "factoryreset" command on the device. */
public void factoryReset() {
try {
mWriter.write("factoryreset\n");
mWriter.flush();
// fill the input buffer to avoid truncating next command
for (int i = 0; i < 1000; ++i) {
mWriter.write("\n");
}
mWriter.flush();
} catch (IOException e) {
throw new IllegalStateException("Failed to run factoryreset on ot-cli-ftd", e);
}
}
private List<String> executeCommand(String command) {
try {
mWriter.write(command + "\n");
mWriter.flush();
} catch (IOException e) {
throw new IllegalStateException(
"Failed to write the command " + command + " to ot-cli-ftd", e);
}
try {
return readUntilDone();
} catch (IOException e) {
throw new IllegalStateException(
"Failed to read the ot-cli-ftd output of command: " + command, e);
}
}
private List<String> readUntilDone() throws IOException {
ArrayList<String> result = new ArrayList<>();
String line;
while ((line = mReader.readLine()) != null) {
if (line.equals("Done")) {
break;
}
if (line.startsWith("Error:")) {
fail("ot-cli-ftd reported an error: " + line);
}
if (!line.startsWith("> ")) {
result.add(line);
}
}
return result;
}
}
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.net.thread;
import static android.net.thread.IntegrationTestUtils.getRaPios;
import static android.net.thread.IntegrationTestUtils.readPacketFrom;
import static android.net.thread.IntegrationTestUtils.waitFor;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
import android.net.InetAddresses;
import android.net.MacAddress;
import com.android.net.module.util.Ipv6Utils;
import com.android.net.module.util.structs.LlaOption;
import com.android.net.module.util.structs.PrefixInformationOption;
import com.android.testutils.TapPacketReader;
import java.io.IOException;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeoutException;
/**
* A class that simulates a device on the infrastructure network.
*
* <p>This class directly interacts with the TUN interface of the test network to pretend there's a
* device on the infrastructure network.
*/
public final class InfraNetworkDevice {
// The MAC address of this device.
public final MacAddress macAddr;
// The packet reader of the TUN interface of the test network.
public final TapPacketReader packetReader;
// The IPv6 address generated by SLAAC for the device.
public Inet6Address ipv6Addr;
/**
* Constructs an InfraNetworkDevice with the given {@link MAC address} and {@link
* TapPacketReader}.
*
* @param macAddr the MAC address of the device
* @param packetReader the packet reader of the TUN interface of the test network.
*/
public InfraNetworkDevice(MacAddress macAddr, TapPacketReader packetReader) {
this.macAddr = macAddr;
this.packetReader = packetReader;
}
/**
* Sends an ICMPv6 echo request message to the given {@link Inet6Address}.
*
* @param dstAddr the destination address of the packet.
* @throws IOException when it fails to send the packet.
*/
public void sendEchoRequest(Inet6Address dstAddr) throws IOException {
ByteBuffer icmp6Packet = Ipv6Utils.buildEchoRequestPacket(ipv6Addr, dstAddr);
packetReader.sendResponse(icmp6Packet);
}
/**
* Sends an ICMPv6 Router Solicitation (RS) message to all routers on the network.
*
* @throws IOException when it fails to send the packet.
*/
public void sendRsPacket() throws IOException {
ByteBuffer slla = LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, macAddr);
ByteBuffer rs =
Ipv6Utils.buildRsPacket(
(Inet6Address) InetAddresses.parseNumericAddress("fe80::1"),
IPV6_ADDR_ALL_ROUTERS_MULTICAST,
slla);
packetReader.sendResponse(rs);
}
/**
* Runs SLAAC to generate an IPv6 address for the device.
*
* <p>The devices sends an RS message, processes the received RA messages and generates an IPv6
* address if there's any available Prefix Information Option (PIO). For now it only generates
* one address in total and doesn't track the expiration.
*
* @param timeoutSeconds the number of seconds to wait for.
* @throws TimeoutException when the device fails to generate a SLAAC address in given timeout.
*/
public void runSlaac(int timeoutSeconds) throws TimeoutException {
waitFor(() -> (ipv6Addr = runSlaac()) != null, timeoutSeconds, 5 /* intervalSeconds */);
}
private Inet6Address runSlaac() {
try {
sendRsPacket();
final byte[] raPacket = readPacketFrom(packetReader, p -> !getRaPios(p).isEmpty());
final List<PrefixInformationOption> options = getRaPios(raPacket);
for (PrefixInformationOption pio : options) {
if (pio.validLifetime > 0 && pio.preferredLifetime > 0) {
final byte[] addressBytes = pio.prefix;
addressBytes[addressBytes.length - 1] = (byte) (new Random()).nextInt();
addressBytes[addressBytes.length - 2] = (byte) (new Random()).nextInt();
return (Inet6Address) InetAddress.getByAddress(addressBytes);
}
}
} catch (IOException e) {
throw new IllegalStateException("Failed to generate an address by SLAAC", e);
}
return null;
}
}
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.net.thread;
import static android.system.OsConstants.IPPROTO_ICMPV6;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import android.net.TestNetworkInterface;
import android.os.Handler;
import android.os.SystemClock;
import com.android.net.module.util.Struct;
import com.android.net.module.util.structs.Icmpv6Header;
import com.android.net.module.util.structs.Ipv6Header;
import com.android.net.module.util.structs.PrefixInformationOption;
import com.android.net.module.util.structs.RaHeader;
import com.android.testutils.HandlerUtils;
import com.android.testutils.TapPacketReader;
import com.google.common.util.concurrent.SettableFuture;
import java.io.FileDescriptor;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;
import java.util.function.Supplier;
/** Static utility methods relating to Thread integration tests. */
public final class IntegrationTestUtils {
private IntegrationTestUtils() {}
/**
* Waits for the given {@link Supplier} to be true until given timeout.
*
* <p>It checks the condition once every second.
*
* @param condition the condition to check.
* @param timeoutSeconds the number of seconds to wait for.
* @throws TimeoutException if the condition is not met after the timeout.
*/
public static void waitFor(Supplier<Boolean> condition, int timeoutSeconds)
throws TimeoutException {
waitFor(condition, timeoutSeconds, 1);
}
/**
* Waits for the given {@link Supplier} to be true until given timeout.
*
* <p>It checks the condition once every {@code intervalSeconds}.
*
* @param condition the condition to check.
* @param timeoutSeconds the number of seconds to wait for.
* @param intervalSeconds the period to check the {@code condition}.
* @throws TimeoutException if the condition is still not met when the timeout expires.
*/
public static void waitFor(Supplier<Boolean> condition, int timeoutSeconds, int intervalSeconds)
throws TimeoutException {
for (int i = 0; i < timeoutSeconds; i += intervalSeconds) {
if (condition.get()) {
return;
}
SystemClock.sleep(intervalSeconds * 1000L);
}
if (condition.get()) {
return;
}
throw new TimeoutException(
String.format(
"The condition failed to become true in %d seconds.", timeoutSeconds));
}
/**
* Creates a {@link TapPacketReader} given the {@link TestNetworkInterface} and {@link Handler}.
*
* @param testNetworkInterface the TUN interface of the test network.
* @param handler the handler to process the packets.
* @return the {@link TapPacketReader}.
*/
public static TapPacketReader newPacketReader(
TestNetworkInterface testNetworkInterface, Handler handler) {
FileDescriptor fd = testNetworkInterface.getFileDescriptor().getFileDescriptor();
final TapPacketReader reader =
new TapPacketReader(handler, fd, testNetworkInterface.getMtu());
handler.post(() -> reader.start());
HandlerUtils.waitForIdle(handler, 5000 /* timeout in milliseconds */);
return reader;
}
/**
* Waits for the Thread module to enter any state of the given {@code deviceRoles}.
*
* @param controller the {@link ThreadNetworkController}.
* @param deviceRoles the desired device roles. See also {@link
* ThreadNetworkController.DeviceRole}.
* @param timeoutSeconds the number of seconds ot wait for.
* @return the {@link ThreadNetworkController.DeviceRole} after waiting.
* @throws TimeoutException if the device hasn't become any of expected roles until the timeout
* expires.
*/
public static int waitForStateAnyOf(
ThreadNetworkController controller, List<Integer> deviceRoles, int timeoutSeconds)
throws TimeoutException {
SettableFuture<Integer> future = SettableFuture.create();
ThreadNetworkController.StateCallback callback =
newRole -> {
if (deviceRoles.contains(newRole)) {
future.set(newRole);
}
};
controller.registerStateCallback(directExecutor(), callback);
try {
int role = future.get(timeoutSeconds, TimeUnit.SECONDS);
controller.unregisterStateCallback(callback);
return role;
} catch (InterruptedException | ExecutionException e) {
throw new TimeoutException(
String.format(
"The device didn't become an expected role in %d seconds.",
timeoutSeconds));
}
}
/**
* Reads a packet from a given {@link TapPacketReader} that satisfies the {@code filter}.
*
* @param packetReader a TUN packet reader.
* @param filter the filter to be applied on the packet.
* @return the first IPv6 packet that satisfies the {@code filter}. If it has waited for more
* than 3000ms to read the next packet, the method will return null.
*/
public static byte[] readPacketFrom(TapPacketReader packetReader, Predicate<byte[]> filter) {
byte[] packet;
while ((packet = packetReader.poll(3000 /* timeoutMs */)) != null) {
if (filter.test(packet)) return packet;
}
return null;
}
/** Returns {@code true} if {@code packet} is an ICMPv6 packet of given {@code type}. */
public static boolean isExpectedIcmpv6Packet(byte[] packet, int type) {
if (packet == null) {
return false;
}
ByteBuffer buf = ByteBuffer.wrap(packet);
try {
if (Struct.parse(Ipv6Header.class, buf).nextHeader != (byte) IPPROTO_ICMPV6) {
return false;
}
return Struct.parse(Icmpv6Header.class, buf).type == (short) type;
} catch (IllegalArgumentException ignored) {
// It's fine that the passed in packet is malformed because it's could be sent
// by anybody.
}
return false;
}
/** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message. */
public static List<PrefixInformationOption> getRaPios(byte[] raMsg) {
final ArrayList<PrefixInformationOption> pioList = new ArrayList<>();
if (raMsg == null) {
return pioList;
}
final ByteBuffer buf = ByteBuffer.wrap(raMsg);
final Ipv6Header ipv6Header = Struct.parse(Ipv6Header.class, buf);
if (ipv6Header.nextHeader != (byte) IPPROTO_ICMPV6) {
return pioList;
}
final Icmpv6Header icmpv6Header = Struct.parse(Icmpv6Header.class, buf);
if (icmpv6Header.type != (short) ICMPV6_ROUTER_ADVERTISEMENT) {
return pioList;
}
Struct.parse(RaHeader.class, buf);
while (buf.position() < raMsg.length) {
final int currentPos = buf.position();
final int type = Byte.toUnsignedInt(buf.get());
final int length = Byte.toUnsignedInt(buf.get());
if (type == ICMPV6_ND_OPTION_PIO) {
final ByteBuffer pioBuf =
ByteBuffer.wrap(
buf.array(),
currentPos,
Struct.getSize(PrefixInformationOption.class));
final PrefixInformationOption pio =
Struct.parse(PrefixInformationOption.class, pioBuf);
pioList.add(pio);
// Move ByteBuffer position to the next option.
buf.position(currentPos + Struct.getSize(PrefixInformationOption.class));
} else {
// The length is in units of 8 octets.
buf.position(currentPos + (length * 8));
}
}
return pioList;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment