Merge "EthernetTetheringTest: testTetherUdpV4Dns"

This commit is contained in:
Nucca Chen
2022-08-24 08:30:56 +00:00
committed by Gerrit Code Review
2 changed files with 331 additions and 5 deletions

View File

@@ -24,7 +24,9 @@ import static android.net.InetAddresses.parseNumericAddress;
import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
import static android.net.TetheringManager.TETHERING_ETHERNET;
import static android.net.TetheringTester.TestDnsPacket;
import static android.net.TetheringTester.isExpectedIcmpv6Packet;
import static android.net.TetheringTester.isExpectedUdpDnsPacket;
import static android.net.TetheringTester.isExpectedUdpPacket;
import static android.system.OsConstants.IPPROTO_IP;
import static android.system.OsConstants.IPPROTO_IPV6;
@@ -81,7 +83,9 @@ import com.android.net.module.util.bpf.Tether4Key;
import com.android.net.module.util.bpf.Tether4Value;
import com.android.net.module.util.bpf.TetherStatsKey;
import com.android.net.module.util.bpf.TetherStatsValue;
import com.android.net.module.util.structs.Ipv4Header;
import com.android.net.module.util.structs.Ipv6Header;
import com.android.net.module.util.structs.UdpHeader;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DeviceInfoUtils;
@@ -156,6 +160,8 @@ public class EthernetTetheringTest {
private static final ByteBuffer TEST_REACHABILITY_PAYLOAD =
ByteBuffer.wrap(new byte[] { (byte) 0x55, (byte) 0xaa });
private static final short DNS_PORT = 53;
private static final String DUMPSYS_TETHERING_RAWMAP_ARG = "bpfRawMap";
private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
@@ -165,6 +171,66 @@ public class EthernetTetheringTest {
private static final int VERSION_TRAFFICCLASS_FLOWLABEL = 0x60000000;
private static final short HOP_LIMIT = 0x40;
// TODO: use class DnsPacket to build DNS query and reply message once DnsPacket supports
// building packet for given arguments.
private static final ByteBuffer DNS_QUERY = ByteBuffer.wrap(new byte[] {
// scapy.DNS(
// id=0xbeef,
// qr=0,
// qd=scapy.DNSQR(qname="hello.example.com"))
//
/* Header */
(byte) 0xbe, (byte) 0xef, /* Transaction ID: 0xbeef */
(byte) 0x01, (byte) 0x00, /* Flags: rd */
(byte) 0x00, (byte) 0x01, /* Questions: 1 */
(byte) 0x00, (byte) 0x00, /* Answer RRs: 0 */
(byte) 0x00, (byte) 0x00, /* Authority RRs: 0 */
(byte) 0x00, (byte) 0x00, /* Additional RRs: 0 */
/* Queries */
(byte) 0x05, (byte) 0x68, (byte) 0x65, (byte) 0x6c,
(byte) 0x6c, (byte) 0x6f, (byte) 0x07, (byte) 0x65,
(byte) 0x78, (byte) 0x61, (byte) 0x6d, (byte) 0x70,
(byte) 0x6c, (byte) 0x65, (byte) 0x03, (byte) 0x63,
(byte) 0x6f, (byte) 0x6d, (byte) 0x00, /* Name: hello.example.com */
(byte) 0x00, (byte) 0x01, /* Type: A */
(byte) 0x00, (byte) 0x01 /* Class: IN */
});
private static final byte[] DNS_REPLY = new byte[] {
// scapy.DNS(
// id=0,
// qr=1,
// qd=scapy.DNSQR(qname="hello.example.com"),
// an=scapy.DNSRR(rrname="hello.example.com", rdata='1.2.3.4'))
//
/* Header */
(byte) 0x00, (byte) 0x00, /* Transaction ID: 0x0, must be updated by dns query id */
(byte) 0x81, (byte) 0x00, /* Flags: qr rd */
(byte) 0x00, (byte) 0x01, /* Questions: 1 */
(byte) 0x00, (byte) 0x01, /* Answer RRs: 1 */
(byte) 0x00, (byte) 0x00, /* Authority RRs: 0 */
(byte) 0x00, (byte) 0x00, /* Additional RRs: 0 */
/* Queries */
(byte) 0x05, (byte) 0x68, (byte) 0x65, (byte) 0x6c,
(byte) 0x6c, (byte) 0x6f, (byte) 0x07, (byte) 0x65,
(byte) 0x78, (byte) 0x61, (byte) 0x6d, (byte) 0x70,
(byte) 0x6c, (byte) 0x65, (byte) 0x03, (byte) 0x63,
(byte) 0x6f, (byte) 0x6d, (byte) 0x00, /* Name: hello.example.com */
(byte) 0x00, (byte) 0x01, /* Type: A */
(byte) 0x00, (byte) 0x01, /* Class: IN */
/* Answers */
(byte) 0x05, (byte) 0x68, (byte) 0x65, (byte) 0x6c,
(byte) 0x6c, (byte) 0x6f, (byte) 0x07, (byte) 0x65,
(byte) 0x78, (byte) 0x61, (byte) 0x6d, (byte) 0x70,
(byte) 0x6c, (byte) 0x65, (byte) 0x03, (byte) 0x63,
(byte) 0x6f, (byte) 0x6d, (byte) 0x00, /* Name: hello.example.com */
(byte) 0x00, (byte) 0x01, /* Type: A */
(byte) 0x00, (byte) 0x01, /* Class: IN */
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, /* Time to live: 0 */
(byte) 0x00, (byte) 0x04, /* Data length: 4 */
(byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04 /* Address: 1.2.3.4 */
};
private final Context mContext = InstrumentationRegistry.getContext();
private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class);
private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class);
@@ -1371,6 +1437,94 @@ public class EthernetTetheringTest {
runClatUdpTest();
}
@NonNull
private ByteBuffer buildDnsReplyMessageById(short id) {
byte[] replyMessage = Arrays.copyOf(DNS_REPLY, DNS_REPLY.length);
// Assign transaction id of reply message pattern with a given DNS transaction id.
replyMessage[0] = (byte) ((id >> 8) & 0xff);
replyMessage[1] = (byte) (id & 0xff);
Log.d(TAG, "Built DNS reply: " + dumpHexString(replyMessage));
return ByteBuffer.wrap(replyMessage);
}
@NonNull
private void sendDownloadPacketDnsV4(@NonNull final Inet4Address srcIp,
@NonNull final Inet4Address dstIp, short srcPort, short dstPort, short dnsId,
@NonNull final TetheringTester tester) throws Exception {
// DNS response transaction id must be copied from DNS query. Used by the requester
// to match up replies to outstanding queries. See RFC 1035 section 4.1.1.
final ByteBuffer dnsReplyMessage = buildDnsReplyMessageById(dnsId);
final ByteBuffer testPacket = buildUdpPacket((InetAddress) srcIp,
(InetAddress) dstIp, srcPort, dstPort, dnsReplyMessage);
tester.verifyDownload(testPacket, p -> {
Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
return isExpectedUdpDnsPacket(p, true /* hasEther */, true /* isIpv4 */,
dnsReplyMessage);
});
}
// Send IPv4 UDP DNS packet and return the forwarded DNS packet on upstream.
@NonNull
private byte[] sendUploadPacketDnsV4(@NonNull final MacAddress srcMac,
@NonNull final MacAddress dstMac, @NonNull final Inet4Address srcIp,
@NonNull final Inet4Address dstIp, short srcPort, short dstPort,
@NonNull final TetheringTester tester) throws Exception {
final ByteBuffer testPacket = buildUdpPacket(srcMac, dstMac, srcIp, dstIp,
srcPort, dstPort, DNS_QUERY);
return tester.verifyUpload(testPacket, p -> {
Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
return isExpectedUdpDnsPacket(p, false /* hasEther */, true /* isIpv4 */,
DNS_QUERY);
});
}
@Test
public void testTetherUdpV4Dns() throws Exception {
final TetheringTester tester = initTetheringTester(toList(TEST_IP4_ADDR),
toList(TEST_IP4_DNS));
final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, false /* hasIpv6 */);
// TODO: remove the connectivity verification for upstream connected notification race.
// See the same reason in runUdp4Test().
probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
// [1] Send DNS query.
// tethered device --> downstream --> dnsmasq forwarding --> upstream --> DNS server
//
// Need to extract DNS transaction id and source port from dnsmasq forwarded DNS query
// packet. dnsmasq forwarding creats new query which means UDP source port and DNS
// transaction id are changed from original sent DNS query. See forward_query() in
// external/dnsmasq/src/forward.c. Note that #TetheringTester.isExpectedUdpDnsPacket
// guarantees that |forwardedQueryPacket| is a valid DNS packet. So we can parse it as DNS
// packet.
final MacAddress srcMac = tethered.macAddr;
final MacAddress dstMac = tethered.routerMacAddr;
final Inet4Address clientIp = tethered.ipv4Addr;
final Inet4Address gatewayIp = tethered.ipv4Gatway;
final byte[] forwardedQueryPacket = sendUploadPacketDnsV4(srcMac, dstMac, clientIp,
gatewayIp, LOCAL_PORT, DNS_PORT, tester);
final ByteBuffer buf = ByteBuffer.wrap(forwardedQueryPacket);
Struct.parse(Ipv4Header.class, buf);
final UdpHeader udpHeader = Struct.parse(UdpHeader.class, buf);
final TestDnsPacket dnsQuery = TestDnsPacket.getTestDnsPacket(buf);
assertNotNull(dnsQuery);
Log.d(TAG, "Forwarded UDP source port: " + udpHeader.srcPort + ", DNS query id: "
+ dnsQuery.getHeader().id);
// [2] Send DNS reply.
// DNS server --> upstream --> dnsmasq forwarding --> downstream --> tethered device
//
// DNS reply transaction id must be copied from DNS query. Used by the requester to match
// up replies to outstanding queries. See RFC 1035 section 4.1.1.
final Inet4Address remoteIp = (Inet4Address) TEST_IP4_DNS;
final Inet4Address tetheringUpstreamIp = (Inet4Address) TEST_IP4_ADDR.getAddress();
sendDownloadPacketDnsV4(remoteIp, tetheringUpstreamIp, DNS_PORT,
(short) udpHeader.srcPort, (short) dnsQuery.getHeader().id, tester);
}
private <T> List<T> toList(T... array) {
return Arrays.asList(array);
}

