diff --git a/core/java/android/net/IVpnManager.aidl b/core/java/android/net/IVpnManager.aidl index b4647cabe1bc0..f30237853a3ea 100644 --- a/core/java/android/net/IVpnManager.aidl +++ b/core/java/android/net/IVpnManager.aidl @@ -42,6 +42,8 @@ interface IVpnManager { String startVpnProfile(String packageName); void stopVpnProfile(String packageName); VpnProfileState getProvisionedVpnProfileState(String packageName); + boolean setAppExclusionList(int userId, String vpnPackage, in List excludedApps); + List getAppExclusionList(int userId, String vpnPackage); /** Always-on VPN APIs */ boolean isAlwaysOnVpnPackageSupported(int userId, String packageName); diff --git a/core/java/android/net/VpnManager.java b/core/java/android/net/VpnManager.java index 37eb74a58235e..f62d7c4a698d2 100644 --- a/core/java/android/net/VpnManager.java +++ b/core/java/android/net/VpnManager.java @@ -594,6 +594,63 @@ public class VpnManager { } } + /** + * Sets the application exclusion list for the specified VPN profile. + * + *

If an app in the set of excluded apps is not installed for the given user, it will be + * skipped in the list of app exclusions. If apps are installed or removed, any active VPN will + * have its UID set updated automatically. If the caller is not {@code userId}, + * {@link android.Manifest.permission.INTERACT_ACROSS_USERS_FULL} permission is required. + * + *

This will ONLY affect VpnManager profiles. As such, the NETWORK_SETTINGS provider MUST NOT + * allow configuration of these options if the application has not provided a VPN profile. + * + * @param userId the identifier of the user to set app exclusion list + * @param vpnPackage The package name for an installed VPN app on the device + * @param excludedApps the app exclusion list + * @throws IllegalStateException exception if vpn for the @code userId} is not ready yet. + * + * @return whether setting the list is successful or not + * @hide + */ + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_SETTINGS, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK}) + public boolean setAppExclusionList(int userId, @NonNull String vpnPackage, + @NonNull List excludedApps) { + try { + return mService.setAppExclusionList(userId, vpnPackage, excludedApps); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Gets the application exclusion list for the specified VPN profile. If the caller is not + * {@code userId}, {@link android.Manifest.permission.INTERACT_ACROSS_USERS_FULL} permission + * is required. + * + * @param userId the identifier of the user to set app exclusion list + * @param vpnPackage The package name for an installed VPN app on the device + * @return the list of packages for the specified VPN profile or null if no corresponding VPN + * profile configured. + * + * @hide + */ + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_SETTINGS, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK}) + @Nullable + public List getAppExclusionList(int userId, @NonNull String vpnPackage) { + try { + return mService.getAppExclusionList(userId, vpnPackage); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * @return the list of packages that are allowed to access network when always-on VPN is in * lockdown mode but not connected. Returns {@code null} when VPN lockdown is not active. diff --git a/services/core/java/com/android/server/VpnManagerService.java b/services/core/java/com/android/server/VpnManagerService.java index c1d8e7bf3dc05..d3ef6bed46a09 100644 --- a/services/core/java/com/android/server/VpnManagerService.java +++ b/services/core/java/com/android/server/VpnManagerService.java @@ -880,6 +880,38 @@ public class VpnManagerService extends IVpnManager.Stub { } } + @Override + public boolean setAppExclusionList(int userId, String vpnPackage, List excludedApps) { + enforceSettingsPermission(); + enforceCrossUserPermission(userId); + + synchronized (mVpns) { + final Vpn vpn = mVpns.get(userId); + if (vpn != null) { + return vpn.setAppExclusionList(vpnPackage, excludedApps); + } else { + logw("User " + userId + " has no Vpn configuration"); + throw new IllegalStateException( + "VPN for user " + userId + " not ready yet. Skipping setting the list"); + } + } + } + + @Override + public List getAppExclusionList(int userId, String vpnPackage) { + enforceSettingsPermission(); + enforceCrossUserPermission(userId); + + synchronized (mVpns) { + final Vpn vpn = mVpns.get(userId); + if (vpn != null) { + return vpn.getAppExclusionList(vpnPackage); + } else { + logw("User " + userId + " has no Vpn configuration"); + return null; + } + } + } @Override public void factoryReset() { diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 64ec12f7c131c..d07f151f25bdc 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -27,6 +27,8 @@ import static android.net.VpnManager.NOTIFICATION_CHANNEL_VPN; import static android.os.PowerWhitelistManager.REASON_VPN; import static android.os.UserHandle.PER_USER_RANGE; +import static com.android.server.vcn.util.PersistableBundleUtils.STRING_DESERIALIZER; + import static java.util.Objects.requireNonNull; import android.Manifest; @@ -100,6 +102,7 @@ import android.os.INetworkManagementService; import android.os.Looper; import android.os.Parcel; import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; @@ -131,6 +134,7 @@ import com.android.net.module.util.NetworkStackConstants; import com.android.server.DeviceIdleInternal; import com.android.server.LocalServices; import com.android.server.net.BaseNetworkObserver; +import com.android.server.vcn.util.PersistableBundleUtils; import libcore.io.IoUtils; @@ -178,6 +182,8 @@ public class Vpn { private static final String VPN_PROVIDER_NAME_BASE = "VpnNetworkProvider:"; private static final boolean LOGD = true; private static final String ANDROID_KEYSTORE_PROVIDER = "AndroidKeyStore"; + /** Key containing prefix of vpn app excluded list */ + @VisibleForTesting static final String VPN_APP_EXCLUDED = "VPN_APP_EXCLUDED_"; // Length of time (in milliseconds) that an app hosting an always-on VPN is placed on // the device idle allowlist during service launch and VPN bootstrap. @@ -2727,6 +2733,8 @@ public class Vpn { mConfig.underlyingNetworks = new Network[] {network}; + mConfig.disallowedApplications = getAppExclusionList(mPackage); + networkAgent = mNetworkAgent; // The below must be done atomically with the mConfig update, otherwise @@ -3681,6 +3689,88 @@ public class Vpn { } } + private boolean storeAppExclusionList(@NonNull String packageName, + @NonNull List excludedApps) { + byte[] data; + try { + final PersistableBundle bundle = PersistableBundleUtils.fromList( + excludedApps, PersistableBundleUtils.STRING_SERIALIZER); + data = PersistableBundleUtils.toDiskStableBytes(bundle); + } catch (IOException e) { + Log.e(TAG, "problem writing into stream", e); + return false; + } + + final long oldId = Binder.clearCallingIdentity(); + try { + getVpnProfileStore().put(getVpnAppExcludedForPackage(packageName), data); + } finally { + Binder.restoreCallingIdentity(oldId); + } + return true; + } + + @VisibleForTesting + String getVpnAppExcludedForPackage(String packageName) { + return VPN_APP_EXCLUDED + mUserId + "_" + packageName; + } + + /** + * Set the application exclusion list for the specified VPN profile. + * + * @param packageName the package name of the app provisioning this profile + * @param excludedApps the list of excluded packages + * + * @return whether setting the list is successful or not + */ + public synchronized boolean setAppExclusionList(@NonNull String packageName, + @NonNull List excludedApps) { + enforceNotRestrictedUser(); + if (!storeAppExclusionList(packageName, excludedApps)) return false; + // Re-build and update NetworkCapabilities via NetworkAgent. + if (mNetworkAgent != null) { + // Only update the platform VPN + if (isIkev2VpnRunner()) { + mConfig.disallowedApplications = List.copyOf(excludedApps); + mNetworkCapabilities = new NetworkCapabilities.Builder(mNetworkCapabilities) + .setUids(createUserAndRestrictedProfilesRanges( + mUserId, null /* allowedApplications */, excludedApps)) + .build(); + mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities); + } + } + + return true; + } + + /** + * Gets the application exclusion list for the specified VPN profile. + * + * @param packageName the package name of the app provisioning this profile + * @return the list of excluded packages for the specified VPN profile or empty list if there is + * no provisioned VPN profile. + */ + @NonNull + public synchronized List getAppExclusionList(@NonNull String packageName) { + enforceNotRestrictedUser(); + + final long oldId = Binder.clearCallingIdentity(); + try { + final byte[] bytes = getVpnProfileStore().get(getVpnAppExcludedForPackage(packageName)); + + if (bytes == null || bytes.length == 0) return new ArrayList<>(); + + final PersistableBundle bundle = PersistableBundleUtils.fromDiskStableBytes(bytes); + return PersistableBundleUtils.toList(bundle, STRING_DESERIALIZER); + } catch (IOException e) { + Log.e(TAG, "problem reading from stream", e); + } finally { + Binder.restoreCallingIdentity(oldId); + } + + return new ArrayList<>(); + } + private @VpnProfileState.State int getStateFromLegacyState(int legacyState) { switch (legacyState) { case LegacyVpnInfo.STATE_CONNECTING: diff --git a/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java b/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java index 08e8eebb87406..999d4064c9516 100644 --- a/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java +++ b/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java @@ -23,6 +23,8 @@ import android.os.PersistableBundle; import com.android.internal.util.HexDump; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -295,6 +297,30 @@ public class PersistableBundleUtils { return result; } + /** + * Converts a PersistableBundle into a disk-stable byte array format + * + * @param bundle the PersistableBundle to be converted to a disk-stable format + * @return the byte array representation of the PersistableBundle + */ + @Nullable + public static byte[] toDiskStableBytes(@NonNull PersistableBundle bundle) throws IOException { + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + bundle.writeToStream(outputStream); + return outputStream.toByteArray(); + } + + /** + * Converts from a disk-stable byte array format to a PersistableBundle + * + * @param bytes the disk-stable byte array + * @return the PersistableBundle parsed from this byte array. + */ + public static PersistableBundle fromDiskStableBytes(@NonNull byte[] bytes) throws IOException { + final ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); + return PersistableBundle.readFromStream(inputStream); + } + /** * Ensures safe reading and writing of {@link PersistableBundle}s to and from disk. * diff --git a/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java b/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java index 294f5c1f4842a..9c6d85238b774 100644 --- a/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java +++ b/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java @@ -267,6 +267,15 @@ public class PersistableBundleUtilsTest { assertTrue(PersistableBundleUtils.isEqual(testBundle, minimized)); } + @Test + public void testToFromDiskStableBytes() throws Exception { + final PersistableBundle testBundle = getTestBundle(); + final PersistableBundle result = + PersistableBundleUtils.fromDiskStableBytes( + PersistableBundleUtils.toDiskStableBytes(testBundle)); + assertTrue(PersistableBundleUtils.isEqual(testBundle, result)); + } + @Test public void testEquality_identical() throws Exception { final PersistableBundle left = getTestBundle();