• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 static com.android.net.module.util.DnsPacket.DnsRecord.NAME_COMPRESSION;
20 import static com.android.net.module.util.DnsPacket.DnsRecord.NAME_NORMAL;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.net.InetAddresses;
25 import android.net.ParseException;
26 import android.text.TextUtils;
27 import android.util.Patterns;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 
31 import java.io.ByteArrayOutputStream;
32 import java.io.IOException;
33 import java.nio.BufferUnderflowException;
34 import java.nio.ByteBuffer;
35 import java.nio.charset.StandardCharsets;
36 import java.text.DecimalFormat;
37 import java.text.FieldPosition;
38 
39 /**
40  * Utilities for decoding the contents of a DnsPacket.
41  *
42  * @hide
43  */
44 public final class DnsPacketUtils {
45     /**
46      * Reads the passed ByteBuffer from its current position and decodes a DNS record.
47      */
48     public static class DnsRecordParser {
49         private static final int MAXLABELSIZE = 63;
50         private static final int MAXNAMESIZE = 255;
51         private static final int MAXLABELCOUNT = 128;
52 
53         private static final DecimalFormat sByteFormat = new DecimalFormat();
54         private static final FieldPosition sPos = new FieldPosition(0);
55 
56         /**
57          * Convert label from {@code byte[]} to {@code String}
58          *
59          * <p>Follows the same conversion rules of the native code (ns_name.c in libc).
60          */
61         @VisibleForTesting
labelToString(@onNull byte[] label)62         static String labelToString(@NonNull byte[] label) {
63             final StringBuffer sb = new StringBuffer();
64 
65             for (int i = 0; i < label.length; ++i) {
66                 int b = Byte.toUnsignedInt(label[i]);
67                 // Control characters and non-ASCII characters.
68                 if (b <= 0x20 || b >= 0x7f) {
69                     // Append the byte as an escaped decimal number, e.g., "\19" for 0x13.
70                     sb.append('\\');
71                     sByteFormat.format(b, sb, sPos);
72                 } else if (b == '"' || b == '.' || b == ';' || b == '\\' || b == '(' || b == ')'
73                         || b == '@' || b == '$') {
74                     // Append the byte as an escaped character, e.g., "\:" for 0x3a.
75                     sb.append('\\');
76                     sb.append((char) b);
77                 } else {
78                     // Append the byte as a character, e.g., "a" for 0x61.
79                     sb.append((char) b);
80                 }
81             }
82             return sb.toString();
83         }
84 
85         /**
86          * Converts domain name to labels according to RFC 1035.
87          *
88          * @param name Domain name as String that needs to be converted to labels.
89          * @return An encoded byte array that is constructed out of labels,
90          *         and ends with zero-length label.
91          * @throws ParseException if failed to parse the given domain name or
92          *         IOException if failed to output labels.
93          */
domainNameToLabels(@onNull String name)94         public static @NonNull byte[] domainNameToLabels(@NonNull String name) throws
95                 IOException, ParseException {
96             if (name.length() > MAXNAMESIZE) {
97                 throw new ParseException("Domain name exceeds max length: " + name.length());
98             }
99             if (!isHostName(name)) {
100                 throw new ParseException("Failed to parse domain name: " + name);
101             }
102             final ByteArrayOutputStream buf = new ByteArrayOutputStream();
103             final String[] labels = name.split("\\.");
104             for (final String label : labels) {
105                 if (label.length() > MAXLABELSIZE) {
106                     throw new ParseException("label is too long: " + label);
107                 }
108                 buf.write(label.length());
109                 // Encode as UTF-8 as suggested in RFC 6055 section 3.
110                 buf.write(label.getBytes(StandardCharsets.UTF_8));
111             }
112             buf.write(0x00); // end with zero-length label
113             return buf.toByteArray();
114         }
115 
116         /**
117          * Check whether the input is a valid hostname based on rfc 1035 section 3.3.
118          *
119          * @param hostName the target host name.
120          * @return true if the input is a valid hostname.
121          */
isHostName(@ullable String hostName)122         public static boolean isHostName(@Nullable String hostName) {
123             // TODO: Use {@code Patterns.HOST_NAME} if available.
124             // Patterns.DOMAIN_NAME accepts host names or IP addresses, so reject
125             // IP addresses.
126             return hostName != null
127                     && Patterns.DOMAIN_NAME.matcher(hostName).matches()
128                     && !InetAddresses.isNumericAddress(hostName);
129         }
130 
131         /**
132          * Parses the domain / target name of a DNS record.
133          *
134          * As described in RFC 1035 Section 4.1.3, the NAME field of a DNS Resource Record always
135          * supports Name Compression, whereas domain names contained in the RDATA payload of a DNS
136          * record may or may not support Name Compression, depending on the record TYPE. Moreover,
137          * even if Name Compression is supported, its usage is left to the implementation.
138          */
parseName(ByteBuffer buf, int depth, boolean isNameCompressionSupported)139         public static String parseName(ByteBuffer buf, int depth,
140                 boolean isNameCompressionSupported) throws
141                 BufferUnderflowException, DnsPacket.ParseException {
142             if (depth > MAXLABELCOUNT) {
143                 throw new DnsPacket.ParseException("Failed to parse name, too many labels");
144             }
145             final int len = Byte.toUnsignedInt(buf.get());
146             final int mask = len & NAME_COMPRESSION;
147             if (0 == len) {
148                 return "";
149             } else if (mask != NAME_NORMAL && mask != NAME_COMPRESSION
150                     || (!isNameCompressionSupported && mask == NAME_COMPRESSION)) {
151                 throw new DnsPacket.ParseException("Parse name fail, bad label type: " + mask);
152             } else if (mask == NAME_COMPRESSION) {
153                 // Name compression based on RFC 1035 - 4.1.4 Message compression
154                 final int offset = ((len & ~NAME_COMPRESSION) << 8) + Byte.toUnsignedInt(buf.get());
155                 final int oldPos = buf.position();
156                 if (offset >= oldPos - 2) {
157                     throw new DnsPacket.ParseException(
158                             "Parse compression name fail, invalid compression");
159                 }
160                 buf.position(offset);
161                 final String pointed = parseName(buf, depth + 1, isNameCompressionSupported);
162                 buf.position(oldPos);
163                 return pointed;
164             } else {
165                 final byte[] label = new byte[len];
166                 buf.get(label);
167                 final String head = labelToString(label);
168                 if (head.length() > MAXLABELSIZE) {
169                     throw new DnsPacket.ParseException("Parse name fail, invalid label length");
170                 }
171                 final String tail = parseName(buf, depth + 1, isNameCompressionSupported);
172                 return TextUtils.isEmpty(tail) ? head : head + "." + tail;
173             }
174         }
175 
DnsRecordParser()176         private DnsRecordParser() {}
177     }
178 
DnsPacketUtils()179     private DnsPacketUtils() {}
180 }
181