View Javadoc
1   /*
2    * Copyright 2026 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  package io.netty.resolver.dns;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.handler.codec.dns.DefaultDnsQuestion;
20  import io.netty.handler.codec.dns.DnsOptCookieRecord;
21  import io.netty.handler.codec.dns.DnsQuestion;
22  import io.netty.handler.codec.dns.DnsRawRecord;
23  import io.netty.handler.codec.dns.DnsRecord;
24  import io.netty.handler.codec.dns.DnsRecordType;
25  import io.netty.handler.codec.dns.DnsResponse;
26  import io.netty.handler.codec.dns.DnsSection;
27  
28  import java.net.InetSocketAddress;
29  import java.security.SecureRandom;
30  import java.util.Set;
31  import java.util.concurrent.ConcurrentHashMap;
32  
33  /**
34   * Defences that make it harder for an off-path attacker to spoof the response of a UDP DNS query on top of the
35   * query id and the source port, as recommended by
36   * <a href="https://tools.ietf.org/html/rfc5452">RFC 5452</a>:
37   * <ul>
38   *     <li>"0x20" randomization of the case of the name in the question
39   *     (<a href="https://datatracker.ietf.org/doc/html/draft-vixie-dnsext-dns0x20-00">draft-vixie-dnsext-dns0x20</a>)
40   *     </li>
41   *     <li>client cookies as defined in <a href="https://tools.ietf.org/html/rfc7873">RFC 7873</a></li>
42   * </ul>
43   */
44  final class DnsQueryProtection {
45  
46      /**
47       * Upper bound for the number of nameservers we remember to support cookies. Once reached we stop remembering
48       * new ones, which means that responses from those will not be rejected if they omit the cookie.
49       */
50      private static final int MAX_COOKIE_SERVERS = 1024;
51  
52      private static final String ACE_PREFIX = "xn--";
53  
54      /**
55       * The result of checking the cookie in a response.
56       */
57      enum CookieStatus {
58          /**
59           * The response contained no cookie.
60           */
61          ABSENT,
62          /**
63           * The response contained a cookie that echoes the client cookie.
64           */
65          VALID,
66          /**
67           * The response contained a cookie that does not echo the client cookie or is malformed.
68           */
69          INVALID
70      }
71  
72      private final SecureRandom random = new SecureRandom();
73      private final boolean caseRandomization;
74      // null if cookies are disabled.
75      private final Set<InetSocketAddress> cookieServers;
76  
77      DnsQueryProtection(boolean caseRandomization, boolean cookies) {
78          this.caseRandomization = caseRandomization;
79          this.cookieServers = cookies ? ConcurrentHashMap.<InetSocketAddress>newKeySet() : null;
80      }
81  
82      boolean isCaseRandomizationEnabled() {
83          return caseRandomization;
84      }
85  
86      boolean isCookiesEnabled() {
87          return cookieServers != null;
88      }
89  
90      /**
91       * Returns a copy of the given {@link DnsQuestion} in which the case of every ASCII letter was picked at random.
92       */
93      DnsQuestion randomizeCase(DnsQuestion question) {
94          final String name = question.name();
95          final int length = name.length();
96          final char[] chars = name.toCharArray();
97          final byte[] bits = new byte[(length + 7) >>> 3];
98          random.nextBytes(bits);
99  
100         int labelStart = 0;
101         while (labelStart < length) {
102             int labelEnd = name.indexOf('.', labelStart);
103             if (labelEnd == -1) {
104                 labelEnd = length;
105             }
106             // Don't touch labels that carry punycode, so they stay valid for IDN.toASCII(...).
107             if (!name.regionMatches(true, labelStart, ACE_PREFIX, 0, ACE_PREFIX.length())) {
108                 for (int i = labelStart; i < labelEnd; i++) {
109                     char c = chars[i];
110                     if (((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) && (bits[i >>> 3] >>> (i & 7) & 1) != 0) {
111                         // Toggle the case bit.
112                         chars[i] = (char) (c ^ 0x20);
113                     }
114                 }
115             }
116             labelStart = labelEnd + 1;
117         }
118         return new DefaultDnsQuestion(new String(chars), question.type(), question.dnsClass());
119     }
120 
121     byte[] newClientCookie() {
122         byte[] cookie = new byte[DnsOptCookieRecord.CLIENT_COOKIE_LENGTH];
123         random.nextBytes(cookie);
124         return cookie;
125     }
126 
127     /**
128      * Returns {@code true} if the given nameserver has returned a valid cookie before, and so is expected to do so
129      * for every response.
130      */
131     boolean isCookieExpected(InetSocketAddress nameServerAddr) {
132         return cookieServers != null && cookieServers.contains(nameServerAddr);
133     }
134 
135     void cookieReceived(InetSocketAddress nameServerAddr) {
136         if (cookieServers != null && cookieServers.size() < MAX_COOKIE_SERVERS) {
137             cookieServers.add(nameServerAddr);
138         }
139     }
140 
141     /**
142      * Inspect the COOKIE option of the OPT record of the response.
143      */
144     static CookieStatus checkCookie(DnsResponse response, byte[] clientCookie) {
145         final int count = response.count(DnsSection.ADDITIONAL);
146         for (int i = 0; i < count; i++) {
147             DnsRecord record = response.recordAt(DnsSection.ADDITIONAL, i);
148             if (record.type() == DnsRecordType.OPT) {
149                 // There must be only one OPT record, so just look at the first.
150                 if (!(record instanceof DnsRawRecord)) {
151                     return CookieStatus.INVALID;
152                 }
153                 return checkCookie(((DnsRawRecord) record).content(), clientCookie);
154             }
155         }
156         return CookieStatus.ABSENT;
157     }
158 
159     private static CookieStatus checkCookie(ByteBuf rdata, byte[] clientCookie) {
160         // See https://tools.ietf.org/html/rfc6891#section-6.1.2 for the layout of the options.
161         int index = rdata.readerIndex();
162         final int end = rdata.writerIndex();
163         CookieStatus status = CookieStatus.ABSENT;
164         while (index < end) {
165             if (end - index < 4) {
166                 return CookieStatus.INVALID;
167             }
168             final int code = rdata.getUnsignedShort(index);
169             final int length = rdata.getUnsignedShort(index + 2);
170             index += 4;
171             if (end - index < length) {
172                 return CookieStatus.INVALID;
173             }
174             if (code == DnsOptCookieRecord.OPTION_CODE) {
175                 if (status != CookieStatus.ABSENT) {
176                     // Only one cookie option is allowed.
177                     return CookieStatus.INVALID;
178                 }
179                 // The client cookie may be followed by a server cookie which is 8 to 32 bytes long.
180                 if (length != clientCookie.length && (length < clientCookie.length + 8 ||
181                         length > clientCookie.length + 32)) {
182                     return CookieStatus.INVALID;
183                 }
184                 status = CookieStatus.VALID;
185                 // Compare in constant time, just to be on the safe side.
186                 int diff = 0;
187                 for (int i = 0; i < clientCookie.length; i++) {
188                     diff |= rdata.getByte(index + i) ^ clientCookie[i];
189                 }
190                 if (diff != 0) {
191                     return CookieStatus.INVALID;
192                 }
193             }
194             index += length;
195         }
196         return status;
197     }
198 }