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 }