Dynamically set MTU based on proposed algorithms

This change adds the relevant utilities and plumbing to ensure that MTUs
are set dynamically based on the underlying network's MTU.

Bug: 184697651
Test: atest FrameworksVcnTests
Change-Id: I77e34a92eb4e81e83d20fe6019b38ea5f1af4765
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index 8d24c3a..83ac36f 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -85,6 +85,7 @@
 import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkRecord;
 import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkTrackerCallback;
 import com.android.server.vcn.Vcn.VcnGatewayStatusCallback;
+import com.android.server.vcn.util.MtuUtils;
 
 import java.io.IOException;
 import java.net.Inet4Address;
@@ -449,6 +450,44 @@
      */
     private static final int EVENT_SAFE_MODE_TIMEOUT_EXCEEDED = 10;
 
+    /**
+     * Sent when an IKE has completed migration, and created updated transforms for application.
+     *
+     * <p>Only relevant in the Connected state.
+     *
+     * @param arg1 The session token for the IKE Session that completed migration, used to prevent
+     *     out-of-date signals from propagating.
+     * @param obj @NonNull An EventMigrationCompletedInfo instance with relevant data.
+     */
+    private static final int EVENT_MIGRATION_COMPLETED = 11;
+
+    private static class EventMigrationCompletedInfo implements EventInfo {
+        @NonNull public final IpSecTransform inTransform;
+        @NonNull public final IpSecTransform outTransform;
+
+        EventMigrationCompletedInfo(
+                @NonNull IpSecTransform inTransform, @NonNull IpSecTransform outTransform) {
+            this.inTransform = Objects.requireNonNull(inTransform);
+            this.outTransform = Objects.requireNonNull(outTransform);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(inTransform, outTransform);
+        }
+
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (!(other instanceof EventMigrationCompletedInfo)) {
+                return false;
+            }
+
+            final EventMigrationCompletedInfo rhs = (EventMigrationCompletedInfo) other;
+            return Objects.equals(inTransform, rhs.inTransform)
+                    && Objects.equals(outTransform, rhs.outTransform);
+        }
+    }
+
     @VisibleForTesting(visibility = Visibility.PRIVATE)
     @NonNull
     final DisconnectedState mDisconnectedState = new DisconnectedState();
@@ -1054,6 +1093,14 @@
         sendMessageAndAcquireWakeLock(EVENT_SESSION_CLOSED, token);
     }
 
+    private void migrationCompleted(
+            int token, @NonNull IpSecTransform inTransform, @NonNull IpSecTransform outTransform) {
+        sendMessageAndAcquireWakeLock(
+                EVENT_MIGRATION_COMPLETED,
+                token,
+                new EventMigrationCompletedInfo(inTransform, outTransform));
+    }
+
     private void childTransformCreated(
             int token, @NonNull IpSecTransform transform, int direction) {
         sendMessageAndAcquireWakeLock(
@@ -1149,7 +1196,9 @@
                 case EVENT_SETUP_COMPLETED: // Fallthrough
                 case EVENT_DISCONNECT_REQUESTED: // Fallthrough
                 case EVENT_TEARDOWN_TIMEOUT_EXPIRED: // Fallthrough
-                case EVENT_SUBSCRIPTIONS_CHANGED:
+                case EVENT_SUBSCRIPTIONS_CHANGED: // Fallthrough
+                case EVENT_SAFE_MODE_TIMEOUT_EXCEEDED: // Fallthrough
+                case EVENT_MIGRATION_COMPLETED:
                     logUnexpectedEvent(msg.what);
                     break;
                 default:
@@ -1446,7 +1495,8 @@
             final NetworkCapabilities caps =
                     buildNetworkCapabilities(mConnectionConfig, mUnderlying);
             final LinkProperties lp =
-                    buildConnectedLinkProperties(mConnectionConfig, tunnelIface, childConfig);
+                    buildConnectedLinkProperties(
+                            mConnectionConfig, tunnelIface, childConfig, mUnderlying);
 
             agent.sendNetworkCapabilities(caps);
             agent.sendLinkProperties(lp);
@@ -1458,7 +1508,8 @@
             final NetworkCapabilities caps =
                     buildNetworkCapabilities(mConnectionConfig, mUnderlying);
             final LinkProperties lp =
-                    buildConnectedLinkProperties(mConnectionConfig, tunnelIface, childConfig);
+                    buildConnectedLinkProperties(
+                            mConnectionConfig, tunnelIface, childConfig, mUnderlying);
             final NetworkAgentConfig nac =
                     new NetworkAgentConfig.Builder()
                             .setLegacyType(ConnectivityManager.TYPE_MOBILE)
@@ -1627,12 +1678,36 @@
                 case EVENT_SAFE_MODE_TIMEOUT_EXCEEDED:
                     handleSafeModeTimeoutExceeded();
                     break;
+                case EVENT_MIGRATION_COMPLETED:
+                    final EventMigrationCompletedInfo migrationCompletedInfo =
+                            (EventMigrationCompletedInfo) msg.obj;
+
+                    handleMigrationCompleted(migrationCompletedInfo);
+                    break;
                 default:
                     logUnhandledMessage(msg);
                     break;
             }
         }
 
