From 43ddfe74fa792885a1a6c6d87c3168b32df11353 Mon Sep 17 00:00:00 2001 From: Kangping Dong Date: Thu, 26 Jun 2025 14:49:00 +0800 Subject: [PATCH 1/2] [Thread] support Thread NetworkRequest Bug: 374037595 Flag: com.android.net.thread.flags.thread_mobile_enabled Change-Id: Ib13d2a65f7c9693fb998bb98361538e3b6085913 --- thread/framework/Android.bp | 1 + .../net/thread/ActiveOperationalDataset.java | 23 +- .../net/thread/ThreadNetworkSpecifier.java | 4 + .../ThreadNetworkControllerService.java | 117 +++++++-- .../server/thread/ThreadNetworkFactory.java | 163 +++++++++++++ .../thread/cts/ThreadNetworkRequestTest.java | 227 ++++++++++++++++++ thread/tests/unit/AndroidManifest.xml | 1 + .../ThreadNetworkControllerServiceTest.java | 13 +- .../thread/ThreadNetworkFactoryTest.java | 137 +++++++++++ .../thread/utils/TapTestNetworkTracker.java | 2 +- .../utils/ThreadNetworkControllerWrapper.java | 8 +- 11 files changed, 662 insertions(+), 34 deletions(-) create mode 100644 thread/service/java/com/android/server/thread/ThreadNetworkFactory.java create mode 100644 thread/tests/cts/src/android/net/thread/cts/ThreadNetworkRequestTest.java create mode 100644 thread/tests/unit/src/com/android/server/thread/ThreadNetworkFactoryTest.java diff --git a/thread/framework/Android.bp b/thread/framework/Android.bp index f8fe4228cb..3763b36d6f 100644 --- a/thread/framework/Android.bp +++ b/thread/framework/Android.bp @@ -21,6 +21,7 @@ package { filegroup { name: "framework-thread-sources", + defaults: ["framework-sources-module-defaults"], srcs: [ "java/**/*.java", "java/**/*.aidl", diff --git a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java index cd256c9991..43a96757ff 100644 --- a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java +++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java @@ -602,17 +602,18 @@ public final class ActiveOperationalDataset implements Parcelable { // SparseArray is not hashable int result = byteArraySparseArrayHashCode(mChannelMask); result = result * 31 + byteArraySparseArrayHashCode(mUnknownTlvs); - return result * 31 + deepHashCode( - mActiveTimestamp, - mNetworkName, - mExtendedPanId, - mPanId, - mChannel, - mChannelPage, - mPskc, - mNetworkKey, - mMeshLocalPrefix, - mSecurityPolicy); + return result * 31 + + deepHashCode( + mActiveTimestamp, + mNetworkName, + mExtendedPanId, + mPanId, + mChannel, + mChannelPage, + mPskc, + mNetworkKey, + mMeshLocalPrefix, + mSecurityPolicy); } @Override diff --git a/thread/framework/java/android/net/thread/ThreadNetworkSpecifier.java b/thread/framework/java/android/net/thread/ThreadNetworkSpecifier.java index 406de97876..de38a9ed23 100644 --- a/thread/framework/java/android/net/thread/ThreadNetworkSpecifier.java +++ b/thread/framework/java/android/net/thread/ThreadNetworkSpecifier.java @@ -112,6 +112,10 @@ public final class ThreadNetworkSpecifier extends NetworkSpecifier implements Pa /** @hide */ @Override public NetworkSpecifier redact(long redactions) { + if (mActiveDataset == null) { + return new ThreadNetworkSpecifier.Builder(this).build(); + } + var redactedDatasetBuilder = new ActiveOperationalDataset.Builder(mActiveDataset); if ((redactions & REDACT_FOR_THREAD_NETWORK_PRIVILEGED) != 0 || (redactions & REDACT_FOR_ACCESS_FINE_LOCATION) != 0) { diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java index b040498070..e731a5fb6c 100644 --- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java +++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java @@ -66,6 +66,7 @@ import static com.android.server.thread.openthread.IOtDaemon.OT_STATE_ENABLED; import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; import android.Manifest.permission; import android.annotation.NonNull; @@ -89,7 +90,6 @@ import android.net.Network; import android.net.NetworkAgent; import android.net.NetworkAgentConfig; import android.net.NetworkCapabilities; -import android.net.NetworkProvider; import android.net.NetworkRequest; import android.net.NetworkScore; import android.net.TestNetworkSpecifier; @@ -110,11 +110,13 @@ import android.net.thread.ThreadNetworkController; import android.net.thread.ThreadNetworkController.DeviceRole; import android.net.thread.ThreadNetworkException; import android.net.thread.ThreadNetworkException.ErrorCode; +import android.net.thread.ThreadNetworkSpecifier; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; +import android.os.OutcomeReceiver; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.SystemClock; @@ -198,7 +200,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub // particular, the constructor does not run on the handler thread, so it must not touch any of // the non-final fields, nor must it mutate any of the non-final fields inside these objects. - private final NetworkProvider mNetworkProvider; + private final ThreadNetworkFactory mNetworkFactory; private final Supplier mOtDaemonSupplier; private final ConnectivityManager mConnectivityManager; private final RoutingCoordinatorManager mRoutingCoordinatorManager; @@ -233,12 +235,14 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub private InfraLinkState mInfraLinkState; + @Nullable private ThreadNetworkSpecifier mJoinNetworkSpecifier; + @VisibleForTesting ThreadNetworkControllerService( Context context, Handler handler, MockableSystemProperties systemProperties, - NetworkProvider networkProvider, + ThreadNetworkFactory networkFactory, Supplier otDaemonSupplier, ConnectivityManager connectivityManager, RoutingCoordinatorManager routingCoordinatorManager, @@ -253,7 +257,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub mContext = context; mHandler = handler; mSystemProperties = systemProperties; - mNetworkProvider = networkProvider; + mNetworkFactory = networkFactory; mOtDaemonSupplier = otDaemonSupplier; mConnectivityManager = connectivityManager; mRoutingCoordinatorManager = routingCoordinatorManager; @@ -278,8 +282,6 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub HandlerThread handlerThread = new HandlerThread("ThreadHandlerThread"); handlerThread.start(); Handler handler = new Handler(handlerThread.getLooper()); - NetworkProvider networkProvider = - new NetworkProvider(context, handlerThread.getLooper(), "ThreadNetworkProvider"); Map networkToLinkProperties = new HashMap<>(); final ConnectivityManager connectivityManager = context.getSystemService(ConnectivityManager.class); @@ -291,7 +293,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub context, handler, new MockableSystemProperties(), - networkProvider, + ThreadNetworkFactory.newInstance(context, handler.getLooper()), () -> IOtDaemon.Stub.asInterface(CommonConnectivityJni.waitForService("ot_daemon")), connectivityManager, routingCoordinatorManager, @@ -501,7 +503,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub } catch (IOException e) { throw new IllegalStateException("Failed to create Thread tunnel interface", e); } - mConnectivityManager.registerNetworkProvider(mNetworkProvider); + mNetworkFactory.initialize(this); + mNetworkFactory.register(); mUserRestricted = isThreadUserRestricted(); registerUserRestrictionsReceiver(); @@ -904,31 +907,47 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub mTestNetworkAgent = testNetworkAgent; } + private NetworkCapabilities computeNetworkCapabilities( + @Nullable ActiveOperationalDataset activeDataset) { + final var netCapsBuilder = + new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_THREAD) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED) + .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK); + if (activeDataset != null) { + boolean createPartition = + (mJoinNetworkSpecifier != null) + ? mJoinNetworkSpecifier.shouldCreatePartitionIfNotFound() + : false; + final var actualSpecifier = + new ThreadNetworkSpecifier.Builder() + .setActiveOperationalDataset(activeDataset) + .setShouldCreatePartitionIfNotFound(createPartition) + .build(); + netCapsBuilder.setNetworkSpecifier(actualSpecifier); + } + return netCapsBuilder.build(); + } + private NetworkAgent newNetworkAgent() { if (mTestNetworkAgent != null) { return mTestNetworkAgent; } - final var netCapsBuilder = - new NetworkCapabilities.Builder() - .addTransportType(TRANSPORT_THREAD) - .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) - .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED); final var scoreBuilder = new NetworkScore.Builder(); - - netCapsBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK); scoreBuilder.setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK); return new NetworkAgent( mContext, mHandler.getLooper(), LOG.getTag(), - netCapsBuilder.build(), + computeNetworkCapabilities(mOtDaemonCallbackProxy.mActiveDataset), getTunIfLinkProperties(), newLocalNetworkConfig(), scoreBuilder.build(), new NetworkAgentConfig.Builder().build(), - mNetworkProvider) { + mNetworkFactory.getProvider()) { // TODO(b/374037595): use NetworkFactory to handle dynamic network requests @Override @@ -1235,6 +1254,22 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub }; } + private IOtStatusReceiver newOtStatusReceiver( + OutcomeReceiver receiver) { + return new IOtStatusReceiver.Stub() { + @Override + public void onSuccess() { + receiver.onResult(null); + } + + @Override + public void onError(int otError, String message) { + receiver.onError( + new ThreadNetworkException(otErrorToAndroidError(otError), message)); + } + }; + } + private IOtOutputReceiver newOtOutputReceiver(OutputReceiverWrapper receiver) { return new IOtOutputReceiver.Stub() { @Override @@ -1299,15 +1334,46 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub @NonNull OperationReceiverWrapper receiver) { checkOnHandlerThread(); + mJoinNetworkSpecifier = null; try { // The otDaemon.join() will leave first if this device is currently attached - getOtDaemon().join(activeDataset.toThreadTlvs(), newOtStatusReceiver(receiver)); + getOtDaemon() + .join( + activeDataset.toThreadTlvs(), + true /* createPartitionIfNotFound */, + newOtStatusReceiver(receiver)); } catch (RemoteException | ThreadNetworkException e) { LOG.e("otDaemon.join failed", e); receiver.onError(e); } } + void joinWithSpecifier( + ThreadNetworkSpecifier specifier, + OutcomeReceiver receiver) { + mHandler.post(() -> joinWithSpecifierInternal(specifier, receiver)); + } + + private void joinWithSpecifierInternal( + ThreadNetworkSpecifier specifier, + OutcomeReceiver receiver) { + checkOnHandlerThread(); + + mJoinNetworkSpecifier = specifier; + byte[] dataset = requireNonNull(specifier.getActiveOperationalDataset()).toThreadTlvs(); + boolean createPartitionIfNotFound = specifier.shouldCreatePartitionIfNotFound(); + try { + getOtDaemon().join(dataset, createPartitionIfNotFound, newOtStatusReceiver(receiver)); + } catch (RemoteException | ThreadNetworkException e) { + LOG.e("otDaemon.joinWithSpecifier failed", e); + if (e instanceof RemoteException re) { + receiver.onError(new ThreadNetworkException(ERROR_INTERNAL_ERROR, re.getMessage())); + } else { + receiver.onError((ThreadNetworkException) e); + } + } + } + @Override public void scheduleMigration( @NonNull PendingOperationalDataset pendingDataset, @@ -1509,6 +1575,13 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub setInfraLinkState(newInfraLinkStateBuilder().build()); } + private void handleActiveDatasetChanged(@Nullable ActiveOperationalDataset newActiveDataset) { + if (mNetworkAgent == null) { + return; + } + mNetworkAgent.sendNetworkCapabilities(computeNetworkCapabilities(newActiveDataset)); + } + private void handleThreadInterfaceStateChanged(boolean isUp) { try { mTunIfController.setInterfaceUp(isUp); @@ -1994,6 +2067,14 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub ActiveOperationalDataset activeDataset, long listenerId) { checkOnHandlerThread(); boolean hasChange = !Objects.equals(mActiveDataset, activeDataset); + if (hasChange) { + LOG.i( + "Current Active Operational Dataset changed: " + + mActiveDataset + + " -> " + + activeDataset); + handleActiveDatasetChanged(activeDataset); + } for (var callbackEntry : mOpDatasetCallbacks.entrySet()) { if (!hasChange && callbackEntry.getValue().id != listenerId) { diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkFactory.java b/thread/service/java/com/android/server/thread/ThreadNetworkFactory.java new file mode 100644 index 0000000000..ee3bba50aa --- /dev/null +++ b/thread/service/java/com/android/server/thread/ThreadNetworkFactory.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2025 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 com.android.server.thread; + +import static android.net.NetworkCapabilities.TRANSPORT_THREAD; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.MatchAllNetworkSpecifier; +import android.net.NetworkCapabilities; +import android.net.NetworkFactory; +import android.net.NetworkRequest; +import android.net.thread.IOperationReceiver; +import android.net.thread.ThreadNetworkException; +import android.net.thread.ThreadNetworkSpecifier; +import android.os.Looper; +import android.os.OutcomeReceiver; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.net.module.util.SharedLog; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +// TODO(b/442787508): replace NetworkFactory subclass with direct usage of NetworkProvider +/** + * Serves network requests that are initiated from apps via the {@link + * ConnectivityManager#requestNetwork} API. + */ +public final class ThreadNetworkFactory extends NetworkFactory { + private static final SharedLog LOG = + ThreadNetworkLogger.forSubComponent("ThreadNetworkFactory"); + private final List mActiveRequests = new ArrayList<>(); + private @Nullable ThreadNetworkControllerService mControllerService; + + /** Creates a new {@link ThreadNetworkFactory} instance. */ + public static ThreadNetworkFactory newInstance(Context context, Looper looper) { + return new ThreadNetworkFactory(context, looper); + } + + @VisibleForTesting + ThreadNetworkFactory(Context context, Looper looper) { + super(looper, context, LOG.getTag(), createBaseCapabilities()); + mControllerService = null; + } + + void initialize(ThreadNetworkControllerService controllerService) { + mControllerService = controllerService; + } + + /** Returns the basic capabilities that all Thread networks initially have. */ + private static NetworkCapabilities createBaseCapabilities() { + return new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_THREAD) + .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED) + .setLinkUpstreamBandwidthKbps(100) + .setLinkDownstreamBandwidthKbps(100) + // By default accepts all network requests which have a specifier + .setNetworkSpecifier(new MatchAllNetworkSpecifier()) + .build(); + } + + @Override + public boolean acceptRequest(@NonNull final NetworkRequest request) { + // the filtering is done in {@link #needNetworkFor} + return true; + } + + @Override + public void needNetworkFor(@NonNull final NetworkRequest request) { + LOG.i("network request is received for " + request); + if (!request.hasTransport(TRANSPORT_THREAD)) { + return; + } + + if (request.getNetworkSpecifier() instanceof ThreadNetworkSpecifier specifier + && specifier.getActiveOperationalDataset() != null) { + mActiveRequests.add(request); + join(specifier); + } else { + // Can do nothing if ThreadNetworkSpecifier or the Dataset is missing + return; + } + } + + private void join(ThreadNetworkSpecifier specifier) { + var receiver = + new OutcomeReceiver() { + @Override + public void onError(ThreadNetworkException e) { + LOG.e("Failed to join Thread network: " + specifier, e); + } + + @Override + public void onResult(Void v) { + LOG.i("Successfully joined Thread network: " + specifier); + } + }; + mControllerService.joinWithSpecifier(specifier, receiver); + } + + @Override + public void releaseNetworkFor(@NonNull final NetworkRequest request) { + LOG.i("network release request is received for " + request); + if (mActiveRequests.isEmpty()) { + LOG.w("Unexpect release of untracked request " + request); + return; + } + + NetworkRequest lastRequest = mActiveRequests.getLast(); + mActiveRequests.removeAll(Collections.singletonList(request)); + + if (mActiveRequests.isEmpty()) { + leave(request); + } else if (!Objects.equals(mActiveRequests.getLast(), lastRequest)) { + // If the last request is changed, let's re-join the network with the new latest + // request + join((ThreadNetworkSpecifier) mActiveRequests.getLast().getNetworkSpecifier()); + } + } + + private void leave(NetworkRequest request) { + var receiver = + new IOperationReceiver.Stub() { + @Override + public void onSuccess() { + LOG.i("Successfully left Thread network for releasing request " + request); + } + + @Override + public void onError(int errorCode, String errorMessage) { + var ex = new ThreadNetworkException(errorCode, errorMessage); + LOG.e( + "Failed to leave Thread network for releasing request " + request, + ex); + } + }; + mControllerService.leave(receiver); + } +} diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkRequestTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkRequestTest.java new file mode 100644 index 0000000000..45957037d7 --- /dev/null +++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkRequestTest.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2025 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.cts; + +import static android.Manifest.permission.ACCESS_FINE_LOCATION; +import static android.Manifest.permission.THREAD_NETWORK_PRIVILEGED; +import static android.net.ConnectivityManager.NetworkCallback.FLAG_INCLUDE_LOCATION_INFO; +import static android.net.NetworkCapabilities.TRANSPORT_THREAD; +import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.net.thread.flags.Flags.FLAG_THREAD_MOBILE_ENABLED; + +import static com.google.common.io.BaseEncoding.base16; + +import static org.junit.Assert.assertThrows; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkRequest; +import android.net.thread.ActiveOperationalDataset; +import android.net.thread.ThreadNetworkSpecifier; +import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature; +import android.net.thread.utils.ThreadNetworkControllerWrapper; +import android.platform.test.annotations.RequiresFlagsEnabled; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.filters.LargeTest; + +import com.android.testutils.TestableNetworkCallback; +import com.android.testutils.TestableNetworkCallback.Event; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Tests for requesting a Thread network via the {@link ConnectivityManager#requestNetwork} and + * {@link ThreadNetworkSpecifier} API. + */ +@LargeTest +@RequiresThreadFeature +@RequiresFlagsEnabled(FLAG_THREAD_MOBILE_ENABLED) +public final class ThreadNetworkRequestTest { + private static final int JOIN_TIMEOUT_MILLIS = 30_000; + private static final Duration STOP_TIMEOUT = Duration.ofSeconds(2_000); + + // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new": + // Active Timestamp: 1 + // Channel: 13 + // Wake-up Channel: 18 + // Channel Mask: 0x07fff800 + // Ext PAN ID: 492cc112e634e51b + // Mesh Local Prefix: fd2f:357c:dd90:2120::/64 + // Network Key: 9f55333f52eb25dbfdbe62121b39bee4 + // Network Name: OpenThread-145a + // PAN ID: 0x145a + // PSKc: 6d81fe700160ab9f63f50f78ccf0733c + // Security Policy: 672 onrc 0 + private static final byte[] DATASET_TLVS_1 = + base16().ignoreCase() + .decode( + "0e080000000000010000000300000d4a0300001235060004001fff" + + "e00208492cc112e634e51b0708fd2f357cdd90212005109f55333f" + + "52eb25dbfdbe62121b39bee4030f4f70656e5468726561642d3134" + + "35610102145a04106d81fe700160ab9f63f50f78ccf0733c0c0402a0f7f8"); + // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new": + // Active Timestamp: 1 + // Channel: 24 + // Wake-up Channel: 19 + // Channel Mask: 0x07fff800 + // Ext PAN ID: 779732128edb4591 + // Mesh Local Prefix: fdbd:3671:821d:18c1::/64 + // Network Key: 6a643ec02d38ef55b21b26cec46f622d + // Network Name: OpenThread-64be + // PAN ID: 0x64be + // PSKc: 37968aab079fac83fa67b14c9bb7f598 + // Security Policy: 672 onrc 0 + private static final byte[] DATASET_TLVS_2 = + base16().ignoreCase() + .decode( + "0e08000000000001000000030000184a0300001335060004001fff" + + "e00208779732128edb45910708fdbd3671821d18c105106a643ec0" + + "2d38ef55b21b26cec46f622d030f4f70656e5468726561642d3634" + + "6265010264be041037968aab079fac83fa67b14c9bb7f5980c0402a0f7f8"); + + private static final ActiveOperationalDataset TEST_DATASET_1 = + ActiveOperationalDataset.fromThreadTlvs(DATASET_TLVS_1); + private static final ActiveOperationalDataset TEST_DATASET_2 = + ActiveOperationalDataset.fromThreadTlvs(DATASET_TLVS_2); + + private final Context mContext = ApplicationProvider.getApplicationContext(); + private ConnectivityManager cm; + private ThreadNetworkControllerWrapper mController; + private List mCallbacks; + private Set mGrantedPermissions; + + private void grantPermissions(String... permissions) { + for (String permission : permissions) { + mGrantedPermissions.add(permission); + } + String[] allPermissions = new String[mGrantedPermissions.size()]; + mGrantedPermissions.toArray(allPermissions); + getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(allPermissions); + } + + private static void dropAllPermissions() { + getInstrumentation().getUiAutomation().dropShellPermissionIdentity(); + } + + @Before + public void setUp() throws Exception { + mCallbacks = new ArrayList<>(); + mGrantedPermissions = new HashSet(); + cm = mContext.getSystemService(ConnectivityManager.class); + mController = ThreadNetworkControllerWrapper.newInstance(mContext); + mController.setBorderRouterEnabledAndWait(false); + mController.leaveAndWait(); + } + + @After + public void tearDown() throws Exception { + dropAllPermissions(); + for (var callback : mCallbacks) { + cm.unregisterNetworkCallback(callback); + } + mController.waitForRole(DEVICE_ROLE_STOPPED, STOP_TIMEOUT); + } + + private TestableNetworkCallback newNetworkCallback() { + var callback = + new TestableNetworkCallback( + 250L /* timeoutMs */, + 250L /* noCallbackTimeoutMs */, + () -> {} /* waiterFunc */, + "ThreadNetworkRequestTest" /* logTag */, + FLAG_INCLUDE_LOCATION_INFO); + mCallbacks.add(callback); + return callback; + } + + private NetworkRequest newThreadNetworkRequest( + ActiveOperationalDataset dataset, boolean createPartitionIfNotFound) { + var specifier = + new ThreadNetworkSpecifier.Builder() + .setActiveOperationalDataset(dataset) + .setShouldCreatePartitionIfNotFound(createPartitionIfNotFound) + .build(); + return new NetworkRequest.Builder() + .addTransportType(TRANSPORT_THREAD) + .setNetworkSpecifier(specifier) + .build(); + } + + @Test + public void requestNetwork_withoutThreadPrivilegedPermission_failsWithUnavailable() { + dropAllPermissions(); + var request = newThreadNetworkRequest(TEST_DATASET_1, true /* createPartitionIfNotFound */); + var callback = newNetworkCallback(); + + assertThrows(SecurityException.class, () -> cm.requestNetwork(request, callback)); + mCallbacks.remove(callback); + } + + @Test + public void requestNetwork_withCreatePartitionEnabled_succeedsWithAvailable() { + grantPermissions(THREAD_NETWORK_PRIVILEGED, ACCESS_FINE_LOCATION); + var request = newThreadNetworkRequest(TEST_DATASET_1, true /* createPartitionIfNotFound */); + + var callback = newNetworkCallback(); + cm.requestNetwork(request, callback); + + callback.eventuallyExpect(Event.AVAILABLE, JOIN_TIMEOUT_MILLIS); + } + + @Test + public void requestNetwork_multipleRequests_theLastOneWins() { + grantPermissions(THREAD_NETWORK_PRIVILEGED, ACCESS_FINE_LOCATION); + var request1 = + newThreadNetworkRequest(TEST_DATASET_1, true /* createPartitionIfNotFound */); + var request2 = + newThreadNetworkRequest(TEST_DATASET_2, true /* createPartitionIfNotFound */); + + var callback1 = newNetworkCallback(); + var callback2 = newNetworkCallback(); + cm.requestNetwork(request1, callback1); + cm.requestNetwork(request2, callback2); + + callback2.eventuallyExpect(Event.AVAILABLE, JOIN_TIMEOUT_MILLIS); + } + + @Test + public void unregisterNetworkCallback_threadIsStopped() throws Exception { + grantPermissions(THREAD_NETWORK_PRIVILEGED, ACCESS_FINE_LOCATION); + var request = newThreadNetworkRequest(TEST_DATASET_1, true /* createPartitionIfNotFound */); + var callback = newNetworkCallback(); + cm.requestNetwork(request, callback); + callback.eventuallyExpect(Event.AVAILABLE, JOIN_TIMEOUT_MILLIS); + + cm.unregisterNetworkCallback(callback); + mCallbacks.remove(callback); + + dropAllPermissions(); + mController.waitForRole(DEVICE_ROLE_STOPPED, STOP_TIMEOUT); + } +} diff --git a/thread/tests/unit/AndroidManifest.xml b/thread/tests/unit/AndroidManifest.xml index 8442e804e6..c6f67deef1 100644 --- a/thread/tests/unit/AndroidManifest.xml +++ b/thread/tests/unit/AndroidManifest.xml @@ -20,6 +20,7 @@ package="android.net.thread.unittests"> + 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 51f20e15d6..c52abf38a7 100644 --- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java +++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java @@ -179,8 +179,11 @@ public final class ThreadNetworkControllerServiceTest { private static final String TEST_VENDOR_NAME = "test vendor"; private static final String TEST_MODEL_NAME = "test model"; private static final LinkAddress TEST_NAT64_CIDR = new LinkAddress("192.168.255.0/24"); + private static final int THREAD_NETWORK_PROVIDER_ID = 1; @Mock private MockableSystemProperties mMockSystemProperties; + @Mock private ThreadNetworkFactory mMockNetworkFactory; + @Mock private NetworkProvider mMockNetworkProvider; @Mock private ConnectivityManager mMockConnectivityManager; @Mock private RoutingCoordinatorManager mMockRoutingCoordinatorManager; @Mock private NetworkAgent mMockNetworkAgent; @@ -233,8 +236,12 @@ public final class ThreadNetworkControllerServiceTest { mTestLooper = new TestLooper(); final Handler handler = new Handler(mTestLooper.getLooper()); - NetworkProvider networkProvider = - new NetworkProvider(mContext, mTestLooper.getLooper(), "ThreadNetworkProvider"); + + when(mMockNetworkFactory.getProvider()).thenReturn(mMockNetworkProvider); + doNothing().when(mMockNetworkFactory).register(); + + when(mMockConnectivityManager.registerNetworkProvider(any(NetworkProvider.class))) + .thenReturn(THREAD_NETWORK_PROVIDER_ID); when(mMockRoutingCoordinatorManager.requestDownstreamAddress(any())) .thenReturn(TEST_NAT64_CIDR); @@ -272,7 +279,7 @@ public final class ThreadNetworkControllerServiceTest { mContext, handler, mMockSystemProperties, - networkProvider, + mMockNetworkFactory, () -> mFakeOtDaemon, mMockConnectivityManager, mMockRoutingCoordinatorManager, diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkFactoryTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkFactoryTest.java new file mode 100644 index 0000000000..931c18fdc3 --- /dev/null +++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkFactoryTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2025 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 com.android.server.thread; + +import static com.google.common.io.BaseEncoding.base16; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import android.content.Context; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.thread.ActiveOperationalDataset; +import android.net.thread.ThreadNetworkSpecifier; +import android.os.test.TestLooper; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Unit tests for {@link ThreadNetworkFactory}. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public final class ThreadNetworkFactoryTest { + // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new": + // Active Timestamp: 1 + // Channel: 19 + // Channel Mask: 0x07FFF800 + // Ext PAN ID: ACC214689BC40BDF + // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64 + // Network Key: F26B3153760F519A63BAFDDFFC80D2AF + // Network Name: OpenThread-d9a0 + // PAN ID: 0xD9A0 + // PSKc: A245479C836D551B9CA557F7B9D351B4 + // Security Policy: 672 onrcb + private static final byte[] DATASET_TLVS = + base16().decode( + "0E080000000000010000000300001335060004001FFFE002" + + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31" + + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561" + + "642D643961300102D9A00410A245479C836D551B9CA557F7" + + "B9D351B40C0402A0FFF8"); + private static final ActiveOperationalDataset ACTIVE_DATASET = + ActiveOperationalDataset.fromThreadTlvs(DATASET_TLVS); + + private Context mContext; + private TestLooper mTestLooper; + @Mock private ThreadNetworkControllerService mMockControllerService; + + private ThreadNetworkFactory mFactory; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = ApplicationProvider.getApplicationContext(); + mTestLooper = new TestLooper(); + mFactory = new ThreadNetworkFactory(mContext, mTestLooper.getLooper()); + mFactory.initialize(mMockControllerService); + } + + @Test + public void needNetworkFor_noThreadTransport_doNothing() { + NetworkRequest request = + new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build(); + + mFactory.needNetworkFor(request); + + verifyNoInteractions(mMockControllerService); + } + + @Test + public void needNetworkFor_threadTransportAndNoSpecifier_doNothing() { + NetworkRequest request = + new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_THREAD) + .build(); + + mFactory.needNetworkFor(request); + + verifyNoInteractions(mMockControllerService); + } + + @Test + public void needNetworkFor_threadTransportAndSpecifier_joinIsCalled() { + ThreadNetworkSpecifier specifier = + new ThreadNetworkSpecifier.Builder() + .setActiveOperationalDataset(ACTIVE_DATASET) + .build(); + NetworkRequest request = + new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_THREAD) + .setNetworkSpecifier(specifier) + .build(); + + mFactory.needNetworkFor(request); + + verify(mMockControllerService).joinWithSpecifier(eq(specifier), any()); + } + + @Test + public void needNetworkFor_specifierWithNoDataset_doNothing() { + NetworkRequest request = + new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_THREAD) + .setNetworkSpecifier(new ThreadNetworkSpecifier.Builder().build()) + .build(); + + mFactory.needNetworkFor(request); + + verifyNoInteractions(mMockControllerService); + } +} diff --git a/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java index c117975fc7..ba94289977 100644 --- a/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java +++ b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java @@ -17,8 +17,8 @@ package android.net.thread.utils; import static android.Manifest.permission.MANAGE_TEST_NETWORKS; -import static com.android.testutils.TestableNetworkCallback.Event.LINK_PROPERTIES_CHANGED; import static com.android.testutils.TestPermissionUtil.runAsShell; +import static com.android.testutils.TestableNetworkCallback.Event.LINK_PROPERTIES_CHANGED; import android.content.Context; import android.net.ConnectivityManager; diff --git a/thread/tests/utils/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java b/thread/tests/utils/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java index c4150cb103..a6efc08cb1 100644 --- a/thread/tests/utils/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java +++ b/thread/tests/utils/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java @@ -189,7 +189,7 @@ public final class ThreadNetworkControllerWrapper { try { return future.get(timeout.toSeconds(), SECONDS); } finally { - mController.unregisterStateCallback(callback); + runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback)); } } @@ -230,6 +230,12 @@ public final class ThreadNetworkControllerWrapper { future.get(CONFIG_TIMEOUT.toSeconds(), SECONDS); } + public void setBorderRouterEnabledAndWait(boolean enabled) throws Exception { + final ThreadConfiguration config = + new ThreadConfiguration.Builder().setBorderRouterEnabled(enabled).build(); + setConfigurationAndWait(config); + } + public void setNat64EnabledAndWait(boolean enabled) throws Exception { final ThreadConfiguration config = getConfiguration(); final ThreadConfiguration newConfig = From c2c6ce19663bdea4ccab6a457881ac705829a09b Mon Sep 17 00:00:00 2001 From: Sandeep Date: Mon, 15 Sep 2025 02:01:57 -0700 Subject: [PATCH 2/2] bpf: move MSS calculation under CE check to avoid overhead Currently, the MSS-based packet count calculation is performed for every packet, regardless of whether the IP ECN field indicates CE (congestion experienced). Since the computed value is only used when CE is set, this results in unnecessary per-packet processing. Move the MSS calculation inside the ip_ecn == 0b11 condition so that it is only executed when a CE mark is actually present. This reduces overhead on the fast path while preserving correct CE accounting. No functional change in CE handling, only an optimization in execution flow. Test: TreeHugger Flag: EXEMPT PURE_REFACTOR Signed-off-by: Sandeep Change-Id: I5cae40f914473d9f914c826d50dd91c93e047ccc --- bpf/progs/tcpAccECN.c | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/bpf/progs/tcpAccECN.c b/bpf/progs/tcpAccECN.c index 609d09413d..4c8f5a563b 100644 --- a/bpf/progs/tcpAccECN.c +++ b/bpf/progs/tcpAccECN.c @@ -299,17 +299,16 @@ DEFINE_BPF_PROG_KVER(schedcls, ingress_accecn_eth, , AID_SYSTEM, 6_1) return TC_ACT_PIPE; } - uint32_t ce_packets = 1; - uint16_t *mss_ptr = bpf_l4s_accecn_mss_map_lookup_elem(&flow_key); - if (mss_ptr) { - uint16_t mss = *mss_ptr; - if (mss != 0xFFFF && mss != 0) { - ce_packets = (payload_size / mss) + 1; - } - } - // update the map if CE is marked if (ip_ecn == 0b11) { + uint32_t ce_packets = 1; + uint16_t *mss_ptr = bpf_l4s_accecn_mss_map_lookup_elem(&flow_key); + if (mss_ptr) { + uint16_t mss = *mss_ptr; + if (mss != 0xFFFF && mss != 0) { + ce_packets = (payload_size / mss) + 1; + } + } __sync_fetch_and_add(ce_count, ce_packets); } @@ -547,17 +546,16 @@ DEFINE_BPF_PROG_KVER(schedcls, ingress_accecn_rawip, , AID_SYSTEM, 6_1) return TC_ACT_PIPE; } - uint32_t ce_packets = 1; - uint16_t *mss_ptr = bpf_l4s_accecn_mss_map_lookup_elem(&flow_key); - if (mss_ptr) { - uint16_t mss = *mss_ptr; - if (mss != 0xFFFF && mss != 0) { - ce_packets = (payload_size / mss) + 1; - } - } - // update the map if CE is marked if (ip_ecn == 0b11) { + uint32_t ce_packets = 1; + uint16_t *mss_ptr = bpf_l4s_accecn_mss_map_lookup_elem(&flow_key); + if (mss_ptr) { + uint16_t mss = *mss_ptr; + if (mss != 0xFFFF && mss != 0) { + ce_packets = (payload_size / mss) + 1; + } + } __sync_fetch_and_add(ce_count, ce_packets); }