Merge "Maximize VCN MTU when using IPv6" am: 6d70c7f36a am: 8f8e80779a am: 30ac2c94d3

Original change: https://android-review.googlesource.com/c/platform/frameworks/base/+/2063435

Change-Id: I0de762eeadbb7218f1b41c18d2475b459c722a85
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index cefd8ef..05df22f 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -61,6 +61,7 @@
 import android.net.ipsec.ike.IkeSession;
 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.IkeTunnelConnectionParams;
 import android.net.ipsec.ike.exceptions.IkeException;
@@ -509,6 +510,42 @@
         }
     }
 
+    /**
+     * Sent when an IKE session connection information has changed.
+     *
+     * <p>This signal is always fired before EVENT_SETUP_COMPLETED and EVENT_MIGRATION_COMPLETED.
+     *
+     * <p>Only relevant in the Connecting and Connected state.
+     *
+     * @param arg1 The session token for the IKE Session whose connection information has changed,
+     *     used to prevent out-of-date signals from propagating.
+     * @param obj @NonNull An EventIkeConnectionInfoChangedInfo instance with relevant data.
+     */
+    private static final int EVENT_IKE_CONNECTION_INFO_CHANGED = 12;
+
+    private static class EventIkeConnectionInfoChangedInfo implements EventInfo {
+        @NonNull public final IkeSessionConnectionInfo ikeConnectionInfo;
+
+        EventIkeConnectionInfoChangedInfo(@NonNull IkeSessionConnectionInfo ikeConnectionInfo) {
+            this.ikeConnectionInfo = ikeConnectionInfo;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(ikeConnectionInfo);
+        }
+
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (!(other instanceof EventIkeConnectionInfoChangedInfo)) {
+                return false;
+            }
+
+            final EventIkeConnectionInfoChangedInfo rhs = (EventIkeConnectionInfoChangedInfo) other;
+            return Objects.equals(ikeConnectionInfo, rhs.ikeConnectionInfo);
+        }
+    }
+
     @VisibleForTesting(visibility = Visibility.PRIVATE)
     @NonNull
     final DisconnectedState mDisconnectedState = new DisconnectedState();
