[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(""));
+    }
 }