diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java index 8aab3a66ada3c..0ed26dbe837ef 100644 --- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java +++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java @@ -38,10 +38,12 @@ import android.net.LinkAddress; import android.net.LinkProperties; import android.net.Network; import android.net.NetworkAgent; +import android.net.NetworkAgent.ValidationStatus; import android.net.NetworkAgentConfig; import android.net.NetworkCapabilities; import android.net.RouteInfo; import android.net.TelephonyNetworkSpecifier; +import android.net.Uri; import android.net.annotations.PolicyDirection; import android.net.ipsec.ike.ChildSessionCallback; import android.net.ipsec.ike.ChildSessionConfiguration; @@ -151,6 +153,9 @@ public class VcnGatewayConnection extends StateMachine { @VisibleForTesting(visibility = Visibility.PRIVATE) static final String RETRY_TIMEOUT_ALARM = TAG + "_RETRY_TIMEOUT_ALARM"; + @VisibleForTesting(visibility = Visibility.PRIVATE) + static final String SAFEMODE_TIMEOUT_ALARM = TAG + "_SAFEMODE_TIMEOUT_ALARM"; + private static final int[] MERGED_CAPABILITIES = new int[] {NET_CAPABILITY_NOT_METERED, NET_CAPABILITY_NOT_ROAMING}; private static final int ARG_NOT_PRESENT = Integer.MIN_VALUE; @@ -167,6 +172,9 @@ public class VcnGatewayConnection extends StateMachine { @VisibleForTesting(visibility = Visibility.PRIVATE) static final int TEARDOWN_TIMEOUT_SECONDS = 5; + @VisibleForTesting(visibility = Visibility.PRIVATE) + static final int SAFEMODE_TIMEOUT_SECONDS = 30; + private interface EventInfo {} /** @@ -409,6 +417,23 @@ public class VcnGatewayConnection extends StateMachine { // TODO(b/178426520): implement handling of this event private static final int EVENT_SUBSCRIPTIONS_CHANGED = 9; + /** + * Sent when this VcnGatewayConnection has entered Safemode. + * + *
A VcnGatewayConnection enters Safemode when it takes over {@link + * #SAFEMODE_TIMEOUT_SECONDS} to enter {@link ConnectedState}. + * + *
When a VcnGatewayConnection enters safe mode, it will fire {@link + * VcnGatewayStatusCallback#onEnteredSafemode()} to notify its Vcn. The Vcn will then shut down + * its VcnGatewayConnectin(s). + * + *
Relevant in DisconnectingState, ConnectingState, ConnectedState (if the Vcn Network is not + * validated yet), and RetryTimeoutState. + * + * @param arg1 The "all" token; this signal is always honored. + */ + private static final int EVENT_SAFEMODE_TIMEOUT_EXCEEDED = 10; + @VisibleForTesting(visibility = Visibility.PRIVATE) @NonNull final DisconnectedState mDisconnectedState = new DisconnectedState(); @@ -520,11 +545,13 @@ public class VcnGatewayConnection extends StateMachine { *
Set in Connected state, always @NonNull in Connected, Migrating states, @Nullable * otherwise. */ - private NetworkAgent mNetworkAgent; + @VisibleForTesting(visibility = Visibility.PRIVATE) + NetworkAgent mNetworkAgent; @Nullable private WakeupMessage mTeardownTimeoutAlarm; @Nullable private WakeupMessage mDisconnectRequestAlarm; @Nullable private WakeupMessage mRetryTimeoutAlarm; + @Nullable private WakeupMessage mSafemodeTimeoutAlarm; public VcnGatewayConnection( @NonNull VcnContext vcnContext, @@ -611,6 +638,7 @@ public class VcnGatewayConnection extends StateMachine { cancelTeardownTimeoutAlarm(); cancelDisconnectRequestAlarm(); cancelRetryTimeoutAlarm(); + cancelSafemodeAlarm(); mUnderlyingNetworkTracker.teardown(); } @@ -899,6 +927,30 @@ public class VcnGatewayConnection extends StateMachine { removeEqualMessages(EVENT_RETRY_TIMEOUT_EXPIRED); } + @VisibleForTesting(visibility = Visibility.PRIVATE) + void setSafemodeAlarm() { + // Only schedule a NEW alarm if none is already set. + if (mSafemodeTimeoutAlarm != null) { + return; + } + + final Message delayedMessage = obtainMessage(EVENT_SAFEMODE_TIMEOUT_EXCEEDED, TOKEN_ALL); + mSafemodeTimeoutAlarm = + createScheduledAlarm( + SAFEMODE_TIMEOUT_ALARM, + delayedMessage, + TimeUnit.SECONDS.toMillis(SAFEMODE_TIMEOUT_SECONDS)); + } + + private void cancelSafemodeAlarm() { + if (mSafemodeTimeoutAlarm != null) { + mSafemodeTimeoutAlarm.cancel(); + mSafemodeTimeoutAlarm = null; + } + + removeEqualMessages(EVENT_SAFEMODE_TIMEOUT_EXCEEDED); + } + private void sessionLost(int token, @Nullable Exception exception) { sendMessageAndAcquireWakeLock( EVENT_SESSION_LOST, token, new EventSessionLostInfo(exception)); @@ -1072,6 +1124,8 @@ public class VcnGatewayConnection extends StateMachine { if (mIkeSession != null || mNetworkAgent != null) { Slog.wtf(TAG, "Active IKE Session or NetworkAgent in DisconnectedState"); } + + cancelSafemodeAlarm(); } @Override @@ -1095,6 +1149,12 @@ public class VcnGatewayConnection extends StateMachine { break; } } + + @Override + protected void exitState() { + // Safe to blindly set up, as it is cancelled and cleared on entering this state + setSafemodeAlarm(); + } } private abstract class ActiveBaseState extends BaseState { @@ -1185,6 +1245,10 @@ public class VcnGatewayConnection extends StateMachine { transitionTo(mDisconnectedState); } break; + case EVENT_SAFEMODE_TIMEOUT_EXCEEDED: + mGatewayStatusCallback.onEnteredSafemode(); + mSafemodeTimeoutAlarm = null; + break; default: logUnhandledMessage(msg); break; @@ -1267,6 +1331,10 @@ public class VcnGatewayConnection extends StateMachine { case EVENT_DISCONNECT_REQUESTED: handleDisconnectRequested(((EventDisconnectRequestedInfo) msg.obj).reason); break; + case EVENT_SAFEMODE_TIMEOUT_EXCEEDED: + mGatewayStatusCallback.onEnteredSafemode(); + mSafemodeTimeoutAlarm = null; + break; default: logUnhandledMessage(msg); break; @@ -1310,6 +1378,14 @@ public class VcnGatewayConnection extends StateMachine { public void unwanted() { teardownAsynchronously(); } + + @Override + public void onValidationStatus( + @ValidationStatus int status, @Nullable Uri redirectUri) { + if (status == NetworkAgent.VALIDATION_STATUS_VALID) { + clearFailedAttemptCounterAndSafeModeAlarm(); + } + } }; agent.register(); @@ -1318,6 +1394,14 @@ public class VcnGatewayConnection extends StateMachine { return agent; } + protected void clearFailedAttemptCounterAndSafeModeAlarm() { + mVcnContext.ensureRunningOnLooperThread(); + + // Validated connection, clear failed attempt counter + mFailedAttempts = 0; + cancelSafemodeAlarm(); + } + protected void applyTransform( int token, @NonNull IpSecTunnelInterface tunnelIface, @@ -1397,9 +1481,6 @@ public class VcnGatewayConnection extends StateMachine { teardownAsynchronously(); } } - - // Successful connection, clear failed attempt counter - mFailedAttempts = 0; } @Override @@ -1436,6 +1517,10 @@ public class VcnGatewayConnection extends StateMachine { case EVENT_DISCONNECT_REQUESTED: handleDisconnectRequested(((EventDisconnectRequestedInfo) msg.obj).reason); break; + case EVENT_SAFEMODE_TIMEOUT_EXCEEDED: + mGatewayStatusCallback.onEnteredSafemode(); + mSafemodeTimeoutAlarm = null; + break; default: logUnhandledMessage(msg); break; @@ -1478,8 +1563,19 @@ public class VcnGatewayConnection extends StateMachine { mNetworkAgent = buildNetworkAgent(tunnelIface, childConfig); } else { updateNetworkAgent(tunnelIface, mNetworkAgent, childConfig); + + // mNetworkAgent not null, so the VCN Network has already been established. Clear + // the failed attempt counter and safe mode alarm since this transition is complete. + clearFailedAttemptCounterAndSafeModeAlarm(); } } + + @Override + protected void exitState() { + // Attempt to set the safe mode alarm - this requires the Vcn Network being validated + // while in ConnectedState (which cancels the previous alarm) + setSafemodeAlarm(); + } } /** @@ -1526,6 +1622,10 @@ public class VcnGatewayConnection extends StateMachine { case EVENT_DISCONNECT_REQUESTED: handleDisconnectRequested(((EventDisconnectRequestedInfo) msg.obj).reason); break; + case EVENT_SAFEMODE_TIMEOUT_EXCEEDED: + mGatewayStatusCallback.onEnteredSafemode(); + mSafemodeTimeoutAlarm = null; + break; default: logUnhandledMessage(msg); break; diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java index faa8b234cf51f..a6eae9610036e 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java @@ -34,8 +34,10 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import android.net.LinkProperties; +import android.net.NetworkAgent; import android.net.NetworkCapabilities; import androidx.test.filters.SmallTest; @@ -72,6 +74,11 @@ public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnection verify(mDeps).newIkeSession(any(), any(), any(), any(), any()); } + @Test + public void testEnterStateDoesNotCancelSafemodeAlarm() { + verifySafemodeTimeoutAlarmAndGetCallback(false /* expectCanceled */); + } + @Test public void testNullNetworkDoesNotTriggerDisconnect() throws Exception { mGatewayConnection @@ -122,6 +129,9 @@ public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnection @Test public void testChildOpenedRegistersNetwork() throws Exception { + // Verify scheduled but not canceled when entering ConnectedState + verifySafemodeTimeoutAlarmAndGetCallback(false /* expectCanceled */); + final VcnChildSessionConfiguration mMockChildSessionConfig = mock(VcnChildSessionConfiguration.class); doReturn(Collections.singletonList(TEST_INTERNAL_ADDR)) @@ -163,24 +173,41 @@ public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnection for (int cap : mConfig.getAllExposedCapabilities()) { assertTrue(nc.hasCapability(cap)); } + + // Now that Vcn Network is up, notify it as validated and verify the Safemode alarm is + // canceled + mGatewayConnection.mNetworkAgent.onValidationStatus( + NetworkAgent.VALIDATION_STATUS_VALID, null /* redirectUri */); + verify(mSafemodeTimeoutAlarm).cancel(); } @Test public void testChildSessionClosedTriggersDisconnect() throws Exception { + // Verify scheduled but not canceled when entering ConnectedState + verifySafemodeTimeoutAlarmAndGetCallback(false /* expectCanceled */); + getChildSessionCallback().onClosed(); mTestLooper.dispatchAll(); assertEquals(mGatewayConnection.mDisconnectingState, mGatewayConnection.getCurrentState()); verifyTeardownTimeoutAlarmAndGetCallback(false /* expectCanceled */); + + // Since network never validated, verify mSafemodeTimeoutAlarm not canceled + verifyNoMoreInteractions(mSafemodeTimeoutAlarm); } @Test public void testIkeSessionClosedTriggersDisconnect() throws Exception { + // Verify scheduled but not canceled when entering ConnectedState + verifySafemodeTimeoutAlarmAndGetCallback(false /* expectCanceled */); + getIkeSessionCallback().onClosed(); mTestLooper.dispatchAll(); assertEquals(mGatewayConnection.mRetryTimeoutState, mGatewayConnection.getCurrentState()); verify(mIkeSession).close(); - verifyTeardownTimeoutAlarmAndGetCallback(true /* expectCanceled */); + + // Since network never validated, verify mSafemodeTimeoutAlarm not canceled + verifyNoMoreInteractions(mSafemodeTimeoutAlarm); } } diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java index 760379d6649da..07282c920088b 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java @@ -106,4 +106,9 @@ public class VcnGatewayConnectionConnectingStateTest extends VcnGatewayConnectio verify(mIkeSession).close(); verifyTeardownTimeoutAlarmAndGetCallback(true /* expectCanceled */); } + + @Test + public void testSafemodeTimeoutNotifiesCallback() { + verifySafemodeTimeoutNotifiesCallback(mGatewayConnection.mConnectingState); + } } diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java index 1a1787ac19d97..49ce54d4f6845 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java @@ -48,7 +48,8 @@ public class VcnGatewayConnectionDisconnectedStateTest extends VcnGatewayConnect .createIpSecTunnelInterface( DUMMY_ADDR, DUMMY_ADDR, TEST_UNDERLYING_NETWORK_RECORD_1.network); mGatewayConnection.setTunnelInterface(tunnelIface); - mGatewayConnection.transitionTo(mGatewayConnection.mDisconnectedState); + + // Don't need to transition to DisconnectedState because it is the starting state mTestLooper.dispatchAll(); } @@ -78,6 +79,7 @@ public class VcnGatewayConnectionDisconnectedStateTest extends VcnGatewayConnect mTestLooper.dispatchAll(); assertEquals(mGatewayConnection.mConnectingState, mGatewayConnection.getCurrentState()); + verifySafemodeTimeoutAlarmAndGetCallback(false /* expectCanceled */); } @Test @@ -98,5 +100,6 @@ public class VcnGatewayConnectionDisconnectedStateTest extends VcnGatewayConnect assertNull(mGatewayConnection.getCurrentState()); verify(mIpSecSvc).deleteTunnelInterface(eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), any()); + verifySafemodeTimeoutAlarmAndGetCallback(true /* expectCanceled */); } } diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java index b09f3a008c298..22eab2a1aa65d 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java @@ -80,4 +80,9 @@ public class VcnGatewayConnectionDisconnectingStateTest extends VcnGatewayConnec assertEquals(mGatewayConnection.mDisconnectingState, mGatewayConnection.getCurrentState()); verifyTeardownTimeoutAlarmAndGetCallback(false /* expectCanceled */); } + + @Test + public void testSafemodeTimeoutNotifiesCallback() { + verifySafemodeTimeoutNotifiesCallback(mGatewayConnection.mDisconnectingState); + } } diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java index d37e92f661cc7..6c26075866298 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java @@ -91,4 +91,9 @@ public class VcnGatewayConnectionRetryTimeoutStateTest extends VcnGatewayConnect assertEquals(mGatewayConnection.mConnectingState, mGatewayConnection.getCurrentState()); verifyRetryTimeoutAlarmAndGetCallback(mFirstRetryInterval, true /* expectCanceled */); } + + @Test + public void testSafemodeTimeoutNotifiesCallback() { + verifySafemodeTimeoutNotifiesCallback(mGatewayConnection.mRetryTimeoutState); + } } diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java index 0b44e03a5e5a0..ac9ec0663df2b 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java @@ -20,6 +20,7 @@ import static com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkR import static com.android.server.vcn.VcnGatewayConnection.VcnIkeSession; import static com.android.server.vcn.VcnTestUtils.setupIpSecManager; +import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.atLeastOnce; @@ -50,6 +51,7 @@ import android.os.ParcelUuid; import android.os.PowerManager; import android.os.test.TestLooper; +import com.android.internal.util.State; import com.android.internal.util.WakeupMessage; import com.android.server.IpSecService; import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; @@ -108,6 +110,7 @@ public class VcnGatewayConnectionTestBase { @NonNull protected final WakeupMessage mTeardownTimeoutAlarm; @NonNull protected final WakeupMessage mDisconnectRequestAlarm; @NonNull protected final WakeupMessage mRetryTimeoutAlarm; + @NonNull protected final WakeupMessage mSafemodeTimeoutAlarm; @NonNull protected final IpSecService mIpSecSvc; @NonNull protected final ConnectivityManager mConnMgr; @@ -128,6 +131,7 @@ public class VcnGatewayConnectionTestBase { mTeardownTimeoutAlarm = mock(WakeupMessage.class); mDisconnectRequestAlarm = mock(WakeupMessage.class); mRetryTimeoutAlarm = mock(WakeupMessage.class); + mSafemodeTimeoutAlarm = mock(WakeupMessage.class); mIpSecSvc = mock(IpSecService.class); setupIpSecManager(mContext, mIpSecSvc); @@ -150,6 +154,7 @@ public class VcnGatewayConnectionTestBase { setUpWakeupMessage(mTeardownTimeoutAlarm, VcnGatewayConnection.TEARDOWN_TIMEOUT_ALARM); setUpWakeupMessage(mDisconnectRequestAlarm, VcnGatewayConnection.DISCONNECT_REQUEST_ALARM); setUpWakeupMessage(mRetryTimeoutAlarm, VcnGatewayConnection.RETRY_TIMEOUT_ALARM); + setUpWakeupMessage(mSafemodeTimeoutAlarm, VcnGatewayConnection.SAFEMODE_TIMEOUT_ALARM); doReturn(ELAPSED_REAL_TIME).when(mDeps).getElapsedRealTime(); } @@ -253,4 +258,24 @@ public class VcnGatewayConnectionTestBase { delayInMillis, expectCanceled); } + + protected Runnable verifySafemodeTimeoutAlarmAndGetCallback(boolean expectCanceled) { + return verifyWakeupMessageSetUpAndGetCallback( + VcnGatewayConnection.SAFEMODE_TIMEOUT_ALARM, + mSafemodeTimeoutAlarm, + TimeUnit.SECONDS.toMillis(VcnGatewayConnection.SAFEMODE_TIMEOUT_SECONDS), + expectCanceled); + } + + protected void verifySafemodeTimeoutNotifiesCallback(@NonNull State expectedState) { + // Safemode timer starts when VcnGatewayConnection exits DisconnectedState (the initial + // state) + final Runnable delayedEvent = + verifySafemodeTimeoutAlarmAndGetCallback(false /* expectCanceled */); + delayedEvent.run(); + mTestLooper.dispatchAll(); + + verify(mGatewayStatusCallback).onEnteredSafemode(); + assertEquals(expectedState, mGatewayConnection.getCurrentState()); + } }