@@ -624,6 +661,14 @@
     private UnderlyingNetworkRecord mUnderlying;
 
     /**
+     * The current IKE Session connection information
+     *
+     * <p>Set in Connected and Migrating states, always @NonNull in Connected, Migrating
+     * states, @Nullable otherwise.
+     */
+    private IkeSessionConnectionInfo mIkeConnectionInfo;
+
+    /**
      * The active IKE session.
      *
      * <p>Set in Connecting or Migrating States, always @NonNull in Connecting, Connected, and
@@ -1197,6 +1242,14 @@
                 exceptionMessage);
     }
 
+    private void ikeConnectionInfoChanged(
+            int token, @NonNull IkeSessionConnectionInfo ikeConnectionInfo) {
+        sendMessageAndAcquireWakeLock(
+                EVENT_IKE_CONNECTION_INFO_CHANGED,
+                token,
+                new EventIkeConnectionInfoChangedInfo(ikeConnectionInfo));
+    }
+
     private void sessionClosed(int token, @Nullable Exception exception) {
         if (exception != null) {
             notifyStatusCallbackForSessionClosed(exception);
@@ -1313,7 +1366,8 @@
                 case EVENT_TEARDOWN_TIMEOUT_EXPIRED: // Fallthrough
                 case EVENT_SUBSCRIPTIONS_CHANGED: // Fallthrough
                 case EVENT_SAFE_MODE_TIMEOUT_EXCEEDED: // Fallthrough
-                case EVENT_MIGRATION_COMPLETED:
+                case EVENT_MIGRATION_COMPLETED: // Fallthrough
+                case EVENT_IKE_CONNECTION_INFO_CHANGED:
                     logUnexpectedEvent(msg.what);
                     break;
                 default:
@@ -1592,6 +1646,7 @@
                     transitionTo(mDisconnectingState);
                     break;
                 case EVENT_SETUP_COMPLETED: // fallthrough
+                case EVENT_IKE_CONNECTION_INFO_CHANGED: // fallthrough
                 case EVENT_TRANSFORM_CREATED:
                     // Child setup complete; move to ConnectedState for NetworkAgent registration
                     deferMessage(msg);
@@ -1614,12 +1669,17 @@
         protected void updateNetworkAgent(
                 @NonNull IpSecTunnelInterface tunnelIface,
                 @NonNull VcnNetworkAgent agent,
-                @NonNull VcnChildSessionConfiguration childConfig) {
+                @NonNull VcnChildSessionConfiguration childConfig,
+                @NonNull IkeSessionConnectionInfo ikeConnectionInfo) {
             final NetworkCapabilities caps =
                     buildNetworkCapabilities(mConnectionConfig, mUnderlying, mIsMobileDataEnabled);
             final LinkProperties lp =
                     buildConnectedLinkProperties(
-                            mConnectionConfig, tunnelIface, childConfig, mUnderlying);
+                            mConnectionConfig,
+                            tunnelIface,
+                            childConfig,
+                            mUnderlying,
+                            ikeConnectionInfo);
 
             agent.sendNetworkCapabilities(caps);
             agent.sendLinkProperties(lp);
@@ -1630,12 +1690,17 @@
 
         protected VcnNetworkAgent buildNetworkAgent(
                 @NonNull IpSecTunnelInterface tunnelIface,
-                @NonNull VcnChildSessionConfiguration childConfig) {
+                @NonNull VcnChildSessionConfiguration childConfig,
+                @NonNull IkeSessionConnectionInfo ikeConnectionInfo) {
             final NetworkCapabilities caps =
                     buildNetworkCapabilities(mConnectionConfig, mUnderlying, mIsMobileDataEnabled);
             final LinkProperties lp =
                     buildConnectedLinkProperties(
-                            mConnectionConfig, tunnelIface, childConfig, mUnderlying);
+                            mConnectionConfig,
+                            tunnelIface,
+                            childConfig,
+                            mUnderlying,
+                            ikeConnectionInfo);
             final NetworkAgentConfig nac =
                     new NetworkAgentConfig.Builder()
                             .setLegacyType(ConnectivityManager.TYPE_MOBILE)
@@ -1838,7 +1903,11 @@
                     mChildConfig = ((EventSetupCompletedInfo) msg.obj).childSessionConfig;
 
                     setupInterfaceAndNetworkAgent(
-                            mCurrentToken, mTunnelIface, mChildConfig, oldChildConfig);
+                            mCurrentToken,
+                            mTunnelIface,
+                            mChildConfig,
+                            oldChildConfig,
+                            mIkeConnectionInfo);
                     break;
                 case EVENT_DISCONNECT_REQUESTED:
                     handleDisconnectRequested((EventDisconnectRequestedInfo) msg.obj);
@@ -1852,6 +1921,10 @@
 
                     handleMigrationCompleted(migrationCompletedInfo);
                     break;
+                case EVENT_IKE_CONNECTION_INFO_CHANGED:
+                    mIkeConnectionInfo =
+                            ((EventIkeConnectionInfoChangedInfo) msg.obj).ikeConnectionInfo;
+                    break;
                 default:
                     logUnhandledMessage(msg);
                     break;
@@ -1875,7 +1948,7 @@
                     migrationCompletedInfo.outTransform,
                     IpSecManager.DIRECTION_OUT);
 
-            updateNetworkAgent(mTunnelIface, mNetworkAgent, mChildConfig);
+            updateNetworkAgent(mTunnelIface, mNetworkAgent, mChildConfig, mIkeConnectionInfo);
 
             // Trigger re-validation after migration events.
             mConnectivityManager.reportNetworkConnectivity(
@@ -1906,7 +1979,8 @@
                 // Network not yet set up, or child not yet connected.
                 if (mNetworkAgent != null && mChildConfig != null) {
                     // If only network properties changed and agent is active, update properties
-                    updateNetworkAgent(mTunnelIface, mNetworkAgent, mChildConfig);
+                    updateNetworkAgent(
+                            mTunnelIface, mNetworkAgent, mChildConfig, mIkeConnectionInfo);
                 }
             }
         }
@@ -1915,13 +1989,14 @@
                 int token,
                 @NonNull IpSecTunnelInterface tunnelIface,
                 @NonNull VcnChildSessionConfiguration childConfig,
-                @NonNull VcnChildSessionConfiguration oldChildConfig) {
+                @NonNull VcnChildSessionConfiguration oldChildConfig,
+                @NonNull IkeSessionConnectionInfo ikeConnectionInfo) {
             setupInterface(token, tunnelIface, childConfig, oldChildConfig);
 
             if (mNetworkAgent == null) {
-                mNetworkAgent = buildNetworkAgent(tunnelIface, childConfig);
+                mNetworkAgent = buildNetworkAgent(tunnelIface, childConfig, ikeConnectionInfo);
             } else {
-                updateNetworkAgent(tunnelIface, mNetworkAgent, childConfig);
+                updateNetworkAgent(tunnelIface, mNetworkAgent, childConfig, ikeConnectionInfo);
 
                 // mNetworkAgent not null, so the VCN Network has already been established. Clear
                 // the failed attempt counter and safe mode alarm since this transition is complete.
@@ -2098,7 +2173,8 @@
             @NonNull VcnGatewayConnectionConfig gatewayConnectionConfig,
             @NonNull IpSecTunnelInterface tunnelIface,
             @NonNull VcnChildSessionConfiguration childConfig,
-            @Nullable UnderlyingNetworkRecord underlying) {
+            @Nullable UnderlyingNetworkRecord underlying,
+            @NonNull IkeSessionConnectionInfo ikeConnectionInfo) {
         final IkeTunnelConnectionParams ikeTunnelParams =
                 gatewayConnectionConfig.getTunnelConnectionParams();
         final LinkProperties lp = new LinkProperties();
@@ -2139,7 +2215,8 @@
                 MtuUtils.getMtu(
                         ikeTunnelParams.getTunnelModeChildSessionParams().getSaProposals(),
                         gatewayConnectionConfig.getMaxMtu(),
-                        underlyingMtu));
+                        underlyingMtu,
+                        ikeConnectionInfo.getLocalAddress() instanceof Inet4Address));
 
         return lp;
     }
@@ -2154,7 +2231,7 @@
         @Override
         public void onOpened(@NonNull IkeSessionConfiguration ikeSessionConfig) {
             logDbg("IkeOpened for token " + mToken);
-            // Nothing to do here.
+            ikeConnectionInfoChanged(mToken, ikeSessionConfig.getIkeSessionConnectionInfo());
         }
 
         @Override
@@ -2174,6 +2251,13 @@
             logInfo("IkeError for token " + mToken, exception);
             // Non-fatal, log and continue.
         }
+
+        @Override
+        public void onIkeSessionConnectionInfoChanged(
+                @NonNull IkeSessionConnectionInfo connectionInfo) {
+            logDbg("onIkeSessionConnectionInfoChanged for token " + mToken);
+            ikeConnectionInfoChanged(mToken, connectionInfo);
+        }
     }
 
     /** Implementation of ChildSessionCallback, exposed for testing. */