View File

@@ -20,6 +20,11 @@ import static android.net.InetAddresses.parseNumericAddress;
import static android.system.OsConstants.IPPROTO_ICMPV6;
import static android.system.OsConstants.IPPROTO_UDP;
import static com.android.net.module.util.DnsPacket.ANSECTION;
import static com.android.net.module.util.DnsPacket.ARSECTION;
import static com.android.net.module.util.DnsPacket.NSSECTION;
import static com.android.net.module.util.DnsPacket.QDSECTION;
import static com.android.net.module.util.HexDump.dumpHexString;
import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
@@ -41,12 +46,14 @@ import static org.junit.Assert.fail;
import android.net.dhcp.DhcpAckPacket;
import android.net.dhcp.DhcpOfferPacket;
import android.net.dhcp.DhcpPacket;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.net.module.util.DnsPacket;
import com.android.net.module.util.Ipv6Utils;
import com.android.net.module.util.Struct;
import com.android.net.module.util.structs.EthernetHeader;
@@ -124,12 +131,14 @@ public final class TetheringTester {
public final MacAddress macAddr;
public final MacAddress routerMacAddr;
public final Inet4Address ipv4Addr;
public final Inet4Address ipv4Gatway;
public final Inet6Address ipv6Addr;
private TetheredDevice(MacAddress mac, boolean hasIpv6) throws Exception {
macAddr = mac;
DhcpResults dhcpResults = runDhcp(macAddr.toByteArray());
ipv4Addr = (Inet4Address) dhcpResults.ipAddress.getAddress();
ipv4Gatway = (Inet4Address) dhcpResults.gateway;
routerMacAddr = getRouterMacAddressFromArp(ipv4Addr, macAddr,
dhcpResults.serverAddress);
ipv6Addr = hasIpv6 ? runSlaac(macAddr, routerMacAddr) : null;
@@ -386,8 +395,8 @@ public final class TetheringTester {
}
}
public static boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEth,
boolean isIpv4, @NonNull final ByteBuffer payload) {
private static boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEth,
boolean isIpv4, Predicate<ByteBuffer> payloadVerifier) {
final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
try {
if (hasEth && !hasExpectedEtherHeader(buf, isIpv4)) return false;
@@ -395,15 +404,178 @@ public final class TetheringTester {
if (!hasExpectedIpHeader(buf, isIpv4, IPPROTO_UDP)) return false;
if (Struct.parse(UdpHeader.class, buf) == null) return false;
if (!payloadVerifier.test(buf)) return false;
} catch (Exception e) {
// Parsing packet fail means it is not udp packet.
return false;
}
return true;
}
if (buf.remaining() != payload.limit()) return false;
// Returns remaining bytes in the ByteBuffer in a new byte array of the right size. The
// ByteBuffer will be empty upon return. Used to avoid lint warning.
// See https://errorprone.info/bugpattern/ByteBufferBackingArray
private static byte[] getRemaining(final ByteBuffer buf) {
final byte[] bytes = new byte[buf.remaining()];
buf.get(bytes);
Log.d(TAG, "Get remaining bytes: " + dumpHexString(bytes));
return bytes;
}
return Arrays.equals(Arrays.copyOfRange(buf.array(), buf.position(), buf.limit()),
payload.array());
// |expectedPayload| is copied as read-only because the caller may reuse it.
public static boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEth,
boolean isIpv4, @NonNull final ByteBuffer expectedPayload) {
return isExpectedUdpPacket(rawPacket, hasEth, isIpv4, p -> {
if (p.remaining() != expectedPayload.limit()) return false;
return Arrays.equals(getRemaining(p), getRemaining(
expectedPayload.asReadOnlyBuffer()));
});
}
// |expectedPayload| is copied as read-only because the caller may reuse it.
// See hasExpectedDnsMessage.
public static boolean isExpectedUdpDnsPacket(@NonNull final byte[] rawPacket, boolean hasEth,
boolean isIpv4, @NonNull final ByteBuffer expectedPayload) {
return isExpectedUdpPacket(rawPacket, hasEth, isIpv4, p -> {
return hasExpectedDnsMessage(p, expectedPayload);
});
}
public static class TestDnsPacket extends DnsPacket {
TestDnsPacket(byte[] data) throws DnsPacket.ParseException {
super(data);
}
@Nullable
public static TestDnsPacket getTestDnsPacket(final ByteBuffer buf) {
try {
// The ByteBuffer will be empty upon return.
return new TestDnsPacket(getRemaining(buf));
} catch (DnsPacket.ParseException e) {
return null;
}
}
public DnsHeader getHeader() {
return mHeader;
}
public List<DnsRecord> getRecordList(int secType) {
return mRecords[secType];
}
public int getANCount() {
return mHeader.getRecordCount(ANSECTION);
}
public int getQDCount() {
return mHeader.getRecordCount(QDSECTION);
}
public int getNSCount() {
return mHeader.getRecordCount(NSSECTION);
}
public int getARCount() {
return mHeader.getRecordCount(ARSECTION);
}
private boolean isRecordsEquals(int type, @NonNull final TestDnsPacket other) {
List<DnsRecord> records = getRecordList(type);
List<DnsRecord> otherRecords = other.getRecordList(type);
if (records.size() != otherRecords.size()) return false;
// Expect that two compared resource records are in the same order. For current tests
// in EthernetTetheringTest, it is okay because dnsmasq doesn't reorder the forwarded
// resource records.
// TODO: consider allowing that compare records out of order.
for (int i = 0; i < records.size(); i++) {
// TODO: use DnsRecord.equals once aosp/1387135 is merged.
if (!TextUtils.equals(records.get(i).dName, otherRecords.get(i).dName)
|| records.get(i).nsType != otherRecords.get(i).nsType
|| records.get(i).nsClass != otherRecords.get(i).nsClass
|| records.get(i).ttl != otherRecords.get(i).ttl
|| !Arrays.equals(records.get(i).getRR(), otherRecords.get(i).getRR())) {
return false;
}
}
return true;
}
public boolean isQDRecordsEquals(@NonNull final TestDnsPacket other) {
return isRecordsEquals(QDSECTION, other);
}
public boolean isANRecordsEquals(@NonNull final TestDnsPacket other) {
return isRecordsEquals(ANSECTION, other);
}
}
// The ByteBuffer |actual| will be empty upon return. The ByteBuffer |excepted| will be copied
// as read-only because the caller may reuse it.
private static boolean hasExpectedDnsMessage(@NonNull final ByteBuffer actual,
@NonNull final ByteBuffer excepted) {
// Forwarded DNS message is extracted from remaining received packet buffer which has
// already parsed ethernet header, if any, IP header and UDP header.
final TestDnsPacket forwardedDns = TestDnsPacket.getTestDnsPacket(actual);
if (forwardedDns == null) return false;
// Original DNS message is the payload of the sending test UDP packet. It is used to check
// that the forwarded DNS query and reply have corresponding contents.
final TestDnsPacket originalDns = TestDnsPacket.getTestDnsPacket(
excepted.asReadOnlyBuffer());
assertNotNull(originalDns);
// Compare original DNS message which is sent to dnsmasq and forwarded DNS message which
// is forwarded by dnsmasq. The original message and forwarded message may be not identical
// because dnsmasq may change the header flags or even recreate the DNS query message and
// so on. We only simple check on forwarded packet and monitor if test will be broken by
// vendor dnsmasq customization. See forward_query() in external/dnsmasq/src/forward.c.
//
// DNS message format. See rfc1035 section 4.1.
// +---------------------+
// | Header |
// +---------------------+
// | Question | the question for the name server
// +---------------------+
// | Answer | RRs answering the question
// +---------------------+
// | Authority | RRs pointing toward an authority
// +---------------------+
// | Additional | RRs holding additional information
// +---------------------+
// [1] Header section. See rfc1035 section 4.1.1.
// Verify QR flag bit, QDCOUNT, ANCOUNT, NSCOUNT, ARCOUNT.
if (originalDns.getHeader().isResponse() != forwardedDns.getHeader().isResponse()) {
return false;
}
if (originalDns.getQDCount() != forwardedDns.getQDCount()) return false;
if (originalDns.getANCount() != forwardedDns.getANCount()) return false;
if (originalDns.getNSCount() != forwardedDns.getNSCount()) return false;
if (originalDns.getARCount() != forwardedDns.getARCount()) return false;
// [2] Question section. See rfc1035 section 4.1.2.
// Question section has at least one entry either DNS query or DNS reply.
if (forwardedDns.getRecordList(QDSECTION).isEmpty()) return false;
// Expect that original and forwarded message have the same question records (usually 1).
if (!originalDns.isQDRecordsEquals(forwardedDns)) return false;
// [3] Answer section. See rfc1035 section 4.1.3.
if (forwardedDns.getHeader().isResponse()) {
// DNS reply has at least have one answer in our tests.
// See EthernetTetheringTest#testTetherUdpV4Dns.
if (forwardedDns.getRecordList(ANSECTION).isEmpty()) return false;
// Expect that original and forwarded message have the same answer records.
if (!originalDns.isANRecordsEquals(forwardedDns)) return false;
}
// Ignore checking {Authority, Additional} sections because they are not tested
// in EthernetTetheringTest.
return true;
}
private void sendUploadPacket(ByteBuffer packet) throws Exception {