Force UDP-Encap by faking NAT situation under IPv4 network

This commit updates the IKE library to always to force UDP-Encap when
using IPv4 network. It makes the IKE library more spec compliant and
resolves the issue of dropping non-UDP-encap packets.

According to the IKE spec RFC 7296 Section 2.23:
1) When both IKE endpoints support NAT-T, both of them MUST be able to
receive both UDP-encapsulated ESP and non-UDP-encapsulated ESP packets
2) An IKE endpoint side can decide whether or not to use UDP
encapsulation for ESP.

Currently, the IKE library does not support building one SA for both UDP
Encap and non-UDP-encap Encap packets because of kernel restrictions.
Thus when both sides support NAT-T, IKE will risk dropping ESP packets.
This commit resolves this risk by faking a NAT to ensure the server only
sends one type of packet: the UDP-encapsulated packets.

Bug: 202096754
Test: atest FrameworksIkeTests (new tests), CtsIkeTestCases
Change-Id: If98b6e9af6863077c94a50f8098b3fee22cf781d
diff --git a/src/java/com/android/internal/net/ipsec/ike/IkeSessionStateMachine.java b/src/java/com/android/internal/net/ipsec/ike/IkeSessionStateMachine.java
index a03eba6..2cd46a4 100644
--- a/src/java/com/android/internal/net/ipsec/ike/IkeSessionStateMachine.java
+++ b/src/java/com/android/internal/net/ipsec/ike/IkeSessionStateMachine.java
@@ -88,6 +88,7 @@
 import android.net.eap.EapSessionConfig;
 import android.net.ipsec.ike.ChildSessionCallback;
 import android.net.ipsec.ike.ChildSessionParams;
+import android.net.ipsec.ike.IkeManager;
 import android.net.ipsec.ike.IkeSaProposal;
 import android.net.ipsec.ike.IkeSessionCallback;
 import android.net.ipsec.ike.IkeSessionConfiguration;
@@ -172,6 +173,7 @@
 import java.io.IOException;
 import java.net.Inet4Address;
 import java.net.InetAddress;
+import java.net.InetSocketAddress;
 import java.nio.ByteBuffer;
 import java.security.GeneralSecurityException;
 import java.security.cert.TrustAnchor;
@@ -211,6 +213,13 @@
     // Package private
     static final String TAG = "IkeSessionStateMachine";
 
+    // "192.0.2.0" is selected from RFC5737, "IPv4 Address Blocks Reserved for Documentation"
+    private static final InetAddress FORCE_ENCAP_FAKE_LOCAL_ADDRESS_IPV4 =
+            new InetSocketAddress("192.0.2.0", 0).getAddress();
+    // "001:DB8::" is selected from RFC3849, "IPv6 Address Prefix Reserved for Documentation"
+    private static final InetAddress FORCE_ENCAP_FAKE_LOCAL_ADDRESS_IPV6 =
+            new InetSocketAddress("2001:DB8::", 0).getAddress();
+
     @VisibleForTesting static final String BUSY_WAKE_LOCK_TAG = "mBusyWakeLock";
 
     // TODO: b/140579254 Allow users to configure fragment size.
@@ -860,6 +869,19 @@
         sendMessageDelayed(localRequest.procedureType, localRequest, RETRY_INTERVAL_MS);
     }
 
+    private boolean needEnableForceUdpEncap() {
+        // When IKE library uses IPv4 and needs to do NAT detection, it needs to enforce UDP
+        // encapsulation to prevent the server from sending non-UDP-encap packets.
+        //
+        // NOTE: Although the IKE spec requires implementations to handle both UDP-encap and
+        // non-UDP-encap ESP packets when both the IKE client and server support NAT-T, due to
+        // kernel restrictions, the Android IPsec stack is unable to allow receiving two types of
+        // packets with a single SA. As a result, before kernel issues (b/210164853) are resolved,
+        // the IKE library MUST enforce UDP Encap to ensure that the server only sends UDP-encap
+        // packets in order to avoid dropping packets.
+        return (mIkeConnectionCtrl.getRemoteAddress() instanceof Inet4Address);
+    }
+
     // TODO: Support initiating Delete IKE exchange when IKE SA expires
 
     // TODO: Add interfaces to initiate IKE exchanges.
