[Thread] add private API for disabling border router

Bug: 374036806
Change-Id: Ib8ca8bbd2b3018c39c9250e1b3ce70b193273c1b
This commit is contained in:
Kangping Dong 2024-10-17 00:22:21 +08:00
commit 56449db1be
11 changed files with 329 additions and 52 deletions

View file

@ -44,3 +44,12 @@ flag {
description: "Controls whether the setConfiguration API of NAT64 feature is enabled" description: "Controls whether the setConfiguration API of NAT64 feature is enabled"
bug: "368456504" 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"
}

View file

@ -44,18 +44,38 @@ import java.util.Objects;
@FlaggedApi(Flags.FLAG_CONFIGURATION_ENABLED) @FlaggedApi(Flags.FLAG_CONFIGURATION_ENABLED)
@SystemApi @SystemApi
public final class ThreadConfiguration implements Parcelable { public final class ThreadConfiguration implements Parcelable {
private final boolean mBorderRouterEnabled;
private final boolean mNat64Enabled; private final boolean mNat64Enabled;
private final boolean mDhcpv6PdEnabled; private final boolean mDhcpv6PdEnabled;
private ThreadConfiguration(Builder builder) { 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.mNat64Enabled = nat64Enabled;
this.mDhcpv6PdEnabled = dhcpv6PdEnabled; 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. */ /** Returns {@code true} if NAT64 is enabled. */
public boolean isNat64Enabled() { public boolean isNat64Enabled() {
return mNat64Enabled; return mNat64Enabled;
@ -78,22 +98,24 @@ public final class ThreadConfiguration implements Parcelable {
return false; return false;
} else { } else {
ThreadConfiguration otherConfig = (ThreadConfiguration) other; ThreadConfiguration otherConfig = (ThreadConfiguration) other;
return mNat64Enabled == otherConfig.mNat64Enabled return mBorderRouterEnabled == otherConfig.mBorderRouterEnabled
&& mNat64Enabled == otherConfig.mNat64Enabled
&& mDhcpv6PdEnabled == otherConfig.mDhcpv6PdEnabled; && mDhcpv6PdEnabled == otherConfig.mDhcpv6PdEnabled;
} }
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(mNat64Enabled, mDhcpv6PdEnabled); return Objects.hash(mBorderRouterEnabled, mNat64Enabled, mDhcpv6PdEnabled);
} }
@Override @Override
public String toString() { public String toString() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append('{'); sb.append('{');
sb.append("Nat64Enabled=").append(mNat64Enabled); sb.append("borderRouterEnabled=").append(mBorderRouterEnabled);
sb.append(", Dhcpv6PdEnabled=").append(mDhcpv6PdEnabled); sb.append(", nat64Enabled=").append(mNat64Enabled);
sb.append(", dhcpv6PdEnabled=").append(mDhcpv6PdEnabled);
sb.append('}'); sb.append('}');
return sb.toString(); return sb.toString();
} }
@ -105,6 +127,7 @@ public final class ThreadConfiguration implements Parcelable {
@Override @Override
public void writeToParcel(@NonNull Parcel dest, int flags) { public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeBoolean(mBorderRouterEnabled);
dest.writeBoolean(mNat64Enabled); dest.writeBoolean(mNat64Enabled);
dest.writeBoolean(mDhcpv6PdEnabled); dest.writeBoolean(mDhcpv6PdEnabled);
} }
@ -114,6 +137,7 @@ public final class ThreadConfiguration implements Parcelable {
@Override @Override
public ThreadConfiguration createFromParcel(Parcel in) { public ThreadConfiguration createFromParcel(Parcel in) {
ThreadConfiguration.Builder builder = new ThreadConfiguration.Builder(); ThreadConfiguration.Builder builder = new ThreadConfiguration.Builder();
builder.setBorderRouterEnabled(in.readBoolean());
builder.setNat64Enabled(in.readBoolean()); builder.setNat64Enabled(in.readBoolean());
builder.setDhcpv6PdEnabled(in.readBoolean()); builder.setDhcpv6PdEnabled(in.readBoolean());
return builder.build(); return builder.build();
@ -133,6 +157,10 @@ public final class ThreadConfiguration implements Parcelable {
@FlaggedApi(Flags.FLAG_SET_NAT64_CONFIGURATION_ENABLED) @FlaggedApi(Flags.FLAG_SET_NAT64_CONFIGURATION_ENABLED)
@SystemApi @SystemApi
public static final class Builder { 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 mNat64Enabled = false;
private boolean mDhcpv6PdEnabled = false; private boolean mDhcpv6PdEnabled = false;
@ -156,10 +184,25 @@ public final class ThreadConfiguration implements Parcelable {
public Builder(@NonNull ThreadConfiguration config) { public Builder(@NonNull ThreadConfiguration config) {
Objects.requireNonNull(config); Objects.requireNonNull(config);
mBorderRouterEnabled = config.mBorderRouterEnabled;
mNat64Enabled = config.mNat64Enabled; mNat64Enabled = config.mNat64Enabled;
mDhcpv6PdEnabled = config.mDhcpv6PdEnabled; 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. * Enables or disables NAT64 for the device.
* *

View file

@ -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.CONFIG_FORWARD_NONE;
import static android.net.MulticastRoutingConfig.FORWARD_SELECTED; import static android.net.MulticastRoutingConfig.FORWARD_SELECTED;
import static android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE; 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.CHANNEL_PAGE_24_GHZ;
import static android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID; import static android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID;
import static android.net.thread.ActiveOperationalDataset.LENGTH_MESH_LOCAL_PREFIX_BITS; 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.connectivity.resources.R;
import com.android.internal.annotations.VisibleForTesting; 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.IIpv4PrefixRequest;
import com.android.net.module.util.RoutingCoordinatorManager;
import com.android.net.module.util.SharedLog; import com.android.net.module.util.SharedLog;
import com.android.server.ServiceManagerWrapper; import com.android.server.ServiceManagerWrapper;
import com.android.server.connectivity.ConnectivityResources; import com.android.server.connectivity.ConnectivityResources;
@ -151,7 +152,6 @@ import java.time.Clock;
import java.time.DateTimeException; import java.time.DateTimeException;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -222,6 +222,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
private NetworkRequest mUpstreamNetworkRequest; private NetworkRequest mUpstreamNetworkRequest;
private UpstreamNetworkCallback mUpstreamNetworkCallback; private UpstreamNetworkCallback mUpstreamNetworkCallback;
private TestNetworkSpecifier mUpstreamTestNetworkSpecifier; private TestNetworkSpecifier mUpstreamTestNetworkSpecifier;
private ThreadNetworkCallback mThreadNetworkCallback;
private final Map<Network, LinkProperties> mNetworkToLinkProperties; private final Map<Network, LinkProperties> mNetworkToLinkProperties;
private final ThreadPersistentSettings mPersistentSettings; private final ThreadPersistentSettings mPersistentSettings;
private final UserManager mUserManager; private final UserManager mUserManager;
@ -315,14 +316,6 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
.build(); .build();
} }
private LocalNetworkConfig newLocalNetworkConfig() {
return new LocalNetworkConfig.Builder()
.setUpstreamMulticastRoutingConfig(mUpstreamMulticastRoutingConfig)
.setDownstreamMulticastRoutingConfig(mDownstreamMulticastRoutingConfig)
.setUpstreamSelector(mUpstreamNetworkRequest)
.build();
}
private void maybeInitializeOtDaemon() { private void maybeInitializeOtDaemon() {
if (!shouldEnableThread()) { if (!shouldEnableThread()) {
return; return;
@ -476,10 +469,16 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
throw new IllegalStateException("Failed to create Thread tunnel interface", e); throw new IllegalStateException("Failed to create Thread tunnel interface", e);
} }
mConnectivityManager.registerNetworkProvider(mNetworkProvider); mConnectivityManager.registerNetworkProvider(mNetworkProvider);
requestUpstreamNetwork();
registerThreadNetworkCallback();
mUserRestricted = isThreadUserRestricted(); mUserRestricted = isThreadUserRestricted();
registerUserRestrictionsReceiver(); registerUserRestrictionsReceiver();
if (isBorderRouterMode()) {
requestUpstreamNetwork();
registerThreadNetworkCallback();
} else {
cancelRequestUpstreamNetwork();
unregisterThreadNetworkCallback();
}
maybeInitializeOtDaemon(); maybeInitializeOtDaemon();
} }
@ -588,7 +587,20 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
LOG.i("Set Thread configuration: " + configuration); LOG.i("Set Thread configuration: " + configuration);
final boolean changed = mPersistentSettings.putConfiguration(configuration); final boolean changed = mPersistentSettings.putConfiguration(configuration);
if (changed) {
if (isBorderRouterMode()) {
requestUpstreamNetwork();
registerThreadNetworkCallback();
} else {
cancelRequestUpstreamNetwork();
unregisterThreadNetworkCallback();
disableBorderRouting();
}
}
receiver.onSuccess(); receiver.onSuccess();
if (changed) { if (changed) {
for (IConfigurationReceiver configReceiver : mConfigurationReceivers.keySet()) { for (IConfigurationReceiver configReceiver : mConfigurationReceivers.keySet()) {
try { try {
@ -598,6 +610,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
} }
} }
} }
try { try {
getOtDaemon() getOtDaemon()
.setConfiguration( .setConfiguration(
@ -612,11 +625,17 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
private static OtDaemonConfiguration newOtDaemonConfig( private static OtDaemonConfiguration newOtDaemonConfig(
@NonNull ThreadConfiguration threadConfig) { @NonNull ThreadConfiguration threadConfig) {
return new OtDaemonConfiguration.Builder() return new OtDaemonConfiguration.Builder()
.setBorderRouterEnabled(threadConfig.isBorderRouterEnabled())
.setNat64Enabled(threadConfig.isNat64Enabled()) .setNat64Enabled(threadConfig.isNat64Enabled())
.setDhcpv6PdEnabled(threadConfig.isDhcpv6PdEnabled()) .setDhcpv6PdEnabled(threadConfig.isDhcpv6PdEnabled())
.build(); .build();
} }
/** Returns {@code true} if this device is operating as a border router. */
private boolean isBorderRouterMode() {
return mPersistentSettings.getConfiguration().isBorderRouterEnabled();
}
@Override @Override
public void registerConfigurationCallback(@NonNull IConfigurationReceiver callback) { public void registerConfigurationCallback(@NonNull IConfigurationReceiver callback) {
enforceAllPermissionsGranted(permission.THREAD_NETWORK_PRIVILEGED); enforceAllPermissionsGranted(permission.THREAD_NETWORK_PRIVILEGED);
@ -723,7 +742,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
private void requestUpstreamNetwork() { private void requestUpstreamNetwork() {
if (mUpstreamNetworkCallback != null) { if (mUpstreamNetworkCallback != null) {
throw new AssertionError("The upstream network request is already there."); return;
} }
mUpstreamNetworkCallback = new UpstreamNetworkCallback(); mUpstreamNetworkCallback = new UpstreamNetworkCallback();
mConnectivityManager.registerNetworkCallback( mConnectivityManager.registerNetworkCallback(
@ -732,7 +751,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
private void cancelRequestUpstreamNetwork() { private void cancelRequestUpstreamNetwork() {
if (mUpstreamNetworkCallback == null) { if (mUpstreamNetworkCallback == null) {
throw new AssertionError("The upstream network request null."); return;
} }
mNetworkToLinkProperties.clear(); mNetworkToLinkProperties.clear();
mConnectivityManager.unregisterNetworkCallback(mUpstreamNetworkCallback); mConnectivityManager.unregisterNetworkCallback(mUpstreamNetworkCallback);
@ -812,16 +831,28 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
} }
private void registerThreadNetworkCallback() { private void registerThreadNetworkCallback() {
mConnectivityManager.registerNetworkCallback( if (mThreadNetworkCallback != null) {
return;
}
mThreadNetworkCallback = new ThreadNetworkCallback();
NetworkRequest request =
new NetworkRequest.Builder() new NetworkRequest.Builder()
// clearCapabilities() is needed to remove forbidden capabilities and UID // clearCapabilities() is needed to remove forbidden capabilities and UID
// requirement. // requirement.
.clearCapabilities() .clearCapabilities()
.addTransportType(NetworkCapabilities.TRANSPORT_THREAD) .addTransportType(TRANSPORT_THREAD)
.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK) .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
.build(), .build();
new ThreadNetworkCallback(), mConnectivityManager.registerNetworkCallback(request, mThreadNetworkCallback, mHandler);
mHandler); }
private void unregisterThreadNetworkCallback() {
if (mThreadNetworkCallback == null) {
return;
}
mConnectivityManager.unregisterNetworkCallback(mThreadNetworkCallback);
mThreadNetworkCallback = null;
} }
/** Injects a {@link NetworkAgent} for testing. */ /** Injects a {@link NetworkAgent} for testing. */
@ -835,27 +866,46 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
return mTestNetworkAgent; return mTestNetworkAgent;
} }
final NetworkCapabilities netCaps = final var netCapsBuilder =
new NetworkCapabilities.Builder() new NetworkCapabilities.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_THREAD) .addTransportType(TRANSPORT_THREAD)
.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED) .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
.build(); final var scoreBuilder = new NetworkScore.Builder();
final NetworkScore score =
new NetworkScore.Builder() if (isBorderRouterMode()) {
.setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK) netCapsBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK);
.build(); scoreBuilder.setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK);
}
return new NetworkAgent( return new NetworkAgent(
mContext, mContext,
mHandler.getLooper(), mHandler.getLooper(),
LOG.getTag(), LOG.getTag(),
netCaps, netCapsBuilder.build(),
getTunIfLinkProperties(), getTunIfLinkProperties(),
newLocalNetworkConfig(), isBorderRouterMode() ? newLocalNetworkConfig() : null,
score, scoreBuilder.build(),
new NetworkAgentConfig.Builder().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() { private void registerThreadNetwork() {
@ -901,6 +951,12 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
long lifetimeMillis, OperationReceiverWrapper receiver) { long lifetimeMillis, OperationReceiverWrapper receiver) {
checkOnHandlerThread(); checkOnHandlerThread();
if (!isBorderRouterMode()) {
receiver.onError(
ERROR_FAILED_PRECONDITION, "This device is not configured a Border Router");
return;
}
try { try {
getOtDaemon().activateEphemeralKeyMode(lifetimeMillis, newOtStatusReceiver(receiver)); getOtDaemon().activateEphemeralKeyMode(lifetimeMillis, newOtStatusReceiver(receiver));
} catch (RemoteException | ThreadNetworkException e) { } catch (RemoteException | ThreadNetworkException e) {
@ -920,6 +976,12 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
private void deactivateEphemeralKeyModeInternal(OperationReceiverWrapper receiver) { private void deactivateEphemeralKeyModeInternal(OperationReceiverWrapper receiver) {
checkOnHandlerThread(); checkOnHandlerThread();
if (!isBorderRouterMode()) {
receiver.onError(
ERROR_FAILED_PRECONDITION, "This device is not configured a Border Router");
return;
}
try { try {
getOtDaemon().deactivateEphemeralKeyMode(newOtStatusReceiver(receiver)); getOtDaemon().deactivateEphemeralKeyMode(newOtStatusReceiver(receiver));
} catch (RemoteException | ThreadNetworkException e) { } catch (RemoteException | ThreadNetworkException e) {
@ -1233,16 +1295,20 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
@Override @Override
public void leave(@NonNull IOperationReceiver receiver) { public void leave(@NonNull IOperationReceiver receiver) {
enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED); leave(true /* eraseDataset */, receiver);
mHandler.post(() -> leaveInternal(new OperationReceiverWrapper(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(); checkOnHandlerThread();
try { try {
getOtDaemon().leave(newOtStatusReceiver(receiver)); getOtDaemon().leave(eraseDataset, newOtStatusReceiver(receiver));
} catch (RemoteException | ThreadNetworkException e) { } catch (RemoteException | ThreadNetworkException e) {
LOG.e("otDaemon.leave failed", e); LOG.e("otDaemon.leave failed", e);
receiver.onError(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) { private void handleThreadInterfaceStateChanged(boolean isUp) {
try { try {
mTunIfController.setInterfaceUp(isUp); 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 static class LoggingOtStatusReceiver extends IOtStatusReceiver.Stub {
private final String mAction; private final String mAction;

View file

@ -137,6 +137,8 @@ public final class ThreadNetworkShellCommand extends BasicShellCommandHandler {
return setThreadEnabled(true); return setThreadEnabled(true);
case "disable": case "disable":
return setThreadEnabled(false); return setThreadEnabled(false);
case "config":
return handleConfigCommand();
case "join": case "join":
return join(); return join();
case "leave": case "leave":
@ -149,8 +151,6 @@ public final class ThreadNetworkShellCommand extends BasicShellCommandHandler {
return forceCountryCode(); return forceCountryCode();
case "get-country-code": case "get-country-code":
return getCountryCode(); return getCountryCode();
case "config":
return handleConfigCommand();
case "ot-ctl": case "ot-ctl":
return handleOtCtlCommand(); return handleOtCtlCommand();
default: default:
@ -321,6 +321,7 @@ public final class ThreadNetworkShellCommand extends BasicShellCommandHandler {
final ThreadConfiguration.Builder newConfigBuilder = final ThreadConfiguration.Builder newConfigBuilder =
new ThreadConfiguration.Builder(oldConfig); new ThreadConfiguration.Builder(oldConfig);
switch (name) { switch (name) {
case "br" -> newConfigBuilder.setBorderRouterEnabled(argEnabledOrDisabled(value));
case "nat64" -> newConfigBuilder.setNat64Enabled(argEnabledOrDisabled(value)); case "nat64" -> newConfigBuilder.setNat64Enabled(argEnabledOrDisabled(value));
case "pd" -> newConfigBuilder.setDhcpv6PdEnabled(argEnabledOrDisabled(value)); case "pd" -> newConfigBuilder.setDhcpv6PdEnabled(argEnabledOrDisabled(value));
default -> throw new IllegalArgumentException("Invalid config name: " + name); default -> throw new IllegalArgumentException("Invalid config name: " + name);

View file

@ -77,6 +77,13 @@ public class ThreadPersistentSettings {
/** Stores the Thread country code, null if no country code is stored. */ /** 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); 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. */ /** Stores the Thread NAT64 feature toggle state, true for enabled and false for disabled. */
private static final Key<Boolean> CONFIG_NAT64_ENABLED = private static final Key<Boolean> CONFIG_NAT64_ENABLED =
new Key<>("config_nat64_enabled", false); new Key<>("config_nat64_enabled", false);
@ -197,6 +204,7 @@ public class ThreadPersistentSettings {
if (getConfiguration().equals(configuration)) { if (getConfiguration().equals(configuration)) {
return false; return false;
} }
putObject(CONFIG_BORDER_ROUTER_ENABLED.key, configuration.isBorderRouterEnabled());
putObject(CONFIG_NAT64_ENABLED.key, configuration.isNat64Enabled()); putObject(CONFIG_NAT64_ENABLED.key, configuration.isNat64Enabled());
putObject(CONFIG_DHCP6_PD_ENABLED.key, configuration.isDhcpv6PdEnabled()); putObject(CONFIG_DHCP6_PD_ENABLED.key, configuration.isDhcpv6PdEnabled());
writeToStoreFile(); writeToStoreFile();
@ -206,6 +214,7 @@ public class ThreadPersistentSettings {
/** Retrieve the {@link ThreadConfiguration} from the persistent settings. */ /** Retrieve the {@link ThreadConfiguration} from the persistent settings. */
public ThreadConfiguration getConfiguration() { public ThreadConfiguration getConfiguration() {
return new ThreadConfiguration.Builder() return new ThreadConfiguration.Builder()
.setBorderRouterEnabled(get(CONFIG_BORDER_ROUTER_ENABLED))
.setNat64Enabled(get(CONFIG_NAT64_ENABLED)) .setNat64Enabled(get(CONFIG_NAT64_ENABLED))
.setDhcpv6PdEnabled(get(CONFIG_DHCP6_PD_ENABLED)) .setDhcpv6PdEnabled(get(CONFIG_DHCP6_PD_ENABLED))
.build(); .build();

View file

@ -41,6 +41,7 @@ import java.util.Collection;
public final class ThreadConfigurationTest { public final class ThreadConfigurationTest {
@Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule(); @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
public final boolean mIsBorderRouterEnabled;
public final boolean mIsNat64Enabled; public final boolean mIsNat64Enabled;
public final boolean mIsDhcpv6PdEnabled; public final boolean mIsDhcpv6PdEnabled;
@ -48,14 +49,16 @@ public final class ThreadConfigurationTest {
public static Collection configArguments() { public static Collection configArguments() {
return Arrays.asList( return Arrays.asList(
new Object[][] { new Object[][] {
{false, false}, // All disabled {false, false, false}, // All disabled
{true, false}, // NAT64 enabled {false, true, false}, // NAT64 enabled
{false, true}, // DHCP6-PD enabled {false, false, true}, // DHCP6-PD enabled
{true, true}, // All enabled {true, true, true}, // All enabled
}); });
} }
public ThreadConfigurationTest(boolean isNat64Enabled, boolean isDhcpv6PdEnabled) { public ThreadConfigurationTest(
boolean isBorderRouterEnabled, boolean isNat64Enabled, boolean isDhcpv6PdEnabled) {
mIsBorderRouterEnabled = isBorderRouterEnabled;
mIsNat64Enabled = isNat64Enabled; mIsNat64Enabled = isNat64Enabled;
mIsDhcpv6PdEnabled = isDhcpv6PdEnabled; mIsDhcpv6PdEnabled = isDhcpv6PdEnabled;
} }
@ -64,6 +67,7 @@ public final class ThreadConfigurationTest {
public void parcelable_parcelingIsLossLess() { public void parcelable_parcelingIsLossLess() {
ThreadConfiguration config = ThreadConfiguration config =
new ThreadConfiguration.Builder() new ThreadConfiguration.Builder()
.setBorderRouterEnabled(mIsBorderRouterEnabled)
.setNat64Enabled(mIsNat64Enabled) .setNat64Enabled(mIsNat64Enabled)
.setDhcpv6PdEnabled(mIsDhcpv6PdEnabled) .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
.build(); .build();
@ -74,10 +78,12 @@ public final class ThreadConfigurationTest {
public void builder_correctValuesAreSet() { public void builder_correctValuesAreSet() {
ThreadConfiguration config = ThreadConfiguration config =
new ThreadConfiguration.Builder() new ThreadConfiguration.Builder()
.setBorderRouterEnabled(mIsBorderRouterEnabled)
.setNat64Enabled(mIsNat64Enabled) .setNat64Enabled(mIsNat64Enabled)
.setDhcpv6PdEnabled(mIsDhcpv6PdEnabled) .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
.build(); .build();
assertThat(config.isBorderRouterEnabled()).isEqualTo(mIsBorderRouterEnabled);
assertThat(config.isNat64Enabled()).isEqualTo(mIsNat64Enabled); assertThat(config.isNat64Enabled()).isEqualTo(mIsNat64Enabled);
assertThat(config.isDhcpv6PdEnabled()).isEqualTo(mIsDhcpv6PdEnabled); assertThat(config.isDhcpv6PdEnabled()).isEqualTo(mIsDhcpv6PdEnabled);
} }
@ -86,6 +92,7 @@ public final class ThreadConfigurationTest {
public void builderConstructor_configsAreEqual() { public void builderConstructor_configsAreEqual() {
ThreadConfiguration config1 = ThreadConfiguration config1 =
new ThreadConfiguration.Builder() new ThreadConfiguration.Builder()
.setBorderRouterEnabled(mIsBorderRouterEnabled)
.setNat64Enabled(mIsNat64Enabled) .setNat64Enabled(mIsNat64Enabled)
.setDhcpv6PdEnabled(mIsDhcpv6PdEnabled) .setDhcpv6PdEnabled(mIsDhcpv6PdEnabled)
.build(); .build();

View file

@ -920,6 +920,27 @@ public class ThreadNetworkControllerTest {
assertThat(threadException.getErrorCode()).isEqualTo(ERROR_BUSY); 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<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 @Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED}) @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void deactivateEphemeralKeyMode_withoutPrivilegedPermission_throwsSecurityException() public void deactivateEphemeralKeyMode_withoutPrivilegedPermission_throwsSecurityException()
@ -931,6 +952,26 @@ public class ThreadNetworkControllerTest {
() -> mController.deactivateEphemeralKeyMode(mExecutor, v -> {})); () -> 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<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 @Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED}) @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void subscribeEpskcState_permissionsGranted_returnsCurrentState() throws Exception { public void subscribeEpskcState_permissionsGranted_returnsCurrentState() throws Exception {
@ -1151,9 +1192,15 @@ public class ThreadNetworkControllerTest {
CompletableFuture<Void> setFuture2 = new CompletableFuture<>(); CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
ConfigurationListener listener = new ConfigurationListener(mController); ConfigurationListener listener = new ConfigurationListener(mController);
ThreadConfiguration config1 = ThreadConfiguration config1 =
new ThreadConfiguration.Builder().setNat64Enabled(true).build(); new ThreadConfiguration.Builder()
.setBorderRouterEnabled(true)
.setNat64Enabled(true)
.build();
ThreadConfiguration config2 = ThreadConfiguration config2 =
new ThreadConfiguration.Builder().setNat64Enabled(false).build(); new ThreadConfiguration.Builder()
.setBorderRouterEnabled(false)
.setNat64Enabled(false)
.build();
try { try {
runAsShell( runAsShell(

View file

@ -16,10 +16,13 @@
package android.net.thread; 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_DETACHED;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER; import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED; 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.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.RESTART_JOIN_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses; import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
import static android.net.thread.utils.IntegrationTestUtils.getPrefixesFromNetData; 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.runShellCommand;
import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow; import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME; 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.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage; import static com.google.common.truth.Truth.assertWithMessage;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.content.Context; import android.content.Context;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.InetAddresses; import android.net.InetAddresses;
import android.net.IpPrefix; import android.net.IpPrefix;
import android.net.LinkAddress; import android.net.LinkAddress;
import android.net.LinkProperties; 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.FullThreadDevice;
import android.net.thread.utils.OtDaemonController; import android.net.thread.utils.OtDaemonController;
import android.net.thread.utils.ThreadFeatureCheckerRule; import android.net.thread.utils.ThreadFeatureCheckerRule;
@ -66,6 +75,7 @@ import java.net.InetAddress;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -83,6 +93,8 @@ public class ThreadIntegrationTest {
// The maximum time for changes to be propagated to netdata. // 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 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". // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
private static final byte[] DEFAULT_DATASET_TLVS = private static final byte[] DEFAULT_DATASET_TLVS =
base16().decode( base16().decode(
@ -93,6 +105,8 @@ public class ThreadIntegrationTest {
+ "B9D351B40C0402A0FFF8"); + "B9D351B40C0402A0FFF8");
private static final ActiveOperationalDataset DEFAULT_DATASET = private static final ActiveOperationalDataset DEFAULT_DATASET =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS); ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
private static final ThreadConfiguration DEFAULT_CONFIG =
new ThreadConfiguration.Builder().build();
private static final Inet6Address GROUP_ADDR_ALL_ROUTERS = private static final Inet6Address GROUP_ADDR_ALL_ROUTERS =
(Inet6Address) InetAddresses.parseNumericAddress("ff02::2"); (Inet6Address) InetAddresses.parseNumericAddress("ff02::2");
@ -126,6 +140,7 @@ public class ThreadIntegrationTest {
public void tearDown() throws Exception { public void tearDown() throws Exception {
mController.setTestNetworkAsUpstreamAndWait(null); mController.setTestNetworkAsUpstreamAndWait(null);
mController.leaveAndWait(); mController.leaveAndWait();
mController.setConfigurationAndWait(DEFAULT_CONFIG);
mFtd.destroy(); mFtd.destroy();
mExecutor.shutdownNow(); mExecutor.shutdownNow();
@ -327,6 +342,44 @@ public class ThreadIntegrationTest {
.isFalse(); .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 // TODO (b/323300829): add more tests for integration with linux platform and
// ConnectivityService // ConnectivityService
@ -341,6 +394,14 @@ public class ThreadIntegrationTest {
ftd.waitForStateAnyOf(List.of("router", "child"), Duration.ofSeconds(8)); 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. * Starts a UDP echo server and replies to the first UDP message.
* *

View file

@ -189,7 +189,7 @@ public class ThreadNetworkShellCommandTest {
final String result = runThreadCommand("config"); final String result = runThreadCommand("config");
assertThat(result).contains("Nat64Enabled=true"); assertThat(result).contains("nat64Enabled=true");
} }
@Test @Test

View file

@ -54,6 +54,16 @@ public final class OtDaemonController {
SystemClock.sleep(500); 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. */ /** Returns the list of IPv6 addresses on ot-daemon. */
public List<Inet6Address> getAddresses() { public List<Inet6Address> getAddresses() {
return executeCommandAndParse("ipaddr").stream() return executeCommandAndParse("ipaddr").stream()

View file

@ -359,7 +359,7 @@ public class ThreadNetworkShellCommandTest {
runShellCommand("config"); runShellCommand("config");
verify(mErrorWriter, never()).println(); verify(mErrorWriter, never()).println();
verify(mOutputWriter, times(1)).println(contains("Nat64Enabled=true")); verify(mOutputWriter, times(1)).println(contains("nat64Enabled=true"));
} }
@Test @Test