diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java index 6014722738..876b945616 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.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 List 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 4d90d398fb..48a406c335 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.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 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 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 records = getRecordList(type); + List 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 {