Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e9b6b197 authored by Benedict Wong's avatar Benedict Wong Committed by Gerrit Code Review
Browse files

Merge "Add experimental support for IPsec tunnel aggregation"

parents d85adb46 dbbbb2cb
Loading
Loading
Loading
Loading
+16 −1
Original line number Diff line number Diff line
@@ -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<
+106 −0
Original line number Diff line number Diff line
@@ -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);
        }
    }

    /**
+5 −0
Original line number Diff line number Diff line
@@ -573,5 +573,10 @@ public class PersistableBundleUtils {

            return isEqual(mBundle, other.mBundle);
        }

        @Override
        public String toString() {
            return mBundle.toString();
        }
    }
}
+103 −3
Original line number Diff line number Diff line
@@ -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 =
+3 −0
Original line number Diff line number Diff line
@@ -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);