diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 6abe5c66c77ab..eb991bd09f20a 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -193,11 +193,19 @@ public class Vpn { private static final long VPN_LAUNCH_IDLE_ALLOWLIST_DURATION_MS = 60 * 1000; // Length of time (in milliseconds) that an app registered for VpnManager events is placed on - // the device idle allowlist each time the a VpnManager event is fired. + // the device idle allowlist each time the VpnManager event is fired. private static final long VPN_MANAGER_EVENT_ALLOWLIST_DURATION_MS = 30 * 1000; private static final String LOCKDOWN_ALLOWLIST_SETTING_NAME = Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST; + + /** + * The retries for consecutive failures. + * + *

If retries have exceeded the length of this array, the last entry in the array will be + * used as a repeating interval. + */ + private static final long[] IKEV2_VPN_RETRY_DELAYS_SEC = {1L, 2L, 5L, 30L, 60L, 300L, 900L}; /** * Largest profile size allowable for Platform VPNs. * @@ -476,6 +484,20 @@ public class Vpn { "Cannot set tunnel's fd as blocking=" + blocking, e); } } + + /** + * Retrieves the next retry delay + * + *

If retries have exceeded the IKEV2_VPN_RETRY_DELAYS_SEC, the last entry in + * the array will be used as a repeating interval. + */ + public long getNextRetryDelaySeconds(int retryCount) { + if (retryCount >= IKEV2_VPN_RETRY_DELAYS_SEC.length) { + return IKEV2_VPN_RETRY_DELAYS_SEC[IKEV2_VPN_RETRY_DELAYS_SEC.length - 1]; + } else { + return IKEV2_VPN_RETRY_DELAYS_SEC[retryCount]; + } + } } public Vpn(Looper looper, Context context, INetworkManagementService netService, INetd netd, @@ -2667,6 +2689,7 @@ public class Vpn { private final ScheduledThreadPoolExecutor mExecutor = new ScheduledThreadPoolExecutor(1); @Nullable private ScheduledFuture mScheduledHandleNetworkLostTimeout; + @Nullable private ScheduledFuture mScheduledHandleRetryIkeSessionTimeout; /** Signal to ensure shutdown is honored even if a new Network is connected. */ private boolean mIsRunning = true; @@ -2690,6 +2713,14 @@ public class Vpn { // mMobikeEnabled can only be updated after IKE AUTH is finished. private boolean mMobikeEnabled = false; + /** + * The number of attempts since the last successful connection. + * + *

This variable controls the retry delay, and is reset when a new IKE session is + * opened or when there is a new default network. + */ + private int mRetryCount = 0; + IkeV2VpnRunner(@NonNull Ikev2VpnProfile profile) { super(TAG); mProfile = profile; @@ -2766,6 +2797,7 @@ public class Vpn { ikeConfiguration.isIkeExtensionEnabled( IkeSessionConfiguration.EXTENSION_TYPE_MOBIKE); onIkeConnectionInfoChanged(token, ikeConfiguration.getIkeSessionConnectionInfo()); + mRetryCount = 0; } /** @@ -2962,16 +2994,42 @@ public class Vpn { public void onDefaultNetworkChanged(@NonNull Network network) { Log.d(TAG, "onDefaultNetworkChanged: " + network); + // If there is a new default network brought up, cancel the retry task to prevent + // establishing an unnecessary IKE session. + cancelRetryNewIkeSessionFuture(); + + // If there is a new default network brought up, cancel the obsolete reset and retry + // task. cancelHandleNetworkLostTimeout(); + if (!mIsRunning) { + Log.d(TAG, "onDefaultNetworkChanged after exit"); + return; // VPN has been shut down. + } + + mActiveNetwork = network; + mRetryCount = 0; + + startOrMigrateIkeSession(network); + } + + /** + * Start a new IKE session. + * + *

This method MUST always be called on the mExecutor thread in order to ensure + * consistency of the Ikev2VpnRunner fields. + * + * @param underlyingNetwork if the value is {@code null}, which means there is no active + * network can be used, do nothing and return immediately. Otherwise, use the + * given network to start a new IKE session. + */ + private void startOrMigrateIkeSession(@Nullable Network underlyingNetwork) { + if (underlyingNetwork == null) { + Log.d(TAG, "There is no active network for starting an IKE session"); + return; + } + try { - if (!mIsRunning) { - Log.d(TAG, "onDefaultNetworkChanged after exit"); - return; // VPN has been shut down. - } - - mActiveNetwork = network; - if (mSession != null && mMobikeEnabled) { // IKE session can schedule a migration event only when IKE AUTH is finished // and mMobikeEnabled is true. @@ -2980,11 +3038,13 @@ public class Vpn { "Migrate IKE Session with token " + mCurrentToken + " to network " - + network); - mSession.setNetwork(network); + + underlyingNetwork); + mSession.setNetwork(underlyingNetwork); return; } + Log.d(TAG, "Start new IKE session on network " + underlyingNetwork); + // Clear mInterface to prevent Ikev2VpnRunner being cleared when // interfaceRemoved() is called. mInterface = null; @@ -3000,12 +3060,12 @@ public class Vpn { final ChildSessionParams childSessionParams; if (ikeTunConnParams != null) { final IkeSessionParams.Builder builder = new IkeSessionParams.Builder( - ikeTunConnParams.getIkeSessionParams()).setNetwork(network); + ikeTunConnParams.getIkeSessionParams()).setNetwork(underlyingNetwork); ikeSessionParams = builder.build(); childSessionParams = ikeTunConnParams.getTunnelModeChildSessionParams(); } else { ikeSessionParams = VpnIkev2Utils.buildIkeSessionParams( - mContext, mProfile, network); + mContext, mProfile, underlyingNetwork); childSessionParams = VpnIkev2Utils.buildChildSessionParams( mProfile.getAllowedAlgorithms()); } @@ -3020,7 +3080,7 @@ public class Vpn { // called. Thus it is safe to build a mTunnelIface before IKE setup. mTunnelIface = mIpSecManager.createIpSecTunnelInterface( - address /* unused */, address /* unused */, network); + address /* unused */, address /* unused */, underlyingNetwork); NetdUtils.setInterfaceUp(mNetd, mTunnelIface.getInterfaceName()); final int token = ++mCurrentToken; @@ -3041,6 +3101,22 @@ public class Vpn { } } + private void scheduleRetryNewIkeSession() { + final long retryDelay = mDeps.getNextRetryDelaySeconds(mRetryCount++); + Log.d(TAG, "Retry new IKE session after " + retryDelay + " seconds."); + // If the default network is lost during the retry delay, the mActiveNetwork will be + // null, and the new IKE session won't be established until there is a new default + // network bringing up. + mScheduledHandleRetryIkeSessionTimeout = + mExecutor.schedule(() -> { + startOrMigrateIkeSession(mActiveNetwork); + + // Reset mScheduledHandleRetryIkeSessionTimeout since it's already run on + // executor thread. + mScheduledHandleRetryIkeSessionTimeout = null; + }, retryDelay, TimeUnit.SECONDS); + } + /** Called when the NetworkCapabilities of underlying network is changed */ public void onDefaultNetworkCapabilitiesChanged(@NonNull NetworkCapabilities nc) { mUnderlyingNetworkCapabilities = nc; @@ -3062,6 +3138,11 @@ public class Vpn { * consistency of the Ikev2VpnRunner fields. */ public void onDefaultNetworkLost(@NonNull Network network) { + // If the default network is torn down, there is no need to call + // startOrMigrateIkeSession() since it will always check if there is an active network + // can be used or not. + cancelRetryNewIkeSessionFuture(); + if (!isActiveNetwork(network)) { Log.d(TAG, "onDefaultNetworkLost called for obsolete network " + network); @@ -3071,6 +3152,8 @@ public class Vpn { // or an error was encountered somewhere else). In both cases, all resources and // sessions are torn down via resetIkeState(). return; + } else { + mActiveNetwork = null; } if (mScheduledHandleNetworkLostTimeout != null @@ -3083,7 +3166,7 @@ public class Vpn { TAG, "Unexpected error in onDefaultNetworkLost. Tear down session", exception); - handleSessionLost(exception); + handleSessionLost(exception, network); return; } @@ -3092,7 +3175,7 @@ public class Vpn { TAG, "IKE Session has mobility. Delay handleSessionLost for losing network " + network - + "on session with token " + + " on session with token " + mCurrentToken); // Delay the teardown in case a new network will be available soon. For example, @@ -3101,13 +3184,13 @@ public class Vpn { mScheduledHandleNetworkLostTimeout = mExecutor.schedule( () -> { - handleSessionLost(null); + handleSessionLost(null, network); }, NETWORK_LOST_TIMEOUT_MS, TimeUnit.MILLISECONDS); } else { Log.d(TAG, "Call handleSessionLost for losing network " + network); - handleSessionLost(null); + handleSessionLost(null, network); } } @@ -3120,6 +3203,20 @@ public class Vpn { // mExecutor who has only one thread. Log.d(TAG, "Cancel the task for handling network lost timeout"); mScheduledHandleNetworkLostTimeout.cancel(false /* mayInterruptIfRunning */); + mScheduledHandleNetworkLostTimeout = null; + } + } + + private void cancelRetryNewIkeSessionFuture() { + if (mScheduledHandleRetryIkeSessionTimeout != null + && !mScheduledHandleRetryIkeSessionTimeout.isDone()) { + // It does not matter what to put in #cancel(boolean), because it is impossible + // that the task tracked by mScheduledHandleRetryIkeSessionTimeout is + // in-progress since both that task and onDefaultNetworkChanged are submitted to + // mExecutor who has only one thread. + Log.d(TAG, "Cancel the task for handling new ike session timeout"); + mScheduledHandleRetryIkeSessionTimeout.cancel(false /* mayInterruptIfRunning */); + mScheduledHandleRetryIkeSessionTimeout = null; } } @@ -3155,10 +3252,10 @@ public class Vpn { return; } - handleSessionLost(exception); + handleSessionLost(exception, mActiveNetwork); } - private void handleSessionLost(@Nullable Exception exception) { + private void handleSessionLost(@Nullable Exception exception, @Nullable Network network) { // Cancel mScheduledHandleNetworkLostTimeout if the session it is going to terminate is // already terminated due to other failures. cancelHandleNetworkLostTimeout(); @@ -3182,7 +3279,7 @@ public class Vpn { VpnManager.ERROR_CLASS_NOT_RECOVERABLE, ikeException.getErrorType(), getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3200,7 +3297,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, ikeException.getErrorType(), getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3219,7 +3316,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, VpnManager.ERROR_CODE_NETWORK_LOST, getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3234,7 +3331,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST, getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3248,7 +3345,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT, getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3262,7 +3359,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, VpnManager.ERROR_CODE_NETWORK_IO, getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3272,9 +3369,10 @@ public class Vpn { } else if (exception != null) { Log.wtf(TAG, "onSessionLost: exception = " + exception); } + + scheduleRetryNewIkeSession(); } - mActiveNetwork = null; mUnderlyingNetworkCapabilities = null; mUnderlyingLinkProperties = null;