@@ -3113,7 +3135,7 @@
                             mIkeConnectionCtrl.getLocalPort(),
                             mIkeConnectionCtrl.getRemotePort(),
                             mIkeContext.getRandomnessFactory(),
-                            mIkeSessionParams.hasIkeOption(IKE_OPTION_MOBIKE));
+                            needEnableForceUdpEncap());
             payloadList.add(
                     new IkeNotifyPayload(
                             IkeNotifyPayload.NOTIFY_TYPE_IKEV2_FRAGMENTATION_SUPPORTED));
@@ -3406,6 +3428,11 @@
             }
         }
 
+        if (!localNatDetected && needEnableForceUdpEncap()) {
+            logd("there is no actual local NAT, but we have faked it");
+            localNatDetected = true;
+        }
+
         return localNatDetected || remoteNatDetected;
     }
 
@@ -5222,7 +5249,8 @@
                         mIkeConnectionCtrl.getLocalPort(),
                         mIkeConnectionCtrl.getRemotePort(),
                         mCurrentIkeSaRecord.getInitiatorSpi(),
-                        mCurrentIkeSaRecord.getResponderSpi());
+                        mCurrentIkeSaRecord.getResponderSpi(),
+                        needEnableForceUdpEncap());
             }
 
             return buildEncryptedInformationalMessage(
@@ -5382,21 +5410,36 @@
             int localPort,
             int remotePort,
             long initIkeSpi,
-            long respIkeSpi) {
+            long respIkeSpi,
+            boolean isForceUdpEncapEnabled) {
         // Though RFC says Notify-NAT payload is "just after the Ni and Nr payloads (before
         // the optional CERTREQ payload)", it also says recipient MUST NOT reject " messages
         // in which the payloads were not in the "right" order" due to the lack of clarity
-        // of the payload order.
-        payloadList.add(
+        // of the payload order
+        InetAddress localAddressToUse = localAddr;
+
+        if (isForceUdpEncapEnabled) {
+            IkeManager.getIkeLog().d(TAG, " Faking NAT situation to enforce UDP encapsulation");
+            localAddressToUse =
+                    (remoteAddr instanceof Inet4Address)
+                            ? FORCE_ENCAP_FAKE_LOCAL_ADDRESS_IPV4
+                            : FORCE_ENCAP_FAKE_LOCAL_ADDRESS_IPV6;
+        }
+
+        IkeNotifyPayload natdSrcIp =
                 new IkeNotifyPayload(
                         NOTIFY_TYPE_NAT_DETECTION_SOURCE_IP,
                         IkeNotifyPayload.generateNatDetectionData(
-                                initIkeSpi, respIkeSpi, localAddr, localPort)));
-        payloadList.add(
+                                initIkeSpi, respIkeSpi, localAddressToUse, localPort));
+
+        IkeNotifyPayload natdDstIp =
                 new IkeNotifyPayload(
                         NOTIFY_TYPE_NAT_DETECTION_DESTINATION_IP,
                         IkeNotifyPayload.generateNatDetectionData(
-                                initIkeSpi, respIkeSpi, remoteAddr, remotePort)));
+                                initIkeSpi, respIkeSpi, remoteAddr, remotePort));
+
+        payloadList.add(natdSrcIp);
+        payloadList.add(natdDstIp);
     }
 
     private static class IkeEapOutboundMsgWrapper {
@@ -5430,7 +5473,7 @@
                 int localPort,
                 int remotePort,
                 RandomnessFactory randomFactory,
-                boolean isMobikeSupportEnabled)
+                boolean isForceUdpEncapEnabled)
                 throws IOException {
             List<IkePayload> payloadList =
                     getCreateIkeSaPayloads(
@@ -5449,7 +5492,8 @@
                         localPort,
                         remotePort,
                         initIkeSpi,
-                        respIkeSpi);
+                        respIkeSpi,
+                        isForceUdpEncapEnabled);
             }
 
             return payloadList;
