diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 77d3392da9931..f526960d0ef3e 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; /** @@ -190,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. * @@ -473,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, @@ -2605,13 +2630,23 @@ 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 onIkeOpened(int token, @NonNull IkeSessionConfiguration ikeConfiguration); - void onSessionLost(@NonNull Network network, @Nullable Exception exception); + 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); } /** @@ -2642,6 +2677,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; @@ -2653,24 +2692,60 @@ 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; + @Nullable private ScheduledFuture mScheduledHandleRetryIkeSessionTimeout; /** 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; @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; + + /** + * 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; 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 @@ -2709,22 +2784,64 @@ public class Vpn { return Objects.equals(mActiveNetwork, network) && mIsRunning; } + private boolean isActiveToken(int token) { + 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()); + mRetryCount = 0; + } + + /** + * 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. * *

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; } @@ -2743,6 +2860,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; @@ -2785,8 +2907,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); } } @@ -2794,19 +2916,19 @@ 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( - @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; } @@ -2815,36 +2937,127 @@ 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); + } + } + + /** + * 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); + + // 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. + 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 " + + 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; // 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. @@ -2854,12 +3067,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()); } @@ -2867,29 +3080,50 @@ 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 */, underlyingNetwork); 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); } } + 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; @@ -2900,6 +3134,99 @@ public class Vpn { mUnderlyingLinkProperties = lp; } + /** + * Handles loss of the default underlying network + * + *

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. + */ + 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); + + // 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; + } else { + mActiveNetwork = 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, network); + 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); + }, + NETWORK_LOST_TIMEOUT_MS, + TimeUnit.MILLISECONDS); + } else { + Log.d(TAG, "Call handleSessionLost for losing network " + network); + handleSessionLost(null, network); + } + } + + 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 */); + 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; + } + } + /** Marks the state as FAILED, and disconnects. */ private void markFailedAndDisconnect(Exception exception) { synchronized (Vpn.this) { @@ -2918,18 +3245,28 @@ 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) { + Log.d(TAG, "onSessionLost() called for token " + token); + + 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, mActiveNetwork); + } + + 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(); + synchronized (Vpn.this) { if (exception instanceof IkeProtocolException) { final IkeProtocolException ikeException = (IkeProtocolException) exception; @@ -2949,7 +3286,7 @@ public class Vpn { VpnManager.ERROR_CLASS_NOT_RECOVERABLE, ikeException.getErrorType(), getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -2967,7 +3304,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, ikeException.getErrorType(), getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -2986,7 +3323,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, VpnManager.ERROR_CODE_NETWORK_LOST, getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3001,7 +3338,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST, getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3015,7 +3352,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT, getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3029,7 +3366,7 @@ public class Vpn { VpnManager.ERROR_CLASS_RECOVERABLE, VpnManager.ERROR_CODE_NETWORK_IO, getPackage(), mSessionKey, makeVpnProfileStateLocked(), - mActiveNetwork, + network, getRedactedNetworkCapabilitiesOfUnderlyingNetwork( mUnderlyingNetworkCapabilities), getRedactedLinkPropertiesOfUnderlyingNetwork( @@ -3039,15 +3376,16 @@ public class Vpn { } else if (exception != null) { Log.wtf(TAG, "onSessionLost: exception = " + exception); } + + scheduleRetryNewIkeSession(); } - mActiveNetwork = null; mUnderlyingNetworkCapabilities = null; mUnderlyingLinkProperties = null; // 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 @@ -3092,6 +3430,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 17058282d9474..857c86de57ca1 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); @@ -298,72 +300,79 @@ 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); - // Nothing to do here. + Log.d(mTag, "IkeOpened for token " + mToken); + mCallback.onIkeOpened(mToken, ikeSessionConfig); } @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. } + + @Override + public void onIkeSessionConnectionInfoChanged( + @NonNull IkeSessionConnectionInfo connectionInfo) { + Log.d(mTag, "onIkeSessionConnectionInfoChanged for token " + mToken); + mCallback.onIkeConnectionInfoChanged(mToken, connectionInfo); + } } 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 +380,15 @@ 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); + } + + @Override + public void onIpSecTransformsMigrated( + @NonNull IpSecTransform inIpSecTransform, + @NonNull IpSecTransform outIpSecTransform) { + Log.d(mTag, "ChildTransformsMigrated; token " + mToken); + mCallback.onChildMigrated(mToken, inIpSecTransform, outIpSecTransform); } } @@ -390,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)); } @@ -412,8 +428,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)); } }