summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--common/src_frameworkcommon/com/android/net/module/util/DnsPacket.java252
-rw-r--r--common/tests/unit/src/com/android/module/util/DnsPacketTest.java159
2 files changed, 411 insertions, 0 deletions
diff --git a/common/src_frameworkcommon/com/android/net/module/util/DnsPacket.java b/common/src_frameworkcommon/com/android/net/module/util/DnsPacket.java
new file mode 100644
index 00000000..ac337e01
--- /dev/null
+++ b/common/src_frameworkcommon/com/android/net/module/util/DnsPacket.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.text.TextUtils;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.text.DecimalFormat;
+import java.text.FieldPosition;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Defines basic data for DNS protocol based on RFC 1035.
+ * Subclasses create the specific format used in DNS packet.
+ *
+ * @hide
+ */
+public abstract class DnsPacket {
+ /**
+ * Thrown when parsing packet failed.
+ */
+ public static class ParseException extends RuntimeException {
+ public String reason;
+ public ParseException(@NonNull String reason) {
+ super(reason);
+ this.reason = reason;
+ }
+
+ public ParseException(@NonNull String reason, @NonNull Throwable cause) {
+ super(reason, cause);
+ this.reason = reason;
+ }
+ }
+
+ /**
+ * DNS header for DNS protocol based on RFC 1035.
+ */
+ public class DnsHeader {
+ private static final String TAG = "DnsHeader";
+ public final int id;
+ public final int flags;
+ public final int rcode;
+ private final int[] mRecordCount;
+
+ /**
+ * Create a new DnsHeader from a positioned ByteBuffer.
+ *
+ * The ByteBuffer must be in network byte order (which is the default).
+ * Reads the passed ByteBuffer from its current position and decodes a DNS header.
+ * When this constructor returns, the reading position of the ByteBuffer has been
+ * 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;
+ mRecordCount = new int[NUM_SECTIONS];
+ for (int i = 0; i < NUM_SECTIONS; ++i) {
+ mRecordCount[i] = Short.toUnsignedInt(buf.getShort());
+ }
+ }
+
+ /**
+ * Get record count by type.
+ */
+ public int getRecordCount(int type) {
+ return mRecordCount[type];
+ }
+ }
+
+ /**
+ * Superclass for DNS questions and DNS resource records.
+ *
+ * DNS questions (No TTL/RDATA)
+ * DNS resource records (With TTL/RDATA)
+ */
+ public class DnsRecord {
+ private static final int MAXNAMESIZE = 255;
+ private static final int MAXLABELSIZE = 63;
+ private static final int MAXLABELCOUNT = 128;
+ private static final int NAME_NORMAL = 0;
+ private static final int NAME_COMPRESSION = 0xC0;
+ private final DecimalFormat mByteFormat = new DecimalFormat();
+ private final FieldPosition mPos = new FieldPosition(0);
+
+ private static final String TAG = "DnsRecord";
+
+ public final String dName;
+ public final int nsType;
+ public final int nsClass;
+ public final long ttl;
+ private final byte[] mRdata;
+
+ /**
+ * 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.
+ * This is meant to chain with other methods reading a DNS response in sequence.
+ *
+ * @param ByteBuffer input of record, must be in network byte order
+ * (which is the default).
+ */
+ DnsRecord(int recordType, @NonNull ByteBuffer buf)
+ throws BufferUnderflowException, ParseException {
+ dName = parseName(buf, 0 /* Parse depth */);
+ if (dName.length() > MAXNAMESIZE) {
+ throw new ParseException(
+ "Parse name fail, name size is too long: " + dName.length());
+ }
+ nsType = Short.toUnsignedInt(buf.getShort());
+ nsClass = Short.toUnsignedInt(buf.getShort());
+
+ if (recordType != QDSECTION) {
+ ttl = Integer.toUnsignedLong(buf.getInt());
+ final int length = Short.toUnsignedInt(buf.getShort());
+ mRdata = new byte[length];
+ buf.get(mRdata);
+ } else {
+ ttl = 0;
+ mRdata = null;
+ }
+ }
+
+ /**
+ * Get a copy of rdata.
+ */
+ @Nullable
+ public byte[] getRR() {
+ return (mRdata == null) ? null : mRdata.clone();
+ }
+
+ /**
+ * Convert label from {@code byte[]} to {@code String}
+ *
+ * Follows the same conversion rules of the native code (ns_name.c in libc)
+ */
+ private String labelToString(@NonNull byte[] label) {
+ final StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < label.length; ++i) {
+ int b = Byte.toUnsignedInt(label[i]);
+ // Control characters and non-ASCII characters.
+ if (b <= 0x20 || b >= 0x7f) {
+ // Append the byte as an escaped decimal number, e.g., "\19" for 0x13.
+ sb.append('\\');
+ mByteFormat.format(b, sb, mPos);
+ } else if (b == '"' || b == '.' || b == ';' || b == '\\'
+ || b == '(' || b == ')' || b == '@' || b == '$') {
+ // Append the byte as an escaped character, e.g., "\:" for 0x3a.
+ sb.append('\\');
+ sb.append((char) b);
+ } else {
+ // Append the byte as a character, e.g., "a" for 0x61.
+ sb.append((char) b);
+ }
+ }
+ return sb.toString();
+ }
+
+ private String parseName(@NonNull ByteBuffer buf, int depth) throws
+ BufferUnderflowException, ParseException {
+ if (depth > MAXLABELCOUNT) {
+ throw new ParseException("Failed to parse name, too many labels");
+ }
+ final int len = Byte.toUnsignedInt(buf.get());
+ final int mask = len & NAME_COMPRESSION;
+ if (0 == len) {
+ return "";
+ } else if (mask != NAME_NORMAL && mask != NAME_COMPRESSION) {
+ throw new ParseException("Parse name fail, bad label type");
+ } else if (mask == NAME_COMPRESSION) {
+ // Name compression based on RFC 1035 - 4.1.4 Message compression
+ final int offset = ((len & ~NAME_COMPRESSION) << 8) + Byte.toUnsignedInt(buf.get());
+ final int oldPos = buf.position();
+ if (offset >= oldPos - 2) {
+ throw new ParseException("Parse compression name fail, invalid compression");
+ }
+ buf.position(offset);
+ final String pointed = parseName(buf, depth + 1);
+ buf.position(oldPos);
+ return pointed;
+ } else {
+ final byte[] label = new byte[len];
+ buf.get(label);
+ final String head = labelToString(label);
+ if (head.length() > MAXLABELSIZE) {
+ throw new ParseException("Parse name fail, invalid label length");
+ }
+ final String tail = parseName(buf, depth + 1);
+ return TextUtils.isEmpty(tail) ? head : head + "." + tail;
+ }
+ }
+ }
+
+ 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;
+
+ private static final String TAG = DnsPacket.class.getSimpleName();
+
+ protected final DnsHeader mHeader;
+ protected final List<DnsRecord>[] mRecords;
+
+ protected DnsPacket(@NonNull byte[] data) throws ParseException {
+ if (null == data) throw new ParseException("Parse header failed, null input data");
+ final ByteBuffer buffer;
+ try {
+ buffer = ByteBuffer.wrap(data);
+ mHeader = new DnsHeader(buffer);
+ } catch (BufferUnderflowException e) {
+ throw new ParseException("Parse Header fail, bad input data", e);
+ }
+
+ mRecords = new ArrayList[NUM_SECTIONS];
+
+ for (int i = 0; i < NUM_SECTIONS; ++i) {
+ final int count = mHeader.getRecordCount(i);
+ if (count > 0) {
+ mRecords[i] = new ArrayList(count);
+ }
+ for (int j = 0; j < count; ++j) {
+ try {
+ mRecords[i].add(new DnsRecord(i, buffer));
+ } catch (BufferUnderflowException e) {
+ throw new ParseException("Parse record fail", e);
+ }
+ }
+ }
+ }
+}
diff --git a/common/tests/unit/src/com/android/module/util/DnsPacketTest.java b/common/tests/unit/src/com/android/module/util/DnsPacketTest.java
new file mode 100644
index 00000000..1bbba088
--- /dev/null
+++ b/common/tests/unit/src/com/android/module/util/DnsPacketTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DnsPacketTest {
+ 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.getRecordCount(DnsPacket.QDSECTION), qCount);
+ assertEquals(header.getRecordCount(DnsPacket.ANSECTION), aCount);
+ assertEquals(header.getRecordCount(DnsPacket.NSSECTION), nsCount);
+ assertEquals(header.getRecordCount(DnsPacket.ARSECTION), arCount);
+ }
+
+ private void assertRecordParses(DnsPacket.DnsRecord record, String dname,
+ int dtype, int dclass, int ttl, byte[] rr) {
+ assertEquals(record.dName, dname);
+ assertEquals(record.nsType, dtype);
+ assertEquals(record.nsClass, dclass);
+ assertEquals(record.ttl, ttl);
+ assertTrue(Arrays.equals(record.getRR(), rr));
+ }
+
+ class TestDnsPacket extends DnsPacket {
+ TestDnsPacket(byte[] data) throws DnsPacket.ParseException {
+ super(data);
+ }
+
+ public DnsHeader getHeader() {
+ return mHeader;
+ }
+ public List<DnsRecord> getRecordList(int secType) {
+ return mRecords[secType];
+ }
+ }
+
+ @Test
+ public void testNullDisallowed() {
+ try {
+ new TestDnsPacket(null);
+ fail("Exception not thrown for null byte array");
+ } catch (DnsPacket.ParseException e) {
+ }
+ }
+
+ @Test
+ public void testV4Answer() throws Exception {
+ final byte[] v4blob = new byte[] {
+ /* Header */
+ 0x55, 0x66, /* Transaction ID */
+ (byte) 0x81, (byte) 0x80, /* Flags */
+ 0x00, 0x01, /* Questions */
+ 0x00, 0x01, /* Answer RRs */
+ 0x00, 0x00, /* Authority RRs */
+ 0x00, 0x00, /* Additional RRs */
+ /* Queries */
+ 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */
+ 0x00, 0x01, /* Type */
+ 0x00, 0x01, /* Class */
+ /* Answers */
+ (byte) 0xc0, 0x0c, /* Name */
+ 0x00, 0x01, /* Type */
+ 0x00, 0x01, /* Class */
+ 0x00, 0x00, 0x01, 0x2b, /* TTL */
+ 0x00, 0x04, /* Data length */
+ (byte) 0xac, (byte) 0xd9, (byte) 0xa1, (byte) 0x84 /* Address */
+ };
+ TestDnsPacket packet = new TestDnsPacket(v4blob);
+
+ // Header part
+ assertHeaderParses(packet.getHeader(), 0x5566, 0x8180, 1, 1, 0, 0);
+
+ // Record part
+ List<DnsPacket.DnsRecord> qdRecordList =
+ packet.getRecordList(DnsPacket.QDSECTION);
+ assertEquals(qdRecordList.size(), 1);
+ assertRecordParses(qdRecordList.get(0), "www.google.com", 1, 1, 0, null);
+
+ List<DnsPacket.DnsRecord> anRecordList =
+ packet.getRecordList(DnsPacket.ANSECTION);
+ assertEquals(anRecordList.size(), 1);
+ assertRecordParses(anRecordList.get(0), "www.google.com", 1, 1, 0x12b,
+ new byte[]{ (byte) 0xac, (byte) 0xd9, (byte) 0xa1, (byte) 0x84 });
+ }
+
+ @Test
+ public void testV6Answer() throws Exception {
+ final byte[] v6blob = new byte[] {
+ /* Header */
+ 0x77, 0x22, /* Transaction ID */
+ (byte) 0x81, (byte) 0x80, /* Flags */
+ 0x00, 0x01, /* Questions */
+ 0x00, 0x01, /* Answer RRs */
+ 0x00, 0x00, /* Authority RRs */
+ 0x00, 0x00, /* Additional RRs */
+ /* Queries */
+ 0x03, 0x77, 0x77, 0x77, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6c, 0x65,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00, /* Name */
+ 0x00, 0x1c, /* Type */
+ 0x00, 0x01, /* Class */
+ /* Answers */
+ (byte) 0xc0, 0x0c, /* Name */
+ 0x00, 0x1c, /* Type */
+ 0x00, 0x01, /* Class */
+ 0x00, 0x00, 0x00, 0x37, /* TTL */
+ 0x00, 0x10, /* Data length */
+ 0x24, 0x04, 0x68, 0x00, 0x40, 0x05, 0x08, 0x0d,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x04 /* Address */
+ };
+ TestDnsPacket packet = new TestDnsPacket(v6blob);
+
+ // Header part
+ assertHeaderParses(packet.getHeader(), 0x7722, 0x8180, 1, 1, 0, 0);
+
+ // Record part
+ List<DnsPacket.DnsRecord> qdRecordList =
+ packet.getRecordList(DnsPacket.QDSECTION);
+ assertEquals(qdRecordList.size(), 1);
+ assertRecordParses(qdRecordList.get(0), "www.google.com", 28, 1, 0, null);
+
+ List<DnsPacket.DnsRecord> anRecordList =
+ packet.getRecordList(DnsPacket.ANSECTION);
+ assertEquals(anRecordList.size(), 1);
+ assertRecordParses(anRecordList.get(0), "www.google.com", 28, 1, 0x37,
+ new byte[]{ 0x24, 0x04, 0x68, 0x00, 0x40, 0x05, 0x08, 0x0d,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x04 });
+ }
+}