Add Fragmented Packet Test to testTetherClatBpfOffloadUdp

Extend testTetherClatBpfOffloadUdp to support fragmented IPv6 packets.
This verifies that downstream fragmented IPv6 UDP packets are
correctly processed by Clatd's BPF offload.

Bug: 285124667
Test: atest TetheringIntegrationTests
Change-Id: Id9c6cfac6fcedbc3615a5502e36851bff62e4dcc
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 3944a8a..1eb6255 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -26,7 +26,9 @@
 import static android.net.TetheringManager.TETHERING_ETHERNET;
 import static android.net.TetheringTester.buildTcpPacket;
 import static android.net.TetheringTester.buildUdpPacket;
+import static android.net.TetheringTester.buildUdpPackets;
 import static android.net.TetheringTester.isAddressIpv4;
+import static android.net.TetheringTester.isExpectedFragmentIpPacket;
 import static android.net.TetheringTester.isExpectedIcmpPacket;
 import static android.net.TetheringTester.isExpectedTcpPacket;
 import static android.net.TetheringTester.isExpectedUdpPacket;
@@ -58,12 +60,14 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.SystemClock;
+import android.util.ArrayMap;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.FragmentHeader;
 import com.android.net.module.util.structs.Ipv6Header;
 import com.android.testutils.HandlerUtils;
 import com.android.testutils.TapPacketReader;
@@ -678,6 +682,57 @@
         });
     }
 
+    protected void sendDownloadFragmentedUdpPackets(@NonNull final Inet6Address srcIp,
+            @NonNull final Inet6Address dstIp, @NonNull final TetheringTester tester,
+            @NonNull final ByteBuffer payload, int l2mtu) throws Exception {
+        final List<ByteBuffer> testPackets = buildUdpPackets(null /* srcMac */, null /* dstMac */,
+                srcIp, dstIp, REMOTE_PORT, LOCAL_PORT, payload, l2mtu);
+        assertTrue("No packet fragmentation occurs", testPackets.size() > 1);
+
+        short id = 0;
+        final ArrayMap<Short, ByteBuffer> fragmentPayloads = new ArrayMap<>();
+        for (ByteBuffer testPacket : testPackets) {
+            Struct.parse(Ipv6Header.class, testPacket);
+            final FragmentHeader fragmentHeader = Struct.parse(FragmentHeader.class, testPacket);
+            // Conversion of IPv6's fragmentOffset field to IPv4's flagsAndFragmentOffset field.
+            // IPv6 Fragment Header:
+            //   '13 bits of offset in multiples of 8' + 2 zero bits + more fragment bit
+            //      0                   1                   2                   3
+            //      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+            //     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            //     |  Next Header  |   Reserved    |      Fragment Offset    |Res|M|
+            //     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            //     |                         Identification                        |
+            //     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            // IPv4 Header:
+            //   zero bit + don't frag bit + more frag bit + '13 bits of offset in multiples of 8'
+            //      0                   1                   2                   3
+            //      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+            //     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            //     |Version|  IHL  |Type of Service|          Total Length         |
+            //     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            //     |         Identification        |Flags|      Fragment Offset    |
+            //     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            //     +                           . . .                               +
+            //     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            short offset = (short) (((fragmentHeader.fragmentOffset & 0x1) << 13)
+                    | (fragmentHeader.fragmentOffset >> 3));
+            // RFC6145: for fragment id, copied from the low-order 16 bits in the identification
+            //          field in the Fragment Header.
+            id = (short) (fragmentHeader.identification & 0xffff);
+            final byte[] fragmentPayload = new byte[testPacket.remaining()];
+            testPacket.get(fragmentPayload);
+            testPacket.flip();
+            fragmentPayloads.put(offset, ByteBuffer.wrap(fragmentPayload));
+        }
+
+        final short fragId = id;
+        tester.verifyDownloadBatch(testPackets, p -> {
+            Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
+            return isExpectedFragmentIpPacket(p, fragId, fragmentPayloads);
+        });
+    }
+
     protected void sendDownloadPacketTcp(@NonNull final InetAddress srcIp,
             @NonNull final InetAddress dstIp, short seq, short ack, byte tcpFlags,
             @NonNull final ByteBuffer payload, @NonNull final TetheringTester tester,
diff --git a/Tethering/tests/integration/base/android/net/TetheringTester.java b/Tethering/tests/integration/base/android/net/TetheringTester.java
index ae4ae55..b152b4c 100644
--- a/Tethering/tests/integration/base/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/base/android/net/TetheringTester.java
@@ -27,6 +27,7 @@
 import static android.system.OsConstants.IPPROTO_IPV6;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
+
 import static com.android.net.module.util.DnsPacket.ANSECTION;
 import static com.android.net.module.util.DnsPacket.DnsHeader;
 import static com.android.net.module.util.DnsPacket.DnsRecord;
@@ -53,6 +54,7 @@
 import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
 import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
 import static com.android.net.module.util.NetworkStackConstants.TCPHDR_SYN;
+
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
@@ -91,6 +93,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 import java.util.Random;
 import java.util.concurrent.TimeoutException;
 import java.util.function.Predicate;
@@ -497,6 +500,51 @@
         });
     }
 
