Create a new API to make a set of UIDs use only VPN by default
Create a new API - setVpnNetworkPreference() for the caller to
set VPN as the preference network.
VPN will be disconnected when its underlying network is gone.
To prevent packets going through an underlying network when the
underlying network is back but VPN is not connected yet, set VPN
as the only preferred network for specific apps.
Bug: 231749077
Test: 1. atest FrameworksNetTests
2. Create a test app to register default network and check if
the VPN is the only default network for the test app.
Change-Id: Iabcd38e2fec2aefedbf78d20e338f222d83a9e7f
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index 752c347..7669e0e 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -31,6 +31,7 @@
method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setProfileNetworkPreferences(@NonNull android.os.UserHandle, @NonNull java.util.List<android.net.ProfileNetworkPreference>, @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<android.util.Range<java.lang.Integer>>);
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<android.util.Range<java.lang.Integer>>);
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 1b4b42f..60bc68c 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -1378,6 +1378,17 @@
}
}
+ private static UidRange[] getUidRangeArray(@NonNull Collection<Range<Integer>> ranges) {
+ Objects.requireNonNull(ranges);
+ final UidRange[] rangesArray = new UidRange[ranges.size()];
+ int index = 0;
+ for (Range<Integer> 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 @@
* {@link NetworkCallback#onBlockedStatusChanged} callbacks called after the changes take
* effect.
* <p>
+ * 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.
+ * <p>
* This method should be called only by the VPN code.
*
* @param ranges the UID ranges to restrict
@@ -1416,11 +1433,7 @@
// 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<Integer> range : ranges) {
- rangesArray[index++] = new UidRange(range.getLower(), range.getUpper());
- }
+ final UidRange[] rangesArray = getUidRangeArray(ranges);
try {
mService.setRequireVpnForUids(requireVpn, rangesArray);
} catch (RemoteException e) {
@@ -1429,6 +1442,41 @@
}
/**
+ * 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<Range<Integer>> 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
* but is still supported for backwards compatibility.
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index 43d2f07..7b6e769 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -249,4 +249,6 @@
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 e3e12fd..df648ac 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.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;
@@ -747,6 +748,12 @@
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 @@
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 @@
nai.onPreventAutomaticReconnect();
nai.disconnect();
break;
+ case EVENT_SET_VPN_NETWORK_PREFERENCE:
+ handleSetVpnNetworkPreference((VpnNetworkPreferenceInfo) msg.obj);
+ break;
}
}
}
@@ -7122,6 +7143,12 @@
private NetworkPreferenceList<UserHandle, ProfileNetworkPreferenceInfo>
mProfileNetworkPreferences = new NetworkPreferenceList<>();
+ // Current VPN network preferences. This object follows the same threading rules as the OEM
+ // network preferences above.
+ @NonNull
+ private NetworkPreferenceList<String, VpnNetworkPreferenceInfo>
+ 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
@@ -11112,6 +11139,60 @@
}
/**
+ * 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<UidRange>(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<NetworkRequestInfo> createNrisForVpnNetworkPreference(
+ @NonNull NetworkPreferenceList<String, VpnNetworkPreferenceInfo> preferenceList) {
+ final ArraySet<NetworkRequestInfo> nris = new ArraySet<>();
+ for (VpnNetworkPreferenceInfo preferenceInfo : preferenceList) {
+ final List<NetworkRequest> 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<UidRange> 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
* @return true if this is a valid OEM network preference test request.
diff --git a/service/src/com/android/server/connectivity/UidRangeUtils.java b/service/src/com/android/server/connectivity/UidRangeUtils.java
index 541340b..f36797d 100644
--- a/service/src/com/android/server/connectivity/UidRangeUtils.java
+++ b/service/src/com/android/server/connectivity/UidRangeUtils.java
@@ -184,4 +184,41 @@
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 0000000..3e111ab
--- /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<String> {
+
+ @NonNull
+ public final String mSession;
+
+ @NonNull
+ public final ArraySet<UidRange> mUidRanges;
+
+ public VpnNetworkPreferenceInfo(@NonNull String session,
+ @NonNull ArraySet<UidRange> uidRanges) {
+ this.mSession = session;
+ this.mUidRanges = uidRanges;
+ }
+
+ @Override
+ public boolean isCancel() {
+ return mUidRanges.isEmpty();
+ }
+
+ @Override
+ @NonNull
+ public String getKey() {
+ return mSession;
+ }
+
+ @NonNull
+ public ArraySet<UidRange> 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 b8c552e..ad4785d 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 @@
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));
+ }
}