Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 14052962 authored by Yan Yan's avatar Yan Yan
Browse files

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
parent 92ce7440
Loading
Loading
Loading
Loading
+219 −17
Original line number Diff line number Diff line
@@ -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;

/**
@@ -2588,10 +2591,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);
    }

@@ -2623,6 +2636,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;
@@ -2634,7 +2651,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;
@@ -2647,12 +2667,17 @@ 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;
@@ -2661,6 +2686,18 @@ public class Vpn {
            // will be called by the mExecutor thread.
            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
@@ -2703,6 +2740,44 @@ public class Vpn {
            return (mCurrentToken == token) && mIsRunning;
        }

        /**
         * Called when an IKE session has been opened
         *
         * <p>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
         *
         * <p>This callback is usually fired when an IKE session has been opened or migrated.
         *
         * <p>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.
         *
@@ -2736,6 +2811,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;

@@ -2754,7 +2834,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);

@@ -2770,8 +2850,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
@@ -2788,7 +2867,7 @@ public class Vpn {
         * Called when an IPsec transform has been created, and should be applied.
         *
         * <p>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(
@@ -2814,17 +2893,66 @@ public class Vpn {
            }
        }

        /**
         * Called when an IPsec transform has been created, and should be re-applied.
         *
         * <p>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.
         *
         * <p>The Ikev2VpnRunner will unconditionally switch to the new network, killing the old IKE
         * state in the process, and starting a new IkeSession instance.
         * <p>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.
         *
         * <p>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) {
@@ -2832,13 +2960,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.
@@ -2861,11 +3003,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;
@@ -2899,7 +3044,9 @@ public class Vpn {
        /**
         * Handles loss of the default underlying network
         *
         * <p>The Ikev2VpnRunner will kill the IKE session and reset the VPN.
         * <p>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.
         *
         * <p>This method MUST always be called on the mExecutor thread in order to ensure
         * consistency of the Ikev2VpnRunner fields.
@@ -2916,8 +3063,55 @@ public class Vpn {
                return;
            }

            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. */
        private void markFailedAndDisconnect(Exception exception) {
@@ -2938,6 +3132,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);

@@ -2953,6 +3149,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;
@@ -3115,6 +3315,8 @@ public class Vpn {
                mSession.kill(); // Kill here to make sure all resources are released immediately
                mSession = null;
            }
            mIkeConnectionInfo = null;
            mMobikeEnabled = false;
        }

        /**
+19 −2
Original line number Diff line number Diff line
@@ -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));
        }