+    /**
+     * Checks if the given raw packet data represents an expected fragmented IP packet.
+     *
+     * @param rawPacket the raw packet data to check.
+     * @param id the identification field of the fragmented IP packet.
+     * @param expectedPayloads a map of fragment offsets to their corresponding payload data.
+     * @return true if the packet is a valid fragmented IP packet with matching payload fragments;
+     *         false otherwise.
+     */
+    public static boolean isExpectedFragmentIpPacket(@NonNull final byte[] rawPacket, int id,
+            @NonNull final Map<Short, ByteBuffer> expectedPayloads) {
+        final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
+        try {
+            // Validate Ethernet header and IPv4 header.
+            if (!hasExpectedEtherHeader(buf, true /* isIpv4 */)) return false;
+            final Ipv4Header ipv4Header = Struct.parse(Ipv4Header.class, buf);
+            if (ipv4Header.protocol != (byte) IPPROTO_UDP) return false;
+            if (ipv4Header.id != id) return false;
+            // Validate payload data which expected at a specific fragment offset.
+            final ByteBuffer expectedPayload =
+                    expectedPayloads.get(ipv4Header.flagsAndFragmentOffset);
+            if (expectedPayload == null) return false;
+            if (buf.remaining() != expectedPayload.limit()) return false;
+            // Validate UDP header (which located in the 1st fragment).
+            // TODO: Validate the checksum field in UDP header. Currently, it'll be altered by NAT.
+            if ((ipv4Header.flagsAndFragmentOffset & 0x1FFF) == 0) {
+                final UdpHeader receivedUdpHeader = Struct.parse(UdpHeader.class, buf);
+                final UdpHeader expectedUdpHeader = Struct.parse(UdpHeader.class, expectedPayload);
+                if (receivedUdpHeader == null || expectedUdpHeader == null) return false;
+                if (receivedUdpHeader.srcPort != expectedUdpHeader.srcPort
+                        || receivedUdpHeader.dstPort != expectedUdpHeader.dstPort
+                        || receivedUdpHeader.length != expectedUdpHeader.length) {
+                    return false;
+                }
+                return true;
+            }
+            // Check the contents of the remaining payload.
+            return Arrays.equals(getRemaining(buf),
+                    getRemaining(expectedPayload.asReadOnlyBuffer()));
+        } catch (Exception e) {
+            // A failed packet parsing indicates that the packet is not a fragmented IPv4 packet.
+            return false;
+        }
+    }
+
     // |expectedPayload| is copied as read-only because the caller may reuse it.
     // See hasExpectedDnsMessage.
     public static boolean isExpectedUdpDnsPacket(@NonNull final byte[] rawPacket, boolean hasEth,
@@ -683,10 +731,10 @@
     }
 
     @NonNull
-    public static ByteBuffer buildUdpPacket(
+    public static List<ByteBuffer> buildUdpPackets(
             @Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
             @NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp,
-            short srcPort, short dstPort, @Nullable final ByteBuffer payload)
+            short srcPort, short dstPort, @Nullable final ByteBuffer payload, int l2mtu)
             throws Exception {
         final int ipProto = getIpProto(srcIp, dstIp);
         final boolean hasEther = (srcMac != null && dstMac != null);
@@ -720,7 +768,30 @@
             payload.clear();
         }
 
