From 1fe26df5382a1573a9b968a6a57ad8376d9e27d3 Mon Sep 17 00:00:00 2001 From: Benedict Wong Date: Tue, 24 Nov 2020 17:00:23 -0800 Subject: [PATCH 1/4] Add basic VcnMgmtSvc --> Vcn signals (startup, teardown, NetworkReq) This change adds the relevant calls to ensure that the VcnMgmtSvc starts, updates and stops a Vcn instance when configs are set/removed Additionally, this change ensures that upon new network requests, the Vcn instance is notified. Bug: 163432852 Test: atest FrameworksVcnTests Change-Id: Ifec34fad8282a3d64b540d24f643f546463f4379 --- .../android/server/VcnManagementService.java | 80 +++++++++++++++---- .../core/java/com/android/server/vcn/Vcn.java | 2 +- .../server/VcnManagementServiceTest.java | 42 +++++++++- 3 files changed, 106 insertions(+), 18 deletions(-) diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java index 74e38510770b6..6ed4bbb2b31fb 100644 --- a/services/core/java/com/android/server/VcnManagementService.java +++ b/services/core/java/com/android/server/VcnManagementService.java @@ -43,6 +43,8 @@ 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.Vcn; +import com.android.server.vcn.VcnContext; import com.android.server.vcn.util.PersistableBundleUtils; import java.io.IOException; @@ -122,11 +124,16 @@ public class VcnManagementService extends IVcnManagementService.Stub { @NonNull private final Looper mLooper; @NonNull private final Handler mHandler; @NonNull private final VcnNetworkProvider mNetworkProvider; + @NonNull private final VcnContext mVcnContext; @GuardedBy("mLock") @NonNull private final Map mConfigs = new ArrayMap<>(); + @GuardedBy("mLock") + @NonNull + private final Map mVcns = new ArrayMap<>(); + @NonNull private final Object mLock = new Object(); @NonNull private final PersistableBundleUtils.LockingReadWriteHelper mConfigDiskRwHelper; @@ -141,6 +148,7 @@ public class VcnManagementService extends IVcnManagementService.Stub { mNetworkProvider = new VcnNetworkProvider(mContext, mLooper); mConfigDiskRwHelper = mDeps.newPersistableBundleLockingReadWriteHelper(VCN_CONFIG_FILE); + mVcnContext = mDeps.newVcnContext(mContext, mLooper, mNetworkProvider); // Run on handler to ensure I/O does not block system server startup mHandler.post(() -> { @@ -225,6 +233,22 @@ public class VcnManagementService extends IVcnManagementService.Stub { newPersistableBundleLockingReadWriteHelper(@NonNull String path) { return new PersistableBundleUtils.LockingReadWriteHelper(path); } + + /** Creates a new VcnContext */ + public VcnContext newVcnContext( + @NonNull Context context, + @NonNull Looper looper, + @NonNull VcnNetworkProvider vcnNetworkProvider) { + return new VcnContext(context, looper, vcnNetworkProvider); + } + + /** Creates a new Vcn instance using the provided configuration */ + public Vcn newVcn( + @NonNull VcnContext vcnContext, + @NonNull ParcelUuid subscriptionGroup, + @NonNull VcnConfig config) { + return new Vcn(vcnContext, subscriptionGroup, config); + } } /** Notifies the VcnManagementService that external dependencies can be set up. */ @@ -277,6 +301,17 @@ public class VcnManagementService extends IVcnManagementService.Stub { "Carrier privilege required for subscription group to set VCN Config"); } + @GuardedBy("mLock") + private void startOrUpdateVcnLocked( + @NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config) { + if (mVcns.containsKey(subscriptionGroup)) { + mVcns.get(subscriptionGroup).updateConfig(config); + } else { + final Vcn newInstance = mDeps.newVcn(mVcnContext, subscriptionGroup, config); + mVcns.put(subscriptionGroup, newInstance); + } + } + /** * Sets a VCN config for a given subscription group. * @@ -289,15 +324,14 @@ public class VcnManagementService extends IVcnManagementService.Stub { enforceCallingUserAndCarrierPrivilege(subscriptionGroup); - synchronized (mLock) { - mConfigs.put(subscriptionGroup, config); + Binder.withCleanCallingIdentity(() -> { + synchronized (mLock) { + mConfigs.put(subscriptionGroup, config); + startOrUpdateVcnLocked(subscriptionGroup, config); - // 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 + writeConfigsToDiskLocked(); + } + }); } /** @@ -311,15 +345,17 @@ public class VcnManagementService extends IVcnManagementService.Stub { enforceCallingUserAndCarrierPrivilege(subscriptionGroup); - synchronized (mLock) { - mConfigs.remove(subscriptionGroup); + Binder.withCleanCallingIdentity(() -> { + synchronized (mLock) { + mConfigs.remove(subscriptionGroup); - // Must be done synchronously to ensure that writes do not happen out-of-order. - writeConfigsToDiskLocked(); - } + if (mVcns.containsKey(subscriptionGroup)) { + mVcns.remove(subscriptionGroup).teardownAsynchronously(); + } - // TODO: Clear Binder calling identity - // TODO: Trigger teardown as necessary + writeConfigsToDiskLocked(); + } + }); } @GuardedBy("mLock") @@ -345,6 +381,14 @@ public class VcnManagementService extends IVcnManagementService.Stub { } } + /** Get current configuration list for testing purposes */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public Map getAllVcns() { + synchronized (mLock) { + return Collections.unmodifiableMap(mVcns); + } + } + /** * Network provider for VCN networks. * @@ -357,7 +401,11 @@ public class VcnManagementService extends IVcnManagementService.Stub { @Override public void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId) { - // TODO: Handle network requests - Ensure VCN started, and start appropriate tunnels. + synchronized (mLock) { + for (Vcn instance : mVcns.values()) { + instance.onNetworkRequested(request, score, providerId); + } + } } } } diff --git a/services/core/java/com/android/server/vcn/Vcn.java b/services/core/java/com/android/server/vcn/Vcn.java index d51d16b1b4df2..493761be62342 100644 --- a/services/core/java/com/android/server/vcn/Vcn.java +++ b/services/core/java/com/android/server/vcn/Vcn.java @@ -69,7 +69,7 @@ public class Vcn extends Handler { } /** Asynchronously tears down this Vcn instance, along with all tunnels and Networks */ - public void teardown() { + public void teardownAsynchronously() { // TODO: Proxy to handler, and teardown there. } diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java index 1cc953239fed3..25c614ced067b 100644 --- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java +++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java @@ -21,6 +21,7 @@ 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.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; @@ -42,6 +43,9 @@ import android.telephony.TelephonyManager; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.server.VcnManagementService.VcnNetworkProvider; +import com.android.server.vcn.Vcn; +import com.android.server.vcn.VcnContext; import com.android.server.vcn.util.PersistableBundleUtils; import org.junit.Test; @@ -92,10 +96,12 @@ public class VcnManagementServiceTest { private final ConnectivityManager mConnMgr = mock(ConnectivityManager.class); private final TelephonyManager mTelMgr = mock(TelephonyManager.class); private final SubscriptionManager mSubMgr = mock(SubscriptionManager.class); - private final VcnManagementService mVcnMgmtSvc; + private final VcnContext mVcnContext = mock(VcnContext.class); private final PersistableBundleUtils.LockingReadWriteHelper mConfigReadWriteHelper = mock(PersistableBundleUtils.LockingReadWriteHelper.class); + private final VcnManagementService mVcnMgmtSvc; + public VcnManagementServiceTest() throws Exception { setupSystemService(mConnMgr, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class); setupSystemService(mTelMgr, Context.TELEPHONY_SERVICE, TelephonyManager.class); @@ -104,10 +110,22 @@ public class VcnManagementServiceTest { doReturn(mTestLooper.getLooper()).when(mMockDeps).getLooper(); doReturn(Process.FIRST_APPLICATION_UID).when(mMockDeps).getBinderCallingUid(); + doReturn(mVcnContext) + .when(mMockDeps) + .newVcnContext( + eq(mMockContext), + eq(mTestLooper.getLooper()), + any(VcnNetworkProvider.class)); doReturn(mConfigReadWriteHelper) .when(mMockDeps) .newPersistableBundleLockingReadWriteHelper(any()); + // Setup VCN instance generation + doAnswer((invocation) -> { + // Mock-within a doAnswer is safe, because it doesn't actually run nested. + return mock(Vcn.class); + }).when(mMockDeps).newVcn(any(), any(), any()); + final PersistableBundle bundle = PersistableBundleUtils.fromMap( TEST_VCN_CONFIG_MAP, @@ -255,4 +273,26 @@ public class VcnManagementServiceTest { assertTrue(mVcnMgmtSvc.getConfigs().isEmpty()); verify(mConfigReadWriteHelper).writeToDisk(any(PersistableBundle.class)); } + + @Test + public void testSetVcnConfigClearVcnConfigStartsUpdatesAndTeardsDownVcns() throws Exception { + // Use a different UUID to simulate a new VCN config. + mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG); + final Map vcnInstances = mVcnMgmtSvc.getAllVcns(); + final Vcn vcnInstance = vcnInstances.get(TEST_UUID_2); + assertEquals(1, vcnInstances.size()); + assertEquals(TEST_VCN_CONFIG, mVcnMgmtSvc.getConfigs().get(TEST_UUID_2)); + verify(mConfigReadWriteHelper).writeToDisk(any(PersistableBundle.class)); + + // Verify Vcn is started + verify(mMockDeps).newVcn(eq(mVcnContext), eq(TEST_UUID_2), eq(TEST_VCN_CONFIG)); + + // Verify Vcn is updated if it was previously started + mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG); + verify(vcnInstance).updateConfig(TEST_VCN_CONFIG); + + // Verify Vcn is stopped if it was already started + mVcnMgmtSvc.clearVcnConfig(TEST_UUID_2); + verify(vcnInstance).teardownAsynchronously(); + } } From 6153723db5f8bb7ec58e220a2085577766b8f46b Mon Sep 17 00:00:00 2001 From: Benedict Wong Date: Fri, 28 Aug 2020 15:46:20 -0700 Subject: [PATCH 2/4] Add TelephonySubscriptionTracker to VcnMgmtSvc This changes adds the TelephonySubscriptionTracker to VcnManagementService, ensuring that VCN Instances are only ever started once a relevant Subscription has been loaded in Telephony Bug: 163430955 Test: New VcnManagementService tests added, passing Change-Id: Ia9396d66b41fadc0a5ed7aa66306e6223a2d29b8 --- .../android/server/VcnManagementService.java | 98 +++++++++++++++- .../vcn/TelephonySubscriptionTracker.java | 3 + .../server/VcnManagementServiceTest.java | 110 +++++++++++++++++- 3 files changed, 207 insertions(+), 4 deletions(-) diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java index 6ed4bbb2b31fb..56bd1a82b3021 100644 --- a/services/core/java/com/android/server/VcnManagementService.java +++ b/services/core/java/com/android/server/VcnManagementService.java @@ -16,6 +16,9 @@ package com.android.server; +import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; +import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionTrackerCallback; + import static java.util.Objects.requireNonNull; import android.annotation.NonNull; @@ -43,6 +46,7 @@ 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.TelephonySubscriptionTracker; import com.android.server.vcn.Vcn; import com.android.server.vcn.VcnContext; import com.android.server.vcn.util.PersistableBundleUtils; @@ -53,6 +57,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; /** * VcnManagementService manages Virtual Carrier Network profiles and lifecycles. @@ -117,6 +122,9 @@ public class VcnManagementService extends IVcnManagementService.Stub { @VisibleForTesting(visibility = Visibility.PRIVATE) static final String VCN_CONFIG_FILE = "/data/system/vcn/configs.xml"; + @VisibleForTesting(visibility = Visibility.PRIVATE) + static final long CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS = TimeUnit.SECONDS.toMillis(30); + /* Binder context for this service */ @NonNull private final Context mContext; @NonNull private final Dependencies mDeps; @@ -124,6 +132,8 @@ public class VcnManagementService extends IVcnManagementService.Stub { @NonNull private final Looper mLooper; @NonNull private final Handler mHandler; @NonNull private final VcnNetworkProvider mNetworkProvider; + @NonNull private final TelephonySubscriptionTrackerCallback mTelephonySubscriptionTrackerCb; + @NonNull private final TelephonySubscriptionTracker mTelephonySubscriptionTracker; @NonNull private final VcnContext mVcnContext; @GuardedBy("mLock") @@ -134,6 +144,11 @@ public class VcnManagementService extends IVcnManagementService.Stub { @NonNull private final Map mVcns = new ArrayMap<>(); + @GuardedBy("mLock") + @NonNull + private TelephonySubscriptionSnapshot mLastSnapshot = + TelephonySubscriptionSnapshot.EMPTY_SNAPSHOT; + @NonNull private final Object mLock = new Object(); @NonNull private final PersistableBundleUtils.LockingReadWriteHelper mConfigDiskRwHelper; @@ -146,6 +161,9 @@ public class VcnManagementService extends IVcnManagementService.Stub { mLooper = mDeps.getLooper(); mHandler = new Handler(mLooper); mNetworkProvider = new VcnNetworkProvider(mContext, mLooper); + mTelephonySubscriptionTrackerCb = new VcnSubscriptionTrackerCallback(); + mTelephonySubscriptionTracker = mDeps.newTelephonySubscriptionTracker( + mContext, mLooper, mTelephonySubscriptionTrackerCb); mConfigDiskRwHelper = mDeps.newPersistableBundleLockingReadWriteHelper(VCN_CONFIG_FILE); mVcnContext = mDeps.newVcnContext(mContext, mLooper, mNetworkProvider); @@ -182,7 +200,10 @@ public class VcnManagementService extends IVcnManagementService.Stub { mConfigs.put(entry.getKey(), entry.getValue()); } } - // TODO: Trigger re-evaluation of active VCNs; start/stop VCNs as needed. + + // Re-evaluate subscriptions, and start/stop VCNs. This starts with an empty + // snapshot, and therefore safe even before telephony subscriptions are loaded. + mTelephonySubscriptionTrackerCb.onNewSnapshot(mLastSnapshot); } } }); @@ -211,6 +232,14 @@ public class VcnManagementService extends IVcnManagementService.Stub { return mHandlerThread.getLooper(); } + /** Creates a new VcnInstance using the provided configuration */ + public TelephonySubscriptionTracker newTelephonySubscriptionTracker( + @NonNull Context context, + @NonNull Looper looper, + @NonNull TelephonySubscriptionTrackerCallback callback) { + return new TelephonySubscriptionTracker(context, new Handler(looper), callback); + } + /** * Retrieves the caller's UID * @@ -255,6 +284,7 @@ public class VcnManagementService extends IVcnManagementService.Stub { public void systemReady() { mContext.getSystemService(ConnectivityManager.class) .registerNetworkProvider(mNetworkProvider); + mTelephonySubscriptionTracker.register(); } private void enforcePrimaryUser() { @@ -301,14 +331,74 @@ public class VcnManagementService extends IVcnManagementService.Stub { "Carrier privilege required for subscription group to set VCN Config"); } + private class VcnSubscriptionTrackerCallback implements TelephonySubscriptionTrackerCallback { + /** + * Handles subscription group changes, as notified by {@link TelephonySubscriptionTracker} + * + *

Start any unstarted VCN instances + * + * @hide + */ + public void onNewSnapshot(@NonNull TelephonySubscriptionSnapshot snapshot) { + // Startup VCN instances + synchronized (mLock) { + mLastSnapshot = snapshot; + + // Start any VCN instances as necessary + for (Entry entry : mConfigs.entrySet()) { + if (snapshot.getActiveSubscriptionGroups().contains(entry.getKey())) { + // TODO: Add checks to ensure provisioning app is currently carrier + // privileged on this subscription + if (!mVcns.containsKey(entry.getKey())) { + startVcnLocked(entry.getKey(), entry.getValue()); + } + + // Cancel any scheduled teardowns for active subscriptions + mHandler.removeCallbacksAndMessages(mVcns.get(entry.getKey())); + } + } + + // Schedule teardown of any VCN instances that have lost carrier privileges (after a + // delay) + for (Entry entry : mVcns.entrySet()) { + if (!snapshot.getActiveSubscriptionGroups().contains(entry.getKey())) { + final ParcelUuid uuidToTeardown = entry.getKey(); + final Vcn instanceToTeardown = entry.getValue(); + + mHandler.postDelayed(() -> { + synchronized (mLock) { + // Guard against case where this is run after a old instance was + // torn down, and a new instance was started. Verify to ensure + // correct instance is torn down. This could happen as a result of a + // Carrier App manually removing/adding a VcnConfig. + if (mVcns.get(uuidToTeardown) == instanceToTeardown) { + mVcns.remove(uuidToTeardown).teardownAsynchronously(); + } + } + }, instanceToTeardown, CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS); + } + } + } + } + } + + @GuardedBy("mLock") + private void startVcnLocked(@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config) { + Slog.v(TAG, "Starting VCN config for subGrp: " + subscriptionGroup); + + final Vcn newInstance = mDeps.newVcn(mVcnContext, subscriptionGroup, config); + mVcns.put(subscriptionGroup, newInstance); + } + @GuardedBy("mLock") private void startOrUpdateVcnLocked( @NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config) { + Slog.v(TAG, "Starting or updating VCN config for subGrp: " + subscriptionGroup); + if (mVcns.containsKey(subscriptionGroup)) { mVcns.get(subscriptionGroup).updateConfig(config); } else { - final Vcn newInstance = mDeps.newVcn(mVcnContext, subscriptionGroup, config); - mVcns.put(subscriptionGroup, newInstance); + startVcnLocked(subscriptionGroup, config); } } @@ -321,6 +411,7 @@ public class VcnManagementService extends IVcnManagementService.Stub { public void setVcnConfig(@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config) { requireNonNull(subscriptionGroup, "subscriptionGroup was null"); requireNonNull(config, "config was null"); + Slog.v(TAG, "VCN config updated for subGrp: " + subscriptionGroup); enforceCallingUserAndCarrierPrivilege(subscriptionGroup); @@ -342,6 +433,7 @@ public class VcnManagementService extends IVcnManagementService.Stub { @Override public void clearVcnConfig(@NonNull ParcelUuid subscriptionGroup) { requireNonNull(subscriptionGroup, "subscriptionGroup was null"); + Slog.v(TAG, "VCN config cleared for subGrp: " + subscriptionGroup); enforceCallingUserAndCarrierPrivilege(subscriptionGroup); diff --git a/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java b/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java index c0608072df9db..9c9ca3df47bf2 100644 --- a/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java +++ b/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java @@ -233,6 +233,9 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver { private final Map mSubIdToGroupMap; private final Set mActiveGroups; + public static final TelephonySubscriptionSnapshot EMPTY_SNAPSHOT = + new TelephonySubscriptionSnapshot(Collections.emptyMap(), Collections.emptySet()); + @VisibleForTesting(visibility = Visibility.PRIVATE) TelephonySubscriptionSnapshot( @NonNull Map subIdToGroupMap, diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java index 25c614ced067b..04091bc05514d 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 com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; +import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionTrackerCallback; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -25,6 +28,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.content.Context; @@ -44,16 +48,19 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.server.VcnManagementService.VcnNetworkProvider; +import com.android.server.vcn.TelephonySubscriptionTracker; import com.android.server.vcn.Vcn; import com.android.server.vcn.VcnContext; import com.android.server.vcn.util.PersistableBundleUtils; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import java.io.FileNotFoundException; import java.util.Collections; import java.util.Map; +import java.util.Set; import java.util.UUID; /** Tests for {@link VcnManagementService}. */ @@ -66,9 +73,10 @@ public class VcnManagementServiceTest { private static final Map TEST_VCN_CONFIG_MAP = Collections.unmodifiableMap(Collections.singletonMap(TEST_UUID_1, TEST_VCN_CONFIG)); + private static final int TEST_SUBSCRIPTION_ID = 1; private static final SubscriptionInfo TEST_SUBSCRIPTION_INFO = new SubscriptionInfo( - 1 /* id */, + TEST_SUBSCRIPTION_ID /* id */, "" /* iccId */, 0 /* simSlotIndex */, "Carrier" /* displayName */, @@ -99,6 +107,8 @@ public class VcnManagementServiceTest { private final VcnContext mVcnContext = mock(VcnContext.class); private final PersistableBundleUtils.LockingReadWriteHelper mConfigReadWriteHelper = mock(PersistableBundleUtils.LockingReadWriteHelper.class); + private final TelephonySubscriptionTracker mSubscriptionTracker = + mock(TelephonySubscriptionTracker.class); private final VcnManagementService mVcnMgmtSvc; @@ -116,6 +126,12 @@ public class VcnManagementServiceTest { eq(mMockContext), eq(mTestLooper.getLooper()), any(VcnNetworkProvider.class)); + doReturn(mSubscriptionTracker) + .when(mMockDeps) + .newTelephonySubscriptionTracker( + eq(mMockContext), + eq(mTestLooper.getLooper()), + any(TelephonySubscriptionTrackerCallback.class)); doReturn(mConfigReadWriteHelper) .when(mMockDeps) .newPersistableBundleLockingReadWriteHelper(any()); @@ -135,6 +151,9 @@ public class VcnManagementServiceTest { setupMockedCarrierPrivilege(true); mVcnMgmtSvc = new VcnManagementService(mMockContext, mMockDeps); + + // Make sure the profiles are loaded. + mTestLooper.dispatchAll(); } private void setupSystemService(Object service, String name, Class serviceClass) { @@ -157,6 +176,7 @@ public class VcnManagementServiceTest { verify(mConnMgr) .registerNetworkProvider(any(VcnManagementService.VcnNetworkProvider.class)); + verify(mSubscriptionTracker).register(); } @Test @@ -189,6 +209,94 @@ public class VcnManagementServiceTest { verify(mConfigReadWriteHelper).readFromDisk(); } + private void triggerSubscriptionTrackerCallback(Set activeSubscriptionGroups) { + final TelephonySubscriptionSnapshot snapshot = mock(TelephonySubscriptionSnapshot.class); + doReturn(activeSubscriptionGroups).when(snapshot).getActiveSubscriptionGroups(); + + final TelephonySubscriptionTrackerCallback cb = getTelephonySubscriptionTrackerCallback(); + cb.onNewSnapshot(snapshot); + } + + private TelephonySubscriptionTrackerCallback getTelephonySubscriptionTrackerCallback() { + final ArgumentCaptor captor = + ArgumentCaptor.forClass(TelephonySubscriptionTrackerCallback.class); + verify(mMockDeps) + .newTelephonySubscriptionTracker( + eq(mMockContext), eq(mTestLooper.getLooper()), captor.capture()); + return captor.getValue(); + } + + private Vcn startAndGetVcnInstance(ParcelUuid uuid) { + mVcnMgmtSvc.setVcnConfig(uuid, TEST_VCN_CONFIG); + return mVcnMgmtSvc.getAllVcns().get(uuid); + } + + @Test + public void testTelephonyNetworkTrackerCallbackStartsInstances() throws Exception { + triggerSubscriptionTrackerCallback(Collections.singleton(TEST_UUID_1)); + verify(mMockDeps).newVcn(eq(mVcnContext), eq(TEST_UUID_1), eq(TEST_VCN_CONFIG)); + } + + @Test + public void testTelephonyNetworkTrackerCallbackStopsInstances() throws Exception { + final TelephonySubscriptionTrackerCallback cb = getTelephonySubscriptionTrackerCallback(); + final Vcn vcn = startAndGetVcnInstance(TEST_UUID_2); + + triggerSubscriptionTrackerCallback(Collections.emptySet()); + + // Verify teardown after delay + mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS); + mTestLooper.dispatchAll(); + verify(vcn).teardownAsynchronously(); + } + + @Test + public void testTelephonyNetworkTrackerCallbackSimSwitchesDoNotKillVcnInstances() + throws Exception { + final TelephonySubscriptionTrackerCallback cb = getTelephonySubscriptionTrackerCallback(); + final Vcn vcn = startAndGetVcnInstance(TEST_UUID_2); + + // Simulate SIM unloaded + triggerSubscriptionTrackerCallback(Collections.emptySet()); + + // Simulate new SIM loaded right during teardown delay. + mTestLooper.moveTimeForward( + VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS / 2); + mTestLooper.dispatchAll(); + triggerSubscriptionTrackerCallback(Collections.singleton(TEST_UUID_2)); + + // Verify that even after the full timeout duration, the VCN instance is not torn down + mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS); + mTestLooper.dispatchAll(); + verify(vcn, never()).teardownAsynchronously(); + } + + @Test + public void testTelephonyNetworkTrackerCallbackDoesNotKillNewVcnInstances() throws Exception { + final TelephonySubscriptionTrackerCallback cb = getTelephonySubscriptionTrackerCallback(); + final Vcn oldInstance = startAndGetVcnInstance(TEST_UUID_2); + + // Simulate SIM unloaded + triggerSubscriptionTrackerCallback(Collections.emptySet()); + + // Config cleared, SIM reloaded & config re-added right before teardown delay, staring new + // vcnInstance. + mTestLooper.moveTimeForward( + VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS - 1); + mTestLooper.dispatchAll(); + mVcnMgmtSvc.clearVcnConfig(TEST_UUID_2); + final Vcn newInstance = startAndGetVcnInstance(TEST_UUID_2); + + // Verify that new instance was different, and the old one was torn down + assertTrue(oldInstance != newInstance); + verify(oldInstance).teardownAsynchronously(); + + // Verify that even after the full timeout duration, the new VCN instance is not torn down + mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS); + mTestLooper.dispatchAll(); + verify(newInstance, never()).teardownAsynchronously(); + } + @Test public void testSetVcnConfigRequiresNonSystemServer() throws Exception { doReturn(Process.SYSTEM_UID).when(mMockDeps).getBinderCallingUid(); From 9cb580784531a0c62164ce4b907dc04ea522ba6a Mon Sep 17 00:00:00 2001 From: Benedict Wong Date: Mon, 30 Nov 2020 17:25:46 -0800 Subject: [PATCH 3/4] Verify carrier privileges for VCN-providing packages This change adds and verifies packages in the VcnManagmentService, ensuring that carrier privilege gain/loss correctly starts/tears down VCN instances. Bug: 163431877 Test: atest FrameworksNetTests Change-Id: I63203188c57fdde1cfc58aaf1108aa3e70eb4a50 --- .../net/vcn/IVcnManagementService.aidl | 2 +- core/java/android/net/vcn/VcnConfig.java | 43 ++++++-- core/java/android/net/vcn/VcnManager.java | 2 +- .../android/server/VcnManagementService.java | 23 +++- .../vcn/TelephonySubscriptionTracker.java | 63 ++++++++--- .../java/android/net/vcn/VcnConfigTest.java | 35 ++++-- .../server/VcnManagementServiceTest.java | 58 ++++++++-- .../vcn/TelephonySubscriptionTrackerTest.java | 100 ++++++++++++++---- 8 files changed, 261 insertions(+), 65 deletions(-) diff --git a/core/java/android/net/vcn/IVcnManagementService.aidl b/core/java/android/net/vcn/IVcnManagementService.aidl index 9dd01140b4139..04b585cdf4208 100644 --- a/core/java/android/net/vcn/IVcnManagementService.aidl +++ b/core/java/android/net/vcn/IVcnManagementService.aidl @@ -23,6 +23,6 @@ import android.os.ParcelUuid; * @hide */ interface IVcnManagementService { - void setVcnConfig(in ParcelUuid subscriptionGroup, in VcnConfig config); + void setVcnConfig(in ParcelUuid subscriptionGroup, in VcnConfig config, in String opPkgName); void clearVcnConfig(in ParcelUuid subscriptionGroup); } diff --git a/core/java/android/net/vcn/VcnConfig.java b/core/java/android/net/vcn/VcnConfig.java index d4a3fa7411b18..ede8faaaf261d 100644 --- a/core/java/android/net/vcn/VcnConfig.java +++ b/core/java/android/net/vcn/VcnConfig.java @@ -19,6 +19,7 @@ import static com.android.internal.annotations.VisibleForTesting.Visibility; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.Context; import android.os.Parcel; import android.os.Parcelable; import android.os.PersistableBundle; @@ -45,11 +46,17 @@ import java.util.Set; public final class VcnConfig implements Parcelable { @NonNull private static final String TAG = VcnConfig.class.getSimpleName(); + private static final String PACKAGE_NAME_KEY = "mPackageName"; + @NonNull private final String mPackageName; + private static final String GATEWAY_CONNECTION_CONFIGS_KEY = "mGatewayConnectionConfigs"; @NonNull private final Set mGatewayConnectionConfigs; - private VcnConfig(@NonNull Set tunnelConfigs) { - mGatewayConnectionConfigs = Collections.unmodifiableSet(tunnelConfigs); + private VcnConfig( + @NonNull String packageName, + @NonNull Set gatewayConnectionConfigs) { + mPackageName = packageName; + mGatewayConnectionConfigs = Collections.unmodifiableSet(gatewayConnectionConfigs); validate(); } @@ -61,6 +68,8 @@ public final class VcnConfig implements Parcelable { */ @VisibleForTesting(visibility = Visibility.PRIVATE) public VcnConfig(@NonNull PersistableBundle in) { + mPackageName = in.getString(PACKAGE_NAME_KEY); + final PersistableBundle gatewayConnectionConfigsBundle = in.getPersistableBundle(GATEWAY_CONNECTION_CONFIGS_KEY); mGatewayConnectionConfigs = @@ -72,8 +81,19 @@ public final class VcnConfig implements Parcelable { } private void validate() { + Objects.requireNonNull(mPackageName, "packageName was null"); Preconditions.checkCollectionNotEmpty( - mGatewayConnectionConfigs, "gatewayConnectionConfigs"); + mGatewayConnectionConfigs, "gatewayConnectionConfigs was empty"); + } + + /** + * Retrieve the package name of the provisioning app. + * + * @hide + */ + @NonNull + public String getProvisioningPackageName() { + return mPackageName; } /** Retrieves the set of configured tunnels. */ @@ -91,6 +111,8 @@ public final class VcnConfig implements Parcelable { public PersistableBundle toPersistableBundle() { final PersistableBundle result = new PersistableBundle(); + result.putString(PACKAGE_NAME_KEY, mPackageName); + final PersistableBundle gatewayConnectionConfigsBundle = PersistableBundleUtils.fromList( new ArrayList<>(mGatewayConnectionConfigs), @@ -102,7 +124,7 @@ public final class VcnConfig implements Parcelable { @Override public int hashCode() { - return Objects.hash(mGatewayConnectionConfigs); + return Objects.hash(mPackageName, mGatewayConnectionConfigs); } @Override @@ -112,7 +134,8 @@ public final class VcnConfig implements Parcelable { } final VcnConfig rhs = (VcnConfig) other; - return mGatewayConnectionConfigs.equals(rhs.mGatewayConnectionConfigs); + return mPackageName.equals(rhs.mPackageName) + && mGatewayConnectionConfigs.equals(rhs.mGatewayConnectionConfigs); } // Parcelable methods @@ -143,9 +166,17 @@ public final class VcnConfig implements Parcelable { /** This class is used to incrementally build {@link VcnConfig} objects. */ public static class Builder { + @NonNull private final String mPackageName; + @NonNull private final Set mGatewayConnectionConfigs = new ArraySet<>(); + public Builder(@NonNull Context context) { + Objects.requireNonNull(context, "context was null"); + + mPackageName = context.getOpPackageName(); + } + /** * Adds a configuration for an individual gateway connection. * @@ -168,7 +199,7 @@ public final class VcnConfig implements Parcelable { */ @NonNull public VcnConfig build() { - return new VcnConfig(mGatewayConnectionConfigs); + return new VcnConfig(mPackageName, mGatewayConnectionConfigs); } } } diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java index 19c183f9fe9ca..b881a339535b0 100644 --- a/core/java/android/net/vcn/VcnManager.java +++ b/core/java/android/net/vcn/VcnManager.java @@ -101,7 +101,7 @@ public final class VcnManager { requireNonNull(config, "config was null"); try { - mService.setVcnConfig(subscriptionGroup, config); + mService.setVcnConfig(subscriptionGroup, config, mContext.getOpPackageName()); } catch (ServiceSpecificException e) { throw new IOException(e); } catch (RemoteException e) { diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java index 56bd1a82b3021..77e8e96a515cc 100644 --- a/services/core/java/com/android/server/VcnManagementService.java +++ b/services/core/java/com/android/server/VcnManagementService.java @@ -22,6 +22,7 @@ import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubsc import static java.util.Objects.requireNonNull; import android.annotation.NonNull; +import android.app.AppOpsManager; import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkProvider; @@ -122,6 +123,7 @@ public class VcnManagementService extends IVcnManagementService.Stub { @VisibleForTesting(visibility = Visibility.PRIVATE) static final String VCN_CONFIG_FILE = "/data/system/vcn/configs.xml"; + // TODO(b/176956496): Directly use CarrierServiceBindHelper.UNBIND_DELAY_MILLIS @VisibleForTesting(visibility = Visibility.PRIVATE) static final long CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS = TimeUnit.SECONDS.toMillis(30); @@ -346,9 +348,8 @@ public class VcnManagementService extends IVcnManagementService.Stub { // Start any VCN instances as necessary for (Entry entry : mConfigs.entrySet()) { - if (snapshot.getActiveSubscriptionGroups().contains(entry.getKey())) { - // TODO: Add checks to ensure provisioning app is currently carrier - // privileged on this subscription + if (snapshot.packageHasPermissionsForSubscriptionGroup( + entry.getKey(), entry.getValue().getProvisioningPackageName())) { if (!mVcns.containsKey(entry.getKey())) { startVcnLocked(entry.getKey(), entry.getValue()); } @@ -361,7 +362,10 @@ public class VcnManagementService extends IVcnManagementService.Stub { // Schedule teardown of any VCN instances that have lost carrier privileges (after a // delay) for (Entry entry : mVcns.entrySet()) { - if (!snapshot.getActiveSubscriptionGroups().contains(entry.getKey())) { + final VcnConfig config = mConfigs.get(entry.getKey()); + if (config == null + || !snapshot.packageHasPermissionsForSubscriptionGroup( + entry.getKey(), config.getProvisioningPackageName())) { final ParcelUuid uuidToTeardown = entry.getKey(); final Vcn instanceToTeardown = entry.getValue(); @@ -408,11 +412,20 @@ public class VcnManagementService extends IVcnManagementService.Stub { *

Implements the IVcnManagementService Binder interface. */ @Override - public void setVcnConfig(@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config) { + public void setVcnConfig( + @NonNull ParcelUuid subscriptionGroup, + @NonNull VcnConfig config, + @NonNull String opPkgName) { requireNonNull(subscriptionGroup, "subscriptionGroup was null"); requireNonNull(config, "config was null"); + requireNonNull(opPkgName, "opPkgName was null"); + if (!config.getProvisioningPackageName().equals(opPkgName)) { + throw new IllegalArgumentException("Mismatched caller and VcnConfig creator"); + } Slog.v(TAG, "VCN config updated for subGrp: " + subscriptionGroup); + mContext.getSystemService(AppOpsManager.class) + .checkPackage(mDeps.getBinderCallingUid(), config.getProvisioningPackageName()); enforceCallingUserAndCarrierPrivilege(subscriptionGroup); Binder.withCleanCallingIdentity(() -> { diff --git a/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java b/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java index 9c9ca3df47bf2..d8a145d9ae339 100644 --- a/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java +++ b/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java @@ -36,6 +36,8 @@ import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener; +import android.telephony.TelephonyManager; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; @@ -79,6 +81,7 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver { @NonNull private final TelephonySubscriptionTrackerCallback mCallback; @NonNull private final Dependencies mDeps; + @NonNull private final TelephonyManager mTelephonyManager; @NonNull private final SubscriptionManager mSubscriptionManager; @NonNull private final CarrierConfigManager mCarrierConfigManager; @@ -106,6 +109,7 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver { mCallback = Objects.requireNonNull(callback, "Missing callback"); mDeps = Objects.requireNonNull(deps, "Missing deps"); + mTelephonyManager = mContext.getSystemService(TelephonyManager.class); mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); mCarrierConfigManager = mContext.getSystemService(CarrierConfigManager.class); @@ -139,7 +143,7 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver { * so callbacks & broadcasts are all serialized on mHandler, avoiding the need for locking. */ public void handleSubscriptionsChanged() { - final Set activeSubGroups = new ArraySet<>(); + final Map> privilegedPackages = new HashMap<>(); final Map newSubIdToGroupMap = new HashMap<>(); final List allSubs = mSubscriptionManager.getAllSubscriptionInfoList(); @@ -166,12 +170,22 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver { // group. if (subInfo.getSimSlotIndex() != INVALID_SIM_SLOT_INDEX && mReadySubIdsBySlotId.values().contains(subInfo.getSubscriptionId())) { - activeSubGroups.add(subInfo.getGroupUuid()); + // TODO (b/172619301): Cache based on callbacks from CarrierPrivilegesTracker + + final TelephonyManager subIdSpecificTelephonyManager = + mTelephonyManager.createForSubscriptionId(subInfo.getSubscriptionId()); + + final ParcelUuid subGroup = subInfo.getGroupUuid(); + final Set pkgs = + privilegedPackages.getOrDefault(subGroup, new ArraySet<>()); + pkgs.addAll(subIdSpecificTelephonyManager.getPackagesWithCarrierPrivileges()); + + privilegedPackages.put(subGroup, pkgs); } } final TelephonySubscriptionSnapshot newSnapshot = - new TelephonySubscriptionSnapshot(newSubIdToGroupMap, activeSubGroups); + new TelephonySubscriptionSnapshot(newSubIdToGroupMap, privilegedPackages); // If snapshot was meaningfully updated, fire the callback if (!newSnapshot.equals(mCurrentSnapshot)) { @@ -231,25 +245,40 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver { /** TelephonySubscriptionSnapshot is a class containing info about active subscriptions */ public static class TelephonySubscriptionSnapshot { private final Map mSubIdToGroupMap; - private final Set mActiveGroups; + private final Map> mPrivilegedPackages; public static final TelephonySubscriptionSnapshot EMPTY_SNAPSHOT = - new TelephonySubscriptionSnapshot(Collections.emptyMap(), Collections.emptySet()); + new TelephonySubscriptionSnapshot(Collections.emptyMap(), Collections.emptyMap()); @VisibleForTesting(visibility = Visibility.PRIVATE) TelephonySubscriptionSnapshot( @NonNull Map subIdToGroupMap, - @NonNull Set activeGroups) { - mSubIdToGroupMap = Collections.unmodifiableMap( - Objects.requireNonNull(subIdToGroupMap, "subIdToGroupMap was null")); - mActiveGroups = Collections.unmodifiableSet( - Objects.requireNonNull(activeGroups, "activeGroups was null")); + @NonNull Map> privilegedPackages) { + Objects.requireNonNull(subIdToGroupMap, "subIdToGroupMap was null"); + Objects.requireNonNull(privilegedPackages, "privilegedPackages was null"); + + mSubIdToGroupMap = Collections.unmodifiableMap(subIdToGroupMap); + + final Map> unmodifiableInnerSets = new ArrayMap<>(); + for (Entry> entry : privilegedPackages.entrySet()) { + unmodifiableInnerSets.put( + entry.getKey(), Collections.unmodifiableSet(entry.getValue())); + } + mPrivilegedPackages = Collections.unmodifiableMap(unmodifiableInnerSets); } /** Returns the active subscription groups */ @NonNull public Set getActiveSubscriptionGroups() { - return mActiveGroups; + return mPrivilegedPackages.keySet(); + } + + /** Checks if the provided package is carrier privileged for the specified sub group. */ + public boolean packageHasPermissionsForSubscriptionGroup( + @NonNull ParcelUuid subGrp, @NonNull String packageName) { + final Set privilegedPackages = mPrivilegedPackages.get(subGrp); + + return privilegedPackages != null && privilegedPackages.contains(packageName); } /** Returns the Subscription Group for a given subId. */ @@ -276,7 +305,7 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver { @Override public int hashCode() { - return Objects.hash(mSubIdToGroupMap, mActiveGroups); + return Objects.hash(mSubIdToGroupMap, mPrivilegedPackages); } @Override @@ -288,7 +317,15 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver { final TelephonySubscriptionSnapshot other = (TelephonySubscriptionSnapshot) obj; return mSubIdToGroupMap.equals(other.mSubIdToGroupMap) - && mActiveGroups.equals(other.mActiveGroups); + && mPrivilegedPackages.equals(other.mPrivilegedPackages); + } + + @Override + public String toString() { + return "TelephonySubscriptionSnapshot{ " + + "mSubIdToGroupMap=" + mSubIdToGroupMap + + ", mPrivilegedPackages=" + mPrivilegedPackages + + " }"; } } diff --git a/tests/vcn/java/android/net/vcn/VcnConfigTest.java b/tests/vcn/java/android/net/vcn/VcnConfigTest.java index 77944deb26f1c..c1ef350e5c4ae 100644 --- a/tests/vcn/java/android/net/vcn/VcnConfigTest.java +++ b/tests/vcn/java/android/net/vcn/VcnConfigTest.java @@ -18,12 +18,17 @@ package android.net.vcn; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import android.annotation.NonNull; +import android.content.Context; import android.os.Parcel; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -33,12 +38,15 @@ import java.util.Set; @RunWith(AndroidJUnit4.class) @SmallTest public class VcnConfigTest { + private static final String TEST_PACKAGE_NAME = VcnConfigTest.class.getPackage().getName(); private static final Set GATEWAY_CONNECTION_CONFIGS = Collections.singleton(VcnGatewayConnectionConfigTest.buildTestConfig()); + private final Context mContext = mock(Context.class); + // Public visibility for VcnManagementServiceTest - public static VcnConfig buildTestConfig() { - VcnConfig.Builder builder = new VcnConfig.Builder(); + public static VcnConfig buildTestConfig(@NonNull Context context) { + VcnConfig.Builder builder = new VcnConfig.Builder(context); for (VcnGatewayConnectionConfig gatewayConnectionConfig : GATEWAY_CONNECTION_CONFIGS) { builder.addGatewayConnectionConfig(gatewayConnectionConfig); @@ -47,10 +55,24 @@ public class VcnConfigTest { return builder.build(); } + @Before + public void setUp() throws Exception { + doReturn(TEST_PACKAGE_NAME).when(mContext).getOpPackageName(); + } + + @Test + public void testBuilderConstructorRequiresContext() { + try { + new VcnConfig.Builder(null); + fail("Expected exception due to null context"); + } catch (NullPointerException e) { + } + } + @Test public void testBuilderRequiresGatewayConnectionConfig() { try { - new VcnConfig.Builder().build(); + new VcnConfig.Builder(mContext).build(); fail("Expected exception due to no VcnGatewayConnectionConfigs provided"); } catch (IllegalArgumentException e) { } @@ -58,21 +80,22 @@ public class VcnConfigTest { @Test public void testBuilderAndGetters() { - final VcnConfig config = buildTestConfig(); + final VcnConfig config = buildTestConfig(mContext); + assertEquals(TEST_PACKAGE_NAME, config.getProvisioningPackageName()); assertEquals(GATEWAY_CONNECTION_CONFIGS, config.getGatewayConnectionConfigs()); } @Test public void testPersistableBundle() { - final VcnConfig config = buildTestConfig(); + final VcnConfig config = buildTestConfig(mContext); assertEquals(config, new VcnConfig(config.toPersistableBundle())); } @Test public void testParceling() { - final VcnConfig config = buildTestConfig(); + final VcnConfig config = buildTestConfig(mContext); Parcel parcel = Parcel.obtain(); config.writeToParcel(parcel, 0); diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java index 04091bc05514d..d2caa8f0beeb2 100644 --- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java +++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java @@ -24,6 +24,7 @@ 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.argThat; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; @@ -31,6 +32,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import android.app.AppOpsManager; import android.content.Context; import android.net.ConnectivityManager; import android.net.vcn.VcnConfig; @@ -67,9 +69,20 @@ import java.util.UUID; @RunWith(AndroidJUnit4.class) @SmallTest public class VcnManagementServiceTest { + private static final String TEST_PACKAGE_NAME = + VcnManagementServiceTest.class.getPackage().getName(); 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 VcnConfig TEST_VCN_CONFIG; + private static final int TEST_UID = Process.FIRST_APPLICATION_UID; + + static { + final Context mockConfigContext = mock(Context.class); + doReturn(TEST_PACKAGE_NAME).when(mockConfigContext).getOpPackageName(); + + TEST_VCN_CONFIG = VcnConfigTest.buildTestConfig(mockConfigContext); + } + private static final Map TEST_VCN_CONFIG_MAP = Collections.unmodifiableMap(Collections.singletonMap(TEST_UUID_1, TEST_VCN_CONFIG)); @@ -104,6 +117,7 @@ public class VcnManagementServiceTest { private final ConnectivityManager mConnMgr = mock(ConnectivityManager.class); private final TelephonyManager mTelMgr = mock(TelephonyManager.class); private final SubscriptionManager mSubMgr = mock(SubscriptionManager.class); + private final AppOpsManager mAppOpsMgr = mock(AppOpsManager.class); private final VcnContext mVcnContext = mock(VcnContext.class); private final PersistableBundleUtils.LockingReadWriteHelper mConfigReadWriteHelper = mock(PersistableBundleUtils.LockingReadWriteHelper.class); @@ -117,9 +131,12 @@ public class VcnManagementServiceTest { setupSystemService(mTelMgr, Context.TELEPHONY_SERVICE, TelephonyManager.class); setupSystemService( mSubMgr, Context.TELEPHONY_SUBSCRIPTION_SERVICE, SubscriptionManager.class); + setupSystemService(mAppOpsMgr, Context.APP_OPS_SERVICE, AppOpsManager.class); + + doReturn(TEST_PACKAGE_NAME).when(mMockContext).getOpPackageName(); doReturn(mTestLooper.getLooper()).when(mMockDeps).getLooper(); - doReturn(Process.FIRST_APPLICATION_UID).when(mMockDeps).getBinderCallingUid(); + doReturn(TEST_UID).when(mMockDeps).getBinderCallingUid(); doReturn(mVcnContext) .when(mMockDeps) .newVcnContext( @@ -213,6 +230,16 @@ public class VcnManagementServiceTest { final TelephonySubscriptionSnapshot snapshot = mock(TelephonySubscriptionSnapshot.class); doReturn(activeSubscriptionGroups).when(snapshot).getActiveSubscriptionGroups(); + final Set privilegedPackages = + (activeSubscriptionGroups == null || activeSubscriptionGroups.isEmpty()) + ? Collections.emptySet() + : Collections.singleton(TEST_PACKAGE_NAME); + doReturn(true) + .when(snapshot) + .packageHasPermissionsForSubscriptionGroup( + argThat(val -> activeSubscriptionGroups.contains(val)), + eq(TEST_PACKAGE_NAME)); + final TelephonySubscriptionTrackerCallback cb = getTelephonySubscriptionTrackerCallback(); cb.onNewSnapshot(snapshot); } @@ -227,7 +254,7 @@ public class VcnManagementServiceTest { } private Vcn startAndGetVcnInstance(ParcelUuid uuid) { - mVcnMgmtSvc.setVcnConfig(uuid, TEST_VCN_CONFIG); + mVcnMgmtSvc.setVcnConfig(uuid, TEST_VCN_CONFIG, TEST_PACKAGE_NAME); return mVcnMgmtSvc.getAllVcns().get(uuid); } @@ -302,7 +329,7 @@ public class VcnManagementServiceTest { doReturn(Process.SYSTEM_UID).when(mMockDeps).getBinderCallingUid(); try { - mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, VcnConfigTest.buildTestConfig()); + mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, TEST_VCN_CONFIG, TEST_PACKAGE_NAME); fail("Expected IllegalStateException exception for system server"); } catch (IllegalStateException expected) { } @@ -310,12 +337,12 @@ public class VcnManagementServiceTest { @Test public void testSetVcnConfigRequiresSystemUser() throws Exception { - doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, Process.FIRST_APPLICATION_UID)) + doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, TEST_UID)) .when(mMockDeps) .getBinderCallingUid(); try { - mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, VcnConfigTest.buildTestConfig()); + mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, TEST_VCN_CONFIG, TEST_PACKAGE_NAME); fail("Expected security exception for non system user"); } catch (SecurityException expected) { } @@ -326,16 +353,25 @@ public class VcnManagementServiceTest { setupMockedCarrierPrivilege(false); try { - mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, VcnConfigTest.buildTestConfig()); + mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, TEST_VCN_CONFIG, TEST_PACKAGE_NAME); fail("Expected security exception for missing carrier privileges"); } catch (SecurityException expected) { } } + @Test + public void testSetVcnConfigMismatchedPackages() throws Exception { + try { + mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, TEST_VCN_CONFIG, "IncorrectPackage"); + fail("Expected exception due to mismatched packages in config and method call"); + } catch (IllegalArgumentException expected) { + } + } + @Test public void testSetVcnConfig() throws Exception { // Use a different UUID to simulate a new VCN config. - mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG); + mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG, TEST_PACKAGE_NAME); assertEquals(TEST_VCN_CONFIG, mVcnMgmtSvc.getConfigs().get(TEST_UUID_2)); verify(mConfigReadWriteHelper).writeToDisk(any(PersistableBundle.class)); } @@ -353,7 +389,7 @@ public class VcnManagementServiceTest { @Test public void testClearVcnConfigRequiresSystemUser() throws Exception { - doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, Process.FIRST_APPLICATION_UID)) + doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, TEST_UID)) .when(mMockDeps) .getBinderCallingUid(); @@ -385,7 +421,7 @@ public class VcnManagementServiceTest { @Test public void testSetVcnConfigClearVcnConfigStartsUpdatesAndTeardsDownVcns() throws Exception { // Use a different UUID to simulate a new VCN config. - mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG); + mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG, TEST_PACKAGE_NAME); final Map vcnInstances = mVcnMgmtSvc.getAllVcns(); final Vcn vcnInstance = vcnInstances.get(TEST_UUID_2); assertEquals(1, vcnInstances.size()); @@ -396,7 +432,7 @@ public class VcnManagementServiceTest { verify(mMockDeps).newVcn(eq(mVcnContext), eq(TEST_UUID_2), eq(TEST_VCN_CONFIG)); // Verify Vcn is updated if it was previously started - mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG); + mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG, TEST_PACKAGE_NAME); verify(vcnInstance).updateConfig(TEST_VCN_CONFIG); // Verify Vcn is stopped if it was already started diff --git a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java index 17b8f64a13fa9..528f240b99126 100644 --- a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java +++ b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java @@ -30,6 +30,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; @@ -37,6 +38,10 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; +import static java.util.Collections.singletonMap; + import android.annotation.NonNull; import android.content.Context; import android.content.Intent; @@ -49,6 +54,7 @@ import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener; +import android.telephony.TelephonyManager; import android.util.ArraySet; import androidx.test.filters.SmallTest; @@ -63,6 +69,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -71,12 +78,16 @@ import java.util.UUID; @RunWith(AndroidJUnit4.class) @SmallTest public class TelephonySubscriptionTrackerTest { + private static final String PACKAGE_NAME = + TelephonySubscriptionTrackerTest.class.getPackage().getName(); private static final ParcelUuid TEST_PARCEL_UUID = new ParcelUuid(UUID.randomUUID()); private static final int TEST_SIM_SLOT_INDEX = 1; private static final int TEST_SUBSCRIPTION_ID_1 = 2; private static final SubscriptionInfo TEST_SUBINFO_1 = mock(SubscriptionInfo.class); private static final int TEST_SUBSCRIPTION_ID_2 = 3; private static final SubscriptionInfo TEST_SUBINFO_2 = mock(SubscriptionInfo.class); + private static final Map> TEST_PRIVILEGED_PACKAGES = + Collections.singletonMap(TEST_PARCEL_UUID, Collections.singleton(PACKAGE_NAME)); private static final Map TEST_SUBID_TO_GROUP_MAP; static { @@ -91,6 +102,7 @@ public class TelephonySubscriptionTrackerTest { @NonNull private final Handler mHandler; @NonNull private final TelephonySubscriptionTracker.Dependencies mDeps; + @NonNull private final TelephonyManager mTelephonyManager; @NonNull private final SubscriptionManager mSubscriptionManager; @NonNull private final CarrierConfigManager mCarrierConfigManager; @@ -103,9 +115,15 @@ public class TelephonySubscriptionTrackerTest { mHandler = new Handler(mTestLooper.getLooper()); mDeps = mock(TelephonySubscriptionTracker.Dependencies.class); + mTelephonyManager = mock(TelephonyManager.class); mSubscriptionManager = mock(SubscriptionManager.class); mCarrierConfigManager = mock(CarrierConfigManager.class); + doReturn(Context.TELEPHONY_SERVICE) + .when(mContext) + .getSystemServiceName(TelephonyManager.class); + doReturn(mTelephonyManager).when(mContext).getSystemService(Context.TELEPHONY_SERVICE); + doReturn(Context.TELEPHONY_SUBSCRIPTION_SERVICE) .when(mContext) .getSystemServiceName(SubscriptionManager.class); @@ -140,6 +158,9 @@ public class TelephonySubscriptionTrackerTest { doReturn(Arrays.asList(TEST_SUBINFO_1, TEST_SUBINFO_2)) .when(mSubscriptionManager) .getAllSubscriptionInfoList(); + + doReturn(mTelephonyManager).when(mTelephonyManager).createForSubscriptionId(anyInt()); + setPrivilegedPackagesForMock(Collections.singletonList(PACKAGE_NAME)); } private IntentFilter getIntentFilter() { @@ -167,13 +188,15 @@ public class TelephonySubscriptionTrackerTest { return intent; } - private TelephonySubscriptionSnapshot buildExpectedSnapshot(Set activeSubGroups) { - return buildExpectedSnapshot(TEST_SUBID_TO_GROUP_MAP, activeSubGroups); + private TelephonySubscriptionSnapshot buildExpectedSnapshot( + Map> privilegedPackages) { + return buildExpectedSnapshot(TEST_SUBID_TO_GROUP_MAP, privilegedPackages); } private TelephonySubscriptionSnapshot buildExpectedSnapshot( - Map subIdToGroupMap, Set activeSubGroups) { - return new TelephonySubscriptionSnapshot(subIdToGroupMap, activeSubGroups); + Map subIdToGroupMap, + Map> privilegedPackages) { + return new TelephonySubscriptionSnapshot(subIdToGroupMap, privilegedPackages); } private void verifyNoActiveSubscriptions() { @@ -186,6 +209,10 @@ public class TelephonySubscriptionTrackerTest { Collections.singletonMap(TEST_SIM_SLOT_INDEX, TEST_SUBSCRIPTION_ID_1)); } + private void setPrivilegedPackagesForMock(@NonNull List privilegedPackages) { + doReturn(privilegedPackages).when(mTelephonyManager).getPackagesWithCarrierPrivileges(); + } + @Test public void testRegister() throws Exception { verify(mContext) @@ -223,15 +250,30 @@ public class TelephonySubscriptionTrackerTest { } @Test - public void testOnSubscriptionsChangedFired_WithReadySubIds() throws Exception { + public void testOnSubscriptionsChangedFired_WithReadySubidsNoPrivilegedPackages() + throws Exception { + setupReadySubIds(); + setPrivilegedPackagesForMock(Collections.emptyList()); + + final OnSubscriptionsChangedListener listener = getOnSubscriptionsChangedListener(); + listener.onSubscriptionsChanged(); + mTestLooper.dispatchAll(); + + final Map> privilegedPackages = + Collections.singletonMap(TEST_PARCEL_UUID, new ArraySet<>()); + verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(privilegedPackages))); + } + + @Test + public void testOnSubscriptionsChangedFired_WithReadySubidsAndPrivilegedPackages() + throws Exception { setupReadySubIds(); final OnSubscriptionsChangedListener listener = getOnSubscriptionsChangedListener(); listener.onSubscriptionsChanged(); mTestLooper.dispatchAll(); - final Set activeSubGroups = Collections.singleton(TEST_PARCEL_UUID); - verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(activeSubGroups))); + verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(TEST_PRIVILEGED_PACKAGES))); } @Test @@ -239,8 +281,7 @@ public class TelephonySubscriptionTrackerTest { mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(true)); mTestLooper.dispatchAll(); - final Set activeSubGroups = Collections.singleton(TEST_PARCEL_UUID); - verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(activeSubGroups))); + verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(TEST_PRIVILEGED_PACKAGES))); } @Test @@ -253,8 +294,7 @@ public class TelephonySubscriptionTrackerTest { mTestLooper.dispatchAll(); // Expect an empty snapshot - verify(mCallback).onNewSnapshot( - eq(buildExpectedSnapshot(Collections.emptyMap(), Collections.emptySet()))); + verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(emptyMap(), emptyMap()))); } @Test @@ -281,41 +321,57 @@ public class TelephonySubscriptionTrackerTest { @Test public void testSubscriptionsClearedAfterValidTriggersCallbacks() throws Exception { - final Set activeSubGroups = Collections.singleton(TEST_PARCEL_UUID); - mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(true)); mTestLooper.dispatchAll(); - verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(activeSubGroups))); + verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(TEST_PRIVILEGED_PACKAGES))); assertNotNull( mTelephonySubscriptionTracker.getReadySubIdsBySlotId().get(TEST_SIM_SLOT_INDEX)); doReturn(Collections.emptyList()).when(mSubscriptionManager).getAllSubscriptionInfoList(); mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(true)); mTestLooper.dispatchAll(); - verify(mCallback).onNewSnapshot( - eq(buildExpectedSnapshot(Collections.emptyMap(), Collections.emptySet()))); + verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(emptyMap(), emptyMap()))); } @Test public void testSlotClearedAfterValidTriggersCallbacks() throws Exception { - final Set activeSubGroups = Collections.singleton(TEST_PARCEL_UUID); - mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(true)); mTestLooper.dispatchAll(); - verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(activeSubGroups))); + verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(TEST_PRIVILEGED_PACKAGES))); assertNotNull( mTelephonySubscriptionTracker.getReadySubIdsBySlotId().get(TEST_SIM_SLOT_INDEX)); mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(false)); mTestLooper.dispatchAll(); - verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(Collections.emptySet()))); + verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(emptyMap()))); assertNull(mTelephonySubscriptionTracker.getReadySubIdsBySlotId().get(TEST_SIM_SLOT_INDEX)); } + @Test + public void testChangingPrivilegedPackagesAfterValidTriggersCallbacks() throws Exception { + setupReadySubIds(); + + // Setup initial "valid" state + final OnSubscriptionsChangedListener listener = getOnSubscriptionsChangedListener(); + listener.onSubscriptionsChanged(); + mTestLooper.dispatchAll(); + + verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(TEST_PRIVILEGED_PACKAGES))); + + // Simulate a loss of carrier privileges + setPrivilegedPackagesForMock(Collections.emptyList()); + listener.onSubscriptionsChanged(); + mTestLooper.dispatchAll(); + + verify(mCallback) + .onNewSnapshot( + eq(buildExpectedSnapshot(singletonMap(TEST_PARCEL_UUID, emptySet())))); + } + @Test public void testTelephonySubscriptionSnapshotGetGroupForSubId() throws Exception { final TelephonySubscriptionSnapshot snapshot = - new TelephonySubscriptionSnapshot(TEST_SUBID_TO_GROUP_MAP, Collections.emptySet()); + new TelephonySubscriptionSnapshot(TEST_SUBID_TO_GROUP_MAP, emptyMap()); assertEquals(TEST_PARCEL_UUID, snapshot.getGroupForSubId(TEST_SUBSCRIPTION_ID_1)); assertEquals(TEST_PARCEL_UUID, snapshot.getGroupForSubId(TEST_SUBSCRIPTION_ID_2)); @@ -324,7 +380,7 @@ public class TelephonySubscriptionTrackerTest { @Test public void testTelephonySubscriptionSnapshotGetAllSubIdsInGroup() throws Exception { final TelephonySubscriptionSnapshot snapshot = - new TelephonySubscriptionSnapshot(TEST_SUBID_TO_GROUP_MAP, Collections.emptySet()); + new TelephonySubscriptionSnapshot(TEST_SUBID_TO_GROUP_MAP, emptyMap()); assertEquals( new ArraySet<>(Arrays.asList(TEST_SUBSCRIPTION_ID_1, TEST_SUBSCRIPTION_ID_2)), From db8cf2a13d164a7539e8dcf3d15a18deaa060e58 Mon Sep 17 00:00:00 2001 From: Benedict Wong Date: Tue, 1 Dec 2020 17:39:50 -0800 Subject: [PATCH 4/4] Pull VcnNetworkProvider out into a separate class This change makes the VcnNetworkProvider a separate class, and caches all NetworkRequest(s) to ensure that VcnTunnel(s) satisfy all requests that they can accept. Bug: 163431879 Test: atest FrameworksVcnTests Change-Id: I3b7695628d0153a33f7e7f40d839df1463d58b07 --- .../android/server/VcnManagementService.java | 26 +-- .../core/java/com/android/server/vcn/Vcn.java | 149 ++++++++++++++++-- .../com/android/server/vcn/VcnContext.java | 2 - .../server/vcn/VcnGatewayConnection.java | 4 +- .../server/vcn/VcnNetworkProvider.java | 108 +++++++++++++ .../server/VcnManagementServiceTest.java | 7 +- .../server/vcn/VcnNetworkProviderTest.java | 121 ++++++++++++++ 7 files changed, 377 insertions(+), 40 deletions(-) create mode 100644 services/core/java/com/android/server/vcn/VcnNetworkProvider.java create mode 100644 tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java index 77e8e96a515cc..c191a78aad0e2 100644 --- a/services/core/java/com/android/server/VcnManagementService.java +++ b/services/core/java/com/android/server/VcnManagementService.java @@ -25,8 +25,6 @@ import android.annotation.NonNull; import android.app.AppOpsManager; import android.content.Context; import android.net.ConnectivityManager; -import android.net.NetworkProvider; -import android.net.NetworkRequest; import android.net.vcn.IVcnManagementService; import android.net.vcn.VcnConfig; import android.os.Binder; @@ -50,6 +48,7 @@ import com.android.internal.annotations.VisibleForTesting.Visibility; import com.android.server.vcn.TelephonySubscriptionTracker; import com.android.server.vcn.Vcn; import com.android.server.vcn.VcnContext; +import com.android.server.vcn.VcnNetworkProvider; import com.android.server.vcn.util.PersistableBundleUtils; import java.io.IOException; @@ -390,6 +389,9 @@ public class VcnManagementService extends IVcnManagementService.Stub { private void startVcnLocked(@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config) { Slog.v(TAG, "Starting VCN config for subGrp: " + subscriptionGroup); + // TODO(b/176939047): Support multiple VCNs active at the same time, or limit to one active + // VCN. + final Vcn newInstance = mDeps.newVcn(mVcnContext, subscriptionGroup, config); mVcns.put(subscriptionGroup, newInstance); } @@ -493,24 +495,4 @@ public class VcnManagementService extends IVcnManagementService.Stub { return Collections.unmodifiableMap(mVcns); } } - - /** - * Network provider for VCN networks. - * - * @hide - */ - public class VcnNetworkProvider extends NetworkProvider { - VcnNetworkProvider(Context context, Looper looper) { - super(context, looper, VcnNetworkProvider.class.getSimpleName()); - } - - @Override - public void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId) { - synchronized (mLock) { - for (Vcn instance : mVcns.values()) { - instance.onNetworkRequested(request, score, providerId); - } - } - } - } } diff --git a/services/core/java/com/android/server/vcn/Vcn.java b/services/core/java/com/android/server/vcn/Vcn.java index 493761be62342..9d21b9241c0d4 100644 --- a/services/core/java/com/android/server/vcn/Vcn.java +++ b/services/core/java/com/android/server/vcn/Vcn.java @@ -16,32 +16,69 @@ package com.android.server.vcn; + import android.annotation.NonNull; +import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.net.vcn.VcnConfig; +import android.net.vcn.VcnGatewayConnectionConfig; import android.os.Handler; import android.os.Message; import android.os.ParcelUuid; +import android.util.Slog; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; /** * Represents an single instance of a VCN. * - *

Each Vcn instance manages all tunnels for a given subscription group, including per-capability - * networks, network selection, and multi-homing. + *

Each Vcn instance manages all {@link VcnGatewayConnection}(s) for a given subscription group, + * including per-capability networks, network selection, and multi-homing. * * @hide */ public class Vcn extends Handler { private static final String TAG = Vcn.class.getSimpleName(); + private static final int MSG_EVENT_BASE = 0; + private static final int MSG_CMD_BASE = 100; + + /** + * A carrier app updated the configuration. + * + *

Triggers update of config, re-evaluating all active and underlying networks. + * + * @param obj VcnConfig + */ + private static final int MSG_EVENT_CONFIG_UPDATED = MSG_EVENT_BASE; + + /** + * A NetworkRequest was added or updated. + * + *

Triggers an evaluation of all active networks, bringing up a new one if necessary. + * + * @param obj NetworkRequest + */ + private static final int MSG_EVENT_NETWORK_REQUESTED = MSG_EVENT_BASE + 1; + + /** Triggers an immediate teardown of the entire Vcn, including GatewayConnections. */ + private static final int MSG_CMD_TEARDOWN = MSG_CMD_BASE; + @NonNull private final VcnContext mVcnContext; @NonNull private final ParcelUuid mSubscriptionGroup; @NonNull private final Dependencies mDeps; + @NonNull private final VcnNetworkRequestListener mRequestListener; + + @NonNull + private final Map mVcnGatewayConnections = + new HashMap<>(); @NonNull private VcnConfig mConfig; + private boolean mIsRunning = true; + public Vcn( @NonNull VcnContext vcnContext, @NonNull ParcelUuid subscriptionGroup, @@ -58,31 +95,123 @@ public class Vcn extends Handler { mVcnContext = vcnContext; mSubscriptionGroup = Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup"); mDeps = Objects.requireNonNull(deps, "Missing deps"); + mRequestListener = new VcnNetworkRequestListener(); mConfig = Objects.requireNonNull(config, "Missing config"); + + // Register to receive cached and future NetworkRequests + mVcnContext.getVcnNetworkProvider().registerListener(mRequestListener); } /** Asynchronously updates the configuration and triggers a re-evaluation of Networks */ public void updateConfig(@NonNull VcnConfig config) { Objects.requireNonNull(config, "Missing config"); - // TODO: Proxy to handler, and make config there. + + sendMessage(obtainMessage(MSG_EVENT_CONFIG_UPDATED, config)); } - /** Asynchronously tears down this Vcn instance, along with all tunnels and Networks */ + /** Asynchronously tears down this Vcn instance, including VcnGatewayConnection(s) */ public void teardownAsynchronously() { - // TODO: Proxy to handler, and teardown there. + sendMessageAtFrontOfQueue(obtainMessage(MSG_CMD_TEARDOWN)); } - /** Notifies this Vcn instance of a new NetworkRequest */ - public void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId) { - Objects.requireNonNull(request, "Missing request"); + private class VcnNetworkRequestListener implements VcnNetworkProvider.NetworkRequestListener { + @Override + public void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId) { + Objects.requireNonNull(request, "Missing request"); - // TODO: Proxy to handler, and handle there. + sendMessage(obtainMessage(MSG_EVENT_NETWORK_REQUESTED, score, providerId, request)); + } } @Override public void handleMessage(@NonNull Message msg) { - // TODO: Do something + if (!mIsRunning) { + return; + } + + switch (msg.what) { + case MSG_EVENT_CONFIG_UPDATED: + handleConfigUpdated((VcnConfig) msg.obj); + break; + case MSG_EVENT_NETWORK_REQUESTED: + handleNetworkRequested((NetworkRequest) msg.obj, msg.arg1, msg.arg2); + break; + case MSG_CMD_TEARDOWN: + handleTeardown(); + break; + default: + Slog.wtf(getLogTag(), "Unknown msg.what: " + msg.what); + } + } + + private void handleConfigUpdated(@NonNull VcnConfig config) { + // TODO: Add a dump function in VcnConfig that omits PII. Until then, use hashCode() + Slog.v(getLogTag(), String.format("Config updated: config = %s", config.hashCode())); + + mConfig = config; + + // TODO: Reevaluate active VcnGatewayConnection(s) + } + + private void handleTeardown() { + mVcnContext.getVcnNetworkProvider().unregisterListener(mRequestListener); + + for (VcnGatewayConnection gatewayConnection : mVcnGatewayConnections.values()) { + gatewayConnection.teardownAsynchronously(); + } + + mIsRunning = false; + } + + private void handleNetworkRequested( + @NonNull NetworkRequest request, int score, int providerId) { + if (score > getNetworkScore()) { + Slog.v(getLogTag(), + "Request " + request.requestId + " already satisfied by higher-scoring (" + + score + ") network from provider " + providerId); + return; + } + + // If preexisting VcnGatewayConnection(s) satisfy request, return + for (VcnGatewayConnectionConfig gatewayConnectionConfig : mVcnGatewayConnections.keySet()) { + if (requestSatisfiedByGatewayConnectionConfig(request, gatewayConnectionConfig)) { + Slog.v(getLogTag(), + "Request " + request.requestId + + " satisfied by existing VcnGatewayConnection"); + return; + } + } + + // If any supported (but not running) VcnGatewayConnection(s) can satisfy request, bring it + // up + for (VcnGatewayConnectionConfig gatewayConnectionConfig : + mConfig.getGatewayConnectionConfigs()) { + if (requestSatisfiedByGatewayConnectionConfig(request, gatewayConnectionConfig)) { + Slog.v( + getLogTag(), + "Bringing up new VcnGatewayConnection for request " + request.requestId); + + final VcnGatewayConnection vcnGatewayConnection = + new VcnGatewayConnection( + mVcnContext, mSubscriptionGroup, gatewayConnectionConfig); + mVcnGatewayConnections.put(gatewayConnectionConfig, vcnGatewayConnection); + } + } + } + + private boolean requestSatisfiedByGatewayConnectionConfig( + @NonNull NetworkRequest request, @NonNull VcnGatewayConnectionConfig config) { + final NetworkCapabilities configCaps = new NetworkCapabilities(); + for (int cap : config.getAllExposedCapabilities()) { + configCaps.addCapability(cap); + } + + return request.networkCapabilities.satisfiedByNetworkCapabilities(configCaps); + } + + private String getLogTag() { + return String.format("%s [%d]", TAG, mSubscriptionGroup.hashCode()); } /** Retrieves the network score for a VCN Network */ diff --git a/services/core/java/com/android/server/vcn/VcnContext.java b/services/core/java/com/android/server/vcn/VcnContext.java index 8ab52931cae30..dba59bdbee7d7 100644 --- a/services/core/java/com/android/server/vcn/VcnContext.java +++ b/services/core/java/com/android/server/vcn/VcnContext.java @@ -20,8 +20,6 @@ import android.annotation.NonNull; import android.content.Context; import android.os.Looper; -import com.android.server.VcnManagementService.VcnNetworkProvider; - import java.util.Objects; /** diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java index 49c9b3297c778..7ea8e04580f71 100644 --- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java +++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java @@ -65,8 +65,8 @@ public class VcnGatewayConnection extends Handler implements UnderlyingNetworkTr mDeps.newUnderlyingNetworkTracker(mVcnContext, subscriptionGroup, this); } - /** Tears down this GatewayConnection, and any resources used */ - public void teardown() { + /** Asynchronously tears down this GatewayConnection, and any resources used */ + public void teardownAsynchronously() { mUnderlyingNetworkTracker.teardown(); } diff --git a/services/core/java/com/android/server/vcn/VcnNetworkProvider.java b/services/core/java/com/android/server/vcn/VcnNetworkProvider.java new file mode 100644 index 0000000000000..7f5b23c9db6f0 --- /dev/null +++ b/services/core/java/com/android/server/vcn/VcnNetworkProvider.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.vcn; + +import android.annotation.NonNull; +import android.content.Context; +import android.net.NetworkProvider; +import android.net.NetworkRequest; +import android.os.Looper; +import android.util.ArraySet; +import android.util.Slog; +import android.util.SparseArray; + +import java.util.Objects; +import java.util.Set; + +/** + * VCN Network Provider routes NetworkRequests to listeners to bring up tunnels as needed. + * + *

The VcnNetworkProvider provides a caching layer to ensure that all listeners receive all + * active NetworkRequest(s), including ones that were filed prior to listener registration. + * + * @hide + */ +public class VcnNetworkProvider extends NetworkProvider { + private static final String TAG = VcnNetworkProvider.class.getSimpleName(); + + private final Set mListeners = new ArraySet<>(); + private final SparseArray mRequests = new SparseArray<>(); + + public VcnNetworkProvider(Context context, Looper looper) { + super(context, looper, VcnNetworkProvider.class.getSimpleName()); + } + + // Package-private + void registerListener(@NonNull NetworkRequestListener listener) { + mListeners.add(listener); + + // Send listener all cached requests + for (int i = 0; i < mRequests.size(); i++) { + notifyListenerForEvent(listener, mRequests.valueAt(i)); + } + } + + // Package-private + void unregisterListener(@NonNull NetworkRequestListener listener) { + mListeners.remove(listener); + } + + private void notifyListenerForEvent( + @NonNull NetworkRequestListener listener, @NonNull NetworkRequestEntry entry) { + listener.onNetworkRequested(entry.mRequest, entry.mScore, entry.mProviderId); + } + + @Override + public void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId) { + Slog.v( + TAG, + String.format( + "Network requested: Request = %s, score = %d, providerId = %d", + request, score, providerId)); + + final NetworkRequestEntry entry = new NetworkRequestEntry(request, score, providerId); + mRequests.put(request.requestId, entry); + + // TODO(b/176939047): Intelligently route requests to prioritized VcnInstances (based on + // Default Data Sub, or similar) + for (NetworkRequestListener listener : mListeners) { + notifyListenerForEvent(listener, entry); + } + } + + @Override + public void onNetworkRequestWithdrawn(@NonNull NetworkRequest request) { + mRequests.remove(request.requestId); + } + + private static class NetworkRequestEntry { + public final NetworkRequest mRequest; + public final int mScore; + public final int mProviderId; + + private NetworkRequestEntry(@NonNull NetworkRequest request, int score, int providerId) { + mRequest = Objects.requireNonNull(request, "Missing request"); + mScore = score; + mProviderId = providerId; + } + } + + // package-private + interface NetworkRequestListener { + void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId); + } +} diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java index d2caa8f0beeb2..696110f018693 100644 --- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java +++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java @@ -49,10 +49,10 @@ import android.telephony.TelephonyManager; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; -import com.android.server.VcnManagementService.VcnNetworkProvider; import com.android.server.vcn.TelephonySubscriptionTracker; import com.android.server.vcn.Vcn; import com.android.server.vcn.VcnContext; +import com.android.server.vcn.VcnNetworkProvider; import com.android.server.vcn.util.PersistableBundleUtils; import org.junit.Test; @@ -191,8 +191,7 @@ public class VcnManagementServiceTest { public void testSystemReady() throws Exception { mVcnMgmtSvc.systemReady(); - verify(mConnMgr) - .registerNetworkProvider(any(VcnManagementService.VcnNetworkProvider.class)); + verify(mConnMgr).registerNetworkProvider(any(VcnNetworkProvider.class)); verify(mSubscriptionTracker).register(); } @@ -309,7 +308,7 @@ public class VcnManagementServiceTest { // Config cleared, SIM reloaded & config re-added right before teardown delay, staring new // vcnInstance. mTestLooper.moveTimeForward( - VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS - 1); + VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS / 2); mTestLooper.dispatchAll(); mVcnMgmtSvc.clearVcnConfig(TEST_UUID_2); final Vcn newInstance = startAndGetVcnInstance(TEST_UUID_2); diff --git a/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java b/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java new file mode 100644 index 0000000000000..c2c6200fd5f99 --- /dev/null +++ b/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.vcn; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.annotation.NonNull; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.test.TestLooper; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.vcn.VcnNetworkProvider.NetworkRequestListener; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +/** Tests for TelephonySubscriptionTracker */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class VcnNetworkProviderTest { + private static final int TEST_SCORE_UNSATISFIED = 0; + private static final int TEST_SCORE_HIGH = 100; + private static final int TEST_PROVIDER_ID = 1; + private static final int TEST_LEGACY_TYPE = ConnectivityManager.TYPE_MOBILE; + private static final NetworkRequest.Type TEST_REQUEST_TYPE = NetworkRequest.Type.REQUEST; + + @NonNull private final Context mContext; + @NonNull private final TestLooper mTestLooper; + + @NonNull private VcnNetworkProvider mVcnNetworkProvider; + @NonNull private NetworkRequestListener mListener; + + public VcnNetworkProviderTest() { + mContext = mock(Context.class); + mTestLooper = new TestLooper(); + } + + @Before + public void setUp() throws Exception { + mVcnNetworkProvider = new VcnNetworkProvider(mContext, mTestLooper.getLooper()); + mListener = mock(NetworkRequestListener.class); + } + + @Test + public void testRequestsPassedToRegisteredListeners() throws Exception { + mVcnNetworkProvider.registerListener(mListener); + + final NetworkRequest request = mock(NetworkRequest.class); + mVcnNetworkProvider.onNetworkRequested(request, TEST_SCORE_UNSATISFIED, TEST_PROVIDER_ID); + verify(mListener).onNetworkRequested(request, TEST_SCORE_UNSATISFIED, TEST_PROVIDER_ID); + } + + @Test + public void testRequestsPassedToRegisteredListeners_satisfiedByHighScoringProvider() + throws Exception { + mVcnNetworkProvider.registerListener(mListener); + + final NetworkRequest request = mock(NetworkRequest.class); + mVcnNetworkProvider.onNetworkRequested(request, TEST_SCORE_HIGH, TEST_PROVIDER_ID); + verify(mListener).onNetworkRequested(request, TEST_SCORE_HIGH, TEST_PROVIDER_ID); + } + + @Test + public void testUnregisterListener() throws Exception { + mVcnNetworkProvider.registerListener(mListener); + mVcnNetworkProvider.unregisterListener(mListener); + + final NetworkRequest request = mock(NetworkRequest.class); + mVcnNetworkProvider.onNetworkRequested(request, TEST_SCORE_UNSATISFIED, TEST_PROVIDER_ID); + verifyNoMoreInteractions(mListener); + } + + @Test + public void testCachedRequestsPassedOnRegister() throws Exception { + final List requests = new ArrayList<>(); + + for (int i = 0; i < 10; i++) { + final NetworkRequest request = + new NetworkRequest( + new NetworkCapabilities(), + TEST_LEGACY_TYPE, + i /* requestId */, + TEST_REQUEST_TYPE); + + requests.add(request); + mVcnNetworkProvider.onNetworkRequested(request, i, i + 1); + } + + mVcnNetworkProvider.registerListener(mListener); + for (int i = 0; i < requests.size(); i++) { + final NetworkRequest request = requests.get(i); + verify(mListener).onNetworkRequested(request, i, i + 1); + } + verifyNoMoreInteractions(mListener); + } +}