Merge changes from topic "always-on-vpn"

* changes:
  Opt-out for always-on VPN: rename API.
  Opt-out for always-on VPN
This commit is contained in:
Charles He
2017-09-19 07:50:13 +00:00
committed by Gerrit Code Review
10 changed files with 203 additions and 31 deletions

View File

@@ -26002,6 +26002,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 {

View File

@@ -28266,6 +28266,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 {

View File

@@ -26111,6 +26111,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 {

View File

@@ -55,7 +55,6 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.ContactsContract.Directory;
import android.provider.Settings;
import android.security.Credentials;
import android.service.restrictions.RestrictionsReceiver;
import android.telephony.TelephonyManager;
@@ -3902,28 +3901,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.
* <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.

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#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.

View File

@@ -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);

View File

@@ -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;
@@ -123,6 +121,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.

View File

@@ -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();

View File

@@ -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
* <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;

View File

@@ -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);