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