diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt index 752c347423..7669e0e5de 100644 --- a/framework/api/module-lib-current.txt +++ b/framework/api/module-lib-current.txt @@ -31,6 +31,7 @@ package android.net { method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setProfileNetworkPreferences(@NonNull android.os.UserHandle, @NonNull java.util.List, @Nullable java.util.concurrent.Executor, @Nullable Runnable); method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setRequireVpnForUids(boolean, @NonNull java.util.Collection>); method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setUidFirewallRule(int, int, int); + method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setVpnDefaultForUids(@NonNull String, @NonNull java.util.Collection>); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_TEST_NETWORKS, android.Manifest.permission.NETWORK_STACK}) public void simulateDataStall(int, long, @NonNull android.net.Network, @NonNull android.os.PersistableBundle); method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void startCaptivePortalApp(@NonNull android.net.Network); method public void systemReady(); diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java index 1b4b42fbe3..60bc68c047 100644 --- a/framework/src/android/net/ConnectivityManager.java +++ b/framework/src/android/net/ConnectivityManager.java @@ -1378,6 +1378,17 @@ public class ConnectivityManager { } } + private static UidRange[] getUidRangeArray(@NonNull Collection> ranges) { + Objects.requireNonNull(ranges); + final UidRange[] rangesArray = new UidRange[ranges.size()]; + int index = 0; + for (Range range : ranges) { + rangesArray[index++] = new UidRange(range.getLower(), range.getUpper()); + } + + return rangesArray; + } + /** * Adds or removes a requirement for given UID ranges to use the VPN. * @@ -1397,6 +1408,12 @@ public class ConnectivityManager { * {@link NetworkCallback#onBlockedStatusChanged} callbacks called after the changes take * effect. *

+ * This method will block the specified UIDs from accessing non-VPN networks, but does not + * affect what the UIDs get as their default network. + * Compare {@link #setVpnDefaultForUids(String, Collection)}, which declares that the UIDs + * should only have a VPN as their default network, but does not block them from accessing other + * networks if they request them explicitly with the {@link Network} API. + *

* This method should be called only by the VPN code. * * @param ranges the UID ranges to restrict @@ -1416,11 +1433,7 @@ public class ConnectivityManager { // This method is not necessarily expected to be used outside the system server, so // parceling may not be necessary, but it could be used out-of-process, e.g., by the network // stack process, or by tests. - UidRange[] rangesArray = new UidRange[ranges.size()]; - int index = 0; - for (Range range : ranges) { - rangesArray[index++] = new UidRange(range.getLower(), range.getUpper()); - } + final UidRange[] rangesArray = getUidRangeArray(ranges); try { mService.setRequireVpnForUids(requireVpn, rangesArray); } catch (RemoteException e) { @@ -1428,6 +1441,41 @@ public class ConnectivityManager { } } + /** + * Inform the system that this VPN session should manage the passed UIDs. + * + * A VPN with the specified session ID may call this method to inform the system that the UIDs + * in the specified range are subject to a VPN. + * When this is called, the system will only choose a VPN for the default network of the UIDs in + * the specified ranges. + * + * This method declares that the UIDs in the range will only have a VPN for their default + * network, but does not block the UIDs from accessing other networks (permissions allowing) by + * explicitly requesting it with the {@link Network} API. + * Compare {@link #setRequireVpnForUids(boolean, Collection)}, which does not affect what + * network the UIDs get as default, but will block them from accessing non-VPN networks. + * + * @param session The VPN session which manages the passed UIDs. + * @param ranges The uid ranges which will treat VPN as their only default network. + * + * @hide + */ + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK, + android.Manifest.permission.NETWORK_SETTINGS}) + @SystemApi(client = MODULE_LIBRARIES) + public void setVpnDefaultForUids(@NonNull String session, + @NonNull Collection> ranges) { + Objects.requireNonNull(ranges); + final UidRange[] rangesArray = getUidRangeArray(ranges); + try { + mService.setVpnNetworkPreference(session, rangesArray); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Informs ConnectivityService of whether the legacy lockdown VPN, as implemented by * LockdownVpnTracker, is in use. This is deprecated for new devices starting from Android 12 diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl index 43d2f07c7b..7b6e769a6d 100644 --- a/framework/src/android/net/IConnectivityManager.aidl +++ b/framework/src/android/net/IConnectivityManager.aidl @@ -249,4 +249,6 @@ interface IConnectivityManager void replaceFirewallChain(int chain, in int[] uids); IBinder getCompanionDeviceManagerProxyService(); + + void setVpnNetworkPreference(String session, in UidRange[] ranges); } diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index e3e12fd6b2..df648acc0a 100755 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -286,6 +286,7 @@ import com.android.server.connectivity.ProfileNetworkPreferenceInfo; import com.android.server.connectivity.ProxyTracker; import com.android.server.connectivity.QosCallbackTracker; import com.android.server.connectivity.UidRangeUtils; +import com.android.server.connectivity.VpnNetworkPreferenceInfo; import com.android.server.connectivity.wear.CompanionDeviceManagerProxyService; import libcore.io.IoUtils; @@ -746,6 +747,12 @@ public class ConnectivityService extends IConnectivityManager.Stub */ private static final int EVENT_USER_DOES_NOT_WANT = 58; + /** + * Event to set VPN as preferred network for specific apps. + * obj = VpnNetworkPreferenceInfo + */ + private static final int EVENT_SET_VPN_NETWORK_PREFERENCE = 59; + /** * Argument for {@link #EVENT_PROVISIONING_NOTIFICATION} to indicate that the notification * should be shown. @@ -1625,6 +1632,17 @@ public class ConnectivityService extends IConnectivityManager.Stub TYPE_NONE, NetworkRequest.Type.REQUEST); } + private NetworkRequest createVpnRequest() { + final NetworkCapabilities netCap = new NetworkCapabilities.Builder() + .withoutDefaultCapabilities() + .addTransportType(TRANSPORT_VPN) + .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) + .addCapability(NET_CAPABILITY_NOT_RESTRICTED) + .build(); + netCap.setRequestorUidAndPackageName(Process.myUid(), mContext.getPackageName()); + return createNetworkRequest(NetworkRequest.Type.REQUEST, netCap); + } + private NetworkRequest createDefaultInternetRequestForTransport( int transportType, NetworkRequest.Type type) { final NetworkCapabilities netCap = new NetworkCapabilities(); @@ -5531,6 +5549,9 @@ public class ConnectivityService extends IConnectivityManager.Stub nai.onPreventAutomaticReconnect(); nai.disconnect(); break; + case EVENT_SET_VPN_NETWORK_PREFERENCE: + handleSetVpnNetworkPreference((VpnNetworkPreferenceInfo) msg.obj); + break; } } } @@ -7122,6 +7143,12 @@ public class ConnectivityService extends IConnectivityManager.Stub private NetworkPreferenceList mProfileNetworkPreferences = new NetworkPreferenceList<>(); + // Current VPN network preferences. This object follows the same threading rules as the OEM + // network preferences above. + @NonNull + private NetworkPreferenceList + mVpnNetworkPreferences = new NetworkPreferenceList<>(); + // A set of UIDs that should use mobile data preferentially if available. This object follows // the same threading rules as the OEM network preferences above. @NonNull @@ -11111,6 +11138,60 @@ public class ConnectivityService extends IConnectivityManager.Stub new Pair<>(preference, listener))); } + /** + * Sets the specified UIDs to get/receive the VPN as the only default network. + * + * Calling this will overwrite the existing network preference for this session, and the + * specified UIDs won't get any default network when no VPN is connected. + * + * @param session The VPN session which manages the passed UIDs. + * @param ranges The uid ranges which will treat VPN as the only preferred network. Clear the + * setting for this session if the array is empty. Null is not allowed, the + * method will use {@link Objects#requireNonNull(Object)} to check this variable. + * @hide + */ + @Override + public void setVpnNetworkPreference(String session, UidRange[] ranges) { + Objects.requireNonNull(ranges); + enforceNetworkStackOrSettingsPermission(); + final UidRange[] sortedRanges = UidRangeUtils.sortRangesByStartUid(ranges); + if (UidRangeUtils.sortedRangesContainOverlap(sortedRanges)) { + throw new IllegalArgumentException( + "setVpnNetworkPreference: Passed UID ranges overlap"); + } + + mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_VPN_NETWORK_PREFERENCE, + new VpnNetworkPreferenceInfo(session, + new ArraySet(Arrays.asList(ranges))))); + } + + private void handleSetVpnNetworkPreference(VpnNetworkPreferenceInfo preferenceInfo) { + Log.d(TAG, "handleSetVpnNetworkPreference: preferenceInfo = " + preferenceInfo); + + mVpnNetworkPreferences = mVpnNetworkPreferences.minus(preferenceInfo.getKey()); + mVpnNetworkPreferences = mVpnNetworkPreferences.plus(preferenceInfo); + + removeDefaultNetworkRequestsForPreference(PREFERENCE_ORDER_VPN); + addPerAppDefaultNetworkRequests(createNrisForVpnNetworkPreference(mVpnNetworkPreferences)); + // Finally, rematch. + rematchAllNetworksAndRequests(); + } + + private ArraySet createNrisForVpnNetworkPreference( + @NonNull NetworkPreferenceList preferenceList) { + final ArraySet nris = new ArraySet<>(); + for (VpnNetworkPreferenceInfo preferenceInfo : preferenceList) { + final List requests = new ArrayList<>(); + // Request VPN only, so other networks won't be the fallback options when VPN is not + // connected temporarily. + requests.add(createVpnRequest()); + final Set uidRanges = new ArraySet(preferenceInfo.getUidRangesNoCopy()); + setNetworkRequestUids(requests, uidRanges); + nris.add(new NetworkRequestInfo(Process.myUid(), requests, PREFERENCE_ORDER_VPN)); + } + return nris; + } + /** * Check the validity of an OEM network preference to be used for testing purposes. * @param preference the preference to validate diff --git a/service/src/com/android/server/connectivity/UidRangeUtils.java b/service/src/com/android/server/connectivity/UidRangeUtils.java index 541340b7f1..f36797d5c2 100644 --- a/service/src/com/android/server/connectivity/UidRangeUtils.java +++ b/service/src/com/android/server/connectivity/UidRangeUtils.java @@ -184,4 +184,41 @@ public final class UidRangeUtils { uidRangeSet.add(new UidRange(start, stop)); return uidRangeSet; } + + private static int compare(UidRange range1, UidRange range2) { + return range1.start - range2.start; + } + + /** + * Sort the given UidRange array. + * + * @param ranges The array of UidRange which is going to be sorted. + * @return Array of UidRange. + */ + public static UidRange[] sortRangesByStartUid(UidRange[] ranges) { + final ArrayList uidRanges = new ArrayList(Arrays.asList(ranges)); + Collections.sort(uidRanges, UidRangeUtils::compare); + return (UidRange[]) uidRanges.toArray(new UidRange[0]); + } + + /** + * Check if the given sorted UidRange array contains overlap or not. + * + * Note that the sorted UidRange array must be sorted by increasing lower bound. If it's not, + * the behavior is undefined. + * + * @param ranges The sorted UidRange array which is going to be checked if there is an overlap + * or not. + * @return A boolean to indicate if the given sorted UidRange array contains overlap or not. + */ + public static boolean sortedRangesContainOverlap(UidRange[] ranges) { + final ArrayList uidRanges = new ArrayList(Arrays.asList(ranges)); + for (int i = 0; i + 1 < uidRanges.size(); i++) { + if (((UidRange) uidRanges.get(i + 1)).start <= ((UidRange) uidRanges.get(i)).stop) { + return true; + } + } + + return false; + } } diff --git a/service/src/com/android/server/connectivity/VpnNetworkPreferenceInfo.java b/service/src/com/android/server/connectivity/VpnNetworkPreferenceInfo.java new file mode 100644 index 0000000000..3e111abb25 --- /dev/null +++ b/service/src/com/android/server/connectivity/VpnNetworkPreferenceInfo.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 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.connectivity; + +import android.annotation.NonNull; +import android.net.UidRange; +import android.util.ArraySet; + +/** + * Record the session and UidRange for a VPN preference. + */ +public class VpnNetworkPreferenceInfo + implements NetworkPreferenceList.NetworkPreference { + + @NonNull + public final String mSession; + + @NonNull + public final ArraySet mUidRanges; + + public VpnNetworkPreferenceInfo(@NonNull String session, + @NonNull ArraySet uidRanges) { + this.mSession = session; + this.mUidRanges = uidRanges; + } + + @Override + public boolean isCancel() { + return mUidRanges.isEmpty(); + } + + @Override + @NonNull + public String getKey() { + return mSession; + } + + @NonNull + public ArraySet getUidRangesNoCopy() { + return mUidRanges; + } + + /** toString */ + public String toString() { + return "[VpnNetworkPreference session = " + mSession + + " uidRanges = " + mUidRanges + + "]"; + } +} diff --git a/tests/unit/java/com/android/server/connectivity/UidRangeUtilsTest.java b/tests/unit/java/com/android/server/connectivity/UidRangeUtilsTest.java index b8c552ead3..ad4785d262 100644 --- a/tests/unit/java/com/android/server/connectivity/UidRangeUtilsTest.java +++ b/tests/unit/java/com/android/server/connectivity/UidRangeUtilsTest.java @@ -402,4 +402,27 @@ public class UidRangeUtilsTest { expected.add(uids20_24); assertEquals(expected, UidRangeUtils.convertArrayToUidRange(input)); } + + @Test + public void testSortRangesByStartUid() throws Exception { + final UidRange uid1 = new UidRange(100, 110); + final UidRange uid2 = new UidRange(120, 130); + final UidRange[] unsortedRanges = new UidRange[] {uid2, uid1}; + final UidRange[] sortedRanges = UidRangeUtils.sortRangesByStartUid(unsortedRanges); + assertEquals(uid1, sortedRanges[0]); + assertEquals(uid2, sortedRanges[1]); + } + + @Test + public void testSortedRangesContainOverlap() throws Exception { + final UidRange uid1 = new UidRange(100, 110); + final UidRange uid2 = new UidRange(109, 120); + final UidRange uid3 = new UidRange(120, 130); + final UidRange[] overlapRanges1 = new UidRange[] {uid1, uid2}; + final UidRange[] overlapRanges2 = new UidRange[] {uid2, uid3}; + final UidRange[] notOverlapRanges = new UidRange[] {uid1, uid3}; + assertTrue(UidRangeUtils.sortedRangesContainOverlap(overlapRanges1)); + assertTrue(UidRangeUtils.sortedRangesContainOverlap(overlapRanges2)); + assertFalse(UidRangeUtils.sortedRangesContainOverlap(notOverlapRanges)); + } }