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          */
parseName(final ByteBuffer buf, int depth, boolean isNameCompressionSupported)134         public static String parseName(final ByteBuffer buf, int depth,
135                 boolean isNameCompressionSupported) throws
136                 BufferUnderflowException, DnsPacket.ParseException {
137             return parseName(buf, depth, MAXLABELCOUNT, isNameCompressionSupported);
138         }
139 
140         /**
141          * Parses the domain / target name of a DNS record.
142          *
143          * As described in RFC 1035 Section 4.1.3, the NAME field of a DNS Resource Record always
144          * supports Name Compression, whereas domain names contained in the RDATA payload of a DNS
145          * record may or may not support Name Compression, depending on the record TYPE. Moreover,
146          * even if Name Compression is supported, its usage is left to the implementation.
147          */
parseName(final ByteBuffer buf, int depth, int maxLabelCount, boolean isNameCompressionSupported)148         public static String parseName(final ByteBuffer buf, int depth, int maxLabelCount,
149                 boolean isNameCompressionSupported) throws
150                 BufferUnderflowException, DnsPacket.ParseException {
151             if (depth > maxLabelCount) {
152                 throw new DnsPacket.ParseException("Failed to parse name, too many labels");
153             }
154             final int len = Byte.toUnsignedInt(buf.get());
155             final int mask = len & NAME_COMPRESSION;
156             if (0 == len) {
157                 return "";
158             } else if (mask != NAME_NORMAL && mask != NAME_COMPRESSION
159                     || (!isNameCompressionSupported && mask == NAME_COMPRESSION)) {
160                 throw new DnsPacket.ParseException("Parse name fail, bad label type: " + mask);
161             } else if (mask == NAME_COMPRESSION) {
162                 // Name compression based on RFC 1035 - 4.1.4 Message compression
163                 final int offset = ((len & ~NAME_COMPRESSION) << 8) + Byte.toUnsignedInt(buf.get());
164                 final int oldPos = buf.position();
165                 if (offset >= oldPos - 2) {
166                     throw new DnsPacket.ParseException(
167                             "Parse compression name fail, invalid compression");
168                 }
169                 buf.position(offset);
170                 final String pointed = parseName(buf, depth + 1, maxLabelCount,
171                         isNameCompressionSupported);
172                 buf.position(oldPos);
173                 return pointed;
174             } else {
175                 final byte[] label = new byte[len];
176                 buf.get(label);
177                 final String head = labelToString(label);
178                 if (head.length() > MAXLABELSIZE) {
179                     throw new DnsPacket.ParseException("Parse name fail, invalid label length");
180                 }
181                 final String tail = parseName(buf, depth + 1, maxLabelCount,
182                         isNameCompressionSupported);
183                 return TextUtils.isEmpty(tail) ? head : head + "." + tail;
184             }
185         }
186 
DnsRecordParser()187         private DnsRecordParser() {}
188     }
189 
DnsPacketUtils()190     private DnsPacketUtils() {}
191 }
192