1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.net.module.util;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.text.TextUtils;
22 
23 import java.nio.BufferUnderflowException;
24 import java.nio.ByteBuffer;
25 import java.text.DecimalFormat;
26 import java.text.FieldPosition;
27 import java.util.ArrayList;
28 import java.util.List;
29 
30 /**
31  * Defines basic data for DNS protocol based on RFC 1035.
32  * Subclasses create the specific format used in DNS packet.
33  *
34  * @hide
35  */
36 public abstract class DnsPacket {
37     /**
38      * Thrown when parsing packet failed.
39      */
40     public static class ParseException extends RuntimeException {
41         public String reason;
ParseException(@onNull String reason)42         public ParseException(@NonNull String reason) {
43             super(reason);
44             this.reason = reason;
45         }
46 
ParseException(@onNull String reason, @NonNull Throwable cause)47         public ParseException(@NonNull String reason, @NonNull Throwable cause) {
48             super(reason, cause);
49             this.reason = reason;
50         }
51     }
52 
53     /**
54      * DNS header for DNS protocol based on RFC 1035.
55      */
56     public class DnsHeader {
57         private static final String TAG = "DnsHeader";
58         public final int id;
59         public final int flags;
60         public final int rcode;
61         private final int[] mRecordCount;
62 
63         /**
64          * Create a new DnsHeader from a positioned ByteBuffer.
65          *
66          * The ByteBuffer must be in network byte order (which is the default).
67          * Reads the passed ByteBuffer from its current position and decodes a DNS header.
68          * When this constructor returns, the reading position of the ByteBuffer has been
69          * advanced to the end of the DNS header record.
70          * This is meant to chain with other methods reading a DNS response in sequence.
71          */
DnsHeader(@onNull ByteBuffer buf)72         DnsHeader(@NonNull ByteBuffer buf) throws BufferUnderflowException {
73             id = Short.toUnsignedInt(buf.getShort());
74             flags = Short.toUnsignedInt(buf.getShort());
75             rcode = flags & 0xF;
76             mRecordCount = new int[NUM_SECTIONS];
77             for (int i = 0; i < NUM_SECTIONS; ++i) {
78                 mRecordCount[i] = Short.toUnsignedInt(buf.getShort());
79             }
80         }
81 
82         /**
83          * Get record count by type.
84          */
getRecordCount(int type)85         public int getRecordCount(int type) {
86             return mRecordCount[type];
87         }
88     }
89 
90     /**
91      * Superclass for DNS questions and DNS resource records.
92      *
93      * DNS questions (No TTL/RDATA)
94      * DNS resource records (With TTL/RDATA)
95      */
96     public class DnsRecord {
97         private static final int MAXNAMESIZE = 255;
98         private static final int MAXLABELSIZE = 63;
99         private static final int MAXLABELCOUNT = 128;
100         private static final int NAME_NORMAL = 0;
101         private static final int NAME_COMPRESSION = 0xC0;
102         private final DecimalFormat mByteFormat = new DecimalFormat();
103         private final FieldPosition mPos = new FieldPosition(0);
104 
105         private static final String TAG = "DnsRecord";
106 
107         public final String dName;
108         public final int nsType;
109         public final int nsClass;
110         public final long ttl;
111         private final byte[] mRdata;
112 
113         /**
114          * Create a new DnsRecord from a positioned ByteBuffer.
115          *
116          * Reads the passed ByteBuffer from its current position and decodes a DNS record.
117          * When this constructor returns, the reading position of the ByteBuffer has been
118          * advanced to the end of the DNS header record.
119          * This is meant to chain with other methods reading a DNS response in sequence.
120          *
121          * @param ByteBuffer input of record, must be in network byte order
122          *         (which is the default).
123          */
DnsRecord(int recordType, @NonNull ByteBuffer buf)124         DnsRecord(int recordType, @NonNull ByteBuffer buf)
125                 throws BufferUnderflowException, ParseException {
126             dName = parseName(buf, 0 /* Parse depth */);
127             if (dName.length() > MAXNAMESIZE) {
128                 throw new ParseException(
129                         "Parse name fail, name size is too long: " + dName.length());
130             }
131             nsType = Short.toUnsignedInt(buf.getShort());
132             nsClass = Short.toUnsignedInt(buf.getShort());
133 
134             if (recordType != QDSECTION) {
135                 ttl = Integer.toUnsignedLong(buf.getInt());
136                 final int length = Short.toUnsignedInt(buf.getShort());
137                 mRdata = new byte[length];
138                 buf.get(mRdata);
139             } else {
140                 ttl = 0;
141                 mRdata = null;
142             }
143         }
144 
145         /**
146          * Get a copy of rdata.
147          */
148         @Nullable
getRR()149         public byte[] getRR() {
150             return (mRdata == null) ? null : mRdata.clone();
151         }
152 
153         /**
154          * Convert label from {@code byte[]} to {@code String}
155          *
156          * Follows the same conversion rules of the native code (ns_name.c in libc)
157          */
labelToString(@onNull byte[] label)158         private String labelToString(@NonNull byte[] label) {
159             final StringBuffer sb = new StringBuffer();
160             for (int i = 0; i < label.length; ++i) {
161                 int b = Byte.toUnsignedInt(label[i]);
162                 // Control characters and non-ASCII characters.
163                 if (b <= 0x20 || b >= 0x7f) {
164                     // Append the byte as an escaped decimal number, e.g., "\19" for 0x13.
165                     sb.append('\\');
166                     mByteFormat.format(b, sb, mPos);
167                 } else if (b == '"' || b == '.' || b == ';' || b == '\\'
168                         || b == '(' || b == ')' || b == '@' || b == '$') {
169                     // Append the byte as an escaped character, e.g., "\:" for 0x3a.
170                     sb.append('\\');
171                     sb.append((char) b);
172                 } else {
173                     // Append the byte as a character, e.g., "a" for 0x61.
174                     sb.append((char) b);
175                 }
176             }
177             return sb.toString();
178         }
179 
parseName(@onNull ByteBuffer buf, int depth)180         private String parseName(@NonNull ByteBuffer buf, int depth) throws
181                 BufferUnderflowException, ParseException {
182             if (depth > MAXLABELCOUNT) {
183                 throw new ParseException("Failed to parse name, too many labels");
184             }
185             final int len = Byte.toUnsignedInt(buf.get());
186             final int mask = len & NAME_COMPRESSION;
187             if (0 == len) {
188                 return "";
189             } else if (mask != NAME_NORMAL && mask != NAME_COMPRESSION) {
190                 throw new ParseException("Parse name fail, bad label type");
191             } else if (mask == NAME_COMPRESSION) {
192                 // Name compression based on RFC 1035 - 4.1.4 Message compression
193                 final int offset = ((len & ~NAME_COMPRESSION) << 8) + Byte.toUnsignedInt(buf.get());
194                 final int oldPos = buf.position();
195                 if (offset >= oldPos - 2) {
196                     throw new ParseException("Parse compression name fail, invalid compression");
197                 }
198                 buf.position(offset);
199                 final String pointed = parseName(buf, depth + 1);
200                 buf.position(oldPos);
201                 return pointed;
202             } else {
203                 final byte[] label = new byte[len];
204                 buf.get(label);
205                 final String head = labelToString(label);
206                 if (head.length() > MAXLABELSIZE) {
207                     throw new ParseException("Parse name fail, invalid label length");
208                 }
209                 final String tail = parseName(buf, depth + 1);
210                 return TextUtils.isEmpty(tail) ? head : head + "." + tail;
211             }
212         }
213     }
214 
215     public static final int QDSECTION = 0;
216     public static final int ANSECTION = 1;
217     public static final int NSSECTION = 2;
218     public static final int ARSECTION = 3;
219     private static final int NUM_SECTIONS = ARSECTION + 1;
220 
221     private static final String TAG = DnsPacket.class.getSimpleName();
222 
223     protected final DnsHeader mHeader;
224     protected final List<DnsRecord>[] mRecords;
225 
DnsPacket(@onNull byte[] data)226     protected DnsPacket(@NonNull byte[] data) throws ParseException {
227         if (null == data) throw new ParseException("Parse header failed, null input data");
228         final ByteBuffer buffer;
229         try {
230             buffer = ByteBuffer.wrap(data);
231             mHeader = new DnsHeader(buffer);
232         } catch (BufferUnderflowException e) {
233             throw new ParseException("Parse Header fail, bad input data", e);
234         }
235 
236         mRecords = new ArrayList[NUM_SECTIONS];
237 
238         for (int i = 0; i < NUM_SECTIONS; ++i) {
239             final int count = mHeader.getRecordCount(i);
240             if (count > 0) {
241                 mRecords[i] = new ArrayList(count);
242             }
243             for (int j = 0; j < count; ++j) {
244                 try {
245                     mRecords[i].add(new DnsRecord(i, buffer));
246                 } catch (BufferUnderflowException e) {
247                     throw new ParseException("Parse record fail", e);
248                 }
249             }
250         }
251     }
252 }
253