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
This commit is contained in:
Charles He
2017-05-15 17:07:18 +01:00
parent 558f97651f
commit 9369e61e2d
4 changed files with 95 additions and 15 deletions

View File

@@ -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
* <ul>
* <li>target {@link VERSION_CODES#N API 24} or above, and
* <li>not opt out through the {@link VpnService#METADATA_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. * Configures an always-on VPN connection through a specific application.
* This connection is automatically granted and persisted after a reboot. * This connection is automatically granted and persisted after a reboot.

View File

@@ -123,6 +123,7 @@ interface IConnectivityManager
VpnInfo[] getAllVpnInfo(); VpnInfo[] getAllVpnInfo();
boolean updateLockdownVpn(); boolean updateLockdownVpn();
boolean isAlwaysOnVpnPackageSupported(int userId, String packageName);
boolean setAlwaysOnVpnPackage(int userId, String packageName, boolean lockdown); boolean setAlwaysOnVpnPackage(int userId, String packageName, boolean lockdown);
String getAlwaysOnVpnPackage(int userId); String getAlwaysOnVpnPackage(int userId);

View File

@@ -40,7 +40,6 @@ import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.database.ContentObserver; import android.database.ContentObserver;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
@@ -52,10 +51,10 @@ import android.net.INetworkPolicyManager;
import android.net.INetworkStatsService; import android.net.INetworkStatsService;
import android.net.LinkProperties; import android.net.LinkProperties;
import android.net.LinkProperties.CompareResult; import android.net.LinkProperties.CompareResult;
import android.net.MatchAllNetworkSpecifier;
import android.net.Network; import android.net.Network;
import android.net.NetworkAgent; import android.net.NetworkAgent;
import android.net.NetworkCapabilities; import android.net.NetworkCapabilities;
import android.net.MatchAllNetworkSpecifier;
import android.net.NetworkConfig; import android.net.NetworkConfig;
import android.net.NetworkInfo; import android.net.NetworkInfo;
import android.net.NetworkInfo.DetailedState; 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.MessageUtils;
import com.android.internal.util.WakeupMessage; import com.android.internal.util.WakeupMessage;
import com.android.internal.util.XmlUtils; import com.android.internal.util.XmlUtils;
import com.android.server.LocalServices;
import com.android.server.am.BatteryStatsService; import com.android.server.am.BatteryStatsService;
import com.android.server.connectivity.DataConnectionStats; import com.android.server.connectivity.DataConnectionStats;
import com.android.server.connectivity.KeepaliveTracker; import com.android.server.connectivity.KeepaliveTracker;
import com.android.server.connectivity.LingerMonitor;
import com.android.server.connectivity.MockableSystemProperties; import com.android.server.connectivity.MockableSystemProperties;
import com.android.server.connectivity.Nat464Xlat; import com.android.server.connectivity.Nat464Xlat;
import com.android.server.connectivity.LingerMonitor;
import com.android.server.connectivity.NetworkAgentInfo; import com.android.server.connectivity.NetworkAgentInfo;
import com.android.server.connectivity.NetworkDiagnostics; import com.android.server.connectivity.NetworkDiagnostics;
import com.android.server.connectivity.NetworkMonitor; 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.PacManager;
import com.android.server.connectivity.PermissionMonitor; import com.android.server.connectivity.PermissionMonitor;
import com.android.server.connectivity.Tethering; import com.android.server.connectivity.Tethering;
import com.android.server.connectivity.tethering.TetheringDependencies;
import com.android.server.connectivity.Vpn; import com.android.server.connectivity.Vpn;
import com.android.server.connectivity.tethering.TetheringDependencies;
import com.android.server.net.BaseNetworkObserver; import com.android.server.net.BaseNetworkObserver;
import com.android.server.net.LockdownVpnTracker; import com.android.server.net.LockdownVpnTracker;
import com.android.server.net.NetworkPolicyManagerInternal; import com.android.server.net.NetworkPolicyManagerInternal;
@@ -1494,6 +1492,12 @@ public class ConnectivityService extends IConnectivityManager.Stub
ConnectivityManager.enforceChangePermission(mContext); ConnectivityManager.enforceChangePermission(mContext);
} }
private void enforceSettingsPermission() {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.NETWORK_SETTINGS,
"ConnectivityService");
}
private void enforceTetherAccessPermission() { private void enforceTetherAccessPermission() {
mContext.enforceCallingOrSelfPermission( mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.ACCESS_NETWORK_STATE, 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 @Override
public boolean setAlwaysOnVpnPackage(int userId, String packageName, boolean lockdown) { public boolean setAlwaysOnVpnPackage(int userId, String packageName, boolean lockdown) {
enforceConnectivityInternalPermission(); enforceConnectivityInternalPermission();

View File

@@ -27,13 +27,16 @@ import android.annotation.UserIdInt;
import android.app.AppOpsManager; import android.app.AppOpsManager;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo; import android.content.pm.UserInfo;
import android.net.NetworkInfo.DetailedState; import android.net.NetworkInfo.DetailedState;
import android.net.UidRange; 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.INetworkManagementService;
import android.os.Looper; import android.os.Looper;
import android.os.UserHandle; import android.os.UserHandle;
@@ -45,22 +48,22 @@ import android.util.ArraySet;
import com.android.internal.net.VpnConfig; 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.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder; import org.mockito.InOrder;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; 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}. * Tests for {@link Vpn}.
* *
* Build, install and run with: * 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 { public class VpnTest extends AndroidTestCase {
private static final String TAG = "VpnTest"; private static final String TAG = "VpnTest";
@@ -116,7 +119,7 @@ public class VpnTest extends AndroidTestCase {
// Used by {@link Notification.Builder} // Used by {@link Notification.Builder}
ApplicationInfo applicationInfo = new ApplicationInfo(); ApplicationInfo applicationInfo = new ApplicationInfo();
applicationInfo.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; applicationInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT;
when(mContext.getApplicationInfo()).thenReturn(applicationInfo); when(mContext.getApplicationInfo()).thenReturn(applicationInfo);
doNothing().when(mNetService).registerObserver(any()); doNothing().when(mNetService).registerObserver(any());
@@ -314,6 +317,40 @@ public class VpnTest extends AndroidTestCase {
order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(entireUser)); 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 @SmallTest
public void testNotificationShownForAlwaysOnApp() { public void testNotificationShownForAlwaysOnApp() {
final UserHandle userHandle = UserHandle.of(primaryUser.id); final UserHandle userHandle = UserHandle.of(primaryUser.id);