+        private void handleMigrationCompleted(EventMigrationCompletedInfo migrationCompletedInfo) {
+            applyTransform(
+                    mCurrentToken,
+                    mTunnelIface,
+                    mUnderlying.network,
+                    migrationCompletedInfo.inTransform,
+                    IpSecManager.DIRECTION_IN);
+
+            applyTransform(
+                    mCurrentToken,
+                    mTunnelIface,
+                    mUnderlying.network,
+                    migrationCompletedInfo.outTransform,
+                    IpSecManager.DIRECTION_OUT);
+
+            updateNetworkAgent(mTunnelIface, mNetworkAgent, mChildConfig);
+        }
+
         private void handleUnderlyingNetworkChanged(@NonNull Message msg) {
             final UnderlyingNetworkRecord oldUnderlying = mUnderlying;
             mUnderlying = ((EventUnderlyingNetworkChangedInfo) msg.obj).newUnderlying;
@@ -1822,7 +1897,10 @@
     private static LinkProperties buildConnectedLinkProperties(
             @NonNull VcnGatewayConnectionConfig gatewayConnectionConfig,
             @NonNull IpSecTunnelInterface tunnelIface,
-            @NonNull VcnChildSessionConfiguration childConfig) {
+            @NonNull VcnChildSessionConfiguration childConfig,
+            @Nullable UnderlyingNetworkRecord underlying) {
+        final VcnControlPlaneIkeConfig controlPlaneConfig =
+                (VcnControlPlaneIkeConfig) gatewayConnectionConfig.getControlPlaneConfig();
         final LinkProperties lp = new LinkProperties();
 
         lp.setInterfaceName(tunnelIface.getInterfaceName());
@@ -1838,7 +1916,12 @@
         lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null /*gateway*/,
                 null /*iface*/, RouteInfo.RTN_UNICAST));
 
-        lp.setMtu(gatewayConnectionConfig.getMaxMtu());
+        final int underlyingMtu = (underlying == null) ? 0 : underlying.linkProperties.getMtu();
+        lp.setMtu(
+                MtuUtils.getMtu(
+                        controlPlaneConfig.getChildSessionParams().getSaProposals(),
+                        gatewayConnectionConfig.getMaxMtu(),
+                        underlyingMtu));
 
         return lp;
     }
