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/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
+ + "]";
+ }
+}