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 b49654e0f79b8..3e2967b894153 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; @@ -97,6 +99,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; @@ -127,6 +130,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; @@ -174,6 +178,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. @@ -2623,6 +2629,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 @@ -3473,6 +3481,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: