From b81d4e85475708e23c10aff9d1d7f7a222e66441 Mon Sep 17 00:00:00 2001 From: Benedict Wong Date: Thu, 1 Oct 2020 17:50:53 -0700 Subject: [PATCH] Implement ConnectingState This change implements the Connecting state, where the gateway connection will be established. In the event of a network switch during connection, the existing connection attempt will be aborted and restarted on the new network in an attempt to avoid the case where the old network is completely lost (eg wifi to cell switch), and IKE stalls for the entire retry period. Bug: 165827287 Test: atest FrameworksVcnTests Change-Id: Id1c6337e8ba4cc1b97c6c20977153c230bd07723 --- .../server/vcn/VcnGatewayConnection.java | 113 ++++++++++++++++-- ...nGatewayConnectionConnectingStateTest.java | 105 ++++++++++++++++ .../vcn/VcnGatewayConnectionTestBase.java | 8 ++ 3 files changed, 215 insertions(+), 11 deletions(-) create mode 100644 tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java index 8805fa2f4dbb6..703bfab6d8683 100644 --- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java +++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java @@ -638,6 +638,22 @@ public class VcnGatewayConnection extends StateMachine { protected abstract void processStateMsg(Message msg) throws Exception; + @Override + public void exit() { + try { + exitState(); + } catch (Exception e) { + Slog.wtf(TAG, "Uncaught exception", e); + sendMessage( + EVENT_DISCONNECT_REQUESTED, + TOKEN_ALL, + new EventDisconnectRequestedInfo( + DISCONNECT_REASON_INTERNAL_ERROR + e.toString())); + } + } + + protected void exitState() throws Exception {} + protected void logUnhandledMessage(Message msg) { // Log as unexpected all known messages, and log all else as unknown. switch (msg.what) { @@ -664,18 +680,11 @@ public class VcnGatewayConnection extends StateMachine { } } - protected void teardownIke() { - if (mIkeSession != null) { - mIkeSession.close(); - } - } - protected void handleDisconnectRequested(String msg) { Slog.v(TAG, "Tearing down. Cause: " + msg); mIsRunning = false; teardownNetwork(); - teardownIke(); if (mIkeSession == null) { // Already disconnected, go straight to DisconnectedState @@ -768,6 +777,20 @@ public class VcnGatewayConnection extends StateMachine { * does not complete teardown in a timely fashion, it will be killed (forcibly closed). */ private class DisconnectingState extends ActiveBaseState { + /** + * Whether to skip the RetryTimeoutState and go straight to the ConnectingState. + * + *

This is used when an underlying network change triggered a restart on a new network. + * + *

Reset (to false) upon exit of the DisconnectingState. + */ + private boolean mSkipRetryTimeout = false; + + // TODO(b/178441390): Remove this in favor of resetting retry timers on UND_NET change. + public void setSkipRetryTimeout(boolean shouldSkip) { + mSkipRetryTimeout = shouldSkip; + } + @Override protected void enterState() throws Exception { if (mIkeSession == null) { @@ -783,6 +806,7 @@ public class VcnGatewayConnection extends StateMachine { return; } + mIkeSession.close(); sendMessageDelayed( EVENT_TEARDOWN_TIMEOUT_EXPIRED, mCurrentToken, @@ -822,7 +846,7 @@ public class VcnGatewayConnection extends StateMachine { mIkeSession = null; if (mIsRunning && mUnderlying != null) { - transitionTo(mRetryTimeoutState); + transitionTo(mSkipRetryTimeout ? mConnectingState : mRetryTimeoutState); } else { teardownNetwork(); transitionTo(mDisconnectedState); @@ -833,6 +857,11 @@ public class VcnGatewayConnection extends StateMachine { break; } } + + @Override + protected void exitState() throws Exception { + mSkipRetryTimeout = false; + } } /** @@ -843,7 +872,69 @@ public class VcnGatewayConnection extends StateMachine { */ private class ConnectingState extends ActiveBaseState { @Override - protected void processStateMsg(Message msg) {} + protected void enterState() { + if (mIkeSession != null) { + Slog.wtf(TAG, "ConnectingState entered with active session"); + + // Attempt to recover. + mIkeSession.kill(); + mIkeSession = null; + } + + mIkeSession = buildIkeSession(); + } + + @Override + protected void processStateMsg(Message msg) { + switch (msg.what) { + case EVENT_UNDERLYING_NETWORK_CHANGED: + final UnderlyingNetworkRecord oldUnderlying = mUnderlying; + mUnderlying = ((EventUnderlyingNetworkChangedInfo) msg.obj).newUnderlying; + + if (oldUnderlying == null) { + // This should never happen, but if it does, there's likely a nasty bug. + Slog.wtf(TAG, "Old underlying network was null in connected state. Bug?"); + } + + // If new underlying is null, all underlying networks have been lost; disconnect + if (mUnderlying == null) { + transitionTo(mDisconnectingState); + break; + } + + if (oldUnderlying != null + && mUnderlying.network.equals(oldUnderlying.network)) { + break; // Only network properties have changed; continue connecting. + } + // Else, retry on the new network. + + // Immediately come back to the ConnectingState (skip RetryTimeout, since this + // isn't a failure) + mDisconnectingState.setSkipRetryTimeout(true); + + // fallthrough - disconnect, and retry on new network. + case EVENT_SESSION_LOST: + transitionTo(mDisconnectingState); + break; + case EVENT_SESSION_CLOSED: + deferMessage(msg); + + transitionTo(mDisconnectingState); + break; + case EVENT_SETUP_COMPLETED: // fallthrough + case EVENT_TRANSFORM_CREATED: + // Child setup complete; move to ConnectedState for NetworkAgent registration + deferMessage(msg); + transitionTo(mConnectedState); + break; + case EVENT_DISCONNECT_REQUESTED: + handleDisconnectRequested(((EventDisconnectRequestedInfo) msg.obj).reason); + break; + default: + logUnhandledMessage(msg); + break; + } + } } private abstract class ConnectedStateBase extends ActiveBaseState {} @@ -1015,12 +1106,12 @@ public class VcnGatewayConnection extends StateMachine { } private IkeSessionParams buildIkeParams() { - // TODO: Implement this with ConnectingState + // TODO: Implement this once IkeSessionParams is persisted return null; } private ChildSessionParams buildChildParams() { - // TODO: Implement this with ConnectingState + // TODO: Implement this once IkeSessionParams is persisted return null; } diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java new file mode 100644 index 0000000000000..d936183e5a0bd --- /dev/null +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.vcn; + +import static com.android.server.vcn.VcnGatewayConnection.VcnIkeSession; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for VcnGatewayConnection.ConnectingState */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class VcnGatewayConnectionConnectingStateTest extends VcnGatewayConnectionTestBase { + private VcnIkeSession mIkeSession; + + @Before + public void setUp() throws Exception { + super.setUp(); + + mGatewayConnection.setUnderlyingNetwork(TEST_UNDERLYING_NETWORK_RECORD_1); + mGatewayConnection.transitionTo(mGatewayConnection.mConnectingState); + mTestLooper.dispatchAll(); + + mIkeSession = mGatewayConnection.getIkeSession(); + } + + @Test + public void testEnterStateCreatesNewIkeSession() throws Exception { + verify(mDeps).newIkeSession(any(), any(), any(), any(), any()); + } + + @Test + public void testNullNetworkTriggersDisconnect() throws Exception { + mGatewayConnection + .getUnderlyingNetworkTrackerCallback() + .onSelectedUnderlyingNetworkChanged(null); + mTestLooper.dispatchAll(); + + assertEquals(mGatewayConnection.mDisconnectingState, mGatewayConnection.getCurrentState()); + verify(mIkeSession).kill(); + } + + @Test + public void testNewNetworkTriggersReconnect() throws Exception { + mGatewayConnection + .getUnderlyingNetworkTrackerCallback() + .onSelectedUnderlyingNetworkChanged(TEST_UNDERLYING_NETWORK_RECORD_2); + mTestLooper.dispatchAll(); + + assertEquals(mGatewayConnection.mDisconnectingState, mGatewayConnection.getCurrentState()); + verify(mIkeSession).close(); + verify(mIkeSession, never()).kill(); + } + + @Test + public void testSameNetworkDoesNotTriggerReconnect() throws Exception { + mGatewayConnection + .getUnderlyingNetworkTrackerCallback() + .onSelectedUnderlyingNetworkChanged(TEST_UNDERLYING_NETWORK_RECORD_1); + mTestLooper.dispatchAll(); + + assertEquals(mGatewayConnection.mConnectingState, mGatewayConnection.getCurrentState()); + } + + @Test + public void testChildSessionClosedTriggersDisconnect() throws Exception { + getChildSessionCallback().onClosed(); + mTestLooper.dispatchAll(); + + assertEquals(mGatewayConnection.mDisconnectingState, mGatewayConnection.getCurrentState()); + verify(mIkeSession).close(); + } + + @Test + public void testIkeSessionClosedTriggersDisconnect() throws Exception { + getIkeSessionCallback().onClosed(); + mTestLooper.dispatchAll(); + + assertEquals(mGatewayConnection.mRetryTimeoutState, mGatewayConnection.getCurrentState()); + verify(mIkeSession).close(); + } +} diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java index 346785907fcff..b4d39bf74a4b1 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java @@ -32,6 +32,7 @@ import android.net.IpSecTunnelInterfaceResponse; import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; +import android.net.ipsec.ike.ChildSessionCallback; import android.net.ipsec.ike.IkeSessionCallback; import android.net.vcn.VcnGatewayConnectionConfig; import android.net.vcn.VcnGatewayConnectionConfigTest; @@ -117,4 +118,11 @@ public class VcnGatewayConnectionTestBase { verify(mDeps).newIkeSession(any(), any(), any(), captor.capture(), any()); return captor.getValue(); } + + protected ChildSessionCallback getChildSessionCallback() { + ArgumentCaptor captor = + ArgumentCaptor.forClass(ChildSessionCallback.class); + verify(mDeps).newIkeSession(any(), any(), any(), any(), captor.capture()); + return captor.getValue(); + } }