From d9c66124b9c43c91049534b9db114009a96dda3f Mon Sep 17 00:00:00 2001 From: Yan Yan Date: Thu, 28 Apr 2022 16:17:59 -0700 Subject: Use token to identify IKE Session This commit updates VPN to use an integer token to identify an IKE Session. This is a preparation commit to enable IKE mobility where an IKE Session will be able to migrate to a different network and thus network can no longer identify an IKE Session. Bug: 192077544 Test: atest IkeV2VpnTest Test: atest com.android.server.connectivity.VpnTest Change-Id: I18d9a049587d7dfc05e2143320431014d053abbc (cherry picked from commit b93a40558cf645e4dd5fca3455e4f1de6d23edc7) Merged-In: I18d9a049587d7dfc05e2143320431014d053abbc --- .../java/com/android/server/connectivity/Vpn.java | 132 ++++++++++++++------- .../android/server/connectivity/VpnIkev2Utils.java | 47 ++++---- 2 files changed, 109 insertions(+), 70 deletions(-) diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 3551ff91f542..92a4ecf26b85 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -2603,13 +2603,13 @@ public class Vpn { void onDefaultNetworkLinkPropertiesChanged(@NonNull LinkProperties lp); - void onChildOpened( - @NonNull Network network, @NonNull ChildSessionConfiguration childConfig); + void onDefaultNetworkLost(@NonNull Network network); - void onChildTransformCreated( - @NonNull Network network, @NonNull IpSecTransform transform, int direction); + void onChildOpened(int token, @NonNull ChildSessionConfiguration childConfig); - void onSessionLost(@NonNull Network network, @Nullable Exception exception); + void onChildTransformCreated(int token, @NonNull IpSecTransform transform, int direction); + + void onSessionLost(int token, @Nullable Exception exception); } /** @@ -2656,6 +2656,13 @@ public class Vpn { /** Signal to ensure shutdown is honored even if a new Network is connected. */ private boolean mIsRunning = true; + /** + * The token used by the primary/current/active IKE session. + * + *

This token MUST be updated when the VPN switches to use a new IKE session. + */ + private int mCurrentToken = -1; + @Nullable private IpSecTunnelInterface mTunnelIface; @Nullable private IkeSession mSession; @Nullable private Network mActiveNetwork; @@ -2707,22 +2714,25 @@ public class Vpn { return Objects.equals(mActiveNetwork, network) && mIsRunning; } + private boolean isActiveToken(int token) { + return (mCurrentToken == token) && mIsRunning; + } + /** * Called when an IKE Child session has been opened, signalling completion of the startup. * *

This method is only ever called once per IkeSession, and MUST run on the mExecutor * thread in order to ensure consistency of the Ikev2VpnRunner fields. */ - public void onChildOpened( - @NonNull Network network, @NonNull ChildSessionConfiguration childConfig) { - if (!isActiveNetwork(network)) { - Log.d(TAG, "onOpened called for obsolete network " + network); + public void onChildOpened(int token, @NonNull ChildSessionConfiguration childConfig) { + if (!isActiveToken(token)) { + Log.d(TAG, "onChildOpened called for obsolete token " + token); // Do nothing; this signals that either: (1) a new/better Network was found, - // and the Ikev2VpnRunner has switched to it in onDefaultNetworkChanged, or (2) this - // IKE session was already shut down (exited, or an error was encountered somewhere - // else). In both cases, all resources and sessions are torn down via - // resetIkeState(). + // and the Ikev2VpnRunner has switched to it by restarting a new IKE session in + // onDefaultNetworkChanged, or (2) this IKE session was already shut down (exited, + // or an error was encountered somewhere else). In both cases, all resources and + // sessions are torn down via resetIkeState(). return; } @@ -2759,7 +2769,7 @@ public class Vpn { mConfig.dnsServers.clear(); mConfig.dnsServers.addAll(dnsAddrStrings); - mConfig.underlyingNetworks = new Network[] {network}; + mConfig.underlyingNetworks = new Network[] {mActiveNetwork}; mConfig.disallowedApplications = getAppExclusionList(mPackage); @@ -2775,7 +2785,8 @@ public class Vpn { return; // Link properties are already sent. } else { // Underlying networks also set in agentConnect() - networkAgent.setUnderlyingNetworks(Collections.singletonList(network)); + networkAgent.setUnderlyingNetworks( + Collections.singletonList(mActiveNetwork)); } lp = makeLinkProperties(); // Accesses VPN instance fields; must be locked @@ -2783,8 +2794,8 @@ public class Vpn { networkAgent.sendLinkProperties(lp); } catch (Exception e) { - Log.d(TAG, "Error in ChildOpened for network " + network, e); - onSessionLost(network, e); + Log.d(TAG, "Error in ChildOpened for token " + token, e); + onSessionLost(token, e); } } @@ -2796,15 +2807,15 @@ public class Vpn { * consistency of the Ikev2VpnRunner fields. */ public void onChildTransformCreated( - @NonNull Network network, @NonNull IpSecTransform transform, int direction) { - if (!isActiveNetwork(network)) { - Log.d(TAG, "ChildTransformCreated for obsolete network " + network); + int token, @NonNull IpSecTransform transform, int direction) { + if (!isActiveToken(token)) { + Log.d(TAG, "ChildTransformCreated for obsolete token " + token); // Do nothing; this signals that either: (1) a new/better Network was found, - // and the Ikev2VpnRunner has switched to it in onDefaultNetworkChanged, or (2) this - // IKE session was already shut down (exited, or an error was encountered somewhere - // else). In both cases, all resources and sessions are torn down via - // resetIkeState(). + // and the Ikev2VpnRunner has switched to it by restarting a new IKE session in + // onDefaultNetworkChanged, or (2) this IKE session was already shut down (exited, + // or an error was encountered somewhere else). In both cases, all resources and + // sessions are torn down via resetIkeState(). return; } @@ -2813,8 +2824,8 @@ public class Vpn { // them alive for us mIpSecManager.applyTunnelModeTransform(mTunnelIface, direction, transform); } catch (IOException e) { - Log.d(TAG, "Transform application failed for network " + network, e); - onSessionLost(network, e); + Log.d(TAG, "Transform application failed for token " + token, e); + onSessionLost(token, e); } } @@ -2872,19 +2883,21 @@ public class Vpn { network); NetdUtils.setInterfaceUp(mNetd, mTunnelIface.getInterfaceName()); - mSession = mIkev2SessionCreator.createIkeSession( - mContext, - ikeSessionParams, - childSessionParams, - mExecutor, - new VpnIkev2Utils.IkeSessionCallbackImpl( - TAG, IkeV2VpnRunner.this, network), - new VpnIkev2Utils.ChildSessionCallbackImpl( - TAG, IkeV2VpnRunner.this, network)); - Log.d(TAG, "Ike Session started for network " + network); + final int token = ++mCurrentToken; + mSession = + mIkev2SessionCreator.createIkeSession( + mContext, + ikeSessionParams, + childSessionParams, + mExecutor, + new VpnIkev2Utils.IkeSessionCallbackImpl( + TAG, IkeV2VpnRunner.this, token), + new VpnIkev2Utils.ChildSessionCallbackImpl( + TAG, IkeV2VpnRunner.this, token)); + Log.d(TAG, "IKE session started for token " + token); } catch (Exception e) { - Log.i(TAG, "Setup failed for network " + network + ". Aborting", e); - onSessionLost(network, e); + Log.i(TAG, "Setup failed for token " + mCurrentToken + ". Aborting", e); + onSessionLost(mCurrentToken, e); } } @@ -2898,6 +2911,29 @@ public class Vpn { mUnderlyingLinkProperties = lp; } + /** + * Handles loss of the default underlying network + * + *

The Ikev2VpnRunner will kill the IKE session and reset the VPN. + * + *

This method MUST always be called on the mExecutor thread in order to ensure + * consistency of the Ikev2VpnRunner fields. + */ + public void onDefaultNetworkLost(@NonNull Network network) { + if (!isActiveNetwork(network)) { + Log.d(TAG, "onDefaultNetworkLost called for obsolete network " + network); + + // Do nothing; this signals that either: (1) a new/better Network was found, + // and the Ikev2VpnRunner has switched to it by restarting a new IKE session in + // onDefaultNetworkChanged, or (2) this IKE session was already shut down (exited, + // or an error was encountered somewhere else). In both cases, all resources and + // sessions are torn down via resetIkeState(). + return; + } + + handleSessionLost(null); + } + /** Marks the state as FAILED, and disconnects. */ private void markFailedAndDisconnect(Exception exception) { synchronized (Vpn.this) { @@ -2916,18 +2952,22 @@ public class Vpn { *

This method MUST always be called on the mExecutor thread in order to ensure * consistency of the Ikev2VpnRunner fields. */ - public void onSessionLost(@NonNull Network network, @Nullable Exception exception) { - if (!isActiveNetwork(network)) { - Log.d(TAG, "onSessionLost() called for obsolete network " + network); + public void onSessionLost(int token, @Nullable Exception exception) { + if (!isActiveToken(token)) { + Log.d(TAG, "onSessionLost() called for obsolete token " + token); // Do nothing; this signals that either: (1) a new/better Network was found, - // and the Ikev2VpnRunner has switched to it in onDefaultNetworkChanged, or (2) this - // IKE session was already shut down (exited, or an error was encountered somewhere - // else). In both cases, all resources and sessions are torn down via - // onSessionLost() and resetIkeState(). + // and the Ikev2VpnRunner has switched to it by restarting a new IKE session in + // onDefaultNetworkChanged, or (2) this IKE session was already shut down (exited, + // or an error was encountered somewhere else). In both cases, all resources and + // sessions are torn down via resetIkeState(). return; } + handleSessionLost(exception); + } + + private void handleSessionLost(@Nullable Exception exception) { synchronized (Vpn.this) { if (exception instanceof IkeProtocolException) { final IkeProtocolException ikeException = (IkeProtocolException) exception; @@ -3045,7 +3085,7 @@ public class Vpn { // Close all obsolete state, but keep VPN alive incase a usable network comes up. // (Mirrors VpnService behavior) - Log.d(TAG, "Resetting state for network: " + network); + Log.d(TAG, "Resetting state for token: " + mCurrentToken); synchronized (Vpn.this) { // Since this method handles non-fatal errors only, set mInterface to null to diff --git a/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java b/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java index 17058282d947..4f6ed59f332e 100644 --- a/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java +++ b/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java @@ -298,35 +298,35 @@ public class VpnIkev2Utils { static class IkeSessionCallbackImpl implements IkeSessionCallback { private final String mTag; private final Vpn.IkeV2VpnRunnerCallback mCallback; - private final Network mNetwork; + private final int mToken; - IkeSessionCallbackImpl(String tag, Vpn.IkeV2VpnRunnerCallback callback, Network network) { + IkeSessionCallbackImpl(String tag, Vpn.IkeV2VpnRunnerCallback callback, int token) { mTag = tag; mCallback = callback; - mNetwork = network; + mToken = token; } @Override public void onOpened(@NonNull IkeSessionConfiguration ikeSessionConfig) { - Log.d(mTag, "IkeOpened for network " + mNetwork); + Log.d(mTag, "IkeOpened for token " + mToken); // Nothing to do here. } @Override public void onClosed() { - Log.d(mTag, "IkeClosed for network " + mNetwork); - mCallback.onSessionLost(mNetwork, null); // Server requested session closure. Retry? + Log.d(mTag, "IkeClosed for token " + mToken); + mCallback.onSessionLost(mToken, null); // Server requested session closure. Retry? } @Override public void onClosedExceptionally(@NonNull IkeException exception) { - Log.d(mTag, "IkeClosedExceptionally for network " + mNetwork, exception); - mCallback.onSessionLost(mNetwork, exception); + Log.d(mTag, "IkeClosedExceptionally for token " + mToken, exception); + mCallback.onSessionLost(mToken, exception); } @Override public void onError(@NonNull IkeProtocolException exception) { - Log.d(mTag, "IkeError for network " + mNetwork, exception); + Log.d(mTag, "IkeError for token " + mToken, exception); // Non-fatal, log and continue. } } @@ -334,36 +334,36 @@ public class VpnIkev2Utils { static class ChildSessionCallbackImpl implements ChildSessionCallback { private final String mTag; private final Vpn.IkeV2VpnRunnerCallback mCallback; - private final Network mNetwork; + private final int mToken; - ChildSessionCallbackImpl(String tag, Vpn.IkeV2VpnRunnerCallback callback, Network network) { + ChildSessionCallbackImpl(String tag, Vpn.IkeV2VpnRunnerCallback callback, int token) { mTag = tag; mCallback = callback; - mNetwork = network; + mToken = token; } @Override public void onOpened(@NonNull ChildSessionConfiguration childConfig) { - Log.d(mTag, "ChildOpened for network " + mNetwork); - mCallback.onChildOpened(mNetwork, childConfig); + Log.d(mTag, "ChildOpened for token " + mToken); + mCallback.onChildOpened(mToken, childConfig); } @Override public void onClosed() { - Log.d(mTag, "ChildClosed for network " + mNetwork); - mCallback.onSessionLost(mNetwork, null); + Log.d(mTag, "ChildClosed for token " + mToken); + mCallback.onSessionLost(mToken, null); } @Override public void onClosedExceptionally(@NonNull IkeException exception) { - Log.d(mTag, "ChildClosedExceptionally for network " + mNetwork, exception); - mCallback.onSessionLost(mNetwork, exception); + Log.d(mTag, "ChildClosedExceptionally for token " + mToken, exception); + mCallback.onSessionLost(mToken, exception); } @Override public void onIpSecTransformCreated(@NonNull IpSecTransform transform, int direction) { - Log.d(mTag, "ChildTransformCreated; Direction: " + direction + "; network " + mNetwork); - mCallback.onChildTransformCreated(mNetwork, transform, direction); + Log.d(mTag, "ChildTransformCreated; Direction: " + direction + "; token " + mToken); + mCallback.onChildTransformCreated(mToken, transform, direction); } @Override @@ -371,8 +371,7 @@ public class VpnIkev2Utils { // Nothing to be done; no references to the IpSecTransform are held by the // Ikev2VpnRunner (or this callback class), and this transform will be closed by the // IKE library. - Log.d(mTag, - "ChildTransformDeleted; Direction: " + direction + "; for network " + mNetwork); + Log.d(mTag, "ChildTransformDeleted; Direction: " + direction + "; for token " + mToken); } } @@ -412,8 +411,8 @@ public class VpnIkev2Utils { @Override public void onLost(@NonNull Network network) { - Log.d(mTag, "Tearing down; lost network: " + network); - mExecutor.execute(() -> mCallback.onSessionLost(network, null)); + Log.d(mTag, "onLost called for network: " + network); + mExecutor.execute(() -> mCallback.onDefaultNetworkLost(network)); } } -- cgit v1.2.3-59-g8ed1b From 92d5b2fb2ded645b329a6e7547f4337d210d4ea1 Mon Sep 17 00:00:00 2001 From: Yan Yan Date: Fri, 29 Apr 2022 17:27:38 -0700 Subject: Enable MOBIKE in IKEv2 VPN This commit enables MOBIKE on the VPN's IKE Session. In this way if the VPN server also supports MOBIKE, IKEv2 VPN can migrate to a different network by migrating the active IKE session, instead of setting up a new IKE Session. This commit also adds a grace period so that when the underlying network is lost, the IKEv2 VPN will wait for some time before terminating the IKE Session in case a new network will be available. Bug: 192077544 Test: atest IkeV2VpnTest (new tests added) Test: atest com.android.server.connectivity.VpnTest Change-Id: Ie6ef3103200fef3b7e5f00918fee9e791cad9445 (cherry picked from commit 1405296274dbb27414f1dcca0d15b9e8a4d3f70f) Merged-In: Ie6ef3103200fef3b7e5f00918fee9e791cad9445 --- .../java/com/android/server/connectivity/Vpn.java | 236 +++++++++++++++++++-- .../android/server/connectivity/VpnIkev2Utils.java | 21 +- 2 files changed, 238 insertions(+), 19 deletions(-) diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 92a4ecf26b85..89ed9a431273 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -86,6 +86,8 @@ import android.net.ipsec.ike.ChildSessionConfiguration; import android.net.ipsec.ike.ChildSessionParams; import android.net.ipsec.ike.IkeSession; 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.IkeTunnelConnectionParams; import android.net.ipsec.ike.exceptions.IkeNetworkLostException; @@ -168,9 +170,10 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** @@ -2605,10 +2608,20 @@ public class Vpn { void onDefaultNetworkLost(@NonNull Network network); + void onIkeOpened(int token, @NonNull IkeSessionConfiguration ikeConfiguration); + + void onIkeConnectionInfoChanged( + int token, @NonNull IkeSessionConnectionInfo ikeConnectionInfo); + void onChildOpened(int token, @NonNull ChildSessionConfiguration childConfig); void onChildTransformCreated(int token, @NonNull IpSecTransform transform, int direction); + void onChildMigrated( + int token, + @NonNull IpSecTransform inTransform, + @NonNull IpSecTransform outTransform); + void onSessionLost(int token, @Nullable Exception exception); } @@ -2640,6 +2653,10 @@ public class Vpn { class IkeV2VpnRunner extends VpnRunner implements IkeV2VpnRunnerCallback { @NonNull private static final String TAG = "IkeV2VpnRunner"; + // 5 seconds grace period before tearing down the IKE Session in case new default network + // will come up + private static final long NETWORK_LOST_TIMEOUT_MS = 5000L; + @NonNull private final IpSecManager mIpSecManager; @NonNull private final Ikev2VpnProfile mProfile; @NonNull private final ConnectivityManager.NetworkCallback mNetworkCallback; @@ -2651,7 +2668,10 @@ public class Vpn { * of the mutable Ikev2VpnRunner fields. The Ikev2VpnRunner is built mostly lock-free by * virtue of everything being serialized on this executor. */ - @NonNull private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + @NonNull + private final ScheduledThreadPoolExecutor mExecutor = new ScheduledThreadPoolExecutor(1); + + @Nullable private ScheduledFuture mScheduledHandleNetworkLostTimeout; /** Signal to ensure shutdown is honored even if a new Network is connected. */ private boolean mIsRunning = true; @@ -2664,18 +2684,35 @@ public class Vpn { private int mCurrentToken = -1; @Nullable private IpSecTunnelInterface mTunnelIface; - @Nullable private IkeSession mSession; @Nullable private Network mActiveNetwork; @Nullable private NetworkCapabilities mUnderlyingNetworkCapabilities; @Nullable private LinkProperties mUnderlyingLinkProperties; private final String mSessionKey; + @Nullable private IkeSession mSession; + @Nullable private IkeSessionConnectionInfo mIkeConnectionInfo; + + // mMobikeEnabled can only be updated after IKE AUTH is finished. + private boolean mMobikeEnabled = false; + IkeV2VpnRunner(@NonNull Ikev2VpnProfile profile) { super(TAG); mProfile = profile; mIpSecManager = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE); mNetworkCallback = new VpnIkev2Utils.Ikev2VpnNetworkCallback(TAG, this, mExecutor); mSessionKey = UUID.randomUUID().toString(); + + // Set the policy so that cancelled tasks will be removed from the work queue + mExecutor.setRemoveOnCancelPolicy(true); + + // Set the policy so that all delayed tasks will not be executed + mExecutor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + + // To avoid hitting RejectedExecutionException upon shutdown of the mExecutor */ + mExecutor.setRejectedExecutionHandler( + (r, executor) -> { + Log.d(TAG, "Runnable " + r + " rejected by the mExecutor"); + }); } @Override @@ -2718,6 +2755,44 @@ public class Vpn { return (mCurrentToken == token) && mIsRunning; } + /** + * Called when an IKE session has been opened + * + *

This method is only ever called once per IkeSession, and MUST run on the mExecutor + * thread in order to ensure consistency of the Ikev2VpnRunner fields. + */ + public void onIkeOpened(int token, @NonNull IkeSessionConfiguration ikeConfiguration) { + if (!isActiveToken(token)) { + Log.d(TAG, "onIkeOpened called for obsolete token " + token); + return; + } + + mMobikeEnabled = + ikeConfiguration.isIkeExtensionEnabled( + IkeSessionConfiguration.EXTENSION_TYPE_MOBIKE); + onIkeConnectionInfoChanged(token, ikeConfiguration.getIkeSessionConnectionInfo()); + } + + /** + * Called when an IKE session's {@link IkeSessionConnectionInfo} is available or updated + * + *

This callback is usually fired when an IKE session has been opened or migrated. + * + *

This method is called multiple times over the lifetime of an IkeSession, and MUST run + * on the mExecutor thread in order to ensure consistency of the Ikev2VpnRunner fields. + */ + public void onIkeConnectionInfoChanged( + int token, @NonNull IkeSessionConnectionInfo ikeConnectionInfo) { + if (!isActiveToken(token)) { + Log.d(TAG, "onIkeConnectionInfoChanged called for obsolete token " + token); + return; + } + + // The update on VPN and the IPsec tunnel will be done when migration is fully complete + // in onChildMigrated + mIkeConnectionInfo = ikeConnectionInfo; + } + /** * Called when an IKE Child session has been opened, signalling completion of the startup. * @@ -2751,6 +2826,11 @@ public class Vpn { dnsAddrStrings.add(addr.getHostAddress()); } + // The actual network of this IKE session has been set up with is + // mIkeConnectionInfo.getNetwork() instead of mActiveNetwork because + // mActiveNetwork might have been updated after the setup was triggered. + final Network network = mIkeConnectionInfo.getNetwork(); + final NetworkAgent networkAgent; final LinkProperties lp; @@ -2769,7 +2849,7 @@ public class Vpn { mConfig.dnsServers.clear(); mConfig.dnsServers.addAll(dnsAddrStrings); - mConfig.underlyingNetworks = new Network[] {mActiveNetwork}; + mConfig.underlyingNetworks = new Network[] {network}; mConfig.disallowedApplications = getAppExclusionList(mPackage); @@ -2785,8 +2865,7 @@ public class Vpn { return; // Link properties are already sent. } else { // Underlying networks also set in agentConnect() - networkAgent.setUnderlyingNetworks( - Collections.singletonList(mActiveNetwork)); + networkAgent.setUnderlyingNetworks(Collections.singletonList(network)); } lp = makeLinkProperties(); // Accesses VPN instance fields; must be locked @@ -2803,7 +2882,7 @@ public class Vpn { * Called when an IPsec transform has been created, and should be applied. * *

This method is called multiple times over the lifetime of an IkeSession (or default - * network), and is MUST always be called on the mExecutor thread in order to ensure + * network), and MUST always be called on the mExecutor thread in order to ensure * consistency of the Ikev2VpnRunner fields. */ public void onChildTransformCreated( @@ -2829,17 +2908,66 @@ public class Vpn { } } + /** + * Called when an IPsec transform has been created, and should be re-applied. + * + *

This method is called multiple times over the lifetime of an IkeSession (or default + * network), and MUST always be called on the mExecutor thread in order to ensure + * consistency of the Ikev2VpnRunner fields. + */ + public void onChildMigrated( + int token, + @NonNull IpSecTransform inTransform, + @NonNull IpSecTransform outTransform) { + if (!isActiveToken(token)) { + Log.d(TAG, "onChildMigrated for obsolete token " + token); + return; + } + + // The actual network of this IKE session has migrated to is + // mIkeConnectionInfo.getNetwork() instead of mActiveNetwork because mActiveNetwork + // might have been updated after the migration was triggered. + final Network network = mIkeConnectionInfo.getNetwork(); + + try { + synchronized (Vpn.this) { + mConfig.underlyingNetworks = new Network[] {network}; + mNetworkCapabilities = + new NetworkCapabilities.Builder(mNetworkCapabilities) + .setUnderlyingNetworks(Collections.singletonList(network)) + .build(); + mNetworkAgent.setUnderlyingNetworks(Collections.singletonList(network)); + } + + mTunnelIface.setUnderlyingNetwork(network); + + // Transforms do not need to be persisted; the IkeSession will keep them alive for + // us + mIpSecManager.applyTunnelModeTransform( + mTunnelIface, IpSecManager.DIRECTION_IN, inTransform); + mIpSecManager.applyTunnelModeTransform( + mTunnelIface, IpSecManager.DIRECTION_OUT, outTransform); + } catch (IOException e) { + Log.d(TAG, "Transform application failed for token " + token, e); + onSessionLost(token, e); + } + } + /** * Called when a new default network is connected. * - *

The Ikev2VpnRunner will unconditionally switch to the new network, killing the old IKE - * state in the process, and starting a new IkeSession instance. + *

The Ikev2VpnRunner will unconditionally switch to the new network. If the IKE session + * has mobility, Ikev2VpnRunner will migrate the existing IkeSession to the new network. + * Otherwise, Ikev2VpnRunner will kill the old IKE state, and start a new IkeSession + * instance. * *

This method MUST always be called on the mExecutor thread in order to ensure * consistency of the Ikev2VpnRunner fields. */ public void onDefaultNetworkChanged(@NonNull Network network) { - Log.d(TAG, "Starting IKEv2/IPsec session on new network: " + network); + Log.d(TAG, "onDefaultNetworkChanged: " + network); + + cancelHandleNetworkLostTimeout(); try { if (!mIsRunning) { @@ -2847,13 +2975,27 @@ public class Vpn { 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. + Log.d( + TAG, + "Migrate IKE Session with token " + + mCurrentToken + + " to network " + + network); + mSession.setNetwork(network); + return; + } + // Clear mInterface to prevent Ikev2VpnRunner being cleared when // interfaceRemoved() is called. mInterface = null; // Without MOBIKE, we have no way to seamlessly migrate. Close on old // (non-default) network, and start the new one. resetIkeState(); - mActiveNetwork = network; // Get Ike options from IkeTunnelConnectionParams if it's available in the // profile. @@ -2876,11 +3018,14 @@ public class Vpn { // TODO: Remove the need for adding two unused addresses with // IPsec tunnels. final InetAddress address = InetAddress.getLocalHost(); + + // When onChildOpened is called and transforms are applied, it is + // guaranteed that the underlying network is still "network", because the + // all the network switch events will be deferred before onChildOpened is + // called. Thus it is safe to build a mTunnelIface before IKE setup. mTunnelIface = mIpSecManager.createIpSecTunnelInterface( - address /* unused */, - address /* unused */, - network); + address /* unused */, address /* unused */, network); NetdUtils.setInterfaceUp(mNetd, mTunnelIface.getInterfaceName()); final int token = ++mCurrentToken; @@ -2914,7 +3059,9 @@ public class Vpn { /** * Handles loss of the default underlying network * - *

The Ikev2VpnRunner will kill the IKE session and reset the VPN. + *

If the IKE Session has mobility, Ikev2VpnRunner will schedule a teardown event with a + * delay so that the IKE Session can migrate if a new network is available soon. Otherwise, + * Ikev2VpnRunner will kill the IKE session and reset the VPN. * *

This method MUST always be called on the mExecutor thread in order to ensure * consistency of the Ikev2VpnRunner fields. @@ -2931,7 +3078,54 @@ public class Vpn { return; } - handleSessionLost(null); + if (mScheduledHandleNetworkLostTimeout != null + && !mScheduledHandleNetworkLostTimeout.isCancelled() + && !mScheduledHandleNetworkLostTimeout.isDone()) { + final IllegalStateException exception = + new IllegalStateException( + "Found a pending mScheduledHandleNetworkLostTimeout"); + Log.i( + TAG, + "Unexpected error in onDefaultNetworkLost. Tear down session", + exception); + handleSessionLost(exception); + return; + } + + if (mSession != null && mMobikeEnabled) { + Log.d( + TAG, + "IKE Session has mobility. Delay handleSessionLost for losing network " + + network + + "on session with token " + + mCurrentToken); + + // Delay the teardown in case a new network will be available soon. For example, + // during handover between two WiFi networks, Android will disconnect from the + // first WiFi and then connects to the second WiFi. + mScheduledHandleNetworkLostTimeout = + mExecutor.schedule( + () -> { + handleSessionLost(null); + }, + NETWORK_LOST_TIMEOUT_MS, + TimeUnit.MILLISECONDS); + } else { + Log.d(TAG, "Call handleSessionLost for losing network " + network); + handleSessionLost(null); + } + } + + private void cancelHandleNetworkLostTimeout() { + if (mScheduledHandleNetworkLostTimeout != null + && !mScheduledHandleNetworkLostTimeout.isDone()) { + // It does not matter what to put in #cancel(boolean), because it is impossible + // that the task tracked by mScheduledHandleNetworkLostTimeout 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 network lost timeout"); + mScheduledHandleNetworkLostTimeout.cancel(false /* mayInterruptIfRunning */); + } } /** Marks the state as FAILED, and disconnects. */ @@ -2953,6 +3147,8 @@ public class Vpn { * consistency of the Ikev2VpnRunner fields. */ public void onSessionLost(int token, @Nullable Exception exception) { + Log.d(TAG, "onSessionLost() called for token " + token); + if (!isActiveToken(token)) { Log.d(TAG, "onSessionLost() called for obsolete token " + token); @@ -2968,6 +3164,10 @@ public class Vpn { } private void handleSessionLost(@Nullable Exception exception) { + // Cancel mScheduledHandleNetworkLostTimeout if the session it is going to terminate is + // already terminated due to other failures. + cancelHandleNetworkLostTimeout(); + synchronized (Vpn.this) { if (exception instanceof IkeProtocolException) { final IkeProtocolException ikeException = (IkeProtocolException) exception; @@ -3130,6 +3330,8 @@ public class Vpn { mSession.kill(); // Kill here to make sure all resources are released immediately mSession = null; } + mIkeConnectionInfo = null; + mMobikeEnabled = false; } /** diff --git a/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java b/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java index 4f6ed59f332e..857c86de57ca 100644 --- a/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java +++ b/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java @@ -68,6 +68,7 @@ import android.net.ipsec.ike.IkeRfc822AddrIdentification; import android.net.ipsec.ike.IkeSaProposal; 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.TunnelModeChildSessionParams; @@ -107,6 +108,7 @@ public class VpnIkev2Utils { new IkeSessionParams.Builder(context) .setServerHostname(profile.getServerAddr()) .setNetwork(network) + .addIkeOption(IkeSessionParams.IKE_OPTION_MOBIKE) .setLocalIdentification(localId) .setRemoteIdentification(remoteId); setIkeAuth(profile, ikeOptionsBuilder); @@ -309,7 +311,7 @@ public class VpnIkev2Utils { @Override public void onOpened(@NonNull IkeSessionConfiguration ikeSessionConfig) { Log.d(mTag, "IkeOpened for token " + mToken); - // Nothing to do here. + mCallback.onIkeOpened(mToken, ikeSessionConfig); } @Override @@ -329,6 +331,13 @@ public class VpnIkev2Utils { Log.d(mTag, "IkeError for token " + mToken, exception); // Non-fatal, log and continue. } + + @Override + public void onIkeSessionConnectionInfoChanged( + @NonNull IkeSessionConnectionInfo connectionInfo) { + Log.d(mTag, "onIkeSessionConnectionInfoChanged for token " + mToken); + mCallback.onIkeConnectionInfoChanged(mToken, connectionInfo); + } } static class ChildSessionCallbackImpl implements ChildSessionCallback { @@ -373,6 +382,14 @@ public class VpnIkev2Utils { // IKE library. Log.d(mTag, "ChildTransformDeleted; Direction: " + direction + "; for token " + mToken); } + + @Override + public void onIpSecTransformsMigrated( + @NonNull IpSecTransform inIpSecTransform, + @NonNull IpSecTransform outIpSecTransform) { + Log.d(mTag, "ChildTransformsMigrated; token " + mToken); + mCallback.onChildMigrated(mToken, inIpSecTransform, outIpSecTransform); + } } static class Ikev2VpnNetworkCallback extends NetworkCallback { @@ -389,7 +406,7 @@ public class VpnIkev2Utils { @Override public void onAvailable(@NonNull Network network) { - Log.d(mTag, "Starting IKEv2/IPsec session on new network: " + network); + Log.d(mTag, "onAvailable called for network: " + network); mExecutor.execute(() -> mCallback.onDefaultNetworkChanged(network)); } -- cgit v1.2.3-59-g8ed1b From 4dc410d55b66a5ac84185f08b191b2ce71596477 Mon Sep 17 00:00:00 2001 From: lucaslin Date: Thu, 9 Jun 2022 18:24:21 +0800 Subject: 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 (cherry picked from commit b4e39c87c131b34562d7e0e96eac4ea46e75c793) Merged-In: If0fe30f0b98b83a723e14ea8f6cd9a96c54e1691 --- .../java/com/android/server/connectivity/Vpn.java | 148 +++++++++++++++++---- 1 file changed, 123 insertions(+), 25 deletions(-) diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 89ed9a431273..74ba4d941aaa 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, @@ -2672,6 +2694,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; @@ -2695,6 +2718,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; @@ -2771,6 +2802,7 @@ public class Vpn { ikeConfiguration.isIkeExtensionEnabled( IkeSessionConfiguration.EXTENSION_TYPE_MOBIKE); onIkeConnectionInfoChanged(token, ikeConfiguration.getIkeSessionConnectionInfo()); + mRetryCount = 0; } /** @@ -2967,16 +2999,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(); - try { - if (!mIsRunning) { - Log.d(TAG, "onDefaultNetworkChanged after exit"); - return; // VPN has been shut down. - } + if (!mIsRunning) { + Log.d(TAG, "onDefaultNetworkChanged after exit"); + return; // VPN has been shut down. + } + + mActiveNetwork = network; + mRetryCount = 0; - mActiveNetwork = network; + 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 (mSession != null && mMobikeEnabled) { // IKE session can schedule a migration event only when IKE AUTH is finished // and mMobikeEnabled is true. @@ -2985,11 +3043,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; @@ -3005,12 +3065,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()); } @@ -3025,7 +3085,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; @@ -3046,6 +3106,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; @@ -3067,6 +3143,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); @@ -3076,6 +3157,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 @@ -3088,7 +3171,7 @@ public class Vpn { TAG, "Unexpected error in onDefaultNetworkLost. Tear down session", exception); - handleSessionLost(exception); + handleSessionLost(exception, network); return; } @@ -3097,7 +3180,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, @@ -3106,13 +3189,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); } } @@ -3125,6 +3208,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; } } @@ -3160,10 +3257,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(); @@ -3187,7 +3284,7 @@ public class Vpn { VpnManager.ERROR_CLASS_NOT_RECOVERABLE, ikeException.getErrorType(), getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3205,7 +3302,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, ikeException.getErrorType(), getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3224,7 +3321,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, VpnManager.ERROR_CODE_NETWORK_LOST, getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3239,7 +3336,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST, getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3253,7 +3350,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT, getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3267,7 +3364,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, VpnManager.ERROR_CODE_NETWORK_IO, getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3277,9 +3374,10 @@ public class Vpn { } else if (exception != null) { Log.wtf(TAG, "onSessionLost: exception = " + exception); } + + scheduleRetryNewIkeSession(); } - mActiveNetwork = null; mUnderlyingNetworkCapabilities = null; mUnderlyingLinkProperties = null; -- cgit v1.2.3-59-g8ed1b