[Thread] add private API for disabling border router
Bug: 374036806
Change-Id: Ib8ca8bbd2b3018c39c9250e1b3ce70b193273c1b
diff --git a/common/thread_flags.aconfig b/common/thread_flags.aconfig
index 60120bc..8cc2bb4 100644
--- a/common/thread_flags.aconfig
+++ b/common/thread_flags.aconfig
@@ -44,3 +44,12 @@
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 edb5021..0829265 100644
--- a/thread/framework/java/android/net/thread/ThreadConfiguration.java
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.java
@@ -44,18 +44,38 @@
@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.
+ *
+ * <p>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.
+ *
+ * <p>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 @@
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 @@
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeBoolean(mBorderRouterEnabled);
dest.writeBoolean(mNat64Enabled);
dest.writeBoolean(mDhcpv6PdEnabled);
}
@@ -114,6 +137,7 @@
@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 @@
@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,11 +184,26 @@
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.
+ *
+ * <p>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.
*
* <p>Enabling this feature will allow Thread devices to connect to the internet/cloud over
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index ba4eeaf..e3c2a28 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.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 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.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 @@
private NetworkRequest mUpstreamNetworkRequest;
private UpstreamNetworkCallback mUpstreamNetworkCallback;
private TestNetworkSpecifier mUpstreamTestNetworkSpecifier;
+ private ThreadNetworkCallback mThreadNetworkCallback;
private final Map<Network, LinkProperties> mNetworkToLinkProperties;
private final ThreadPersistentSettings mPersistentSettings;
private final UserManager mUserManager;
@@ -315,14 +316,6 @@
.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 @@
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 @@
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 @@
}
}
}
+
try {
getOtDaemon()
.setConfiguration(
@@ -612,11 +625,17 @@
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 @@
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 @@
private void cancelRequestUpstreamNetwork() {
if (mUpstreamNetworkCallback == null) {
- throw new AssertionError("The upstream network request null.");
+ return;
}
mNetworkToLinkProperties.clear();
mConnectivityManager.unregisterNetworkCallback(mUpstreamNetworkCallback);
@@ -812,16 +831,28 @@
}
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 @@
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 @@
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 @@
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 @@
@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 @@
}
}
+ private void disableBorderRouting() {
+ LOG.i("Disabling border routing");
+ setInfraLinkState(newInfraLinkStateBuilder().build());
+ }
+
private void handleThreadInterfaceStateChanged(boolean isUp) {
try {
mTunIfController.setInterfaceUp(isUp);
@@ -1571,6 +1642,25 @@
}
}
+ /** 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 5d869df..18ab1ca 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 @@
return setThreadEnabled(true);
case "disable":
return setThreadEnabled(false);
+ case "config":
+ return handleConfigCommand();
case "join":
return join();
case "leave":
@@ -149,8 +151,6 @@
return forceCountryCode();
case "get-country-code":
return getCountryCode();
- case "config":
- return handleConfigCommand();
case "ot-ctl":
return handleOtCtlCommand();
default:
@@ -321,6 +321,7 @@
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 fc18ef9..746b587 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 @@
/** Stores the Thread country code, null if no country code is stored. */
public static final Key<String> 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<Boolean> 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<Boolean> CONFIG_NAT64_ENABLED =
new Key<>("config_nat64_enabled", false);
@@ -197,6 +204,7 @@
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 @@
/** 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 386412e..e2f0e47 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 @@
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 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 void parcelable_parcelingIsLossLess() {
ThreadConfiguration config =
new ThreadConfiguration.Builder()
+ .setBorderRouterEnabled(mIsBorderRouterEnabled)
.setNat64Enabled(mIsNat64Enabled)
.setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
.build();
@@ -74,10 +78,12 @@
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 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 92227b4..2d487ca 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -922,6 +922,27 @@
@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<Void> 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()
throws Exception {
dropAllPermissions();
@@ -933,6 +954,26 @@
@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<Void> 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 {
CompletableFuture<Integer> stateFuture = new CompletableFuture<>();
CompletableFuture<String> ephemeralKeyFuture = new CompletableFuture<>();
@@ -1151,9 +1192,15 @@
CompletableFuture<Void> 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 61b6eac..5613454 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 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.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 @@
// 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 @@
+ "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 void tearDown() throws Exception {
mController.setTestNetworkAsUpstreamAndWait(null);
mController.leaveAndWait();
+ mController.setConfigurationAndWait(DEFAULT_CONFIG);
mFtd.destroy();
mExecutor.shutdownNow();
@@ -327,6 +342,44 @@
.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<Network> 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 @@
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 32e3b95..2f0ab34 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 @@
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 046d9bf..afb0fc7 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 @@
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<Inet6Address> 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 c0e99d7..640b0f1 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 @@
runShellCommand("config");
verify(mErrorWriter, never()).println();
- verify(mOutputWriter, times(1)).println(contains("Nat64Enabled=true"));
+ verify(mOutputWriter, times(1)).println(contains("nat64Enabled=true"));
}
@Test