diff options
5 files changed, 233 insertions, 4 deletions
diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java index 3a7aea5d5194..70cf97308ffb 100644 --- a/core/java/android/net/vcn/VcnManager.java +++ b/core/java/android/net/vcn/VcnManager.java @@ -114,13 +114,28 @@ public class VcnManager { public static final String VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY = "vcn_restricted_transports"; + /** + * Key for maximum number of parallel SAs for tunnel aggregation + * + * <p>If set to a value > 1, multiple tunnels will be set up, and inbound traffic will be + * aggregated over the various tunnels. + * + * <p>Defaults to 1, unless overridden by carrier config + * + * @hide + */ + @NonNull + public static final String VCN_TUNNEL_AGGREGATION_SA_COUNT_MAX_KEY = + "vcn_tunnel_aggregation_sa_count_max"; + /** List of Carrier Config options to extract from Carrier Config bundles. @hide */ @NonNull public static final String[] VCN_RELATED_CARRIER_CONFIG_KEYS = new String[] { VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY, VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY, - VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY + VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY, + VCN_TUNNEL_AGGREGATION_SA_COUNT_MAX_KEY, }; private static final Map< diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java index 3be16a1fec44..739aff7e87c8 100644 --- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java +++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java @@ -33,6 +33,7 @@ import static android.net.vcn.VcnManager.VCN_ERROR_CODE_NETWORK_ERROR; import static com.android.server.VcnManagementService.LOCAL_LOG; import static com.android.server.VcnManagementService.VDBG; +import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper; import android.annotation.NonNull; import android.annotation.Nullable; @@ -59,6 +60,7 @@ import android.net.RouteInfo; import android.net.TelephonyNetworkSpecifier; import android.net.Uri; import android.net.annotations.PolicyDirection; +import android.net.ipsec.ike.ChildSaProposal; import android.net.ipsec.ike.ChildSessionCallback; import android.net.ipsec.ike.ChildSessionConfiguration; import android.net.ipsec.ike.ChildSessionParams; @@ -67,11 +69,14 @@ 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.IkeTunnelConnectionParams; +import android.net.ipsec.ike.TunnelModeChildSessionParams; import android.net.ipsec.ike.exceptions.IkeException; import android.net.ipsec.ike.exceptions.IkeInternalException; import android.net.ipsec.ike.exceptions.IkeProtocolException; import android.net.vcn.VcnGatewayConnectionConfig; +import android.net.vcn.VcnManager; import android.net.vcn.VcnTransportInfo; import android.net.wifi.WifiInfo; import android.os.Handler; @@ -169,6 +174,9 @@ import java.util.function.Consumer; public class VcnGatewayConnection extends StateMachine { private static final String TAG = VcnGatewayConnection.class.getSimpleName(); + /** Default number of parallel SAs requested */ + static final int TUNNEL_AGGREGATION_SA_COUNT_MAX_DEFAULT = 1; + // Matches DataConnection.NETWORK_TYPE private constant, and magic string from // ConnectivityManager#getNetworkTypeName() @VisibleForTesting(visibility = Visibility.PRIVATE) @@ -1980,6 +1988,22 @@ public class VcnGatewayConnection extends StateMachine { mChildConfig, oldChildConfig, mIkeConnectionInfo); + + // Create opportunistic child SAs; this allows SA aggregation in the downlink, + // reducing lock/atomic contention in high throughput scenarios. All SAs will + // share the same UDP encap socket (and keepalives) as necessary, and are + // effectively free. + final int parallelTunnelCount = + mDeps.getParallelTunnelCount(mLastSnapshot, mSubscriptionGroup); + logInfo("Parallel tunnel count: " + parallelTunnelCount); + + for (int i = 0; i < parallelTunnelCount - 1; i++) { + mIkeSession.openChildSession( + buildOpportunisticChildParams(), + new VcnChildSessionCallback( + mCurrentToken, true /* isOpportunistic */)); + } + break; case EVENT_DISCONNECT_REQUESTED: handleDisconnectRequested((EventDisconnectRequestedInfo) msg.obj); @@ -2350,15 +2374,44 @@ public class VcnGatewayConnection extends StateMachine { @VisibleForTesting(visibility = Visibility.PRIVATE) public class VcnChildSessionCallback implements ChildSessionCallback { private final int mToken; + private final boolean mIsOpportunistic; + + private boolean mIsChildOpened = false; VcnChildSessionCallback(int token) { + this(token, false /* isOpportunistic */); + } + + /** + * Creates a ChildSessionCallback + * + * <p>If configured as opportunistic, transforms will not report initial startup, or + * associated startup failures. This serves the dual purposes of ensuring that if the server + * does not support connection multiplexing, new child SA negotiations will be ignored, and + * at the same time, will notify the VCN session if a successfully negotiated opportunistic + * child SA is subsequently torn down, which could impact uplink traffic if the SA in use + * for outbound/uplink traffic is this opportunistic SA. + * + * <p>While inbound SAs can be used in parallel, the IPsec stack explicitly selects the last + * applied outbound transform for outbound traffic. This means that unlike inbound traffic, + * outbound does not benefit from these parallel SAs in the same manner. + */ + VcnChildSessionCallback(int token, boolean isOpportunistic) { mToken = token; + mIsOpportunistic = isOpportunistic; } /** Internal proxy method for injecting of mocked ChildSessionConfiguration */ @VisibleForTesting(visibility = Visibility.PRIVATE) void onOpened(@NonNull VcnChildSessionConfiguration childConfig) { logDbg("ChildOpened for token " + mToken); + + if (mIsOpportunistic) { + logDbg("ChildOpened for opportunistic child; suppressing event message"); + mIsChildOpened = true; + return; + } + childOpened(mToken, childConfig); } @@ -2370,12 +2423,24 @@ public class VcnGatewayConnection extends StateMachine { @Override public void onClosed() { logDbg("ChildClosed for token " + mToken); + + if (mIsOpportunistic && !mIsChildOpened) { + logDbg("ChildClosed for unopened opportunistic child; ignoring"); + return; + } + sessionLost(mToken, null); } @Override public void onClosedExceptionally(@NonNull IkeException exception) { logInfo("ChildClosedExceptionally for token " + mToken, exception); + + if (mIsOpportunistic && !mIsChildOpened) { + logInfo("ChildClosedExceptionally for unopened opportunistic child; ignoring"); + return; + } + sessionLost(mToken, exception); } @@ -2580,6 +2645,30 @@ public class VcnGatewayConnection extends StateMachine { return mConnectionConfig.getTunnelConnectionParams().getTunnelModeChildSessionParams(); } + private ChildSessionParams buildOpportunisticChildParams() { + final ChildSessionParams baseParams = + mConnectionConfig.getTunnelConnectionParams().getTunnelModeChildSessionParams(); + + final TunnelModeChildSessionParams.Builder builder = + new TunnelModeChildSessionParams.Builder(); + for (ChildSaProposal proposal : baseParams.getChildSaProposals()) { + builder.addChildSaProposal(proposal); + } + + for (IkeTrafficSelector inboundSelector : baseParams.getInboundTrafficSelectors()) { + builder.addInboundTrafficSelectors(inboundSelector); + } + + for (IkeTrafficSelector outboundSelector : baseParams.getOutboundTrafficSelectors()) { + builder.addOutboundTrafficSelectors(outboundSelector); + } + + builder.setLifetimeSeconds( + baseParams.getHardLifetimeSeconds(), baseParams.getSoftLifetimeSeconds()); + + return builder.build(); + } + @VisibleForTesting(visibility = Visibility.PRIVATE) VcnIkeSession buildIkeSession(@NonNull Network network) { final int token = ++mCurrentToken; @@ -2680,6 +2769,23 @@ public class VcnGatewayConnection extends StateMachine { return 0; } } + + /** Gets the max number of parallel tunnels allowed for tunnel aggregation. */ + public int getParallelTunnelCount( + TelephonySubscriptionSnapshot snapshot, ParcelUuid subGrp) { + PersistableBundleWrapper carrierConfig = snapshot.getCarrierConfigForSubGrp(subGrp); + int result = TUNNEL_AGGREGATION_SA_COUNT_MAX_DEFAULT; + + if (carrierConfig != null) { + result = + carrierConfig.getInt( + VcnManager.VCN_TUNNEL_AGGREGATION_SA_COUNT_MAX_KEY, + TUNNEL_AGGREGATION_SA_COUNT_MAX_DEFAULT); + } + + // Guard against tunnel count < 1 + return Math.max(1, result); + } } /** diff --git a/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java b/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java index d22ec0ad456d..d6761a2b37d8 100644 --- a/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java +++ b/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java @@ -573,5 +573,10 @@ public class PersistableBundleUtils { return isEqual(mBundle, other.mBundle); } + + @Override + public String toString() { + return mBundle.toString(); + } } } diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java index 1c21a067bde8..aad7a5eb295c 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java @@ -59,6 +59,7 @@ import android.net.NetworkAgent; import android.net.NetworkCapabilities; import android.net.ipsec.ike.ChildSaProposal; import android.net.ipsec.ike.IkeSessionConnectionInfo; +import android.net.ipsec.ike.TunnelModeChildSessionParams; import android.net.ipsec.ike.exceptions.IkeException; import android.net.ipsec.ike.exceptions.IkeInternalException; import android.net.ipsec.ike.exceptions.IkeProtocolException; @@ -70,6 +71,7 @@ import android.os.PersistableBundle; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.server.vcn.VcnGatewayConnection.VcnChildSessionCallback; import com.android.server.vcn.routeselection.UnderlyingNetworkRecord; import com.android.server.vcn.util.MtuUtils; @@ -90,6 +92,8 @@ import java.util.function.Consumer; @RunWith(AndroidJUnit4.class) @SmallTest public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnectionTestBase { + private static final int PARALLEL_SA_COUNT = 4; + private VcnIkeSession mIkeSession; private VcnNetworkAgent mNetworkAgent; private Network mVcnNetwork; @@ -227,16 +231,29 @@ public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnection private void verifyVcnTransformsApplied( VcnGatewayConnection vcnGatewayConnection, boolean expectForwardTransform) throws Exception { + verifyVcnTransformsApplied( + vcnGatewayConnection, + expectForwardTransform, + Collections.singletonList(getChildSessionCallback())); + } + + private void verifyVcnTransformsApplied( + VcnGatewayConnection vcnGatewayConnection, + boolean expectForwardTransform, + List<VcnChildSessionCallback> callbacks) + throws Exception { for (int direction : new int[] {DIRECTION_IN, DIRECTION_OUT}) { - getChildSessionCallback().onIpSecTransformCreated(makeDummyIpSecTransform(), direction); + for (VcnChildSessionCallback cb : callbacks) { + cb.onIpSecTransformCreated(makeDummyIpSecTransform(), direction); + } mTestLooper.dispatchAll(); - verify(mIpSecSvc) + verify(mIpSecSvc, times(callbacks.size())) .applyTunnelModeTransform( eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(direction), anyInt(), any()); } - verify(mIpSecSvc, expectForwardTransform ? times(1) : never()) + verify(mIpSecSvc, expectForwardTransform ? times(callbacks.size()) : never()) .applyTunnelModeTransform( eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(DIRECTION_FWD), anyInt(), any()); @@ -416,6 +433,89 @@ public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnection verifySafeModeStateAndCallbackFired(1 /* invocationCount */, false /* isInSafeMode */); } + private List<VcnChildSessionCallback> openChildAndVerifyParallelSasRequested() + throws Exception { + doReturn(PARALLEL_SA_COUNT) + .when(mDeps) + .getParallelTunnelCount(eq(TEST_SUBSCRIPTION_SNAPSHOT), eq(TEST_SUB_GRP)); + + // Verify scheduled but not canceled when entering ConnectedState + verifySafeModeTimeoutAlarmAndGetCallback(false /* expectCanceled */); + triggerChildOpened(); + mTestLooper.dispatchAll(); + + // Verify new child sessions requested + final ArgumentCaptor<VcnChildSessionCallback> captor = + ArgumentCaptor.forClass(VcnChildSessionCallback.class); + verify(mIkeSession, times(PARALLEL_SA_COUNT - 1)) + .openChildSession(any(TunnelModeChildSessionParams.class), captor.capture()); + + return captor.getAllValues(); + } + + private List<VcnChildSessionCallback> verifyChildOpenedRequestsAndAppliesParallelSas() + throws Exception { + List<VcnChildSessionCallback> callbacks = openChildAndVerifyParallelSasRequested(); + + verifyVcnTransformsApplied(mGatewayConnection, false, callbacks); + + // Mock IKE calling of onOpened() + for (VcnChildSessionCallback cb : callbacks) { + cb.onOpened(mock(VcnChildSessionConfiguration.class)); + } + mTestLooper.dispatchAll(); + + assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState()); + return callbacks; + } + + @Test + public void testChildOpenedWithParallelSas() throws Exception { + verifyChildOpenedRequestsAndAppliesParallelSas(); + } + + @Test + public void testOpportunisticSa_ignoresPreOpenFailures() throws Exception { + List<VcnChildSessionCallback> callbacks = openChildAndVerifyParallelSasRequested(); + + for (VcnChildSessionCallback cb : callbacks) { + cb.onClosed(); + cb.onClosedExceptionally(mock(IkeException.class)); + } + mTestLooper.dispatchAll(); + + assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState()); + assertEquals(mIkeConnectionInfo, mGatewayConnection.getIkeConnectionInfo()); + } + + private void verifyPostOpenFailuresCloseSession(boolean shouldCloseWithException) + throws Exception { + List<VcnChildSessionCallback> callbacks = verifyChildOpenedRequestsAndAppliesParallelSas(); + + for (VcnChildSessionCallback cb : callbacks) { + if (shouldCloseWithException) { + cb.onClosed(); + } else { + cb.onClosedExceptionally(mock(IkeException.class)); + } + } + mTestLooper.dispatchAll(); + + assertEquals(mGatewayConnection.mDisconnectingState, mGatewayConnection.getCurrentState()); + verify(mIkeSession).close(); + } + + @Test + public void testOpportunisticSa_handlesPostOpenFailures_onClosed() throws Exception { + verifyPostOpenFailuresCloseSession(false /* shouldCloseWithException */); + } + + @Test + public void testOpportunisticSa_handlesPostOpenFailures_onClosedExceptionally() + throws Exception { + verifyPostOpenFailuresCloseSession(true /* shouldCloseWithException */); + } + @Test public void testInternalAndDnsAddressesChanged() throws Exception { final List<LinkAddress> startingInternalAddrs = diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java index 0a18c3d1990d..bb123ffe3073 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java @@ -223,6 +223,9 @@ public class VcnGatewayConnectionTestBase { doReturn(mWakeLock) .when(mDeps) .newWakeLock(eq(mContext), eq(PowerManager.PARTIAL_WAKE_LOCK), any()); + doReturn(1) + .when(mDeps) + .getParallelTunnelCount(eq(TEST_SUBSCRIPTION_SNAPSHOT), eq(TEST_SUB_GRP)); setUpWakeupMessage(mTeardownTimeoutAlarm, VcnGatewayConnection.TEARDOWN_TIMEOUT_ALARM); setUpWakeupMessage(mDisconnectRequestAlarm, VcnGatewayConnection.DISCONNECT_REQUEST_ALARM); |