@@ -2350,6 +2434,11 @@
     }
 
     @VisibleForTesting(visibility = Visibility.PRIVATE)
+    IkeSessionConnectionInfo getIkeConnectionInfo() {
+        return mIkeConnectionInfo;
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
     boolean isQuitting() {
         return mIsQuitting.getValue();
     }
diff --git a/services/core/java/com/android/server/vcn/util/MtuUtils.java b/services/core/java/com/android/server/vcn/util/MtuUtils.java
index 5d40cca..356c71f 100644
--- a/services/core/java/com/android/server/vcn/util/MtuUtils.java
+++ b/services/core/java/com/android/server/vcn/util/MtuUtils.java
@@ -51,9 +51,20 @@
     /**
      * Max ESP overhead possible
      *
-     * <p>60 (Outer IPv4 + options) + 8 (UDP encap) + 4 (SPI) + 4 (Seq) + 2 (Pad + NextHeader)
+     * <p>60 (Outer IPv4 + options) + 8 (UDP encap) + 4 (SPI) + 4 (Seq) + 2 (Pad Length + Next
+     * Header). Note: Payload data, Pad Length and Next Header will need to be padded to be multiple
+     * of the block size of a cipher, and at the same time be aligned on a 4-byte boundary.
      */
-    private static final int GENERIC_ESP_OVERHEAD_MAX = 78;
+    private static final int GENERIC_ESP_OVERHEAD_MAX_V4 = 78;
+
+    /**
+     * Max ESP overhead possible
+     *
+     * <p>40 (Outer IPv6) + 4 (SPI) + 4 (Seq) + 2 (Pad Length + Next Header). Note: Payload data,
+     * Pad Length and Next Header will need to be padded to be multiple of the block size of a
+     * cipher, and at the same time be aligned on a 4-byte boundary.
+     */
+    private static final int GENERIC_ESP_OVERHEAD_MAX_V6 = 50;
 
     /** Maximum overheads of authentication algorithms, keyed on IANA-defined constants */
     private static final Map<Integer, Integer> AUTH_ALGORITHM_OVERHEAD;
@@ -108,7 +119,10 @@
      * </ul>
      */
     public static int getMtu(
-            @NonNull List<ChildSaProposal> childProposals, int maxMtu, int underlyingMtu) {
+            @NonNull List<ChildSaProposal> childProposals,
+            int maxMtu,
+            int underlyingMtu,
+            boolean isIpv4) {
         if (underlyingMtu <= 0) {
             return IPV6_MIN_MTU;
         }
@@ -145,10 +159,13 @@
             }
         }
 
