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 }