Merge "Add experimental support for IPsec tunnel aggregation"

This commit is contained in:
Benedict Wong
2022-12-17 06:28:31 +00:00
committed by Gerrit Code Review
5 changed files with 233 additions and 4 deletions

View File

@@ -114,13 +114,28 @@ public class VcnManager {
public static final String VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY = public static final String VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY =
"vcn_restricted_transports"; "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 */ /** List of Carrier Config options to extract from Carrier Config bundles. @hide */
@NonNull @NonNull
public static final String[] VCN_RELATED_CARRIER_CONFIG_KEYS = public static final String[] VCN_RELATED_CARRIER_CONFIG_KEYS =
new String[] { new String[] {
VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY, VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY,
VCN_NETWORK_SELECTION_WIFI_EXIT_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< private static final Map<

View File

@@ -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.LOCAL_LOG;
import static com.android.server.VcnManagementService.VDBG; import static com.android.server.VcnManagementService.VDBG;
import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
import android.annotation.NonNull; import android.annotation.NonNull;
import android.annotation.Nullable; import android.annotation.Nullable;
@@ -59,6 +60,7 @@ import android.net.RouteInfo;
import android.net.TelephonyNetworkSpecifier; import android.net.TelephonyNetworkSpecifier;
import android.net.Uri; import android.net.Uri;
import android.net.annotations.PolicyDirection; import android.net.annotations.PolicyDirection;
import android.net.ipsec.ike.ChildSaProposal;
import android.net.ipsec.ike.ChildSessionCallback; import android.net.ipsec.ike.ChildSessionCallback;
import android.net.ipsec.ike.ChildSessionConfiguration; import android.net.ipsec.ike.ChildSessionConfiguration;
import android.net.ipsec.ike.ChildSessionParams; 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.IkeSessionConfiguration;
import android.net.ipsec.ike.IkeSessionConnectionInfo; import android.net.ipsec.ike.IkeSessionConnectionInfo;
import android.net.ipsec.ike.IkeSessionParams; import android.net.ipsec.ike.IkeSessionParams;
import android.net.ipsec.ike.IkeTrafficSelector;
import android.net.ipsec.ike.IkeTunnelConnectionParams; 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.IkeException;
import android.net.ipsec.ike.exceptions.IkeInternalException; import android.net.ipsec.ike.exceptions.IkeInternalException;
import android.net.ipsec.ike.exceptions.IkeProtocolException; import android.net.ipsec.ike.exceptions.IkeProtocolException;
import android.net.vcn.VcnGatewayConnectionConfig; import android.net.vcn.VcnGatewayConnectionConfig;
import android.net.vcn.VcnManager;
import android.net.vcn.VcnTransportInfo; import android.net.vcn.VcnTransportInfo;
import android.net.wifi.WifiInfo; import android.net.wifi.WifiInfo;
import android.os.Handler; import android.os.Handler;
@@ -169,6 +174,9 @@ import java.util.function.Consumer;
public class VcnGatewayConnection extends StateMachine { public class VcnGatewayConnection extends StateMachine {
private static final String TAG = VcnGatewayConnection.class.getSimpleName(); 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 // Matches DataConnection.NETWORK_TYPE private constant, and magic string from
// ConnectivityManager#getNetworkTypeName() // ConnectivityManager#getNetworkTypeName()
@VisibleForTesting(visibility = Visibility.PRIVATE) @VisibleForTesting(visibility = Visibility.PRIVATE)
@@ -1980,6 +1988,22 @@ public class VcnGatewayConnection extends StateMachine {
mChildConfig, mChildConfig,
oldChildConfig, oldChildConfig,
mIkeConnectionInfo); 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; break;
case EVENT_DISCONNECT_REQUESTED: case EVENT_DISCONNECT_REQUESTED:
handleDisconnectRequested((EventDisconnectRequestedInfo) msg.obj); handleDisconnectRequested((EventDisconnectRequestedInfo) msg.obj);
@@ -2350,15 +2374,44 @@ public class VcnGatewayConnection extends StateMachine {
@VisibleForTesting(visibility = Visibility.PRIVATE) @VisibleForTesting(visibility = Visibility.PRIVATE)
public class VcnChildSessionCallback implements ChildSessionCallback { public class VcnChildSessionCallback implements ChildSessionCallback {
private final int mToken; private final int mToken;
private final boolean mIsOpportunistic;
private boolean mIsChildOpened = false;
VcnChildSessionCallback(int token) { 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; mToken = token;
mIsOpportunistic = isOpportunistic;
} }
/** Internal proxy method for injecting of mocked ChildSessionConfiguration */ /** Internal proxy method for injecting of mocked ChildSessionConfiguration */
@VisibleForTesting(visibility = Visibility.PRIVATE) @VisibleForTesting(visibility = Visibility.PRIVATE)
void onOpened(@NonNull VcnChildSessionConfiguration childConfig) { void onOpened(@NonNull VcnChildSessionConfiguration childConfig) {
logDbg("ChildOpened for token " + mToken); logDbg("ChildOpened for token " + mToken);
if (mIsOpportunistic) {
logDbg("ChildOpened for opportunistic child; suppressing event message");
mIsChildOpened = true;
return;
}
childOpened(mToken, childConfig); childOpened(mToken, childConfig);
} }
@@ -2370,12 +2423,24 @@ public class VcnGatewayConnection extends StateMachine {
@Override @Override
public void onClosed() { public void onClosed() {
logDbg("ChildClosed for token " + mToken); logDbg("ChildClosed for token " + mToken);
if (mIsOpportunistic && !mIsChildOpened) {
logDbg("ChildClosed for unopened opportunistic child; ignoring");
return;
}
sessionLost(mToken, null); sessionLost(mToken, null);
} }
@Override @Override
public void onClosedExceptionally(@NonNull IkeException exception) { public void onClosedExceptionally(@NonNull IkeException exception) {
logInfo("ChildClosedExceptionally for token " + mToken, exception); logInfo("ChildClosedExceptionally for token " + mToken, exception);
if (mIsOpportunistic && !mIsChildOpened) {
logInfo("ChildClosedExceptionally for unopened opportunistic child; ignoring");
return;
}
sessionLost(mToken, exception); sessionLost(mToken, exception);
} }
@@ -2580,6 +2645,30 @@ public class VcnGatewayConnection extends StateMachine {
return mConnectionConfig.getTunnelConnectionParams().getTunnelModeChildSessionParams(); 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) @VisibleForTesting(visibility = Visibility.PRIVATE)
VcnIkeSession buildIkeSession(@NonNull Network network) { VcnIkeSession buildIkeSession(@NonNull Network network) {
final int token = ++mCurrentToken; final int token = ++mCurrentToken;
@@ -2680,6 +2769,23 @@ public class VcnGatewayConnection extends StateMachine {
return 0; 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);
}
} }
/** /**

View File

@@ -573,5 +573,10 @@ public class PersistableBundleUtils {
return isEqual(mBundle, other.mBundle); return isEqual(mBundle, other.mBundle);
} }
@Override
public String toString() {
return mBundle.toString();
}
} }
} }

View File

@@ -59,6 +59,7 @@ import android.net.NetworkAgent;
import android.net.NetworkCapabilities; import android.net.NetworkCapabilities;
import android.net.ipsec.ike.ChildSaProposal; import android.net.ipsec.ike.ChildSaProposal;
import android.net.ipsec.ike.IkeSessionConnectionInfo; 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.IkeException;
import android.net.ipsec.ike.exceptions.IkeInternalException; import android.net.ipsec.ike.exceptions.IkeInternalException;
import android.net.ipsec.ike.exceptions.IkeProtocolException; import android.net.ipsec.ike.exceptions.IkeProtocolException;
@@ -70,6 +71,7 @@ import android.os.PersistableBundle;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4; import androidx.test.runner.AndroidJUnit4;
import com.android.server.vcn.VcnGatewayConnection.VcnChildSessionCallback;
import com.android.server.vcn.routeselection.UnderlyingNetworkRecord; import com.android.server.vcn.routeselection.UnderlyingNetworkRecord;
import com.android.server.vcn.util.MtuUtils; import com.android.server.vcn.util.MtuUtils;
@@ -90,6 +92,8 @@ import java.util.function.Consumer;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnectionTestBase { public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnectionTestBase {
private static final int PARALLEL_SA_COUNT = 4;
private VcnIkeSession mIkeSession; private VcnIkeSession mIkeSession;
private VcnNetworkAgent mNetworkAgent; private VcnNetworkAgent mNetworkAgent;
private Network mVcnNetwork; private Network mVcnNetwork;
@@ -227,16 +231,29 @@ public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnection
private void verifyVcnTransformsApplied( private void verifyVcnTransformsApplied(
VcnGatewayConnection vcnGatewayConnection, boolean expectForwardTransform) VcnGatewayConnection vcnGatewayConnection, boolean expectForwardTransform)
throws Exception { 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}) { for (int direction : new int[] {DIRECTION_IN, DIRECTION_OUT}) {
getChildSessionCallback().onIpSecTransformCreated(makeDummyIpSecTransform(), direction); for (VcnChildSessionCallback cb : callbacks) {
cb.onIpSecTransformCreated(makeDummyIpSecTransform(), direction);
}
mTestLooper.dispatchAll(); mTestLooper.dispatchAll();
verify(mIpSecSvc) verify(mIpSecSvc, times(callbacks.size()))
.applyTunnelModeTransform( .applyTunnelModeTransform(
eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(direction), anyInt(), any()); eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(direction), anyInt(), any());
} }
verify(mIpSecSvc, expectForwardTransform ? times(1) : never()) verify(mIpSecSvc, expectForwardTransform ? times(callbacks.size()) : never())
.applyTunnelModeTransform( .applyTunnelModeTransform(
eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(DIRECTION_FWD), anyInt(), any()); 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 */); 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 @Test
public void testInternalAndDnsAddressesChanged() throws Exception { public void testInternalAndDnsAddressesChanged() throws Exception {
final List<LinkAddress> startingInternalAddrs = final List<LinkAddress> startingInternalAddrs =

View File

@@ -223,6 +223,9 @@ public class VcnGatewayConnectionTestBase {
doReturn(mWakeLock) doReturn(mWakeLock)
.when(mDeps) .when(mDeps)
.newWakeLock(eq(mContext), eq(PowerManager.PARTIAL_WAKE_LOCK), any()); .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(mTeardownTimeoutAlarm, VcnGatewayConnection.TEARDOWN_TIMEOUT_ALARM);
setUpWakeupMessage(mDisconnectRequestAlarm, VcnGatewayConnection.DISCONNECT_REQUEST_ALARM); setUpWakeupMessage(mDisconnectRequestAlarm, VcnGatewayConnection.DISCONNECT_REQUEST_ALARM);