[ST02] Add methods for synthesizing DNS packets
This re-submit aosp/1387135 but define TYPE_CNAME locally to
prevent from using non-finalize API.
Bug: 139774492
Test: atest NetworkStaticLibTests:com.android.net.moduletests.util.DnsPacketUtilsTest
Change-Id: Ib2e98292be994ca09845c6857b8884f9bcdaba80
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 4dfc752..f4997a1 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -96,7 +96,10 @@
visibility: [
"//packages/services/Iwlan:__subpackages__",
],
- libs: ["framework-annotations-lib"],
+ libs: [
+ "framework-annotations-lib",
+ "framework-connectivity.stubs.module_lib",
+ ],
}
filegroup {
diff --git a/staticlibs/framework/com/android/net/module/util/DnsPacket.java b/staticlibs/framework/com/android/net/module/util/DnsPacket.java
index 080781c..702d114 100644
--- a/staticlibs/framework/com/android/net/module/util/DnsPacket.java
+++ b/staticlibs/framework/com/android/net/module/util/DnsPacket.java
@@ -16,15 +16,34 @@
package com.android.net.module.util;
+import static android.net.DnsResolver.TYPE_A;
+import static android.net.DnsResolver.TYPE_AAAA;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+import static com.android.net.module.util.DnsPacketUtils.DnsRecordParser.domainNameToLabels;
+
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.text.TextUtils;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.DnsPacketUtils.DnsRecordParser;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.InetAddress;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
+import java.util.Objects;
/**
* Defines basic data for DNS protocol based on RFC 1035.
@@ -34,6 +53,12 @@
*/
public abstract class DnsPacket {
/**
+ * Type of the canonical name for an alias. Refer to RFC 1035 section 3.2.2.
+ */
+ // TODO: Define the constant as a public constant in DnsResolver since it can never change.
+ private static final int TYPE_CNAME = 5;
+
+ /**
* Thrown when parsing packet failed.
*/
public static class ParseException extends RuntimeException {
@@ -50,13 +75,29 @@
}
/**
- * DNS header for DNS protocol based on RFC 1035.
+ * DNS header for DNS protocol based on RFC 1035 section 4.1.1.
+ *
+ * 1 1 1 1 1 1
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+ * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ * | ID |
+ * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ * |QR| Opcode |AA|TC|RD|RA| Z | RCODE |
+ * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ * | QDCOUNT |
+ * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ * | ANCOUNT |
+ * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ * | NSCOUNT |
+ * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ * | ARCOUNT |
+ * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
*/
- public class DnsHeader {
+ public static class DnsHeader {
private static final String TAG = "DnsHeader";
- public final int id;
- public final int flags;
- public final int rcode;
+ private static final int SIZE_IN_BYTES = 12;
+ private final int mId;
+ private final int mFlags;
private final int[] mRecordCount;
/* If this bit in the 'flags' field is set to 0, the DNS message corresponding to this
@@ -73,10 +114,11 @@
* advanced to the end of the DNS header record.
* This is meant to chain with other methods reading a DNS response in sequence.
*/
- DnsHeader(@NonNull ByteBuffer buf) throws BufferUnderflowException {
- id = Short.toUnsignedInt(buf.getShort());
- flags = Short.toUnsignedInt(buf.getShort());
- rcode = flags & 0xF;
+ @VisibleForTesting
+ public DnsHeader(@NonNull ByteBuffer buf) throws BufferUnderflowException {
+ Objects.requireNonNull(buf);
+ mId = Short.toUnsignedInt(buf.getShort());
+ mFlags = Short.toUnsignedInt(buf.getShort());
mRecordCount = new int[NUM_SECTIONS];
for (int i = 0; i < NUM_SECTIONS; ++i) {
mRecordCount[i] = Short.toUnsignedInt(buf.getShort());
@@ -88,7 +130,23 @@
* RFC 1035 Section 4.1.1.
*/
public boolean isResponse() {
- return (flags & (1 << FLAGS_SECTION_QR_BIT)) != 0;
+ return (mFlags & (1 << FLAGS_SECTION_QR_BIT)) != 0;
+ }
+
+ /**
+ * Create a new DnsHeader from specified parameters.
+ *
+ * This constructor only builds the question and answer sections. Authority
+ * and additional sections are not supported. Useful when synthesizing dns
+ * responses from query or reply packets.
+ */
+ @VisibleForTesting
+ public DnsHeader(int id, int flags, int qdcount, int ancount) {
+ this.mId = id;
+ this.mFlags = flags;
+ mRecordCount = new int[NUM_SECTIONS];
+ mRecordCount[QDSECTION] = qdcount;
+ mRecordCount[ANSECTION] = ancount;
}
/**
@@ -97,15 +155,103 @@
public int getRecordCount(int type) {
return mRecordCount[type];
}
+
+ /**
+ * Get flags of this instance.
+ */
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Get id of this instance.
+ */
+ public int getId() {
+ return mId;
+ }
+
+ @Override
+ public String toString() {
+ return "DnsHeader{" + "id=" + mId + ", flags=" + mFlags
+ + ", recordCounts=" + Arrays.toString(mRecordCount) + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o.getClass() != getClass()) return false;
+ final DnsHeader other = (DnsHeader) o;
+ return mId == other.mId
+ && mFlags == other.mFlags
+ && Arrays.equals(mRecordCount, other.mRecordCount);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * mId + 37 * mFlags + Arrays.hashCode(mRecordCount);
+ }
+
+ /**
+ * Get DnsHeader as byte array.
+ */
+ @NonNull
+ public byte[] getBytes() {
+ // TODO: if this is called often, optimize the ByteBuffer out and write to the
+ // array directly.
+ final ByteBuffer buf = ByteBuffer.allocate(SIZE_IN_BYTES);
+ buf.putShort((short) mId);
+ buf.putShort((short) mFlags);
+ for (int i = 0; i < NUM_SECTIONS; ++i) {
+ buf.putShort((short) mRecordCount[i]);
+ }
+ return buf.array();
+ }
}
/**
* Superclass for DNS questions and DNS resource records.
*
- * DNS questions (No TTL/RDATA)
- * DNS resource records (With TTL/RDATA)
+ * DNS questions (No TTL/RDLENGTH/RDATA) based on RFC 1035 section 4.1.2.
+ * 1 1 1 1 1 1
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+ * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ * | |
+ * / QNAME /
+ * / /
+ * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ * | QTYPE |
+ * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ * | QCLASS |
+ * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ *
+ * DNS resource records (With TTL/RDLENGTH/RDATA) based on RFC 1035 section 4.1.3.
+ * 1 1 1 1 1 1
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+ * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ * | |
+ * / /
+ * / NAME /
+ * | |
+ * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ * | TYPE |
+ * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ * | CLASS |
+ * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ * | TTL |
+ * | |
+ * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ * | RDLENGTH |
+ * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
+ * / RDATA /
+ * / /
+ * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
*/
- public class DnsRecord {
+ // TODO: Make DnsResourceRecord and DnsQuestion subclasses of DnsRecord, and construct
+ // corresponding object from factory methods.
+ public static class DnsRecord {
+ // Refer to RFC 1035 section 2.3.4 for MAXNAMESIZE.
+ // NAME_NORMAL and NAME_COMPRESSION are used for checking name compression,
+ // refer to rfc 1035 section 4.1.4.
private static final int MAXNAMESIZE = 255;
public static final int NAME_NORMAL = 0;
public static final int NAME_COMPRESSION = 0xC0;
@@ -117,22 +263,31 @@
public final int nsClass;
public final long ttl;
private final byte[] mRdata;
+ /**
+ * Type of this DNS record.
+ */
+ @RecordType
+ public final int rType;
/**
* Create a new DnsRecord from a positioned ByteBuffer.
*
* Reads the passed ByteBuffer from its current position and decodes a DNS record.
* When this constructor returns, the reading position of the ByteBuffer has been
- * advanced to the end of the DNS header record.
+ * advanced to the end of the DNS resource record.
* This is meant to chain with other methods reading a DNS response in sequence.
*
+ * @param rType Type of the record.
* @param buf ByteBuffer input of record, must be in network byte order
* (which is the default).
*/
- DnsRecord(int recordType, @NonNull ByteBuffer buf)
+ @VisibleForTesting(visibility = PACKAGE)
+ public DnsRecord(@RecordType int rType, @NonNull ByteBuffer buf)
throws BufferUnderflowException, ParseException {
+ Objects.requireNonNull(buf);
+ this.rType = rType;
dName = DnsRecordParser.parseName(buf, 0 /* Parse depth */,
- /* isNameCompressionSupported= */ true);
+ true /* isNameCompressionSupported */);
if (dName.length() > MAXNAMESIZE) {
throw new ParseException(
"Parse name fail, name size is too long: " + dName.length());
@@ -140,7 +295,7 @@
nsType = Short.toUnsignedInt(buf.getShort());
nsClass = Short.toUnsignedInt(buf.getShort());
- if (recordType != QDSECTION) {
+ if (rType != QDSECTION) {
ttl = Integer.toUnsignedLong(buf.getInt());
final int length = Short.toUnsignedInt(buf.getShort());
mRdata = new byte[length];
@@ -152,6 +307,95 @@
}
/**
+ * Make an A or AAAA record based on the specified parameters.
+ *
+ * @param rType Type of the record, can be {@link #ANSECTION}, {@link #ARSECTION}
+ * or {@link #NSSECTION}.
+ * @param dName Domain name of the record.
+ * @param nsClass Class of the record. See RFC 1035 section 3.2.4.
+ * @param ttl time interval (in seconds) that the resource record may be
+ * cached before it should be discarded. Zero values are
+ * interpreted to mean that the RR can only be used for the
+ * transaction in progress, and should not be cached.
+ * @param address Instance of {@link InetAddress}
+ * @return A record if the {@code address} is an IPv4 address, or AAAA record if the
+ * {@code address} is an IPv6 address.
+ */
+ public static DnsRecord makeAOrAAAARecord(int rType, @NonNull String dName,
+ int nsClass, long ttl, @NonNull InetAddress address) throws IOException {
+ final int nsType = (address.getAddress().length == 4) ? TYPE_A : TYPE_AAAA;
+ return new DnsRecord(rType, dName, nsType, nsClass, ttl, address, null /* rDataStr */);
+ }
+
+ /**
+ * Make an CNAME record based on the specified parameters.
+ *
+ * @param rType Type of the record, can be {@link #ANSECTION}, {@link #ARSECTION}
+ * or {@link #NSSECTION}.
+ * @param dName Domain name of the record.
+ * @param nsClass Class of the record. See RFC 1035 section 3.2.4.
+ * @param ttl time interval (in seconds) that the resource record may be
+ * cached before it should be discarded. Zero values are
+ * interpreted to mean that the RR can only be used for the
+ * transaction in progress, and should not be cached.
+ * @param domainName Canonical name of the {@code dName}.
+ * @return A record if the {@code address} is an IPv4 address, or AAAA record if the
+ * {@code address} is an IPv6 address.
+ */
+ public static DnsRecord makeCNameRecord(int rType, @NonNull String dName, int nsClass,
+ long ttl, @NonNull String domainName) throws IOException {
+ return new DnsRecord(rType, dName, TYPE_CNAME, nsClass, ttl, null /* address */,
+ domainName);
+ }
+
+ /**
+ * Make a DNS question based on the specified parameters.
+ */
+ public static DnsRecord makeQuestion(@NonNull String dName, int nsType, int nsClass) {
+ return new DnsRecord(dName, nsType, nsClass);
+ }
+
+ private static String requireHostName(@NonNull String name) {
+ if (!DnsRecordParser.isHostName(name)) {
+ throw new IllegalArgumentException("Expected domain name but got " + name);
+ }
+ return name;
+ }
+
+ /**
+ * Create a new query DnsRecord from specified parameters, useful when synthesizing
+ * dns response.
+ */
+ private DnsRecord(@NonNull String dName, int nsType, int nsClass) {
+ this.rType = QDSECTION;
+ this.dName = requireHostName(dName);
+ this.nsType = nsType;
+ this.nsClass = nsClass;
+ mRdata = null;
+ this.ttl = 0;
+ }
+
+ /**
+ * Create a new CNAME/A/AAAA DnsRecord from specified parameters.
+ *
+ * @param address The address only used when synthesizing A or AAAA record.
+ * @param rDataStr The alias of the domain, only used when synthesizing CNAME record.
+ */
+ private DnsRecord(@RecordType int rType, @NonNull String dName, int nsType, int nsClass,
+ long ttl, @Nullable InetAddress address, @Nullable String rDataStr)
+ throws IOException {
+ this.rType = rType;
+ this.dName = requireHostName(dName);
+ this.nsType = nsType;
+ this.nsClass = nsClass;
+ if (rType < 0 || rType >= NUM_SECTIONS || rType == QDSECTION) {
+ throw new IllegalArgumentException("Unexpected record type: " + rType);
+ }
+ mRdata = nsType == TYPE_CNAME ? domainNameToLabels(rDataStr) : address.getAddress();
+ this.ttl = ttl;
+ }
+
+ /**
* Get a copy of rdata.
*/
@Nullable
@@ -159,13 +403,84 @@
return (mRdata == null) ? null : mRdata.clone();
}
+ /**
+ * Get DnsRecord as byte array.
+ */
+ @NonNull
+ public byte[] getBytes() throws IOException {
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ final DataOutputStream dos = new DataOutputStream(baos);
+ dos.write(domainNameToLabels(dName));
+ dos.writeShort(nsType);
+ dos.writeShort(nsClass);
+ if (rType != QDSECTION) {
+ dos.writeInt((int) ttl);
+ if (mRdata == null) {
+ dos.writeShort(0);
+ } else {
+ dos.writeShort(mRdata.length);
+ dos.write(mRdata);
+ }
+ }
+ return baos.toByteArray();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o.getClass() != getClass()) return false;
+ final DnsRecord other = (DnsRecord) o;
+ return rType == other.rType
+ && nsType == other.nsType
+ && nsClass == other.nsClass
+ && ttl == other.ttl
+ && TextUtils.equals(dName, other.dName)
+ && Arrays.equals(mRdata, other.mRdata);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * Objects.hash(dName)
+ + 37 * ((int) (ttl & 0xFFFFFFFF))
+ + 41 * ((int) (ttl >> 32))
+ + 43 * nsType
+ + 47 * nsClass
+ + 53 * rType
+ + Arrays.hashCode(mRdata);
+ }
+
+ @Override
+ public String toString() {
+ return "DnsRecord{"
+ + "rType=" + rType
+ + ", dName='" + dName + '\''
+ + ", nsType=" + nsType
+ + ", nsClass=" + nsClass
+ + ", ttl=" + ttl
+ + ", mRdata=" + Arrays.toString(mRdata)
+ + '}';
+ }
}
+ /**
+ * Header section types, refer to RFC 1035 section 4.1.1.
+ */
public static final int QDSECTION = 0;
public static final int ANSECTION = 1;
public static final int NSSECTION = 2;
public static final int ARSECTION = 3;
- private static final int NUM_SECTIONS = ARSECTION + 1;
+ @VisibleForTesting(visibility = PRIVATE)
+ static final int NUM_SECTIONS = ARSECTION + 1;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ QDSECTION,
+ ANSECTION,
+ NSSECTION,
+ ARSECTION,
+ })
+ public @interface RecordType {}
+
private static final String TAG = DnsPacket.class.getSimpleName();
@@ -189,9 +504,7 @@
for (int i = 0; i < NUM_SECTIONS; ++i) {
final int count = mHeader.getRecordCount(i);
- if (count > 0) {
- mRecords[i] = new ArrayList(count);
- }
+ mRecords[i] = new ArrayList(count);
for (int j = 0; j < count; ++j) {
try {
mRecords[i].add(new DnsRecord(i, buffer));
@@ -201,4 +514,61 @@
}
}
}
+
+ /**
+ * Create a new {@link #DnsPacket} from specified parameters.
+ *
+ * Note that authority records section and additional records section is not supported.
+ */
+ protected DnsPacket(@NonNull DnsHeader header, @NonNull List<DnsRecord> qd,
+ @NonNull List<DnsRecord> an) {
+ mHeader = Objects.requireNonNull(header);
+ mRecords = new List[NUM_SECTIONS];
+ mRecords[QDSECTION] = Collections.unmodifiableList(new ArrayList<>(qd));
+ mRecords[ANSECTION] = Collections.unmodifiableList(new ArrayList<>(an));
+ mRecords[NSSECTION] = new ArrayList<>();
+ mRecords[ARSECTION] = new ArrayList<>();
+ for (int i = 0; i < NUM_SECTIONS; i++) {
+ if (mHeader.mRecordCount[i] != mRecords[i].size()) {
+ throw new IllegalArgumentException("Record count mismatch: expected "
+ + mHeader.mRecordCount[i] + " but was " + mRecords[i]);
+ }
+ }
+ }
+
+ /**
+ * Get DnsPacket as byte array.
+ */
+ public @NonNull byte[] getBytes() throws IOException {
+ final ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ buf.write(mHeader.getBytes());
+
+ for (int i = 0; i < NUM_SECTIONS; ++i) {
+ for (final DnsRecord record : mRecords[i]) {
+ buf.write(record.getBytes());
+ }
+ }
+ return buf.toByteArray();
+ }
+
+ @Override
+ public String toString() {
+ return "DnsPacket{" + "header=" + mHeader + ", records='" + Arrays.toString(mRecords) + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o.getClass() != getClass()) return false;
+ final DnsPacket other = (DnsPacket) o;
+ return Objects.equals(mHeader, other.mHeader)
+ && Arrays.deepEquals(mRecords, other.mRecords);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(mHeader);
+ result = 31 * result + Arrays.hashCode(mRecords);
+ return result;
+ }
}
diff --git a/staticlibs/framework/com/android/net/module/util/DnsPacketUtils.java b/staticlibs/framework/com/android/net/module/util/DnsPacketUtils.java
index 3448cad..c47bfa0 100644
--- a/staticlibs/framework/com/android/net/module/util/DnsPacketUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/DnsPacketUtils.java
@@ -20,10 +20,19 @@
import static com.android.net.module.util.DnsPacket.DnsRecord.NAME_NORMAL;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.InetAddresses;
+import android.net.ParseException;
import android.text.TextUtils;
+import android.util.Patterns;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.text.FieldPosition;
@@ -38,6 +47,7 @@
*/
public static class DnsRecordParser {
private static final int MAXLABELSIZE = 63;
+ private static final int MAXNAMESIZE = 255;
private static final int MAXLABELCOUNT = 128;
private static final DecimalFormat sByteFormat = new DecimalFormat();
@@ -48,7 +58,8 @@
*
* <p>Follows the same conversion rules of the native code (ns_name.c in libc).
*/
- private static String labelToString(@NonNull byte[] label) {
+ @VisibleForTesting
+ static String labelToString(@NonNull byte[] label) {
final StringBuffer sb = new StringBuffer();
for (int i = 0; i < label.length; ++i) {
@@ -72,6 +83,52 @@
}
/**
+ * Converts domain name to labels according to RFC 1035.
+ *
+ * @param name Domain name as String that needs to be converted to labels.
+ * @return An encoded byte array that is constructed out of labels,
+ * and ends with zero-length label.
+ * @throws ParseException if failed to parse the given domain name or
+ * IOException if failed to output labels.
+ */
+ public static @NonNull byte[] domainNameToLabels(@NonNull String name) throws
+ IOException, ParseException {
+ if (name.length() > MAXNAMESIZE) {
+ throw new ParseException("Domain name exceeds max length: " + name.length());
+ }
+ if (!isHostName(name)) {
+ throw new ParseException("Failed to parse domain name: " + name);
+ }
+ final ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ final String[] labels = name.split("\\.");
+ for (final String label : labels) {
+ if (label.length() > MAXLABELSIZE) {
+ throw new ParseException("label is too long: " + label);
+ }
+ buf.write(label.length());
+ // Encode as UTF-8 as suggested in RFC 6055 section 3.
+ buf.write(label.getBytes(StandardCharsets.UTF_8));
+ }
+ buf.write(0x00); // end with zero-length label
+ return buf.toByteArray();
+ }
+
+ /**
+ * Check whether the input is a valid hostname based on rfc 1035 section 3.3.
+ *
+ * @param hostName the target host name.
+ * @return true if the input is a valid hostname.
+ */
+ public static boolean isHostName(@Nullable String hostName) {
+ // TODO: Use {@code Patterns.HOST_NAME} if available.
+ // Patterns.DOMAIN_NAME accepts host names or IP addresses, so reject
+ // IP addresses.
+ return hostName != null
+ && Patterns.DOMAIN_NAME.matcher(hostName).matches()
+ && !InetAddresses.isNumericAddress(hostName);
+ }
+
+ /**
* Parses the domain / target name of a DNS record.
*
* As described in RFC 1035 Section 4.1.3, the NAME field of a DNS Resource Record always
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketTest.java
index 1bbba08..409c1eb 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketTest.java
@@ -16,26 +16,45 @@
package com.android.net.module.util;
+import static android.net.DnsResolver.CLASS_IN;
+import static android.net.DnsResolver.TYPE_A;
+import static android.net.DnsResolver.TYPE_AAAA;
+
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import libcore.net.InetAddressUtils;
+
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class DnsPacketTest {
+ private static final int TEST_DNS_PACKET_ID = 0x7722;
+ private static final int TEST_DNS_PACKET_FLAGS = 0x8180;
+
private void assertHeaderParses(DnsPacket.DnsHeader header, int id, int flag,
int qCount, int aCount, int nsCount, int arCount) {
- assertEquals(header.id, id);
- assertEquals(header.flags, flag);
+ assertEquals(header.getId(), id);
+ assertEquals(header.getFlags(), flag);
assertEquals(header.getRecordCount(DnsPacket.QDSECTION), qCount);
assertEquals(header.getRecordCount(DnsPacket.ANSECTION), aCount);
assertEquals(header.getRecordCount(DnsPacket.NSSECTION), nsCount);
@@ -51,11 +70,16 @@
assertTrue(Arrays.equals(record.getRR(), rr));
}
- class TestDnsPacket extends DnsPacket {
+ static class TestDnsPacket extends DnsPacket {
TestDnsPacket(byte[] data) throws DnsPacket.ParseException {
super(data);
}
+ TestDnsPacket(@NonNull DnsHeader header, @Nullable ArrayList<DnsRecord> qd,
+ @Nullable ArrayList<DnsRecord> an) {
+ super(header, qd, an);
+ }
+
public DnsHeader getHeader() {
return mHeader;
}
@@ -156,4 +180,247 @@
new byte[]{ 0x24, 0x04, 0x68, 0x00, 0x40, 0x05, 0x08, 0x0d,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x04 });
}
+
+ /** Verifies that the synthesized {@link DnsPacket.DnsHeader} can be parsed correctly. */
+ @Test
+ public void testDnsHeaderSynthesize() {
+ final DnsPacket.DnsHeader testHeader = new DnsPacket.DnsHeader(TEST_DNS_PACKET_ID,
+ TEST_DNS_PACKET_FLAGS, 3 /* qcount */, 5 /* ancount */);
+ final DnsPacket.DnsHeader actualHeader = new DnsPacket.DnsHeader(
+ ByteBuffer.wrap(testHeader.getBytes()));
+ assertEquals(testHeader, actualHeader);
+ }
+
+ /** Verifies that the synthesized {@link DnsPacket.DnsRecord} can be parsed correctly. */
+ @Test
+ public void testDnsRecordSynthesize() throws IOException {
+ assertDnsRecordRoundTrip(
+ DnsPacket.DnsRecord.makeAOrAAAARecord(DnsPacket.ANSECTION,
+ "test.com", CLASS_IN, 5 /* ttl */,
+ InetAddressUtils.parseNumericAddress("abcd::fedc")));
+ assertDnsRecordRoundTrip(DnsPacket.DnsRecord.makeQuestion("test.com", TYPE_AAAA, CLASS_IN));
+ assertDnsRecordRoundTrip(DnsPacket.DnsRecord.makeCNameRecord(DnsPacket.ANSECTION,
+ "test.com", CLASS_IN, 0 /* ttl */, "example.com"));
+ }
+
+ /**
+ * Verifies ttl/rData error handling when parsing
+ * {@link DnsPacket.DnsRecord} from bytes.
+ */
+ @Test
+ public void testDnsRecordTTLRDataErrorHandling() throws IOException {
+ // Verify the constructor ignore ttl/rData of questions even if they are supplied.
+ final byte[] qdWithTTLRData = new byte[]{
+ 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */
+ 0x00, 0x00, /* Type */
+ 0x00, 0x01, /* Class */
+ 0x00, 0x00, 0x01, 0x2b, /* TTL */
+ 0x00, 0x04, /* Data length */
+ (byte) 0xac, (byte) 0xd9, (byte) 0xa1, (byte) 0x84 /* Address */};
+ final DnsPacket.DnsRecord questionsFromBytes =
+ new DnsPacket.DnsRecord(DnsPacket.QDSECTION, ByteBuffer.wrap(qdWithTTLRData));
+ assertEquals(0, questionsFromBytes.ttl);
+ assertNull(questionsFromBytes.getRR());
+
+ // Verify ANSECTION must have rData when constructing.
+ final byte[] anWithoutTTLRData = new byte[]{
+ 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */
+ 0x00, 0x01, /* Type */
+ 0x00, 0x01, /* Class */};
+ assertThrows(BufferUnderflowException.class, () ->
+ new DnsPacket.DnsRecord(DnsPacket.ANSECTION, ByteBuffer.wrap(anWithoutTTLRData)));
+ }
+
+ private void assertDnsRecordRoundTrip(DnsPacket.DnsRecord before)
+ throws IOException {
+ final DnsPacket.DnsRecord after = new DnsPacket.DnsRecord(before.rType,
+ ByteBuffer.wrap(before.getBytes()));
+ assertEquals(after, before);
+ }
+
+ /** Verifies that the synthesized {@link DnsPacket} can be parsed correctly. */
+ @Test
+ public void testDnsPacketSynthesize() throws IOException {
+ // Ipv4 dns response packet generated by scapy:
+ // dns_r = scapy.DNS(
+ // id=0xbeef,
+ // qr=1,
+ // qd=scapy.DNSQR(qname="hello.example.com"),
+ // an=scapy.DNSRR(rrname="hello.example.com", type="CNAME", rdata='test.com') /
+ // scapy.DNSRR(rrname="hello.example.com", rdata='1.2.3.4'))
+ // scapy.hexdump(dns_r)
+ // dns_r.show2()
+ // Note that since the synthesizing does not support name compression yet, the domain
+ // name of the sample need to be uncompressed when generating.
+ final byte[] v4BlobUncompressed = new byte[]{
+ /* Header */
+ (byte) 0xbe, (byte) 0xef, /* Transaction ID */
+ (byte) 0x81, 0x00, /* Flags */
+ 0x00, 0x01, /* Questions */
+ 0x00, 0x02, /* Answer RRs */
+ 0x00, 0x00, /* Authority RRs */
+ 0x00, 0x00, /* Additional RRs */
+ /* Queries */
+ 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x07, 0x65, 0x78, 0x61,
+ 0x6D, 0x70, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, /* Name: hello.example.com */
+ 0x00, 0x01, /* Type */
+ 0x00, 0x01, /* Class */
+ /* Answers */
+ 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x07, 0x65, 0x78, 0x61,
+ 0x6D, 0x70, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, /* Name: hello.example.com */
+ 0x00, 0x05, /* Type */
+ 0x00, 0x01, /* Class */
+ 0x00, 0x00, 0x00, 0x00, /* TTL */
+ 0x00, 0x0A, /* Data length */
+ 0x04, 0x74, 0x65, 0x73, 0x74, 0x03, 0x63, 0x6F, 0x6D, 0x00, /* Alias: test.com */
+ 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x07, 0x65, 0x78, 0x61,
+ 0x6D, 0x70, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, /* Name: hello.example.com */
+ 0x00, 0x01, /* Type */
+ 0x00, 0x01, /* Class */
+ 0x00, 0x00, 0x00, 0x00, /* TTL */
+ 0x00, 0x04, /* Data length */
+ 0x01, 0x02, 0x03, 0x04, /* Address: 1.2.3.4 */
+ };
+
+ // Forge one via constructors.
+ final DnsPacket.DnsHeader testHeader = new DnsPacket.DnsHeader(0xbeef,
+ 0x8100, 1 /* qcount */, 2 /* ancount */);
+ final ArrayList<DnsPacket.DnsRecord> qlist = new ArrayList<>();
+ final ArrayList<DnsPacket.DnsRecord> alist = new ArrayList<>();
+ qlist.add(DnsPacket.DnsRecord.makeQuestion(
+ "hello.example.com", TYPE_A, CLASS_IN));
+ alist.add(DnsPacket.DnsRecord.makeCNameRecord(
+ DnsPacket.ANSECTION, "hello.example.com", CLASS_IN, 0 /* ttl */, "test.com"));
+ alist.add(DnsPacket.DnsRecord.makeAOrAAAARecord(
+ DnsPacket.ANSECTION, "hello.example.com", CLASS_IN, 0 /* ttl */,
+ InetAddressUtils.parseNumericAddress("1.2.3.4")));
+ final TestDnsPacket testPacket = new TestDnsPacket(testHeader, qlist, alist);
+
+ // Assert content equals in both ways.
+ assertTrue(Arrays.equals(v4BlobUncompressed, testPacket.getBytes()));
+ assertEquals(new TestDnsPacket(v4BlobUncompressed), testPacket);
+ }
+
+ @Test
+ public void testDnsPacketSynthesize_recordCountMismatch() throws IOException {
+ final DnsPacket.DnsHeader testHeader = new DnsPacket.DnsHeader(0xbeef,
+ 0x8100, 1 /* qcount */, 1 /* ancount */);
+ final ArrayList<DnsPacket.DnsRecord> qlist = new ArrayList<>();
+ final ArrayList<DnsPacket.DnsRecord> alist = new ArrayList<>();
+ qlist.add(DnsPacket.DnsRecord.makeQuestion(
+ "hello.example.com", TYPE_A, CLASS_IN));
+
+ // Assert throws if the supplied answer records fewer than the declared count.
+ assertThrows(IllegalArgumentException.class, () ->
+ new TestDnsPacket(testHeader, qlist, alist));
+
+ // Assert throws if the supplied answer records more than the declared count.
+ alist.add(DnsPacket.DnsRecord.makeCNameRecord(
+ DnsPacket.ANSECTION, "hello.example.com", CLASS_IN, 0 /* ttl */, "test.com"));
+ alist.add(DnsPacket.DnsRecord.makeAOrAAAARecord(
+ DnsPacket.ANSECTION, "hello.example.com", CLASS_IN, 0 /* ttl */,
+ InetAddressUtils.parseNumericAddress("1.2.3.4")));
+ assertThrows(IllegalArgumentException.class, () ->
+ new TestDnsPacket(testHeader, qlist, alist));
+
+ // Assert counts matched if the byte buffer still has data when parsing ended.
+ final byte[] blobTooMuchData = new byte[]{
+ /* Header */
+ (byte) 0xbe, (byte) 0xef, /* Transaction ID */
+ (byte) 0x81, 0x00, /* Flags */
+ 0x00, 0x00, /* Questions */
+ 0x00, 0x00, /* Answer RRs */
+ 0x00, 0x00, /* Authority RRs */
+ 0x00, 0x00, /* Additional RRs */
+ /* Queries */
+ 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x07, 0x65, 0x78, 0x61,
+ 0x6D, 0x70, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, /* Name */
+ 0x00, 0x01, /* Type */
+ 0x00, 0x01, /* Class */
+ };
+ final TestDnsPacket packetFromTooMuchData = new TestDnsPacket(blobTooMuchData);
+ for (int i = 0; i < DnsPacket.NUM_SECTIONS; i++) {
+ assertEquals(0, packetFromTooMuchData.getRecordList(i).size());
+ assertEquals(0, packetFromTooMuchData.getHeader().getRecordCount(i));
+ }
+
+ // Assert throws if the byte buffer ended when expecting more records.
+ final byte[] blobNotEnoughData = new byte[]{
+ /* Header */
+ (byte) 0xbe, (byte) 0xef, /* Transaction ID */
+ (byte) 0x81, 0x00, /* Flags */
+ 0x00, 0x01, /* Questions */
+ 0x00, 0x02, /* Answer RRs */
+ 0x00, 0x00, /* Authority RRs */
+ 0x00, 0x00, /* Additional RRs */
+ /* Queries */
+ 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x07, 0x65, 0x78, 0x61,
+ 0x6D, 0x70, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, /* Name */
+ 0x00, 0x01, /* Type */
+ 0x00, 0x01, /* Class */
+ /* Answers */
+ 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x07, 0x65, 0x78, 0x61,
+ 0x6D, 0x70, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, /* Name */
+ 0x00, 0x01, /* Type */
+ 0x00, 0x01, /* Class */
+ 0x00, 0x00, 0x00, 0x00, /* TTL */
+ 0x00, 0x04, /* Data length */
+ 0x01, 0x02, 0x03, 0x04, /* Address */
+ };
+ assertThrows(DnsPacket.ParseException.class, () -> new TestDnsPacket(blobNotEnoughData));
+ }
+
+ @Test
+ public void testEqualsAndHashCode() throws IOException {
+ // Verify DnsHeader equals and hashCode.
+ final DnsPacket.DnsHeader testHeader = new DnsPacket.DnsHeader(TEST_DNS_PACKET_ID,
+ TEST_DNS_PACKET_FLAGS, 1 /* qcount */, 1 /* ancount */);
+ final DnsPacket.DnsHeader emptyHeader = new DnsPacket.DnsHeader(TEST_DNS_PACKET_ID + 1,
+ TEST_DNS_PACKET_FLAGS + 0x08, 0 /* qcount */, 0 /* ancount */);
+ final DnsPacket.DnsHeader headerFromBytes =
+ new DnsPacket.DnsHeader(ByteBuffer.wrap(testHeader.getBytes()));
+ assertEquals(testHeader, headerFromBytes);
+ assertEquals(testHeader.hashCode(), headerFromBytes.hashCode());
+ assertNotEquals(testHeader, emptyHeader);
+ assertNotEquals(testHeader.hashCode(), emptyHeader.hashCode());
+ assertNotEquals(headerFromBytes, emptyHeader);
+ assertNotEquals(headerFromBytes.hashCode(), emptyHeader.hashCode());
+
+ // Verify DnsRecord equals and hashCode.
+ final DnsPacket.DnsRecord testQuestion = DnsPacket.DnsRecord.makeQuestion(
+ "test.com", TYPE_AAAA, CLASS_IN);
+ final DnsPacket.DnsRecord testAnswer = DnsPacket.DnsRecord.makeCNameRecord(
+ DnsPacket.ANSECTION, "test.com", CLASS_IN, 9, "www.test.com");
+ final DnsPacket.DnsRecord questionFromBytes = new DnsPacket.DnsRecord(DnsPacket.QDSECTION,
+ ByteBuffer.wrap(testQuestion.getBytes()));
+ assertEquals(testQuestion, questionFromBytes);
+ assertEquals(testQuestion.hashCode(), questionFromBytes.hashCode());
+ assertNotEquals(testQuestion, testAnswer);
+ assertNotEquals(testQuestion.hashCode(), testAnswer.hashCode());
+ assertNotEquals(questionFromBytes, testAnswer);
+ assertNotEquals(questionFromBytes.hashCode(), testAnswer.hashCode());
+
+ // Verify DnsPacket equals and hashCode.
+ final ArrayList<DnsPacket.DnsRecord> qlist = new ArrayList<>();
+ final ArrayList<DnsPacket.DnsRecord> alist = new ArrayList<>();
+ qlist.add(testQuestion);
+ alist.add(testAnswer);
+ final TestDnsPacket testPacket = new TestDnsPacket(testHeader, qlist, alist);
+ final TestDnsPacket emptyPacket = new TestDnsPacket(
+ emptyHeader, new ArrayList<>(), new ArrayList<>());
+ final TestDnsPacket packetFromBytes = new TestDnsPacket(testPacket.getBytes());
+ assertEquals(testPacket, packetFromBytes);
+ assertEquals(testPacket.hashCode(), packetFromBytes.hashCode());
+ assertNotEquals(testPacket, emptyPacket);
+ assertNotEquals(testPacket.hashCode(), emptyPacket.hashCode());
+ assertNotEquals(packetFromBytes, emptyPacket);
+ assertNotEquals(packetFromBytes.hashCode(), emptyPacket.hashCode());
+
+ // Verify DnsPacket with empty list.
+ final TestDnsPacket emptyPacketFromBytes = new TestDnsPacket(emptyPacket.getBytes());
+ assertEquals(emptyPacket, emptyPacketFromBytes);
+ assertEquals(emptyPacket.hashCode(), emptyPacketFromBytes.hashCode());
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketUtilsTest.java
index 48777ac..9e1ab82 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketUtilsTest.java
@@ -18,12 +18,20 @@
import static com.android.net.module.util.DnsPacketUtils.DnsRecordParser;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
+import android.net.ParseException;
+import android.os.Build;
+
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.testutils.DevSdkIgnoreRule;
+
+import org.junit.Assert;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -32,6 +40,8 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public class DnsPacketUtilsTest {
+ @Rule
+ public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
/**
* Verifies that the compressed NAME field in the answer section of the DNS message is parsed
@@ -116,4 +126,31 @@
notNameCompressedBuf, /* depth= */ 0, /* isNameCompressionSupported= */ false);
assertEquals(domainName, "www.google.com");
}
+
+ // Skip test on R- devices since ParseException only available on S+ devices.
+ @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
+ public void testDomainNameToLabels() throws Exception {
+ assertArrayEquals(
+ new byte[]{3, 'w', 'w', 'w', 6, 'g', 'o', 'o', 'g', 'l', 'e', 3, 'c', 'o', 'm', 0},
+ DnsRecordParser.domainNameToLabels("www.google.com"));
+ assertThrows(ParseException.class, () ->
+ DnsRecordParser.domainNameToLabels("aaa."));
+ assertThrows(ParseException.class, () ->
+ DnsRecordParser.domainNameToLabels("aaa"));
+ assertThrows(ParseException.class, () ->
+ DnsRecordParser.domainNameToLabels("."));
+ assertThrows(ParseException.class, () ->
+ DnsRecordParser.domainNameToLabels(""));
+ }
+
+ @Test
+ public void testIsHostName() {
+ Assert.assertTrue(DnsRecordParser.isHostName("www.google.com"));
+ Assert.assertFalse(DnsRecordParser.isHostName("com"));
+ Assert.assertFalse(DnsRecordParser.isHostName("1.2.3.4"));
+ Assert.assertFalse(DnsRecordParser.isHostName("1234::5678"));
+ Assert.assertFalse(DnsRecordParser.isHostName(null));
+ Assert.assertFalse(DnsRecordParser.isHostName(""));
+ }
}