[Thread] disable Thread when user restriction is set

This commit follows the guideline in go/ae-v-thread-admin-control that
Thread radio should be disabled when the user restriction is set and
nobody can enable Thread again (it fails with
ERROR_FAILED_PRECONDIFITION).

This commit adds only unit tests, E2E CTS tests are tracked in b/319079428

Bug: 319077876
Change-Id: I8981aa9e71948c64ee79701427ac794880826327
This commit is contained in:
Kangping Dong 2024-02-04 10:50:05 +08:00
commit 505b5ce45b
4 changed files with 229 additions and 20 deletions

View file

@ -89,8 +89,9 @@ public class ThreadNetworkException extends Exception {
/**
* The operation failed because required preconditions were not satisfied. For example, trying
* to schedule a network migration when this device is not attached will receive this error. The
* caller should not retry the same operation before the precondition is satisfied.
* to schedule a network migration when this device is not attached will receive this error or
* enable Thread while User Resitration has disabled it. The caller should not retry the same
* operation before the precondition is satisfied.
*/
public static final int ERROR_FAILED_PRECONDITION = 6;

View file

@ -79,6 +79,17 @@ public final class ThreadNetworkManager {
public static final String PERMISSION_THREAD_NETWORK_PRIVILEGED =
"android.permission.THREAD_NETWORK_PRIVILEGED";
/**
* This user restriction specifies if Thread network is disallowed on the device. If Thread
* network is disallowed it cannot be turned on via Settings.
*
* <p>this is a mirror of {@link UserManager#DISALLOW_THREAD_NETWORK} which is not available
* on Android U devices.
*
* @hide
*/
public static final String DISALLOW_THREAD_NETWORK = "no_thread_network";
@NonNull private final Context mContext;
@NonNull private final List<ThreadNetworkController> mUnmodifiableControllerServices;

View file

@ -41,6 +41,7 @@ import static android.net.thread.ThreadNetworkException.ERROR_RESPONSE_BAD_FORMA
import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
import static android.net.thread.ThreadNetworkException.ERROR_TIMEOUT;
import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_ABORT;
@ -64,7 +65,10 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.LinkAddress;
import android.net.LinkProperties;
@ -98,6 +102,7 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserManager;
import android.util.Log;
import android.util.SparseArray;
@ -167,6 +172,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
private TestNetworkSpecifier mUpstreamTestNetworkSpecifier;
private final HashMap<Network, String> mNetworkToInterface;
private final ThreadPersistentSettings mPersistentSettings;
private final UserManager mUserManager;
private boolean mUserRestricted;
private BorderRouterConfigurationParcel mBorderRouterConfig;
@ -180,7 +187,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
TunInterfaceController tunIfController,
InfraInterfaceController infraIfController,
ThreadPersistentSettings persistentSettings,
NsdPublisher nsdPublisher) {
NsdPublisher nsdPublisher,
UserManager userManager) {
mContext = context;
mHandler = handler;
mNetworkProvider = networkProvider;
@ -193,6 +201,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
mBorderRouterConfig = new BorderRouterConfigurationParcel();
mPersistentSettings = persistentSettings;
mNsdPublisher = nsdPublisher;
mUserManager = userManager;
}
public static ThreadNetworkControllerService newInstance(
@ -212,7 +221,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
new TunInterfaceController(TUN_IF_NAME),
new InfraInterfaceController(),
persistentSettings,
NsdPublisher.newInstance(context, handler));
NsdPublisher.newInstance(context, handler),
context.getSystemService(UserManager.class));
}
private static Inet6Address bytesToInet6Address(byte[] addressBytes) {
@ -288,10 +298,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
if (otDaemon == null) {
throw new RemoteException("Internal error: failed to start OT daemon");
}
otDaemon.initialize(
mTunIfController.getTunFd(),
mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED),
mNsdPublisher);
otDaemon.initialize(mTunIfController.getTunFd(), isEnabled(), mNsdPublisher);
otDaemon.registerStateCallback(mOtDaemonCallbackProxy, -1);
otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
mOtDaemon = otDaemon;
@ -323,23 +330,39 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
mConnectivityManager.registerNetworkProvider(mNetworkProvider);
requestUpstreamNetwork();
requestThreadNetwork();
mUserRestricted = isThreadUserRestricted();
registerUserRestrictionsReceiver();
initializeOtDaemon();
});
}
public void setEnabled(@NonNull boolean isEnabled, @NonNull IOperationReceiver receiver) {
public void setEnabled(boolean isEnabled, @NonNull IOperationReceiver receiver) {
enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
mHandler.post(() -> setEnabledInternal(isEnabled, new OperationReceiverWrapper(receiver)));
mHandler.post(
() ->
setEnabledInternal(
isEnabled,
true /* persist */,
new OperationReceiverWrapper(receiver)));
}
private void setEnabledInternal(
@NonNull boolean isEnabled, @Nullable OperationReceiverWrapper receiver) {
// The persistent setting keeps the desired enabled state, thus it's set regardless
// the otDaemon set enabled state operation succeeded or not, so that it can recover
// to the desired value after reboot.
mPersistentSettings.put(ThreadPersistentSettings.THREAD_ENABLED.key, isEnabled);
boolean isEnabled, boolean persist, @NonNull OperationReceiverWrapper receiver) {
if (isEnabled && isThreadUserRestricted()) {
receiver.onError(
ERROR_FAILED_PRECONDITION,
"Cannot enable Thread: forbidden by user restriction");
return;
}
if (persist) {
// The persistent setting keeps the desired enabled state, thus it's set regardless
// the otDaemon set enabled state operation succeeded or not, so that it can recover
// to the desired value after reboot.
mPersistentSettings.put(ThreadPersistentSettings.THREAD_ENABLED.key, isEnabled);
}
try {
getOtDaemon().setThreadEnabled(isEnabled, newOtStatusReceiver(receiver));
} catch (RemoteException e) {
@ -348,6 +371,67 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
}
}
private void registerUserRestrictionsReceiver() {
mContext.registerReceiver(
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
onUserRestrictionsChanged(isThreadUserRestricted());
}
},
new IntentFilter(UserManager.ACTION_USER_RESTRICTIONS_CHANGED),
null /* broadcastPermission */,
mHandler);
}
private void onUserRestrictionsChanged(boolean newUserRestrictedState) {
checkOnHandlerThread();
if (mUserRestricted == newUserRestrictedState) {
return;
}
Log.i(
TAG,
"Thread user restriction changed: "
+ mUserRestricted
+ " -> "
+ newUserRestrictedState);
mUserRestricted = newUserRestrictedState;
final boolean isEnabled = isEnabled();
final IOperationReceiver receiver =
new IOperationReceiver.Stub() {
@Override
public void onSuccess() {
Log.d(
TAG,
(isEnabled ? "Enabled" : "Disabled")
+ " Thread due to user restriction change");
}
@Override
public void onError(int otError, String messages) {
Log.e(
TAG,
"Failed to "
+ (isEnabled ? "enable" : "disable")
+ " Thread for user restriction change");
}
};
// Do not save the user restriction state to persistent settings so that the user
// configuration won't be overwritten
setEnabledInternal(isEnabled, false /* persist */, new OperationReceiverWrapper(receiver));
}
/** Returns {@code true} if Thread is set enabled. */
private boolean isEnabled() {
return !mUserRestricted && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
}
/** Returns {@code true} if Thread has been restricted for the user. */
private boolean isThreadUserRestricted() {
return mUserManager.hasUserRestriction(DISALLOW_THREAD_NETWORK);
}
private void requestUpstreamNetwork() {
if (mUpstreamNetworkCallback != null) {
throw new AssertionError("The upstream network request is already there.");

View file

@ -16,7 +16,11 @@
package com.android.server.thread;
import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static com.android.testutils.TestPermissionUtil.runAsShell;
@ -24,24 +28,31 @@ 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 org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkAgent;
import android.net.NetworkProvider;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.IOperationReceiver;
import android.net.thread.ThreadNetworkException;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.UserManager;
import android.os.test.TestLooper;
import androidx.test.core.app.ApplicationProvider;
@ -56,6 +67,10 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
/** Unit tests for {@link ThreadNetworkControllerService}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
@ -88,6 +103,7 @@ public final class ThreadNetworkControllerServiceTest {
@Mock private InfraInterfaceController mMockInfraIfController;
@Mock private ThreadPersistentSettings mMockPersistentSettings;
@Mock private NsdPublisher mMockNsdPublisher;
@Mock private UserManager mMockUserManager;
private Context mContext;
private TestLooper mTestLooper;
private FakeOtDaemon mFakeOtDaemon;
@ -97,21 +113,21 @@ public final class ThreadNetworkControllerServiceTest {
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = ApplicationProvider.getApplicationContext();
mContext = spy(ApplicationProvider.getApplicationContext());
mTestLooper = new TestLooper();
final Handler handler = new Handler(mTestLooper.getLooper());
NetworkProvider networkProvider =
new NetworkProvider(mContext, mTestLooper.getLooper(), "ThreadNetworkProvider");
mFakeOtDaemon = new FakeOtDaemon(handler);
when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
when(mMockPersistentSettings.get(any())).thenReturn(true);
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
mService =
new ThreadNetworkControllerService(
ApplicationProvider.getApplicationContext(),
mContext,
handler,
networkProvider,
() -> mFakeOtDaemon,
@ -119,7 +135,8 @@ public final class ThreadNetworkControllerServiceTest {
mMockTunIfController,
mMockInfraIfController,
mMockPersistentSettings,
mMockNsdPublisher);
mMockNsdPublisher,
mMockUserManager);
mService.setTestNetworkAgent(mMockNetworkAgent);
}
@ -168,4 +185,100 @@ public final class ThreadNetworkControllerServiceTest {
verify(mockReceiver, times(1)).onSuccess();
verify(mMockNetworkAgent, times(1)).register();
}
@Test
public void userRestriction_initWithUserRestricted_threadIsDisabled() {
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
mService.initialize();
mTestLooper.dispatchAll();
assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
}
@Test
public void userRestriction_initWithUserNotRestricted_threadIsEnabled() {
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
mService.initialize();
mTestLooper.dispatchAll();
assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
}
@Test
public void userRestriction_userBecomesRestricted_stateIsDisabledButNotPersisted() {
AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
doAnswer(
invocation -> {
receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
return null;
})
.when(mContext)
.registerReceiver(any(BroadcastReceiver.class), any(), any(), any());
mService.initialize();
mTestLooper.dispatchAll();
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
receiverRef.get().onReceive(mContext, new Intent());
mTestLooper.dispatchAll();
assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
verify(mMockPersistentSettings, never())
.put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(false));
}
@Test
public void userRestriction_userBecomesNotRestricted_stateIsEnabledButNotPersisted() {
AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
doAnswer(
invocation -> {
receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
return null;
})
.when(mContext)
.registerReceiver(any(BroadcastReceiver.class), any(), any(), any());
mService.initialize();
mTestLooper.dispatchAll();
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
receiverRef.get().onReceive(mContext, new Intent());
mTestLooper.dispatchAll();
assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
verify(mMockPersistentSettings, never())
.put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(true));
}
@Test
public void userRestriction_setEnabledWhenUserRestricted_failedPreconditionError() {
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
mService.initialize();
CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
runAsShell(
PERMISSION_THREAD_NETWORK_PRIVILEGED,
() -> mService.setEnabled(true, newOperationReceiver(setEnabledFuture)));
mTestLooper.dispatchAll();
var thrown = assertThrows(ExecutionException.class, () -> setEnabledFuture.get());
ThreadNetworkException failure = (ThreadNetworkException) thrown.getCause();
assertThat(failure.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
}
private static IOperationReceiver newOperationReceiver(CompletableFuture<Void> future) {
return new IOperationReceiver.Stub() {
@Override
public void onSuccess() {
future.complete(null);
}
@Override
public void onError(int errorCode, String errorMessage) {
future.completeExceptionally(new ThreadNetworkException(errorCode, errorMessage));
}
};
}
}