| /* |
| * 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 android.net.IpSecManager.DIRECTION_IN; |
| import static android.net.IpSecManager.DIRECTION_OUT; |
| import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; |
| import static android.net.NetworkCapabilities.TRANSPORT_WIFI; |
| import static android.net.vcn.VcnManager.VCN_ERROR_CODE_CONFIG_ERROR; |
| import static android.net.vcn.VcnManager.VCN_ERROR_CODE_INTERNAL_ERROR; |
| import static android.net.vcn.VcnManager.VCN_ERROR_CODE_NETWORK_ERROR; |
| |
| import static com.android.server.vcn.VcnGatewayConnection.VcnChildSessionConfiguration; |
| import static com.android.server.vcn.VcnGatewayConnection.VcnIkeSession; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertTrue; |
| import static org.mockito.Matchers.any; |
| import static org.mockito.Matchers.anyInt; |
| import static org.mockito.Matchers.argThat; |
| import static org.mockito.Matchers.eq; |
| import static org.mockito.Mockito.doReturn; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.never; |
| import static org.mockito.Mockito.times; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.verifyNoMoreInteractions; |
| |
| import android.net.ConnectivityManager; |
| import android.net.LinkAddress; |
| import android.net.LinkProperties; |
| import android.net.NetworkAgent; |
| import android.net.NetworkCapabilities; |
| import android.net.ipsec.ike.exceptions.AuthenticationFailedException; |
| import android.net.ipsec.ike.exceptions.IkeException; |
| import android.net.ipsec.ike.exceptions.IkeInternalException; |
| import android.net.ipsec.ike.exceptions.TemporaryFailureException; |
| import android.net.vcn.VcnManager.VcnErrorCode; |
| |
| import androidx.test.filters.SmallTest; |
| import androidx.test.runner.AndroidJUnit4; |
| |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.ArgumentCaptor; |
| |
| import java.io.IOException; |
| import java.net.InetAddress; |
| import java.net.UnknownHostException; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.function.Consumer; |
| |
| /** Tests for VcnGatewayConnection.ConnectedState */ |
| @RunWith(AndroidJUnit4.class) |
| @SmallTest |
| public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnectionTestBase { |
| private VcnIkeSession mIkeSession; |
| private NetworkAgent mNetworkAgent; |
| |
| @Before |
| public void setUp() throws Exception { |
| super.setUp(); |
| |
| mNetworkAgent = mock(NetworkAgent.class); |
| doReturn(mNetworkAgent) |
| .when(mDeps) |
| .newNetworkAgent(any(), any(), any(), any(), anyInt(), any(), any(), any(), any()); |
| |
| mGatewayConnection.setUnderlyingNetwork(TEST_UNDERLYING_NETWORK_RECORD_1); |
| |
| mIkeSession = mGatewayConnection.buildIkeSession(TEST_UNDERLYING_NETWORK_RECORD_1.network); |
| mGatewayConnection.setIkeSession(mIkeSession); |
| |
| mGatewayConnection.transitionTo(mGatewayConnection.mConnectedState); |
| mTestLooper.dispatchAll(); |
| } |
| |
| @Test |
| public void testEnterStateCreatesNewIkeSession() throws Exception { |
| verify(mDeps).newIkeSession(any(), any(), any(), any(), any()); |
| } |
| |
| @Test |
| public void testEnterStateDoesNotCancelSafeModeAlarm() { |
| verifySafeModeTimeoutAlarmAndGetCallback(false /* expectCanceled */); |
| } |
| |
| @Test |
| public void testNullNetworkDoesNotTriggerDisconnect() throws Exception { |
| mGatewayConnection |
| .getUnderlyingNetworkTrackerCallback() |
| .onSelectedUnderlyingNetworkChanged(null); |
| mTestLooper.dispatchAll(); |
| |
| assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState()); |
| verify(mIkeSession, never()).close(); |
| verifyDisconnectRequestAlarmAndGetCallback(false /* expectCanceled */); |
| } |
| |
| @Test |
| public void testNewNetworkTriggersMigration() throws Exception { |
| mGatewayConnection |
| .getUnderlyingNetworkTrackerCallback() |
| .onSelectedUnderlyingNetworkChanged(TEST_UNDERLYING_NETWORK_RECORD_2); |
| mTestLooper.dispatchAll(); |
| |
| assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState()); |
| verify(mIkeSession, never()).close(); |
| verify(mIkeSession).setNetwork(TEST_UNDERLYING_NETWORK_RECORD_2.network); |
| } |
| |
| @Test |
| public void testSameNetworkDoesNotTriggerMigration() throws Exception { |
| mGatewayConnection |
| .getUnderlyingNetworkTrackerCallback() |
| .onSelectedUnderlyingNetworkChanged(TEST_UNDERLYING_NETWORK_RECORD_1); |
| mTestLooper.dispatchAll(); |
| |
| assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState()); |
| } |
| |
| @Test |
| public void testCreatedTransformsAreApplied() throws Exception { |
| for (int direction : new int[] {DIRECTION_IN, DIRECTION_OUT}) { |
| getChildSessionCallback().onIpSecTransformCreated(makeDummyIpSecTransform(), direction); |
| mTestLooper.dispatchAll(); |
| |
| verify(mIpSecSvc) |
| .applyTunnelModeTransform( |
| eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(direction), anyInt(), any()); |
| } |
| |
| assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState()); |
| } |
| |
| @Test |
| public void testMigratedTransformsAreApplied() throws Exception { |
| getChildSessionCallback() |
| .onIpSecTransformsMigrated(makeDummyIpSecTransform(), makeDummyIpSecTransform()); |
| mTestLooper.dispatchAll(); |
| |
| verify(mIpSecSvc, times(2)) |
| .setNetworkForTunnelInterface( |
| eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), |
| eq(TEST_UNDERLYING_NETWORK_RECORD_1.network), |
| any()); |
| |
| for (int direction : new int[] {DIRECTION_IN, DIRECTION_OUT}) { |
| verify(mIpSecSvc) |
| .applyTunnelModeTransform( |
| eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(direction), anyInt(), any()); |
| } |
| |
| assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState()); |
| } |
| |
| private void triggerChildOpened() { |
| triggerChildOpened(Collections.singletonList(TEST_INTERNAL_ADDR), TEST_DNS_ADDR); |
| } |
| |
| private void triggerChildOpened(List<LinkAddress> internalAddresses, InetAddress dnsAddress) { |
| final VcnChildSessionConfiguration mMockChildSessionConfig = |
| mock(VcnChildSessionConfiguration.class); |
| doReturn(internalAddresses).when(mMockChildSessionConfig).getInternalAddresses(); |
| doReturn(Collections.singletonList(dnsAddress)) |
| .when(mMockChildSessionConfig) |
| .getInternalDnsServers(); |
| |
| getChildSessionCallback().onOpened(mMockChildSessionConfig); |
| } |
| |
| private void triggerValidation(int status) { |
| final ArgumentCaptor<Consumer<Integer>> validationCallbackCaptor = |
| ArgumentCaptor.forClass(Consumer.class); |
| verify(mDeps) |
| .newNetworkAgent( |
| any(), |
| any(), |
| any(), |
| any(), |
| anyInt(), |
| any(), |
| any(), |
| any(), |
| validationCallbackCaptor.capture()); |
| |
| validationCallbackCaptor.getValue().accept(status); |
| } |
| |
| @Test |
| public void testChildOpenedRegistersNetwork() throws Exception { |
| // Verify scheduled but not canceled when entering ConnectedState |
| verifySafeModeTimeoutAlarmAndGetCallback(false /* expectCanceled */); |
| triggerChildOpened(); |
| mTestLooper.dispatchAll(); |
| |
| assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState()); |
| |
| final ArgumentCaptor<LinkProperties> lpCaptor = |
| ArgumentCaptor.forClass(LinkProperties.class); |
| final ArgumentCaptor<NetworkCapabilities> ncCaptor = |
| ArgumentCaptor.forClass(NetworkCapabilities.class); |
| verify(mDeps) |
| .newNetworkAgent( |
| eq(mVcnContext), |
| any(String.class), |
| ncCaptor.capture(), |
| lpCaptor.capture(), |
| anyInt(), |
| argThat(nac -> nac.getLegacyType() == ConnectivityManager.TYPE_MOBILE), |
| any(), |
| any(), |
| any()); |
| verify(mNetworkAgent).register(); |
| verify(mNetworkAgent).markConnected(); |
| |
| verify(mIpSecSvc) |
| .addAddressToTunnelInterface( |
| eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(TEST_INTERNAL_ADDR), any()); |
| |
| final LinkProperties lp = lpCaptor.getValue(); |
| assertEquals(Collections.singletonList(TEST_INTERNAL_ADDR), lp.getLinkAddresses()); |
| assertEquals(Collections.singletonList(TEST_DNS_ADDR), lp.getDnsServers()); |
| |
| final NetworkCapabilities nc = ncCaptor.getValue(); |
| assertTrue(nc.hasTransport(TRANSPORT_CELLULAR)); |
| assertFalse(nc.hasTransport(TRANSPORT_WIFI)); |
| 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 |
| triggerValidation(NetworkAgent.VALIDATION_STATUS_VALID); |
| verify(mSafeModeTimeoutAlarm).cancel(); |
| assertFalse(mGatewayConnection.isInSafeMode()); |
| } |
| |
| @Test |
| public void testInternalAndDnsAddressesChanged() throws Exception { |
| final List<LinkAddress> startingInternalAddrs = |
| Arrays.asList(new LinkAddress[] {TEST_INTERNAL_ADDR, TEST_INTERNAL_ADDR_2}); |
| triggerChildOpened(startingInternalAddrs, TEST_DNS_ADDR); |
| mTestLooper.dispatchAll(); |
| |
| for (LinkAddress addr : startingInternalAddrs) { |
| verify(mIpSecSvc) |
| .addAddressToTunnelInterface( |
| eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(addr), any()); |
| } |
| |
| verify(mDeps) |
| .newNetworkAgent( |
| any(), |
| any(), |
| any(), |
| argThat( |
| lp -> |
| startingInternalAddrs.equals(lp.getLinkAddresses()) |
| && Collections.singletonList(TEST_DNS_ADDR) |
| .equals(lp.getDnsServers())), |
| anyInt(), |
| any(), |
| any(), |
| any(), |
| any()); |
| |
| // Trigger another connection event, and verify that the addresses change |
| final List<LinkAddress> newInternalAddrs = |
| Arrays.asList(new LinkAddress[] {TEST_INTERNAL_ADDR_2, TEST_INTERNAL_ADDR_3}); |
| triggerChildOpened(newInternalAddrs, TEST_DNS_ADDR_2); |
| mTestLooper.dispatchAll(); |
| |
| // Verify addresses on tunnel network added/removed |
| for (LinkAddress addr : newInternalAddrs) { |
| verify(mIpSecSvc) |
| .addAddressToTunnelInterface( |
| eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(addr), any()); |
| } |
| verify(mIpSecSvc) |
| .removeAddressFromTunnelInterface( |
| eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(TEST_INTERNAL_ADDR), any()); |
| |
| // TODO(b/184579891): Also verify link properties updated and sent when sendLinkProperties |
| // is mockable |
| |
| // Verify that IpSecTunnelInterface only created once |
| verify(mIpSecSvc).createTunnelInterface(any(), any(), any(), any(), any()); |
| verifyNoMoreInteractions(mIpSecSvc); |
| } |
| |
| @Test |
| public void testSuccessfulConnectionExitsSafeMode() throws Exception { |
| verifySafeModeTimeoutNotifiesCallbackAndUnregistersNetworkAgent( |
| mGatewayConnection.mConnectedState); |
| |
| assertTrue(mGatewayConnection.isInSafeMode()); |
| assertFalse(mGatewayConnection.isQuitting()); |
| |
| triggerChildOpened(); |
| mTestLooper.dispatchAll(); |
| |
| triggerValidation(NetworkAgent.VALIDATION_STATUS_VALID); |
| |
| assertFalse(mGatewayConnection.isInSafeMode()); |
| } |
| |
| @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); |
| |
| // The child session was closed without exception, so verify that the GatewayStatusCallback |
| // was not notified |
| verifyNoMoreInteractions(mGatewayStatusCallback); |
| } |
| |
| @Test |
| public void testChildSessionClosedExceptionallyNotifiesGatewayStatusCallback() |
| throws Exception { |
| final IkeInternalException exception = new IkeInternalException(mock(IOException.class)); |
| getChildSessionCallback().onClosedExceptionally(exception); |
| mTestLooper.dispatchAll(); |
| |
| verify(mGatewayStatusCallback) |
| .onGatewayConnectionError( |
| eq(mConfig.getGatewayConnectionName()), |
| eq(VCN_ERROR_CODE_INTERNAL_ERROR), |
| any(), |
| any()); |
| } |
| |
| @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(); |
| |
| // Since network never validated, verify mSafeModeTimeoutAlarm not canceled |
| verifyNoMoreInteractions(mSafeModeTimeoutAlarm); |
| |
| // IkeSession closed with no error, so verify that the GatewayStatusCallback was not |
| // notified |
| verifyNoMoreInteractions(mGatewayStatusCallback); |
| } |
| |
| private void verifyIkeSessionClosedExceptionalltyNotifiesStatusCallback( |
| IkeException cause, @VcnErrorCode int expectedErrorType) { |
| getIkeSessionCallback().onClosedExceptionally(cause); |
| mTestLooper.dispatchAll(); |
| |
| verify(mIkeSession).close(); |
| |
| verify(mGatewayStatusCallback) |
| .onGatewayConnectionError( |
| eq(mConfig.getGatewayConnectionName()), |
| eq(expectedErrorType), |
| any(), |
| any()); |
| } |
| |
| @Test |
| public void testIkeSessionClosedExceptionallyAuthenticationFailure() throws Exception { |
| verifyIkeSessionClosedExceptionalltyNotifiesStatusCallback( |
| new AuthenticationFailedException("vcn test"), VCN_ERROR_CODE_CONFIG_ERROR); |
| } |
| |
| @Test |
| public void testIkeSessionClosedExceptionallyDnsFailure() throws Exception { |
| verifyIkeSessionClosedExceptionalltyNotifiesStatusCallback( |
| new IkeInternalException(new UnknownHostException()), VCN_ERROR_CODE_NETWORK_ERROR); |
| } |
| |
| @Test |
| public void testIkeSessionClosedExceptionallyInternalFailure() throws Exception { |
| verifyIkeSessionClosedExceptionalltyNotifiesStatusCallback( |
| new TemporaryFailureException("vcn test"), VCN_ERROR_CODE_INTERNAL_ERROR); |
| } |
| |
| @Test |
| public void testTeardown() throws Exception { |
| mGatewayConnection.teardownAsynchronously(); |
| mTestLooper.dispatchAll(); |
| |
| assertEquals(mGatewayConnection.mDisconnectingState, mGatewayConnection.getCurrentState()); |
| assertTrue(mGatewayConnection.isQuitting()); |
| } |
| |
| @Test |
| public void testNonTeardownDisconnectRequest() throws Exception { |
| mGatewayConnection.sendDisconnectRequestedAndAcquireWakelock("TEST", false); |
| mTestLooper.dispatchAll(); |
| |
| assertEquals(mGatewayConnection.mDisconnectingState, mGatewayConnection.getCurrentState()); |
| assertFalse(mGatewayConnection.isQuitting()); |
| } |
| } |