@@ -1919,8 +2002,7 @@
                 @NonNull IpSecTransform inIpSecTransform,
                 @NonNull IpSecTransform outIpSecTransform) {
             Slog.v(TAG, "ChildTransformsMigrated; token " + mToken);
-            onIpSecTransformCreated(inIpSecTransform, IpSecManager.DIRECTION_IN);
-            onIpSecTransformCreated(outIpSecTransform, IpSecManager.DIRECTION_OUT);
+            migrationCompleted(mToken, inIpSecTransform, outIpSecTransform);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/vcn/util/MtuUtils.java b/services/core/java/com/android/server/vcn/util/MtuUtils.java
new file mode 100644
index 0000000..49c1a02
--- /dev/null
+++ b/services/core/java/com/android/server/vcn/util/MtuUtils.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2021 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.util;
+
+import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_3DES;
+import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_CBC;
+import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_CTR;
+import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12;
+import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_16;
+import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8;
+import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_CHACHA20_POLY1305;
+import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_AES_CMAC_96;
+import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_AES_XCBC_96;
+import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96;
+import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_256_128;
+import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_384_192;
+import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_512_256;
+import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_NONE;
+
+import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU;
+
+import static java.lang.Math.max;
+import static java.util.Collections.unmodifiableMap;
+
+import android.annotation.NonNull;
+import android.net.ipsec.ike.ChildSaProposal;
+import android.util.ArrayMap;
+import android.util.Pair;
+import android.util.Slog;
+
+import java.util.List;
+import java.util.Map;
+
+/** @hide */
+public class MtuUtils {
+    private static final String TAG = MtuUtils.class.getSimpleName();
+    /**
+     * Max ESP overhead possible
+     *
+     * <p>60 (Outer IPv4 + options) + 8 (UDP encap) + 4 (SPI) + 4 (Seq) + 2 (Pad + NextHeader)
+     */
+    private static final int GENERIC_ESP_OVERHEAD_MAX = 78;
+
+    /** Maximum overheads of authentication algorithms, keyed on IANA-defined constants */
+    private static final Map<Integer, Integer> AUTH_ALGORITHM_OVERHEAD;
+
+    static {
+        final Map<Integer, Integer> map = new ArrayMap<>();
+        map.put(INTEGRITY_ALGORITHM_NONE, 0);
+        map.put(INTEGRITY_ALGORITHM_HMAC_SHA1_96, 12);
+        map.put(INTEGRITY_ALGORITHM_AES_XCBC_96, 12);
+        map.put(INTEGRITY_ALGORITHM_HMAC_SHA2_256_128, 32);
+        map.put(INTEGRITY_ALGORITHM_HMAC_SHA2_384_192, 48);
+        map.put(INTEGRITY_ALGORITHM_HMAC_SHA2_512_256, 64);
+        map.put(INTEGRITY_ALGORITHM_AES_CMAC_96, 12);
+
+        AUTH_ALGORITHM_OVERHEAD = unmodifiableMap(map);
+    }
+
+    /** Maximum overheads of encryption algorithms, keyed on IANA-defined constants */
+    private static final Map<Integer, Integer> CRYPT_ALGORITHM_OVERHEAD;
+
+    static {
+        final Map<Integer, Integer> map = new ArrayMap<>();
+        map.put(ENCRYPTION_ALGORITHM_3DES, 15); // 8 (IV) + 7 (Max pad)
+        map.put(ENCRYPTION_ALGORITHM_AES_CBC, 31); // 16 (IV) + 15 (Max pad)
+        map.put(ENCRYPTION_ALGORITHM_AES_CTR, 11); // 8 (IV) + 3 (Max pad)
+
+        CRYPT_ALGORITHM_OVERHEAD = unmodifiableMap(map);
+    }
+
+    /** Maximum overheads of combined mode algorithms, keyed on IANA-defined constants */
+    private static final Map<Integer, Integer> AUTHCRYPT_ALGORITHM_OVERHEAD;
+
+    static {
+        final Map<Integer, Integer> map = new ArrayMap<>();
+        map.put(ENCRYPTION_ALGORITHM_AES_GCM_8, 19); // 8 (IV) + 3 (Max pad) + 8 (ICV)
+        map.put(ENCRYPTION_ALGORITHM_AES_GCM_12, 23); // 8 (IV) + 3 (Max pad) + 12 (ICV)
+        map.put(ENCRYPTION_ALGORITHM_AES_GCM_16, 27); // 8 (IV) + 3 (Max pad) + 16 (ICV)
+        map.put(ENCRYPTION_ALGORITHM_CHACHA20_POLY1305, 27); // 8 (IV) + 3 (Max pad) + 16 (ICV)
+
+        AUTHCRYPT_ALGORITHM_OVERHEAD = unmodifiableMap(map);
+    }
+
+    /**
+     * Calculates the MTU of the inner interface based on the parameters provided
+     *
+     * <p>The MTU of the inner interface will be the minimum of the following:
+     *
+     * <ul>
+     *   <li>The MTU of the outer interface, minus the greatest ESP overhead (based on proposed
+     *       algorithms).
+     *   <li>The maximum MTU as provided in the arguments.
+     * </ul>
+     */
+    public static int getMtu(
+            @NonNull List<ChildSaProposal> childProposals, int maxMtu, int underlyingMtu) {
+        if (underlyingMtu <= 0) {
+            return IPV6_MIN_MTU;
+        }
+
+        boolean hasUnknownAlgorithm = false;
+        int maxAuthOverhead = 0;
+        int maxCryptOverhead = 0;
+        int maxAuthCryptOverhead = 0;
+
+        for (ChildSaProposal proposal : childProposals) {
+            for (Pair<Integer, Integer> encryptionAlgoPair : proposal.getEncryptionAlgorithms()) {
+                final int algo = encryptionAlgoPair.first;
+
+                if (AUTHCRYPT_ALGORITHM_OVERHEAD.containsKey(algo)) {
+                    maxAuthCryptOverhead =
+                            max(maxAuthCryptOverhead, AUTHCRYPT_ALGORITHM_OVERHEAD.get(algo));
+                    continue;
+                } else if (CRYPT_ALGORITHM_OVERHEAD.containsKey(algo)) {
+                    maxCryptOverhead = max(maxCryptOverhead, CRYPT_ALGORITHM_OVERHEAD.get(algo));
+                    continue;
+                }
+
+                Slog.wtf(TAG, "Unknown encryption algorithm requested: " + algo);
+                return IPV6_MIN_MTU;
+            }
+
+            for (int algo : proposal.getIntegrityAlgorithms()) {
+                if (AUTH_ALGORITHM_OVERHEAD.containsKey(algo)) {
+                    maxAuthOverhead = max(maxAuthOverhead, AUTH_ALGORITHM_OVERHEAD.get(algo));
+                    continue;
+                }
+
+                Slog.wtf(TAG, "Unknown integrity algorithm requested: " + algo);
+                return IPV6_MIN_MTU;
+            }
+        }
+
+        // Return minimum of maxMtu, and the adjusted MTUs based on algorithms.
+        final int combinedModeMtu = underlyingMtu - maxAuthCryptOverhead - GENERIC_ESP_OVERHEAD_MAX;
+        final int normalModeMtu =
+                underlyingMtu - maxCryptOverhead - maxAuthOverhead - GENERIC_ESP_OVERHEAD_MAX;
+        return Math.min(Math.min(maxMtu, combinedModeMtu), normalModeMtu);
+    }
+}
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
index cb7c96a..bb67593 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
@@ -47,15 +47,19 @@
 import android.net.LinkProperties;
 import android.net.NetworkAgent;
 import android.net.NetworkCapabilities;
+import android.net.ipsec.ike.ChildSaProposal;
 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.VcnControlPlaneIkeConfig;
 import android.net.vcn.VcnManager.VcnErrorCode;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.vcn.util.MtuUtils;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -153,7 +157,9 @@
     }
 
     @Test
