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