diff options
8 files changed, 867 insertions, 28 deletions
diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java index 33beb6a9d188..fa090f59a8b9 100644 --- a/core/java/android/net/vcn/VcnManager.java +++ b/core/java/android/net/vcn/VcnManager.java @@ -67,7 +67,6 @@ import java.util.concurrent.Executor; public class VcnManager { @NonNull private static final String TAG = VcnManager.class.getSimpleName(); - /** @hide */ @VisibleForTesting public static final Map< VcnUnderlyingNetworkPolicyListener, VcnUnderlyingNetworkPolicyListenerBinder> diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java index 8562b0d9cb82..6a72010738db 100644 --- a/services/core/java/com/android/server/VcnManagementService.java +++ b/services/core/java/com/android/server/VcnManagementService.java @@ -27,10 +27,12 @@ import android.content.Context; import android.net.ConnectivityManager; import android.net.LinkProperties; import android.net.NetworkCapabilities; +import android.net.TelephonyNetworkSpecifier; import android.net.vcn.IVcnManagementService; import android.net.vcn.IVcnUnderlyingNetworkPolicyListener; import android.net.vcn.VcnConfig; import android.net.vcn.VcnUnderlyingNetworkPolicy; +import android.net.wifi.WifiInfo; import android.os.Binder; import android.os.Handler; import android.os.HandlerThread; @@ -291,6 +293,12 @@ public class VcnManagementService extends IVcnManagementService.Stub { @NonNull VcnConfig config) { return new Vcn(vcnContext, subscriptionGroup, config); } + + /** Gets the subId indicated by the given {@link WifiInfo}. */ + public int getSubIdForWifiInfo(@NonNull WifiInfo wifiInfo) { + // TODO(b/178501049): use the subId indicated by WifiInfo#getSubscriptionId + return SubscriptionManager.INVALID_SUBSCRIPTION_ID; + } } /** Notifies the VcnManagementService that external dependencies can be set up. */ @@ -582,8 +590,36 @@ public class VcnManagementService extends IVcnManagementService.Stub { "Must have permission NETWORK_FACTORY or be the SystemServer to get underlying" + " Network policies"); - // TODO(b/175914059): implement policy generation once VcnManagementService is able to - // determine policies + // Defensive copy in case this call is in-process and the given NetworkCapabilities mutates + networkCapabilities = new NetworkCapabilities(networkCapabilities); + + int subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) + && networkCapabilities.getNetworkSpecifier() instanceof TelephonyNetworkSpecifier) { + TelephonyNetworkSpecifier telephonyNetworkSpecifier = + (TelephonyNetworkSpecifier) networkCapabilities.getNetworkSpecifier(); + subId = telephonyNetworkSpecifier.getSubscriptionId(); + } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + && networkCapabilities.getTransportInfo() instanceof WifiInfo) { + WifiInfo wifiInfo = (WifiInfo) networkCapabilities.getTransportInfo(); + subId = mDeps.getSubIdForWifiInfo(wifiInfo); + } + + boolean isVcnManagedNetwork = false; + if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + synchronized (mLock) { + ParcelUuid subGroup = mLastSnapshot.getGroupForSubId(subId); + + // TODO(b/178140910): only mark the Network as VCN-managed if not in safe mode + if (mVcns.containsKey(subGroup)) { + isVcnManagedNetwork = true; + } + } + } + if (isVcnManagedNetwork) { + networkCapabilities.removeCapability( + NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED); + } return new VcnUnderlyingNetworkPolicy(false /* isTearDownRequested */, networkCapabilities); } diff --git a/services/core/java/com/android/server/vcn/UnderlyingNetworkTracker.java b/services/core/java/com/android/server/vcn/UnderlyingNetworkTracker.java index 6427ae2dc13c..fd12c2d2ebb8 100644 --- a/services/core/java/com/android/server/vcn/UnderlyingNetworkTracker.java +++ b/services/core/java/com/android/server/vcn/UnderlyingNetworkTracker.java @@ -18,16 +18,28 @@ package com.android.server.vcn; import android.annotation.NonNull; import android.annotation.Nullable; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; +import android.net.NetworkCapabilities.NetCapability; +import android.net.NetworkRequest; +import android.net.TelephonyNetworkSpecifier; import android.os.Handler; import android.os.ParcelUuid; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.util.ArraySet; +import android.util.Slog; +import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting.Visibility; +import java.util.List; import java.util.Objects; +import java.util.Set; /** * Tracks a set of Networks underpinning a VcnGatewayConnection. @@ -38,53 +50,385 @@ import java.util.Objects; * * @hide */ -public class UnderlyingNetworkTracker extends Handler { +public class UnderlyingNetworkTracker { @NonNull private static final String TAG = UnderlyingNetworkTracker.class.getSimpleName(); @NonNull private final VcnContext mVcnContext; @NonNull private final ParcelUuid mSubscriptionGroup; @NonNull private final UnderlyingNetworkTrackerCallback mCb; @NonNull private final Dependencies mDeps; + @NonNull private final Handler mHandler; + @NonNull private final ConnectivityManager mConnectivityManager; + @NonNull private final SubscriptionManager mSubscriptionManager; + + @NonNull private final SparseArray<NetworkCallback> mCellBringupCallbacks = new SparseArray<>(); + @NonNull private final NetworkCallback mWifiBringupCallback = new NetworkBringupCallback(); + @NonNull private final NetworkCallback mRouteSelectionCallback = new RouteSelectionCallback(); + + @NonNull private final Set<Integer> mSubIds = new ArraySet<>(); + + @NonNull private final Set<Integer> mRequiredUnderlyingNetworkCapabilities; + + @Nullable private UnderlyingNetworkRecord mCurrentRecord; + @Nullable private UnderlyingNetworkRecord.Builder mRecordInProgress; public UnderlyingNetworkTracker( @NonNull VcnContext vcnContext, @NonNull ParcelUuid subscriptionGroup, + @NonNull Set<Integer> requiredUnderlyingNetworkCapabilities, @NonNull UnderlyingNetworkTrackerCallback cb) { - this(vcnContext, subscriptionGroup, cb, new Dependencies()); + this( + vcnContext, + subscriptionGroup, + requiredUnderlyingNetworkCapabilities, + cb, + new Dependencies()); } private UnderlyingNetworkTracker( @NonNull VcnContext vcnContext, @NonNull ParcelUuid subscriptionGroup, + @NonNull Set<Integer> requiredUnderlyingNetworkCapabilities, @NonNull UnderlyingNetworkTrackerCallback cb, @NonNull Dependencies deps) { - super(Objects.requireNonNull(vcnContext, "Missing vcnContext").getLooper()); - mVcnContext = vcnContext; + mVcnContext = Objects.requireNonNull(vcnContext, "Missing vcnContext"); mSubscriptionGroup = Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup"); + mRequiredUnderlyingNetworkCapabilities = + Objects.requireNonNull( + requiredUnderlyingNetworkCapabilities, + "Missing requiredUnderlyingNetworkCapabilities"); mCb = Objects.requireNonNull(cb, "Missing cb"); mDeps = Objects.requireNonNull(deps, "Missing deps"); + + mHandler = new Handler(mVcnContext.getLooper()); + + mConnectivityManager = mVcnContext.getContext().getSystemService(ConnectivityManager.class); + mSubscriptionManager = mVcnContext.getContext().getSystemService(SubscriptionManager.class); + + registerNetworkRequests(); + } + + private void registerNetworkRequests() { + // register bringup requests for underlying Networks + mConnectivityManager.requestBackgroundNetwork( + getWifiNetworkRequest(), mHandler, mWifiBringupCallback); + updateSubIdsAndCellularRequests(); + + // register Network-selection request used to decide selected underlying Network + mConnectivityManager.requestBackgroundNetwork( + getNetworkRequestBase().build(), mHandler, mRouteSelectionCallback); + } + + private NetworkRequest getWifiNetworkRequest() { + return getNetworkRequestBase().addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build(); + } + + private NetworkRequest getCellNetworkRequestForSubId(int subId) { + return getNetworkRequestBase() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .setNetworkSpecifier(new TelephonyNetworkSpecifier(subId)) + .build(); + } + + private NetworkRequest.Builder getNetworkRequestBase() { + NetworkRequest.Builder requestBase = new NetworkRequest.Builder(); + for (@NetCapability int capability : mRequiredUnderlyingNetworkCapabilities) { + requestBase.addCapability(capability); + } + + return requestBase + .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED) + .addUnwantedCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED); + } + + /** + * Update the current subIds and Cellular bringup requests for this UnderlyingNetworkTracker. + */ + private void updateSubIdsAndCellularRequests() { + mVcnContext.ensureRunningOnLooperThread(); + + Set<Integer> prevSubIds = new ArraySet<>(mSubIds); + mSubIds.clear(); + + // Ensure NetworkRequests filed for all current subIds in mSubscriptionGroup + // STOPSHIP: b/177364490 use TelephonySubscriptionSnapshot to avoid querying Telephony + List<SubscriptionInfo> subInfos = + mSubscriptionManager.getSubscriptionsInGroup(mSubscriptionGroup); + + for (SubscriptionInfo subInfo : subInfos) { + final int subId = subInfo.getSubscriptionId(); + mSubIds.add(subId); + + if (!mCellBringupCallbacks.contains(subId)) { + final NetworkBringupCallback cb = new NetworkBringupCallback(); + mCellBringupCallbacks.put(subId, cb); + + mConnectivityManager.requestBackgroundNetwork( + getCellNetworkRequestForSubId(subId), mHandler, cb); + } + } + + // unregister all NetworkCallbacks for outdated subIds + for (final int subId : prevSubIds) { + if (!mSubIds.contains(subId)) { + final NetworkCallback cb = mCellBringupCallbacks.removeReturnOld(subId); + mConnectivityManager.unregisterNetworkCallback(cb); + } + } } /** Tears down this Tracker, and releases all underlying network requests. */ - public void teardown() {} + public void teardown() { + mVcnContext.ensureRunningOnLooperThread(); + + mConnectivityManager.unregisterNetworkCallback(mWifiBringupCallback); + mConnectivityManager.unregisterNetworkCallback(mRouteSelectionCallback); + + for (final int subId : mSubIds) { + final NetworkCallback cb = mCellBringupCallbacks.removeReturnOld(subId); + mConnectivityManager.unregisterNetworkCallback(cb); + } + mSubIds.clear(); + } + + /** Returns whether the currently selected Network matches the given network. */ + private static boolean isSameNetwork( + @Nullable UnderlyingNetworkRecord.Builder recordInProgress, @NonNull Network network) { + return recordInProgress != null && recordInProgress.getNetwork().equals(network); + } + + /** Notify the Callback if a full UnderlyingNetworkRecord exists. */ + private void maybeNotifyCallback() { + // Only forward this update if a complete record has been received + if (!mRecordInProgress.isValid()) { + return; + } + + // Only forward this update if the updated record differs form the current record + UnderlyingNetworkRecord updatedRecord = mRecordInProgress.build(); + if (!updatedRecord.equals(mCurrentRecord)) { + mCurrentRecord = updatedRecord; + + mCb.onSelectedUnderlyingNetworkChanged(mCurrentRecord); + } + } - /** An record of a single underlying network, caching relevant fields. */ + private void handleNetworkAvailable(@NonNull Network network) { + mVcnContext.ensureRunningOnLooperThread(); + + mRecordInProgress = new UnderlyingNetworkRecord.Builder(network); + } + + private void handleNetworkLost(@NonNull Network network) { + mVcnContext.ensureRunningOnLooperThread(); + + if (!isSameNetwork(mRecordInProgress, network)) { + Slog.wtf(TAG, "Non-underlying Network lost"); + return; + } + + mRecordInProgress = null; + mCurrentRecord = null; + mCb.onSelectedUnderlyingNetworkChanged(null /* underlyingNetworkRecord */); + } + + private void handleCapabilitiesChanged( + @NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) { + mVcnContext.ensureRunningOnLooperThread(); + + if (!isSameNetwork(mRecordInProgress, network)) { + Slog.wtf(TAG, "Invalid update to NetworkCapabilities"); + return; + } + + mRecordInProgress.setNetworkCapabilities(networkCapabilities); + + maybeNotifyCallback(); + } + + private void handleNetworkSuspended(@NonNull Network network, boolean isSuspended) { + mVcnContext.ensureRunningOnLooperThread(); + + if (!isSameNetwork(mRecordInProgress, network)) { + Slog.wtf(TAG, "Invalid update to isSuspended"); + return; + } + + final NetworkCapabilities newCaps = + new NetworkCapabilities(mRecordInProgress.getNetworkCapabilities()); + if (isSuspended) { + newCaps.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED); + } else { + newCaps.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED); + } + + handleCapabilitiesChanged(network, newCaps); + } + + private void handlePropertiesChanged( + @NonNull Network network, @NonNull LinkProperties linkProperties) { + mVcnContext.ensureRunningOnLooperThread(); + + if (!isSameNetwork(mRecordInProgress, network)) { + Slog.wtf(TAG, "Invalid update to LinkProperties"); + return; + } + + mRecordInProgress.setLinkProperties(linkProperties); + + maybeNotifyCallback(); + } + + private void handleNetworkBlocked(@NonNull Network network, boolean isBlocked) { + mVcnContext.ensureRunningOnLooperThread(); + + if (!isSameNetwork(mRecordInProgress, network)) { + Slog.wtf(TAG, "Invalid update to isBlocked"); + return; + } + + mRecordInProgress.setIsBlocked(isBlocked); + + maybeNotifyCallback(); + } + + /** + * NetworkBringupCallback is used to keep background, VCN-managed Networks from being reaped. + * + * <p>NetworkBringupCallback only exists to prevent matching (VCN-managed) Networks from being + * reaped, and no action is taken on any events firing. + */ + @VisibleForTesting + class NetworkBringupCallback extends NetworkCallback {} + + /** + * RouteSelectionCallback is used to select the "best" underlying Network. + * + * <p>The "best" network is determined by ConnectivityService, which is treated as a source of + * truth. + */ + @VisibleForTesting + class RouteSelectionCallback extends NetworkCallback { + @Override + public void onAvailable(@NonNull Network network) { + handleNetworkAvailable(network); + } + + @Override + public void onLost(@NonNull Network network) { + handleNetworkLost(network); + } + + @Override + public void onCapabilitiesChanged( + @NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) { + handleCapabilitiesChanged(network, networkCapabilities); + } + + @Override + public void onNetworkSuspended(@NonNull Network network) { + handleNetworkSuspended(network, true /* isSuspended */); + } + + @Override + public void onNetworkResumed(@NonNull Network network) { + handleNetworkSuspended(network, false /* isSuspended */); + } + + @Override + public void onLinkPropertiesChanged( + @NonNull Network network, @NonNull LinkProperties linkProperties) { + handlePropertiesChanged(network, linkProperties); + } + + @Override + public void onBlockedStatusChanged(@NonNull Network network, boolean isBlocked) { + handleNetworkBlocked(network, isBlocked); + } + } + + /** A record of a single underlying network, caching relevant fields. */ public static class UnderlyingNetworkRecord { @NonNull public final Network network; @NonNull public final NetworkCapabilities networkCapabilities; @NonNull public final LinkProperties linkProperties; - public final boolean blocked; + public final boolean isBlocked; @VisibleForTesting(visibility = Visibility.PRIVATE) UnderlyingNetworkRecord( @NonNull Network network, @NonNull NetworkCapabilities networkCapabilities, @NonNull LinkProperties linkProperties, - boolean blocked) { + boolean isBlocked) { this.network = network; this.networkCapabilities = networkCapabilities; this.linkProperties = linkProperties; - this.blocked = blocked; + this.isBlocked = isBlocked; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof UnderlyingNetworkRecord)) return false; + final UnderlyingNetworkRecord that = (UnderlyingNetworkRecord) o; + + return network.equals(that.network) + && networkCapabilities.equals(that.networkCapabilities) + && linkProperties.equals(that.linkProperties) + && isBlocked == that.isBlocked; + } + + @Override + public int hashCode() { + return Objects.hash(network, networkCapabilities, linkProperties, isBlocked); + } + + /** Builder to incrementally construct an UnderlyingNetworkRecord. */ + private static class Builder { + @NonNull private final Network mNetwork; + + @Nullable private NetworkCapabilities mNetworkCapabilities; + @Nullable private LinkProperties mLinkProperties; + boolean mIsBlocked; + boolean mWasIsBlockedSet; + + private Builder(@NonNull Network network) { + mNetwork = network; + } + + @NonNull + private Network getNetwork() { + return mNetwork; + } + + private void setNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) { + mNetworkCapabilities = networkCapabilities; + } + + @Nullable + private NetworkCapabilities getNetworkCapabilities() { + return mNetworkCapabilities; + } + + private void setLinkProperties(@NonNull LinkProperties linkProperties) { + mLinkProperties = linkProperties; + } + + private void setIsBlocked(boolean isBlocked) { + mIsBlocked = isBlocked; + mWasIsBlockedSet = true; + } + + private boolean isValid() { + return mNetworkCapabilities != null && mLinkProperties != null && mWasIsBlockedSet; + } + + private UnderlyingNetworkRecord build() { + return new UnderlyingNetworkRecord( + mNetwork, mNetworkCapabilities, mLinkProperties, mIsBlocked); + } } } @@ -95,9 +439,10 @@ public class UnderlyingNetworkTracker extends Handler { * * <p>This callback does NOT signal a mobility event. * - * @param underlying The details of the new underlying network + * @param underlyingNetworkRecord The details of the new underlying network */ - void onSelectedUnderlyingNetworkChanged(@Nullable UnderlyingNetworkRecord underlying); + void onSelectedUnderlyingNetworkChanged( + @Nullable UnderlyingNetworkRecord underlyingNetworkRecord); } private static class Dependencies {} diff --git a/services/core/java/com/android/server/vcn/VcnContext.java b/services/core/java/com/android/server/vcn/VcnContext.java index dba59bdbee7d..7399e56b3a95 100644 --- a/services/core/java/com/android/server/vcn/VcnContext.java +++ b/services/core/java/com/android/server/vcn/VcnContext.java @@ -55,4 +55,15 @@ public class VcnContext { public VcnNetworkProvider getVcnNetworkProvider() { return mVcnNetworkProvider; } + + /** + * Verifies that the caller is running on the VcnContext Thread. + * + * @throwsIllegalStateException if the caller is not running on the VcnContext Thread. + */ + public void ensureRunningOnLooperThread() { + if (getLooper().getThread() != Thread.currentThread()) { + throw new IllegalStateException("Not running on VcnMgmtSvc thread"); + } + } } diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java index 3cfa00eb6079..39c96069f9c6 100644 --- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java +++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java @@ -65,6 +65,7 @@ import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.util.Objects; +import java.util.Set; import java.util.concurrent.TimeUnit; /** @@ -476,7 +477,10 @@ public class VcnGatewayConnection extends StateMachine { mUnderlyingNetworkTracker = mDeps.newUnderlyingNetworkTracker( - mVcnContext, subscriptionGroup, mUnderlyingNetworkTrackerCallback); + mVcnContext, + subscriptionGroup, + mConnectionConfig.getAllUnderlyingCapabilities(), + mUnderlyingNetworkTrackerCallback); mIpSecManager = mVcnContext.getContext().getSystemService(IpSecManager.class); IpSecTunnelInterface iface; @@ -1134,8 +1138,10 @@ public class VcnGatewayConnection extends StateMachine { public UnderlyingNetworkTracker newUnderlyingNetworkTracker( VcnContext vcnContext, ParcelUuid subscriptionGroup, + Set<Integer> requiredUnderlyingNetworkCapabilities, UnderlyingNetworkTrackerCallback callback) { - return new UnderlyingNetworkTracker(vcnContext, subscriptionGroup, callback); + return new UnderlyingNetworkTracker( + vcnContext, subscriptionGroup, requiredUnderlyingNetworkCapabilities, callback); } /** Builds a new IkeSession. */ diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java index e26bf19488d0..e7d334ebd490 100644 --- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java +++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java @@ -22,7 +22,6 @@ import static com.android.server.vcn.VcnTestUtils.setupSystemService; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -30,6 +29,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; @@ -39,16 +39,20 @@ 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 static org.mockito.Mockito.when; import android.app.AppOpsManager; import android.content.Context; import android.net.ConnectivityManager; import android.net.LinkProperties; import android.net.NetworkCapabilities; +import android.net.NetworkCapabilities.Transport; +import android.net.TelephonyNetworkSpecifier; import android.net.vcn.IVcnUnderlyingNetworkPolicyListener; import android.net.vcn.VcnConfig; import android.net.vcn.VcnConfigTest; import android.net.vcn.VcnUnderlyingNetworkPolicy; +import android.net.wifi.WifiInfo; import android.os.IBinder; import android.os.ParcelUuid; import android.os.PersistableBundle; @@ -253,7 +257,14 @@ public class VcnManagementServiceTest { verify(mConfigReadWriteHelper).readFromDisk(); } - private void triggerSubscriptionTrackerCallback(Set<ParcelUuid> activeSubscriptionGroups) { + private TelephonySubscriptionSnapshot triggerSubscriptionTrackerCbAndGetSnapshot( + Set<ParcelUuid> activeSubscriptionGroups) { + return triggerSubscriptionTrackerCbAndGetSnapshot( + activeSubscriptionGroups, Collections.emptyMap()); + } + + private TelephonySubscriptionSnapshot triggerSubscriptionTrackerCbAndGetSnapshot( + Set<ParcelUuid> activeSubscriptionGroups, Map<Integer, ParcelUuid> subIdToGroupMap) { final TelephonySubscriptionSnapshot snapshot = mock(TelephonySubscriptionSnapshot.class); doReturn(activeSubscriptionGroups).when(snapshot).getActiveSubscriptionGroups(); @@ -267,8 +278,14 @@ public class VcnManagementServiceTest { argThat(val -> activeSubscriptionGroups.contains(val)), eq(TEST_PACKAGE_NAME)); + doAnswer(invocation -> { + return subIdToGroupMap.get(invocation.getArgument(0)); + }).when(snapshot).getGroupForSubId(anyInt()); + final TelephonySubscriptionTrackerCallback cb = getTelephonySubscriptionTrackerCallback(); cb.onNewSnapshot(snapshot); + + return snapshot; } private TelephonySubscriptionTrackerCallback getTelephonySubscriptionTrackerCallback() { @@ -287,7 +304,7 @@ public class VcnManagementServiceTest { @Test public void testTelephonyNetworkTrackerCallbackStartsInstances() throws Exception { - triggerSubscriptionTrackerCallback(Collections.singleton(TEST_UUID_1)); + triggerSubscriptionTrackerCbAndGetSnapshot(Collections.singleton(TEST_UUID_1)); verify(mMockDeps).newVcn(eq(mVcnContext), eq(TEST_UUID_1), eq(TEST_VCN_CONFIG)); } @@ -296,7 +313,7 @@ public class VcnManagementServiceTest { final TelephonySubscriptionTrackerCallback cb = getTelephonySubscriptionTrackerCallback(); final Vcn vcn = startAndGetVcnInstance(TEST_UUID_2); - triggerSubscriptionTrackerCallback(Collections.emptySet()); + triggerSubscriptionTrackerCbAndGetSnapshot(Collections.emptySet()); // Verify teardown after delay mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS); @@ -311,13 +328,13 @@ public class VcnManagementServiceTest { final Vcn vcn = startAndGetVcnInstance(TEST_UUID_2); // Simulate SIM unloaded - triggerSubscriptionTrackerCallback(Collections.emptySet()); + triggerSubscriptionTrackerCbAndGetSnapshot(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)); + triggerSubscriptionTrackerCbAndGetSnapshot(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); @@ -331,7 +348,7 @@ public class VcnManagementServiceTest { final Vcn oldInstance = startAndGetVcnInstance(TEST_UUID_2); // Simulate SIM unloaded - triggerSubscriptionTrackerCallback(Collections.emptySet()); + triggerSubscriptionTrackerCbAndGetSnapshot(Collections.emptySet()); // Config cleared, SIM reloaded & config re-added right before teardown delay, staring new // vcnInstance. @@ -496,14 +513,73 @@ public class VcnManagementServiceTest { mVcnMgmtSvc.removeVcnUnderlyingNetworkPolicyListener(mMockPolicyListener); } + private void setUpVcnSubscription(int subId, ParcelUuid subGroup) { + mVcnMgmtSvc.setVcnConfig(subGroup, TEST_VCN_CONFIG, TEST_PACKAGE_NAME); + + triggerSubscriptionTrackerCbAndGetSnapshot( + Collections.singleton(subGroup), Collections.singletonMap(subId, subGroup)); + } + + private void verifyMergedNetworkCapabilities( + NetworkCapabilities mergedCapabilities, @Transport int transportType) { + assertTrue(mergedCapabilities.hasTransport(transportType)); + assertFalse( + mergedCapabilities.hasCapability( + NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)); + } + @Test - public void testGetUnderlyingNetworkPolicy() throws Exception { + public void testGetUnderlyingNetworkPolicyTransportCell() throws Exception { + setUpVcnSubscription(TEST_SUBSCRIPTION_ID, TEST_UUID_2); + + NetworkCapabilities nc = + new NetworkCapabilities.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .setNetworkSpecifier(new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID)) + .build(); + + VcnUnderlyingNetworkPolicy policy = + mVcnMgmtSvc.getUnderlyingNetworkPolicy(nc, new LinkProperties()); + + assertFalse(policy.isTeardownRequested()); + verifyMergedNetworkCapabilities( + policy.getMergedNetworkCapabilities(), NetworkCapabilities.TRANSPORT_CELLULAR); + } + + @Test + public void testGetUnderlyingNetworkPolicyTransportWifi() throws Exception { + setUpVcnSubscription(TEST_SUBSCRIPTION_ID, TEST_UUID_2); + + WifiInfo wifiInfo = mock(WifiInfo.class); + when(wifiInfo.makeCopy(anyBoolean())).thenReturn(wifiInfo); + when(mMockDeps.getSubIdForWifiInfo(eq(wifiInfo))).thenReturn(TEST_SUBSCRIPTION_ID); + NetworkCapabilities nc = + new NetworkCapabilities.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .setTransportInfo(wifiInfo) + .build(); + + VcnUnderlyingNetworkPolicy policy = + mVcnMgmtSvc.getUnderlyingNetworkPolicy(nc, new LinkProperties()); + + assertFalse(policy.isTeardownRequested()); + verifyMergedNetworkCapabilities( + policy.getMergedNetworkCapabilities(), NetworkCapabilities.TRANSPORT_WIFI); + } + + @Test + public void testGetUnderlyingNetworkPolicyNonVcnNetwork() throws Exception { + NetworkCapabilities nc = + new NetworkCapabilities.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .setNetworkSpecifier(new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID)) + .build(); + VcnUnderlyingNetworkPolicy policy = - mVcnMgmtSvc.getUnderlyingNetworkPolicy( - new NetworkCapabilities(), new LinkProperties()); + mVcnMgmtSvc.getUnderlyingNetworkPolicy(nc, new LinkProperties()); assertFalse(policy.isTeardownRequested()); - assertNotNull(policy.getMergedNetworkCapabilities()); + assertEquals(nc, policy.getMergedNetworkCapabilities()); } @Test(expected = SecurityException.class) diff --git a/tests/vcn/java/com/android/server/vcn/UnderlyingNetworkTrackerTest.java b/tests/vcn/java/com/android/server/vcn/UnderlyingNetworkTrackerTest.java new file mode 100644 index 000000000000..48e068d14182 --- /dev/null +++ b/tests/vcn/java/com/android/server/vcn/UnderlyingNetworkTrackerTest.java @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2021 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 com.android.server.vcn.VcnTestUtils.setupSystemService; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.TelephonyNetworkSpecifier; +import android.os.ParcelUuid; +import android.os.test.TestLooper; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; + +import com.android.server.vcn.UnderlyingNetworkTracker.NetworkBringupCallback; +import com.android.server.vcn.UnderlyingNetworkTracker.RouteSelectionCallback; +import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkRecord; +import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkTrackerCallback; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +public class UnderlyingNetworkTrackerTest { + private static final ParcelUuid SUB_GROUP = new ParcelUuid(new UUID(0, 0)); + private static final int INITIAL_SUB_ID_1 = 1; + private static final int INITIAL_SUB_ID_2 = 2; + + private static final NetworkCapabilities INITIAL_NETWORK_CAPABILITIES = + new NetworkCapabilities.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) + .build(); + private static final NetworkCapabilities SUSPENDED_NETWORK_CAPABILITIES = + new NetworkCapabilities.Builder(INITIAL_NETWORK_CAPABILITIES) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) + .build(); + private static final NetworkCapabilities UPDATED_NETWORK_CAPABILITIES = + new NetworkCapabilities.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .build(); + + private static final LinkProperties INITIAL_LINK_PROPERTIES = + getLinkPropertiesWithName("initial_iface"); + private static final LinkProperties UPDATED_LINK_PROPERTIES = + getLinkPropertiesWithName("updated_iface"); + + @Mock private Context mContext; + @Mock private VcnNetworkProvider mVcnNetworkProvider; + @Mock private ConnectivityManager mConnectivityManager; + @Mock private SubscriptionManager mSubscriptionManager; + @Mock private UnderlyingNetworkTrackerCallback mNetworkTrackerCb; + @Mock private Network mNetwork; + + @Captor private ArgumentCaptor<RouteSelectionCallback> mRouteSelectionCallbackCaptor; + + private TestLooper mTestLooper; + private VcnContext mVcnContext; + private UnderlyingNetworkTracker mUnderlyingNetworkTracker; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mTestLooper = new TestLooper(); + mVcnContext = spy(new VcnContext(mContext, mTestLooper.getLooper(), mVcnNetworkProvider)); + doNothing().when(mVcnContext).ensureRunningOnLooperThread(); + + setupSystemService( + mContext, + mConnectivityManager, + Context.CONNECTIVITY_SERVICE, + ConnectivityManager.class); + setupSystemService( + mContext, + mSubscriptionManager, + Context.TELEPHONY_SUBSCRIPTION_SERVICE, + SubscriptionManager.class); + + List<SubscriptionInfo> initialSubInfos = + Arrays.asList( + getSubscriptionInfoForSubId(INITIAL_SUB_ID_1), + getSubscriptionInfoForSubId(INITIAL_SUB_ID_2)); + when(mSubscriptionManager.getSubscriptionsInGroup(eq(SUB_GROUP))) + .thenReturn(initialSubInfos); + + mUnderlyingNetworkTracker = + new UnderlyingNetworkTracker( + mVcnContext, + SUB_GROUP, + Collections.singleton(NetworkCapabilities.NET_CAPABILITY_INTERNET), + mNetworkTrackerCb); + } + + private static LinkProperties getLinkPropertiesWithName(String iface) { + LinkProperties linkProperties = new LinkProperties(); + linkProperties.setInterfaceName(iface); + return linkProperties; + } + + private SubscriptionInfo getSubscriptionInfoForSubId(int subId) { + SubscriptionInfo subInfo = mock(SubscriptionInfo.class); + when(subInfo.getSubscriptionId()).thenReturn(subId); + return subInfo; + } + + @Test + public void testNetworkCallbacksRegisteredOnStartup() { + // verify NetworkCallbacks registered when instantiated + verify(mConnectivityManager) + .requestBackgroundNetwork( + eq(getWifiRequest()), + any(), + any(NetworkBringupCallback.class)); + verify(mConnectivityManager) + .requestBackgroundNetwork( + eq(getCellRequestForSubId(INITIAL_SUB_ID_1)), + any(), + any(NetworkBringupCallback.class)); + verify(mConnectivityManager) + .requestBackgroundNetwork( + eq(getCellRequestForSubId(INITIAL_SUB_ID_2)), + any(), + any(NetworkBringupCallback.class)); + verify(mConnectivityManager) + .requestBackgroundNetwork( + eq(getRouteSelectionRequest()), + any(), + any(RouteSelectionCallback.class)); + + verify(mSubscriptionManager).getSubscriptionsInGroup(eq(SUB_GROUP)); + } + + private NetworkRequest getWifiRequest() { + return getExpectedRequestBase() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build(); + } + + private NetworkRequest getCellRequestForSubId(int subId) { + return getExpectedRequestBase() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .setNetworkSpecifier(new TelephonyNetworkSpecifier(subId)) + .build(); + } + + private NetworkRequest getRouteSelectionRequest() { + return getExpectedRequestBase().build(); + } + + private NetworkRequest.Builder getExpectedRequestBase() { + return new NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED) + .addUnwantedCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED); + } + + @Test + public void testTeardown() { + mUnderlyingNetworkTracker.teardown(); + + // Expect 3 NetworkBringupCallbacks to be unregistered: 1 for WiFi and 2 for Cellular (1x + // for each subId) + verify(mConnectivityManager, times(3)) + .unregisterNetworkCallback(any(NetworkBringupCallback.class)); + verify(mConnectivityManager).unregisterNetworkCallback(any(RouteSelectionCallback.class)); + } + + @Test + public void testUnderlyingNetworkRecordEquals() { + UnderlyingNetworkRecord recordA = + new UnderlyingNetworkRecord( + mNetwork, + INITIAL_NETWORK_CAPABILITIES, + INITIAL_LINK_PROPERTIES, + false /* isBlocked */); + UnderlyingNetworkRecord recordB = + new UnderlyingNetworkRecord( + mNetwork, + INITIAL_NETWORK_CAPABILITIES, + INITIAL_LINK_PROPERTIES, + false /* isBlocked */); + UnderlyingNetworkRecord recordC = + new UnderlyingNetworkRecord( + mNetwork, + UPDATED_NETWORK_CAPABILITIES, + UPDATED_LINK_PROPERTIES, + false /* isBlocked */); + + assertEquals(recordA, recordB); + assertNotEquals(recordA, recordC); + } + + @Test + public void testRecordTrackerCallbackNotifiedForNetworkChange() { + verifyRegistrationOnAvailableAndGetCallback(); + } + + private RouteSelectionCallback verifyRegistrationOnAvailableAndGetCallback() { + return verifyRegistrationOnAvailableAndGetCallback(INITIAL_NETWORK_CAPABILITIES); + } + + private RouteSelectionCallback verifyRegistrationOnAvailableAndGetCallback( + NetworkCapabilities networkCapabilities) { + verify(mConnectivityManager) + .requestBackgroundNetwork( + eq(getRouteSelectionRequest()), + any(), + mRouteSelectionCallbackCaptor.capture()); + + RouteSelectionCallback cb = mRouteSelectionCallbackCaptor.getValue(); + cb.onAvailable(mNetwork); + cb.onCapabilitiesChanged(mNetwork, networkCapabilities); + cb.onLinkPropertiesChanged(mNetwork, INITIAL_LINK_PROPERTIES); + cb.onBlockedStatusChanged(mNetwork, false /* isFalse */); + + UnderlyingNetworkRecord expectedRecord = + new UnderlyingNetworkRecord( + mNetwork, + networkCapabilities, + INITIAL_LINK_PROPERTIES, + false /* isBlocked */); + verify(mNetworkTrackerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord)); + return cb; + } + + @Test + public void testRecordTrackerCallbackNotifiedForNetworkCapabilitiesChange() { + RouteSelectionCallback cb = verifyRegistrationOnAvailableAndGetCallback(); + + cb.onCapabilitiesChanged(mNetwork, UPDATED_NETWORK_CAPABILITIES); + + UnderlyingNetworkRecord expectedRecord = + new UnderlyingNetworkRecord( + mNetwork, + UPDATED_NETWORK_CAPABILITIES, + INITIAL_LINK_PROPERTIES, + false /* isBlocked */); + verify(mNetworkTrackerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord)); + } + + @Test + public void testRecordTrackerCallbackNotifiedForLinkPropertiesChange() { + RouteSelectionCallback cb = verifyRegistrationOnAvailableAndGetCallback(); + + cb.onLinkPropertiesChanged(mNetwork, UPDATED_LINK_PROPERTIES); + + UnderlyingNetworkRecord expectedRecord = + new UnderlyingNetworkRecord( + mNetwork, + INITIAL_NETWORK_CAPABILITIES, + UPDATED_LINK_PROPERTIES, + false /* isBlocked */); + verify(mNetworkTrackerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord)); + } + + @Test + public void testRecordTrackerCallbackNotifiedForNetworkSuspended() { + RouteSelectionCallback cb = verifyRegistrationOnAvailableAndGetCallback(); + + cb.onNetworkSuspended(mNetwork); + + UnderlyingNetworkRecord expectedRecord = + new UnderlyingNetworkRecord( + mNetwork, + SUSPENDED_NETWORK_CAPABILITIES, + INITIAL_LINK_PROPERTIES, + false /* isBlocked */); + verify(mNetworkTrackerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord)); + } + + @Test + public void testRecordTrackerCallbackNotifiedForNetworkResumed() { + RouteSelectionCallback cb = + verifyRegistrationOnAvailableAndGetCallback(SUSPENDED_NETWORK_CAPABILITIES); + + cb.onNetworkResumed(mNetwork); + + UnderlyingNetworkRecord expectedRecord = + new UnderlyingNetworkRecord( + mNetwork, + INITIAL_NETWORK_CAPABILITIES, + INITIAL_LINK_PROPERTIES, + false /* isBlocked */); + verify(mNetworkTrackerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord)); + } + + @Test + public void testRecordTrackerCallbackNotifiedForBlocked() { + RouteSelectionCallback cb = verifyRegistrationOnAvailableAndGetCallback(); + + cb.onBlockedStatusChanged(mNetwork, true /* isBlocked */); + + UnderlyingNetworkRecord expectedRecord = + new UnderlyingNetworkRecord( + mNetwork, + INITIAL_NETWORK_CAPABILITIES, + INITIAL_LINK_PROPERTIES, + true /* isBlocked */); + verify(mNetworkTrackerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord)); + } + + @Test + public void testRecordTrackerCallbackNotifiedForNetworkLoss() { + RouteSelectionCallback cb = verifyRegistrationOnAvailableAndGetCallback(); + + cb.onLost(mNetwork); + + verify(mNetworkTrackerCb).onSelectedUnderlyingNetworkChanged(null); + } + + @Test + public void testRecordTrackerCallbackIgnoresDuplicateRecord() { + RouteSelectionCallback cb = verifyRegistrationOnAvailableAndGetCallback(); + + cb.onCapabilitiesChanged(mNetwork, INITIAL_NETWORK_CAPABILITIES); + + // Verify no more calls to the UnderlyingNetworkTrackerCallback when the + // UnderlyingNetworkRecord does not actually change + verifyNoMoreInteractions(mNetworkTrackerCb); + } +} diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java index b4d39bf74a4b..4d92fb9c42f2 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java @@ -94,7 +94,7 @@ public class VcnGatewayConnectionTestBase { doReturn(mUnderlyingNetworkTracker) .when(mDeps) - .newUnderlyingNetworkTracker(any(), any(), any()); + .newUnderlyingNetworkTracker(any(), any(), any(), any()); } @Before |