diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java index 3944a8a9bb..1eb6255668 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.CONNECTIVITY_SCOPE_LOCAL; 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.net.cts.util.CtsNetUtils; 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 @@ public abstract class EthernetTetheringTestBase { }); } + 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 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 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 ae4ae55098..b152b4c08e 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_IP; 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.IPV6_ADDR_ALL_NO 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.nio.ByteBuffer; 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 @@ public final class TetheringTester { }); } + /** + * 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 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 @@ public final class TetheringTester { } @NonNull - public static ByteBuffer buildUdpPacket( + public static List 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 @@ public final class TetheringTester { 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 @@ public final class TetheringTester { 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 packets, final Predicate 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 c8aab88467..049f5f07d8 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_REPL 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 @@ public class EthernetTetheringTest extends EthernetTetheringTestBase { 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.