+        final int genericEspOverheadMax =
+                isIpv4 ? GENERIC_ESP_OVERHEAD_MAX_V4 : GENERIC_ESP_OVERHEAD_MAX_V6;
+
         // Return minimum of maxMtu, and the adjusted MTUs based on algorithms.
-        final int combinedModeMtu = underlyingMtu - maxAuthCryptOverhead - GENERIC_ESP_OVERHEAD_MAX;
+        final int combinedModeMtu = underlyingMtu - maxAuthCryptOverhead - genericEspOverheadMax;
         final int normalModeMtu =
-                underlyingMtu - maxCryptOverhead - maxAuthOverhead - GENERIC_ESP_OVERHEAD_MAX;
+                underlyingMtu - maxCryptOverhead - maxAuthOverhead - genericEspOverheadMax;
         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 841b81c..15d4f10 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
@@ -56,6 +56,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.exceptions.IkeException;
 import android.net.ipsec.ike.exceptions.IkeInternalException;
 import android.net.ipsec.ike.exceptions.IkeProtocolException;
@@ -216,14 +217,23 @@
     @Test
     public void testMigration() throws Exception {
         triggerChildOpened();
+        mTestLooper.dispatchAll();
+        assertEquals(mIkeConnectionInfo, mGatewayConnection.getIkeConnectionInfo());
 
         mGatewayConnection
                 .getUnderlyingNetworkControllerCallback()
                 .onSelectedUnderlyingNetworkChanged(TEST_UNDERLYING_NETWORK_RECORD_2);
+
+        final IkeSessionConnectionInfo newIkeConnectionInfo =
+                new IkeSessionConnectionInfo(
+                        TEST_ADDR_V4, TEST_ADDR_V4_2, TEST_UNDERLYING_NETWORK_RECORD_2.network);
+        getIkeSessionCallback().onIkeSessionConnectionInfoChanged(newIkeConnectionInfo);
         getChildSessionCallback()
                 .onIpSecTransformsMigrated(makeDummyIpSecTransform(), makeDummyIpSecTransform());
         mTestLooper.dispatchAll();
 
+        assertEquals(newIkeConnectionInfo, mGatewayConnection.getIkeConnectionInfo());
+
         verify(mIpSecSvc, times(2))
                 .setNetworkForTunnelInterface(
                         eq(TEST_IPSEC_TUNNEL_RESOURCE_ID),
@@ -246,7 +256,8 @@
                 MtuUtils.getMtu(
                         saProposals,
                         mConfig.getMaxMtu(),
-                        TEST_UNDERLYING_NETWORK_RECORD_2.linkProperties.getMtu());
+                        TEST_UNDERLYING_NETWORK_RECORD_2.linkProperties.getMtu(),
+                        true /* isIpv4 */);
         verify(mNetworkAgent).sendLinkProperties(
                 argThat(lp -> expectedMtu == lp.getMtu()
                         && TEST_TCP_BUFFER_SIZES_2.equals(lp.getTcpBufferSizes())));
@@ -269,6 +280,7 @@
                 .when(mMockChildSessionConfig)
                 .getInternalDnsServers();
 
+        getIkeSessionCallback().onOpened(mIkeSessionConfiguration);
         getChildSessionCallback().onOpened(mMockChildSessionConfig);
     }
 
@@ -298,6 +310,7 @@
         mTestLooper.dispatchAll();
 
         assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState());
