diff options
| author | 2017-09-19 08:09:41 +0000 | |
|---|---|---|
| committer | 2017-09-19 08:09:41 +0000 | |
| commit | 01014743c054b7f0ad0132933afdd020026c9e05 (patch) | |
| tree | ddb5b5d93e8887deed8db2e5c948533b2b038519 | |
| parent | 19625788a13e72bc44f103d8ba466947c27dae7d (diff) | |
| parent | 692b55390fd5d8e010c6d994bf2dd5dd9849137e (diff) | |
Merge changes from topic "always-on-vpn" am: cc15c7f8c1
am: 692b55390f
Change-Id: I3931c053c4daff092b06e3e1d673817708762a62
| -rw-r--r-- | api/current.txt | 1 | ||||
| -rw-r--r-- | api/system-current.txt | 1 | ||||
| -rw-r--r-- | api/test-current.txt | 1 | ||||
| -rw-r--r-- | core/java/android/app/admin/DevicePolicyManager.java | 26 | ||||
| -rw-r--r-- | core/java/android/net/ConnectivityManager.java | 23 | ||||
| -rw-r--r-- | core/java/android/net/IConnectivityManager.aidl | 1 | ||||
| -rw-r--r-- | core/java/android/net/VpnService.java | 32 | ||||
| -rw-r--r-- | services/core/java/com/android/server/ConnectivityService.java | 23 | ||||
| -rw-r--r-- | services/core/java/com/android/server/connectivity/Vpn.java | 68 | ||||
| -rw-r--r-- | tests/net/java/com/android/server/connectivity/VpnTest.java | 57 |
10 files changed, 203 insertions, 30 deletions
diff --git a/api/current.txt b/api/current.txt index ef2abe64414b..8582610b8b9a 100644 --- a/api/current.txt +++ b/api/current.txt @@ -25975,6 +25975,7 @@ package android.net { method public boolean protect(java.net.DatagramSocket); method public boolean setUnderlyingNetworks(android.net.Network[]); field public static final java.lang.String SERVICE_INTERFACE = "android.net.VpnService"; + field public static final java.lang.String SERVICE_META_DATA_SUPPORTS_ALWAYS_ON = "android.net.VpnService.SUPPORTS_ALWAYS_ON"; } public class VpnService.Builder { diff --git a/api/system-current.txt b/api/system-current.txt index a634d41dfcc4..9575dad48266 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -28240,6 +28240,7 @@ package android.net { method public boolean protect(java.net.DatagramSocket); method public boolean setUnderlyingNetworks(android.net.Network[]); field public static final java.lang.String SERVICE_INTERFACE = "android.net.VpnService"; + field public static final java.lang.String SERVICE_META_DATA_SUPPORTS_ALWAYS_ON = "android.net.VpnService.SUPPORTS_ALWAYS_ON"; } public class VpnService.Builder { diff --git a/api/test-current.txt b/api/test-current.txt index 5eebafd014cb..753e17d542d7 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -26085,6 +26085,7 @@ package android.net { method public boolean protect(java.net.DatagramSocket); method public boolean setUnderlyingNetworks(android.net.Network[]); field public static final java.lang.String SERVICE_INTERFACE = "android.net.VpnService"; + field public static final java.lang.String SERVICE_META_DATA_SUPPORTS_ALWAYS_ON = "android.net.VpnService.SUPPORTS_ALWAYS_ON"; } public class VpnService.Builder { diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index a749fe70fb94..f8aa94879322 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -3930,26 +3930,18 @@ public class DevicePolicyManager { /** * Called by a device or profile owner to configure an always-on VPN connection through a - * specific application for the current user. - * - * @deprecated this version only exists for compability with previous developer preview builds. - * TODO: delete once there are no longer any live references. - * @hide - */ - @Deprecated - public void setAlwaysOnVpnPackage(@NonNull ComponentName admin, @Nullable String vpnPackage) - throws NameNotFoundException, UnsupportedOperationException { - setAlwaysOnVpnPackage(admin, vpnPackage, /* lockdownEnabled */ true); - } - - /** - * Called by a device or profile owner to configure an always-on VPN connection through a * specific application for the current user. This connection is automatically granted and * persisted after a reboot. * <p> - * The designated package should declare a {@link android.net.VpnService} in its manifest - * guarded by {@link android.Manifest.permission#BIND_VPN_SERVICE}, otherwise the call will - * fail. + * To support the always-on feature, an app must + * <ul> + * <li>declare a {@link android.net.VpnService} in its manifest, guarded by + * {@link android.Manifest.permission#BIND_VPN_SERVICE};</li> + * <li>target {@link android.os.Build.VERSION_CODES#N API 24} or above; and</li> + * <li><i>not</i> explicitly opt out of the feature through + * {@link android.net.VpnService#SERVICE_META_DATA_SUPPORTS_ALWAYS_ON}.</li> + * </ul> + * The call will fail if called with the package name of an unsupported VPN app. * * @param vpnPackage The package name for an installed VPN app on the device, or {@code null} to * remove an existing always-on VPN configuration. diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index d91482331f09..3460f564fcb8 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -835,6 +835,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 + * <ul> + * <li>target {@link VERSION_CODES#N API 24} or above, and + * <li>not opt out through the {@link VpnService#SERVICE_META_DATA_SUPPORTS_ALWAYS_ON} + * meta-data field. + * </ul> + * + * @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 27729dcce779..b9dd207aaa42 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/core/java/android/net/VpnService.java b/core/java/android/net/VpnService.java index 4b79cbb98d8c..185b1818df45 100644 --- a/core/java/android/net/VpnService.java +++ b/core/java/android/net/VpnService.java @@ -28,8 +28,6 @@ import android.content.Context; import android.content.Intent; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; -import android.net.Network; -import android.net.NetworkUtils; import android.os.Binder; import android.os.IBinder; import android.os.Parcel; @@ -131,6 +129,36 @@ public class VpnService extends Service { public static final String SERVICE_INTERFACE = VpnConfig.SERVICE_INTERFACE; /** + * Key for boolean meta-data field indicating whether this VpnService supports always-on mode. + * + * <p>For a VPN app targeting {@link android.os.Build.VERSION_CODES#N API 24} or above, Android + * provides users with the ability to set it as always-on, so that VPN connection is + * persisted after device reboot and app upgrade. Always-on VPN can also be enabled by device + * owner and profile owner apps through + * {@link android.app.admin.DevicePolicyManager#setAlwaysOnVpnPackage}. + * + * <p>VPN apps not supporting this feature should opt out by adding this meta-data field to the + * {@code VpnService} component of {@code AndroidManifest.xml}. In case there is more than one + * {@code VpnService} component defined in {@code AndroidManifest.xml}, opting out any one of + * them will opt out the entire app. For example, + * <pre> {@code + * <service android:name=".ExampleVpnService" + * android:permission="android.permission.BIND_VPN_SERVICE"> + * <intent-filter> + * <action android:name="android.net.VpnService"/> + * </intent-filter> + * <meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON" + * android:value=false/> + * </service> + * } </pre> + * + * <p>This meta-data field defaults to {@code true} if absent. It will only have effect on + * {@link android.os.Build.VERSION_CODES#O_MR1} or higher. + */ + public static final String SERVICE_META_DATA_SUPPORTS_ALWAYS_ON = + "android.net.VpnService.SUPPORTS_ALWAYS_ON"; + + /** * Use IConnectivityManager since those methods are hidden and not * available in ConnectivityManager. */ diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index 31fc874db2fb..f39bd7f71e88 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -128,9 +128,9 @@ 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; @@ -1515,6 +1515,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, @@ -3646,6 +3652,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(); enforceCrossUserPermission(userId); diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 27968a99cb02..56cff7c715d6 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -36,6 +36,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; @@ -56,7 +57,10 @@ import android.net.NetworkMisc; import android.net.RouteInfo; import android.net.UidRange; import android.net.Uri; +import android.net.VpnService; import android.os.Binder; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; import android.os.FileUtils; import android.os.IBinder; import android.os.INetworkManagementService; @@ -296,6 +300,56 @@ public class Vpn { } /** + * Checks if a VPN app supports always-on mode. + * + * In order to support the always-on feature, an app has to + * <ul> + * <li>target {@link VERSION_CODES#N API 24} or above, and + * <li>not opt out through the {@link VpnService#SERVICE_META_DATA_SUPPORTS_ALWAYS_ON} + * meta-data field. + * </ul> + * + * @param packageName the canonical package name of the VPN app + * @return {@code true} if and only if the VPN app exists and supports always-on mode + */ + public boolean isAlwaysOnPackageSupported(String packageName) { + enforceSettingsPermission(); + + if (packageName == null) { + return false; + } + + PackageManager pm = mContext.getPackageManager(); + ApplicationInfo appInfo = null; + try { + appInfo = pm.getApplicationInfoAsUser(packageName, 0 /*flags*/, mUserHandle); + } catch (NameNotFoundException unused) { + Log.w(TAG, "Can't find \"" + packageName + "\" when checking always-on support"); + } + if (appInfo == null || appInfo.targetSdkVersion < VERSION_CODES.N) { + return false; + } + + final Intent intent = new Intent(VpnConfig.SERVICE_INTERFACE); + intent.setPackage(packageName); + List<ResolveInfo> services = + pm.queryIntentServicesAsUser(intent, PackageManager.GET_META_DATA, mUserHandle); + if (services == null || services.size() == 0) { + return false; + } + + for (ResolveInfo rInfo : services) { + final Bundle metaData = rInfo.serviceInfo.metaData; + if (metaData != null && + !metaData.getBoolean(VpnService.SERVICE_META_DATA_SUPPORTS_ALWAYS_ON, true)) { + return false; + } + } + + return true; + } + + /** * Configures an always-on VPN connection through a specific application. * This connection is automatically granted and persisted after a reboot. * @@ -303,6 +357,10 @@ public class Vpn { * manifest guarded by {@link android.Manifest.permission.BIND_VPN_SERVICE}, * otherwise the call will fail. * + * <p>Note that this method does not check if the VPN app supports always-on mode. The check is + * delayed to {@link #startAlwaysOnVpn()}, which is always called immediately after this + * method in {@link android.net.IConnectivityManager#setAlwaysOnVpnPackage}. + * * @param packageName the package to designate as always-on VPN supplier. * @param lockdown whether to prevent traffic outside of a VPN, for example while connecting. * @return {@code true} if the package has been set as always-on, {@code false} otherwise. @@ -443,6 +501,11 @@ public class Vpn { if (alwaysOnPackage == null) { return true; } + // Remove always-on VPN if it's not supported. + if (!isAlwaysOnPackageSupported(alwaysOnPackage)) { + setAlwaysOnPackage(null, false); + return false; + } // 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. @@ -1219,6 +1282,11 @@ public class Vpn { "Unauthorized Caller"); } + private void enforceSettingsPermission() { + mContext.enforceCallingOrSelfPermission(Manifest.permission.NETWORK_SETTINGS, + "Unauthorized Caller"); + } + private class Connection implements ServiceConnection { private IBinder mService; diff --git a/tests/net/java/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java index 506d9e5043f4..296cb76560af 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()); @@ -315,6 +318,40 @@ public class VpnTest extends AndroidTestCase { } @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.SERVICE_META_DATA_SUPPORTS_ALWAYS_ON, false); + svcInfo.metaData = metaData; + assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0])); + } + + @SmallTest public void testNotificationShownForAlwaysOnApp() { final UserHandle userHandle = UserHandle.of(primaryUser.id); final Vpn vpn = createVpn(primaryUser.id); |