diff --git a/tests/iketests/src/java/com/android/internal/net/ipsec/ike/IkeSessionStateMachineTest.java b/tests/iketests/src/java/com/android/internal/net/ipsec/ike/IkeSessionStateMachineTest.java
index 80db0a7..9b00e12 100644
--- a/tests/iketests/src/java/com/android/internal/net/ipsec/ike/IkeSessionStateMachineTest.java
+++ b/tests/iketests/src/java/com/android/internal/net/ipsec/ike/IkeSessionStateMachineTest.java
@@ -1772,20 +1772,17 @@
     }
 
     @Test
-    public void testCreateIkeLocalIkeInitSwitchesToEncapPorts() throws Exception {
+    public void testCreateIkeLocalIkeInitSwitchesToEncapPortsIpv4() throws Exception {
         setupFirstIkeSa();
-        mIkeSessionStateMachine.sendMessage(IkeSessionStateMachine.CMD_LOCAL_REQUEST_CREATE_IKE);
-        mLooper.dispatchAll();
+        assertFalse(mSpyIkeConnectionCtrl.useUdpEncapSocket());
 
-        // Receive IKE INIT response
-        ReceivedIkePacket dummyReceivedIkePacket = makeIkeInitResponse();
-        mIkeSessionStateMachine.sendMessage(
-                IkeSessionStateMachine.CMD_RECEIVE_IKE_PACKET, dummyReceivedIkePacket);
-        mLooper.dispatchAll();
+        triggerAndVerifyIkeInitReq(true /* expectingNatDetection */, true /* expectingFakedNatd */);
 
-        // Validate socket switched
+        receiveAndGetIkeInitResp();
+
         assertTrue(mSpyIkeConnectionCtrl.useUdpEncapSocket());
         assertEquals(NAT_DETECTED, mSpyIkeConnectionCtrl.getNatStatus());
+
         verify(mMockIkeUdp4Socket).unregisterIke(anyLong());
     }
 
@@ -1809,15 +1806,13 @@
     public void testCreateIkeLocalIkeInitNatTraversalWithEnforcePort4500() throws Exception {
         restartIkeSessionWithEnforcePort4500AndVerifyIkeSocket();
         setupFirstIkeSa();
+        assertTrue(mSpyIkeConnectionCtrl.useUdpEncapSocket());
 
-        final IkeSocket ikeSocket = mSpyIkeConnectionCtrl.getIkeSocket();
-
-        mIkeSessionStateMachine.sendMessage(IkeSessionStateMachine.CMD_LOCAL_REQUEST_CREATE_IKE);
-        mLooper.dispatchAll();
+        triggerAndVerifyIkeInitReq(true /* expectingNatDetection */, true /* expectingFakedNatd */);
 
         receiveAndGetIkeInitResp();
 
-        assertEquals(ikeSocket, mSpyIkeConnectionCtrl.getIkeSocket());
+        assertTrue(mSpyIkeConnectionCtrl.useUdpEncapSocket());
         assertEquals(NAT_DETECTED, mSpyIkeConnectionCtrl.getNatStatus());
     }
 
@@ -1841,7 +1836,41 @@
         verify(mMockIkeUdp4Socket, never()).unregisterIke(anyLong());
     }
 
