Merge "implement Thread Border Router e2e integration test" into main am: 5b96f865cd
Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/2817100 Change-Id: Ied6091a5d5733019cbb21badf7363c546173003f Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
commit
57d9265d54
11 changed files with 836 additions and 2 deletions
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
55
thread/tests/integration/Android.bp
Normal file
55
thread/tests/integration/Android.bp
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
//
|
||||
// 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",
|
||||
}
|
||||
35
thread/tests/integration/AndroidManifest.xml
Normal file
35
thread/tests/integration/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?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>
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* 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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue