View Javadoc
1   /*
2    * Copyright 2019 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://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, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package io.netty.handler.codec.dns;
18  
19  import io.netty.buffer.ByteBuf;
20  import io.netty.buffer.ByteBufUtil;
21  import io.netty.handler.codec.CorruptedFrameException;
22  import io.netty.handler.codec.TooLongFrameException;
23  import io.netty.util.CharsetUtil;
24  
25  import static io.netty.handler.codec.dns.DefaultDnsRecordDecoder.*;
26  
27  final class DnsCodecUtil {
28  
29      // See https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4
30      static final int MAX_DOMAIN_NAME_LENGTH = 255;
31  
32      // A decoded name is at most 255 characters and every label contributes at least 2 ("x."), so a valid name has
33      // at most 127 labels and therefore never needs to follow more compression pointers than that. Bounding the hops
34      // per name keeps the decoding cost of each name constant instead of proportional to the message size.
35      static final int MAX_COMPRESSION_POINTERS = 127;
36  
37      // The RFC does not define a maximum number of records per section (the count is a 16-bit value). The cost of
38      // decoding a single record is bounded (see MAX_DOMAIN_NAME_LENGTH and MAX_COMPRESSION_POINTERS), so this only
39      // limits the total work per message. It is high enough that a full 64 KiB TCP message of minimal records with
40      // compressed names (e.g. A records, 16 bytes each) still decodes.
41      static final int MAX_RECORDS_PER_SECTION = 4096;
42  
43      private DnsCodecUtil() {
44          // Util class
45      }
46  
47      static void encodeDomainName(String name, ByteBuf buf) {
48          if (ROOT.equals(name)) {
49              // Root domain
50              buf.writeByte(0);
51              return;
52          }
53  
54          int totalLength = 0;
55          final String[] labels = name.split("\\.");
56          for (int i = 0; i < labels.length; i++) {
57              String label = labels[i];
58              final int labelLen = label.length();
59              if (labelLen == 0) {
60                  if (i == labels.length - 1) {
61                      // zero-length label at the end means the end of the name.
62                      break;
63                  } else {
64                      throw new IllegalArgumentException("DNS name contains empty label: " + name);
65                  }
66              }
67              if (labelLen > 63) {
68                  throw new IllegalArgumentException(
69                          "DNS label length " + labelLen + " exceeds maximum of 63: " + name);
70              }
71              int idx = label.indexOf('\0');
72              if (idx != -1) {
73                  throw new IllegalArgumentException(
74                          "DNS label contains null byte at index " + idx);
75              }
76              totalLength += 1 + labelLen;
77              if (totalLength > MAX_DOMAIN_NAME_LENGTH) {
78                  throw new IllegalArgumentException(
79                          "DNS name exceeds maximum length of " + MAX_DOMAIN_NAME_LENGTH + ": " + name);
80              }
81              buf.writeByte(labelLen);
82              ByteBufUtil.writeAscii(buf, label);
83          }
84  
85          buf.writeByte(0); // marks end of name field
86      }
87  
88      /**
89       * Returns the initial capacity to use for the {@link StringBuilder} that accumulates a decoded domain name.
90       * A decoded name can never exceed {@link #MAX_DOMAIN_NAME_LENGTH} characters (see the check in
91       * {@link #decodeDomainName(ByteBuf)}), so the capacity must not scale with {@code readable}, the number of
92       * bytes left in the whole message: doing so would let a message with a huge amount of trailing data (e.g.
93       * many more records after this name) force an oversized allocation for every single name decoded from it.
94       */
95      static int calculateMaxNameLength(int readable) {
96          return Math.min(readable, MAX_DOMAIN_NAME_LENGTH);
97      }
98  
99      /**
100      * Ensures the record count of a section, as announced in the header of a DNS message, does not exceed
101      * {@link #MAX_RECORDS_PER_SECTION}.
102      *
103      * @throws TooLongFrameException if the count is too large.
104      */
105     static int checkRecordCount(DnsSection section, int count) {
106         if (count > MAX_RECORDS_PER_SECTION) {
107             throw new TooLongFrameException(
108                     section + " record count must be <= " + MAX_RECORDS_PER_SECTION + " but was " + count);
109         }
110         return count;
111     }
112 
113     static String decodeDomainName(ByteBuf in) {
114         int position = -1;
115         int pointers = 0;
116         final int end = in.writerIndex();
117         final int readable = in.readableBytes();
118 
119         // Looking at the spec we should always have at least enough readable bytes to read a byte here but it seems
120         // some servers do not respect this for empty names. So just workaround this and return an empty name in this
121         // case.
122         //
123         // See:
124         // - https://github.com/netty/netty/issues/5014
125         // - https://www.ietf.org/rfc/rfc1035.txt , Section 3.1
126         if (readable == 0) {
127             return ROOT;
128         }
129 
130         final StringBuilder name = new StringBuilder(calculateMaxNameLength(readable));
131         while (in.isReadable()) {
132             final int len = in.readUnsignedByte();
133             final boolean pointer = (len & 0xc0) == 0xc0;
134             if (pointer) {
135                 if (position == -1) {
136                     position = in.readerIndex() + 1;
137                 }
138 
139                 if (!in.isReadable()) {
140                     throw new CorruptedFrameException("truncated pointer in a name");
141                 }
142 
143                 final int next = (len & 0x3f) << 8 | in.readUnsignedByte();
144                 if (next >= end) {
145                     throw new CorruptedFrameException("name has an out-of-range pointer");
146                 }
147                 in.readerIndex(next);
148 
149                 // Bounds the work per name, which also terminates loops.
150                 if (++pointers > MAX_COMPRESSION_POINTERS) {
151                     throw new CorruptedFrameException(
152                             "name contains more than " + MAX_COMPRESSION_POINTERS + " compression pointers");
153                 }
154             } else if (len != 0) {
155                 if (!in.isReadable(len)) {
156                     throw new CorruptedFrameException("truncated label in a name");
157                 }
158                 // See https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4
159                 if (len > 63) {
160                     throw new TooLongFrameException("label must be <= 63 but was " + len);
161                 }
162                 name.append(in.toString(in.readerIndex(), len, CharsetUtil.UTF_8)).append('.');
163                 in.skipBytes(len);
164                 // See https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4
165                 if (name.length() > MAX_DOMAIN_NAME_LENGTH) {
166                     throw new TooLongFrameException(
167                             "domain name must be <= " + MAX_DOMAIN_NAME_LENGTH + " but was " + name.length());
168                 }
169             } else { // len == 0
170                 break;
171             }
172         }
173 
174         if (position != -1) {
175             in.readerIndex(position);
176         }
177 
178         if (name.length() == 0) {
179             return ROOT;
180         }
181 
182         if (name.charAt(name.length() - 1) != '.') {
183             name.append('.');
184         }
185 
186         return name.toString();
187     }
188 
189     /**
190      * Decompress pointer data.
191      * @param compression compressed data
192      * @return decompressed data
193      */
194     static ByteBuf decompressDomainName(ByteBuf compression) {
195         String domainName = decodeDomainName(compression);
196         ByteBuf result = compression.alloc().buffer(domainName.length() << 1);
197         try {
198             encodeDomainName(domainName, result);
199         } catch (Throwable cause) {
200             result.release();
201             throw cause;
202         }
203         return result;
204     }
205 }