From 56449db1be17b87bff8e99e2af7697fc1eb00d30 Mon Sep 17 00:00:00 2001 From: Kangping Dong Date: Thu, 17 Oct 2024 00:22:21 +0800 Subject: [PATCH] [Thread] add private API for disabling border router Bug: 374036806 Change-Id: Ib8ca8bbd2b3018c39c9250e1b3ce70b193273c1b --- common/thread_flags.aconfig | 9 + .../net/thread/ThreadConfiguration.java | 55 +++++- .../ThreadNetworkControllerService.java | 164 ++++++++++++++---- .../thread/ThreadNetworkShellCommand.java | 5 +- .../thread/ThreadPersistentSettings.java | 9 + .../thread/cts/ThreadConfigurationTest.java | 17 +- .../cts/ThreadNetworkControllerTest.java | 51 +++++- .../net/thread/ThreadIntegrationTest.java | 61 +++++++ .../thread/ThreadNetworkShellCommandTest.java | 2 +- .../net/thread/utils/OtDaemonController.java | 10 ++ .../thread/ThreadNetworkShellCommandTest.java | 2 +- 11 files changed, 331 insertions(+), 54 deletions(-) diff --git a/common/thread_flags.aconfig b/common/thread_flags.aconfig index 60120bcc54..8cc2bb4918 100644 --- a/common/thread_flags.aconfig +++ b/common/thread_flags.aconfig @@ -44,3 +44,12 @@ flag { description: "Controls whether the setConfiguration API of NAT64 feature is enabled" bug: "368456504" } + +flag { + name: "thread_mobile_enabled" + is_exported: true + is_fixed_read_only: true + namespace: "thread_network" + description: "Controls whether Thread support for mobile devices is enabled" + bug: "368867060" +} diff --git a/thread/framework/java/android/net/thread/ThreadConfiguration.java b/thread/framework/java/android/net/thread/ThreadConfiguration.java index edb5021f14..082926544b 100644 --- a/thread/framework/java/android/net/thread/ThreadConfiguration.java +++ b/thread/framework/java/android/net/thread/ThreadConfiguration.java @@ -44,18 +44,38 @@ import java.util.Objects; @FlaggedApi(Flags.FLAG_CONFIGURATION_ENABLED) @SystemApi public final class ThreadConfiguration implements Parcelable { + private final boolean mBorderRouterEnabled; private final boolean mNat64Enabled; private final boolean mDhcpv6PdEnabled; private ThreadConfiguration(Builder builder) { - this(builder.mNat64Enabled, builder.mDhcpv6PdEnabled); + this(builder.mBorderRouterEnabled, builder.mNat64Enabled, builder.mDhcpv6PdEnabled); } - private ThreadConfiguration(boolean nat64Enabled, boolean dhcpv6PdEnabled) { + private ThreadConfiguration( + boolean borderRouterEnabled, boolean nat64Enabled, boolean dhcpv6PdEnabled) { + this.mBorderRouterEnabled = borderRouterEnabled; this.mNat64Enabled = nat64Enabled; this.mDhcpv6PdEnabled = dhcpv6PdEnabled; } + /** + * Returns {@code true} if this device is operating as a Thread Border Router. + * + *

A Thread Border Router works on both Thread and infrastructure networks. For example, it + * can route packets between Thread and infrastructure networks (e.g. Wi-Fi or Ethernet), makes + * devices in both networks discoverable to each other, and accepts connections from external + * commissioner. + * + *

Note it costs significantly more power to operate as a Border Router, so this is typically + * only enabled for wired Android devices (e.g. TV or display). + * + * @hide + */ + public boolean isBorderRouterEnabled() { + return mBorderRouterEnabled; + } + /** Returns {@code true} if NAT64 is enabled. */ public boolean isNat64Enabled() { return mNat64Enabled; @@ -78,22 +98,24 @@ public final class ThreadConfiguration implements Parcelable { return false; } else { ThreadConfiguration otherConfig = (ThreadConfiguration) other; - return mNat64Enabled == otherConfig.mNat64Enabled + return mBorderRouterEnabled == otherConfig.mBorderRouterEnabled + && mNat64Enabled == otherConfig.mNat64Enabled && mDhcpv6PdEnabled == otherConfig.mDhcpv6PdEnabled; } } @Override public int hashCode() { - return Objects.hash(mNat64Enabled, mDhcpv6PdEnabled); + return Objects.hash(mBorderRouterEnabled, mNat64Enabled, mDhcpv6PdEnabled); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append('{'); - sb.append("Nat64Enabled=").append(mNat64Enabled); - sb.append(", Dhcpv6PdEnabled=").append(mDhcpv6PdEnabled); + sb.append("borderRouterEnabled=").append(mBorderRouterEnabled); + sb.append(", nat64Enabled=").append(mNat64Enabled); + sb.append(", dhcpv6PdEnabled=").append(mDhcpv6PdEnabled); sb.append('}'); return sb.toString(); } @@ -105,6 +127,7 @@ public final class ThreadConfiguration implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeBoolean(mBorderRouterEnabled); dest.writeBoolean(mNat64Enabled); dest.writeBoolean(mDhcpv6PdEnabled); } @@ -114,6 +137,7 @@ public final class ThreadConfiguration implements Parcelable { @Override public ThreadConfiguration createFromParcel(Parcel in) { ThreadConfiguration.Builder builder = new ThreadConfiguration.Builder(); + builder.setBorderRouterEnabled(in.readBoolean()); builder.setNat64Enabled(in.readBoolean()); builder.setDhcpv6PdEnabled(in.readBoolean()); return builder.build(); @@ -133,6 +157,10 @@ public final class ThreadConfiguration implements Parcelable { @FlaggedApi(Flags.FLAG_SET_NAT64_CONFIGURATION_ENABLED) @SystemApi public static final class Builder { + // Thread in Android V is default to a Border Router device, so the default value here needs + // to be {@code true} to be compatible. + private boolean mBorderRouterEnabled = true; + private boolean mNat64Enabled = false; private boolean mDhcpv6PdEnabled = false; @@ -156,10 +184,25 @@ public final class ThreadConfiguration implements Parcelable { public Builder(@NonNull ThreadConfiguration config) { Objects.requireNonNull(config); + mBorderRouterEnabled = config.mBorderRouterEnabled; mNat64Enabled = config.mNat64Enabled; mDhcpv6PdEnabled = config.mDhcpv6PdEnabled; } + /** + * Enables or disables this device as a Border Router. + * + *

Defaults to {@code true} if this method is not called. + * + * @see ThreadConfiguration#isBorderRouterEnabled + * @hide + */ + @NonNull + public Builder setBorderRouterEnabled(boolean enabled) { + this.mBorderRouterEnabled = enabled; + return this; + } + /** * Enables or disables NAT64 for the device. * diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java index ba4eeafc2f..e3c2a28ee5 100644 --- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java +++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java @@ -19,6 +19,7 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.net.MulticastRoutingConfig.CONFIG_FORWARD_NONE; import static android.net.MulticastRoutingConfig.FORWARD_SELECTED; import static android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE; +import static android.net.NetworkCapabilities.TRANSPORT_THREAD; import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ; import static android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID; import static android.net.thread.ActiveOperationalDataset.LENGTH_MESH_LOCAL_PREFIX_BITS; @@ -122,8 +123,8 @@ import android.util.SparseArray; import com.android.connectivity.resources.R; import com.android.internal.annotations.VisibleForTesting; -import com.android.net.module.util.RoutingCoordinatorManager; import com.android.net.module.util.IIpv4PrefixRequest; +import com.android.net.module.util.RoutingCoordinatorManager; import com.android.net.module.util.SharedLog; import com.android.server.ServiceManagerWrapper; import com.android.server.connectivity.ConnectivityResources; @@ -151,7 +152,6 @@ import java.time.Clock; import java.time.DateTimeException; import java.time.Instant; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -222,6 +222,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub private NetworkRequest mUpstreamNetworkRequest; private UpstreamNetworkCallback mUpstreamNetworkCallback; private TestNetworkSpecifier mUpstreamTestNetworkSpecifier; + private ThreadNetworkCallback mThreadNetworkCallback; private final Map mNetworkToLinkProperties; private final ThreadPersistentSettings mPersistentSettings; private final UserManager mUserManager; @@ -315,14 +316,6 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub .build(); } - private LocalNetworkConfig newLocalNetworkConfig() { - return new LocalNetworkConfig.Builder() - .setUpstreamMulticastRoutingConfig(mUpstreamMulticastRoutingConfig) - .setDownstreamMulticastRoutingConfig(mDownstreamMulticastRoutingConfig) - .setUpstreamSelector(mUpstreamNetworkRequest) - .build(); - } - private void maybeInitializeOtDaemon() { if (!shouldEnableThread()) { return; @@ -476,10 +469,16 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub throw new IllegalStateException("Failed to create Thread tunnel interface", e); } mConnectivityManager.registerNetworkProvider(mNetworkProvider); - requestUpstreamNetwork(); - registerThreadNetworkCallback(); mUserRestricted = isThreadUserRestricted(); registerUserRestrictionsReceiver(); + + if (isBorderRouterMode()) { + requestUpstreamNetwork(); + registerThreadNetworkCallback(); + } else { + cancelRequestUpstreamNetwork(); + unregisterThreadNetworkCallback(); + } maybeInitializeOtDaemon(); } @@ -588,7 +587,20 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub LOG.i("Set Thread configuration: " + configuration); final boolean changed = mPersistentSettings.putConfiguration(configuration); + + if (changed) { + if (isBorderRouterMode()) { + requestUpstreamNetwork(); + registerThreadNetworkCallback(); + } else { + cancelRequestUpstreamNetwork(); + unregisterThreadNetworkCallback(); + disableBorderRouting(); + } + } + receiver.onSuccess(); + if (changed) { for (IConfigurationReceiver configReceiver : mConfigurationReceivers.keySet()) { try { @@ -598,6 +610,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub } } } + try { getOtDaemon() .setConfiguration( @@ -612,11 +625,17 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub private static OtDaemonConfiguration newOtDaemonConfig( @NonNull ThreadConfiguration threadConfig) { return new OtDaemonConfiguration.Builder() + .setBorderRouterEnabled(threadConfig.isBorderRouterEnabled()) .setNat64Enabled(threadConfig.isNat64Enabled()) .setDhcpv6PdEnabled(threadConfig.isDhcpv6PdEnabled()) .build(); } + /** Returns {@code true} if this device is operating as a border router. */ + private boolean isBorderRouterMode() { + return mPersistentSettings.getConfiguration().isBorderRouterEnabled(); + } + @Override public void registerConfigurationCallback(@NonNull IConfigurationReceiver callback) { enforceAllPermissionsGranted(permission.THREAD_NETWORK_PRIVILEGED); @@ -723,7 +742,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub private void requestUpstreamNetwork() { if (mUpstreamNetworkCallback != null) { - throw new AssertionError("The upstream network request is already there."); + return; } mUpstreamNetworkCallback = new UpstreamNetworkCallback(); mConnectivityManager.registerNetworkCallback( @@ -732,7 +751,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub private void cancelRequestUpstreamNetwork() { if (mUpstreamNetworkCallback == null) { - throw new AssertionError("The upstream network request null."); + return; } mNetworkToLinkProperties.clear(); mConnectivityManager.unregisterNetworkCallback(mUpstreamNetworkCallback); @@ -812,16 +831,28 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub } private void registerThreadNetworkCallback() { - mConnectivityManager.registerNetworkCallback( + if (mThreadNetworkCallback != null) { + return; + } + + mThreadNetworkCallback = new ThreadNetworkCallback(); + NetworkRequest request = new NetworkRequest.Builder() // clearCapabilities() is needed to remove forbidden capabilities and UID // requirement. .clearCapabilities() - .addTransportType(NetworkCapabilities.TRANSPORT_THREAD) + .addTransportType(TRANSPORT_THREAD) .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK) - .build(), - new ThreadNetworkCallback(), - mHandler); + .build(); + mConnectivityManager.registerNetworkCallback(request, mThreadNetworkCallback, mHandler); + } + + private void unregisterThreadNetworkCallback() { + if (mThreadNetworkCallback == null) { + return; + } + mConnectivityManager.unregisterNetworkCallback(mThreadNetworkCallback); + mThreadNetworkCallback = null; } /** Injects a {@link NetworkAgent} for testing. */ @@ -835,27 +866,46 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub return mTestNetworkAgent; } - final NetworkCapabilities netCaps = + final var netCapsBuilder = new NetworkCapabilities.Builder() - .addTransportType(NetworkCapabilities.TRANSPORT_THREAD) - .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK) + .addTransportType(TRANSPORT_THREAD) .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) - .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED) - .build(); - final NetworkScore score = - new NetworkScore.Builder() - .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK) - .build(); + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED); + final var scoreBuilder = new NetworkScore.Builder(); + + if (isBorderRouterMode()) { + netCapsBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK); + scoreBuilder.setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK); + } + return new NetworkAgent( mContext, mHandler.getLooper(), LOG.getTag(), - netCaps, + netCapsBuilder.build(), getTunIfLinkProperties(), - newLocalNetworkConfig(), - score, + isBorderRouterMode() ? newLocalNetworkConfig() : null, + scoreBuilder.build(), new NetworkAgentConfig.Builder().build(), - mNetworkProvider) {}; + mNetworkProvider) { + + // TODO(b/374037595): use NetworkFactory to handle dynamic network requests + @Override + public void onNetworkUnwanted() { + LOG.i("Thread network is unwanted by ConnectivityService"); + if (!isBorderRouterMode()) { + leave(false /* eraseDataset */, new LoggingOperationReceiver("leave")); + } + } + }; + } + + private LocalNetworkConfig newLocalNetworkConfig() { + return new LocalNetworkConfig.Builder() + .setUpstreamMulticastRoutingConfig(mUpstreamMulticastRoutingConfig) + .setDownstreamMulticastRoutingConfig(mDownstreamMulticastRoutingConfig) + .setUpstreamSelector(mUpstreamNetworkRequest) + .build(); } private void registerThreadNetwork() { @@ -901,6 +951,12 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub long lifetimeMillis, OperationReceiverWrapper receiver) { checkOnHandlerThread(); + if (!isBorderRouterMode()) { + receiver.onError( + ERROR_FAILED_PRECONDITION, "This device is not configured a Border Router"); + return; + } + try { getOtDaemon().activateEphemeralKeyMode(lifetimeMillis, newOtStatusReceiver(receiver)); } catch (RemoteException | ThreadNetworkException e) { @@ -920,6 +976,12 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub private void deactivateEphemeralKeyModeInternal(OperationReceiverWrapper receiver) { checkOnHandlerThread(); + if (!isBorderRouterMode()) { + receiver.onError( + ERROR_FAILED_PRECONDITION, "This device is not configured a Border Router"); + return; + } + try { getOtDaemon().deactivateEphemeralKeyMode(newOtStatusReceiver(receiver)); } catch (RemoteException | ThreadNetworkException e) { @@ -1233,16 +1295,20 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub @Override public void leave(@NonNull IOperationReceiver receiver) { - enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED); - - mHandler.post(() -> leaveInternal(new OperationReceiverWrapper(receiver))); + leave(true /* eraseDataset */, receiver); } - private void leaveInternal(@NonNull OperationReceiverWrapper receiver) { + private void leave(boolean eraseDataset, @NonNull IOperationReceiver receiver) { + enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED); + + mHandler.post(() -> leaveInternal(eraseDataset, new OperationReceiverWrapper(receiver))); + } + + private void leaveInternal(boolean eraseDataset, @NonNull OperationReceiverWrapper receiver) { checkOnHandlerThread(); try { - getOtDaemon().leave(newOtStatusReceiver(receiver)); + getOtDaemon().leave(eraseDataset, newOtStatusReceiver(receiver)); } catch (RemoteException | ThreadNetworkException e) { LOG.e("otDaemon.leave failed", e); receiver.onError(e); @@ -1388,6 +1454,11 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub } } + private void disableBorderRouting() { + LOG.i("Disabling border routing"); + setInfraLinkState(newInfraLinkStateBuilder().build()); + } + private void handleThreadInterfaceStateChanged(boolean isUp) { try { mTunIfController.setInterfaceUp(isUp); @@ -1571,6 +1642,25 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub } } + /** An implementation of {@link IOperationReceiver} that simply logs the operation result. */ + private static class LoggingOperationReceiver extends IOperationReceiver.Stub { + private final String mOperation; + + LoggingOperationReceiver(String operation) { + mOperation = operation; + } + + @Override + public void onSuccess() { + LOG.i("The operation " + mOperation + " succeeded"); + } + + @Override + public void onError(int errorCode, String errorMessage) { + LOG.w("The operation " + mOperation + " failed: " + errorCode + " " + errorMessage); + } + } + private static class LoggingOtStatusReceiver extends IOtStatusReceiver.Stub { private final String mAction; diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java index 5d869dfff4..18ab1ca34f 100644 --- a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java +++ b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java @@ -137,6 +137,8 @@ public final class ThreadNetworkShellCommand extends BasicShellCommandHandler { return setThreadEnabled(true); case "disable": return setThreadEnabled(false); + case "config": + return handleConfigCommand(); case "join": return join(); case "leave": @@ -149,8 +151,6 @@ public final class ThreadNetworkShellCommand extends BasicShellCommandHandler { return forceCountryCode(); case "get-country-code": return getCountryCode(); - case "config": - return handleConfigCommand(); case "ot-ctl": return handleOtCtlCommand(); default: @@ -321,6 +321,7 @@ public final class ThreadNetworkShellCommand extends BasicShellCommandHandler { final ThreadConfiguration.Builder newConfigBuilder = new ThreadConfiguration.Builder(oldConfig); switch (name) { + case "br" -> newConfigBuilder.setBorderRouterEnabled(argEnabledOrDisabled(value)); case "nat64" -> newConfigBuilder.setNat64Enabled(argEnabledOrDisabled(value)); case "pd" -> newConfigBuilder.setDhcpv6PdEnabled(argEnabledOrDisabled(value)); default -> throw new IllegalArgumentException("Invalid config name: " + name); diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java index fc18ef9a63..746b587553 100644 --- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java +++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java @@ -77,6 +77,13 @@ public class ThreadPersistentSettings { /** Stores the Thread country code, null if no country code is stored. */ public static final Key THREAD_COUNTRY_CODE = new Key<>("thread_country_code", null); + /** + * Saves the boolean flag for border router being enabled. The value defaults to {@code true} if + * this config is missing. + */ + private static final Key CONFIG_BORDER_ROUTER_ENABLED = + new Key<>("config_border_router_enabled", true); + /** Stores the Thread NAT64 feature toggle state, true for enabled and false for disabled. */ private static final Key CONFIG_NAT64_ENABLED = new Key<>("config_nat64_enabled", false); @@ -197,6 +204,7 @@ public class ThreadPersistentSettings { if (getConfiguration().equals(configuration)) { return false; } + putObject(CONFIG_BORDER_ROUTER_ENABLED.key, configuration.isBorderRouterEnabled()); putObject(CONFIG_NAT64_ENABLED.key, configuration.isNat64Enabled()); putObject(CONFIG_DHCP6_PD_ENABLED.key, configuration.isDhcpv6PdEnabled()); writeToStoreFile(); @@ -206,6 +214,7 @@ public class ThreadPersistentSettings { /** Retrieve the {@link ThreadConfiguration} from the persistent settings. */ public ThreadConfiguration getConfiguration() { return new ThreadConfiguration.Builder() + .setBorderRouterEnabled(get(CONFIG_BORDER_ROUTER_ENABLED)) .setNat64Enabled(get(CONFIG_NAT64_ENABLED)) .setDhcpv6PdEnabled(get(CONFIG_DHCP6_PD_ENABLED)) .build(); diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java index 386412efab..e2f0e47476 100644 --- a/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java +++ b/thread/tests/cts/src/android/net/thread/cts/ThreadConfigurationTest.java @@ -41,6 +41,7 @@ import java.util.Collection; public final class ThreadConfigurationTest { @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule(); + public final boolean mIsBorderRouterEnabled; public final boolean mIsNat64Enabled; public final boolean mIsDhcpv6PdEnabled; @@ -48,14 +49,16 @@ public final class ThreadConfigurationTest { public static Collection configArguments() { return Arrays.asList( new Object[][] { - {false, false}, // All disabled - {true, false}, // NAT64 enabled - {false, true}, // DHCP6-PD enabled - {true, true}, // All enabled + {false, false, false}, // All disabled + {false, true, false}, // NAT64 enabled + {false, false, true}, // DHCP6-PD enabled + {true, true, true}, // All enabled }); } - public ThreadConfigurationTest(boolean isNat64Enabled, boolean isDhcpv6PdEnabled) { + public ThreadConfigurationTest( + boolean isBorderRouterEnabled, boolean isNat64Enabled, boolean isDhcpv6PdEnabled) { + mIsBorderRouterEnabled = isBorderRouterEnabled; mIsNat64Enabled = isNat64Enabled; mIsDhcpv6PdEnabled = isDhcpv6PdEnabled; } @@ -64,6 +67,7 @@ public final class ThreadConfigurationTest { public void parcelable_parcelingIsLossLess() { ThreadConfiguration config = new ThreadConfiguration.Builder() + .setBorderRouterEnabled(mIsBorderRouterEnabled) .setNat64Enabled(mIsNat64Enabled) .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled) .build(); @@ -74,10 +78,12 @@ public final class ThreadConfigurationTest { public void builder_correctValuesAreSet() { ThreadConfiguration config = new ThreadConfiguration.Builder() + .setBorderRouterEnabled(mIsBorderRouterEnabled) .setNat64Enabled(mIsNat64Enabled) .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled) .build(); + assertThat(config.isBorderRouterEnabled()).isEqualTo(mIsBorderRouterEnabled); assertThat(config.isNat64Enabled()).isEqualTo(mIsNat64Enabled); assertThat(config.isDhcpv6PdEnabled()).isEqualTo(mIsDhcpv6PdEnabled); } @@ -86,6 +92,7 @@ public final class ThreadConfigurationTest { public void builderConstructor_configsAreEqual() { ThreadConfiguration config1 = new ThreadConfiguration.Builder() + .setBorderRouterEnabled(mIsBorderRouterEnabled) .setNat64Enabled(mIsNat64Enabled) .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled) .build(); 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 92227b436f..2d487ca46e 100644 --- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java +++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java @@ -920,6 +920,27 @@ public class ThreadNetworkControllerTest { assertThat(threadException.getErrorCode()).isEqualTo(ERROR_BUSY); } + @Test + @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED}) + public void activateEphemeralKeyMode_notBorderRouter_failsWithFailedPrecondition() + throws Exception { + setConfigurationAndWait( + mController, + new ThreadConfiguration.Builder().setBorderRouterEnabled(false).build()); + grantPermissions(THREAD_NETWORK_PRIVILEGED); + CompletableFuture future = new CompletableFuture<>(); + + mController.activateEphemeralKeyMode( + Duration.ofSeconds(1), mExecutor, newOutcomeReceiver(future)); + + var thrown = + assertThrows( + ExecutionException.class, + () -> future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)); + var threadException = (ThreadNetworkException) thrown.getCause(); + assertThat(threadException.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION); + } + @Test @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED}) public void deactivateEphemeralKeyMode_withoutPrivilegedPermission_throwsSecurityException() @@ -931,6 +952,26 @@ public class ThreadNetworkControllerTest { () -> mController.deactivateEphemeralKeyMode(mExecutor, v -> {})); } + @Test + @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED}) + public void deactivateEphemeralKeyMode_notBorderRouter_failsWithFailedPrecondition() + throws Exception { + setConfigurationAndWait( + mController, + new ThreadConfiguration.Builder().setBorderRouterEnabled(false).build()); + grantPermissions(THREAD_NETWORK_PRIVILEGED); + CompletableFuture future = new CompletableFuture<>(); + + mController.deactivateEphemeralKeyMode(mExecutor, newOutcomeReceiver(future)); + + var thrown = + assertThrows( + ExecutionException.class, + () -> future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)); + var threadException = (ThreadNetworkException) thrown.getCause(); + assertThat(threadException.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION); + } + @Test @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED}) public void subscribeEpskcState_permissionsGranted_returnsCurrentState() throws Exception { @@ -1151,9 +1192,15 @@ public class ThreadNetworkControllerTest { CompletableFuture setFuture2 = new CompletableFuture<>(); ConfigurationListener listener = new ConfigurationListener(mController); ThreadConfiguration config1 = - new ThreadConfiguration.Builder().setNat64Enabled(true).build(); + new ThreadConfiguration.Builder() + .setBorderRouterEnabled(true) + .setNat64Enabled(true) + .build(); ThreadConfiguration config2 = - new ThreadConfiguration.Builder().setNat64Enabled(false).build(); + new ThreadConfiguration.Builder() + .setBorderRouterEnabled(false) + .setNat64Enabled(false) + .build(); try { runAsShell( diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java index 61b6eac02a..56134545ff 100644 --- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java +++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java @@ -16,10 +16,13 @@ package android.net.thread; +import static android.Manifest.permission.ACCESS_NETWORK_STATE; +import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK; import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED; import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER; import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED; import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT; +import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_CONFIG; import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT; import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses; import static android.net.thread.utils.IntegrationTestUtils.getPrefixesFromNetData; @@ -30,17 +33,23 @@ import static android.net.thread.utils.IntegrationTestUtils.waitFor; import static com.android.compatibility.common.util.SystemUtil.runShellCommand; import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow; import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME; +import static com.android.testutils.TestPermissionUtil.runAsShell; import static com.google.common.io.BaseEncoding.base16; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static java.util.concurrent.TimeUnit.SECONDS; + import android.content.Context; import android.net.ConnectivityManager; import android.net.InetAddresses; import android.net.IpPrefix; import android.net.LinkAddress; import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; import android.net.thread.utils.FullThreadDevice; import android.net.thread.utils.OtDaemonController; import android.net.thread.utils.ThreadFeatureCheckerRule; @@ -66,6 +75,7 @@ import java.net.InetAddress; import java.time.Duration; import java.util.Arrays; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -83,6 +93,8 @@ public class ThreadIntegrationTest { // The maximum time for changes to be propagated to netdata. private static final Duration NET_DATA_UPDATE_TIMEOUT = Duration.ofSeconds(1); + private static final Duration NETWORK_CALLBACK_TIMEOUT = Duration.ofSeconds(10); + // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new". private static final byte[] DEFAULT_DATASET_TLVS = base16().decode( @@ -93,6 +105,8 @@ public class ThreadIntegrationTest { + "B9D351B40C0402A0FFF8"); private static final ActiveOperationalDataset DEFAULT_DATASET = ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS); + private static final ThreadConfiguration DEFAULT_CONFIG = + new ThreadConfiguration.Builder().build(); private static final Inet6Address GROUP_ADDR_ALL_ROUTERS = (Inet6Address) InetAddresses.parseNumericAddress("ff02::2"); @@ -126,6 +140,7 @@ public class ThreadIntegrationTest { public void tearDown() throws Exception { mController.setTestNetworkAsUpstreamAndWait(null); mController.leaveAndWait(); + mController.setConfigurationAndWait(DEFAULT_CONFIG); mFtd.destroy(); mExecutor.shutdownNow(); @@ -327,6 +342,44 @@ public class ThreadIntegrationTest { .isFalse(); } + @Test + public void setConfiguration_disableBorderRouter_noBrfunctionsEnabled() throws Exception { + NetworkRequest request = + new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_THREAD) + .build(); + startFtdLeader(mFtd, DEFAULT_DATASET); + + mController.setConfigurationAndWait( + new ThreadConfiguration.Builder().setBorderRouterEnabled(false).build()); + mController.joinAndWait(DEFAULT_DATASET); + NetworkCapabilities caps = registerNetworkCallbackAndWait(request); + + assertThat(caps.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)).isFalse(); + assertThat(mOtCtl.getBorderRoutingState()).ignoringCase().isEqualTo("disabled"); + assertThat(mOtCtl.getSrpServerState()).ignoringCase().isNotEqualTo("disabled"); + // TODO: b/376217403 - enables / disables Border Agent at runtime + } + + private NetworkCapabilities registerNetworkCallbackAndWait(NetworkRequest request) + throws Exception { + CompletableFuture networkFuture = new CompletableFuture<>(); + ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class); + ConnectivityManager.NetworkCallback callback = + new ConnectivityManager.NetworkCallback() { + @Override + public void onAvailable(Network network) { + networkFuture.complete(network); + } + }; + + runAsShell(ACCESS_NETWORK_STATE, () -> cm.registerNetworkCallback(request, callback)); + + assertThat(networkFuture.get(NETWORK_CALLBACK_TIMEOUT.getSeconds(), SECONDS)).isNotNull(); + return runAsShell( + ACCESS_NETWORK_STATE, () -> cm.getNetworkCapabilities(networkFuture.get())); + } + // TODO (b/323300829): add more tests for integration with linux platform and // ConnectivityService @@ -341,6 +394,14 @@ public class ThreadIntegrationTest { ftd.waitForStateAnyOf(List.of("router", "child"), Duration.ofSeconds(8)); } + /** Starts a Thread FTD device as a leader. */ + private void startFtdLeader(FullThreadDevice ftd, ActiveOperationalDataset activeDataset) + throws Exception { + ftd.factoryReset(); + ftd.joinNetwork(activeDataset); + ftd.waitForStateAnyOf(List.of("leader"), Duration.ofSeconds(8)); + } + /** * Starts a UDP echo server and replies to the first UDP message. * diff --git a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java index 32e3b9565c..2f0ab34adf 100644 --- a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java +++ b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java @@ -189,7 +189,7 @@ public class ThreadNetworkShellCommandTest { final String result = runThreadCommand("config"); - assertThat(result).contains("Nat64Enabled=true"); + assertThat(result).contains("nat64Enabled=true"); } @Test diff --git a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java index 046d9bf577..afb0fc798d 100644 --- a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java +++ b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java @@ -54,6 +54,16 @@ public final class OtDaemonController { SystemClock.sleep(500); } + /** Returns the output string of the "ot-ctl br state" command. */ + public String getBorderRoutingState() { + return executeCommandAndParse("br state").getFirst(); + } + + /** Returns the output string of the "ot-ctl srp server state" command. */ + public String getSrpServerState() { + return executeCommandAndParse("srp server state").getFirst(); + } + /** Returns the list of IPv6 addresses on ot-daemon. */ public List getAddresses() { return executeCommandAndParse("ipaddr").stream() diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java index c0e99d712d..640b0f1334 100644 --- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java +++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java @@ -359,7 +359,7 @@ public class ThreadNetworkShellCommandTest { runShellCommand("config"); verify(mErrorWriter, never()).println(); - verify(mOutputWriter, times(1)).println(contains("Nat64Enabled=true")); + verify(mOutputWriter, times(1)).println(contains("nat64Enabled=true")); } @Test