diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp index 676eb0e69640ed324c02f647553f8289a0e67434..c1cf0a09eeef9706c6b577b4f9c6535161b81f6f 100644 --- a/thread/tests/cts/Android.bp +++ b/thread/tests/cts/Android.bp @@ -42,6 +42,7 @@ android_test { "guava", "guava-android-testlib", "net-tests-utils", + "ThreadNetworkTestUtils", "truth", ], libs: [ diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java index 3bec36b46b991180b72a17d81e8894cf8bf87cad..95496568cab9b126c04a11de167f3ff1f8130b5e 100644 --- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java +++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java @@ -17,7 +17,6 @@ package android.net.thread.cts; import static android.Manifest.permission.ACCESS_NETWORK_STATE; -import static android.Manifest.permission.MANAGE_TEST_NETWORKS; import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD; import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER; import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_ROUTER; @@ -33,7 +32,6 @@ import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; -import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork; import static com.android.testutils.TestPermissionUtil.runAsShell; import static com.google.common.truth.Truth.assertThat; @@ -48,7 +46,6 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.content.Context; import android.net.ConnectivityManager; -import android.net.LinkAddress; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; @@ -62,7 +59,9 @@ import android.net.thread.ThreadNetworkController.OperationalDatasetCallback; import android.net.thread.ThreadNetworkController.StateCallback; import android.net.thread.ThreadNetworkException; import android.net.thread.ThreadNetworkManager; +import android.net.thread.utils.TapTestNetworkTracker; import android.os.Build; +import android.os.HandlerThread; import android.os.OutcomeReceiver; import androidx.annotation.NonNull; @@ -74,7 +73,6 @@ import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; import com.android.testutils.DevSdkIgnoreRunner; import com.android.testutils.FunctionalUtils.ThrowingRunnable; -import com.android.testutils.TestNetworkTracker; import org.junit.After; import org.junit.Before; @@ -110,7 +108,7 @@ public class ThreadNetworkControllerTest { private static final int NETWORK_CALLBACK_TIMEOUT_MILLIS = 10 * 1000; private static final int CALLBACK_TIMEOUT_MILLIS = 1_000; private static final int ENABLED_TIMEOUT_MILLIS = 2_000; - private static final int SERVICE_DISCOVERY_TIMEOUT_MILLIS = 10 * 1000; + private static final int SERVICE_DISCOVERY_TIMEOUT_MILLIS = 30_000; private static final String MESHCOP_SERVICE_TYPE = "_meshcop._udp"; private static final String THREAD_NETWORK_PRIVILEGED = "android.permission.THREAD_NETWORK_PRIVILEGED"; @@ -123,6 +121,8 @@ public class ThreadNetworkControllerTest { private NsdManager mNsdManager; private Set<String> mGrantedPermissions; + private HandlerThread mHandlerThread; + private TapTestNetworkTracker mTestNetworkTracker; @Before public void setUp() throws Exception { @@ -141,6 +141,8 @@ public class ThreadNetworkControllerTest { setEnabledAndWait(mController, true); mNsdManager = mContext.getSystemService(NsdManager.class); + mHandlerThread = new HandlerThread(this.getClass().getSimpleName()); + mHandlerThread.start(); } @After @@ -152,6 +154,7 @@ public class ThreadNetworkControllerTest { future.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS); } dropAllPermissions(); + tearDownTestNetwork(); } @Test @@ -829,7 +832,7 @@ public class ThreadNetworkControllerTest { @Test public void meshcopService_threadEnabledButNotJoined_discoveredButNoNetwork() throws Exception { - TestNetworkTracker testNetwork = setUpTestNetwork(); + setUpTestNetwork(); setEnabledAndWait(mController, true); leaveAndWait(mController); @@ -845,13 +848,11 @@ public class ThreadNetworkControllerTest { assertThat(txtMap.get("rv")).isNotNull(); assertThat(txtMap.get("tv")).isNotNull(); assertThat(txtMap.get("sb")).isNotNull(); - - tearDownTestNetwork(testNetwork); } @Test public void meshcopService_joinedNetwork_discoveredHasNetwork() throws Exception { - TestNetworkTracker testNetwork = setUpTestNetwork(); + setUpTestNetwork(); String networkName = "TestNet" + new Random().nextInt(10_000); joinRandomizedDatasetAndWait(mController, networkName); @@ -872,27 +873,26 @@ public class ThreadNetworkControllerTest { assertThat(txtMap.get("tv")).isNotNull(); assertThat(txtMap.get("sb")).isNotNull(); assertThat(txtMap.get("id").length).isEqualTo(16); - - tearDownTestNetwork(testNetwork); } @Test public void meshcopService_threadDisabled_notDiscovered() throws Exception { - TestNetworkTracker testNetwork = setUpTestNetwork(); + setUpTestNetwork(); CompletableFuture<NsdServiceInfo> serviceLostFuture = new CompletableFuture<>(); NsdManager.DiscoveryListener listener = discoverForServiceLost(MESHCOP_SERVICE_TYPE, serviceLostFuture); setEnabledAndWait(mController, false); - try { - serviceLostFuture.get(10_000, MILLISECONDS); + serviceLostFuture.get(SERVICE_DISCOVERY_TIMEOUT_MILLIS, MILLISECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException ignored) { + // It's fine if the service lost event didn't show up. The service may not ever be + // advertised. } finally { mNsdManager.stopServiceDiscovery(listener); } - assertThrows(TimeoutException.class, () -> discoverService(MESHCOP_SERVICE_TYPE)); - tearDownTestNetwork(testNetwork); + assertThrows(TimeoutException.class, () -> discoverService(MESHCOP_SERVICE_TYPE)); } private static void dropAllPermissions() { @@ -1163,14 +1163,17 @@ public class ThreadNetworkControllerTest { } } - TestNetworkTracker setUpTestNetwork() { - return runAsShell( - MANAGE_TEST_NETWORKS, - () -> initTestNetwork(mContext, new LinkAddress("2001:db8:123::/64"), 10_000)); + private void setUpTestNetwork() { + assertThat(mTestNetworkTracker).isNull(); + mTestNetworkTracker = new TapTestNetworkTracker(mContext, mHandlerThread.getLooper()); } - void tearDownTestNetwork(TestNetworkTracker testNetwork) { - runAsShell(MANAGE_TEST_NETWORKS, () -> testNetwork.teardown()); + private void tearDownTestNetwork() throws InterruptedException { + if (mTestNetworkTracker != null) { + mTestNetworkTracker.tearDown(); + } + mHandlerThread.quitSafely(); + mHandlerThread.join(); } private static class DefaultDiscoveryListener implements NsdManager.DiscoveryListener { diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java index 4948c22c3defa5342b9fc3c8568f38da22138377..60a5f2b618f5090361f1691a2230421d87d5f89a 100644 --- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java +++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java @@ -16,7 +16,6 @@ package com.android.server.thread; -import static android.Manifest.permission.ACCESS_NETWORK_STATE; import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ; import static android.net.thread.ThreadNetworkController.STATE_DISABLED; import static android.net.thread.ThreadNetworkController.STATE_ENABLED; diff --git a/thread/tests/utils/Android.bp b/thread/tests/utils/Android.bp new file mode 100644 index 0000000000000000000000000000000000000000..24e9bb9c8fd8d0ca8e722037ba4b7134da5f32e2 --- /dev/null +++ b/thread/tests/utils/Android.bp @@ -0,0 +1,37 @@ +// +// 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_team: "trendy_team_fwk_thread_network", + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_library { + name: "ThreadNetworkTestUtils", + min_sdk_version: "30", + static_libs: [ + "compatibility-device-util-axt", + "net-tests-utils", + "net-utils-device-common", + "net-utils-device-common-bpf", + ], + srcs: [ + "src/**/*.java", + ], + defaults: [ + "framework-connectivity-test-defaults", + ], +} diff --git a/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java new file mode 100644 index 0000000000000000000000000000000000000000..43f177df06d1b10dd73a18cbf303a5087e58bb06 --- /dev/null +++ b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2024 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.utils; + +import static android.Manifest.permission.MANAGE_TEST_NETWORKS; +import static android.net.InetAddresses.parseNumericAddress; +import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; +import static android.system.OsConstants.AF_INET6; +import static android.system.OsConstants.IPPROTO_UDP; +import static android.system.OsConstants.SOCK_DGRAM; + +import static com.android.testutils.RecorderCallback.CallbackEntry.LINK_PROPERTIES_CHANGED; +import static com.android.testutils.TestPermissionUtil.runAsShell; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.TestNetworkInterface; +import android.net.TestNetworkManager; +import android.net.TestNetworkSpecifier; +import android.os.Looper; +import android.system.ErrnoException; +import android.system.Os; + +import com.android.compatibility.common.util.PollingCheck; +import com.android.testutils.TestableNetworkAgent; +import com.android.testutils.TestableNetworkCallback; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** A class that can create/destroy a test network based on TAP interface. */ +public final class TapTestNetworkTracker { + private static final Duration TIMEOUT = Duration.ofSeconds(2); + private final Context mContext; + private final Looper mLooper; + private TestNetworkInterface mInterface; + private TestableNetworkAgent mAgent; + private final TestableNetworkCallback mNetworkCallback; + private final ConnectivityManager mConnectivityManager; + + /** + * Constructs a {@link TapTestNetworkTracker}. + * + * <p>It creates a TAP interface (e.g. testtap0) and registers a test network using that + * interface. It also requests the test network by {@link ConnectivityManager#requestNetwork} so + * the test network won't be automatically turned down by {@link + * com.android.server.ConnectivityService}. + */ + public TapTestNetworkTracker(Context context, Looper looper) { + mContext = context; + mLooper = looper; + mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); + mNetworkCallback = new TestableNetworkCallback(); + runAsShell(MANAGE_TEST_NETWORKS, this::setUpTestNetwork); + } + + /** Tears down the test network. */ + public void tearDown() { + runAsShell(MANAGE_TEST_NETWORKS, this::tearDownTestNetwork); + } + + /** Returns the interface name of the test network. */ + public String getInterfaceName() { + return mInterface.getInterfaceName(); + } + + private void setUpTestNetwork() throws Exception { + mInterface = mContext.getSystemService(TestNetworkManager.class).createTapInterface(); + + mConnectivityManager.requestNetwork(newNetworkRequest(), mNetworkCallback); + + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(getInterfaceName()); + mAgent = + new TestableNetworkAgent( + mContext, + mLooper, + newNetworkCapabilities(), + lp, + new NetworkAgentConfig.Builder().build()); + final Network network = mAgent.register(); + mAgent.markConnected(); + + PollingCheck.check( + "No usable address on interface", + TIMEOUT.toMillis(), + () -> hasUsableAddress(network, getInterfaceName())); + + lp.setLinkAddresses(makeLinkAddresses()); + mAgent.sendLinkProperties(lp); + mNetworkCallback.eventuallyExpect( + LINK_PROPERTIES_CHANGED, + TIMEOUT.toMillis(), + l -> !l.getLp().getAddresses().isEmpty()); + } + + private void tearDownTestNetwork() throws IOException { + mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); + mAgent.unregister(); + mInterface.getFileDescriptor().close(); + mAgent.waitForIdle(TIMEOUT.toMillis()); + } + + private NetworkRequest newNetworkRequest() { + return new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_TRUSTED) + .addTransportType(TRANSPORT_TEST) + .setNetworkSpecifier(new TestNetworkSpecifier(getInterfaceName())) + .build(); + } + + private NetworkCapabilities newNetworkCapabilities() { + return new NetworkCapabilities() + .removeCapability(NET_CAPABILITY_TRUSTED) + .addTransportType(TRANSPORT_TEST) + .setNetworkSpecifier(new TestNetworkSpecifier(getInterfaceName())); + } + + private List<LinkAddress> makeLinkAddresses() { + List<LinkAddress> linkAddresses = new ArrayList<>(); + List<InterfaceAddress> interfaceAddresses = Collections.emptyList(); + + try { + interfaceAddresses = + NetworkInterface.getByName(getInterfaceName()).getInterfaceAddresses(); + } catch (SocketException ignored) { + // Ignore failures when getting the addresses. + } + + for (InterfaceAddress address : interfaceAddresses) { + linkAddresses.add( + new LinkAddress(address.getAddress(), address.getNetworkPrefixLength())); + } + + return linkAddresses; + } + + private static boolean hasUsableAddress(Network network, String interfaceName) { + try { + if (NetworkInterface.getByName(interfaceName).getInterfaceAddresses().isEmpty()) { + return false; + } + } catch (SocketException e) { + return false; + } + // Check if the link-local address can be used. Address flags are not available without + // elevated permissions, so check that bindSocket works. + try { + FileDescriptor sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); + network.bindSocket(sock); + Os.connect(sock, parseNumericAddress("ff02::fb%" + interfaceName), 12345); + Os.close(sock); + } catch (ErrnoException | IOException e) { + return false; + } + return true; + } +}