-        return packetBuilder.finalizePacket();
+        return l2mtu == 0
+                ? Arrays.asList(packetBuilder.finalizePacket())
+                : packetBuilder.finalizePacket(l2mtu);
+    }
+
+    /**
+     * Builds a UDP packet.
+     *
+     * @param srcMac the source MAC address.
+     * @param dstMac the destination MAC address.
+     * @param srcIp the source IP address.
+     * @param dstIp the destination IP address.
+     * @param srcPort the source port number.
+     * @param dstPort the destination port number.
+     * @param payload the optional payload data to be included in the packet.
+     * @return a ByteBuffer containing the constructed UDP packet.
+     */
+    @NonNull
+    public static ByteBuffer buildUdpPacket(
+            @Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
+            @NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp,
+            short srcPort, short dstPort, @Nullable final ByteBuffer payload)
+            throws Exception {
+        return buildUdpPackets(srcMac, dstMac, srcIp, dstIp, srcPort, dstPort, payload, 0).get(0);
     }
 
     @NonNull
@@ -994,6 +1065,28 @@
         return verifyPacketNotNull("Download fail", getDownloadPacket(filter));
     }
 
+    /**
+     * Sends a batch of download packets and verifies against a specified filtering condition.
+     *
+     * This method is designed for testing fragmented packets. All packets are sent before
+     * verification because the kernel buffers fragments until the last one is received.
+     * Captured packets are then verified against the provided filter.
+     *
+     * @param packets the list of ByteBuffers containing the packets to send.
+     * @param filter a Predicate that defines the filtering condition to apply to each received
+     *               packet. If the filter returns true for a packet's data, it is considered to
+     *               meet the verification criteria.
+     */
+    public void verifyDownloadBatch(final List<ByteBuffer> packets, final Predicate<byte[]> filter)
+            throws Exception {
+        for (ByteBuffer packet : packets) {
+            sendDownloadPacket(packet);
+        }
+        for (int i = 0; i < packets.size(); ++i) {
+            verifyPacketNotNull("Download fail", getDownloadPacket(filter));
+        }
+    }
+
     // Send DHCPDISCOVER to DHCP server to see if DHCP server is still alive to handle
     // the upcoming DHCP packets. This method should be only used when we know the DHCP
     // server has been created successfully before.
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index c8aab88..049f5f0 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -35,6 +35,7 @@
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
 import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN;
 import static com.android.testutils.DeviceInfoUtils.KVersion;
 import static com.android.testutils.TestPermissionUtil.runAsShell;
 
@@ -1130,12 +1131,25 @@
             sendDownloadPacketUdp(REMOTE_NAT64_ADDR, clatIp6, tester, true /* is6To4 */);
         }
 
+        // Send fragmented IPv6 UDP packets in the reply direction.
+        // IPv6 frament packet -- CLAT translation --> IPv4 fragment packet
+        final int payloadLen = 1500;
+        final int l2mtu = 1000;
+        final int fragPktCnt = 2; // 1500 bytes of UDP payload were fragmented into two packets.
+        final long fragRxBytes = payloadLen + UDP_HEADER_LEN + fragPktCnt * IPV4_HEADER_MIN_LEN;
+        final byte[] payload = new byte[payloadLen];
+        // Initialize the payload with random bytes.
+        Random random = new Random();
+        random.nextBytes(payload);
+        sendDownloadFragmentedUdpPackets(REMOTE_NAT64_ADDR, clatIp6, tester,
+                ByteBuffer.wrap(payload), l2mtu);
+
         // After sending test packets, get stats again to verify their differences.
         final ClatEgress4Value newEgress4 = getClatEgress4Value();
         final ClatIngress6Value newIngress6 = getClatIngress6Value();
 
-        assertEquals(RX_UDP_PACKET_COUNT, newIngress6.packets - oldIngress6.packets);
-        assertEquals(RX_UDP_PACKET_COUNT * RX_UDP_PACKET_SIZE,
+        assertEquals(RX_UDP_PACKET_COUNT + fragPktCnt, newIngress6.packets - oldIngress6.packets);
+        assertEquals(RX_UDP_PACKET_COUNT * RX_UDP_PACKET_SIZE + fragRxBytes,
                 newIngress6.bytes - oldIngress6.bytes);
         assertEquals(TX_UDP_PACKET_COUNT, newEgress4.packets - oldEgress4.packets);
         // The increase in egress traffic equals the expected size of the translated UDP packets.