From b4e39c87c131b34562d7e0e96eac4ea46e75c793 Mon Sep 17 00:00:00 2001 From: lucaslin Date: Thu, 9 Jun 2022 18:24:21 +0800 Subject: [PATCH] Add a retry mechanism when error is recoverable Have a retry delay arrays for getting the next retry delay, and the max retry delay is 900 seconds, when the max retry delay is reached, the retry mechanism will always use it delay the retry. Bug: 229350078 Test: atest FrameworksNetTests:VpnTest Change-Id: If0fe30f0b98b83a723e14ea8f6cd9a96c54e1691 --- .../com/android/server/connectivity/Vpn.java | 150 +++++++++++++++--- 1 file changed, 124 insertions(+), 26 deletions(-) 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;