diff options
3 files changed, 197 insertions, 7 deletions
diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java index 6769b9e46e4c..46d1c1c7a23a 100644 --- a/core/java/android/net/vcn/VcnManager.java +++ b/core/java/android/net/vcn/VcnManager.java @@ -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(); } diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java index e9f17fff5a61..5e85409b0a42 100644 --- a/services/core/java/com/android/server/VcnManagementService.java +++ b/services/core/java/com/android/server/VcnManagementService.java @@ -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); + + // Must be done synchronously to ensure that writes do not happen out-of-order. + writeConfigsToDiskLocked(); + } - // TODO: Store VCN configuration, trigger startup as necessary + // TODO: Clear Binder calling identity + // TODO: Trigger startup as necessary } /** @@ -233,9 +311,38 @@ public class VcnManagementService extends IVcnManagementService.Stub { enforceCallingUserAndCarrierPrivilege(subscriptionGroup); + synchronized (mLock) { + mConfigs.remove(subscriptionGroup); + + // 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 + } - // TODO: Clear VCN configuration, 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); + } } /** diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java index 876e07fbce0f..1cc953239fed 100644 --- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java +++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java @@ -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); @@ -116,6 +142,36 @@ public class VcnManagementServiceTest { } @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(); @@ -151,6 +207,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)); + } } |