+    private void verifyNatdSrcIpFromIkeInitReqMessage(IkeMessage ikeInitReqMessag) {
+        verifyNatdSrcIpFromIkeInitReqMessage(ikeInitReqMessag, false /* expectingFakedNatd */);
+    }
+
+    private void verifyNatdSrcIpFromIkeInitReqMessage(
+            IkeMessage ikeInitReqMessage, boolean expectingFakedNatd) {
+        List<IkeNotifyPayload> notifyPayloads =
+                ikeInitReqMessage.getPayloadListForType(
+                        IkePayload.PAYLOAD_TYPE_NOTIFY, IkeNotifyPayload.class);
+        IkeNotifyPayload natdSrcIpPayload = null;
+        for (IkeNotifyPayload notifyPayload : notifyPayloads) {
+            if (notifyPayload.notifyType == NOTIFY_TYPE_NAT_DETECTION_SOURCE_IP) {
+                natdSrcIpPayload = notifyPayload;
+            }
+        }
+        assertNotNull(natdSrcIpPayload);
+
+        byte[] localNatDataNotFaked =
+                IkeNotifyPayload.generateNatDetectionData(
+                        ikeInitReqMessage.ikeHeader.ikeInitiatorSpi,
+                        ikeInitReqMessage.ikeHeader.ikeResponderSpi,
+                        mSpyIkeConnectionCtrl.getLocalAddress(),
+                        mSpyIkeConnectionCtrl.getLocalPort());
+
+        assertEquals(
+                !expectingFakedNatd,
+                Arrays.equals(localNatDataNotFaked, natdSrcIpPayload.notifyData));
+    }
+
     private void triggerAndVerifyIkeInitReq(boolean expectingNatDetection) throws Exception {
+        triggerAndVerifyIkeInitReq(expectingNatDetection, false);
+    }
+
+    private void triggerAndVerifyIkeInitReq(
+            boolean expectingNatDetection, boolean expectingFakedNatd) throws Exception {
         // Send IKE INIT request
         mIkeSessionStateMachine.sendMessage(IkeSessionStateMachine.CMD_LOCAL_REQUEST_CREATE_IKE);
         mLooper.dispatchAll();
@@ -1869,6 +1898,10 @@
         assertEquals(
                 expectingNatDetection,
                 isNotifyExist(payloadList, NOTIFY_TYPE_NAT_DETECTION_DESTINATION_IP));
+
+        if (expectingNatDetection) {
+            verifyNatdSrcIpFromIkeInitReqMessage(ikeInitReqMessage, expectingFakedNatd);
+        }
     }
 
     private ReceivedIkePacket receiveAndGetIkeInitResp() throws Exception {
@@ -6079,8 +6112,12 @@
                         configuredNetwork, isEnforcePort4500, isIpv4);
         new IkeFirstAuthTestPretest().mockIkeInitAndTransitionToIkeAuth();
 
+        // Enable force_udp_encap under IPv4 network because the kernel cannot process both
+        // UDP-encap and non-UDP-encap ESP packets for a single SA
+        boolean doesEnableForceUdpEncap = isIpv4;
+
         if (isIpv4) {
-            if (doesPeerSupportNatt) {
+            if (doesPeerSupportNatt || doesEnableForceUdpEncap) {
                 // Either NAT detected or not detected won't affect the test since both cases
                 // indicate the server support NAT-T
                 mSpyIkeConnectionCtrl.handleNatDetectionResultInIkeInit(
@@ -6154,7 +6191,10 @@
                         : IkeSpecificNetworkCallback.class;
         assertTrue(expectedCallbackType.isInstance(networkCallback));
         assertTrue(
-                getExpectedSocketType(doesPeerSupportNatt, isEnforcePort4500, isIpv4)
+                getExpectedSocketType(
+                                doesPeerSupportNatt || doesEnableForceUdpEncap,
+                                isEnforcePort4500,
+                                isIpv4)
                         .isInstance(mSpyIkeConnectionCtrl.getIkeSocket()));
         return networkCallback;
     }
@@ -6267,7 +6307,7 @@
     @SdkSuppress(minSdkVersion = 31, codeName = "S")
     public void testMobikeActiveMobilityEvent() throws Exception {
         verifyMobikeActiveMobilityEvent(false /* isEnforcePort4500 */);
-        assertTrue(mSpyIkeConnectionCtrl.getIkeSocket() instanceof IkeUdp4Socket);
+        assertTrue(mSpyIkeConnectionCtrl.getIkeSocket() instanceof IkeUdpEncapSocket);
     }
 
     @Test
@@ -6442,7 +6482,8 @@
                 isIpv4AfterNetworkChange);
         assertTrue(
                 getExpectedSocketType(
-                                doesPeerSupportNatt,
+                                doesPeerSupportNatt
+                                        || isIpv4AfterNetworkChange /* force UDP-encap */,
                                 false /* isEnforcePort4500*/,
                                 isIpv4AfterNetworkChange)
                         .isInstance(mSpyIkeConnectionCtrl.getIkeSocket()));