EthernetTetheringTest: testTetherUdpV4Dns

Add a tethering IPv4 DNS test.

DNS query:
tethered device --> downstream --> dnsmasq forwarding --> upstream --> DNS server

DNS reply:
DNS server --> upstream --> dnsmasq forwarding --> downstream --> tethered device

Bug: 237369591
Test: atest EthernetTetheringTest
Change-Id: I2baa9d7ccf55d117f644c80e867fb8272f7daac5
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index c9b3686..4c08b27 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -24,7 +24,9 @@
 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;
@@ -83,7 +85,9 @@
 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;
@@ -159,6 +163,8 @@
     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";
@@ -168,6 +174,66 @@
     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);
@@ -1399,6 +1465,94 @@
         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);
     }
diff --git a/Tethering/tests/integration/src/android/net/TetheringTester.java b/Tethering/tests/integration/src/android/net/TetheringTester.java
index 4d90d39..48a406c 100644
--- a/Tethering/tests/integration/src/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/src/android/net/TetheringTester.java
@@ -20,6 +20,11 @@
 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 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 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 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 @@
             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 {