diff --git a/api/current.txt b/api/current.txt index ef2abe64414ba..8582610b8b9aa 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 d8066f46a8772..1bb7fdf8bd731 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -28239,6 +28239,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 5eebafd014cb1..753e17d542d76 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 a749fe70fb94d..f8aa948793224 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -3928,28 +3928,20 @@ public class DevicePolicyManager { return null; } - /** - * 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. *
- * 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 + *
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}. + * + *
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, + *
{@code
+ *
+ *
+ *
+ *
+ *
+ *
+ * }
+ *
+ * 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 31fc874db2fb1..f39bd7f71e885 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, @@ -3645,6 +3651,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/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 27968a99cb028..56cff7c715d63 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; @@ -295,6 +299,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 + *
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 506d9e5043f4b..296cb76560af4 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.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);