From d19b3c03d2ec1d7f54601cf72a3100699b8ed315 Mon Sep 17 00:00:00 2001 From: Chiachang Date: Fri, 29 Apr 2022 02:34:05 +0000 Subject: [PATCH] Allow getting/setting app exclusion list for specific vpn profile Provide a way for the system Setting app or other similar system apps to set the app exclusion list for the specific VPN from VpnManager. The list affects only VPN provisioned from VpnManager to keep the design consistence for app using VpnService. The list is stored in the keystore for its persistence. Bug: 192078259 Test: atest FrameworksNetTests CtsNetTestCases Change-Id: I157823866e1899b40aa36ea2c0bc4a80370108be --- core/java/android/net/IVpnManager.aidl | 2 + core/java/android/net/VpnManager.java | 57 ++++++++++++ .../com/android/server/VpnManagerService.java | 32 +++++++ .../com/android/server/connectivity/Vpn.java | 90 +++++++++++++++++++ 4 files changed, 181 insertions(+) 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: