Add persistence for VcnConfig objects by Subscription Group

This commit adds the ability for the VcnManagementService to track/store
VCN profiles by subscription groups, and saving/loading to/from disk.

Bug: 163611304
Test: New tests added, passing
Change-Id: Ifabf5e2be090d529cd29e2c68d55ece4858b2aad
This commit is contained in:
Benedict Wong
2020-08-18 19:49:17 -07:00
parent 264973663c
commit ab1b484ac7
3 changed files with 198 additions and 8 deletions

View File

@@ -23,6 +23,9 @@ import android.annotation.SystemService;
import android.content.Context;
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import java.io.IOException;
/**
* VcnManager publishes APIs for applications to configure and manage Virtual Carrier Networks.
@@ -63,15 +66,20 @@ public final class VcnManager {
* @param config the configuration parameters for the VCN
* @throws SecurityException if the caller does not have carrier privileges, or is not running
* as the primary user
* @throws IOException if the configuration failed to be persisted. A caller encountering this
* exception should attempt to retry (possibly after a delay).
* @hide
*/
@RequiresPermission("carrier privileges") // TODO (b/72967236): Define a system-wide constant
public void setVcnConfig(@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config) {
public void setVcnConfig(@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config)
throws IOException {
requireNonNull(subscriptionGroup, "subscriptionGroup was null");
requireNonNull(config, "config was null");
try {
mService.setVcnConfig(subscriptionGroup, config);
} catch (ServiceSpecificException e) {
throw new IOException(e);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -88,14 +96,18 @@ public final class VcnManager {
* @param subscriptionGroup the subscription group that the configuration should be applied to
* @throws SecurityException if the caller does not have carrier privileges, or is not running
* as the primary user
* @throws IOException if the configuration failed to be cleared. A caller encountering this
* exception should attempt to retry (possibly after a delay).
* @hide
*/
@RequiresPermission("carrier privileges") // TODO (b/72967236): Define a system-wide constant
public void clearVcnConfig(@NonNull ParcelUuid subscriptionGroup) {
public void clearVcnConfig(@NonNull ParcelUuid subscriptionGroup) throws IOException {
requireNonNull(subscriptionGroup, "subscriptionGroup was null");
try {
mService.clearVcnConfig(subscriptionGroup);
} catch (ServiceSpecificException e) {
throw new IOException(e);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}

View File

@@ -26,20 +26,31 @@ import android.net.NetworkRequest;
import android.net.vcn.IVcnManagementService;
import android.net.vcn.VcnConfig;
import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.ParcelUuid;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.ServiceSpecificException;
import android.os.UserHandle;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.ArrayMap;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting.Visibility;
import com.android.server.vcn.util.PersistableBundleUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* VcnManagementService manages Virtual Carrier Network profiles and lifecycles.
@@ -101,20 +112,72 @@ public class VcnManagementService extends IVcnManagementService.Stub {
public static final boolean VDBG = false; // STOPSHIP: if true
@VisibleForTesting(visibility = Visibility.PRIVATE)
static final String VCN_CONFIG_FILE = "/data/system/vcn/configs.xml";
/* Binder context for this service */
@NonNull private final Context mContext;
@NonNull private final Dependencies mDeps;
@NonNull private final Looper mLooper;
@NonNull private final Handler mHandler;
@NonNull private final VcnNetworkProvider mNetworkProvider;
@GuardedBy("mLock")
@NonNull
private final Map<ParcelUuid, VcnConfig> mConfigs = new ArrayMap<>();
@NonNull private final Object mLock = new Object();
@NonNull private final PersistableBundleUtils.LockingReadWriteHelper mConfigDiskRwHelper;
@VisibleForTesting(visibility = Visibility.PRIVATE)
VcnManagementService(@NonNull Context context, @NonNull Dependencies deps) {
mContext = requireNonNull(context, "Missing context");
mDeps = requireNonNull(deps, "Missing dependencies");
mLooper = mDeps.getLooper();
mHandler = new Handler(mLooper);
mNetworkProvider = new VcnNetworkProvider(mContext, mLooper);
mConfigDiskRwHelper = mDeps.newPersistableBundleLockingReadWriteHelper(VCN_CONFIG_FILE);
// Run on handler to ensure I/O does not block system server startup
mHandler.post(() -> {
PersistableBundle configBundle = null;
try {
configBundle = mConfigDiskRwHelper.readFromDisk();
} catch (IOException e1) {
Slog.e(TAG, "Failed to read configs from disk; retrying", e1);
// Retry immediately. The IOException may have been transient.
try {
configBundle = mConfigDiskRwHelper.readFromDisk();
} catch (IOException e2) {
Slog.wtf(TAG, "Failed to read configs from disk", e2);
return;
}
}
if (configBundle != null) {
final Map<ParcelUuid, VcnConfig> configs =
PersistableBundleUtils.toMap(
configBundle,
PersistableBundleUtils::toParcelUuid,
VcnConfig::new);
synchronized (mLock) {
for (Entry<ParcelUuid, VcnConfig> entry : configs.entrySet()) {
// Ensure no new configs are overwritten; a carrier app may have added a new
// config.
if (!mConfigs.containsKey(entry.getKey())) {
mConfigs.put(entry.getKey(), entry.getValue());
}
}
// TODO: Trigger re-evaluation of active VCNs; start/stop VCNs as needed.
}
}
});
}
// Package-visibility for SystemServer to create instances.
@@ -151,12 +214,21 @@ public class VcnManagementService extends IVcnManagementService.Stub {
public int getBinderCallingUid() {
return Binder.getCallingUid();
}
/**
* Creates and returns a new {@link PersistableBundle.LockingReadWriteHelper}
*
* @param path the file path to read/write from/to.
* @return the {@link PersistableBundleUtils.LockingReadWriteHelper} instance
*/
public PersistableBundleUtils.LockingReadWriteHelper
newPersistableBundleLockingReadWriteHelper(@NonNull String path) {
return new PersistableBundleUtils.LockingReadWriteHelper(path);
}
}
/** Notifies the VcnManagementService that external dependencies can be set up. */
public void systemReady() {
// TODO: Retrieve existing profiles from KeyStore
mContext.getSystemService(ConnectivityManager.class)
.registerNetworkProvider(mNetworkProvider);
}
@@ -217,9 +289,15 @@ public class VcnManagementService extends IVcnManagementService.Stub {
enforceCallingUserAndCarrierPrivilege(subscriptionGroup);
// TODO: Clear Binder calling identity
synchronized (mLock) {
mConfigs.put(subscriptionGroup, config);
// TODO: Store VCN configuration, trigger startup as necessary
// Must be done synchronously to ensure that writes do not happen out-of-order.
writeConfigsToDiskLocked();
}
// TODO: Clear Binder calling identity
// TODO: Trigger startup as necessary
}
/**
@@ -233,9 +311,38 @@ public class VcnManagementService extends IVcnManagementService.Stub {
enforceCallingUserAndCarrierPrivilege(subscriptionGroup);
// TODO: Clear Binder calling identity
synchronized (mLock) {
mConfigs.remove(subscriptionGroup);
// TODO: Clear VCN configuration, trigger teardown as necessary
// Must be done synchronously to ensure that writes do not happen out-of-order.
writeConfigsToDiskLocked();
}
// TODO: Clear Binder calling identity
// TODO: Trigger teardown as necessary
}
@GuardedBy("mLock")
private void writeConfigsToDiskLocked() {
try {
PersistableBundle bundle =
PersistableBundleUtils.fromMap(
mConfigs,
PersistableBundleUtils::fromParcelUuid,
VcnConfig::toPersistableBundle);
mConfigDiskRwHelper.writeToDisk(bundle);
} catch (IOException e) {
Slog.e(TAG, "Failed to save configs to disk", e);
throw new ServiceSpecificException(0, "Failed to save configs");
}
}
/** Get current configuration list for testing purposes */
@VisibleForTesting(visibility = Visibility.PRIVATE)
Map<ParcelUuid, VcnConfig> getConfigs() {
synchronized (mLock) {
return Collections.unmodifiableMap(mConfigs);
}
}
/**

View File

@@ -16,6 +16,9 @@
package com.android.server;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doReturn;
@@ -25,8 +28,10 @@ import static org.mockito.Mockito.verify;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.vcn.VcnConfig;
import android.net.vcn.VcnConfigTest;
import android.os.ParcelUuid;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.UserHandle;
import android.os.test.TestLooper;
@@ -37,10 +42,14 @@ import android.telephony.TelephonyManager;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.vcn.util.PersistableBundleUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.FileNotFoundException;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
/** Tests for {@link VcnManagementService}. */
@@ -48,6 +57,11 @@ import java.util.UUID;
@SmallTest
public class VcnManagementServiceTest {
private static final ParcelUuid TEST_UUID_1 = new ParcelUuid(new UUID(0, 0));
private static final ParcelUuid TEST_UUID_2 = new ParcelUuid(new UUID(1, 1));
private static final VcnConfig TEST_VCN_CONFIG = VcnConfigTest.buildTestConfig();
private static final Map<ParcelUuid, VcnConfig> TEST_VCN_CONFIG_MAP =
Collections.unmodifiableMap(Collections.singletonMap(TEST_UUID_1, TEST_VCN_CONFIG));
private static final SubscriptionInfo TEST_SUBSCRIPTION_INFO =
new SubscriptionInfo(
1 /* id */,
@@ -79,6 +93,8 @@ public class VcnManagementServiceTest {
private final TelephonyManager mTelMgr = mock(TelephonyManager.class);
private final SubscriptionManager mSubMgr = mock(SubscriptionManager.class);
private final VcnManagementService mVcnMgmtSvc;
private final PersistableBundleUtils.LockingReadWriteHelper mConfigReadWriteHelper =
mock(PersistableBundleUtils.LockingReadWriteHelper.class);
public VcnManagementServiceTest() throws Exception {
setupSystemService(mConnMgr, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class);
@@ -88,6 +104,16 @@ public class VcnManagementServiceTest {
doReturn(mTestLooper.getLooper()).when(mMockDeps).getLooper();
doReturn(Process.FIRST_APPLICATION_UID).when(mMockDeps).getBinderCallingUid();
doReturn(mConfigReadWriteHelper)
.when(mMockDeps)
.newPersistableBundleLockingReadWriteHelper(any());
final PersistableBundle bundle =
PersistableBundleUtils.fromMap(
TEST_VCN_CONFIG_MAP,
PersistableBundleUtils::fromParcelUuid,
VcnConfig::toPersistableBundle);
doReturn(bundle).when(mConfigReadWriteHelper).readFromDisk();
setupMockedCarrierPrivilege(true);
mVcnMgmtSvc = new VcnManagementService(mMockContext, mMockDeps);
@@ -115,6 +141,36 @@ public class VcnManagementServiceTest {
.registerNetworkProvider(any(VcnManagementService.VcnNetworkProvider.class));
}
@Test
public void testNonSystemServerRealConfigFileAccessPermission() throws Exception {
// Attempt to build a real instance of the dependencies, and verify we cannot write to the
// file.
VcnManagementService.Dependencies deps = new VcnManagementService.Dependencies();
PersistableBundleUtils.LockingReadWriteHelper configReadWriteHelper =
deps.newPersistableBundleLockingReadWriteHelper(
VcnManagementService.VCN_CONFIG_FILE);
// Even tests should not be able to read/write configs from disk; SELinux policies restrict
// it to only the system server.
// Reading config should always return null since the file "does not exist", and writing
// should throw an IOException.
assertNull(configReadWriteHelper.readFromDisk());
try {
configReadWriteHelper.writeToDisk(new PersistableBundle());
fail("Expected IOException due to SELinux policy");
} catch (FileNotFoundException expected) {
}
}
@Test
public void testLoadVcnConfigsOnStartup() throws Exception {
mTestLooper.dispatchAll();
assertEquals(TEST_VCN_CONFIG_MAP, mVcnMgmtSvc.getConfigs());
verify(mConfigReadWriteHelper).readFromDisk();
}
@Test
public void testSetVcnConfigRequiresNonSystemServer() throws Exception {
doReturn(Process.SYSTEM_UID).when(mMockDeps).getBinderCallingUid();
@@ -150,6 +206,14 @@ public class VcnManagementServiceTest {
}
}
@Test
public void testSetVcnConfig() throws Exception {
// Use a different UUID to simulate a new VCN config.
mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG);
assertEquals(TEST_VCN_CONFIG, mVcnMgmtSvc.getConfigs().get(TEST_UUID_2));
verify(mConfigReadWriteHelper).writeToDisk(any(PersistableBundle.class));
}
@Test
public void testClearVcnConfigRequiresNonSystemServer() throws Exception {
doReturn(Process.SYSTEM_UID).when(mMockDeps).getBinderCallingUid();
@@ -184,4 +248,11 @@ public class VcnManagementServiceTest {
} catch (SecurityException expected) {
}
}
@Test
public void testClearVcnConfig() throws Exception {
mVcnMgmtSvc.clearVcnConfig(TEST_UUID_1);
assertTrue(mVcnMgmtSvc.getConfigs().isEmpty());
verify(mConfigReadWriteHelper).writeToDisk(any(PersistableBundle.class));
}
}