Merge "Add Fragmented Packet Test to testTetherClatBpfOffloadUdp" into main

This commit is contained in:
Maciej Żenczykowski 2024-08-20 18:34:57 +00:00 • committed by Gerrit Code Review
commit cc6c9d3de7
3 changed files with 167 additions and 5 deletions

View file

@ -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<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,

View file

@ -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<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 @@ public final class TetheringTester {
}
@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 @@ 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<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.

View file

@ -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.