+        assertEquals(mIkeConnectionInfo, mGatewayConnection.getIkeConnectionInfo());
 
         final ArgumentCaptor<LinkProperties> lpCaptor =
                 ArgumentCaptor.forClass(LinkProperties.class);
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
index b9dfda3..6a9a1e2 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
@@ -213,7 +213,8 @@
                         VcnGatewayConnectionConfigTest.buildTestConfig(),
                         tunnelIface,
                         childSessionConfig,
-                        record);
+                        record,
+                        mIkeConnectionInfo);
 
         verify(mDeps).getUnderlyingIfaceMtu(LOOPBACK_IFACE);
 
@@ -226,7 +227,8 @@
                         VcnGatewayConnectionConfigTest.buildTestConfig(),
                         tunnelIface,
                         childSessionConfig,
-                        record);
+                        record,
+                        mIkeConnectionInfo);
 
         verify(mDeps, times(2)).getUnderlyingIfaceMtu(LOOPBACK_IFACE);
 
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
index 5628321..3d95a9b 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
@@ -47,6 +47,8 @@
 import android.net.NetworkCapabilities;
 import android.net.ipsec.ike.ChildSessionCallback;
 import android.net.ipsec.ike.IkeSessionCallback;
+import android.net.ipsec.ike.IkeSessionConfiguration;
+import android.net.ipsec.ike.IkeSessionConnectionInfo;
 import android.net.vcn.VcnGatewayConnectionConfig;
 import android.net.vcn.VcnGatewayConnectionConfigTest;
 import android.os.ParcelUuid;
@@ -80,6 +82,13 @@
         doReturn(TEST_SUB_GRP).when(TEST_SUB_INFO).getGroupUuid();
     }
 
+    protected static final InetAddress TEST_ADDR = InetAddresses.parseNumericAddress("2001:db8::1");
+    protected static final InetAddress TEST_ADDR_2 =
+            InetAddresses.parseNumericAddress("2001:db8::2");
+    protected static final InetAddress TEST_ADDR_V4 =
+            InetAddresses.parseNumericAddress("192.0.2.1");
+    protected static final InetAddress TEST_ADDR_V4_2 =
+            InetAddresses.parseNumericAddress("192.0.2.2");
     protected static final InetAddress TEST_DNS_ADDR =
             InetAddresses.parseNumericAddress("2001:DB8:0:1::");
     protected static final InetAddress TEST_DNS_ADDR_2 =
@@ -148,6 +157,9 @@
     @NonNull protected final IpSecService mIpSecSvc;
     @NonNull protected final ConnectivityManager mConnMgr;
 
+    @NonNull protected final IkeSessionConnectionInfo mIkeConnectionInfo;
+    @NonNull protected final IkeSessionConfiguration mIkeSessionConfiguration;
+
     protected VcnIkeSession mMockIkeSession;
     protected VcnGatewayConnection mGatewayConnection;
 
@@ -173,6 +185,10 @@
         VcnTestUtils.setupSystemService(
                 mContext, mConnMgr, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class);
 
+        mIkeConnectionInfo =
+                new IkeSessionConnectionInfo(TEST_ADDR, TEST_ADDR_2, mock(Network.class));
+        mIkeSessionConfiguration = new IkeSessionConfiguration.Builder(mIkeConnectionInfo).build();
+
         doReturn(mContext).when(mVcnContext).getContext();
         doReturn(mTestLooper.getLooper()).when(mVcnContext).getLooper();
         doReturn(mVcnNetworkProvider).when(mVcnContext).getVcnNetworkProvider();
