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.