diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-03-04 22:21:36 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2024-03-04 22:21:36 +0000 |
commit | 472242e2fa81aa586fbace7a74109b8b1ed47e3d (patch) | |
tree | 74ec6a43117fbb9016a9688ad7ff1861f6267d49 | |
parent | b1750e388469e55e941df56d7fe77fccaaf5ed9e (diff) | |
parent | afbfded10cc17736b1f10f881dcafcd8d45303c2 (diff) | |
download | base-simpleperf-release.tar.gz |
Merge "Snap for 11526323 from a42c8c5a45864920f6f6d645f016dbc4e7cc98f7 to simpleperf-release" into simpleperf-releasesimpleperf-release
36 files changed, 3978 insertions, 169 deletions
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp index 2edbd9138f6e..d94890e23ae5 100644 --- a/api/StubLibraries.bp +++ b/api/StubLibraries.bp @@ -458,13 +458,21 @@ java_defaults { libs: ["stub-annotations"], } +java_defaults { + name: "android-non-updatable_everything_from_text_defaults", + defaults: [ + "android-non-updatable_from_text_defaults", + ], + stubs_type: "everything", +} + java_api_library { name: "android-non-updatable.stubs.from-text", api_surface: "public", api_contributions: [ "api-stubs-docs-non-updatable.api.contribution", ], - defaults: ["android-non-updatable_from_text_defaults"], + defaults: ["android-non-updatable_everything_from_text_defaults"], full_api_surface_stub: "android_stubs_current.from-text", } @@ -475,7 +483,7 @@ java_api_library { "api-stubs-docs-non-updatable.api.contribution", "system-api-stubs-docs-non-updatable.api.contribution", ], - defaults: ["android-non-updatable_from_text_defaults"], + defaults: ["android-non-updatable_everything_from_text_defaults"], full_api_surface_stub: "android_system_stubs_current.from-text", } @@ -487,7 +495,7 @@ java_api_library { "system-api-stubs-docs-non-updatable.api.contribution", "test-api-stubs-docs-non-updatable.api.contribution", ], - defaults: ["android-non-updatable_from_text_defaults"], + defaults: ["android-non-updatable_everything_from_text_defaults"], full_api_surface_stub: "android_test_stubs_current.from-text", } @@ -499,7 +507,7 @@ java_api_library { "system-api-stubs-docs-non-updatable.api.contribution", "module-lib-api-stubs-docs-non-updatable.api.contribution", ], - defaults: ["android-non-updatable_from_text_defaults"], + defaults: ["android-non-updatable_everything_from_text_defaults"], full_api_surface_stub: "android_module_lib_stubs_current_full.from-text", } @@ -515,7 +523,7 @@ java_api_library { "test-api-stubs-docs-non-updatable.api.contribution", "module-lib-api-stubs-docs-non-updatable.api.contribution", ], - defaults: ["android-non-updatable_from_text_defaults"], + defaults: ["android-non-updatable_everything_from_text_defaults"], full_api_surface_stub: "android_test_module_lib_stubs_current.from-text", // This module is only used for hiddenapi, and other modules should not @@ -827,6 +835,7 @@ java_api_library { ], visibility: ["//visibility:public"], enable_validation: false, + stubs_type: "everything", } java_api_library { @@ -843,6 +852,7 @@ java_api_library { ], visibility: ["//visibility:public"], enable_validation: false, + stubs_type: "everything", } java_api_library { @@ -861,6 +871,7 @@ java_api_library { ], visibility: ["//visibility:public"], enable_validation: false, + stubs_type: "everything", } java_api_library { @@ -879,6 +890,7 @@ java_api_library { "system-api-stubs-docs-non-updatable.api.contribution", ], enable_validation: false, + stubs_type: "everything", } java_api_library { @@ -899,6 +911,7 @@ java_api_library { ], visibility: ["//visibility:public"], enable_validation: false, + stubs_type: "everything", } java_api_library { @@ -913,6 +926,7 @@ java_api_library { ], visibility: ["//visibility:public"], enable_validation: false, + stubs_type: "everything", } java_api_library { @@ -938,6 +952,7 @@ java_api_library { "//visibility:private", ], enable_validation: false, + stubs_type: "everything", } java_api_library { @@ -955,6 +970,7 @@ java_api_library { ], visibility: ["//visibility:public"], enable_validation: false, + stubs_type: "everything", } //////////////////////////////////////////////////////////////////////// diff --git a/cmds/svc/src/com/android/commands/svc/PowerCommand.java b/cmds/svc/src/com/android/commands/svc/PowerCommand.java index a7560b23c6bd..12b79f4c42f8 100644 --- a/cmds/svc/src/com/android/commands/svc/PowerCommand.java +++ b/cmds/svc/src/com/android/commands/svc/PowerCommand.java @@ -23,8 +23,6 @@ import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; -import android.os.SystemProperties; -import android.sysprop.InitProperties; public class PowerCommand extends Svc.Command { private static final int FORCE_SUSPEND_DELAY_DEFAULT_MILLIS = 0; @@ -142,12 +140,10 @@ public class PowerCommand extends Svc.Command { // Check if remote exception is benign during shutdown. Pm can be killed // before system server during shutdown, so remote exception can be ignored // if it is already in shutdown flow. + // sys.powerctl is no longer set to avoid a possible DOS attack (see + // bionic/libc/bionic/system_property_set.cpp) so we have no real way of knowing if a + // remote exception is real or simply because pm is killed (b/318323013) + // So we simply do not display anything. private void maybeLogRemoteException(String msg) { - String powerProp = SystemProperties.get("sys.powerctl"); - // Also check if userspace reboot is ongoing, since in case of userspace reboot value of the - // sys.powerctl property will be reset. - if (powerProp.isEmpty() && !InitProperties.userspace_reboot_in_progress().orElse(false)) { - System.err.println(msg); - } } } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 12802e4d3a3d..04367197100b 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -10583,7 +10583,7 @@ package android.os { method public final double readDouble(); method public final java.util.ArrayList<java.lang.Double> readDoubleVector(); method public final android.os.HwBlob readEmbeddedBuffer(long, long, long, boolean); - method @NonNull @Nullable public final android.os.HidlMemory readEmbeddedHidlMemory(long, long, long); + method @NonNull public final android.os.HidlMemory readEmbeddedHidlMemory(long, long, long); method @Nullable public final android.os.NativeHandle readEmbeddedNativeHandle(long, long); method public final float readFloat(); method public final java.util.ArrayList<java.lang.Float> readFloatVector(); diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java index 4b3eb3a000ff..0d2a8cbd7795 100644 --- a/core/java/android/database/sqlite/SQLiteConnection.java +++ b/core/java/android/database/sqlite/SQLiteConnection.java @@ -379,8 +379,6 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen return; } - Log.i(TAG, walFile.getAbsolutePath() + " " + size + " bytes: Bigger than " - + threshold + "; truncating"); try { executeForString("PRAGMA wal_checkpoint(TRUNCATE)", null, null); mConfiguration.shouldTruncateWalFile = false; diff --git a/core/java/android/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java index 5e523c0112b1..78c8954cfe5f 100644 --- a/core/java/android/database/sqlite/SQLiteOpenHelper.java +++ b/core/java/android/database/sqlite/SQLiteOpenHelper.java @@ -377,8 +377,7 @@ public abstract class SQLiteOpenHelper implements AutoCloseable { if (writable) { throw ex; } - Log.e(TAG, "Couldn't open " + mName - + " for writing (will try read-only):", ex); + Log.e(TAG, "Couldn't open database for writing (will try read-only):", ex); params = params.toBuilder().addOpenFlags(SQLiteDatabase.OPEN_READONLY).build(); db = SQLiteDatabase.openDatabase(filePath, params); } @@ -425,11 +424,6 @@ public abstract class SQLiteOpenHelper implements AutoCloseable { } onOpen(db); - - if (db.isReadOnly()) { - Log.w(TAG, "Opened " + mName + " in read-only mode"); - } - mDatabase = db; return db; } finally { diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java index d6c58ac1d7fe..738b6d9ca27b 100644 --- a/core/java/android/hardware/camera2/params/OutputConfiguration.java +++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java @@ -1379,7 +1379,6 @@ public final class OutputConfiguration implements Parcelable { mSurfaceType != other.mSurfaceType || mIsDeferredConfig != other.mIsDeferredConfig || mIsShared != other.mIsShared || - mConfiguredFormat != other.mConfiguredFormat || mConfiguredDataspace != other.mConfiguredDataspace || mConfiguredGenerationId != other.mConfiguredGenerationId || !Objects.equals(mPhysicalCameraId, other.mPhysicalCameraId) || diff --git a/core/java/android/os/BatteryConsumer.java b/core/java/android/os/BatteryConsumer.java index ca84b3563561..d5e910120c63 100644 --- a/core/java/android/os/BatteryConsumer.java +++ b/core/java/android/os/BatteryConsumer.java @@ -239,7 +239,7 @@ public abstract class BatteryConsumer { new Dimensions(POWER_COMPONENT_ANY, PROCESS_STATE_ANY); /** - * Identifies power attribution dimensions that are captured by an data element of + * Identifies power attribution dimensions that are captured by a data element of * a BatteryConsumer. These Keys are used to access those values and to set them using * Builders. See for example {@link #getConsumedPower(Key)}. * diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index 143983ad5d11..0ac2d2718117 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -1576,7 +1576,7 @@ public abstract class BatteryStats { outNumOfInterest[0] = numOfInterest; } - // The estimated time is the average time we spend in each level, multipled + // The estimated time is the average time we spend in each level, multiplied // by 100 -- the total number of battery levels return (total / numOfInterest) * 100; } @@ -2023,7 +2023,7 @@ public abstract class BatteryStats { public static final int EVENT_TOP = 0x0003; // Event is about active sync operations. public static final int EVENT_SYNC = 0x0004; - // Events for all additional wake locks aquired/release within a wake block. + // Events for all additional wake locks acquired/release within a wake block. // These are not generated by default. public static final int EVENT_WAKE_LOCK = 0x0005; // Event is about an application executing a scheduled job. @@ -3389,7 +3389,7 @@ public abstract class BatteryStats { * incoming service calls from apps. The result is returned as an array of longs, * organized as a sequence like this: * <pre> - * cluster1-speeed1, cluster1-speed2, ..., cluster2-speed1, cluster2-speed2, ... + * cluster1-speed1, cluster1-speed2, ..., cluster2-speed1, cluster2-speed2, ... * </pre> * * @see com.android.internal.os.PowerProfile#getNumCpuClusters() diff --git a/core/java/android/os/HwParcel.java b/core/java/android/os/HwParcel.java index 9fd37d4548ac..fb500a96c737 100644 --- a/core/java/android/os/HwParcel.java +++ b/core/java/android/os/HwParcel.java @@ -618,7 +618,7 @@ public class HwParcel { */ @FastNative @NonNull - public native final @Nullable + public native final HidlMemory readEmbeddedHidlMemory(long fieldHandle, long parentHandle, long offset); /** diff --git a/core/java/android/os/health/HealthStatsWriter.java b/core/java/android/os/health/HealthStatsWriter.java index d4d10b056c5c..4118775af28a 100644 --- a/core/java/android/os/health/HealthStatsWriter.java +++ b/core/java/android/os/health/HealthStatsWriter.java @@ -58,7 +58,7 @@ public class HealthStatsWriter { * Construct a HealthStatsWriter object with the given constants. * * The "getDataType()" of the resulting HealthStats object will be the - * short name of the java class that the Constants object was initalized + * short name of the java class that the Constants object was initialized * with. */ public HealthStatsWriter(HealthKeys.Constants constants) { diff --git a/core/java/android/os/image/DynamicSystemClient.java b/core/java/android/os/image/DynamicSystemClient.java index 88096ab91261..ada708bda34c 100644 --- a/core/java/android/os/image/DynamicSystemClient.java +++ b/core/java/android/os/image/DynamicSystemClient.java @@ -52,7 +52,7 @@ import java.util.concurrent.Executor; * * After the installation is completed, the device will be running in the new system on next the * reboot. Then, when the user reboots the device again, it will leave {@code DynamicSystem} and go - * back to the original system. While running in {@code DynamicSystem}, persitent storage for + * back to the original system. While running in {@code DynamicSystem}, persistent storage for * factory reset protection (FRP) remains unchanged. Since the user is running the new system with * a temporarily created data partition, their original user data are kept unchanged.</p> * diff --git a/core/java/android/os/image/DynamicSystemManager.java b/core/java/android/os/image/DynamicSystemManager.java index 536795bafb1c..8ce87e97da9c 100644 --- a/core/java/android/os/image/DynamicSystemManager.java +++ b/core/java/android/os/image/DynamicSystemManager.java @@ -172,7 +172,7 @@ public class DynamicSystemManager { } } /** - * Finish a previously started installation. Installations without a cooresponding + * Finish a previously started installation. Installations without a corresponding * finishInstallation() will be cleaned up during device boot. */ @RequiresPermission(android.Manifest.permission.MANAGE_DYNAMIC_SYSTEM) diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 4c3f330ab4f3..e4e67d4d4faf 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -1393,7 +1393,7 @@ public class StorageManager { // Package name can be null if the activity thread is running but the app // hasn't bound yet. In this case we fall back to the first package in the // current UID. This works for runtime permissions as permission state is - // per UID and permission realted app ops are updated for all UID packages. + // per UID and permission related app ops are updated for all UID packages. String[] packageNames = ActivityThread.getPackageManager().getPackagesForUid( android.os.Process.myUid()); if (packageNames == null || packageNames.length <= 0) { diff --git a/core/java/android/os/storage/StorageVolume.java b/core/java/android/os/storage/StorageVolume.java index e1f112af41e7..4cf2fd4dce69 100644 --- a/core/java/android/os/storage/StorageVolume.java +++ b/core/java/android/os/storage/StorageVolume.java @@ -62,7 +62,7 @@ import java.util.UUID; * <li>To get access to standard directories (like the {@link Environment#DIRECTORY_PICTURES}), they * can use the {@link #createAccessIntent(String)}. This is the recommend way, since it provides a * simpler API and narrows the access to the given directory (and its descendants). - * <li>To get access to any directory (and its descendants), they can use the Storage Acess + * <li>To get access to any directory (and its descendants), they can use the Storage Access * Framework APIs (such as {@link Intent#ACTION_OPEN_DOCUMENT} and * {@link Intent#ACTION_OPEN_DOCUMENT_TREE}, although these APIs do not guarantee the user will * select this specific volume. diff --git a/core/java/android/security/ConfirmationPrompt.java b/core/java/android/security/ConfirmationPrompt.java index d8c44adcc322..f626149b03c4 100644 --- a/core/java/android/security/ConfirmationPrompt.java +++ b/core/java/android/security/ConfirmationPrompt.java @@ -92,7 +92,6 @@ public class ConfirmationPrompt { private Executor mExecutor; private Context mContext; - private final KeyStore mKeyStore = KeyStore.getInstance(); private AndroidProtectedConfirmation mProtectedConfirmation; private AndroidProtectedConfirmation getService() { diff --git a/core/java/android/security/keystore/recovery/RecoveryController.java b/core/java/android/security/keystore/recovery/RecoveryController.java index f1054ec8ef15..c171c1b4b3b6 100644 --- a/core/java/android/security/keystore/recovery/RecoveryController.java +++ b/core/java/android/security/keystore/recovery/RecoveryController.java @@ -26,7 +26,6 @@ import android.content.Context; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceSpecificException; -import android.security.KeyStore; import android.security.KeyStore2; import android.security.keystore.KeyPermanentlyInvalidatedException; import android.security.keystore2.AndroidKeyStoreProvider; @@ -272,11 +271,9 @@ public class RecoveryController { public static final int ERROR_KEY_NOT_FOUND = 30; private final ILockSettings mBinder; - private final KeyStore mKeyStore; - private RecoveryController(ILockSettings binder, KeyStore keystore) { + private RecoveryController(ILockSettings binder) { mBinder = binder; - mKeyStore = keystore; } /** @@ -296,7 +293,7 @@ public class RecoveryController { // lockSettings may be null. ILockSettings lockSettings = ILockSettings.Stub.asInterface(ServiceManager.getService("lock_settings")); - return new RecoveryController(lockSettings, KeyStore.getInstance()); + return new RecoveryController(lockSettings); } /** diff --git a/keystore/java/android/security/keystore/AndroidKeyStoreProvider.java b/keystore/java/android/security/keystore/AndroidKeyStoreProvider.java index 62fe54f1f089..ef03d3a3b286 100644 --- a/keystore/java/android/security/keystore/AndroidKeyStoreProvider.java +++ b/keystore/java/android/security/keystore/AndroidKeyStoreProvider.java @@ -19,9 +19,9 @@ package android.security.keystore; import android.annotation.NonNull; import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; -import android.security.KeyStore; import java.io.IOException; +import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; @@ -47,13 +47,13 @@ public class AndroidKeyStoreProvider extends Provider { } /** - * Gets the {@link KeyStore} operation handle corresponding to the provided JCA crypto + * Gets the Android KeyStore operation handle corresponding to the provided JCA crypto * primitive. * * <p>The following primitives are supported: {@link Cipher} and {@link Mac}. * - * @return KeyStore operation handle or {@code 0} if the provided primitive's KeyStore operation - * is not in progress. + * @return Android KeyStore operation handle or {@code 0} if the provided primitive's Android + * KeyStore operation is not in progress. * * @throws IllegalArgumentException if the provided primitive is not supported or is not backed * by AndroidKeyStore provider. @@ -67,10 +67,10 @@ public class AndroidKeyStoreProvider extends Provider { } /** - * Returns an {@code AndroidKeyStore} {@link java.security.KeyStore}} of the specified UID. - * The {@code KeyStore} contains keys and certificates owned by that UID. Such cross-UID - * access is permitted to a few system UIDs and only to a few other UIDs (e.g., Wi-Fi, VPN) - * all of which are system. + * Returns an {@code AndroidKeyStore} {@link KeyStore} of the specified UID. The {@code + * KeyStore} contains keys and certificates owned by that UID. Such cross-UID access is + * permitted to a few system UIDs and only to a few other UIDs (e.g., Wi-Fi, VPN) all of which + * are system. * * <p>Note: the returned {@code KeyStore} is already initialized/loaded. Thus, there is * no need to invoke {@code load} on it. @@ -84,12 +84,12 @@ public class AndroidKeyStoreProvider extends Provider { */ @SystemApi @NonNull - public static java.security.KeyStore getKeyStoreForUid(int uid) + public static KeyStore getKeyStoreForUid(int uid) throws KeyStoreException, NoSuchProviderException { - final java.security.KeyStore.LoadStoreParameter loadParameter = + final KeyStore.LoadStoreParameter loadParameter = new android.security.keystore2.AndroidKeyStoreLoadStoreParameter( KeyProperties.legacyUidToNamespace(uid)); - java.security.KeyStore result = java.security.KeyStore.getInstance(PROVIDER_NAME); + KeyStore result = KeyStore.getInstance(PROVIDER_NAME); try { result.load(loadParameter); } catch (NoSuchAlgorithmException | CertificateException | IOException e) { diff --git a/keystore/java/android/security/keystore/KeyGenParameterSpec.java b/keystore/java/android/security/keystore/KeyGenParameterSpec.java index 244fe3033dca..7aecfd8d4a0d 100644 --- a/keystore/java/android/security/keystore/KeyGenParameterSpec.java +++ b/keystore/java/android/security/keystore/KeyGenParameterSpec.java @@ -910,7 +910,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec, UserAu /** * Returns whether this key is critical to the device encryption flow. * - * @see android.security.KeyStore#FLAG_CRITICAL_TO_DEVICE_ENCRYPTION + * @see Builder#setCriticalToDeviceEncryption(boolean) * @hide */ public boolean isCriticalToDeviceEncryption() { diff --git a/keystore/java/android/security/keystore/KeyProtection.java b/keystore/java/android/security/keystore/KeyProtection.java index 2495d1a85864..31b4a5eac619 100644 --- a/keystore/java/android/security/keystore/KeyProtection.java +++ b/keystore/java/android/security/keystore/KeyProtection.java @@ -569,7 +569,7 @@ public final class KeyProtection implements ProtectionParameter, UserAuthArgs { /** * Return whether this key is critical to the device encryption flow. * - * @see android.security.KeyStore#FLAG_CRITICAL_TO_DEVICE_ENCRYPTION + * @see Builder#setCriticalToDeviceEncryption(boolean) * @hide */ public boolean isCriticalToDeviceEncryption() { @@ -1105,9 +1105,10 @@ public final class KeyProtection implements ProtectionParameter, UserAuthArgs { * Set whether this key is critical to the device encryption flow * * This is a special flag only available to system servers to indicate the current key - * is part of the device encryption flow. + * is part of the device encryption flow. Setting this flag causes the key to not + * be cryptographically bound to the LSKF even if the key is otherwise authentication + * bound. * - * @see android.security.KeyStore#FLAG_CRITICAL_TO_DEVICE_ENCRYPTION * @hide */ public Builder setCriticalToDeviceEncryption(boolean critical) { diff --git a/keystore/java/android/security/keystore/KeyStoreCryptoOperation.java b/keystore/java/android/security/keystore/KeyStoreCryptoOperation.java index 2c709ae1ac5b..c42c9e4d99a6 100644 --- a/keystore/java/android/security/keystore/KeyStoreCryptoOperation.java +++ b/keystore/java/android/security/keystore/KeyStoreCryptoOperation.java @@ -16,18 +16,16 @@ package android.security.keystore; -import android.security.KeyStore; - /** - * Cryptographic operation backed by {@link KeyStore}. + * Cryptographic operation backed by Android KeyStore. * * @hide */ public interface KeyStoreCryptoOperation { /** - * Gets the KeyStore operation handle of this crypto operation. + * Gets the Android KeyStore operation handle of this crypto operation. * - * @return handle or {@code 0} if the KeyStore operation is not in progress. + * @return handle or {@code 0} if the Android KeyStore operation is not in progress. */ long getOperationHandle(); } diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyFactorySpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyFactorySpi.java index a8dd7f3f8b14..8eca67f090d4 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyFactorySpi.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyFactorySpi.java @@ -16,7 +16,6 @@ package android.security.keystore2; -import android.security.KeyStore; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyInfo; @@ -39,8 +38,6 @@ import java.security.spec.X509EncodedKeySpec; */ public class AndroidKeyStoreKeyFactorySpi extends KeyFactorySpi { - private final KeyStore mKeyStore = KeyStore.getInstance(); - @Override protected <T extends KeySpec> T engineGetKeySpec(Key key, Class<T> keySpecClass) throws InvalidKeySpecException { diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java index d204f13d4d78..99100de12684 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java @@ -17,7 +17,6 @@ package android.security.keystore2; import android.annotation.NonNull; -import android.security.KeyStore; import android.security.KeyStore2; import android.security.KeyStoreSecurityLevel; import android.security.keymaster.KeymasterDefs; @@ -161,13 +160,13 @@ public class AndroidKeyStoreProvider extends Provider { } /** - * Gets the {@link KeyStore} operation handle corresponding to the provided JCA crypto + * Gets the Android KeyStore operation handle corresponding to the provided JCA crypto * primitive. * * <p>The following primitives are supported: {@link Cipher}, {@link Signature} and {@link Mac}. * - * @return KeyStore operation handle or {@code 0} if the provided primitive's KeyStore operation - * is not in progress. + * @return Android KeyStore operation handle or {@code 0} if the provided primitive's Android + * KeyStore operation is not in progress. * * @throws IllegalArgumentException if the provided primitive is not supported or is not backed * by AndroidKeyStore provider. diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java index 2682eb657963..22230916b084 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java @@ -18,7 +18,6 @@ package android.security.keystore2; import android.annotation.NonNull; import android.security.GateKeeper; -import android.security.KeyStore; import android.security.keymaster.KeymasterArguments; import android.security.keymaster.KeymasterDefs; import android.security.keystore.KeyGenParameterSpec; @@ -46,8 +45,6 @@ import javax.crypto.spec.SecretKeySpec; */ public class AndroidKeyStoreSecretKeyFactorySpi extends SecretKeyFactorySpi { - private final KeyStore mKeyStore = KeyStore.getInstance(); - @Override protected KeySpec engineGetKeySpec(SecretKey key, @SuppressWarnings("rawtypes") Class keySpecClass) throws InvalidKeySpecException { diff --git a/keystore/java/android/security/keystore2/KeyStoreCryptoOperationStreamer.java b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationStreamer.java index 07d6a69eda01..5bd98bce9f39 100644 --- a/keystore/java/android/security/keystore2/KeyStoreCryptoOperationStreamer.java +++ b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationStreamer.java @@ -16,12 +16,11 @@ package android.security.keystore2; -import android.security.KeyStore; import android.security.KeyStoreException; /** - * Helper for streaming a crypto operation's input and output via {@link KeyStore} service's - * {@code update} and {@code finish} operations. + * Helper for streaming a crypto operation's input and output via KeyStore service's {@code update} + * and {@code finish} operations. * * <p>The helper abstracts away to issues that need to be solved in most code that uses KeyStore's * update and finish operations. Firstly, KeyStore's update operation can consume only a limited diff --git a/packages/Connectivity/framework/src/android/net/ConnectivityAnnotations.java b/packages/Connectivity/framework/src/android/net/ConnectivityAnnotations.java deleted file mode 100644 index eb1faa0aa25c..000000000000 --- a/packages/Connectivity/framework/src/android/net/ConnectivityAnnotations.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2021 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 android.net; - -import android.annotation.IntDef; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * Type annotations for constants used in the connectivity API surface. - * - * The annotations are maintained in a separate class so that it can be built as - * a separate library that other modules can build against, as Typedef should not - * be exposed as SystemApi. - * - * @hide - */ -public final class ConnectivityAnnotations { - private ConnectivityAnnotations() {} - - @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = { - ConnectivityManager.MULTIPATH_PREFERENCE_HANDOVER, - ConnectivityManager.MULTIPATH_PREFERENCE_RELIABILITY, - ConnectivityManager.MULTIPATH_PREFERENCE_PERFORMANCE, - }) - public @interface MultipathPreference {} - - @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = false, value = { - ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED, - ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED, - ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED, - }) - public @interface RestrictBackgroundStatus {} -} diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index e9acce6ecb2b..c1aa7f554b7c 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -3577,7 +3577,7 @@ public class AudioService extends IAudioService.Stub && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device) && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) { if (DEBUG_VOL) { - Log.d(TAG, "adjustSreamVolume: postSetAvrcpAbsoluteVolumeIndex index=" + Log.d(TAG, "adjustStreamVolume: postSetAvrcpAbsoluteVolumeIndex index=" + newIndex + "stream=" + streamType); } mDeviceBroker.postSetAvrcpAbsoluteVolumeIndex(newIndex / 10); @@ -3591,7 +3591,7 @@ public class AudioService extends IAudioService.Stub && streamType == getBluetoothContextualVolumeStream() && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) { if (DEBUG_VOL) { - Log.d(TAG, "adjustSreamVolume postSetLeAudioVolumeIndex index=" + Log.d(TAG, "adjustStreamVolume postSetLeAudioVolumeIndex index=" + newIndex + " stream=" + streamType); } mDeviceBroker.postSetLeAudioVolumeIndex(newIndex, @@ -3604,7 +3604,7 @@ public class AudioService extends IAudioService.Stub // the one expected by the hearing aid if (streamType == getBluetoothContextualVolumeStream()) { if (DEBUG_VOL) { - Log.d(TAG, "adjustSreamVolume postSetHearingAidVolumeIndex index=" + Log.d(TAG, "adjustStreamVolume postSetHearingAidVolumeIndex index=" + newIndex + " stream=" + streamType); } mDeviceBroker.postSetHearingAidVolumeIndex(newIndex, streamType); @@ -4544,7 +4544,7 @@ public class AudioService extends IAudioService.Stub && streamType == getBluetoothContextualVolumeStream() && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) { if (DEBUG_VOL) { - Log.d(TAG, "adjustSreamVolume postSetLeAudioVolumeIndex index=" + Log.d(TAG, "setStreamVolume postSetLeAudioVolumeIndex index=" + index + " stream=" + streamType); } mDeviceBroker.postSetLeAudioVolumeIndex(index, mStreamStates[streamType].getMaxIndex(), diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index 939423f27c3c..a9a9d87bfaf7 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -104,7 +104,6 @@ import android.provider.DeviceConfig; import android.provider.Settings; import android.security.AndroidKeyStoreMaintenance; import android.security.Authorization; -import android.security.KeyStore; import android.security.keystore.KeyProperties; import android.security.keystore.KeyProtection; import android.security.keystore.recovery.KeyChainProtectionParams; @@ -165,6 +164,7 @@ import java.io.PrintWriter; import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; +import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @@ -288,7 +288,7 @@ public class LockSettingsService extends ILockSettings.Stub { private final IActivityManager mActivityManager; private final SyntheticPasswordManager mSpManager; - private final java.security.KeyStore mJavaKeyStore; + private final KeyStore mKeyStore; private final RecoverableKeyStoreManager mRecoverableKeyStoreManager; private final UnifiedProfilePasswordCache mUnifiedProfilePasswordCache; @@ -560,10 +560,6 @@ public class LockSettingsService extends ILockSettings.Stub { return DeviceStateCache.getInstance(); } - public KeyStore getKeyStore() { - return KeyStore.getInstance(); - } - public RecoverableKeyStoreManager getRecoverableKeyStoreManager() { return RecoverableKeyStoreManager.getInstance(mContext); } @@ -615,9 +611,9 @@ public class LockSettingsService extends ILockSettings.Stub { return (BiometricManager) mContext.getSystemService(Context.BIOMETRIC_SERVICE); } - public java.security.KeyStore getJavaKeyStore() { + public KeyStore getKeyStore() { try { - java.security.KeyStore ks = java.security.KeyStore.getInstance( + KeyStore ks = KeyStore.getInstance( SyntheticPasswordCrypto.androidKeystoreProviderName()); ks.load(new AndroidKeyStoreLoadStoreParameter( SyntheticPasswordCrypto.keyNamespace())); @@ -627,8 +623,7 @@ public class LockSettingsService extends ILockSettings.Stub { } } - public @NonNull UnifiedProfilePasswordCache getUnifiedProfilePasswordCache( - java.security.KeyStore ks) { + public @NonNull UnifiedProfilePasswordCache getUnifiedProfilePasswordCache(KeyStore ks) { return new UnifiedProfilePasswordCache(ks); } @@ -650,7 +645,7 @@ public class LockSettingsService extends ILockSettings.Stub { protected LockSettingsService(Injector injector) { mInjector = injector; mContext = injector.getContext(); - mJavaKeyStore = injector.getJavaKeyStore(); + mKeyStore = injector.getKeyStore(); mRecoverableKeyStoreManager = injector.getRecoverableKeyStoreManager(); mHandler = injector.getHandler(injector.getServiceThread()); mStrongAuth = injector.getStrongAuth(); @@ -672,7 +667,7 @@ public class LockSettingsService extends ILockSettings.Stub { mGatekeeperPasswords = new LongSparseArray<>(); mSpManager = injector.getSyntheticPasswordManager(mStorage); - mUnifiedProfilePasswordCache = injector.getUnifiedProfilePasswordCache(mJavaKeyStore); + mUnifiedProfilePasswordCache = injector.getUnifiedProfilePasswordCache(mKeyStore); mBiometricDeferredQueue = new BiometricDeferredQueue(mSpManager, mHandler); mRebootEscrowManager = injector.getRebootEscrowManager(new RebootEscrowCallbacks(), @@ -1457,7 +1452,7 @@ public class LockSettingsService extends ILockSettings.Stub { byte[] encryptedPassword = Arrays.copyOfRange(storedData, PROFILE_KEY_IV_SIZE, storedData.length); byte[] decryptionResult; - SecretKey decryptionKey = (SecretKey) mJavaKeyStore.getKey( + SecretKey decryptionKey = (SecretKey) mKeyStore.getKey( PROFILE_KEY_NAME_DECRYPT + userId, null); Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" @@ -2051,16 +2046,16 @@ public class LockSettingsService extends ILockSettings.Stub { keyGenerator.init(new SecureRandom()); SecretKey secretKey = keyGenerator.generateKey(); try { - mJavaKeyStore.setEntry( + mKeyStore.setEntry( PROFILE_KEY_NAME_ENCRYPT + profileUserId, - new java.security.KeyStore.SecretKeyEntry(secretKey), + new KeyStore.SecretKeyEntry(secretKey), new KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .build()); - mJavaKeyStore.setEntry( + mKeyStore.setEntry( PROFILE_KEY_NAME_DECRYPT + profileUserId, - new java.security.KeyStore.SecretKeyEntry(secretKey), + new KeyStore.SecretKeyEntry(secretKey), new KeyProtection.Builder(KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) @@ -2069,7 +2064,7 @@ public class LockSettingsService extends ILockSettings.Stub { .setUserAuthenticationValidityDurationSeconds(30) .build()); // Key imported, obtain a reference to it. - SecretKey keyStoreEncryptionKey = (SecretKey) mJavaKeyStore.getKey( + SecretKey keyStoreEncryptionKey = (SecretKey) mKeyStore.getKey( PROFILE_KEY_NAME_ENCRYPT + profileUserId, null); Cipher cipher = Cipher.getInstance( KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_GCM + "/" @@ -2079,7 +2074,7 @@ public class LockSettingsService extends ILockSettings.Stub { iv = cipher.getIV(); } finally { // The original key can now be discarded. - mJavaKeyStore.deleteEntry(PROFILE_KEY_NAME_ENCRYPT + profileUserId); + mKeyStore.deleteEntry(PROFILE_KEY_NAME_ENCRYPT + profileUserId); } } catch (UnrecoverableKeyException | BadPaddingException | IllegalBlockSizeException | KeyStoreException @@ -2526,11 +2521,10 @@ public class LockSettingsService extends ILockSettings.Stub { final String encryptAlias = PROFILE_KEY_NAME_ENCRYPT + targetUserId; final String decryptAlias = PROFILE_KEY_NAME_DECRYPT + targetUserId; try { - if (mJavaKeyStore.containsAlias(encryptAlias) || - mJavaKeyStore.containsAlias(decryptAlias)) { + if (mKeyStore.containsAlias(encryptAlias) || mKeyStore.containsAlias(decryptAlias)) { Slogf.i(TAG, "Removing keystore profile key for user %d", targetUserId); - mJavaKeyStore.deleteEntry(encryptAlias); - mJavaKeyStore.deleteEntry(decryptAlias); + mKeyStore.deleteEntry(encryptAlias); + mKeyStore.deleteEntry(decryptAlias); } } catch (KeyStoreException e) { // We have tried our best to remove the key. @@ -3408,7 +3402,7 @@ public class LockSettingsService extends ILockSettings.Stub { private void dumpKeystoreKeys(IndentingPrintWriter pw) { try { - final Enumeration<String> aliases = mJavaKeyStore.aliases(); + final Enumeration<String> aliases = mKeyStore.aliases(); while (aliases.hasMoreElements()) { pw.println(aliases.nextElement()); } diff --git a/services/tests/VpnTests/Android.bp b/services/tests/VpnTests/Android.bp new file mode 100644 index 000000000000..64a9a3b4f119 --- /dev/null +++ b/services/tests/VpnTests/Android.bp @@ -0,0 +1,39 @@ +//######################################################################## +// Build FrameworksVpnTests package +//######################################################################## +package { + default_team: "trendy_team_fwk_core_networking", + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "Android-Apache-2.0" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_test { + name: "FrameworksVpnTests", + srcs: [ + "java/**/*.java", + "java/**/*.kt", + ], + + defaults: ["framework-connectivity-test-defaults"], + test_suites: ["device-tests"], + static_libs: [ + "androidx.test.rules", + "frameworks-base-testutils", + "framework-protos", + "mockito-target-minus-junit4", + "net-tests-utils", + "platform-test-annotations", + "services.core", + "cts-net-utils", + "service-connectivity-tiramisu-pre-jarjar", + ], + libs: [ + "android.test.runner", + "android.test.base", + "android.test.mock", + ], +} diff --git a/services/tests/VpnTests/AndroidManifest.xml b/services/tests/VpnTests/AndroidManifest.xml new file mode 100644 index 000000000000..d884084f2eb7 --- /dev/null +++ b/services/tests/VpnTests/AndroidManifest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.frameworks.tests.vpn"> + <application> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.frameworks.tests.vpn" + android:label="Frameworks VPN Tests" /> +</manifest>
\ No newline at end of file diff --git a/services/tests/VpnTests/AndroidTest.xml b/services/tests/VpnTests/AndroidTest.xml new file mode 100644 index 000000000000..ebeeac7d269b --- /dev/null +++ b/services/tests/VpnTests/AndroidTest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 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. + --> +<configuration description="Runs VPN Tests."> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="test-file-name" value="FrameworksVpnTests.apk" /> + </target_preparer> + + <option name="test-tag" value="FrameworksVpnTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.frameworks.tests.vpn" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration>
\ No newline at end of file diff --git a/services/tests/VpnTests/OWNERS b/services/tests/VpnTests/OWNERS new file mode 100644 index 000000000000..45ea251a02e4 --- /dev/null +++ b/services/tests/VpnTests/OWNERS @@ -0,0 +1,2 @@ +set noparent +file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking
\ No newline at end of file diff --git a/services/tests/VpnTests/java/com/android/server/VpnManagerServiceTest.java b/services/tests/VpnTests/java/com/android/server/VpnManagerServiceTest.java new file mode 100644 index 000000000000..ecc70e3669d6 --- /dev/null +++ b/services/tests/VpnTests/java/com/android/server/VpnManagerServiceTest.java @@ -0,0 +1,400 @@ +/* + * Copyright (C) 2022 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.server; + +import static com.android.testutils.ContextUtils.mockService; +import static com.android.testutils.MiscAsserts.assertThrows; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.annotation.UserIdInt; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.INetd; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.INetworkManagementService; +import android.os.Looper; +import android.os.UserHandle; +import android.os.UserManager; +import android.security.Credentials; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.internal.net.VpnProfile; +import com.android.server.connectivity.Vpn; +import com.android.server.connectivity.VpnProfileStore; +import com.android.server.net.LockdownVpnTracker; +import com.android.testutils.HandlerUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class VpnManagerServiceTest extends VpnTestBase { + private static final String CONTEXT_ATTRIBUTION_TAG = "VPN_MANAGER"; + + private static final int TIMEOUT_MS = 2_000; + + @Mock Context mContext; + @Mock Context mContextWithoutAttributionTag; + @Mock Context mSystemContext; + @Mock Context mUserAllContext; + private HandlerThread mHandlerThread; + @Mock private Vpn mVpn; + @Mock private INetworkManagementService mNms; + @Mock private ConnectivityManager mCm; + @Mock private UserManager mUserManager; + @Mock private INetd mNetd; + @Mock private PackageManager mPackageManager; + @Mock private VpnProfileStore mVpnProfileStore; + @Mock private LockdownVpnTracker mLockdownVpnTracker; + + private VpnManagerServiceDependencies mDeps; + private VpnManagerService mService; + private BroadcastReceiver mUserPresentReceiver; + private BroadcastReceiver mIntentReceiver; + private final String mNotMyVpnPkg = "com.not.my.vpn"; + + class VpnManagerServiceDependencies extends VpnManagerService.Dependencies { + @Override + public HandlerThread makeHandlerThread() { + return mHandlerThread; + } + + @Override + public INetworkManagementService getINetworkManagementService() { + return mNms; + } + + @Override + public INetd getNetd() { + return mNetd; + } + + @Override + public Vpn createVpn(Looper looper, Context context, INetworkManagementService nms, + INetd netd, @UserIdInt int userId) { + return mVpn; + } + + @Override + public VpnProfileStore getVpnProfileStore() { + return mVpnProfileStore; + } + + @Override + public LockdownVpnTracker createLockDownVpnTracker(Context context, Handler handler, + Vpn vpn, VpnProfile profile) { + return mLockdownVpnTracker; + } + + @Override + public @UserIdInt int getMainUserId() { + return UserHandle.USER_SYSTEM; + } + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mHandlerThread = new HandlerThread("TestVpnManagerService"); + mDeps = new VpnManagerServiceDependencies(); + + // The attribution tag is a dependency for IKE library to collect VPN metrics correctly + // and thus should not be changed without updating the IKE code. + doReturn(mContext) + .when(mContextWithoutAttributionTag) + .createAttributionContext(CONTEXT_ATTRIBUTION_TAG); + + doReturn(mUserAllContext).when(mContext).createContextAsUser(UserHandle.ALL, 0); + doReturn(mSystemContext).when(mContext).createContextAsUser(UserHandle.SYSTEM, 0); + doReturn(mPackageManager).when(mContext).getPackageManager(); + setMockedPackages(mPackageManager, sPackages); + + mockService(mContext, ConnectivityManager.class, Context.CONNECTIVITY_SERVICE, mCm); + mockService(mContext, UserManager.class, Context.USER_SERVICE, mUserManager); + doReturn(SYSTEM_USER).when(mUserManager).getUserInfo(eq(SYSTEM_USER_ID)); + + mService = new VpnManagerService(mContextWithoutAttributionTag, mDeps); + mService.systemReady(); + + final ArgumentCaptor<BroadcastReceiver> intentReceiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + final ArgumentCaptor<BroadcastReceiver> userPresentReceiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + verify(mSystemContext).registerReceiver( + userPresentReceiverCaptor.capture(), any(), any(), any()); + verify(mUserAllContext, times(2)).registerReceiver( + intentReceiverCaptor.capture(), any(), any(), any()); + mUserPresentReceiver = userPresentReceiverCaptor.getValue(); + mIntentReceiver = intentReceiverCaptor.getValue(); + + // Add user to create vpn in mVpn + onUserStarted(SYSTEM_USER_ID); + assertNotNull(mService.mVpns.get(SYSTEM_USER_ID)); + } + + @Test + public void testUpdateAppExclusionList() { + // Start vpn + mService.startVpnProfile(TEST_VPN_PKG); + verify(mVpn).startVpnProfile(eq(TEST_VPN_PKG)); + + // Remove package due to package replaced. + onPackageRemoved(PKGS[0], PKG_UIDS[0], true /* isReplacing */); + verify(mVpn, never()).refreshPlatformVpnAppExclusionList(); + + // Add package due to package replaced. + onPackageAdded(PKGS[0], PKG_UIDS[0], true /* isReplacing */); + verify(mVpn, never()).refreshPlatformVpnAppExclusionList(); + + // Remove package + onPackageRemoved(PKGS[0], PKG_UIDS[0], false /* isReplacing */); + verify(mVpn).refreshPlatformVpnAppExclusionList(); + + // Add the package back + onPackageAdded(PKGS[0], PKG_UIDS[0], false /* isReplacing */); + verify(mVpn, times(2)).refreshPlatformVpnAppExclusionList(); + } + + @Test + public void testStartVpnProfileFromDiffPackage() { + assertThrows( + SecurityException.class, () -> mService.startVpnProfile(mNotMyVpnPkg)); + } + + @Test + public void testStopVpnProfileFromDiffPackage() { + assertThrows(SecurityException.class, () -> mService.stopVpnProfile(mNotMyVpnPkg)); + } + + @Test + public void testGetProvisionedVpnProfileStateFromDiffPackage() { + assertThrows(SecurityException.class, () -> + mService.getProvisionedVpnProfileState(mNotMyVpnPkg)); + } + + @Test + public void testGetProvisionedVpnProfileState() { + mService.getProvisionedVpnProfileState(TEST_VPN_PKG); + verify(mVpn).getProvisionedVpnProfileState(TEST_VPN_PKG); + } + + private Intent buildIntent(String action, String packageName, int userId, int uid, + boolean isReplacing) { + final Intent intent = new Intent(action); + intent.putExtra(Intent.EXTRA_USER_HANDLE, userId); + intent.putExtra(Intent.EXTRA_UID, uid); + intent.putExtra(Intent.EXTRA_REPLACING, isReplacing); + if (packageName != null) { + intent.setData(Uri.fromParts("package" /* scheme */, packageName, null /* fragment */)); + } + + return intent; + } + + private void sendIntent(Intent intent) { + sendIntent(mIntentReceiver, mContext, intent); + } + + private void sendIntent(BroadcastReceiver receiver, Context context, Intent intent) { + final Handler h = mHandlerThread.getThreadHandler(); + + // Send in handler thread. + h.post(() -> receiver.onReceive(context, intent)); + HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS); + } + + private void onUserStarted(int userId) { + sendIntent(buildIntent(Intent.ACTION_USER_STARTED, + null /* packageName */, userId, -1 /* uid */, false /* isReplacing */)); + } + + private void onUserUnlocked(int userId) { + sendIntent(buildIntent(Intent.ACTION_USER_UNLOCKED, + null /* packageName */, userId, -1 /* uid */, false /* isReplacing */)); + } + + private void onUserStopped(int userId) { + sendIntent(buildIntent(Intent.ACTION_USER_STOPPED, + null /* packageName */, userId, -1 /* uid */, false /* isReplacing */)); + } + + private void onLockDownReset() { + sendIntent(buildIntent(LockdownVpnTracker.ACTION_LOCKDOWN_RESET, null /* packageName */, + UserHandle.USER_SYSTEM, -1 /* uid */, false /* isReplacing */)); + } + + private void onPackageAdded(String packageName, int userId, int uid, boolean isReplacing) { + sendIntent(buildIntent(Intent.ACTION_PACKAGE_ADDED, packageName, userId, uid, isReplacing)); + } + + private void onPackageAdded(String packageName, int uid, boolean isReplacing) { + onPackageAdded(packageName, UserHandle.USER_SYSTEM, uid, isReplacing); + } + + private void onPackageRemoved(String packageName, int userId, int uid, boolean isReplacing) { + sendIntent(buildIntent(Intent.ACTION_PACKAGE_REMOVED, packageName, userId, uid, + isReplacing)); + } + + private void onPackageRemoved(String packageName, int uid, boolean isReplacing) { + onPackageRemoved(packageName, UserHandle.USER_SYSTEM, uid, isReplacing); + } + + @Test + public void testReceiveIntentFromNonHandlerThread() { + assertThrows(IllegalStateException.class, () -> + mIntentReceiver.onReceive(mContext, buildIntent(Intent.ACTION_PACKAGE_REMOVED, + PKGS[0], UserHandle.USER_SYSTEM, PKG_UIDS[0], true /* isReplacing */))); + + assertThrows(IllegalStateException.class, () -> + mUserPresentReceiver.onReceive(mContext, new Intent(Intent.ACTION_USER_PRESENT))); + } + + private void setupLockdownVpn(String packageName) { + final byte[] profileTag = packageName.getBytes(StandardCharsets.UTF_8); + doReturn(profileTag).when(mVpnProfileStore).get(Credentials.LOCKDOWN_VPN); + } + + private void setupVpnProfile(String profileName) { + final VpnProfile profile = new VpnProfile(profileName); + profile.name = profileName; + profile.server = "192.0.2.1"; + profile.dnsServers = "8.8.8.8"; + profile.type = VpnProfile.TYPE_IPSEC_XAUTH_PSK; + final byte[] encodedProfile = profile.encode(); + doReturn(encodedProfile).when(mVpnProfileStore).get(Credentials.VPN + profileName); + } + + @Test + public void testUserPresent() { + // Verify that LockDownVpnTracker is not created. + verify(mLockdownVpnTracker, never()).init(); + + setupLockdownVpn(TEST_VPN_PKG); + setupVpnProfile(TEST_VPN_PKG); + + // mUserPresentReceiver only registers ACTION_USER_PRESENT intent and does no verification + // on action, so an empty intent is enough. + sendIntent(mUserPresentReceiver, mSystemContext, new Intent()); + + verify(mLockdownVpnTracker).init(); + verify(mSystemContext).unregisterReceiver(mUserPresentReceiver); + verify(mUserAllContext, never()).unregisterReceiver(any()); + } + + @Test + public void testUpdateLockdownVpn() { + setupLockdownVpn(TEST_VPN_PKG); + onUserUnlocked(SYSTEM_USER_ID); + + // Will not create lockDownVpnTracker w/o valid profile configured in the keystore + verify(mLockdownVpnTracker, never()).init(); + + setupVpnProfile(TEST_VPN_PKG); + + // Remove the user from mVpns + onUserStopped(SYSTEM_USER_ID); + onUserUnlocked(SYSTEM_USER_ID); + verify(mLockdownVpnTracker, never()).init(); + + // Add user back + onUserStarted(SYSTEM_USER_ID); + verify(mLockdownVpnTracker).init(); + + // Trigger another update. The existing LockDownVpnTracker should be shut down and + // initialize another one. + onUserUnlocked(SYSTEM_USER_ID); + verify(mLockdownVpnTracker).shutdown(); + verify(mLockdownVpnTracker, times(2)).init(); + } + + @Test + public void testLockdownReset() { + // Init LockdownVpnTracker + setupLockdownVpn(TEST_VPN_PKG); + setupVpnProfile(TEST_VPN_PKG); + onUserUnlocked(SYSTEM_USER_ID); + verify(mLockdownVpnTracker).init(); + + onLockDownReset(); + verify(mLockdownVpnTracker).reset(); + } + + @Test + public void testLockdownResetWhenLockdownVpnTrackerIsNotInit() { + setupLockdownVpn(TEST_VPN_PKG); + setupVpnProfile(TEST_VPN_PKG); + + onLockDownReset(); + + // LockDownVpnTracker is not created. Lockdown reset will not take effect. + verify(mLockdownVpnTracker, never()).reset(); + } + + @Test + public void testIsVpnLockdownEnabled() { + // Vpn is created but the VPN lockdown is not enabled. + assertFalse(mService.isVpnLockdownEnabled(SYSTEM_USER_ID)); + + // Set lockdown for the SYSTEM_USER_ID VPN. + doReturn(true).when(mVpn).getLockdown(); + assertTrue(mService.isVpnLockdownEnabled(SYSTEM_USER_ID)); + + // Even lockdown is enabled but no Vpn is created for SECONDARY_USER. + assertFalse(mService.isVpnLockdownEnabled(SECONDARY_USER.id)); + } + + @Test + public void testGetVpnLockdownAllowlist() { + doReturn(null).when(mVpn).getLockdownAllowlist(); + assertNull(mService.getVpnLockdownAllowlist(SYSTEM_USER_ID)); + + final List<String> expected = List.of(PKGS); + doReturn(expected).when(mVpn).getLockdownAllowlist(); + assertEquals(expected, mService.getVpnLockdownAllowlist(SYSTEM_USER_ID)); + + // Even lockdown is enabled but no Vpn is created for SECONDARY_USER. + assertNull(mService.getVpnLockdownAllowlist(SECONDARY_USER.id)); + } +} diff --git a/services/tests/VpnTests/java/com/android/server/VpnTestBase.java b/services/tests/VpnTests/java/com/android/server/VpnTestBase.java new file mode 100644 index 000000000000..6113872e213f --- /dev/null +++ b/services/tests/VpnTests/java/com/android/server/VpnTestBase.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2022 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.server; + +import static android.content.pm.UserInfo.FLAG_ADMIN; +import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE; +import static android.content.pm.UserInfo.FLAG_PRIMARY; +import static android.content.pm.UserInfo.FLAG_RESTRICTED; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; + +import android.content.pm.PackageManager; +import android.content.pm.UserInfo; +import android.os.Process; +import android.os.UserHandle; +import android.util.ArrayMap; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** Common variables or methods shared between VpnTest and VpnManagerServiceTest. */ +public class VpnTestBase { + protected static final String TEST_VPN_PKG = "com.testvpn.vpn"; + /** + * Names and UIDs for some fake packages. Important points: + * - UID is ordered increasing. + * - One pair of packages have consecutive UIDs. + */ + protected static final String[] PKGS = {"com.example", "org.example", "net.example", "web.vpn"}; + protected static final int[] PKG_UIDS = {10066, 10077, 10078, 10400}; + // Mock packages + protected static final Map<String, Integer> sPackages = new ArrayMap<>(); + static { + for (int i = 0; i < PKGS.length; i++) { + sPackages.put(PKGS[i], PKG_UIDS[i]); + } + sPackages.put(TEST_VPN_PKG, Process.myUid()); + } + + // Mock users + protected static final int SYSTEM_USER_ID = 0; + protected static final UserInfo SYSTEM_USER = new UserInfo(0, "system", UserInfo.FLAG_PRIMARY); + protected static final UserInfo PRIMARY_USER = new UserInfo(27, "Primary", + FLAG_ADMIN | FLAG_PRIMARY); + protected static final UserInfo SECONDARY_USER = new UserInfo(15, "Secondary", FLAG_ADMIN); + protected static final UserInfo RESTRICTED_PROFILE_A = new UserInfo(40, "RestrictedA", + FLAG_RESTRICTED); + protected static final UserInfo RESTRICTED_PROFILE_B = new UserInfo(42, "RestrictedB", + FLAG_RESTRICTED); + protected static final UserInfo MANAGED_PROFILE_A = new UserInfo(45, "ManagedA", + FLAG_MANAGED_PROFILE); + static { + RESTRICTED_PROFILE_A.restrictedProfileParentId = PRIMARY_USER.id; + RESTRICTED_PROFILE_B.restrictedProfileParentId = SECONDARY_USER.id; + MANAGED_PROFILE_A.profileGroupId = PRIMARY_USER.id; + } + + // Populate a fake packageName-to-UID mapping. + protected void setMockedPackages(PackageManager mockPm, final Map<String, Integer> packages) { + try { + doAnswer(invocation -> { + final String appName = (String) invocation.getArguments()[0]; + final int userId = (int) invocation.getArguments()[1]; + + final Integer appId = packages.get(appName); + if (appId == null) { + throw new PackageManager.NameNotFoundException(appName); + } + + return UserHandle.getUid(userId, appId); + }).when(mockPm).getPackageUidAsUser(anyString(), anyInt()); + } catch (Exception e) { + } + } + + protected List<Integer> toList(int[] arr) { + return Arrays.stream(arr).boxed().collect(Collectors.toList()); + } +} diff --git a/services/tests/VpnTests/java/com/android/server/connectivity/VpnTest.java b/services/tests/VpnTests/java/com/android/server/connectivity/VpnTest.java new file mode 100644 index 000000000000..9115f952b724 --- /dev/null +++ b/services/tests/VpnTests/java/com/android/server/connectivity/VpnTest.java @@ -0,0 +1,3293 @@ +/* + * Copyright (C) 2016 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.server.connectivity; + +import static android.Manifest.permission.BIND_VPN_SERVICE; +import static android.Manifest.permission.CONTROL_VPN; +import static android.content.pm.PackageManager.PERMISSION_DENIED; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback; +import static android.net.ConnectivityDiagnosticsManager.DataStallReport; +import static android.net.ConnectivityManager.NetworkCallback; +import static android.net.INetd.IF_STATE_DOWN; +import static android.net.INetd.IF_STATE_UP; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.RouteInfo.RTN_UNREACHABLE; +import static android.net.VpnManager.TYPE_VPN_PLATFORM; +import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS; +import static android.net.cts.util.IkeSessionTestUtils.TEST_IDENTITY; +import static android.net.cts.util.IkeSessionTestUtils.TEST_KEEPALIVE_TIMEOUT_UNSET; +import static android.net.cts.util.IkeSessionTestUtils.getTestIkeSessionParams; +import static android.net.ipsec.ike.IkeSessionConfiguration.EXTENSION_TYPE_MOBIKE; +import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_AUTO; +import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_NONE; +import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_UDP; +import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_AUTO; +import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_IPV4; +import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_IPV6; +import static android.os.UserHandle.PER_USER_RANGE; +import static android.telephony.CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL; +import static android.telephony.CarrierConfigManager.KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT; +import static android.telephony.CarrierConfigManager.KEY_PREFERRED_IKE_PROTOCOL_INT; + +import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU; +import static com.android.server.connectivity.Vpn.AUTOMATIC_KEEPALIVE_DELAY_SECONDS; +import static com.android.server.connectivity.Vpn.DEFAULT_LONG_LIVED_TCP_CONNS_EXPENSIVE_TIMEOUT_SEC; +import static com.android.server.connectivity.Vpn.DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT; +import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_AUTO; +import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV4_UDP; +import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_ESP; +import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_UDP; +import static com.android.testutils.HandlerUtils.waitForIdleSerialExecutor; +import static com.android.testutils.MiscAsserts.assertThrows; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.longThat; +import static org.mockito.Mockito.after; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.AppOpsManager; +import android.app.NotificationManager; +import android.app.PendingIntent; +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.content.res.Resources; +import android.net.ConnectivityDiagnosticsManager; +import android.net.ConnectivityManager; +import android.net.INetd; +import android.net.Ikev2VpnProfile; +import android.net.InetAddresses; +import android.net.InterfaceConfigurationParcel; +import android.net.IpPrefix; +import android.net.IpSecConfig; +import android.net.IpSecManager; +import android.net.IpSecTransform; +import android.net.IpSecTunnelInterfaceResponse; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkAgent; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo.DetailedState; +import android.net.RouteInfo; +import android.net.TelephonyNetworkSpecifier; +import android.net.UidRangeParcel; +import android.net.VpnManager; +import android.net.VpnProfileState; +import android.net.VpnService; +import android.net.VpnTransportInfo; +import android.net.ipsec.ike.ChildSessionCallback; +import android.net.ipsec.ike.ChildSessionConfiguration; +import android.net.ipsec.ike.IkeFqdnIdentification; +import android.net.ipsec.ike.IkeSessionCallback; +import android.net.ipsec.ike.IkeSessionConfiguration; +import android.net.ipsec.ike.IkeSessionConnectionInfo; +import android.net.ipsec.ike.IkeSessionParams; +import android.net.ipsec.ike.IkeTrafficSelector; +import android.net.ipsec.ike.IkeTunnelConnectionParams; +import android.net.ipsec.ike.exceptions.IkeException; +import android.net.ipsec.ike.exceptions.IkeNetworkLostException; +import android.net.ipsec.ike.exceptions.IkeNonProtocolException; +import android.net.ipsec.ike.exceptions.IkeProtocolException; +import android.net.ipsec.ike.exceptions.IkeTimeoutException; +import android.net.vcn.VcnTransportInfo; +import android.net.wifi.WifiInfo; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.INetworkManagementService; +import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; +import android.os.PowerWhitelistManager; +import android.os.Process; +import android.os.UserHandle; +import android.os.UserManager; +import android.os.test.TestLooper; +import android.provider.Settings; +import android.security.Credentials; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Pair; +import android.util.Range; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.internal.R; +import com.android.internal.net.LegacyVpnInfo; +import com.android.internal.net.VpnConfig; +import com.android.internal.net.VpnProfile; +import com.android.internal.util.HexDump; +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.DeviceIdleInternal; +import com.android.server.IpSecService; +import com.android.server.VpnTestBase; +import com.android.server.vcn.util.PersistableBundleUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.StringWriter; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Tests for {@link Vpn}. + * + * Build, install and run with: + * runtest frameworks-net -c com.android.server.connectivity.VpnTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class VpnTest extends VpnTestBase { + private static final String TAG = "VpnTest"; + + static final Network EGRESS_NETWORK = new Network(101); + static final String EGRESS_IFACE = "wlan0"; + private static final String TEST_VPN_CLIENT = "2.4.6.8"; + private static final String TEST_VPN_SERVER = "1.2.3.4"; + private static final String TEST_VPN_IDENTITY = "identity"; + private static final byte[] TEST_VPN_PSK = "psk".getBytes(); + + private static final int IP4_PREFIX_LEN = 32; + private static final int IP6_PREFIX_LEN = 64; + private static final int MIN_PORT = 0; + private static final int MAX_PORT = 65535; + + private static final InetAddress TEST_VPN_CLIENT_IP = + InetAddresses.parseNumericAddress(TEST_VPN_CLIENT); + private static final InetAddress TEST_VPN_SERVER_IP = + InetAddresses.parseNumericAddress(TEST_VPN_SERVER); + private static final InetAddress TEST_VPN_CLIENT_IP_2 = + InetAddresses.parseNumericAddress("192.0.2.200"); + private static final InetAddress TEST_VPN_SERVER_IP_2 = + InetAddresses.parseNumericAddress("192.0.2.201"); + private static final InetAddress TEST_VPN_INTERNAL_IP = + InetAddresses.parseNumericAddress("198.51.100.10"); + private static final InetAddress TEST_VPN_INTERNAL_IP6 = + InetAddresses.parseNumericAddress("2001:db8::1"); + private static final InetAddress TEST_VPN_INTERNAL_DNS = + InetAddresses.parseNumericAddress("8.8.8.8"); + private static final InetAddress TEST_VPN_INTERNAL_DNS6 = + InetAddresses.parseNumericAddress("2001:4860:4860::8888"); + + private static final IkeTrafficSelector IN_TS = + new IkeTrafficSelector(MIN_PORT, MAX_PORT, TEST_VPN_INTERNAL_IP, TEST_VPN_INTERNAL_IP); + private static final IkeTrafficSelector IN_TS6 = + new IkeTrafficSelector( + MIN_PORT, MAX_PORT, TEST_VPN_INTERNAL_IP6, TEST_VPN_INTERNAL_IP6); + private static final IkeTrafficSelector OUT_TS = + new IkeTrafficSelector(MIN_PORT, MAX_PORT, + InetAddresses.parseNumericAddress("0.0.0.0"), + InetAddresses.parseNumericAddress("255.255.255.255")); + private static final IkeTrafficSelector OUT_TS6 = + new IkeTrafficSelector( + MIN_PORT, + MAX_PORT, + InetAddresses.parseNumericAddress("::"), + InetAddresses.parseNumericAddress("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")); + + private static final Network TEST_NETWORK = new Network(Integer.MAX_VALUE); + private static final Network TEST_NETWORK_2 = new Network(Integer.MAX_VALUE - 1); + private static final String TEST_IFACE_NAME = "TEST_IFACE"; + private static final int TEST_TUNNEL_RESOURCE_ID = 0x2345; + private static final long TEST_TIMEOUT_MS = 500L; + private static final long TIMEOUT_CROSSTHREAD_MS = 20_000L; + private static final String PRIMARY_USER_APP_EXCLUDE_KEY = + "VPNAPPEXCLUDED_27_com.testvpn.vpn"; + static final String PKGS_BYTES = getPackageByteString(List.of(PKGS)); + private static final Range<Integer> PRIMARY_USER_RANGE = uidRangeForUser(PRIMARY_USER.id); + private static final int TEST_KEEPALIVE_TIMER = 800; + private static final int TEST_SUB_ID = 1234; + private static final String TEST_MCCMNC = "12345"; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext; + @Mock private UserManager mUserManager; + @Mock private PackageManager mPackageManager; + @Mock private INetworkManagementService mNetService; + @Mock private INetd mNetd; + @Mock private AppOpsManager mAppOps; + @Mock private NotificationManager mNotificationManager; + @Mock private Vpn.SystemServices mSystemServices; + @Mock private Vpn.IkeSessionWrapper mIkeSessionWrapper; + @Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator; + @Mock private Vpn.VpnNetworkAgentWrapper mMockNetworkAgent; + @Mock private ConnectivityManager mConnectivityManager; + @Mock private ConnectivityDiagnosticsManager mCdm; + @Mock private TelephonyManager mTelephonyManager; + @Mock private TelephonyManager mTmPerSub; + @Mock private CarrierConfigManager mConfigManager; + @Mock private SubscriptionManager mSubscriptionManager; + @Mock private IpSecService mIpSecService; + @Mock private VpnProfileStore mVpnProfileStore; + private final TestExecutor mExecutor; + @Mock DeviceIdleInternal mDeviceIdleInternal; + private final VpnProfile mVpnProfile; + + @Captor private ArgumentCaptor<Collection<Range<Integer>>> mUidRangesCaptor; + + private IpSecManager mIpSecManager; + private TestDeps mTestDeps; + + public static class TestExecutor extends ScheduledThreadPoolExecutor { + public static final long REAL_DELAY = -1; + + // For the purposes of the test, run all scheduled tasks after 10ms to save + // execution time, unless overridden by the specific test. Set to REAL_DELAY + // to actually wait for the delay specified by the real call to schedule(). + public long delayMs = 10; + // If this is true, execute() will call the runnable inline. This is useful because + // super.execute() calls schedule(), which messes with checks that scheduled() is + // called a given number of times. + public boolean executeDirect = false; + + public TestExecutor() { + super(1); + } + + @Override + public void execute(final Runnable command) { + // See |executeDirect| for why this is necessary. + if (executeDirect) { + command.run(); + } else { + super.execute(command); + } + } + + @Override + public ScheduledFuture<?> schedule(final Runnable command, final long delay, + TimeUnit unit) { + if (0 == delay || delayMs == REAL_DELAY) { + // super.execute() calls schedule() with 0, so use the real delay if it's 0. + return super.schedule(command, delay, unit); + } else { + return super.schedule(command, delayMs, TimeUnit.MILLISECONDS); + } + } + } + + public VpnTest() throws Exception { + // Build an actual VPN profile that is capable of being converted to and from an + // Ikev2VpnProfile + final Ikev2VpnProfile.Builder builder = + new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY); + builder.setAuthPsk(TEST_VPN_PSK); + builder.setBypassable(true /* isBypassable */); + mExecutor = spy(new TestExecutor()); + mVpnProfile = builder.build().toVpnProfile(); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mIpSecManager = new IpSecManager(mContext, mIpSecService); + mTestDeps = spy(new TestDeps()); + doReturn(IPV6_MIN_MTU) + .when(mTestDeps) + .calculateVpnMtu(any(), anyInt(), anyInt(), anyBoolean()); + doReturn(1500).when(mTestDeps).getJavaNetworkInterfaceMtu(any(), anyInt()); + + when(mContext.getPackageManager()).thenReturn(mPackageManager); + setMockedPackages(sPackages); + + when(mContext.getPackageName()).thenReturn(TEST_VPN_PKG); + when(mContext.getOpPackageName()).thenReturn(TEST_VPN_PKG); + mockService(UserManager.class, Context.USER_SERVICE, mUserManager); + mockService(AppOpsManager.class, Context.APP_OPS_SERVICE, mAppOps); + mockService(NotificationManager.class, Context.NOTIFICATION_SERVICE, mNotificationManager); + mockService(ConnectivityManager.class, Context.CONNECTIVITY_SERVICE, mConnectivityManager); + mockService(IpSecManager.class, Context.IPSEC_SERVICE, mIpSecManager); + mockService(ConnectivityDiagnosticsManager.class, Context.CONNECTIVITY_DIAGNOSTICS_SERVICE, + mCdm); + mockService(TelephonyManager.class, Context.TELEPHONY_SERVICE, mTelephonyManager); + mockService(CarrierConfigManager.class, Context.CARRIER_CONFIG_SERVICE, mConfigManager); + mockService(SubscriptionManager.class, Context.TELEPHONY_SUBSCRIPTION_SERVICE, + mSubscriptionManager); + doReturn(mTmPerSub).when(mTelephonyManager).createForSubscriptionId(anyInt()); + when(mContext.getString(R.string.config_customVpnAlwaysOnDisconnectedDialogComponent)) + .thenReturn(Resources.getSystem().getString( + R.string.config_customVpnAlwaysOnDisconnectedDialogComponent)); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS)) + .thenReturn(true); + + // Used by {@link Notification.Builder} + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT; + when(mContext.getApplicationInfo()).thenReturn(applicationInfo); + when(mPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())) + .thenReturn(applicationInfo); + + doNothing().when(mNetService).registerObserver(any()); + + // Deny all appops by default. + when(mAppOps.noteOpNoThrow(anyString(), anyInt(), anyString(), any(), any())) + .thenReturn(AppOpsManager.MODE_IGNORED); + + // Setup IpSecService + final IpSecTunnelInterfaceResponse tunnelResp = + new IpSecTunnelInterfaceResponse( + IpSecManager.Status.OK, TEST_TUNNEL_RESOURCE_ID, TEST_IFACE_NAME); + when(mIpSecService.createTunnelInterface(any(), any(), any(), any(), any())) + .thenReturn(tunnelResp); + doReturn(new LinkProperties()).when(mConnectivityManager).getLinkProperties(any()); + + // The unit test should know what kind of permission it needs and set the permission by + // itself, so set the default value of Context#checkCallingOrSelfPermission to + // PERMISSION_DENIED. + doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(any()); + + // Set up mIkev2SessionCreator and mExecutor + resetIkev2SessionCreator(mIkeSessionWrapper); + } + + private void resetIkev2SessionCreator(Vpn.IkeSessionWrapper ikeSession) { + reset(mIkev2SessionCreator); + when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any())) + .thenReturn(ikeSession); + } + + private <T> void mockService(Class<T> clazz, String name, T service) { + doReturn(service).when(mContext).getSystemService(name); + doReturn(name).when(mContext).getSystemServiceName(clazz); + if (mContext.getSystemService(clazz).getClass().equals(Object.class)) { + // Test is using mockito-extended (mContext uses Answers.RETURNS_DEEP_STUBS and returned + // a mock object on a final method) + doCallRealMethod().when(mContext).getSystemService(clazz); + } + } + + private Set<Range<Integer>> rangeSet(Range<Integer> ... ranges) { + final Set<Range<Integer>> range = new ArraySet<>(); + for (Range<Integer> r : ranges) range.add(r); + + return range; + } + + private static Range<Integer> uidRangeForUser(int userId) { + return new Range<Integer>(userId * PER_USER_RANGE, (userId + 1) * PER_USER_RANGE - 1); + } + + private Range<Integer> uidRange(int start, int stop) { + return new Range<Integer>(start, stop); + } + + private static String getPackageByteString(List<String> packages) { + try { + return HexDump.toHexString( + PersistableBundleUtils.toDiskStableBytes(PersistableBundleUtils.fromList( + packages, PersistableBundleUtils.STRING_SERIALIZER)), + true /* upperCase */); + } catch (IOException e) { + return null; + } + } + + @Test + public void testRestrictedProfilesAreAddedToVpn() { + setMockedUsers(PRIMARY_USER, SECONDARY_USER, RESTRICTED_PROFILE_A, RESTRICTED_PROFILE_B); + + final Vpn vpn = createVpn(PRIMARY_USER.id); + + // Assume the user can have restricted profiles. + doReturn(true).when(mUserManager).canHaveRestrictedProfile(); + final Set<Range<Integer>> ranges = + vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id, null, null); + + assertEquals(rangeSet(PRIMARY_USER_RANGE, uidRangeForUser(RESTRICTED_PROFILE_A.id)), + ranges); + } + + @Test + public void testManagedProfilesAreNotAddedToVpn() { + setMockedUsers(PRIMARY_USER, MANAGED_PROFILE_A); + + final Vpn vpn = createVpn(PRIMARY_USER.id); + final Set<Range<Integer>> ranges = vpn.createUserAndRestrictedProfilesRanges( + PRIMARY_USER.id, null, null); + + assertEquals(rangeSet(PRIMARY_USER_RANGE), ranges); + } + + @Test + public void testAddUserToVpnOnlyAddsOneUser() { + setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A, MANAGED_PROFILE_A); + + final Vpn vpn = createVpn(PRIMARY_USER.id); + final Set<Range<Integer>> ranges = new ArraySet<>(); + vpn.addUserToRanges(ranges, PRIMARY_USER.id, null, null); + + assertEquals(rangeSet(PRIMARY_USER_RANGE), ranges); + } + + @Test + public void testUidAllowAndDenylist() throws Exception { + final Vpn vpn = createVpn(PRIMARY_USER.id); + final Range<Integer> user = PRIMARY_USER_RANGE; + final int userStart = user.getLower(); + final int userStop = user.getUpper(); + final String[] packages = {PKGS[0], PKGS[1], PKGS[2]}; + + // Allowed list + final Set<Range<Integer>> allow = vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id, + Arrays.asList(packages), null /* disallowedApplications */); + assertEquals(rangeSet( + uidRange(userStart + PKG_UIDS[0], userStart + PKG_UIDS[0]), + uidRange(userStart + PKG_UIDS[1], userStart + PKG_UIDS[2]), + uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[0]), + Process.toSdkSandboxUid(userStart + PKG_UIDS[0])), + uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[1]), + Process.toSdkSandboxUid(userStart + PKG_UIDS[2]))), + allow); + + // Denied list + final Set<Range<Integer>> disallow = + vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id, + null /* allowedApplications */, Arrays.asList(packages)); + assertEquals(rangeSet( + uidRange(userStart, userStart + PKG_UIDS[0] - 1), + uidRange(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1), + /* Empty range between UIDS[1] and UIDS[2], should be excluded, */ + uidRange(userStart + PKG_UIDS[2] + 1, + Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)), + uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), + Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)), + uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)), + disallow); + } + + private void verifyPowerSaveTempWhitelistApp(String packageName) { + verify(mDeviceIdleInternal, timeout(TEST_TIMEOUT_MS)).addPowerSaveTempWhitelistApp( + anyInt(), eq(packageName), anyLong(), anyInt(), eq(false), + eq(PowerWhitelistManager.REASON_VPN), eq("VpnManager event")); + } + + @Test + public void testGetAlwaysAndOnGetLockDown() throws Exception { + final Vpn vpn = createVpn(PRIMARY_USER.id); + + // Default state. + assertFalse(vpn.getAlwaysOn()); + assertFalse(vpn.getLockdown()); + + // Set always-on without lockdown. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, Collections.emptyList())); + assertTrue(vpn.getAlwaysOn()); + assertFalse(vpn.getLockdown()); + + // Set always-on with lockdown. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, Collections.emptyList())); + assertTrue(vpn.getAlwaysOn()); + assertTrue(vpn.getLockdown()); + + // Remove always-on configuration. + assertTrue(vpn.setAlwaysOnPackage(null, false, Collections.emptyList())); + assertFalse(vpn.getAlwaysOn()); + assertFalse(vpn.getLockdown()); + } + + @Test + public void testAlwaysOnWithoutLockdown() throws Exception { + final Vpn vpn = createVpn(PRIMARY_USER.id); + assertTrue(vpn.setAlwaysOnPackage( + PKGS[1], false /* lockdown */, null /* lockdownAllowlist */)); + verify(mConnectivityManager, never()).setRequireVpnForUids(anyBoolean(), any()); + + assertTrue(vpn.setAlwaysOnPackage( + null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */)); + verify(mConnectivityManager, never()).setRequireVpnForUids(anyBoolean(), any()); + } + + @Test + public void testLockdownChangingPackage() throws Exception { + final Vpn vpn = createVpn(PRIMARY_USER.id); + final Range<Integer> user = PRIMARY_USER_RANGE; + final int userStart = user.getLower(); + final int userStop = user.getUpper(); + // Set always-on without lockdown. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, null)); + + // Set always-on with lockdown. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, null)); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1), + new UidRangeParcel(userStart + PKG_UIDS[1] + 1, + Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop) + })); + + // Switch to another app. + assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null)); + verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1), + new UidRangeParcel(userStart + PKG_UIDS[1] + 1, + Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop) + })); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart, userStart + PKG_UIDS[3] - 1), + new UidRangeParcel(userStart + PKG_UIDS[3] + 1, + Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop) + })); + } + + @Test + public void testLockdownAllowlist() throws Exception { + final Vpn vpn = createVpn(PRIMARY_USER.id); + final Range<Integer> user = PRIMARY_USER_RANGE; + final int userStart = user.getLower(); + final int userStop = user.getUpper(); + // Set always-on with lockdown and allow app PKGS[2] from lockdown. + assertTrue(vpn.setAlwaysOnPackage( + PKGS[1], true, Collections.singletonList(PKGS[2]))); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1), + new UidRangeParcel(userStart + PKG_UIDS[2] + 1, + Process.toSdkSandboxUid(userStart + PKG_UIDS[1]) - 1), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop) + })); + // Change allowed app list to PKGS[3]. + assertTrue(vpn.setAlwaysOnPackage( + PKGS[1], true, Collections.singletonList(PKGS[3]))); + verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[2] + 1, + Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop) + })); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1), + new UidRangeParcel(userStart + PKG_UIDS[3] + 1, + Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), + Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop) + })); + + // Change the VPN app. + assertTrue(vpn.setAlwaysOnPackage( + PKGS[0], true, Collections.singletonList(PKGS[3]))); + verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1), + new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1), + new UidRangeParcel(userStart + PKG_UIDS[3] + 1, + Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), + Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)) + })); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart, userStart + PKG_UIDS[0] - 1), + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1), + new UidRangeParcel(userStart + PKG_UIDS[3] + 1, + Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), + Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)) + })); + + // Remove the list of allowed packages. + assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, null)); + verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1), + new UidRangeParcel(userStart + PKG_UIDS[3] + 1, + Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), + Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop) + })); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, + Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), userStop), + })); + + // Add the list of allowed packages. + assertTrue(vpn.setAlwaysOnPackage( + PKGS[0], true, Collections.singletonList(PKGS[1]))); + verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, + Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), userStop), + })); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1), + new UidRangeParcel(userStart + PKG_UIDS[1] + 1, + Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), + Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop) + })); + + // Try allowing a package with a comma, should be rejected. + assertFalse(vpn.setAlwaysOnPackage( + PKGS[0], true, Collections.singletonList("a.b,c.d"))); + + // Pass a non-existent packages in the allowlist, they (and only they) should be ignored. + // allowed package should change from PGKS[1] to PKGS[2]. + assertTrue(vpn.setAlwaysOnPackage( + PKGS[0], true, Arrays.asList("com.foo.app", PKGS[2], "com.bar.app"))); + verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1), + new UidRangeParcel(userStart + PKG_UIDS[1] + 1, + Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), + Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop) + })); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[2] - 1), + new UidRangeParcel(userStart + PKG_UIDS[2] + 1, + Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), + Process.toSdkSandboxUid(userStart + PKG_UIDS[2] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop) + })); + } + + @Test + public void testLockdownSystemUser() throws Exception { + final Vpn vpn = createVpn(SYSTEM_USER_ID); + + // Uid 0 is always excluded and PKG_UIDS[1] is the uid of the VPN. + final List<Integer> excludedUids = new ArrayList<>(List.of(0, PKG_UIDS[1])); + final List<Range<Integer>> ranges = makeVpnUidRange(SYSTEM_USER_ID, excludedUids); + + // Set always-on with lockdown. + assertTrue(vpn.setAlwaysOnPackage( + PKGS[1], true /* lockdown */, null /* lockdownAllowlist */)); + verify(mConnectivityManager).setRequireVpnForUids(true, ranges); + + // Disable always-on with lockdown. + assertTrue(vpn.setAlwaysOnPackage( + null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */)); + verify(mConnectivityManager).setRequireVpnForUids(false, ranges); + + // Set always-on with lockdown and allow the app PKGS[2]. + excludedUids.add(PKG_UIDS[2]); + final List<Range<Integer>> ranges2 = makeVpnUidRange(SYSTEM_USER_ID, excludedUids); + assertTrue(vpn.setAlwaysOnPackage( + PKGS[1], true /* lockdown */, Collections.singletonList(PKGS[2]))); + verify(mConnectivityManager).setRequireVpnForUids(true, ranges2); + + // Disable always-on with lockdown. + assertTrue(vpn.setAlwaysOnPackage( + null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */)); + verify(mConnectivityManager).setRequireVpnForUids(false, ranges2); + } + + @Test + public void testLockdownRuleRepeatability() throws Exception { + final Vpn vpn = createVpn(PRIMARY_USER.id); + final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] { + new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())}; + // Given legacy lockdown is already enabled, + vpn.setLockdown(true); + verify(mConnectivityManager, times(1)).setRequireVpnForUids(true, + toRanges(primaryUserRangeParcel)); + + // Enabling legacy lockdown twice should do nothing. + vpn.setLockdown(true); + verify(mConnectivityManager, times(1)).setRequireVpnForUids(anyBoolean(), any()); + + // And disabling should remove the rules exactly once. + vpn.setLockdown(false); + verify(mConnectivityManager, times(1)).setRequireVpnForUids(false, + toRanges(primaryUserRangeParcel)); + + // Removing the lockdown again should have no effect. + vpn.setLockdown(false); + verify(mConnectivityManager, times(2)).setRequireVpnForUids(anyBoolean(), any()); + } + + private ArrayList<Range<Integer>> toRanges(UidRangeParcel[] ranges) { + ArrayList<Range<Integer>> rangesArray = new ArrayList<>(ranges.length); + for (int i = 0; i < ranges.length; i++) { + rangesArray.add(new Range<>(ranges[i].start, ranges[i].stop)); + } + return rangesArray; + } + + @Test + public void testLockdownRuleReversibility() throws Exception { + doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN); + final Vpn vpn = createVpn(PRIMARY_USER.id); + final UidRangeParcel[] entireUser = { + new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper()) + }; + final UidRangeParcel[] exceptPkg0 = { + new UidRangeParcel(entireUser[0].start, entireUser[0].start + PKG_UIDS[0] - 1), + new UidRangeParcel(entireUser[0].start + PKG_UIDS[0] + 1, + Process.toSdkSandboxUid(entireUser[0].start + PKG_UIDS[0] - 1)), + new UidRangeParcel(Process.toSdkSandboxUid(entireUser[0].start + PKG_UIDS[0] + 1), + entireUser[0].stop), + }; + + final InOrder order = inOrder(mConnectivityManager); + + // Given lockdown is enabled with no package (legacy VPN), + vpn.setLockdown(true); + order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(entireUser)); + + // When a new VPN package is set the rules should change to cover that package. + vpn.prepare(null, PKGS[0], VpnManager.TYPE_VPN_SERVICE); + order.verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(entireUser)); + order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(exceptPkg0)); + + // When that VPN package is unset, everything should be undone again in reverse. + vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE); + order.verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(exceptPkg0)); + order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(entireUser)); + } + + @Test + public void testOnUserAddedAndRemoved_restrictedUser() throws Exception { + final InOrder order = inOrder(mMockNetworkAgent); + final Vpn vpn = createVpn(PRIMARY_USER.id); + final Set<Range<Integer>> initialRange = rangeSet(PRIMARY_USER_RANGE); + // Note since mVpnProfile is a Ikev2VpnProfile, this starts an IkeV2VpnRunner. + startLegacyVpn(vpn, mVpnProfile); + // Set an initial Uid range and mock the network agent + vpn.mNetworkCapabilities.setUids(initialRange); + vpn.mNetworkAgent = mMockNetworkAgent; + + // Add the restricted user + setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A); + vpn.onUserAdded(RESTRICTED_PROFILE_A.id); + // Expect restricted user range to be added to the NetworkCapabilities. + final Set<Range<Integer>> expectRestrictedRange = + rangeSet(PRIMARY_USER_RANGE, uidRangeForUser(RESTRICTED_PROFILE_A.id)); + assertEquals(expectRestrictedRange, vpn.mNetworkCapabilities.getUids()); + order.verify(mMockNetworkAgent).doSendNetworkCapabilities( + argThat(nc -> expectRestrictedRange.equals(nc.getUids()))); + + // Remove the restricted user + vpn.onUserRemoved(RESTRICTED_PROFILE_A.id); + // Expect restricted user range to be removed from the NetworkCapabilities. + assertEquals(initialRange, vpn.mNetworkCapabilities.getUids()); + order.verify(mMockNetworkAgent).doSendNetworkCapabilities( + argThat(nc -> initialRange.equals(nc.getUids()))); + } + + @Test + public void testOnUserAddedAndRemoved_restrictedUserLockdown() throws Exception { + final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] { + new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())}; + final Range<Integer> restrictedUserRange = uidRangeForUser(RESTRICTED_PROFILE_A.id); + final UidRangeParcel[] restrictedUserRangeParcel = new UidRangeParcel[] { + new UidRangeParcel(restrictedUserRange.getLower(), restrictedUserRange.getUpper())}; + final Vpn vpn = createVpn(PRIMARY_USER.id); + + // Set lockdown calls setRequireVpnForUids + vpn.setLockdown(true); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(primaryUserRangeParcel)); + + // Add the restricted user + doReturn(true).when(mUserManager).canHaveRestrictedProfile(); + setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A); + vpn.onUserAdded(RESTRICTED_PROFILE_A.id); + + // Expect restricted user range to be added. + verify(mConnectivityManager).setRequireVpnForUids(true, + toRanges(restrictedUserRangeParcel)); + + // Mark as partial indicates that the user is removed, mUserManager.getAliveUsers() does not + // return the restricted user but it is still returned in mUserManager.getUserInfo(). + RESTRICTED_PROFILE_A.partial = true; + // Remove the restricted user + vpn.onUserRemoved(RESTRICTED_PROFILE_A.id); + verify(mConnectivityManager).setRequireVpnForUids(false, + toRanges(restrictedUserRangeParcel)); + // reset to avoid affecting other tests since RESTRICTED_PROFILE_A is static. + RESTRICTED_PROFILE_A.partial = false; + } + + @Test + public void testOnUserAddedAndRemoved_restrictedUserAlwaysOn() throws Exception { + final Vpn vpn = createVpn(PRIMARY_USER.id); + + // setAlwaysOnPackage() calls setRequireVpnForUids() + assertTrue(vpn.setAlwaysOnPackage( + PKGS[0], true /* lockdown */, null /* lockdownAllowlist */)); + final List<Integer> excludedUids = List.of(PKG_UIDS[0]); + final List<Range<Integer>> primaryRanges = + makeVpnUidRange(PRIMARY_USER.id, excludedUids); + verify(mConnectivityManager).setRequireVpnForUids(true, primaryRanges); + + // Add the restricted user + doReturn(true).when(mUserManager).canHaveRestrictedProfile(); + setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A); + vpn.onUserAdded(RESTRICTED_PROFILE_A.id); + + final List<Range<Integer>> restrictedRanges = + makeVpnUidRange(RESTRICTED_PROFILE_A.id, excludedUids); + // Expect restricted user range to be added. + verify(mConnectivityManager).setRequireVpnForUids(true, restrictedRanges); + + // Mark as partial indicates that the user is removed, mUserManager.getAliveUsers() does not + // return the restricted user but it is still returned in mUserManager.getUserInfo(). + RESTRICTED_PROFILE_A.partial = true; + // Remove the restricted user + vpn.onUserRemoved(RESTRICTED_PROFILE_A.id); + verify(mConnectivityManager).setRequireVpnForUids(false, restrictedRanges); + + // reset to avoid affecting other tests since RESTRICTED_PROFILE_A is static. + RESTRICTED_PROFILE_A.partial = false; + } + + @Test + public void testPrepare_throwSecurityExceptionWhenGivenPackageDoesNotBelongToTheCaller() + throws Exception { + mTestDeps.mIgnoreCallingUidChecks = false; + final Vpn vpn = createVpn(); + assertThrows(SecurityException.class, + () -> vpn.prepare("com.not.vpn.owner", null, VpnManager.TYPE_VPN_SERVICE)); + assertThrows(SecurityException.class, + () -> vpn.prepare(null, "com.not.vpn.owner", VpnManager.TYPE_VPN_SERVICE)); + assertThrows(SecurityException.class, + () -> vpn.prepare("com.not.vpn.owner1", "com.not.vpn.owner2", + VpnManager.TYPE_VPN_SERVICE)); + } + + @Test + public void testPrepare_bothOldPackageAndNewPackageAreNull() throws Exception { + final Vpn vpn = createVpn(); + assertTrue(vpn.prepare(null, null, VpnManager.TYPE_VPN_SERVICE)); + + } + + @Test + public void testPrepare_legacyVpnWithoutControlVpn() + throws Exception { + doThrow(new SecurityException("no CONTROL_VPN")).when(mContext) + .enforceCallingOrSelfPermission(eq(CONTROL_VPN), any()); + final Vpn vpn = createVpn(); + assertThrows(SecurityException.class, + () -> vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE)); + + // CONTROL_VPN can be held by the caller or another system server process - both are + // allowed. Just checking for `enforceCallingPermission` may not be sufficient. + verify(mContext, never()).enforceCallingPermission(eq(CONTROL_VPN), any()); + } + + @Test + public void testPrepare_legacyVpnWithControlVpn() + throws Exception { + doNothing().when(mContext).enforceCallingOrSelfPermission(eq(CONTROL_VPN), any()); + final Vpn vpn = createVpn(); + assertTrue(vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE)); + + // CONTROL_VPN can be held by the caller or another system server process - both are + // allowed. Just checking for `enforceCallingPermission` may not be sufficient. + verify(mContext, never()).enforceCallingPermission(eq(CONTROL_VPN), any()); + } + + @Test + public void testIsAlwaysOnPackageSupported() throws Exception { + final Vpn vpn = createVpn(PRIMARY_USER.id); + + ApplicationInfo appInfo = new ApplicationInfo(); + when(mPackageManager.getApplicationInfoAsUser(eq(PKGS[0]), anyInt(), eq(PRIMARY_USER.id))) + .thenReturn(appInfo); + + ServiceInfo svcInfo = new ServiceInfo(); + ResolveInfo resInfo = new ResolveInfo(); + resInfo.serviceInfo = svcInfo; + when(mPackageManager.queryIntentServicesAsUser(any(), eq(PackageManager.GET_META_DATA), + eq(PRIMARY_USER.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])); + } + + @Test + public void testNotificationShownForAlwaysOnApp() throws Exception { + final UserHandle userHandle = UserHandle.of(PRIMARY_USER.id); + final Vpn vpn = createVpn(PRIMARY_USER.id); + setMockedUsers(PRIMARY_USER); + + final InOrder order = inOrder(mNotificationManager); + + // Don't show a notification for regular disconnected states. + vpn.updateState(DetailedState.DISCONNECTED, TAG); + order.verify(mNotificationManager, atLeastOnce()).cancel(anyString(), anyInt()); + + // Start showing a notification for disconnected once always-on. + vpn.setAlwaysOnPackage(PKGS[0], false, null); + order.verify(mNotificationManager).notify(anyString(), anyInt(), any()); + + // Stop showing the notification once connected. + vpn.updateState(DetailedState.CONNECTED, TAG); + order.verify(mNotificationManager).cancel(anyString(), anyInt()); + + // Show the notification if we disconnect again. + vpn.updateState(DetailedState.DISCONNECTED, TAG); + order.verify(mNotificationManager).notify(anyString(), anyInt(), any()); + + // Notification should be cleared after unsetting always-on package. + vpn.setAlwaysOnPackage(null, false, null); + order.verify(mNotificationManager).cancel(anyString(), anyInt()); + } + + /** + * The profile name should NOT change between releases for backwards compatibility + * + * <p>If this is changed between releases, the {@link Vpn#getVpnProfilePrivileged()} method MUST + * be updated to ensure backward compatibility. + */ + @Test + public void testGetProfileNameForPackage() throws Exception { + final Vpn vpn = createVpn(PRIMARY_USER.id); + setMockedUsers(PRIMARY_USER); + + final String expected = Credentials.PLATFORM_VPN + PRIMARY_USER.id + "_" + TEST_VPN_PKG; + assertEquals(expected, vpn.getProfileNameForPackage(TEST_VPN_PKG)); + } + + private Vpn createVpn(String... grantedOps) throws Exception { + return createVpn(PRIMARY_USER, grantedOps); + } + + private Vpn createVpn(UserInfo user, String... grantedOps) throws Exception { + final Vpn vpn = createVpn(user.id); + setMockedUsers(user); + + for (final String opStr : grantedOps) { + when(mAppOps.noteOpNoThrow(opStr, Process.myUid(), TEST_VPN_PKG, + null /* attributionTag */, null /* message */)) + .thenReturn(AppOpsManager.MODE_ALLOWED); + } + + return vpn; + } + + private void checkProvisionVpnProfile(Vpn vpn, boolean expectedResult, String... checkedOps) { + assertEquals(expectedResult, vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile)); + + // The profile should always be stored, whether or not consent has been previously granted. + verify(mVpnProfileStore) + .put( + eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)), + eq(mVpnProfile.encode())); + + for (final String checkedOpStr : checkedOps) { + verify(mAppOps).noteOpNoThrow(checkedOpStr, Process.myUid(), TEST_VPN_PKG, + null /* attributionTag */, null /* message */); + } + } + + @Test + public void testProvisionVpnProfileNoIpsecTunnels() throws Exception { + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS)) + .thenReturn(false); + final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + try { + checkProvisionVpnProfile( + vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + fail("Expected exception due to missing feature"); + } catch (UnsupportedOperationException expected) { + } + } + + private String startVpnForVerifyAppExclusionList(Vpn vpn) throws Exception { + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + when(mVpnProfileStore.get(PRIMARY_USER_APP_EXCLUDE_KEY)) + .thenReturn(HexDump.hexStringToByteArray(PKGS_BYTES)); + final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG); + final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges( + PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS)); + verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges)); + clearInvocations(mConnectivityManager); + verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + vpn.mNetworkAgent = mMockNetworkAgent; + + return sessionKey; + } + + private Vpn prepareVpnForVerifyAppExclusionList() throws Exception { + final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + startVpnForVerifyAppExclusionList(vpn); + + return vpn; + } + + @Test + public void testSetAndGetAppExclusionList() throws Exception { + final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + final String sessionKey = startVpnForVerifyAppExclusionList(vpn); + verify(mVpnProfileStore, never()).put(eq(PRIMARY_USER_APP_EXCLUDE_KEY), any()); + vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS)); + verify(mVpnProfileStore) + .put(eq(PRIMARY_USER_APP_EXCLUDE_KEY), + eq(HexDump.hexStringToByteArray(PKGS_BYTES))); + final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges( + PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS)); + verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges)); + assertEquals(uidRanges, vpn.mNetworkCapabilities.getUids()); + assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG)); + } + + @Test + public void testRefreshPlatformVpnAppExclusionList_updatesExcludedUids() throws Exception { + final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + final String sessionKey = startVpnForVerifyAppExclusionList(vpn); + vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS)); + final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges( + PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS)); + verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges)); + verify(mMockNetworkAgent).doSendNetworkCapabilities(any()); + assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG)); + + reset(mMockNetworkAgent); + + // Remove one of the package + List<Integer> newExcludedUids = toList(PKG_UIDS); + newExcludedUids.remove((Integer) PKG_UIDS[0]); + Set<Range<Integer>> newUidRanges = makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids); + sPackages.remove(PKGS[0]); + vpn.refreshPlatformVpnAppExclusionList(); + + // List in keystore is not changed, but UID for the removed packages is no longer exempted. + assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG)); + assertEquals(newUidRanges, vpn.mNetworkCapabilities.getUids()); + ArgumentCaptor<NetworkCapabilities> ncCaptor = + ArgumentCaptor.forClass(NetworkCapabilities.class); + verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture()); + assertEquals(newUidRanges, ncCaptor.getValue().getUids()); + verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(newUidRanges)); + + reset(mMockNetworkAgent); + + // Add the package back + newExcludedUids.add(PKG_UIDS[0]); + newUidRanges = makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids); + sPackages.put(PKGS[0], PKG_UIDS[0]); + vpn.refreshPlatformVpnAppExclusionList(); + + // List in keystore is not changed and the uid list should be updated in the net cap. + assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG)); + assertEquals(newUidRanges, vpn.mNetworkCapabilities.getUids()); + verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture()); + assertEquals(newUidRanges, ncCaptor.getValue().getUids()); + + // The uidRange is the same as the original setAppExclusionList so this is the second call + verify(mConnectivityManager, times(2)) + .setVpnDefaultForUids(eq(sessionKey), eq(newUidRanges)); + } + + private List<Range<Integer>> makeVpnUidRange(int userId, List<Integer> excludedAppIdList) { + final SortedSet<Integer> list = new TreeSet<>(); + + final int userBase = userId * UserHandle.PER_USER_RANGE; + for (int appId : excludedAppIdList) { + final int uid = UserHandle.getUid(userId, appId); + list.add(uid); + if (Process.isApplicationUid(uid)) { + list.add(Process.toSdkSandboxUid(uid)); // Add Sdk Sandbox UID + } + } + + final int minUid = userBase; + final int maxUid = userBase + UserHandle.PER_USER_RANGE - 1; + final List<Range<Integer>> ranges = new ArrayList<>(); + + // Iterate the list to create the ranges between each uid. + int start = minUid; + for (int uid : list) { + if (uid == start) { + start++; + } else { + ranges.add(new Range<>(start, uid - 1)); + start = uid + 1; + } + } + + // Create the range between last uid and max uid. + if (start <= maxUid) { + ranges.add(new Range<>(start, maxUid)); + } + + return ranges; + } + + private Set<Range<Integer>> makeVpnUidRangeSet(int userId, List<Integer> excludedAppIdList) { + return new ArraySet<>(makeVpnUidRange(userId, excludedAppIdList)); + } + + @Test + public void testSetAndGetAppExclusionListRestrictedUser() throws Exception { + final Vpn vpn = prepareVpnForVerifyAppExclusionList(); + + // Mock it to restricted profile + when(mUserManager.getUserInfo(anyInt())).thenReturn(RESTRICTED_PROFILE_A); + + // Restricted users cannot configure VPNs + assertThrows(SecurityException.class, + () -> vpn.setAppExclusionList(TEST_VPN_PKG, new ArrayList<>())); + + assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG)); + } + + @Test + public void testProvisionVpnProfilePreconsented() throws Exception { + final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + checkProvisionVpnProfile( + vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + } + + @Test + public void testProvisionVpnProfileNotPreconsented() throws Exception { + final Vpn vpn = createVpn(); + + // Expect that both the ACTIVATE_VPN and ACTIVATE_PLATFORM_VPN were tried, but the caller + // had neither. + checkProvisionVpnProfile(vpn, false /* expectedResult */, + AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN, AppOpsManager.OPSTR_ACTIVATE_VPN); + } + + @Test + public void testProvisionVpnProfileVpnServicePreconsented() throws Exception { + final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_VPN); + + checkProvisionVpnProfile(vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_VPN); + } + + @Test + public void testProvisionVpnProfileTooLarge() throws Exception { + final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + final VpnProfile bigProfile = new VpnProfile(""); + bigProfile.name = new String(new byte[Vpn.MAX_VPN_PROFILE_SIZE_BYTES + 1]); + + try { + vpn.provisionVpnProfile(TEST_VPN_PKG, bigProfile); + fail("Expected IAE due to profile size"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testProvisionVpnProfileRestrictedUser() throws Exception { + final Vpn vpn = + createVpn( + RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + try { + vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile); + fail("Expected SecurityException due to restricted user"); + } catch (SecurityException expected) { + } + } + + @Test + public void testDeleteVpnProfile() throws Exception { + final Vpn vpn = createVpn(); + + vpn.deleteVpnProfile(TEST_VPN_PKG); + + verify(mVpnProfileStore) + .remove(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + } + + @Test + public void testDeleteVpnProfileRestrictedUser() throws Exception { + final Vpn vpn = + createVpn( + RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + try { + vpn.deleteVpnProfile(TEST_VPN_PKG); + fail("Expected SecurityException due to restricted user"); + } catch (SecurityException expected) { + } + } + + @Test + public void testGetVpnProfilePrivileged() throws Exception { + final Vpn vpn = createVpn(); + + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(new VpnProfile("").encode()); + + vpn.getVpnProfilePrivileged(TEST_VPN_PKG); + + verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + } + + private void verifyPlatformVpnIsActivated(String packageName) { + verify(mAppOps).noteOpNoThrow( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(packageName), + eq(null) /* attributionTag */, + eq(null) /* message */); + verify(mAppOps).startOp( + eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER), + eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())), + eq(packageName), + eq(null) /* attributionTag */, + eq(null) /* message */); + } + + private void verifyPlatformVpnIsDeactivated(String packageName) { + // Add a small delay to double confirm that finishOp is only called once. + verify(mAppOps, after(100)).finishOp( + eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER), + eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())), + eq(packageName), + eq(null) /* attributionTag */); + } + + @Test + public void testStartVpnProfile() throws Exception { + final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + + vpn.startVpnProfile(TEST_VPN_PKG); + + verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + verifyPlatformVpnIsActivated(TEST_VPN_PKG); + } + + @Test + public void testStartVpnProfileVpnServicePreconsented() throws Exception { + final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_VPN); + + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + + vpn.startVpnProfile(TEST_VPN_PKG); + + // Verify that the ACTIVATE_VPN appop was checked, but no error was thrown. + verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(), + TEST_VPN_PKG, null /* attributionTag */, null /* message */); + } + + @Test + public void testStartVpnProfileNotConsented() throws Exception { + final Vpn vpn = createVpn(); + + try { + vpn.startVpnProfile(TEST_VPN_PKG); + fail("Expected failure due to no user consent"); + } catch (SecurityException expected) { + } + + // Verify both appops were checked. + verify(mAppOps) + .noteOpNoThrow( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(null) /* attributionTag */, + eq(null) /* message */); + verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(), + TEST_VPN_PKG, null /* attributionTag */, null /* message */); + + // Keystore should never have been accessed. + verify(mVpnProfileStore, never()).get(any()); + } + + @Test + public void testStartVpnProfileMissingProfile() throws Exception { + final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))).thenReturn(null); + + try { + vpn.startVpnProfile(TEST_VPN_PKG); + fail("Expected failure due to missing profile"); + } catch (IllegalArgumentException expected) { + } + + verify(mVpnProfileStore).get(vpn.getProfileNameForPackage(TEST_VPN_PKG)); + verify(mAppOps) + .noteOpNoThrow( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(null) /* attributionTag */, + eq(null) /* message */); + } + + @Test + public void testStartVpnProfileRestrictedUser() throws Exception { + final Vpn vpn = createVpn(RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + try { + vpn.startVpnProfile(TEST_VPN_PKG); + fail("Expected SecurityException due to restricted user"); + } catch (SecurityException expected) { + } + } + + @Test + public void testStopVpnProfileRestrictedUser() throws Exception { + final Vpn vpn = createVpn(RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + try { + vpn.stopVpnProfile(TEST_VPN_PKG); + fail("Expected SecurityException due to restricted user"); + } catch (SecurityException expected) { + } + } + + @Test + public void testStartOpAndFinishOpWillBeCalledWhenPlatformVpnIsOnAndOff() throws Exception { + final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + vpn.startVpnProfile(TEST_VPN_PKG); + verifyPlatformVpnIsActivated(TEST_VPN_PKG); + // Add a small delay to make sure that startOp is only called once. + verify(mAppOps, after(100).times(1)).startOp( + eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER), + eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())), + eq(TEST_VPN_PKG), + eq(null) /* attributionTag */, + eq(null) /* message */); + // Check that the startOp is not called with OPSTR_ESTABLISH_VPN_SERVICE. + verify(mAppOps, never()).startOp( + eq(AppOpsManager.OPSTR_ESTABLISH_VPN_SERVICE), + eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())), + eq(TEST_VPN_PKG), + eq(null) /* attributionTag */, + eq(null) /* message */); + vpn.stopVpnProfile(TEST_VPN_PKG); + verifyPlatformVpnIsDeactivated(TEST_VPN_PKG); + } + + @Test + public void testStartOpWithSeamlessHandover() throws Exception { + // Create with SYSTEM_USER so that establish() will match the user ID when checking + // against Binder.getCallerUid + final Vpn vpn = createVpn(SYSTEM_USER, AppOpsManager.OPSTR_ACTIVATE_VPN); + assertTrue(vpn.prepare(TEST_VPN_PKG, null, VpnManager.TYPE_VPN_SERVICE)); + final VpnConfig config = new VpnConfig(); + config.user = "VpnTest"; + config.addresses.add(new LinkAddress("192.0.2.2/32")); + config.mtu = 1450; + final ResolveInfo resolveInfo = new ResolveInfo(); + final ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.permission = BIND_VPN_SERVICE; + resolveInfo.serviceInfo = serviceInfo; + when(mPackageManager.resolveService(any(), anyInt())).thenReturn(resolveInfo); + when(mContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true); + vpn.establish(config); + verify(mAppOps, times(1)).startOp( + eq(AppOpsManager.OPSTR_ESTABLISH_VPN_SERVICE), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(null) /* attributionTag */, + eq(null) /* message */); + // Call establish() twice with the same config, it should match seamless handover case and + // startOp() shouldn't be called again. + vpn.establish(config); + verify(mAppOps, times(1)).startOp( + eq(AppOpsManager.OPSTR_ESTABLISH_VPN_SERVICE), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(null) /* attributionTag */, + eq(null) /* message */); + } + + private void verifyVpnManagerEvent(String sessionKey, String category, int errorClass, + int errorCode, String[] packageName, @NonNull VpnProfileState... profileState) { + final Context userContext = + mContext.createContextAsUser(UserHandle.of(PRIMARY_USER.id), 0 /* flags */); + final ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class); + + final int verifyTimes = profileState.length; + verify(userContext, timeout(TEST_TIMEOUT_MS).times(verifyTimes)) + .startService(intentArgumentCaptor.capture()); + + for (int i = 0; i < verifyTimes; i++) { + final Intent intent = intentArgumentCaptor.getAllValues().get(i); + assertEquals(packageName[i], intent.getPackage()); + assertEquals(sessionKey, intent.getStringExtra(VpnManager.EXTRA_SESSION_KEY)); + final Set<String> categories = intent.getCategories(); + assertTrue(categories.contains(category)); + assertEquals(1, categories.size()); + assertEquals(errorClass, + intent.getIntExtra(VpnManager.EXTRA_ERROR_CLASS, -1 /* defaultValue */)); + assertEquals(errorCode, + intent.getIntExtra(VpnManager.EXTRA_ERROR_CODE, -1 /* defaultValue */)); + // CATEGORY_EVENT_DEACTIVATED_BY_USER & CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED won't + // send NetworkCapabilities & LinkProperties to VPN app. + // For ERROR_CODE_NETWORK_LOST, the NetworkCapabilities & LinkProperties of underlying + // network will be cleared. So the VPN app will receive null for those 2 extra values. + if (category.equals(VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER) + || category.equals(VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED) + || errorCode == VpnManager.ERROR_CODE_NETWORK_LOST) { + assertNull(intent.getParcelableExtra( + VpnManager.EXTRA_UNDERLYING_NETWORK_CAPABILITIES)); + assertNull(intent.getParcelableExtra(VpnManager.EXTRA_UNDERLYING_LINK_PROPERTIES)); + } else { + assertNotNull(intent.getParcelableExtra( + VpnManager.EXTRA_UNDERLYING_NETWORK_CAPABILITIES)); + assertNotNull(intent.getParcelableExtra( + VpnManager.EXTRA_UNDERLYING_LINK_PROPERTIES)); + } + + assertEquals(profileState[i], intent.getParcelableExtra( + VpnManager.EXTRA_VPN_PROFILE_STATE, VpnProfileState.class)); + } + reset(userContext); + } + + private void verifyDeactivatedByUser(String sessionKey, String[] packageName) { + // CATEGORY_EVENT_DEACTIVATED_BY_USER is not an error event, so both of errorClass and + // errorCode won't be set. + verifyVpnManagerEvent(sessionKey, VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER, + -1 /* errorClass */, -1 /* errorCode */, packageName, + // VPN NetworkAgnet does not switch to CONNECTED in the test, and the state is not + // important here. Verify that the state as it is, i.e. CONNECTING state. + new VpnProfileState(VpnProfileState.STATE_CONNECTING, + sessionKey, false /* alwaysOn */, false /* lockdown */)); + } + + private void verifyAlwaysOnStateChanged(String[] packageName, VpnProfileState... profileState) { + verifyVpnManagerEvent(null /* sessionKey */, + VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */, + -1 /* errorCode */, packageName, profileState); + } + + @Test + public void testVpnManagerEventForUserDeactivated() throws Exception { + // For security reasons, Vpn#prepare() will check that oldPackage and newPackage are either + // null or the package of the caller. This test will call Vpn#prepare() to pretend the old + // VPN is replaced by a new one. But only Settings can change to some other packages, and + // this is checked with CONTROL_VPN so simulate holding CONTROL_VPN in order to pass the + // security checks. + doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN); + final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + + // Test the case that the user deactivates the vpn in vpn app. + final String sessionKey1 = vpn.startVpnProfile(TEST_VPN_PKG); + verifyPlatformVpnIsActivated(TEST_VPN_PKG); + vpn.stopVpnProfile(TEST_VPN_PKG); + verifyPlatformVpnIsDeactivated(TEST_VPN_PKG); + verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG); + reset(mDeviceIdleInternal); + verifyDeactivatedByUser(sessionKey1, new String[] {TEST_VPN_PKG}); + reset(mAppOps); + + // Test the case that the user chooses another vpn and the original one is replaced. + final String sessionKey2 = vpn.startVpnProfile(TEST_VPN_PKG); + verifyPlatformVpnIsActivated(TEST_VPN_PKG); + vpn.prepare(TEST_VPN_PKG, "com.new.vpn" /* newPackage */, TYPE_VPN_PLATFORM); + verifyPlatformVpnIsDeactivated(TEST_VPN_PKG); + verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG); + reset(mDeviceIdleInternal); + verifyDeactivatedByUser(sessionKey2, new String[] {TEST_VPN_PKG}); + } + + @Test + public void testVpnManagerEventForAlwaysOnChanged() throws Exception { + // Calling setAlwaysOnPackage() needs to hold CONTROL_VPN. + doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN); + final Vpn vpn = createVpn(PRIMARY_USER.id); + // Enable VPN always-on for PKGS[1]. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */, + null /* lockdownAllowlist */)); + verifyPowerSaveTempWhitelistApp(PKGS[1]); + reset(mDeviceIdleInternal); + verifyAlwaysOnStateChanged(new String[] {PKGS[1]}, + new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, + null /* sessionKey */, true /* alwaysOn */, false /* lockdown */)); + + // Enable VPN lockdown for PKGS[1]. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true /* lockdown */, + null /* lockdownAllowlist */)); + verifyPowerSaveTempWhitelistApp(PKGS[1]); + reset(mDeviceIdleInternal); + verifyAlwaysOnStateChanged(new String[] {PKGS[1]}, + new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, + null /* sessionKey */, true /* alwaysOn */, true /* lockdown */)); + + // Disable VPN lockdown for PKGS[1]. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */, + null /* lockdownAllowlist */)); + verifyPowerSaveTempWhitelistApp(PKGS[1]); + reset(mDeviceIdleInternal); + verifyAlwaysOnStateChanged(new String[] {PKGS[1]}, + new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, + null /* sessionKey */, true /* alwaysOn */, false /* lockdown */)); + + // Disable VPN always-on. + assertTrue(vpn.setAlwaysOnPackage(null, false /* lockdown */, + null /* lockdownAllowlist */)); + verifyPowerSaveTempWhitelistApp(PKGS[1]); + reset(mDeviceIdleInternal); + verifyAlwaysOnStateChanged(new String[] {PKGS[1]}, + new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, + null /* sessionKey */, false /* alwaysOn */, false /* lockdown */)); + + // Enable VPN always-on for PKGS[1] again. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */, + null /* lockdownAllowlist */)); + verifyPowerSaveTempWhitelistApp(PKGS[1]); + reset(mDeviceIdleInternal); + verifyAlwaysOnStateChanged(new String[] {PKGS[1]}, + new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, + null /* sessionKey */, true /* alwaysOn */, false /* lockdown */)); + + // Enable VPN always-on for PKGS[2]. + assertTrue(vpn.setAlwaysOnPackage(PKGS[2], false /* lockdown */, + null /* lockdownAllowlist */)); + verifyPowerSaveTempWhitelistApp(PKGS[2]); + reset(mDeviceIdleInternal); + // PKGS[1] is replaced with PKGS[2]. + // Pass 2 VpnProfileState objects to verifyVpnManagerEvent(), the first one is sent to + // PKGS[1] to notify PKGS[1] that the VPN always-on is disabled, the second one is sent to + // PKGS[2] to notify PKGS[2] that the VPN always-on is enabled. + verifyAlwaysOnStateChanged(new String[] {PKGS[1], PKGS[2]}, + new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, + null /* sessionKey */, false /* alwaysOn */, false /* lockdown */), + new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, + null /* sessionKey */, true /* alwaysOn */, false /* lockdown */)); + } + + @Test + public void testReconnectVpnManagerVpnWithAlwaysOnEnabled() throws Exception { + final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + vpn.startVpnProfile(TEST_VPN_PKG); + verifyPlatformVpnIsActivated(TEST_VPN_PKG); + + // Enable VPN always-on for TEST_VPN_PKG. + assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, false /* lockdown */, + null /* lockdownAllowlist */)); + + // Reset to verify next startVpnProfile. + reset(mAppOps); + + vpn.stopVpnProfile(TEST_VPN_PKG); + + // Reconnect the vpn with different package will cause exception. + assertThrows(SecurityException.class, () -> vpn.startVpnProfile(PKGS[0])); + + // Reconnect the vpn again with the vpn always on package w/o exception. + vpn.startVpnProfile(TEST_VPN_PKG); + verifyPlatformVpnIsActivated(TEST_VPN_PKG); + } + + @Test + public void testLockdown_enableDisableWhileConnected() throws Exception { + final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn( + createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */)); + + final InOrder order = inOrder(mTestDeps); + order.verify(mTestDeps, timeout(TIMEOUT_CROSSTHREAD_MS)) + .newNetworkAgent(any(), any(), any(), any(), any(), any(), + argThat(config -> config.allowBypass), any(), any()); + + // Make VPN lockdown. + assertTrue(vpnSnapShot.vpn.setAlwaysOnPackage(TEST_VPN_PKG, true /* lockdown */, + null /* lockdownAllowlist */)); + + order.verify(mTestDeps, timeout(TIMEOUT_CROSSTHREAD_MS)) + .newNetworkAgent(any(), any(), any(), any(), any(), any(), + argThat(config -> !config.allowBypass), any(), any()); + + // Disable lockdown. + assertTrue(vpnSnapShot.vpn.setAlwaysOnPackage(TEST_VPN_PKG, false /* lockdown */, + null /* lockdownAllowlist */)); + + order.verify(mTestDeps, timeout(TIMEOUT_CROSSTHREAD_MS)) + .newNetworkAgent(any(), any(), any(), any(), any(), any(), + argThat(config -> config.allowBypass), any(), any()); + } + + @Test + public void testSetPackageAuthorizationVpnService() throws Exception { + final Vpn vpn = createVpn(); + + assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_SERVICE)); + verify(mAppOps) + .setMode( + eq(AppOpsManager.OPSTR_ACTIVATE_VPN), + eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())), + eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_ALLOWED)); + } + + @Test + public void testSetPackageAuthorizationPlatformVpn() throws Exception { + final Vpn vpn = createVpn(); + + assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, TYPE_VPN_PLATFORM)); + verify(mAppOps) + .setMode( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), + eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())), + eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_ALLOWED)); + } + + @Test + public void testSetPackageAuthorizationRevokeAuthorization() throws Exception { + final Vpn vpn = createVpn(); + + assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_NONE)); + verify(mAppOps) + .setMode( + eq(AppOpsManager.OPSTR_ACTIVATE_VPN), + eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())), + eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_IGNORED)); + verify(mAppOps) + .setMode( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), + eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())), + eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_IGNORED)); + } + + private NetworkCallback triggerOnAvailableAndGetCallback() throws Exception { + return triggerOnAvailableAndGetCallback(new NetworkCapabilities.Builder().build()); + } + + private NetworkCallback triggerOnAvailableAndGetCallback( + @NonNull final NetworkCapabilities caps) throws Exception { + final ArgumentCaptor<NetworkCallback> networkCallbackCaptor = + ArgumentCaptor.forClass(NetworkCallback.class); + verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)) + .registerSystemDefaultNetworkCallback(networkCallbackCaptor.capture(), any()); + + // onAvailable() will trigger onDefaultNetworkChanged(), so NetdUtils#setInterfaceUp will be + // invoked. Set the return value of INetd#interfaceGetCfg to prevent NullPointerException. + final InterfaceConfigurationParcel config = new InterfaceConfigurationParcel(); + config.flags = new String[] {IF_STATE_DOWN}; + when(mNetd.interfaceGetCfg(anyString())).thenReturn(config); + final NetworkCallback cb = networkCallbackCaptor.getValue(); + cb.onAvailable(TEST_NETWORK); + // Trigger onCapabilitiesChanged() and onLinkPropertiesChanged() so the test can verify that + // if NetworkCapabilities and LinkProperties of underlying network will be sent/cleared or + // not. + // See verifyVpnManagerEvent(). + cb.onCapabilitiesChanged(TEST_NETWORK, caps); + cb.onLinkPropertiesChanged(TEST_NETWORK, new LinkProperties()); + return cb; + } + + private void verifyInterfaceSetCfgWithFlags(String flag) throws Exception { + // Add a timeout for waiting for interfaceSetCfg to be called. + verify(mNetd, timeout(TEST_TIMEOUT_MS)).interfaceSetCfg(argThat( + config -> Arrays.asList(config.flags).contains(flag))); + } + + private void doTestPlatformVpnWithException(IkeException exception, + String category, int errorType, int errorCode) throws Exception { + final ArgumentCaptor<IkeSessionCallback> captor = + ArgumentCaptor.forClass(IkeSessionCallback.class); + + final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + + doReturn(new NetworkCapabilities()).when(mConnectivityManager) + .getRedactedNetworkCapabilitiesForPackage(any(), anyInt(), anyString()); + doReturn(new LinkProperties()).when(mConnectivityManager) + .getRedactedLinkPropertiesForPackage(any(), anyInt(), anyString()); + + final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG); + final Set<Range<Integer>> uidRanges = rangeSet(PRIMARY_USER_RANGE); + // This is triggered by Ikev2VpnRunner constructor. + verify(mConnectivityManager, times(1)).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges)); + final NetworkCallback cb = triggerOnAvailableAndGetCallback(); + + verifyInterfaceSetCfgWithFlags(IF_STATE_UP); + + // Wait for createIkeSession() to be called before proceeding in order to ensure consistent + // state + verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS)) + .createIkeSession(any(), any(), any(), any(), captor.capture(), any()); + // This is triggered by Vpn#startOrMigrateIkeSession(). + verify(mConnectivityManager, times(2)).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges)); + reset(mIkev2SessionCreator); + // For network lost case, the process should be triggered by calling onLost(), which is the + // same process with the real case. + if (errorCode == VpnManager.ERROR_CODE_NETWORK_LOST) { + cb.onLost(TEST_NETWORK); + verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any()); + } else { + final IkeSessionCallback ikeCb = captor.getValue(); + mExecutor.execute(() -> ikeCb.onClosedWithException(exception)); + } + + verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG); + reset(mDeviceIdleInternal); + verifyVpnManagerEvent(sessionKey, category, errorType, errorCode, + // VPN NetworkAgnet does not switch to CONNECTED in the test, and the state is not + // important here. Verify that the state as it is, i.e. CONNECTING state. + new String[] {TEST_VPN_PKG}, new VpnProfileState(VpnProfileState.STATE_CONNECTING, + sessionKey, false /* alwaysOn */, false /* lockdown */)); + if (errorType == VpnManager.ERROR_CLASS_NOT_RECOVERABLE) { + verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), + eq(Collections.EMPTY_LIST)); + verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)) + .unregisterNetworkCallback(eq(cb)); + } else if (errorType == VpnManager.ERROR_CLASS_RECOVERABLE + // Vpn won't retry when there is no usable underlying network. + && errorCode != VpnManager.ERROR_CODE_NETWORK_LOST) { + int retryIndex = 0; + // First failure occurred above. + final IkeSessionCallback retryCb = verifyRetryAndGetNewIkeCb(retryIndex++); + // Trigger 2 more failures to let the retry delay increase to 5s. + mExecutor.execute(() -> retryCb.onClosedWithException(exception)); + final IkeSessionCallback retryCb2 = verifyRetryAndGetNewIkeCb(retryIndex++); + mExecutor.execute(() -> retryCb2.onClosedWithException(exception)); + final IkeSessionCallback retryCb3 = verifyRetryAndGetNewIkeCb(retryIndex++); + + // setVpnDefaultForUids may be called again but the uidRanges should not change. + verify(mConnectivityManager, atLeast(2)).setVpnDefaultForUids(eq(sessionKey), + mUidRangesCaptor.capture()); + final List<Collection<Range<Integer>>> capturedUidRanges = + mUidRangesCaptor.getAllValues(); + for (int i = 2; i < capturedUidRanges.size(); i++) { + // Assert equals no order. + assertTrue( + "uid ranges should not be modified. Expected: " + uidRanges + + ", actual: " + capturedUidRanges.get(i), + capturedUidRanges.get(i).containsAll(uidRanges) + && capturedUidRanges.get(i).size() == uidRanges.size()); + } + + // A fourth failure will cause the retry delay to be greater than 5s. + mExecutor.execute(() -> retryCb3.onClosedWithException(exception)); + verifyRetryAndGetNewIkeCb(retryIndex++); + + // The VPN network preference will be cleared when the retry delay is greater than 5s. + verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), + eq(Collections.EMPTY_LIST)); + } + } + + private IkeSessionCallback verifyRetryAndGetNewIkeCb(int retryIndex) { + final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor = + ArgumentCaptor.forClass(IkeSessionCallback.class); + + // Verify retry is scheduled + final long expectedDelayMs = mTestDeps.getNextRetryDelayMs(retryIndex); + verify(mExecutor, timeout(TEST_TIMEOUT_MS)).schedule(any(Runnable.class), + eq(expectedDelayMs), eq(TimeUnit.MILLISECONDS)); + + verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS + expectedDelayMs)) + .createIkeSession(any(), any(), any(), any(), ikeCbCaptor.capture(), any()); + + // Forget the mIkev2SessionCreator#createIkeSession call and mExecutor#schedule call + // for the next retry verification + resetIkev2SessionCreator(mIkeSessionWrapper); + + return ikeCbCaptor.getValue(); + } + + @Test + public void testStartPlatformVpnAuthenticationFailed() throws Exception { + final IkeProtocolException exception = mock(IkeProtocolException.class); + final int errorCode = IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED; + when(exception.getErrorType()).thenReturn(errorCode); + doTestPlatformVpnWithException(exception, + VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_NOT_RECOVERABLE, + errorCode); + } + + @Test + public void testStartPlatformVpnFailedWithRecoverableError() throws Exception { + final IkeProtocolException exception = mock(IkeProtocolException.class); + final int errorCode = IkeProtocolException.ERROR_TYPE_TEMPORARY_FAILURE; + when(exception.getErrorType()).thenReturn(errorCode); + doTestPlatformVpnWithException(exception, + VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, errorCode); + } + + @Test + public void testStartPlatformVpnFailedWithUnknownHostException() throws Exception { + final IkeNonProtocolException exception = mock(IkeNonProtocolException.class); + final UnknownHostException unknownHostException = new UnknownHostException(); + final int errorCode = VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST; + when(exception.getCause()).thenReturn(unknownHostException); + doTestPlatformVpnWithException(exception, + VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, + errorCode); + } + + @Test + public void testStartPlatformVpnFailedWithIkeTimeoutException() throws Exception { + final IkeNonProtocolException exception = mock(IkeNonProtocolException.class); + final IkeTimeoutException ikeTimeoutException = + new IkeTimeoutException("IkeTimeoutException"); + final int errorCode = VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT; + when(exception.getCause()).thenReturn(ikeTimeoutException); + doTestPlatformVpnWithException(exception, + VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, + errorCode); + } + + @Test + public void testStartPlatformVpnFailedWithIkeNetworkLostException() throws Exception { + final IkeNetworkLostException exception = new IkeNetworkLostException( + new Network(100)); + doTestPlatformVpnWithException(exception, + VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, + VpnManager.ERROR_CODE_NETWORK_LOST); + } + + @Test + public void testStartPlatformVpnFailedWithIOException() throws Exception { + final IkeNonProtocolException exception = mock(IkeNonProtocolException.class); + final IOException ioException = new IOException(); + final int errorCode = VpnManager.ERROR_CODE_NETWORK_IO; + when(exception.getCause()).thenReturn(ioException); + doTestPlatformVpnWithException(exception, + VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, + errorCode); + } + + @Test + public void testStartPlatformVpnIllegalArgumentExceptionInSetup() throws Exception { + when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any())) + .thenThrow(new IllegalArgumentException()); + final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), mVpnProfile); + final NetworkCallback cb = triggerOnAvailableAndGetCallback(); + + verifyInterfaceSetCfgWithFlags(IF_STATE_UP); + + // Wait for createIkeSession() to be called before proceeding in order to ensure consistent + // state + verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)).unregisterNetworkCallback(eq(cb)); + assertEquals(LegacyVpnInfo.STATE_FAILED, vpn.getLegacyVpnInfo().state); + } + + @Test + public void testVpnManagerEventWillNotBeSentToSettingsVpn() throws Exception { + startLegacyVpn(createVpn(PRIMARY_USER.id), mVpnProfile); + triggerOnAvailableAndGetCallback(); + + verifyInterfaceSetCfgWithFlags(IF_STATE_UP); + + final IkeNonProtocolException exception = mock(IkeNonProtocolException.class); + final IkeTimeoutException ikeTimeoutException = + new IkeTimeoutException("IkeTimeoutException"); + when(exception.getCause()).thenReturn(ikeTimeoutException); + + final ArgumentCaptor<IkeSessionCallback> captor = + ArgumentCaptor.forClass(IkeSessionCallback.class); + verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS)) + .createIkeSession(any(), any(), any(), any(), captor.capture(), any()); + final IkeSessionCallback ikeCb = captor.getValue(); + ikeCb.onClosedWithException(exception); + + final Context userContext = + mContext.createContextAsUser(UserHandle.of(PRIMARY_USER.id), 0 /* flags */); + verify(userContext, never()).startService(any()); + } + + private void setAndVerifyAlwaysOnPackage(Vpn vpn, int uid, boolean lockdownEnabled) { + assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, lockdownEnabled, null)); + + verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + verify(mAppOps).setMode( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), eq(uid), eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_ALLOWED)); + + verify(mSystemServices).settingsSecurePutStringForUser( + eq(Settings.Secure.ALWAYS_ON_VPN_APP), eq(TEST_VPN_PKG), eq(PRIMARY_USER.id)); + verify(mSystemServices).settingsSecurePutIntForUser( + eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN), eq(lockdownEnabled ? 1 : 0), + eq(PRIMARY_USER.id)); + verify(mSystemServices).settingsSecurePutStringForUser( + eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST), eq(""), eq(PRIMARY_USER.id)); + } + + @Test + public void testSetAndStartAlwaysOnVpn() throws Exception { + final Vpn vpn = createVpn(PRIMARY_USER.id); + setMockedUsers(PRIMARY_USER); + + // UID checks must return a different UID; otherwise it'll be treated as already prepared. + final int uid = Process.myUid() + 1; + when(mPackageManager.getPackageUidAsUser(eq(TEST_VPN_PKG), anyInt())) + .thenReturn(uid); + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + + setAndVerifyAlwaysOnPackage(vpn, uid, false); + assertTrue(vpn.startAlwaysOnVpn()); + + // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in + // a subsequent CL. + } + + private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception { + setMockedUsers(PRIMARY_USER); + vpn.startLegacyVpn(vpnProfile); + return vpn; + } + + private IkeSessionConnectionInfo createIkeConnectInfo() { + return new IkeSessionConnectionInfo(TEST_VPN_CLIENT_IP, TEST_VPN_SERVER_IP, TEST_NETWORK); + } + + private IkeSessionConnectionInfo createIkeConnectInfo_2() { + return new IkeSessionConnectionInfo( + TEST_VPN_CLIENT_IP_2, TEST_VPN_SERVER_IP_2, TEST_NETWORK_2); + } + + private IkeSessionConfiguration createIkeConfig( + IkeSessionConnectionInfo ikeConnectInfo, boolean isMobikeEnabled) { + final IkeSessionConfiguration.Builder builder = + new IkeSessionConfiguration.Builder(ikeConnectInfo); + + if (isMobikeEnabled) { + builder.addIkeExtension(EXTENSION_TYPE_MOBIKE); + } + + return builder.build(); + } + + private ChildSessionConfiguration createChildConfig() { + return new ChildSessionConfiguration.Builder( + Arrays.asList(IN_TS, IN_TS6), Arrays.asList(OUT_TS, OUT_TS6)) + .addInternalAddress(new LinkAddress(TEST_VPN_INTERNAL_IP, IP4_PREFIX_LEN)) + .addInternalAddress(new LinkAddress(TEST_VPN_INTERNAL_IP6, IP6_PREFIX_LEN)) + .addInternalDnsServer(TEST_VPN_INTERNAL_DNS) + .addInternalDnsServer(TEST_VPN_INTERNAL_DNS6) + .build(); + } + + private IpSecTransform createIpSecTransform() { + return new IpSecTransform(mContext, new IpSecConfig()); + } + + private void verifyApplyTunnelModeTransforms(int expectedTimes) throws Exception { + verify(mIpSecService, times(expectedTimes)).applyTunnelModeTransform( + eq(TEST_TUNNEL_RESOURCE_ID), eq(IpSecManager.DIRECTION_IN), + anyInt(), anyString()); + verify(mIpSecService, times(expectedTimes)).applyTunnelModeTransform( + eq(TEST_TUNNEL_RESOURCE_ID), eq(IpSecManager.DIRECTION_OUT), + anyInt(), anyString()); + } + + private Pair<IkeSessionCallback, ChildSessionCallback> verifyCreateIkeAndCaptureCbs() + throws Exception { + final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor = + ArgumentCaptor.forClass(IkeSessionCallback.class); + final ArgumentCaptor<ChildSessionCallback> childCbCaptor = + ArgumentCaptor.forClass(ChildSessionCallback.class); + + verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS)).createIkeSession( + any(), any(), any(), any(), ikeCbCaptor.capture(), childCbCaptor.capture()); + + return new Pair<>(ikeCbCaptor.getValue(), childCbCaptor.getValue()); + } + + private static class PlatformVpnSnapshot { + public final Vpn vpn; + public final NetworkCallback nwCb; + public final IkeSessionCallback ikeCb; + public final ChildSessionCallback childCb; + + PlatformVpnSnapshot(Vpn vpn, NetworkCallback nwCb, + IkeSessionCallback ikeCb, ChildSessionCallback childCb) { + this.vpn = vpn; + this.nwCb = nwCb; + this.ikeCb = ikeCb; + this.childCb = childCb; + } + } + + private PlatformVpnSnapshot verifySetupPlatformVpn(IkeSessionConfiguration ikeConfig) + throws Exception { + return verifySetupPlatformVpn(ikeConfig, true); + } + + private PlatformVpnSnapshot verifySetupPlatformVpn( + IkeSessionConfiguration ikeConfig, boolean mtuSupportsIpv6) throws Exception { + return verifySetupPlatformVpn(mVpnProfile, ikeConfig, mtuSupportsIpv6); + } + + private PlatformVpnSnapshot verifySetupPlatformVpn(VpnProfile vpnProfile, + IkeSessionConfiguration ikeConfig, boolean mtuSupportsIpv6) throws Exception { + return verifySetupPlatformVpn(vpnProfile, ikeConfig, + new NetworkCapabilities.Builder().build() /* underlying network caps */, + mtuSupportsIpv6, false /* areLongLivedTcpConnectionsExpensive */); + } + + private PlatformVpnSnapshot verifySetupPlatformVpn(VpnProfile vpnProfile, + IkeSessionConfiguration ikeConfig, + @NonNull final NetworkCapabilities underlyingNetworkCaps, + boolean mtuSupportsIpv6, + boolean areLongLivedTcpConnectionsExpensive) throws Exception { + if (!mtuSupportsIpv6) { + doReturn(IPV6_MIN_MTU - 1).when(mTestDeps).calculateVpnMtu(any(), anyInt(), anyInt(), + anyBoolean()); + } + + doReturn(mMockNetworkAgent).when(mTestDeps) + .newNetworkAgent( + any(), any(), anyString(), any(), any(), any(), any(), any(), any()); + doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork(); + + final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(vpnProfile.encode()); + + final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG); + final Set<Range<Integer>> uidRanges = Collections.singleton(PRIMARY_USER_RANGE); + verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges)); + final NetworkCallback nwCb = triggerOnAvailableAndGetCallback(underlyingNetworkCaps); + // There are 4 interactions with the executor. + // - Network available + // - LP change + // - NC change + // - schedule() calls in scheduleStartIkeSession() + // The first 3 calls are triggered from Executor.execute(). The execute() will also call to + // schedule() with 0 delay. Verify the exact interaction here so that it won't cause flakes + // in the follow-up flow. + verify(mExecutor, timeout(TEST_TIMEOUT_MS).times(4)) + .schedule(any(Runnable.class), anyLong(), any()); + reset(mExecutor); + + // Mock the setup procedure by firing callbacks + final Pair<IkeSessionCallback, ChildSessionCallback> cbPair = + verifyCreateIkeAndCaptureCbs(); + final IkeSessionCallback ikeCb = cbPair.first; + final ChildSessionCallback childCb = cbPair.second; + + ikeCb.onOpened(ikeConfig); + childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_IN); + childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_OUT); + childCb.onOpened(createChildConfig()); + + // Verification VPN setup + verifyApplyTunnelModeTransforms(1); + + ArgumentCaptor<LinkProperties> lpCaptor = ArgumentCaptor.forClass(LinkProperties.class); + ArgumentCaptor<NetworkCapabilities> ncCaptor = + ArgumentCaptor.forClass(NetworkCapabilities.class); + ArgumentCaptor<NetworkAgentConfig> nacCaptor = + ArgumentCaptor.forClass(NetworkAgentConfig.class); + verify(mTestDeps).newNetworkAgent( + any(), any(), anyString(), ncCaptor.capture(), lpCaptor.capture(), + any(), nacCaptor.capture(), any(), any()); + verify(mIkeSessionWrapper).setUnderpinnedNetwork(TEST_NETWORK); + // Check LinkProperties + final LinkProperties lp = lpCaptor.getValue(); + final List<RouteInfo> expectedRoutes = + new ArrayList<>( + Arrays.asList( + new RouteInfo( + new IpPrefix(Inet4Address.ANY, 0), + null /* gateway */, + TEST_IFACE_NAME, + RouteInfo.RTN_UNICAST))); + final List<LinkAddress> expectedAddresses = + new ArrayList<>( + Arrays.asList(new LinkAddress(TEST_VPN_INTERNAL_IP, IP4_PREFIX_LEN))); + final List<InetAddress> expectedDns = new ArrayList<>(Arrays.asList(TEST_VPN_INTERNAL_DNS)); + + if (mtuSupportsIpv6) { + expectedRoutes.add( + new RouteInfo( + new IpPrefix(Inet6Address.ANY, 0), + null /* gateway */, + TEST_IFACE_NAME, + RouteInfo.RTN_UNICAST)); + expectedAddresses.add(new LinkAddress(TEST_VPN_INTERNAL_IP6, IP6_PREFIX_LEN)); + expectedDns.add(TEST_VPN_INTERNAL_DNS6); + } else { + expectedRoutes.add( + new RouteInfo( + new IpPrefix(Inet6Address.ANY, 0), + null /* gateway */, + TEST_IFACE_NAME, + RTN_UNREACHABLE)); + } + + assertEquals(expectedRoutes, lp.getRoutes()); + assertEquals(expectedAddresses, lp.getLinkAddresses()); + assertEquals(expectedDns, lp.getDnsServers()); + + // Check NetworkCapabilities + assertEquals(Arrays.asList(TEST_NETWORK), ncCaptor.getValue().getUnderlyingNetworks()); + + // Check if allowBypass is set or not. + assertTrue(nacCaptor.getValue().isBypassableVpn()); + // Check if extra info for VPN is set. + assertTrue(nacCaptor.getValue().getLegacyExtraInfo().contains(TEST_VPN_PKG)); + final VpnTransportInfo info = (VpnTransportInfo) ncCaptor.getValue().getTransportInfo(); + assertTrue(info.isBypassable()); + assertEquals(areLongLivedTcpConnectionsExpensive, + info.areLongLivedTcpConnectionsExpensive()); + return new PlatformVpnSnapshot(vpn, nwCb, ikeCb, childCb); + } + + @Test + public void testStartPlatformVpn() throws Exception { + final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn( + createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */)); + vpnSnapShot.vpn.mVpnRunner.exitVpnRunner(); + verify(mConnectivityManager).setVpnDefaultForUids(anyString(), eq(Collections.EMPTY_LIST)); + } + + @Test + public void testMigrateIkeSession_FromIkeTunnConnParams_AutoTimerNoTimer() throws Exception { + doTestMigrateIkeSession_FromIkeTunnConnParams( + false /* isAutomaticIpVersionSelectionEnabled */, + true /* isAutomaticNattKeepaliveTimerEnabled */, + TEST_KEEPALIVE_TIMEOUT_UNSET /* keepaliveInProfile */, + ESP_IP_VERSION_AUTO /* ipVersionInProfile */, + ESP_ENCAP_TYPE_AUTO /* encapTypeInProfile */); + } + + @Test + public void testMigrateIkeSession_FromIkeTunnConnParams_AutoTimerTimerSet() throws Exception { + doTestMigrateIkeSession_FromIkeTunnConnParams( + false /* isAutomaticIpVersionSelectionEnabled */, + true /* isAutomaticNattKeepaliveTimerEnabled */, + TEST_KEEPALIVE_TIMER /* keepaliveInProfile */, + ESP_IP_VERSION_AUTO /* ipVersionInProfile */, + ESP_ENCAP_TYPE_AUTO /* encapTypeInProfile */); + } + + @Test + public void testMigrateIkeSession_FromIkeTunnConnParams_AutoIp() throws Exception { + doTestMigrateIkeSession_FromIkeTunnConnParams( + true /* isAutomaticIpVersionSelectionEnabled */, + false /* isAutomaticNattKeepaliveTimerEnabled */, + TEST_KEEPALIVE_TIMEOUT_UNSET /* keepaliveInProfile */, + ESP_IP_VERSION_AUTO /* ipVersionInProfile */, + ESP_ENCAP_TYPE_AUTO /* encapTypeInProfile */); + } + + @Test + public void testMigrateIkeSession_FromIkeTunnConnParams_AssignedIpProtocol() throws Exception { + doTestMigrateIkeSession_FromIkeTunnConnParams( + false /* isAutomaticIpVersionSelectionEnabled */, + false /* isAutomaticNattKeepaliveTimerEnabled */, + TEST_KEEPALIVE_TIMEOUT_UNSET /* keepaliveInProfile */, + ESP_IP_VERSION_IPV4 /* ipVersionInProfile */, + ESP_ENCAP_TYPE_UDP /* encapTypeInProfile */); + } + + @Test + public void testMigrateIkeSession_FromNotIkeTunnConnParams_AutoTimer() throws Exception { + doTestMigrateIkeSession_FromNotIkeTunnConnParams( + false /* isAutomaticIpVersionSelectionEnabled */, + true /* isAutomaticNattKeepaliveTimerEnabled */); + } + + @Test + public void testMigrateIkeSession_FromNotIkeTunnConnParams_AutoIp() throws Exception { + doTestMigrateIkeSession_FromNotIkeTunnConnParams( + true /* isAutomaticIpVersionSelectionEnabled */, + false /* isAutomaticNattKeepaliveTimerEnabled */); + } + + private void doTestMigrateIkeSession_FromNotIkeTunnConnParams( + boolean isAutomaticIpVersionSelectionEnabled, + boolean isAutomaticNattKeepaliveTimerEnabled) throws Exception { + final Ikev2VpnProfile ikeProfile = + new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY) + .setAuthPsk(TEST_VPN_PSK) + .setBypassable(true /* isBypassable */) + .setAutomaticNattKeepaliveTimerEnabled(isAutomaticNattKeepaliveTimerEnabled) + .setAutomaticIpVersionSelectionEnabled(isAutomaticIpVersionSelectionEnabled) + .build(); + + final int expectedKeepalive = isAutomaticNattKeepaliveTimerEnabled + ? AUTOMATIC_KEEPALIVE_DELAY_SECONDS + : DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT; + doTestMigrateIkeSession(ikeProfile.toVpnProfile(), + expectedKeepalive, + ESP_IP_VERSION_AUTO /* expectedIpVersion */, + ESP_ENCAP_TYPE_AUTO /* expectedEncapType */, + new NetworkCapabilities.Builder().build()); + } + + private Ikev2VpnProfile makeIkeV2VpnProfile( + boolean isAutomaticIpVersionSelectionEnabled, + boolean isAutomaticNattKeepaliveTimerEnabled, + int keepaliveInProfile, + int ipVersionInProfile, + int encapTypeInProfile) { + // TODO: Update helper function in IkeSessionTestUtils to support building IkeSessionParams + // with IP version and encap type when mainline-prod branch support these two APIs. + final IkeSessionParams params = getTestIkeSessionParams(true /* testIpv6 */, + new IkeFqdnIdentification(TEST_IDENTITY), keepaliveInProfile); + final IkeSessionParams ikeSessionParams = new IkeSessionParams.Builder(params) + .setIpVersion(ipVersionInProfile) + .setEncapType(encapTypeInProfile) + .build(); + + final IkeTunnelConnectionParams tunnelParams = + new IkeTunnelConnectionParams(ikeSessionParams, CHILD_PARAMS); + return new Ikev2VpnProfile.Builder(tunnelParams) + .setBypassable(true) + .setAutomaticNattKeepaliveTimerEnabled(isAutomaticNattKeepaliveTimerEnabled) + .setAutomaticIpVersionSelectionEnabled(isAutomaticIpVersionSelectionEnabled) + .build(); + } + + private void doTestMigrateIkeSession_FromIkeTunnConnParams( + boolean isAutomaticIpVersionSelectionEnabled, + boolean isAutomaticNattKeepaliveTimerEnabled, + int keepaliveInProfile, + int ipVersionInProfile, + int encapTypeInProfile) throws Exception { + doTestMigrateIkeSession_FromIkeTunnConnParams(isAutomaticIpVersionSelectionEnabled, + isAutomaticNattKeepaliveTimerEnabled, keepaliveInProfile, ipVersionInProfile, + encapTypeInProfile, new NetworkCapabilities.Builder().build()); + } + + private void doTestMigrateIkeSession_FromIkeTunnConnParams( + boolean isAutomaticIpVersionSelectionEnabled, + boolean isAutomaticNattKeepaliveTimerEnabled, + int keepaliveInProfile, + int ipVersionInProfile, + int encapTypeInProfile, + @NonNull final NetworkCapabilities nc) throws Exception { + final Ikev2VpnProfile ikeProfile = makeIkeV2VpnProfile( + isAutomaticIpVersionSelectionEnabled, + isAutomaticNattKeepaliveTimerEnabled, + keepaliveInProfile, + ipVersionInProfile, + encapTypeInProfile); + + final IkeSessionParams ikeSessionParams = + ikeProfile.getIkeTunnelConnectionParams().getIkeSessionParams(); + final int expectedKeepalive = isAutomaticNattKeepaliveTimerEnabled + ? AUTOMATIC_KEEPALIVE_DELAY_SECONDS + : ikeSessionParams.getNattKeepAliveDelaySeconds(); + final int expectedIpVersion = isAutomaticIpVersionSelectionEnabled + ? ESP_IP_VERSION_AUTO + : ikeSessionParams.getIpVersion(); + final int expectedEncapType = isAutomaticIpVersionSelectionEnabled + ? ESP_ENCAP_TYPE_AUTO + : ikeSessionParams.getEncapType(); + doTestMigrateIkeSession(ikeProfile.toVpnProfile(), expectedKeepalive, + expectedIpVersion, expectedEncapType, nc); + } + + @Test + public void doTestMigrateIkeSession_Vcn() throws Exception { + final int expectedKeepalive = 2097; // Any unlikely number will do + final NetworkCapabilities vcnNc = new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .setTransportInfo(new VcnTransportInfo(TEST_SUB_ID, expectedKeepalive)) + .build(); + final Ikev2VpnProfile ikev2VpnProfile = makeIkeV2VpnProfile( + true /* isAutomaticIpVersionSelectionEnabled */, + true /* isAutomaticNattKeepaliveTimerEnabled */, + 234 /* keepaliveInProfile */, // Should be ignored, any value will do + ESP_IP_VERSION_IPV4, // Should be ignored + ESP_ENCAP_TYPE_UDP // Should be ignored + ); + doTestMigrateIkeSession( + ikev2VpnProfile.toVpnProfile(), + expectedKeepalive, + ESP_IP_VERSION_AUTO /* expectedIpVersion */, + ESP_ENCAP_TYPE_AUTO /* expectedEncapType */, + vcnNc); + } + + private void doTestMigrateIkeSession( + @NonNull final VpnProfile profile, + final int expectedKeepalive, + final int expectedIpVersion, + final int expectedEncapType, + @NonNull final NetworkCapabilities caps) throws Exception { + final PlatformVpnSnapshot vpnSnapShot = + verifySetupPlatformVpn(profile, + createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */), + caps /* underlying network capabilities */, + false /* mtuSupportsIpv6 */, + expectedKeepalive < DEFAULT_LONG_LIVED_TCP_CONNS_EXPENSIVE_TIMEOUT_SEC); + // Simulate a new network coming up + vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2); + verify(mIkeSessionWrapper, never()).setNetwork(any(), anyInt(), anyInt(), anyInt()); + + vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2, caps); + // Verify MOBIKE is triggered + verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(TEST_NETWORK_2, + expectedIpVersion, expectedEncapType, expectedKeepalive); + + vpnSnapShot.vpn.mVpnRunner.exitVpnRunner(); + } + + @Test + public void testLinkPropertiesUpdateTriggerReevaluation() throws Exception { + final boolean hasV6 = true; + + mockCarrierConfig(TEST_SUB_ID, TelephonyManager.SIM_STATE_LOADED, TEST_KEEPALIVE_TIMER, + PREFERRED_IKE_PROTOCOL_IPV6_ESP); + final IkeSessionParams params = getTestIkeSessionParams(hasV6, + new IkeFqdnIdentification(TEST_IDENTITY), TEST_KEEPALIVE_TIMER); + final IkeTunnelConnectionParams tunnelParams = + new IkeTunnelConnectionParams(params, CHILD_PARAMS); + final Ikev2VpnProfile ikeProfile = new Ikev2VpnProfile.Builder(tunnelParams) + .setBypassable(true) + .setAutomaticNattKeepaliveTimerEnabled(false) + .setAutomaticIpVersionSelectionEnabled(true) + .build(); + final PlatformVpnSnapshot vpnSnapShot = + verifySetupPlatformVpn(ikeProfile.toVpnProfile(), + createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */), + new NetworkCapabilities.Builder().build() /* underlying network caps */, + hasV6 /* mtuSupportsIpv6 */, + false /* areLongLivedTcpConnectionsExpensive */); + reset(mExecutor); + + // Simulate a new network coming up + final LinkProperties lp = new LinkProperties(); + lp.addLinkAddress(new LinkAddress("192.0.2.2/32")); + + // Have the executor use the real delay to make sure schedule() was called only + // once for all calls. Also, arrange for execute() not to call schedule() to avoid + // messing with the checks for schedule(). + mExecutor.delayMs = TestExecutor.REAL_DELAY; + mExecutor.executeDirect = true; + vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2); + vpnSnapShot.nwCb.onCapabilitiesChanged( + TEST_NETWORK_2, new NetworkCapabilities.Builder().build()); + vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp)); + verify(mExecutor).schedule(any(Runnable.class), longThat(it -> it > 0), any()); + reset(mExecutor); + + final InOrder order = inOrder(mIkeSessionWrapper); + + // Verify the network is started + order.verify(mIkeSessionWrapper, timeout(TIMEOUT_CROSSTHREAD_MS)).setNetwork(TEST_NETWORK_2, + ESP_IP_VERSION_AUTO, ESP_ENCAP_TYPE_AUTO, TEST_KEEPALIVE_TIMER); + + // Send the same properties, check that no migration is scheduled + vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp)); + verify(mExecutor, never()).schedule(any(Runnable.class), anyLong(), any()); + + // Add v6 address, verify MOBIKE is triggered + lp.addLinkAddress(new LinkAddress("2001:db8::1/64")); + vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp)); + order.verify(mIkeSessionWrapper, timeout(TIMEOUT_CROSSTHREAD_MS)).setNetwork(TEST_NETWORK_2, + ESP_IP_VERSION_AUTO, ESP_ENCAP_TYPE_AUTO, TEST_KEEPALIVE_TIMER); + + // Add another v4 address, verify MOBIKE is triggered + final LinkProperties stacked = new LinkProperties(); + stacked.setInterfaceName("v4-" + lp.getInterfaceName()); + stacked.addLinkAddress(new LinkAddress("192.168.0.1/32")); + lp.addStackedLink(stacked); + vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp)); + order.verify(mIkeSessionWrapper, timeout(TIMEOUT_CROSSTHREAD_MS)).setNetwork(TEST_NETWORK_2, + ESP_IP_VERSION_AUTO, ESP_ENCAP_TYPE_AUTO, TEST_KEEPALIVE_TIMER); + + vpnSnapShot.vpn.mVpnRunner.exitVpnRunner(); + } + + private void mockCarrierConfig(int subId, int simStatus, int keepaliveTimer, int ikeProtocol) { + final SubscriptionInfo subscriptionInfo = mock(SubscriptionInfo.class); + doReturn(subId).when(subscriptionInfo).getSubscriptionId(); + doReturn(List.of(subscriptionInfo)).when(mSubscriptionManager) + .getActiveSubscriptionInfoList(); + + doReturn(simStatus).when(mTmPerSub).getSimApplicationState(); + doReturn(TEST_MCCMNC).when(mTmPerSub).getSimOperator(subId); + + final PersistableBundle persistableBundle = new PersistableBundle(); + persistableBundle.putInt(KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT, keepaliveTimer); + persistableBundle.putInt(KEY_PREFERRED_IKE_PROTOCOL_INT, ikeProtocol); + // For CarrierConfigManager.isConfigForIdentifiedCarrier check + persistableBundle.putBoolean(KEY_CARRIER_CONFIG_APPLIED_BOOL, true); + doReturn(persistableBundle).when(mConfigManager).getConfigForSubId(subId); + } + + private CarrierConfigManager.CarrierConfigChangeListener getCarrierConfigListener() { + final ArgumentCaptor<CarrierConfigManager.CarrierConfigChangeListener> listenerCaptor = + ArgumentCaptor.forClass(CarrierConfigManager.CarrierConfigChangeListener.class); + + verify(mConfigManager).registerCarrierConfigChangeListener(any(), listenerCaptor.capture()); + + return listenerCaptor.getValue(); + } + + @Test + public void testNattKeepaliveTimerFromCarrierConfig_noSubId() throws Exception { + doTestReadCarrierConfig(new NetworkCapabilities(), + TelephonyManager.SIM_STATE_LOADED, + PREFERRED_IKE_PROTOCOL_IPV4_UDP, + AUTOMATIC_KEEPALIVE_DELAY_SECONDS /* expectedKeepaliveTimer */, + ESP_IP_VERSION_AUTO /* expectedIpVersion */, + ESP_ENCAP_TYPE_AUTO /* expectedEncapType */, + false /* expectedReadFromCarrierConfig*/, + true /* areLongLivedTcpConnectionsExpensive */); + } + + @Test + public void testNattKeepaliveTimerFromCarrierConfig_simAbsent() throws Exception { + doTestReadCarrierConfig(new NetworkCapabilities.Builder().build(), + TelephonyManager.SIM_STATE_ABSENT, + PREFERRED_IKE_PROTOCOL_IPV4_UDP, + AUTOMATIC_KEEPALIVE_DELAY_SECONDS /* expectedKeepaliveTimer */, + ESP_IP_VERSION_AUTO /* expectedIpVersion */, + ESP_ENCAP_TYPE_AUTO /* expectedEncapType */, + false /* expectedReadFromCarrierConfig*/, + true /* areLongLivedTcpConnectionsExpensive */); + } + + @Test + public void testNattKeepaliveTimerFromCarrierConfig() throws Exception { + doTestReadCarrierConfig(createTestCellNc(), + TelephonyManager.SIM_STATE_LOADED, + PREFERRED_IKE_PROTOCOL_AUTO, + TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */, + ESP_IP_VERSION_AUTO /* expectedIpVersion */, + ESP_ENCAP_TYPE_AUTO /* expectedEncapType */, + true /* expectedReadFromCarrierConfig*/, + false /* areLongLivedTcpConnectionsExpensive */); + } + + @Test + public void testNattKeepaliveTimerFromCarrierConfig_NotCell() throws Exception { + final NetworkCapabilities nc = new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_WIFI) + .setTransportInfo(new WifiInfo.Builder().build()) + .build(); + doTestReadCarrierConfig(nc, + TelephonyManager.SIM_STATE_LOADED, + PREFERRED_IKE_PROTOCOL_IPV4_UDP, + AUTOMATIC_KEEPALIVE_DELAY_SECONDS /* expectedKeepaliveTimer */, + ESP_IP_VERSION_AUTO /* expectedIpVersion */, + ESP_ENCAP_TYPE_AUTO /* expectedEncapType */, + false /* expectedReadFromCarrierConfig*/, + true /* areLongLivedTcpConnectionsExpensive */); + } + + @Test + public void testPreferredIpProtocolFromCarrierConfig_v4UDP() throws Exception { + doTestReadCarrierConfig(createTestCellNc(), + TelephonyManager.SIM_STATE_LOADED, + PREFERRED_IKE_PROTOCOL_IPV4_UDP, + TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */, + ESP_IP_VERSION_IPV4 /* expectedIpVersion */, + ESP_ENCAP_TYPE_UDP /* expectedEncapType */, + true /* expectedReadFromCarrierConfig*/, + false /* areLongLivedTcpConnectionsExpensive */); + } + + @Test + public void testPreferredIpProtocolFromCarrierConfig_v6ESP() throws Exception { + doTestReadCarrierConfig(createTestCellNc(), + TelephonyManager.SIM_STATE_LOADED, + PREFERRED_IKE_PROTOCOL_IPV6_ESP, + TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */, + ESP_IP_VERSION_IPV6 /* expectedIpVersion */, + ESP_ENCAP_TYPE_NONE /* expectedEncapType */, + true /* expectedReadFromCarrierConfig*/, + false /* areLongLivedTcpConnectionsExpensive */); + } + + @Test + public void testPreferredIpProtocolFromCarrierConfig_v6UDP() throws Exception { + doTestReadCarrierConfig(createTestCellNc(), + TelephonyManager.SIM_STATE_LOADED, + PREFERRED_IKE_PROTOCOL_IPV6_UDP, + TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */, + ESP_IP_VERSION_IPV6 /* expectedIpVersion */, + ESP_ENCAP_TYPE_UDP /* expectedEncapType */, + true /* expectedReadFromCarrierConfig*/, + false /* areLongLivedTcpConnectionsExpensive */); + } + + private NetworkCapabilities createTestCellNc() { + return new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .setNetworkSpecifier(new TelephonyNetworkSpecifier.Builder() + .setSubscriptionId(TEST_SUB_ID) + .build()) + .build(); + } + + private void doTestReadCarrierConfig(NetworkCapabilities nc, int simState, int preferredIpProto, + int expectedKeepaliveTimer, int expectedIpVersion, int expectedEncapType, + boolean expectedReadFromCarrierConfig, + boolean areLongLivedTcpConnectionsExpensive) + throws Exception { + final Ikev2VpnProfile ikeProfile = + new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY) + .setAuthPsk(TEST_VPN_PSK) + .setBypassable(true /* isBypassable */) + .setAutomaticNattKeepaliveTimerEnabled(true) + .setAutomaticIpVersionSelectionEnabled(true) + .build(); + + final PlatformVpnSnapshot vpnSnapShot = + verifySetupPlatformVpn(ikeProfile.toVpnProfile(), + createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */), + new NetworkCapabilities.Builder().build() /* underlying network caps */, + false /* mtuSupportsIpv6 */, + true /* areLongLivedTcpConnectionsExpensive */); + + final CarrierConfigManager.CarrierConfigChangeListener listener = + getCarrierConfigListener(); + + // Simulate a new network coming up + vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2); + // Migration will not be started until receiving network capabilities change. + verify(mIkeSessionWrapper, never()).setNetwork(any(), anyInt(), anyInt(), anyInt()); + + reset(mIkeSessionWrapper); + mockCarrierConfig(TEST_SUB_ID, simState, TEST_KEEPALIVE_TIMER, preferredIpProto); + vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2, nc); + verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(TEST_NETWORK_2, + expectedIpVersion, expectedEncapType, expectedKeepaliveTimer); + if (expectedReadFromCarrierConfig) { + final ArgumentCaptor<NetworkCapabilities> ncCaptor = + ArgumentCaptor.forClass(NetworkCapabilities.class); + verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS)) + .doSendNetworkCapabilities(ncCaptor.capture()); + + final VpnTransportInfo info = + (VpnTransportInfo) ncCaptor.getValue().getTransportInfo(); + assertEquals(areLongLivedTcpConnectionsExpensive, + info.areLongLivedTcpConnectionsExpensive()); + } else { + verify(mMockNetworkAgent, never()).doSendNetworkCapabilities(any()); + } + + reset(mExecutor); + reset(mIkeSessionWrapper); + reset(mMockNetworkAgent); + + // Trigger carrier config change + listener.onCarrierConfigChanged(1 /* logicalSlotIndex */, TEST_SUB_ID, + -1 /* carrierId */, -1 /* specificCarrierId */); + verify(mIkeSessionWrapper).setNetwork(TEST_NETWORK_2, + expectedIpVersion, expectedEncapType, expectedKeepaliveTimer); + // Expect no NetworkCapabilities change. + // Call to doSendNetworkCapabilities() will not be triggered. + verify(mMockNetworkAgent, never()).doSendNetworkCapabilities(any()); + } + + @Test + public void testStartPlatformVpn_mtuDoesNotSupportIpv6() throws Exception { + final PlatformVpnSnapshot vpnSnapShot = + verifySetupPlatformVpn( + createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */), + false /* mtuSupportsIpv6 */); + vpnSnapShot.vpn.mVpnRunner.exitVpnRunner(); + } + + @Test + public void testStartPlatformVpn_underlyingNetworkNotChange() throws Exception { + final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn( + createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */)); + // Trigger update on the same network should not cause underlying network change in NC of + // the VPN network + vpnSnapShot.nwCb.onAvailable(TEST_NETWORK); + vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, + new NetworkCapabilities.Builder() + .setSubscriptionIds(Set.of(TEST_SUB_ID)) + .build()); + // Verify setNetwork() called but no underlying network update + verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(eq(TEST_NETWORK), + eq(ESP_IP_VERSION_AUTO) /* ipVersion */, + eq(ESP_ENCAP_TYPE_AUTO) /* encapType */, + eq(DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT) /* keepaliveDelay */); + verify(mMockNetworkAgent, never()) + .doSetUnderlyingNetworks(any()); + + vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2); + vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2, + new NetworkCapabilities.Builder().build()); + + // A new network should trigger both setNetwork() and a underlying network update. + verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(eq(TEST_NETWORK_2), + eq(ESP_IP_VERSION_AUTO) /* ipVersion */, + eq(ESP_ENCAP_TYPE_AUTO) /* encapType */, + eq(DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT) /* keepaliveDelay */); + verify(mMockNetworkAgent).doSetUnderlyingNetworks( + Collections.singletonList(TEST_NETWORK_2)); + + vpnSnapShot.vpn.mVpnRunner.exitVpnRunner(); + } + + @Test + public void testStartPlatformVpnMobility_mobikeEnabled() throws Exception { + final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn( + createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */)); + + // Set new MTU on a different network + final int newMtu = IPV6_MIN_MTU + 1; + doReturn(newMtu).when(mTestDeps).calculateVpnMtu(any(), anyInt(), anyInt(), anyBoolean()); + + // Mock network loss and verify a cleanup task is scheduled + vpnSnapShot.nwCb.onLost(TEST_NETWORK); + verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any()); + + // Mock new network comes up and the cleanup task is cancelled + vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2); + verify(mIkeSessionWrapper, never()).setNetwork(any(), anyInt(), anyInt(), anyInt()); + + vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2, + new NetworkCapabilities.Builder().build()); + // Verify MOBIKE is triggered + verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(eq(TEST_NETWORK_2), + eq(ESP_IP_VERSION_AUTO) /* ipVersion */, + eq(ESP_ENCAP_TYPE_AUTO) /* encapType */, + eq(DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT) /* keepaliveDelay */); + // Verify mNetworkCapabilities is updated + assertEquals( + Collections.singletonList(TEST_NETWORK_2), + vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks()); + verify(mMockNetworkAgent) + .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2)); + + // Mock the MOBIKE procedure + vpnSnapShot.ikeCb.onIkeSessionConnectionInfoChanged(createIkeConnectInfo_2()); + vpnSnapShot.childCb.onIpSecTransformsMigrated( + createIpSecTransform(), createIpSecTransform()); + + verify(mIpSecService).setNetworkForTunnelInterface( + eq(TEST_TUNNEL_RESOURCE_ID), eq(TEST_NETWORK_2), anyString()); + + // Expect 2 times: one for initial setup and one for MOBIKE + verifyApplyTunnelModeTransforms(2); + + // Verify mNetworkAgent is updated + verify(mMockNetworkAgent).doSendLinkProperties(argThat(lp -> lp.getMtu() == newMtu)); + verify(mMockNetworkAgent, never()).unregister(); + // No further doSetUnderlyingNetworks interaction. The interaction count should stay one. + verify(mMockNetworkAgent, times(1)).doSetUnderlyingNetworks(any()); + vpnSnapShot.vpn.mVpnRunner.exitVpnRunner(); + } + + @Test + public void testStartPlatformVpnMobility_mobikeEnabledMtuDoesNotSupportIpv6() throws Exception { + final PlatformVpnSnapshot vpnSnapShot = + verifySetupPlatformVpn( + createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */)); + + // Set MTU below 1280 + final int newMtu = IPV6_MIN_MTU - 1; + doReturn(newMtu).when(mTestDeps).calculateVpnMtu(any(), anyInt(), anyInt(), anyBoolean()); + + // Mock new network available & MOBIKE procedures + vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2); + vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2, + new NetworkCapabilities.Builder().build()); + // Verify mNetworkCapabilities is updated + verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS)) + .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2)); + assertEquals( + Collections.singletonList(TEST_NETWORK_2), + vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks()); + + vpnSnapShot.ikeCb.onIkeSessionConnectionInfoChanged(createIkeConnectInfo_2()); + vpnSnapShot.childCb.onIpSecTransformsMigrated( + createIpSecTransform(), createIpSecTransform()); + + // Verify removal of IPv6 addresses and routes triggers a network agent restart + final ArgumentCaptor<LinkProperties> lpCaptor = + ArgumentCaptor.forClass(LinkProperties.class); + verify(mTestDeps, times(2)) + .newNetworkAgent(any(), any(), anyString(), any(), lpCaptor.capture(), any(), any(), + any(), any()); + verify(mMockNetworkAgent).unregister(); + // mMockNetworkAgent is an old NetworkAgent, so it won't update LinkProperties after + // unregistering. + verify(mMockNetworkAgent, never()).doSendLinkProperties(any()); + + final LinkProperties lp = lpCaptor.getValue(); + + for (LinkAddress addr : lp.getLinkAddresses()) { + if (addr.isIpv6()) { + fail("IPv6 address found on VPN with MTU < IPv6 minimum MTU"); + } + } + + for (InetAddress dnsAddr : lp.getDnsServers()) { + if (dnsAddr instanceof Inet6Address) { + fail("IPv6 DNS server found on VPN with MTU < IPv6 minimum MTU"); + } + } + + for (RouteInfo routeInfo : lp.getRoutes()) { + if (routeInfo.getDestinationLinkAddress().isIpv6() + && !routeInfo.isIPv6UnreachableDefault()) { + fail("IPv6 route found on VPN with MTU < IPv6 minimum MTU"); + } + } + + assertEquals(newMtu, lp.getMtu()); + + vpnSnapShot.vpn.mVpnRunner.exitVpnRunner(); + } + + @Test + public void testStartPlatformVpnReestablishes_mobikeDisabled() throws Exception { + final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn( + createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */)); + + // Forget the first IKE creation to be prepared to capture callbacks of the second + // IKE session + resetIkev2SessionCreator(mock(Vpn.IkeSessionWrapper.class)); + + // Mock network switch + vpnSnapShot.nwCb.onLost(TEST_NETWORK); + vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2); + // The old IKE Session will not be killed until receiving network capabilities change. + verify(mIkeSessionWrapper, never()).kill(); + + vpnSnapShot.nwCb.onCapabilitiesChanged( + TEST_NETWORK_2, new NetworkCapabilities.Builder().build()); + // Verify the old IKE Session is killed + verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).kill(); + + // Capture callbacks of the new IKE Session + final Pair<IkeSessionCallback, ChildSessionCallback> cbPair = + verifyCreateIkeAndCaptureCbs(); + final IkeSessionCallback ikeCb = cbPair.first; + final ChildSessionCallback childCb = cbPair.second; + + // Mock the IKE Session setup + ikeCb.onOpened(createIkeConfig(createIkeConnectInfo_2(), false /* isMobikeEnabled */)); + + childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_IN); + childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_OUT); + childCb.onOpened(createChildConfig()); + + // Expect 2 times since there have been two Session setups + verifyApplyTunnelModeTransforms(2); + + // Verify mNetworkCapabilities and mNetworkAgent are updated + assertEquals( + Collections.singletonList(TEST_NETWORK_2), + vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks()); + verify(mMockNetworkAgent) + .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2)); + + vpnSnapShot.vpn.mVpnRunner.exitVpnRunner(); + } + + private String getDump(@NonNull final Vpn vpn) { + final StringWriter sw = new StringWriter(); + final IndentingPrintWriter writer = new IndentingPrintWriter(sw, ""); + vpn.dump(writer); + writer.flush(); + return sw.toString(); + } + + private int countMatches(@NonNull final Pattern regexp, @NonNull final String string) { + final Matcher m = regexp.matcher(string); + int i = 0; + while (m.find()) ++i; + return i; + } + + @Test + public void testNCEventChanges() throws Exception { + final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NET_CAPABILITY_NOT_RESTRICTED) + .setLinkDownstreamBandwidthKbps(1000) + .setLinkUpstreamBandwidthKbps(500); + + final Ikev2VpnProfile ikeProfile = + new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY) + .setAuthPsk(TEST_VPN_PSK) + .setBypassable(true /* isBypassable */) + .setAutomaticNattKeepaliveTimerEnabled(true) + .setAutomaticIpVersionSelectionEnabled(true) + .build(); + + final PlatformVpnSnapshot vpnSnapShot = + verifySetupPlatformVpn(ikeProfile.toVpnProfile(), + createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */), + ncBuilder.build(), false /* mtuSupportsIpv6 */, + true /* areLongLivedTcpConnectionsExpensive */); + + // Calls to onCapabilitiesChanged will be thrown to the executor for execution ; by + // default this will incur a 10ms delay before it's executed, messing with the timing + // of the log and having the checks for counts in equals() below flake. + mExecutor.executeDirect = true; + + // First nc changed triggered by verifySetupPlatformVpn + final Pattern pattern = Pattern.compile("Cap changed from", Pattern.MULTILINE); + final String stage1 = getDump(vpnSnapShot.vpn); + assertEquals(1, countMatches(pattern, stage1)); + + vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build()); + final String stage2 = getDump(vpnSnapShot.vpn); + // Was the same caps, there should still be only 1 match + assertEquals(1, countMatches(pattern, stage2)); + + ncBuilder.setLinkDownstreamBandwidthKbps(1200) + .setLinkUpstreamBandwidthKbps(300); + vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build()); + final String stage3 = getDump(vpnSnapShot.vpn); + // Was not an important change, should not be logged, still only 1 match + assertEquals(1, countMatches(pattern, stage3)); + + ncBuilder.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED); + vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build()); + final String stage4 = getDump(vpnSnapShot.vpn); + // Change to caps is important, should cause a new match + assertEquals(2, countMatches(pattern, stage4)); + + ncBuilder.removeCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED); + ncBuilder.setLinkDownstreamBandwidthKbps(600); + vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build()); + final String stage5 = getDump(vpnSnapShot.vpn); + // Change to caps is important, should cause a new match even with the unimportant change + assertEquals(3, countMatches(pattern, stage5)); + } + // TODO : beef up event logs tests + + private void verifyHandlingNetworkLoss(PlatformVpnSnapshot vpnSnapShot) throws Exception { + // Forget the #sendLinkProperties during first setup. + reset(mMockNetworkAgent); + + // Mock network loss + vpnSnapShot.nwCb.onLost(TEST_NETWORK); + + // Mock the grace period expires + verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any()); + + final ArgumentCaptor<LinkProperties> lpCaptor = + ArgumentCaptor.forClass(LinkProperties.class); + verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS)) + .doSendLinkProperties(lpCaptor.capture()); + final LinkProperties lp = lpCaptor.getValue(); + + assertNull(lp.getInterfaceName()); + final List<RouteInfo> expectedRoutes = Arrays.asList( + new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null /* gateway */, + null /* iface */, RTN_UNREACHABLE), + new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null /* gateway */, + null /* iface */, RTN_UNREACHABLE)); + assertEquals(expectedRoutes, lp.getRoutes()); + + verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS)).unregister(); + } + + @Test + public void testStartPlatformVpnHandlesNetworkLoss_mobikeEnabled() throws Exception { + final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn( + createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */)); + verifyHandlingNetworkLoss(vpnSnapShot); + } + + @Test + public void testStartPlatformVpnHandlesNetworkLoss_mobikeDisabled() throws Exception { + final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn( + createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */)); + verifyHandlingNetworkLoss(vpnSnapShot); + } + + private ConnectivityDiagnosticsCallback getConnectivityDiagCallback() { + final ArgumentCaptor<ConnectivityDiagnosticsCallback> cdcCaptor = + ArgumentCaptor.forClass(ConnectivityDiagnosticsCallback.class); + verify(mCdm).registerConnectivityDiagnosticsCallback( + any(), any(), cdcCaptor.capture()); + return cdcCaptor.getValue(); + } + + private DataStallReport createDataStallReport() { + return new DataStallReport(TEST_NETWORK, 1234 /* reportTimestamp */, + 1 /* detectionMethod */, new LinkProperties(), new NetworkCapabilities(), + new PersistableBundle()); + } + + private void verifyMobikeTriggered(List<Network> expected, int retryIndex) { + // Verify retry is scheduled + final long expectedDelayMs = mTestDeps.getValidationFailRecoveryMs(retryIndex); + final ArgumentCaptor<Long> delayCaptor = ArgumentCaptor.forClass(Long.class); + verify(mExecutor, times(retryIndex + 1)).schedule( + any(Runnable.class), delayCaptor.capture(), eq(TimeUnit.MILLISECONDS)); + final List<Long> delays = delayCaptor.getAllValues(); + assertEquals(expectedDelayMs, (long) delays.get(delays.size() - 1)); + + final ArgumentCaptor<Network> networkCaptor = ArgumentCaptor.forClass(Network.class); + verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS + expectedDelayMs)) + .setNetwork(networkCaptor.capture(), anyInt() /* ipVersion */, + anyInt() /* encapType */, anyInt() /* keepaliveDelay */); + assertEquals(expected, Collections.singletonList(networkCaptor.getValue())); + } + + @Test + public void testDataStallInIkev2VpnMobikeDisabled() throws Exception { + final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn( + createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */)); + + doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork(); + ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus( + NetworkAgent.VALIDATION_STATUS_NOT_VALID); + + // Should not trigger MOBIKE if MOBIKE is not enabled + verify(mIkeSessionWrapper, never()).setNetwork(any() /* network */, + anyInt() /* ipVersion */, anyInt() /* encapType */, anyInt() /* keepaliveDelay */); + } + + @Test + public void testDataStallInIkev2VpnRecoveredByMobike() throws Exception { + final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn( + createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */)); + + doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork(); + ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus( + NetworkAgent.VALIDATION_STATUS_NOT_VALID); + // Verify MOBIKE is triggered + verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(), + 0 /* retryIndex */); + // Validation failure on VPN network should trigger a re-evaluation request for the + // underlying network. + verify(mConnectivityManager).reportNetworkConnectivity(TEST_NETWORK, false); + + reset(mIkev2SessionCreator); + reset(mExecutor); + + // Send validation status update. + // Recovered and get network validated. It should not trigger the ike session reset. + ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus( + NetworkAgent.VALIDATION_STATUS_VALID); + // Verify that the retry count is reset. The mValidationFailRetryCount will not be reset + // until the executor finishes the execute() call, so wait until the all tasks are executed. + waitForIdleSerialExecutor(mExecutor, TEST_TIMEOUT_MS); + assertEquals(0, + ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).mValidationFailRetryCount); + verify(mIkev2SessionCreator, never()).createIkeSession( + any(), any(), any(), any(), any(), any()); + + reset(mIkeSessionWrapper); + reset(mExecutor); + + // Another validation fail should trigger another reportNetworkConnectivity + ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus( + NetworkAgent.VALIDATION_STATUS_NOT_VALID); + verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(), + 0 /* retryIndex */); + verify(mConnectivityManager, times(2)).reportNetworkConnectivity(TEST_NETWORK, false); + } + + @Test + public void testDataStallInIkev2VpnNotRecoveredByMobike() throws Exception { + final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn( + createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */)); + + int retry = 0; + doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork(); + ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus( + NetworkAgent.VALIDATION_STATUS_NOT_VALID); + verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(), + retry++); + // Validation failure on VPN network should trigger a re-evaluation request for the + // underlying network. + verify(mConnectivityManager).reportNetworkConnectivity(TEST_NETWORK, false); + reset(mIkev2SessionCreator); + + // Second validation status update. + ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus( + NetworkAgent.VALIDATION_STATUS_NOT_VALID); + verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(), + retry++); + // Call to reportNetworkConnectivity should only happen once. No further interaction. + verify(mConnectivityManager, times(1)).reportNetworkConnectivity(TEST_NETWORK, false); + + // Use real delay to verify reset session will not be performed if there is an existing + // recovery for resetting the session. + mExecutor.delayMs = TestExecutor.REAL_DELAY; + mExecutor.executeDirect = true; + // Send validation status update should result in ike session reset. + ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus( + NetworkAgent.VALIDATION_STATUS_NOT_VALID); + + // Verify session reset is scheduled + long expectedDelay = mTestDeps.getValidationFailRecoveryMs(retry++); + final ArgumentCaptor<Long> delayCaptor = ArgumentCaptor.forClass(Long.class); + verify(mExecutor, times(retry)).schedule(any(Runnable.class), delayCaptor.capture(), + eq(TimeUnit.MILLISECONDS)); + final List<Long> delays = delayCaptor.getAllValues(); + assertEquals(expectedDelay, (long) delays.get(delays.size() - 1)); + // Call to reportNetworkConnectivity should only happen once. No further interaction. + verify(mConnectivityManager, times(1)).reportNetworkConnectivity(TEST_NETWORK, false); + + // Another invalid status reported should not trigger other scheduled recovery. + expectedDelay = mTestDeps.getValidationFailRecoveryMs(retry++); + ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus( + NetworkAgent.VALIDATION_STATUS_NOT_VALID); + verify(mExecutor, never()).schedule( + any(Runnable.class), eq(expectedDelay), eq(TimeUnit.MILLISECONDS)); + + // Verify that session being reset + verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS + expectedDelay)) + .createIkeSession(any(), any(), any(), any(), any(), any()); + // Call to reportNetworkConnectivity should only happen once. No further interaction. + verify(mConnectivityManager, times(1)).reportNetworkConnectivity(TEST_NETWORK, false); + } + + @Test + public void testStartLegacyVpnType() throws Exception { + setMockedUsers(PRIMARY_USER); + final Vpn vpn = createVpn(PRIMARY_USER.id); + final VpnProfile profile = new VpnProfile("testProfile" /* key */); + + profile.type = VpnProfile.TYPE_PPTP; + assertThrows(UnsupportedOperationException.class, () -> startLegacyVpn(vpn, profile)); + profile.type = VpnProfile.TYPE_L2TP_IPSEC_PSK; + assertThrows(UnsupportedOperationException.class, () -> startLegacyVpn(vpn, profile)); + } + + @Test + public void testStartLegacyVpnModifyProfile_TypePSK() throws Exception { + setMockedUsers(PRIMARY_USER); + final Vpn vpn = createVpn(PRIMARY_USER.id); + final Ikev2VpnProfile ikev2VpnProfile = + new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY) + .setAuthPsk(TEST_VPN_PSK) + .build(); + final VpnProfile profile = ikev2VpnProfile.toVpnProfile(); + + startLegacyVpn(vpn, profile); + assertEquals(profile, ikev2VpnProfile.toVpnProfile()); + } + + private void assertTransportInfoMatches(NetworkCapabilities nc, int type) { + assertNotNull(nc); + VpnTransportInfo ti = (VpnTransportInfo) nc.getTransportInfo(); + assertNotNull(ti); + assertEquals(type, ti.getType()); + } + + // Make it public and un-final so as to spy it + public class TestDeps extends Vpn.Dependencies { + TestDeps() {} + + @Override + public boolean isCallerSystem() { + return true; + } + + @Override + public PendingIntent getIntentForStatusPanel(Context context) { + return null; + } + + @Override + public ParcelFileDescriptor adoptFd(Vpn vpn, int mtu) { + return new ParcelFileDescriptor(new FileDescriptor()); + } + + @Override + public int jniCreate(Vpn vpn, int mtu) { + // Pick a random positive number as fd to return. + return 345; + } + + @Override + public String jniGetName(Vpn vpn, int fd) { + return TEST_IFACE_NAME; + } + + @Override + public int jniSetAddresses(Vpn vpn, String interfaze, String addresses) { + if (addresses == null) return 0; + // Return the number of addresses. + return addresses.split(" ").length; + } + + @Override + public void setBlocking(FileDescriptor fd, boolean blocking) {} + + @Override + public DeviceIdleInternal getDeviceIdleInternal() { + return mDeviceIdleInternal; + } + + @Override + public long getValidationFailRecoveryMs(int retryCount) { + // Simply return retryCount as the delay seconds for retrying. + return retryCount * 100L; + } + + @Override + public ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor() { + return mExecutor; + } + + public boolean mIgnoreCallingUidChecks = true; + @Override + public void verifyCallingUidAndPackage(Context context, String packageName, int userId) { + if (!mIgnoreCallingUidChecks) { + super.verifyCallingUidAndPackage(context, packageName, userId); + } + } + } + + /** + * Mock some methods of vpn object. + */ + private Vpn createVpn(@UserIdInt int userId) { + final Context asUserContext = mock(Context.class, AdditionalAnswers.delegatesTo(mContext)); + doReturn(UserHandle.of(userId)).when(asUserContext).getUser(); + when(mContext.createContextAsUser(eq(UserHandle.of(userId)), anyInt())) + .thenReturn(asUserContext); + final TestLooper testLooper = new TestLooper(); + final Vpn vpn = new Vpn(testLooper.getLooper(), mContext, mTestDeps, mNetService, + mNetd, userId, mVpnProfileStore, mSystemServices, mIkev2SessionCreator); + verify(mConnectivityManager, times(1)).registerNetworkProvider(argThat( + provider -> provider.getName().contains("VpnNetworkProvider") + )); + return vpn; + } + + /** + * Populate {@link #mUserManager} with a list of fake users. + */ + private void setMockedUsers(UserInfo... users) { + final Map<Integer, UserInfo> userMap = new ArrayMap<>(); + for (UserInfo user : users) { + userMap.put(user.id, user); + } + + /** + * @see UserManagerService#getUsers(boolean) + */ + doAnswer(invocation -> { + final ArrayList<UserInfo> result = new ArrayList<>(users.length); + for (UserInfo ui : users) { + if (ui.isEnabled() && !ui.partial) { + result.add(ui); + } + } + return result; + }).when(mUserManager).getAliveUsers(); + + doAnswer(invocation -> { + final int id = (int) invocation.getArguments()[0]; + return userMap.get(id); + }).when(mUserManager).getUserInfo(anyInt()); + } + + /** + * Populate {@link #mPackageManager} with a fake packageName-to-UID mapping. + */ + private void setMockedPackages(final Map<String, Integer> packages) { + try { + doAnswer(invocation -> { + final String appName = (String) invocation.getArguments()[0]; + final int userId = (int) invocation.getArguments()[1]; + Integer appId = packages.get(appName); + if (appId == null) throw new PackageManager.NameNotFoundException(appName); + return UserHandle.getUid(userId, appId); + }).when(mPackageManager).getPackageUidAsUser(anyString(), anyInt()); + } catch (Exception e) { + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java index 6986cab72f56..87b007c4fdff 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java @@ -48,7 +48,6 @@ import android.os.UserManager; import android.os.storage.IStorageManager; import android.os.storage.StorageManager; import android.provider.Settings; -import android.security.KeyStore; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; @@ -102,7 +101,6 @@ public abstract class BaseLockSettingsServiceTests { IActivityManager mActivityManager; DevicePolicyManager mDevicePolicyManager; DevicePolicyManagerInternal mDevicePolicyManagerInternal; - KeyStore mKeyStore; MockSyntheticPasswordManager mSpManager; IAuthSecret mAuthSecretService; WindowManagerInternal mMockWindowManager; @@ -165,7 +163,6 @@ public abstract class BaseLockSettingsServiceTests { new LockSettingsServiceTestable.MockInjector( mContext, mStorage, - mKeyStore, mActivityManager, setUpStorageManagerMock(), mSpManager, diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java index ee076c6bcf4b..865a4f1b48dd 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java @@ -28,7 +28,6 @@ import android.os.Parcel; import android.os.Process; import android.os.RemoteException; import android.os.storage.IStorageManager; -import android.security.KeyStore; import android.security.keystore.KeyPermanentlyInvalidatedException; import android.service.gatekeeper.IGateKeeperService; @@ -39,13 +38,13 @@ import com.android.server.locksettings.recoverablekeystore.RecoverableKeyStoreMa import com.android.server.pm.UserManagerInternal; import java.io.FileNotFoundException; +import java.security.KeyStore; public class LockSettingsServiceTestable extends LockSettingsService { public static class MockInjector extends LockSettingsService.Injector { private LockSettingsStorage mLockSettingsStorage; - private KeyStore mKeyStore; private IActivityManager mActivityManager; private IStorageManager mStorageManager; private SyntheticPasswordManager mSpManager; @@ -57,14 +56,13 @@ public class LockSettingsServiceTestable extends LockSettingsService { public boolean mIsHeadlessSystemUserMode = false; public boolean mIsMainUserPermanentAdmin = false; - public MockInjector(Context context, LockSettingsStorage storage, KeyStore keyStore, - IActivityManager activityManager, - IStorageManager storageManager, SyntheticPasswordManager spManager, - FakeGsiService gsiService, RecoverableKeyStoreManager recoverableKeyStoreManager, + public MockInjector(Context context, LockSettingsStorage storage, + IActivityManager activityManager, IStorageManager storageManager, + SyntheticPasswordManager spManager, FakeGsiService gsiService, + RecoverableKeyStoreManager recoverableKeyStoreManager, UserManagerInternal userManagerInternal, DeviceStateCache deviceStateCache) { super(context); mLockSettingsStorage = storage; - mKeyStore = keyStore; mActivityManager = activityManager; mStorageManager = storageManager; mSpManager = spManager; @@ -105,11 +103,6 @@ public class LockSettingsServiceTestable extends LockSettingsService { } @Override - public KeyStore getKeyStore() { - return mKeyStore; - } - - @Override public IStorageManager getStorageManager() { return mStorageManager; } @@ -140,8 +133,7 @@ public class LockSettingsServiceTestable extends LockSettingsService { } @Override - public UnifiedProfilePasswordCache getUnifiedProfilePasswordCache( - java.security.KeyStore ks) { + public UnifiedProfilePasswordCache getUnifiedProfilePasswordCache(KeyStore ks) { return mock(UnifiedProfilePasswordCache.class); } |