From 9369e61e2d6947c053d0b788a7d36c5db6951ede Mon Sep 17 00:00:00 2001 From: Charles He Date: Mon, 15 May 2017 17:07:18 +0100 Subject: [PATCH] Opt-out for always-on VPN Always-on VPN is a feature introduced in N. Since then, all VPN apps targeting N+ are assumed to support the feature, and the user or the DPC can turn on / off always-on for any such VPN app. However, a few VPN apps are not designed to support the always-on feature. Enabling always-on for these apps will result in undefined behavior and confusing "Always-on VPN disconnected" notification. This feature provides a new manifest meta-data field through which a VPN app can opt out of the always-on feature explicitly. This will stop the always-on feature from being enabled for the app, both by the user and by the DPC, and will clear its existing always-on state. A @hide API is provided to check whether an app supports always-on VPN. Documentation is updated to reflect the behavior change. Bug: 36650087 Test: runtest --path java/com/android/server/connectivity/VpnTest.java Test: cts-tradefed run cts --module CtsDevicePolicyManagerTestCases --test 'com.android.cts.devicepolicy.MixedDeviceOwnerTest#testAlwaysOnVpnUnsupportedPackage' Test: cts-tradefed run cts --module CtsDevicePolicyManagerTestCases --test 'com.android.cts.devicepolicy.MixedDeviceOwnerTest#testAlwaysOnVpnUnsupportedPackageReplaced' Test: cts-tradefed run cts --module CtsDevicePolicyManagerTestCases --test 'com.android.cts.devicepolicy.MixedProfileOwnerTest#testAlwaysOnVpnUnsupportedPackage' Test: cts-tradefed run cts --module CtsDevicePolicyManagerTestCases --test 'com.android.cts.devicepolicy.MixedProfileOwnerTest#testAlwaysOnVpnUnsupportedPackageReplaced' Test: cts-tradefed run cts --module CtsDevicePolicyManagerTestCases --test 'com.android.cts.devicepolicy.MixedManagedProfileOwnerTest#testAlwaysOnVpnUnsupportedPackage' Test: cts-tradefed run cts --module CtsDevicePolicyManagerTestCases --test 'com.android.cts.devicepolicy.MixedManagedProfileOwnerTest#testAlwaysOnVpnUnsupportedPackageReplaced' Change-Id: I477897a29175e3994d4ecf8ec546e26043c90f13 --- .../java/android/net/ConnectivityManager.java | 23 ++++++++ .../android/net/IConnectivityManager.aidl | 1 + .../android/server/ConnectivityService.java | 29 ++++++++-- .../android/server/connectivity/VpnTest.java | 57 +++++++++++++++---- 4 files changed, 95 insertions(+), 15 deletions(-) diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index 7a1d85c93d..48123fede6 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -834,6 +834,29 @@ public class ConnectivityManager { } } + /** + * Checks if a VPN app supports always-on mode. + * + * In order to support the always-on feature, an app has to + * + * + * @param userId The identifier of the user for whom the VPN app is installed. + * @param vpnPackage The canonical package name of the VPN app. + * @return {@code true} if and only if the VPN app exists and supports always-on mode. + * @hide + */ + public boolean isAlwaysOnVpnPackageSupportedForUser(int userId, @Nullable String vpnPackage) { + try { + return mService.isAlwaysOnVpnPackageSupported(userId, vpnPackage); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Configures an always-on VPN connection through a specific application. * This connection is automatically granted and persisted after a reboot. diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index 14cee3604d..a6fe7389bc 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -123,6 +123,7 @@ interface IConnectivityManager VpnInfo[] getAllVpnInfo(); boolean updateLockdownVpn(); + boolean isAlwaysOnVpnPackageSupported(int userId, String packageName); boolean setAlwaysOnVpnPackage(int userId, String packageName, boolean lockdown); String getAlwaysOnVpnPackage(int userId); diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index 8200289a76..71c423c58d 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -40,7 +40,6 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageManager; import android.content.res.Configuration; import android.database.ContentObserver; import android.net.ConnectivityManager; @@ -52,10 +51,10 @@ import android.net.INetworkPolicyManager; import android.net.INetworkStatsService; import android.net.LinkProperties; import android.net.LinkProperties.CompareResult; +import android.net.MatchAllNetworkSpecifier; import android.net.Network; import android.net.NetworkAgent; import android.net.NetworkCapabilities; -import android.net.MatchAllNetworkSpecifier; import android.net.NetworkConfig; import android.net.NetworkInfo; import android.net.NetworkInfo.DetailedState; @@ -124,13 +123,12 @@ import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.MessageUtils; import com.android.internal.util.WakeupMessage; import com.android.internal.util.XmlUtils; -import com.android.server.LocalServices; import com.android.server.am.BatteryStatsService; import com.android.server.connectivity.DataConnectionStats; import com.android.server.connectivity.KeepaliveTracker; +import com.android.server.connectivity.LingerMonitor; import com.android.server.connectivity.MockableSystemProperties; import com.android.server.connectivity.Nat464Xlat; -import com.android.server.connectivity.LingerMonitor; import com.android.server.connectivity.NetworkAgentInfo; import com.android.server.connectivity.NetworkDiagnostics; import com.android.server.connectivity.NetworkMonitor; @@ -139,8 +137,8 @@ import com.android.server.connectivity.NetworkNotificationManager.NotificationTy import com.android.server.connectivity.PacManager; import com.android.server.connectivity.PermissionMonitor; import com.android.server.connectivity.Tethering; -import com.android.server.connectivity.tethering.TetheringDependencies; import com.android.server.connectivity.Vpn; +import com.android.server.connectivity.tethering.TetheringDependencies; import com.android.server.net.BaseNetworkObserver; import com.android.server.net.LockdownVpnTracker; import com.android.server.net.NetworkPolicyManagerInternal; @@ -1494,6 +1492,12 @@ public class ConnectivityService extends IConnectivityManager.Stub ConnectivityManager.enforceChangePermission(mContext); } + private void enforceSettingsPermission() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.NETWORK_SETTINGS, + "ConnectivityService"); + } + private void enforceTetherAccessPermission() { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.ACCESS_NETWORK_STATE, @@ -3624,6 +3628,21 @@ public class ConnectivityService extends IConnectivityManager.Stub } } + @Override + public boolean isAlwaysOnVpnPackageSupported(int userId, String packageName) { + enforceSettingsPermission(); + enforceCrossUserPermission(userId); + + synchronized (mVpns) { + Vpn vpn = mVpns.get(userId); + if (vpn == null) { + Slog.w(TAG, "User " + userId + " has no Vpn configuration"); + return false; + } + return vpn.isAlwaysOnPackageSupported(packageName); + } + } + @Override public boolean setAlwaysOnVpnPackage(int userId, String packageName, boolean lockdown) { enforceConnectivityInternalPermission(); diff --git a/tests/net/java/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java index 506d9e5043..f0b3724955 100644 --- a/tests/net/java/com/android/server/connectivity/VpnTest.java +++ b/tests/net/java/com/android/server/connectivity/VpnTest.java @@ -27,13 +27,16 @@ import android.annotation.UserIdInt; import android.app.AppOpsManager; import android.app.NotificationManager; import android.content.Context; -import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; import android.net.NetworkInfo.DetailedState; import android.net.UidRange; -import android.os.Build; +import android.net.VpnService; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; import android.os.INetworkManagementService; import android.os.Looper; import android.os.UserHandle; @@ -45,22 +48,22 @@ import android.util.ArraySet; import com.android.internal.net.VpnConfig; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Map; -import java.util.Set; - import org.mockito.Answers; -import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + /** * Tests for {@link Vpn}. * * Build, install and run with: - * runtest --path src/com/android/server/connectivity/VpnTest.java + * runtest --path java/com/android/server/connectivity/VpnTest.java */ public class VpnTest extends AndroidTestCase { private static final String TAG = "VpnTest"; @@ -116,7 +119,7 @@ public class VpnTest extends AndroidTestCase { // Used by {@link Notification.Builder} ApplicationInfo applicationInfo = new ApplicationInfo(); - applicationInfo.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; + applicationInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT; when(mContext.getApplicationInfo()).thenReturn(applicationInfo); doNothing().when(mNetService).registerObserver(any()); @@ -314,6 +317,40 @@ public class VpnTest extends AndroidTestCase { order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(entireUser)); } + @SmallTest + public void testIsAlwaysOnPackageSupported() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + + ApplicationInfo appInfo = new ApplicationInfo(); + when(mPackageManager.getApplicationInfoAsUser(eq(PKGS[0]), anyInt(), eq(primaryUser.id))) + .thenReturn(appInfo); + + ServiceInfo svcInfo = new ServiceInfo(); + ResolveInfo resInfo = new ResolveInfo(); + resInfo.serviceInfo = svcInfo; + when(mPackageManager.queryIntentServicesAsUser(any(), eq(PackageManager.GET_META_DATA), + eq(primaryUser.id))) + .thenReturn(Collections.singletonList(resInfo)); + + // null package name should return false + assertFalse(vpn.isAlwaysOnPackageSupported(null)); + + // Pre-N apps are not supported + appInfo.targetSdkVersion = VERSION_CODES.M; + assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0])); + + // N+ apps are supported by default + appInfo.targetSdkVersion = VERSION_CODES.N; + assertTrue(vpn.isAlwaysOnPackageSupported(PKGS[0])); + + // Apps that opt out explicitly are not supported + appInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT; + Bundle metaData = new Bundle(); + metaData.putBoolean(VpnService.METADATA_SUPPORTS_ALWAYS_ON, false); + svcInfo.metaData = metaData; + assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0])); + } + @SmallTest public void testNotificationShownForAlwaysOnApp() { final UserHandle userHandle = UserHandle.of(primaryUser.id);