Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7d1b14eb authored by Chiachang's avatar Chiachang Committed by Chiachang Wang
Browse files

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
(cherry picked from commit d19b3c03)
Merged-In: I157823866e1899b40aa36ea2c0bc4a80370108be
parent 1e3c0ae6
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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<String> excludedApps);
    List<String> getAppExclusionList(int userId, String vpnPackage);

    /** Always-on VPN APIs */
    boolean isAlwaysOnVpnPackageSupported(int userId, String packageName);
+57 −0
Original line number Diff line number Diff line
@@ -594,6 +594,63 @@ public class VpnManager {
        }
    }

    /**
     * Sets the application exclusion list for the specified VPN profile.
     *
     * <p>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.
     *
     * <p>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<String> 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<String> 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.
+32 −0
Original line number Diff line number Diff line
@@ -880,6 +880,38 @@ public class VpnManagerService extends IVpnManager.Stub {
        }
    }

    @Override
    public boolean setAppExclusionList(int userId, String vpnPackage, List<String> 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<String> 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() {
+90 −0
Original line number Diff line number Diff line
@@ -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.
@@ -2636,6 +2642,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
@@ -3486,6 +3494,88 @@ public class Vpn {
        }
    }

    private boolean storeAppExclusionList(@NonNull String packageName,
            @NonNull List<String> 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<String> 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<String> 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: