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