diff options
| -rw-r--r-- | core/java/android/net/IConnectivityManager.aidl | 2 | ||||
| -rw-r--r-- | core/java/android/net/VpnManager.java | 17 | ||||
| -rw-r--r-- | core/java/android/net/VpnService.java | 2 | ||||
| -rw-r--r-- | core/res/res/values/config.xml | 6 | ||||
| -rw-r--r-- | core/res/res/values/symbols.xml | 1 | ||||
| -rw-r--r-- | packages/VpnDialogs/AndroidManifest.xml | 7 | ||||
| -rw-r--r-- | packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java | 13 | ||||
| -rw-r--r-- | packages/VpnDialogs/src/com/android/vpndialogs/PlatformVpnConfirmDialog.java | 29 | ||||
| -rw-r--r-- | services/core/java/com/android/server/ConnectivityService.java | 27 | ||||
| -rw-r--r-- | services/core/java/com/android/server/connectivity/Vpn.java | 77 | ||||
| -rw-r--r-- | tests/net/java/android/net/VpnManagerTest.java | 11 | ||||
| -rw-r--r-- | tests/net/java/com/android/server/connectivity/VpnTest.java | 50 |
12 files changed, 200 insertions, 42 deletions
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index 6baa3832403f..1089a197ff59 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -117,7 +117,7 @@ interface IConnectivityManager boolean prepareVpn(String oldPackage, String newPackage, int userId); - void setVpnPackageAuthorization(String packageName, int userId, boolean authorized); + void setVpnPackageAuthorization(String packageName, int userId, int vpnType); ParcelFileDescriptor establishVpn(in VpnConfig config); diff --git a/core/java/android/net/VpnManager.java b/core/java/android/net/VpnManager.java index e60cc81bf9d2..f19ba0f5ef51 100644 --- a/core/java/android/net/VpnManager.java +++ b/core/java/android/net/VpnManager.java @@ -18,6 +18,7 @@ package android.net; import static com.android.internal.util.Preconditions.checkNotNull; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; @@ -30,6 +31,8 @@ import android.os.RemoteException; import com.android.internal.net.VpnProfile; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.security.GeneralSecurityException; /** @@ -47,6 +50,18 @@ import java.security.GeneralSecurityException; * @see Ikev2VpnProfile */ public class VpnManager { + /** Type representing a lack of VPN @hide */ + public static final int TYPE_VPN_NONE = -1; + /** VPN service type code @hide */ + public static final int TYPE_VPN_SERVICE = 1; + /** Platform VPN type code @hide */ + public static final int TYPE_VPN_PLATFORM = 2; + + /** @hide */ + @IntDef(value = {TYPE_VPN_NONE, TYPE_VPN_SERVICE, TYPE_VPN_PLATFORM}) + @Retention(RetentionPolicy.SOURCE) + public @interface VpnType {} + @NonNull private final Context mContext; @NonNull private final IConnectivityManager mService; @@ -54,7 +69,7 @@ public class VpnManager { final Intent intent = new Intent(); final ComponentName componentName = ComponentName.unflattenFromString( Resources.getSystem().getString( - com.android.internal.R.string.config_customVpnConfirmDialogComponent)); + com.android.internal.R.string.config_platformVpnConfirmDialogComponent)); intent.setComponent(componentName); return intent; } diff --git a/core/java/android/net/VpnService.java b/core/java/android/net/VpnService.java index 4b804b097d59..63e510733907 100644 --- a/core/java/android/net/VpnService.java +++ b/core/java/android/net/VpnService.java @@ -234,7 +234,7 @@ public class VpnService extends Service { if (!cm.prepareVpn(packageName, null, userId)) { cm.prepareVpn(null, packageName, userId); } - cm.setVpnPackageAuthorization(packageName, userId, true); + cm.setVpnPackageAuthorization(packageName, userId, VpnManager.TYPE_VPN_SERVICE); } catch (RemoteException e) { // ignore } diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index d240c9c18702..3d7b1e18be7b 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2565,7 +2565,11 @@ <string name="config_usbResolverActivity" translatable="false" >com.android.systemui/com.android.systemui.usb.UsbResolverActivity</string> - <!-- Name of the dialog that is used to request the user's consent to VPN connection --> + <!-- Name of the dialog that is used to request the user's consent for a Platform VPN --> + <string name="config_platformVpnConfirmDialogComponent" translatable="false" + >com.android.vpndialogs/com.android.vpndialogs.PlatformVpnConfirmDialog</string> + + <!-- Name of the dialog that is used to request the user's consent for a VpnService VPN --> <string name="config_customVpnConfirmDialogComponent" translatable="false" >com.android.vpndialogs/com.android.vpndialogs.ConfirmDialog</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index f47da62654e2..36296a850dcb 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2143,6 +2143,7 @@ <java-symbol type="string" name="config_customAdbPublicKeyConfirmationSecondaryUserComponent" /> <java-symbol type="string" name="config_customVpnConfirmDialogComponent" /> <java-symbol type="string" name="config_customVpnAlwaysOnDisconnectedDialogComponent" /> + <java-symbol type="string" name="config_platformVpnConfirmDialogComponent" /> <java-symbol type="string" name="config_carrierAppInstallDialogComponent" /> <java-symbol type="string" name="config_defaultNetworkScorerPackageName" /> <java-symbol type="string" name="config_persistentDataPackageName" /> diff --git a/packages/VpnDialogs/AndroidManifest.xml b/packages/VpnDialogs/AndroidManifest.xml index e4de6259e07d..693ca52b4ed1 100644 --- a/packages/VpnDialogs/AndroidManifest.xml +++ b/packages/VpnDialogs/AndroidManifest.xml @@ -34,6 +34,13 @@ </intent-filter> </activity> + <activity android:name=".PlatformVpnConfirmDialog" + android:theme="@*android:style/Theme.DeviceDefault.Dialog.Alert.DayNight" + android:noHistory="true" + android:excludeFromRecents="true" + android:exported="true"> + </activity> + <activity android:name=".ManageDialog" android:theme="@*android:style/Theme.DeviceDefault.Dialog.Alert.DayNight" android:noHistory="true" diff --git a/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java b/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java index 48adb9ba3f63..e66f2cc17a7f 100644 --- a/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java +++ b/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java @@ -23,6 +23,7 @@ import android.content.DialogInterface; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.net.IConnectivityManager; +import android.net.VpnManager; import android.os.Bundle; import android.os.RemoteException; import android.os.ServiceManager; @@ -43,10 +44,20 @@ public class ConfirmDialog extends AlertActivity implements DialogInterface.OnClickListener, ImageGetter { private static final String TAG = "VpnConfirm"; + @VpnManager.VpnType private final int mVpnType; + private String mPackage; private IConnectivityManager mService; + public ConfirmDialog() { + this(VpnManager.TYPE_VPN_SERVICE); + } + + public ConfirmDialog(@VpnManager.VpnType int vpnType) { + mVpnType = vpnType; + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -138,7 +149,7 @@ public class ConfirmDialog extends AlertActivity if (mService.prepareVpn(null, mPackage, UserHandle.myUserId())) { // Authorize this app to initiate VPN connections in the future without user // intervention. - mService.setVpnPackageAuthorization(mPackage, UserHandle.myUserId(), true); + mService.setVpnPackageAuthorization(mPackage, UserHandle.myUserId(), mVpnType); setResult(RESULT_OK); } } catch (Exception e) { diff --git a/packages/VpnDialogs/src/com/android/vpndialogs/PlatformVpnConfirmDialog.java b/packages/VpnDialogs/src/com/android/vpndialogs/PlatformVpnConfirmDialog.java new file mode 100644 index 000000000000..e2e297caad14 --- /dev/null +++ b/packages/VpnDialogs/src/com/android/vpndialogs/PlatformVpnConfirmDialog.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.vpndialogs; + +import android.net.VpnManager; + +/** + * PlatformVpnConfirmDialog is a minimal subclass for requesting user consent for platform VPN + * profiles. + */ +public class PlatformVpnConfirmDialog extends ConfirmDialog { + public PlatformVpnConfirmDialog() { + super(VpnManager.TYPE_VPN_PLATFORM); + } +} diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index 3877e9a71f26..2968a816835d 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -109,6 +109,7 @@ import android.net.SocketKeepalive; import android.net.TetheringManager; import android.net.UidRange; import android.net.Uri; +import android.net.VpnManager; import android.net.VpnService; import android.net.metrics.IpConnectivityLog; import android.net.metrics.NetworkEvent; @@ -4317,7 +4318,7 @@ public class ConnectivityService extends IConnectivityManager.Stub throwIfLockdownEnabled(); Vpn vpn = mVpns.get(userId); if (vpn != null) { - return vpn.prepare(oldPackage, newPackage, false); + return vpn.prepare(oldPackage, newPackage, VpnManager.TYPE_VPN_SERVICE); } else { return false; } @@ -4325,26 +4326,29 @@ public class ConnectivityService extends IConnectivityManager.Stub } /** - * Set whether the VPN package has the ability to launch VPNs without user intervention. - * This method is used by system-privileged apps. - * VPN permissions are checked in the {@link Vpn} class. If the caller is not {@code userId}, - * {@link android.Manifest.permission.INTERACT_ACROSS_USERS_FULL} permission is required. + * Set whether the VPN package has the ability to launch VPNs without user intervention. This + * method is used by system-privileged apps. VPN permissions are checked in the {@link Vpn} + * class. If the caller is not {@code userId}, {@link + * android.Manifest.permission.INTERACT_ACROSS_USERS_FULL} permission is required. * * @param packageName The package for which authorization state should change. * @param userId User for whom {@code packageName} is installed. * @param authorized {@code true} if this app should be able to start a VPN connection without - * explicit user approval, {@code false} if not. - * + * explicit user approval, {@code false} if not. + * @param vpnType The {@link VpnManager.VpnType} constant representing what class of VPN + * permissions should be granted. When unauthorizing an app, {@link + * VpnManager.TYPE_VPN_NONE} should be used. * @hide */ @Override - public void setVpnPackageAuthorization(String packageName, int userId, boolean authorized) { + public void setVpnPackageAuthorization( + String packageName, int userId, @VpnManager.VpnType int vpnType) { enforceCrossUserPermission(userId); synchronized (mVpns) { Vpn vpn = mVpns.get(userId); if (vpn != null) { - vpn.setPackageAuthorization(packageName, authorized); + vpn.setPackageAuthorization(packageName, vpnType); } } } @@ -7230,7 +7234,7 @@ public class ConnectivityService extends IConnectivityManager.Stub final String alwaysOnPackage = getAlwaysOnVpnPackage(userId); if (alwaysOnPackage != null) { setAlwaysOnVpnPackage(userId, null, false, null); - setVpnPackageAuthorization(alwaysOnPackage, userId, false); + setVpnPackageAuthorization(alwaysOnPackage, userId, VpnManager.TYPE_VPN_NONE); } // Turn Always-on VPN off @@ -7253,7 +7257,8 @@ public class ConnectivityService extends IConnectivityManager.Stub } else { // Prevent this app (packagename = vpnConfig.user) from initiating // VPN connections in the future without user intervention. - setVpnPackageAuthorization(vpnConfig.user, userId, false); + setVpnPackageAuthorization( + vpnConfig.user, userId, VpnManager.TYPE_VPN_NONE); prepareVpn(null, VpnConfig.LEGACY_VPN, userId); } diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 03795597f47a..89af5b58ff72 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -62,6 +62,7 @@ import android.net.NetworkInfo.DetailedState; import android.net.NetworkProvider; import android.net.RouteInfo; import android.net.UidRange; +import android.net.VpnManager; import android.net.VpnService; import android.os.Binder; import android.os.Build.VERSION_CODES; @@ -519,8 +520,11 @@ public class Vpn { } if (packageName != null) { - // Pre-authorize new always-on VPN package. - if (!setPackageAuthorization(packageName, true)) { + // TODO: Give the minimum permission possible; if there is a Platform VPN profile, only + // grant ACTIVATE_PLATFORM_VPN. + // Pre-authorize new always-on VPN package. Grant the full ACTIVATE_VPN appop, allowing + // both VpnService and Platform VPNs. + if (!setPackageAuthorization(packageName, VpnManager.TYPE_VPN_SERVICE)) { return false; } mAlwaysOn = true; @@ -691,13 +695,12 @@ public class Vpn { * * @param oldPackage The package name of the old VPN application * @param newPackage The package name of the new VPN application - * @param isPlatformVpn Whether the package being prepared is using a platform VPN profile. - * Preparing a platform VPN profile requires only the lesser ACTIVATE_PLATFORM_VPN appop. + * @param vpnType The type of VPN being prepared. One of {@link VpnManager.VpnType} Preparing a + * platform VPN profile requires only the lesser ACTIVATE_PLATFORM_VPN appop. * @return true if the operation succeeded. */ - // TODO: Use an Intdef'd type to represent what kind of VPN the caller is preparing. public synchronized boolean prepare( - String oldPackage, String newPackage, boolean isPlatformVpn) { + String oldPackage, String newPackage, @VpnManager.VpnType int vpnType) { if (oldPackage != null) { // Stop an existing always-on VPN from being dethroned by other apps. if (mAlwaysOn && !isCurrentPreparedPackage(oldPackage)) { @@ -709,13 +712,13 @@ public class Vpn { // The package doesn't match. We return false (to obtain user consent) unless the // user has already consented to that VPN package. if (!oldPackage.equals(VpnConfig.LEGACY_VPN) - && isVpnPreConsented(mContext, oldPackage, isPlatformVpn)) { + && isVpnPreConsented(mContext, oldPackage, vpnType)) { prepareInternal(oldPackage); return true; } return false; } else if (!oldPackage.equals(VpnConfig.LEGACY_VPN) - && !isVpnPreConsented(mContext, oldPackage, isPlatformVpn)) { + && !isVpnPreConsented(mContext, oldPackage, vpnType)) { // Currently prepared VPN is revoked, so unprepare it and return false. prepareInternal(VpnConfig.LEGACY_VPN); return false; @@ -798,25 +801,49 @@ public class Vpn { } } - /** - * Set whether a package has the ability to launch VPNs without user intervention. - */ - public boolean setPackageAuthorization(String packageName, boolean authorized) { + /** Set whether a package has the ability to launch VPNs without user intervention. */ + public boolean setPackageAuthorization(String packageName, @VpnManager.VpnType int vpnType) { // Check if the caller is authorized. enforceControlPermissionOrInternalCaller(); - int uid = getAppUid(packageName, mUserHandle); + final int uid = getAppUid(packageName, mUserHandle); if (uid == -1 || VpnConfig.LEGACY_VPN.equals(packageName)) { // Authorization for nonexistent packages (or fake ones) can't be updated. return false; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { - AppOpsManager appOps = + final int[] toChange; + + // Clear all AppOps if the app is being unauthorized. + switch (vpnType) { + case VpnManager.TYPE_VPN_NONE: + toChange = new int[] { + AppOpsManager.OP_ACTIVATE_VPN, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN + }; + break; + case VpnManager.TYPE_VPN_PLATFORM: + toChange = new int[] {AppOpsManager.OP_ACTIVATE_PLATFORM_VPN}; + break; + case VpnManager.TYPE_VPN_SERVICE: + toChange = new int[] {AppOpsManager.OP_ACTIVATE_VPN}; + break; + default: + Log.wtf(TAG, "Unrecognized VPN type while granting authorization"); + return false; + } + + final AppOpsManager appOpMgr = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); - appOps.setMode(AppOpsManager.OP_ACTIVATE_VPN, uid, packageName, - authorized ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED); + for (final int appOp : toChange) { + appOpMgr.setMode( + appOp, + uid, + packageName, + vpnType == VpnManager.TYPE_VPN_NONE + ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED); + } return true; } catch (Exception e) { Log.wtf(TAG, "Failed to set app ops for package " + packageName + ", uid " + uid, e); @@ -826,11 +853,15 @@ public class Vpn { return false; } - private static boolean isVpnPreConsented( - Context context, String packageName, boolean isPlatformVpn) { - return isPlatformVpn - ? isVpnProfilePreConsented(context, packageName) - : isVpnServicePreConsented(context, packageName); + private static boolean isVpnPreConsented(Context context, String packageName, int vpnType) { + switch (vpnType) { + case VpnManager.TYPE_VPN_SERVICE: + return isVpnServicePreConsented(context, packageName); + case VpnManager.TYPE_VPN_PLATFORM: + return isVpnProfilePreConsented(context, packageName); + default: + return false; + } } private static boolean doesPackageHaveAppop(Context context, String packageName, int appop) { @@ -2377,7 +2408,7 @@ public class Vpn { checkNotNull(keyStore, "KeyStore missing"); // Prepare VPN for startup - if (!prepare(packageName, null /* newPackage */, true /* isPlatformVpn */)) { + if (!prepare(packageName, null /* newPackage */, VpnManager.TYPE_VPN_PLATFORM)) { throw new SecurityException("User consent not granted for package " + packageName); } diff --git a/tests/net/java/android/net/VpnManagerTest.java b/tests/net/java/android/net/VpnManagerTest.java index 97551c94e2ae..95a794235a2e 100644 --- a/tests/net/java/android/net/VpnManagerTest.java +++ b/tests/net/java/android/net/VpnManagerTest.java @@ -16,6 +16,7 @@ package android.net; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.Matchers.any; @@ -24,6 +25,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.ComponentName; +import android.content.Intent; import android.test.mock.MockContext; import androidx.test.filters.SmallTest; @@ -78,7 +81,13 @@ public class VpnManagerTest { when(mMockCs.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME))).thenReturn(false); // Expect intent to be returned, as consent has not already been granted. - assertNotNull(mVpnManager.provisionVpnProfile(profile)); + final Intent intent = mVpnManager.provisionVpnProfile(profile); + assertNotNull(intent); + + final ComponentName expectedComponentName = + ComponentName.unflattenFromString( + "com.android.vpndialogs/com.android.vpndialogs.PlatformVpnConfirmDialog"); + assertEquals(expectedComponentName, intent.getComponent()); verify(mMockCs).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME)); } diff --git a/tests/net/java/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java index 084ec7330236..155c61f3f8c7 100644 --- a/tests/net/java/com/android/server/connectivity/VpnTest.java +++ b/tests/net/java/com/android/server/connectivity/VpnTest.java @@ -63,6 +63,7 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkInfo.DetailedState; import android.net.UidRange; +import android.net.VpnManager; import android.net.VpnService; import android.os.Build.VERSION_CODES; import android.os.Bundle; @@ -471,12 +472,12 @@ public class VpnTest { order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(entireUser)); // When a new VPN package is set the rules should change to cover that package. - vpn.prepare(null, PKGS[0], false /* isPlatformVpn */); + vpn.prepare(null, PKGS[0], VpnManager.TYPE_VPN_SERVICE); order.verify(mNetService).setAllowOnlyVpnForUids(eq(false), aryEq(entireUser)); order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(exceptPkg0)); // When that VPN package is unset, everything should be undone again in reverse. - vpn.prepare(null, VpnConfig.LEGACY_VPN, false /* isPlatformVpn */); + vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE); order.verify(mNetService).setAllowOnlyVpnForUids(eq(false), aryEq(exceptPkg0)); order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(entireUser)); } @@ -817,6 +818,51 @@ public class VpnTest { eq(TEST_VPN_PKG)); } + @Test + public void testSetPackageAuthorizationVpnService() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_SERVICE)); + verify(mAppOps) + .setMode( + eq(AppOpsManager.OP_ACTIVATE_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_ALLOWED)); + } + + @Test + public void testSetPackageAuthorizationPlatformVpn() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_PLATFORM)); + verify(mAppOps) + .setMode( + eq(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_ALLOWED)); + } + + @Test + public void testSetPackageAuthorizationRevokeAuthorization() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_NONE)); + verify(mAppOps) + .setMode( + eq(AppOpsManager.OP_ACTIVATE_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_IGNORED)); + verify(mAppOps) + .setMode( + eq(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_IGNORED)); + } + /** * Mock some methods of vpn object. */ |