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