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

Commit 812800cb authored by Robin Lee's avatar Robin Lee
Browse files

Package changed/removed listeners for always-on VPN

Fix 2 problems of always-on vpn after always-on package is removed
1. Prevent network being locked down (blocking all network traffic)
   Otherwise, user has no way to download the vpn app from Play Store,
   and never be able to gain control of the network again.
2. Allow user to connect other vpn app.

Implementation
1. Switch off always-on mode if the package gets removed.
2. Restart always-on mode if the package gets replaced/upgraded.

Bug: 29050764
Change-Id: Id3e389ae0b11c6002a5167919292d9634c2014cb
parent 258aa3e5
Loading
Loading
Loading
Loading
+2 −32
Original line number Diff line number Diff line
@@ -3388,7 +3388,6 @@ public class ConnectivityService extends IConnectivityManager.Stub
     *         was no always-on VPN to start. {@code false} otherwise.
     */
    private boolean startAlwaysOnVpn(int userId) {
        final String alwaysOnPackage;
        synchronized (mVpns) {
            Vpn vpn = mVpns.get(userId);
            if (vpn == null) {
@@ -3397,27 +3396,8 @@ public class ConnectivityService extends IConnectivityManager.Stub
                Slog.wtf(TAG, "User " + userId + " has no Vpn configuration");
                return false;
            }
            alwaysOnPackage = vpn.getAlwaysOnPackage();
            // Skip if there is no service to start.
            if (alwaysOnPackage == null) {
                return true;
            }
            // Skip if the service is already established. This isn't bulletproof: it's not bound
            // until after establish(), so if it's mid-setup onStartCommand will be sent twice,
            // which may restart the connection.
            if (vpn.getNetworkInfo().isConnected()) {
                return true;
            }
        }

        // Start the VPN service declared in the app's manifest.
        Intent serviceIntent = new Intent(VpnConfig.SERVICE_INTERFACE);
        serviceIntent.setPackage(alwaysOnPackage);
        try {
            return mContext.startServiceAsUser(serviceIntent, UserHandle.of(userId)) != null;
        } catch (RuntimeException e) {
            Slog.w(TAG, "VpnService " + serviceIntent + " failed to start", e);
            return false;
            return vpn.startAlwaysOnVpn();
        }
    }

@@ -3449,17 +3429,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
                return false;
            }

            // Save the configuration
            final long token = Binder.clearCallingIdentity();
            try {
                final ContentResolver cr = mContext.getContentResolver();
                Settings.Secure.putStringForUser(cr, Settings.Secure.ALWAYS_ON_VPN_APP,
                        packageName, userId);
                Settings.Secure.putIntForUser(cr, Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN,
                        (lockdown ? 1 : 0), userId);
            } finally {
                Binder.restoreCallingIdentity(token);
            }
            vpn.saveAlwaysOnPackage();
        }
        return true;
    }
