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 }