-    public void testMigratedTransformsAreApplied() throws Exception {
+    public void testMigration() throws Exception {
+        triggerChildOpened();
+
         getChildSessionCallback()
                 .onIpSecTransformsMigrated(makeDummyIpSecTransform(), makeDummyIpSecTransform());
         mTestLooper.dispatchAll();
@@ -171,6 +177,17 @@
         }
 
         assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState());
+
+        final List<ChildSaProposal> saProposals =
+                ((VcnControlPlaneIkeConfig) mConfig.getControlPlaneConfig())
+                        .getChildSessionParams()
+                        .getSaProposals();
+        final int expectedMtu =
+                MtuUtils.getMtu(
+                        saProposals,
+                        mConfig.getMaxMtu(),
+                        TEST_UNDERLYING_NETWORK_RECORD_1.linkProperties.getMtu());
+        verify(mNetworkAgent).sendLinkProperties(argThat(lp -> expectedMtu == lp.getMtu()));
     }
 
     private void triggerChildOpened() {
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
index b93347b..dc73be2 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
@@ -90,12 +90,18 @@
     protected static final int TEST_SUB_ID = 5;
     protected static final long ELAPSED_REAL_TIME = 123456789L;
     protected static final String TEST_IPSEC_TUNNEL_IFACE = "IPSEC_IFACE";
+
     protected static final UnderlyingNetworkRecord TEST_UNDERLYING_NETWORK_RECORD_1 =
             new UnderlyingNetworkRecord(
                     new Network(0),
                     new NetworkCapabilities(),
                     new LinkProperties(),
                     false /* blocked */);
+
+    static {
+        TEST_UNDERLYING_NETWORK_RECORD_1.linkProperties.setMtu(1500);
+    }
+
     protected static final UnderlyingNetworkRecord TEST_UNDERLYING_NETWORK_RECORD_2 =
             new UnderlyingNetworkRecord(
                     new Network(1),
@@ -103,6 +109,10 @@
                     new LinkProperties(),
                     false /* blocked */);
 
+    static {
+        TEST_UNDERLYING_NETWORK_RECORD_2.linkProperties.setMtu(1460);
+    }
+
     protected static final TelephonySubscriptionSnapshot TEST_SUBSCRIPTION_SNAPSHOT =
             new TelephonySubscriptionSnapshot(
                     Collections.singletonMap(TEST_SUB_ID, TEST_SUB_GRP), Collections.EMPTY_MAP);
diff --git a/tests/vcn/java/com/android/server/vcn/util/MtuUtilsTest.java b/tests/vcn/java/com/android/server/vcn/util/MtuUtilsTest.java
new file mode 100644
index 0000000..29511f7
--- /dev/null
+++ b/tests/vcn/java/com/android/server/vcn/util/MtuUtilsTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.util;
+
+import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_CBC;
+import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12;
+import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_16;
+import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8;
+import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_256_128;
+import static android.net.ipsec.ike.SaProposal.KEY_LEN_AES_256;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_MTU;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU;
+import static com.android.server.vcn.util.MtuUtils.getMtu;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import static java.util.Collections.emptyList;
+
+import android.net.ipsec.ike.ChildSaProposal;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MtuUtilsTest {
+    @Test
+    public void testUnderlyingMtuZero() {
+        assertEquals(
+                IPV6_MIN_MTU, getMtu(emptyList(), ETHER_MTU /* maxMtu */, 0 /* underlyingMtu */));
+    }
+
+    @Test
+    public void testClampsToMaxMtu() {
+        assertEquals(0, getMtu(emptyList(), 0 /* maxMtu */, IPV6_MIN_MTU /* underlyingMtu */));
+    }
+
+    @Test
+    public void testNormalModeAlgorithmLessThanUnderlyingMtu() {
+        final List<ChildSaProposal> saProposals =
+                Arrays.asList(
+                        new ChildSaProposal.Builder()
+                                .addEncryptionAlgorithm(
+                                        ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_256)
+                                .addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA2_256_128)
+                                .build());
+
+        final int actualMtu =
+                getMtu(saProposals, ETHER_MTU /* maxMtu */, ETHER_MTU /* underlyingMtu */);
+        assertTrue(ETHER_MTU > actualMtu);
+    }
+
+    @Test
+    public void testCombinedModeAlgorithmLessThanUnderlyingMtu() {
+        final List<ChildSaProposal> saProposals =
+                Arrays.asList(
+                        new ChildSaProposal.Builder()
+                                .addEncryptionAlgorithm(
+                                        ENCRYPTION_ALGORITHM_AES_GCM_16, KEY_LEN_AES_256)
+                                .addEncryptionAlgorithm(
+                                        ENCRYPTION_ALGORITHM_AES_GCM_12, KEY_LEN_AES_256)
+                                .addEncryptionAlgorithm(
+                                        ENCRYPTION_ALGORITHM_AES_GCM_8, KEY_LEN_AES_256)
+                                .build());
+
+        final int actualMtu =
+                getMtu(saProposals, ETHER_MTU /* maxMtu */, ETHER_MTU /* underlyingMtu */);
+        assertTrue(ETHER_MTU > actualMtu);
+    }
+}