From 812800cb92090db31f609b907c4458ba76cf7f42 Mon Sep 17 00:00:00 2001 From: Robin Lee Date: Fri, 13 May 2016 15:38:08 +0100 Subject: 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 --- .../com/android/server/ConnectivityService.java | 34 +---- .../java/com/android/server/connectivity/Vpn.java | 150 +++++++++++++++++++-- 2 files changed, 142 insertions(+), 42 deletions(-) diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index 6a0d48872a6b..acf8009e34a6 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -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; } diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 1bdb48a23d9d..ebacc71515bb 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -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(); } -- cgit v1.2.3-59-g8ed1b