diff --git a/tests/vcn/java/com/android/server/vcn/util/MtuUtilsTest.java b/tests/vcn/java/com/android/server/vcn/util/MtuUtilsTest.java
index 29511f7..e9e7078 100644
--- a/tests/vcn/java/com/android/server/vcn/util/MtuUtilsTest.java
+++ b/tests/vcn/java/com/android/server/vcn/util/MtuUtilsTest.java
@@ -46,34 +46,85 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class MtuUtilsTest {
-    @Test
-    public void testUnderlyingMtuZero() {
+    private void verifyUnderlyingMtuZero(boolean isIpv4) {
         assertEquals(
-                IPV6_MIN_MTU, getMtu(emptyList(), ETHER_MTU /* maxMtu */, 0 /* underlyingMtu */));
+                IPV6_MIN_MTU,
+                getMtu(emptyList(), ETHER_MTU /* maxMtu */, 0 /* underlyingMtu */, isIpv4));
     }
 
     @Test
-    public void testClampsToMaxMtu() {
-        assertEquals(0, getMtu(emptyList(), 0 /* maxMtu */, IPV6_MIN_MTU /* underlyingMtu */));
+    public void testUnderlyingMtuZeroV4() {
+        verifyUnderlyingMtuZero(true /* isIpv4 */);
     }
 
     @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());
+    public void testUnderlyingMtuZeroV6() {
+        verifyUnderlyingMtuZero(false /* isIpv4 */);
+    }
 
+    private void verifyClampsToMaxMtu(boolean isIpv4) {
+        assertEquals(
+                0, getMtu(emptyList(), 0 /* maxMtu */, IPV6_MIN_MTU /* underlyingMtu */, isIpv4));
+    }
+
+    @Test
+    public void testClampsToMaxMtuV4() {
+        verifyClampsToMaxMtu(true /* isIpv4 */);
+    }
+
+    @Test
+    public void testClampsToMaxMtuV6() {
+        verifyClampsToMaxMtu(false /* isIpv4 */);
+    }
+
+    private List<ChildSaProposal> buildChildSaProposalsWithNormalModeAlgo() {
+        return Arrays.asList(
+                new ChildSaProposal.Builder()
+                        .addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_256)
+                        .addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA2_256_128)
+                        .build());
+    }
+
+    private void verifyNormalModeAlgorithmLessThanUnderlyingMtu(boolean isIpv4) {
         final int actualMtu =
-                getMtu(saProposals, ETHER_MTU /* maxMtu */, ETHER_MTU /* underlyingMtu */);
+                getMtu(
+                        buildChildSaProposalsWithNormalModeAlgo(),
+                        ETHER_MTU /* maxMtu */,
+                        ETHER_MTU /* underlyingMtu */,
+                        isIpv4);
         assertTrue(ETHER_MTU > actualMtu);
     }
 
     @Test
-    public void testCombinedModeAlgorithmLessThanUnderlyingMtu() {
+    public void testNormalModeAlgorithmLessThanUnderlyingMtuV4() {
+        verifyNormalModeAlgorithmLessThanUnderlyingMtu(true /* isIpv4 */);
+    }
+
+    @Test
+    public void testNormalModeAlgorithmLessThanUnderlyingMtuV6() {
+        verifyNormalModeAlgorithmLessThanUnderlyingMtu(false /* isIpv4 */);
+    }
+
+    @Test
+    public void testMtuIpv4LessThanMtuIpv6() {
+        final int actualMtuV4 =
+                getMtu(
+                        buildChildSaProposalsWithNormalModeAlgo(),
+                        ETHER_MTU /* maxMtu */,
+                        ETHER_MTU /* underlyingMtu */,
+                        true /* isIpv4 */);
+
+        final int actualMtuV6 =
+                getMtu(
+                        buildChildSaProposalsWithNormalModeAlgo(),
+                        ETHER_MTU /* maxMtu */,
+                        ETHER_MTU /* underlyingMtu */,
+                        false /* isIpv4 */);
+
+        assertTrue(actualMtuV4 < actualMtuV6);
+    }
+
+    private void verifyCombinedModeAlgorithmLessThanUnderlyingMtu(boolean isIpv4) {
         final List<ChildSaProposal> saProposals =
                 Arrays.asList(
                         new ChildSaProposal.Builder()
@@ -86,7 +137,17 @@
                                 .build());
 
         final int actualMtu =
-                getMtu(saProposals, ETHER_MTU /* maxMtu */, ETHER_MTU /* underlyingMtu */);
+                getMtu(saProposals, ETHER_MTU /* maxMtu */, ETHER_MTU /* underlyingMtu */, isIpv4);
         assertTrue(ETHER_MTU > actualMtu);
     }
+
+    @Test
+    public void testCombinedModeAlgorithmLessThanUnderlyingMtuV4() {
+        verifyCombinedModeAlgorithmLessThanUnderlyingMtu(true /* isIpv4 */);
+    }
+
+    @Test
+    public void testCombinedModeAlgorithmLessThanUnderlyingMtuV6() {
+        verifyCombinedModeAlgorithmLessThanUnderlyingMtu(false /* isIpv4 */);
+    }
 }