+140 −10
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import android.app.AppOpsManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -53,6 +54,7 @@ import android.net.NetworkInfo.DetailedState;
import android.net.NetworkMisc;
import android.net.RouteInfo;
import android.net.UidRange;
import android.net.Uri;
import android.os.Binder;
import android.os.FileUtils;
import android.os.IBinder;
@@ -60,12 +62,14 @@ import android.os.INetworkManagementService;
import android.os.Looper;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.PatternMatcher;
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.SystemService;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.security.Credentials;
import android.security.KeyStore;
import android.text.TextUtils;
@@ -163,6 +167,45 @@ public class Vpn {
    // Handle of user initiating VPN.
    private final int mUserHandle;

    // Listen to package remove and change event in this user
    private final BroadcastReceiver mPackageIntentReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            final Uri data = intent.getData();
            final String packageName = data == null ? null : data.getSchemeSpecificPart();
            if (packageName == null) {
                return;
            }

            synchronized (Vpn.this) {
                // Avoid race that always-on package has been unset
                if (!packageName.equals(getAlwaysOnPackage())) {
                    return;
                }

                final String action = intent.getAction();
                Log.i(TAG, "Received broadcast " + action + " for always-on package " + packageName
                        + " in user " + mUserHandle);

                switch(action) {
                    case Intent.ACTION_PACKAGE_REPLACED:
                        // Start vpn after app upgrade
                        startAlwaysOnVpn();
                        break;
                    case Intent.ACTION_PACKAGE_REMOVED:
                        final boolean isPackageRemoved = !intent.getBooleanExtra(
                                Intent.EXTRA_REPLACING, false);
                        if (isPackageRemoved) {
                            setAndSaveAlwaysOnPackage(null, false);
                        }
                        break;
                }
            }
        }
    };

    private boolean mIsPackageIntentReceiverRegistered = false;

    public Vpn(Looper looper, Context context, INetworkManagementService netService,
            int userHandle) {
        mContext = context;
@@ -233,10 +276,37 @@ public class Vpn {

        mAlwaysOn = (packageName != null);
        mLockdown = (mAlwaysOn && lockdown);
        maybeRegisterPackageChangeReceiverLocked(packageName);
        setVpnForcedLocked(mLockdown);
        return true;
    }

    private void unregisterPackageChangeReceiverLocked() {
        // register previous intent filter
        if (mIsPackageIntentReceiverRegistered) {
            mContext.unregisterReceiver(mPackageIntentReceiver);
            mIsPackageIntentReceiverRegistered = false;
        }
    }

    private void maybeRegisterPackageChangeReceiverLocked(String packageName) {
        // Unregister IntentFilter listening for previous always-on package change
        unregisterPackageChangeReceiverLocked();

        if (packageName != null) {
            mIsPackageIntentReceiverRegistered = true;

            IntentFilter intentFilter = new IntentFilter();
            // Protected intent can only be sent by system. No permission required in register.
            intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
            intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
            intentFilter.addDataScheme("package");
            intentFilter.addDataSchemeSpecificPart(packageName, PatternMatcher.PATTERN_LITERAL);
            mContext.registerReceiverAsUser(
                    mPackageIntentReceiver, UserHandle.of(mUserHandle), intentFilter, null, null);
        }
    }

    /**
     * @return the package name of the VPN controller responsible for always-on VPN,
     *         or {@code null} if none is set or always-on VPN is controlled through
@@ -248,6 +318,69 @@ public class Vpn {
        return (mAlwaysOn ? mPackage : null);
    }

    /**
     * Save the always-on package and lockdown config into Settings.Secure
     */
    public synchronized void saveAlwaysOnPackage() {
        final long token = Binder.clearCallingIdentity();
        try {
            final ContentResolver cr = mContext.getContentResolver();
            Settings.Secure.putStringForUser(cr, Settings.Secure.ALWAYS_ON_VPN_APP,
                    getAlwaysOnPackage(), mUserHandle);
            Settings.Secure.putIntForUser(cr, Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN,
                    (mLockdown ? 1 : 0), mUserHandle);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    /**
     * Set and save always-on package and lockdown config
     * @see Vpn#setAlwaysOnPackage(String, boolean)
     * @see Vpn#saveAlwaysOnPackage()
     *
     * @return result of Vpn#setAndSaveAlwaysOnPackage(String, boolean)
     */
    private synchronized boolean setAndSaveAlwaysOnPackage(String packageName, boolean lockdown) {
        if (setAlwaysOnPackage(packageName, lockdown)) {
            saveAlwaysOnPackage();
            return true;
        } else {
            return false;
        }
    }

    /**
     * @return {@code true} if the service was started, the service was already connected, or there
     *         was no always-on VPN to start. {@code false} otherwise.
     */
    public boolean startAlwaysOnVpn() {
        final String alwaysOnPackage;
        synchronized (this) {
            alwaysOnPackage = getAlwaysOnPackage();
            // Skip if there is no service to start.
            if (alwaysOnPackage == null) {
                return true;
            }
            // Skip if the service is already established. This isn't bulletproof: it's not bound
            // until after establish(), so if it's mid-setup onStartCommand will be sent twice,
            // which may restart the connection.
            if (getNetworkInfo().isConnected()) {
                return true;
            }
        }

        // Start the VPN service declared in the app's manifest.
        Intent serviceIntent = new Intent(VpnConfig.SERVICE_INTERFACE);
        serviceIntent.setPackage(alwaysOnPackage);
        try {
            return mContext.startServiceAsUser(serviceIntent, UserHandle.of(mUserHandle)) != null;
        } catch (RuntimeException e) {
            Log.e(TAG, "VpnService " + serviceIntent + " failed to start", e);
            return false;
        }
    }

    /**
     * Prepare for a VPN application. This method is designed to solve
     * race conditions. It first compares the current prepared package
@@ -270,11 +403,12 @@ public class Vpn {
     *
     * - oldPackage non-null, newPackage null: App calling VpnService#prepare().
     * - oldPackage null, newPackage non-null: ConfirmDialog calling prepareVpn().
     * - oldPackage non-null, newPackage=LEGACY_VPN: Used internally to disconnect
     * - oldPackage null, newPackage=LEGACY_VPN: Used internally to disconnect
     *   and revoke any current app VPN and re-prepare legacy vpn.
     *
     * TODO: Rename the variables - or split this method into two - and end this
     * confusion.
     * TODO: Rename the variables - or split this method into two - and end this confusion.
     * TODO: b/29032008 Migrate code from prepare(oldPackage=non-null, newPackage=LEGACY_VPN)
     * to prepare(oldPackage=null, newPackage=LEGACY_VPN)
     *
     * @param oldPackage The package name of the old VPN application
     * @param newPackage The package name of the new VPN application
@@ -284,10 +418,7 @@ public class Vpn {
    public synchronized boolean prepare(String oldPackage, String newPackage) {
        if (oldPackage != null) {
            // Stop an existing always-on VPN from being dethroned by other apps.
            // TODO: Replace TextUtils.equals by isCurrentPreparedPackage when ConnectivityService
            // can unset always-on after always-on package is uninstalled. Make sure when package
            // is reinstalled, the consent dialog is not shown.
            if (mAlwaysOn && !TextUtils.equals(mPackage, oldPackage)) {
            if (mAlwaysOn && !isCurrentPreparedPackage(oldPackage)) {
                return false;
            }

@@ -318,9 +449,7 @@ public class Vpn {
        enforceControlPermission();

        // Stop an existing always-on VPN from being dethroned by other apps.
        // TODO: Replace TextUtils.equals by isCurrentPreparedPackage when ConnectivityService
        // can unset always-on after always-on package is uninstalled
        if (mAlwaysOn && !TextUtils.equals(mPackage, newPackage)) {
        if (mAlwaysOn && !isCurrentPreparedPackage(newPackage)) {
            return false;
        }

@@ -862,6 +991,7 @@ public class Vpn {
        setVpnForcedLocked(false);
        mAlwaysOn = false;

        unregisterPackageChangeReceiverLocked();
        // Quit any active connections
        agentDisconnect();
    }