View Javadoc
1   /*
2    * Copyright 2014 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.resolver.dns;
18  
19  import io.netty.buffer.ByteBuf;
20  import io.netty.buffer.ByteBufHolder;
21  import io.netty.channel.AddressedEnvelope;
22  import io.netty.channel.Channel;
23  import io.netty.channel.ChannelFuture;
24  import io.netty.channel.EventLoop;
25  import io.netty.handler.codec.CorruptedFrameException;
26  import io.netty.handler.codec.dns.DefaultDnsQuestion;
27  import io.netty.handler.codec.dns.DefaultDnsRecordDecoder;
28  import io.netty.handler.codec.dns.DnsQuestion;
29  import io.netty.handler.codec.dns.DnsRawRecord;
30  import io.netty.handler.codec.dns.DnsRecord;
31  import io.netty.handler.codec.dns.DnsRecordType;
32  import io.netty.handler.codec.dns.DnsResponse;
33  import io.netty.handler.codec.dns.DnsResponseCode;
34  import io.netty.handler.codec.dns.DnsSection;
35  import io.netty.util.NetUtil;
36  import io.netty.util.ReferenceCountUtil;
37  import io.netty.util.concurrent.Future;
38  import io.netty.util.concurrent.FutureListener;
39  import io.netty.util.concurrent.Promise;
40  import io.netty.util.internal.ObjectUtil;
41  import io.netty.util.internal.PlatformDependent;
42  import io.netty.util.internal.StringUtil;
43  import io.netty.util.internal.SystemPropertyUtil;
44  import io.netty.util.internal.ThrowableUtil;
45  import io.netty.util.internal.logging.InternalLogger;
46  import io.netty.util.internal.logging.InternalLoggerFactory;
47  
48  import java.net.InetAddress;
49  import java.net.InetSocketAddress;
50  import java.net.UnknownHostException;
51  import java.util.AbstractList;
52  import java.util.ArrayList;
53  import java.util.Arrays;
54  import java.util.Collections;
55  import java.util.HashMap;
56  import java.util.IdentityHashMap;
57  import java.util.Iterator;
58  import java.util.List;
59  import java.util.Locale;
60  import java.util.Map;
61  import java.util.NoSuchElementException;
62  import java.util.Set;
63  
64  import static io.netty.handler.codec.dns.DnsResponseCode.NXDOMAIN;
65  import static io.netty.handler.codec.dns.DnsResponseCode.SERVFAIL;
66  import static io.netty.resolver.dns.DnsAddressDecoder.decodeAddress;
67  import static java.lang.Math.min;
68  
69  abstract class DnsResolveContext<T> {
70      private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsResolveContext.class);
71      private static final String PROP_TRY_FINAL_CNAME_ON_ADDRESS_LOOKUPS =
72              "io.netty.resolver.dns.tryCnameOnAddressLookups";
73      static boolean TRY_FINAL_CNAME_ON_ADDRESS_LOOKUPS;
74  
75      static {
76          TRY_FINAL_CNAME_ON_ADDRESS_LOOKUPS =
77                  SystemPropertyUtil.getBoolean(PROP_TRY_FINAL_CNAME_ON_ADDRESS_LOOKUPS, false);
78  
79          if (logger.isDebugEnabled()) {
80              logger.debug("-D{}: {}", PROP_TRY_FINAL_CNAME_ON_ADDRESS_LOOKUPS, TRY_FINAL_CNAME_ON_ADDRESS_LOOKUPS);
81          }
82      }
83  
84      private static final RuntimeException NXDOMAIN_QUERY_FAILED_EXCEPTION =
85              DnsResolveContextException.newStatic("No answer found and NXDOMAIN response code returned",
86              DnsResolveContext.class, "onResponse(..)");
87      private static final RuntimeException CNAME_NOT_FOUND_QUERY_FAILED_EXCEPTION =
88              DnsResolveContextException.newStatic("No matching CNAME record found",
89              DnsResolveContext.class, "onResponseCNAME(..)");
90      private static final RuntimeException NO_MATCHING_RECORD_QUERY_FAILED_EXCEPTION =
91              DnsResolveContextException.newStatic("No matching record type found",
92              DnsResolveContext.class, "onResponseAorAAAA(..)");
93      private static final RuntimeException UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION =
94              DnsResolveContextException.newStatic("Response type was unrecognized",
95              DnsResolveContext.class, "onResponse(..)");
96      private static final RuntimeException NAME_SERVERS_EXHAUSTED_EXCEPTION =
97              DnsResolveContextException.newStatic("No name servers returned an answer",
98              DnsResolveContext.class, "tryToFinishResolve(..)");
99      private static final RuntimeException SERVFAIL_QUERY_FAILED_EXCEPTION =
100             DnsErrorCauseException.newStatic("Query failed with SERVFAIL", SERVFAIL,
101                     DnsResolveContext.class, "onResponse(..)");
102     private static final RuntimeException NXDOMAIN_CAUSE_QUERY_FAILED_EXCEPTION =
103             DnsErrorCauseException.newStatic("Query failed with NXDOMAIN", NXDOMAIN,
104                     DnsResolveContext.class, "onResponse(..)");
105 
106     final DnsNameResolver parent;
107     private final Channel channel;
108     private final Promise<?> originalPromise;
109     private final DnsServerAddressStream nameServerAddrs;
110     private final String hostname;
111     private final int dnsClass;
112     private final DnsRecordType[] expectedTypes;
113     final DnsRecord[] additionals;
114 
115     private final Set<Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> queriesInProgress =
116             Collections.newSetFromMap(
117                     new IdentityHashMap<Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>, Boolean>());
118 
119     private List<T> finalResult;
120     private int allowedQueries;
121     private boolean triedCNAME;
122     private boolean completeEarly;
123 
124     DnsResolveContext(DnsNameResolver parent, Channel channel,
125                       Promise<?> originalPromise, String hostname, int dnsClass, DnsRecordType[] expectedTypes,
126                       DnsRecord[] additionals, DnsServerAddressStream nameServerAddrs, int allowedQueries) {
127         assert expectedTypes.length > 0;
128 
129         this.parent = parent;
130         this.channel = channel;
131         this.originalPromise = originalPromise;
132         this.hostname = hostname;
133         this.dnsClass = dnsClass;
134         this.expectedTypes = expectedTypes;
135         this.additionals = additionals;
136 
137         this.nameServerAddrs = ObjectUtil.checkNotNull(nameServerAddrs, "nameServerAddrs");
138         this.allowedQueries = allowedQueries;
139     }
140 
141     static final class DnsResolveContextException extends RuntimeException {
142 
143         private static final long serialVersionUID = 1209303419266433003L;
144 
145         private DnsResolveContextException(String message, boolean shared) {
146             super(message, null, false, true);
147             assert shared;
148         }
149 
150         // Override fillInStackTrace() so we not populate the backtrace via a native call and so leak the
151         // Classloader.
152         @Override
153         public Throwable fillInStackTrace() {
154             return this;
155         }
156 
157         static DnsResolveContextException newStatic(String message, Class<?> clazz, String method) {
158             final DnsResolveContextException exception = new DnsResolveContextException(message, true);
159             return ThrowableUtil.unknownStackTrace(exception, clazz, method);
160         }
161     }
162 
163     /**
164      * The {@link Channel} used.
165      */
166     Channel channel() {
167         return channel;
168     }
169 
170     /**
171      * The {@link DnsCache} to use while resolving.
172      */
173     DnsCache resolveCache() {
174         return parent.resolveCache();
175     }
176 
177     /**
178      * The {@link DnsCnameCache} that is used for resolving.
179      */
180     DnsCnameCache cnameCache() {
181         return parent.cnameCache();
182     }
183 
184     /**
185      * The {@link AuthoritativeDnsServerCache} to use while resolving.
186      */
187     AuthoritativeDnsServerCache authoritativeDnsServerCache() {
188         return parent.authoritativeDnsServerCache();
189     }
190 
191     /**
192      * Creates a new context with the given parameters.
193      */
194     abstract DnsResolveContext<T> newResolverContext(DnsNameResolver parent, Channel channel,
195                                                      Promise<?> originalPromise,
196                                                      String hostname,
197                                                      int dnsClass, DnsRecordType[] expectedTypes,
198                                                      DnsRecord[] additionals,
199                                                      DnsServerAddressStream nameServerAddrs, int allowedQueries);
200 
201     /**
202      * Converts the given {@link DnsRecord} into {@code T}.
203      */
204     abstract T convertRecord(DnsRecord record, String hostname, DnsRecord[] additionals, EventLoop eventLoop);
205 
206     /**
207      * Returns a filtered list of results which should be the final result of DNS resolution. This must take into
208      * account JDK semantics such as {@link NetUtil#isIpV6AddressesPreferred()}.
209      */
210     abstract List<T> filterResults(List<T> unfiltered);
211 
212     abstract boolean isCompleteEarly(T resolved);
213 
214     /**
215      * Returns {@code true} if we should allow duplicates in the result or {@code false} if no duplicates should
216      * be included.
217      */
218     abstract boolean isDuplicateAllowed();
219 
220     /**
221      * Caches a successful resolution.
222      */
223     abstract void cache(String hostname, DnsRecord[] additionals,
224                         DnsRecord result, T convertedResult);
225 
226     /**
227      * Caches a failed resolution.
228      */
229     abstract void cache(String hostname, DnsRecord[] additionals,
230                         UnknownHostException cause);
231 
232     void resolve(final Promise<List<T>> promise) {
233         final String[] searchDomains = parent.searchDomains();
234         if (searchDomains.length == 0 || parent.ndots() == 0 || StringUtil.endsWith(hostname, '.')) {
235             internalResolve(hostname, promise);
236         } else {
237             final boolean startWithoutSearchDomain = hasNDots();
238             final String initialHostname = startWithoutSearchDomain ? hostname : hostname + '.' + searchDomains[0];
239             final int initialSearchDomainIdx = startWithoutSearchDomain ? 0 : 1;
240 
241             final Promise<List<T>> searchDomainPromise = parent.executor().newPromise();
242             searchDomainPromise.addListener(new FutureListener<List<T>>() {
243                 private int searchDomainIdx = initialSearchDomainIdx;
244                 @Override
245                 public void operationComplete(Future<List<T>> future) {
246                     Throwable cause = future.cause();
247                     if (cause == null) {
248                         final List<T> result = future.getNow();
249                         if (!promise.trySuccess(result)) {
250                             for (T item : result) {
251                                 ReferenceCountUtil.safeRelease(item);
252                             }
253                         }
254                     } else {
255                         if (DnsNameResolver.isTransportOrTimeoutError(cause)) {
256                             promise.tryFailure(new SearchDomainUnknownHostException(cause, hostname, expectedTypes,
257                                     searchDomains));
258                         } else if (searchDomainIdx < searchDomains.length) {
259                             Promise<List<T>> newPromise = parent.executor().newPromise();
260                             newPromise.addListener(this);
261                             doSearchDomainQuery(hostname + '.' + searchDomains[searchDomainIdx++], newPromise);
262                         } else if (!startWithoutSearchDomain) {
263                             internalResolve(hostname, promise);
264                         } else {
265                             promise.tryFailure(new SearchDomainUnknownHostException(cause, hostname, expectedTypes,
266                                     searchDomains));
267                         }
268                     }
269                 }
270             });
271             doSearchDomainQuery(initialHostname, searchDomainPromise);
272         }
273     }
274 
275     private boolean hasNDots() {
276         for (int idx = hostname.length() - 1, dots = 0; idx >= 0; idx--) {
277             if (hostname.charAt(idx) == '.' && ++dots >= parent.ndots()) {
278                 return true;
279             }
280         }
281         return false;
282     }
283 
284     private static final class SearchDomainUnknownHostException extends UnknownHostException {
285         private static final long serialVersionUID = -8573510133644997085L;
286 
287         SearchDomainUnknownHostException(Throwable cause, String originalHostname,
288                 DnsRecordType[] queryTypes, String[] searchDomains) {
289             super("Failed to resolve '" + originalHostname + "' " + Arrays.toString(queryTypes) +
290                     " and search domain query for configured domains failed as well: " +
291                     Arrays.toString(searchDomains));
292             setStackTrace(cause.getStackTrace());
293             // Preserve the cause
294             initCause(cause.getCause());
295         }
296 
297         // Suppress a warning since this method doesn't need synchronization
298         @Override
299         public Throwable fillInStackTrace() {
300             return this;
301         }
302     }
303 
304     void doSearchDomainQuery(String hostname, Promise<List<T>> nextPromise) {
305         DnsResolveContext<T> nextContext = newResolverContext(parent, channel,
306                 originalPromise, hostname, dnsClass,
307                 expectedTypes, additionals, nameServerAddrs,
308                 parent.maxQueriesPerResolve());
309         nextContext.internalResolve(hostname, nextPromise);
310     }
311 
312     private static String hostnameWithDot(String name) {
313         if (StringUtil.endsWith(name, '.')) {
314             return name;
315         }
316         return name + '.';
317     }
318 
319     // Resolve the final name from the CNAME cache until there is nothing to follow anymore. This also
320     // guards against loops in the cache but early return once a loop is detected.
321     //
322     // Visible for testing only
323     static String cnameResolveFromCache(DnsCnameCache cnameCache, String name) throws UnknownHostException {
324         String first = cnameCache.get(hostnameWithDot(name));
325         if (first == null) {
326             // Nothing in the cache at all
327             return name;
328         }
329 
330         String second = cnameCache.get(hostnameWithDot(first));
331         if (second == null) {
332             // Nothing else to follow, return first match.
333             return first;
334         }
335 
336         checkCnameLoop(name, first, second);
337         return cnameResolveFromCacheLoop(cnameCache, name, first, second);
338     }
339 
340     private static String cnameResolveFromCacheLoop(
341             DnsCnameCache cnameCache, String hostname, String first, String mapping) throws UnknownHostException {
342         // Detect loops by advance only every other iteration.
343         // See https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_Tortoise_and_Hare
344         boolean advance = false;
345 
346         String name = mapping;
347         // Resolve from cnameCache() until there is no more cname entry cached.
348         while ((mapping = cnameCache.get(hostnameWithDot(name))) != null) {
349             checkCnameLoop(hostname, first, mapping);
350             name = mapping;
351             if (advance) {
352                 first = cnameCache.get(first);
353             }
354             advance = !advance;
355         }
356         return name;
357     }
358 
359     private static void checkCnameLoop(String hostname, String first, String second) throws UnknownHostException {
360         if (first.equals(second)) {
361             // Follow CNAME from cache would loop. Lets throw and so fail the resolution.
362             throw new UnknownHostException("CNAME loop detected for '" + hostname + '\'');
363         }
364     }
365     private void internalResolve(String name, Promise<List<T>> promise) {
366         try {
367             // Resolve from cnameCache() until there is no more cname entry cached.
368             name = cnameResolveFromCache(cnameCache(), name);
369         } catch (Throwable cause) {
370             promise.tryFailure(cause);
371             return;
372         }
373 
374         try {
375             DnsServerAddressStream nameServerAddressStream = getNameServers(name);
376 
377             final int end = expectedTypes.length - 1;
378             for (int i = 0; i < end; ++i) {
379                 if (!query(name, expectedTypes[i], nameServerAddressStream.duplicate(), false, promise)) {
380                     return;
381                 }
382             }
383             query(name, expectedTypes[end], nameServerAddressStream, false, promise);
384         } finally {
385             // Now flush everything we submitted before for the Channel.
386             channel.flush();
387         }
388     }
389 
390     /**
391      * Returns the {@link DnsServerAddressStream} that was cached for the given hostname or {@code null} if non
392      *  could be found.
393      */
394     private DnsServerAddressStream getNameServersFromCache(String hostname) {
395         int len = hostname.length();
396 
397         if (len == 0) {
398             // We never cache for root servers.
399             return null;
400         }
401 
402         // We always store in the cache with a trailing '.'.
403         if (hostname.charAt(len - 1) != '.') {
404             hostname += ".";
405         }
406 
407         int idx = hostname.indexOf('.');
408         if (idx == hostname.length() - 1) {
409             // We are not interested in handling '.' as we should never serve the root servers from cache.
410             return null;
411         }
412 
413         // We start from the closed match and then move down.
414         for (;;) {
415             // Skip '.' as well.
416             hostname = hostname.substring(idx + 1);
417 
418             int idx2 = hostname.indexOf('.');
419             if (idx2 <= 0 || idx2 == hostname.length() - 1) {
420                 // We are not interested in handling '.TLD.' as we should never serve the root servers from cache.
421                 return null;
422             }
423             idx = idx2;
424 
425             DnsServerAddressStream entries = authoritativeDnsServerCache().get(hostname);
426             if (entries != null) {
427                 // The returned List may contain unresolved InetSocketAddress instances that will be
428                 // resolved on the fly in query(....).
429                 return entries;
430             }
431         }
432     }
433 
434     private void query(final DnsServerAddressStream nameServerAddrStream,
435                        final int nameServerAddrStreamIndex,
436                        final DnsQuestion question,
437                        final DnsQueryLifecycleObserver queryLifecycleObserver,
438                        final boolean flush,
439                        final Promise<List<T>> promise,
440                        final Throwable cause) {
441         if (completeEarly || nameServerAddrStreamIndex >= nameServerAddrStream.size() ||
442                 allowedQueries == 0 || originalPromise.isCancelled() || promise.isCancelled()) {
443             tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question, queryLifecycleObserver,
444                                promise, cause);
445             return;
446         }
447 
448         --allowedQueries;
449 
450         final InetSocketAddress nameServerAddr = nameServerAddrStream.next();
451         if (nameServerAddr.isUnresolved()) {
452             queryUnresolvedNameServer(nameServerAddr, nameServerAddrStream, nameServerAddrStreamIndex, question,
453                                       queryLifecycleObserver, promise, cause);
454             return;
455         }
456         final Promise<AddressedEnvelope<? extends DnsResponse, InetSocketAddress>> queryPromise =
457                 channel.eventLoop().newPromise();
458 
459         final long queryStartTimeNanos;
460         final boolean isFeedbackAddressStream;
461         if (nameServerAddrStream instanceof DnsServerResponseFeedbackAddressStream) {
462             queryStartTimeNanos = System.nanoTime();
463             isFeedbackAddressStream = true;
464         } else {
465             queryStartTimeNanos = -1;
466             isFeedbackAddressStream = false;
467         }
468 
469         final Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> f =
470                 parent.doQuery(channel, nameServerAddr, question,
471                         queryLifecycleObserver, additionals, flush, queryPromise);
472 
473         queriesInProgress.add(f);
474 
475         f.addListener((FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>>) future -> {
476             queriesInProgress.remove(future);
477 
478             if (promise.isDone() || future.isCancelled()) {
479                 queryLifecycleObserver.queryCancelled(allowedQueries);
480 
481                 // Check if we need to release the envelope itself. If the query was cancelled the getNow() will
482                 // return null as well as the Future will be failed with a CancellationException.
483                 AddressedEnvelope<DnsResponse, InetSocketAddress> result = future.getNow();
484                 if (result != null) {
485                     result.release();
486                 }
487                 return;
488             }
489 
490             final Throwable queryCause = future.cause();
491             try {
492                 if (queryCause == null) {
493                     if (isFeedbackAddressStream) {
494                         final DnsServerResponseFeedbackAddressStream feedbackNameServerAddrStream =
495                                 (DnsServerResponseFeedbackAddressStream) nameServerAddrStream;
496                         feedbackNameServerAddrStream.feedbackSuccess(nameServerAddr,
497                                 System.nanoTime() - queryStartTimeNanos);
498                     }
499                     onResponse(nameServerAddrStream, nameServerAddrStreamIndex, question, future.getNow(),
500                                queryLifecycleObserver, promise);
501                 } else {
502                     // Server did not respond or I/O error occurred; try again.
503                     if (isFeedbackAddressStream) {
504                         final DnsServerResponseFeedbackAddressStream feedbackNameServerAddrStream =
505                                 (DnsServerResponseFeedbackAddressStream) nameServerAddrStream;
506                         feedbackNameServerAddrStream.feedbackFailure(nameServerAddr, queryCause,
507                                 System.nanoTime() - queryStartTimeNanos);
508                     }
509                     queryLifecycleObserver.queryFailed(queryCause);
510                     query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question,
511                           newDnsQueryLifecycleObserver(question), true, promise, queryCause);
512                 }
513             } finally {
514                 tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question,
515                                    // queryLifecycleObserver has already been terminated at this point so we must
516                                    // not allow it to be terminated again by tryToFinishResolve.
517                                    NoopDnsQueryLifecycleObserver.INSTANCE,
518                                    promise, queryCause);
519             }
520         });
521     }
522 
523     private void queryUnresolvedNameServer(final InetSocketAddress nameServerAddr,
524                                            final DnsServerAddressStream nameServerAddrStream,
525                                            final int nameServerAddrStreamIndex,
526                                            final DnsQuestion question,
527                                            final DnsQueryLifecycleObserver queryLifecycleObserver,
528                                            final Promise<List<T>> promise,
529                                            final Throwable cause) {
530         final String nameServerName = nameServerAddr.getHostString();
531         assert nameServerName != null;
532 
533         // Placeholder so we will not try to finish the original query yet.
534         final Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> resolveFuture = parent.executor()
535                 .newSucceededFuture(null);
536         queriesInProgress.add(resolveFuture);
537 
538         Promise<List<InetAddress>> resolverPromise = parent.executor().newPromise();
539         resolverPromise.addListener((FutureListener<List<InetAddress>>) future -> {
540             // Remove placeholder.
541             queriesInProgress.remove(resolveFuture);
542 
543             if (future.isSuccess()) {
544                 List<InetAddress> resolvedAddresses = future.getNow();
545                 DnsServerAddressStream addressStream = new CombinedDnsServerAddressStream(
546                         nameServerAddr, resolvedAddresses, nameServerAddrStream);
547                 query(addressStream, nameServerAddrStreamIndex, question,
548                       queryLifecycleObserver, true, promise, cause);
549             } else {
550                 // Ignore the server and try the next one...
551                 query(nameServerAddrStream, nameServerAddrStreamIndex + 1,
552                       question, queryLifecycleObserver, true, promise, cause);
553             }
554         });
555         DnsCache resolveCache = resolveCache();
556         if (!DnsNameResolver.doResolveAllCached(nameServerName, additionals, resolverPromise, resolveCache,
557                 parent.searchDomains(), parent.ndots(), parent.resolvedInternetProtocolFamiliesUnsafe())) {
558 
559             new DnsAddressResolveContext(parent, channel,
560                     originalPromise, nameServerName, additionals, parent.newNameServerAddressStream(nameServerName),
561                     // Resolving the unresolved nameserver must be limited by allowedQueries
562                     // so we eventually fail
563                     allowedQueries,
564                     resolveCache,
565                     redirectAuthoritativeDnsServerCache(authoritativeDnsServerCache()), false)
566                     .resolve(resolverPromise);
567         }
568     }
569 
570     private static AuthoritativeDnsServerCache redirectAuthoritativeDnsServerCache(
571             AuthoritativeDnsServerCache authoritativeDnsServerCache) {
572         // Don't wrap again to prevent the possibility of an StackOverflowError when wrapping another
573         // RedirectAuthoritativeDnsServerCache.
574         if (authoritativeDnsServerCache instanceof RedirectAuthoritativeDnsServerCache) {
575             return authoritativeDnsServerCache;
576         }
577         return new RedirectAuthoritativeDnsServerCache(authoritativeDnsServerCache);
578     }
579 
580     private static final class RedirectAuthoritativeDnsServerCache implements AuthoritativeDnsServerCache {
581         private final AuthoritativeDnsServerCache wrapped;
582 
583         RedirectAuthoritativeDnsServerCache(AuthoritativeDnsServerCache authoritativeDnsServerCache) {
584             wrapped = authoritativeDnsServerCache;
585         }
586 
587         @Override
588         public DnsServerAddressStream get(String hostname) {
589             // To not risk falling into any loop, we will not use the cache while following redirects but only
590             // on the initial query.
591             return null;
592         }
593 
594         @Override
595         public void cache(String hostname, InetSocketAddress address, long originalTtl, EventLoop loop) {
596             wrapped.cache(hostname, address, originalTtl, loop);
597         }
598 
599         @Override
600         public void clear() {
601             wrapped.clear();
602         }
603 
604         @Override
605         public boolean clear(String hostname) {
606             return wrapped.clear(hostname);
607         }
608     }
609 
610     private void onResponse(final DnsServerAddressStream nameServerAddrStream, final int nameServerAddrStreamIndex,
611                             final DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
612                             final DnsQueryLifecycleObserver queryLifecycleObserver,
613                             Promise<List<T>> promise) {
614         try {
615             final DnsResponse res = envelope.content();
616             final DnsResponseCode code = res.code();
617             if (code == DnsResponseCode.NOERROR) {
618                 if (handleRedirect(question, envelope, queryLifecycleObserver, promise)) {
619                     // Was a redirect so return here as everything else is handled in handleRedirect(...)
620                     return;
621                 }
622                 final DnsRecordType type = question.type();
623 
624                 if (type == DnsRecordType.CNAME) {
625                     onResponseCNAME(question,
626                             buildAliasMap(question.name(), envelope.content(), cnameCache(), parent.executor()),
627                             queryLifecycleObserver, promise);
628                     return;
629                 }
630 
631                 for (DnsRecordType expectedType : expectedTypes) {
632                     if (type == expectedType) {
633                         onExpectedResponse(question, envelope, queryLifecycleObserver, promise);
634                         return;
635                     }
636                 }
637 
638                 queryLifecycleObserver.queryFailed(UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION);
639                 return;
640             }
641 
642             // Retry with the next server if the server did not tell us that the domain does not exist.
643             if (code != DnsResponseCode.NXDOMAIN) {
644                 query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question,
645                       queryLifecycleObserver.queryNoAnswer(code), true, promise, cause(code));
646             } else {
647                 queryLifecycleObserver.queryFailed(NXDOMAIN_QUERY_FAILED_EXCEPTION);
648 
649                 // Try with the next server if is not authoritative for the domain.
650                 //
651                 // From https://tools.ietf.org/html/rfc1035 :
652                 //
653                 //   RCODE        Response code - this 4 bit field is set as part of
654                 //                responses.  The values have the following
655                 //                interpretation:
656                 //
657                 //                ....
658                 //                ....
659                 //
660                 //                3               Name Error - Meaningful only for
661                 //                                responses from an authoritative name
662                 //                                server, this code signifies that the
663                 //                                domain name referenced in the query does
664                 //                                not exist.
665                 //                ....
666                 //                ....
667                 if (!res.isAuthoritativeAnswer()) {
668                     query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question,
669                             newDnsQueryLifecycleObserver(question), true, promise, cause(code));
670                 } else {
671                     // Failed with NX cause - distinction between NXDOMAIN vs a timeout
672                     tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question,
673                             queryLifecycleObserver, promise, NXDOMAIN_CAUSE_QUERY_FAILED_EXCEPTION);
674                 }
675             }
676         } finally {
677             ReferenceCountUtil.safeRelease(envelope);
678         }
679     }
680 
681     /**
682      * Handles a redirect answer if needed and returns {@code true} if a redirect query has been made.
683      */
684     private boolean handleRedirect(
685             DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
686             final DnsQueryLifecycleObserver queryLifecycleObserver, Promise<List<T>> promise) {
687         final DnsResponse res = envelope.content();
688 
689         // Check if we have answers, if not this may be an non authority NS and so redirects must be handled.
690         if (res.count(DnsSection.ANSWER) == 0) {
691             AuthoritativeNameServerList serverNames = extractAuthoritativeNameServers(question.name(), res);
692             if (serverNames != null) {
693                 int additionalCount = res.count(DnsSection.ADDITIONAL);
694 
695                 AuthoritativeDnsServerCache authoritativeDnsServerCache = authoritativeDnsServerCache();
696                 for (int i = 0; i < additionalCount; i++) {
697                     final DnsRecord r = res.recordAt(DnsSection.ADDITIONAL, i);
698 
699                     if (r.type() == DnsRecordType.A && !parent.supportsARecords() ||
700                         r.type() == DnsRecordType.AAAA && !parent.supportsAAAARecords()) {
701                         continue;
702                     }
703 
704                     // We may have multiple ADDITIONAL entries for the same nameserver name. For example one AAAA and
705                     // one A record.
706                     serverNames.handleWithAdditional(parent, r, authoritativeDnsServerCache);
707                 }
708 
709                 // Process all unresolved nameservers as well.
710                 serverNames.handleWithoutAdditionals(parent, resolveCache(), authoritativeDnsServerCache);
711 
712                 List<InetSocketAddress> addresses = serverNames.addressList();
713 
714                 // Give the user the chance to sort or filter the used servers for the query.
715                 DnsServerAddressStream serverStream = parent.newRedirectDnsServerStream(
716                         question.name(), addresses);
717 
718                 if (serverStream != null) {
719                     query(serverStream, 0, question,
720                           queryLifecycleObserver.queryRedirected(new DnsAddressStreamList(serverStream)),
721                           true, promise, null);
722                     return true;
723                 }
724             }
725         }
726         return false;
727     }
728 
729     private static Throwable cause(final DnsResponseCode code) {
730         assert code != null;
731         if (SERVFAIL.intValue() == code.intValue()) {
732             return SERVFAIL_QUERY_FAILED_EXCEPTION;
733         } else if (NXDOMAIN.intValue() == code.intValue()) {
734             return NXDOMAIN_CAUSE_QUERY_FAILED_EXCEPTION;
735         }
736 
737         return null;
738     }
739 
740     private static final class DnsAddressStreamList extends AbstractList<InetSocketAddress> {
741 
742         private final DnsServerAddressStream duplicate;
743         private List<InetSocketAddress> addresses;
744 
745         DnsAddressStreamList(DnsServerAddressStream stream) {
746             duplicate = stream.duplicate();
747         }
748 
749         @Override
750         public InetSocketAddress get(int index) {
751             if (addresses == null) {
752                 DnsServerAddressStream stream = duplicate.duplicate();
753                 addresses = new ArrayList<InetSocketAddress>(size());
754                 for (int i = 0; i < stream.size(); i++) {
755                     addresses.add(stream.next());
756                 }
757             }
758             return addresses.get(index);
759         }
760 
761         @Override
762         public int size() {
763             return duplicate.size();
764         }
765 
766         @Override
767         public Iterator<InetSocketAddress> iterator() {
768             return new Iterator<InetSocketAddress>() {
769                 private final DnsServerAddressStream stream = duplicate.duplicate();
770                 private int i;
771 
772                 @Override
773                 public boolean hasNext() {
774                     return i < stream.size();
775                 }
776 
777                 @Override
778                 public InetSocketAddress next() {
779                     if (!hasNext()) {
780                         throw new NoSuchElementException();
781                     }
782                     i++;
783                     return stream.next();
784                 }
785 
786                 @Override
787                 public void remove() {
788                     throw new UnsupportedOperationException();
789                 }
790             };
791         }
792     }
793 
794     /**
795      * Returns the {@code {@link AuthoritativeNameServerList} which were included in {@link DnsSection#AUTHORITY}
796      * or {@code null} if non are found.
797      */
798     private static AuthoritativeNameServerList extractAuthoritativeNameServers(String questionName, DnsResponse res) {
799         int authorityCount = res.count(DnsSection.AUTHORITY);
800         if (authorityCount == 0) {
801             return null;
802         }
803 
804         AuthoritativeNameServerList serverNames = new AuthoritativeNameServerList(questionName);
805         for (int i = 0; i < authorityCount; i++) {
806             serverNames.add(res.recordAt(DnsSection.AUTHORITY, i));
807         }
808         return serverNames.isEmpty() ? null : serverNames;
809     }
810 
811     private void onExpectedResponse(
812             DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
813             final DnsQueryLifecycleObserver queryLifecycleObserver, Promise<List<T>> promise) {
814 
815         // We often get a bunch of CNAMES as well when we asked for A/AAAA.
816         final DnsResponse response = envelope.content();
817         final Map<String, String> cnames = buildAliasMap(question.name(), response, cnameCache(), parent.executor());
818         final int answerCount = response.count(DnsSection.ANSWER);
819 
820         boolean found = false;
821         boolean completeEarly = this.completeEarly;
822         boolean cnameNeedsFollow = !cnames.isEmpty();
823         for (int i = 0; i < answerCount; i ++) {
824             final DnsRecord r = response.recordAt(DnsSection.ANSWER, i);
825             final DnsRecordType type = r.type();
826             boolean matches = false;
827             for (DnsRecordType expectedType : expectedTypes) {
828                 if (type == expectedType) {
829                     matches = true;
830                     break;
831                 }
832             }
833 
834             if (!matches) {
835                 continue;
836             }
837 
838             final String questionName = question.name().toLowerCase(Locale.US);
839             final String recordName = r.name().toLowerCase(Locale.US);
840 
841             // Make sure the record is for the questioned domain.
842             if (!recordName.equals(questionName)) {
843                 Map<String, String> cnamesCopy = new HashMap<String, String>(cnames);
844                 // Even if the record's name is not exactly same, it might be an alias defined in the CNAME records.
845                 String resolved = questionName;
846                 do {
847                     resolved = cnamesCopy.remove(resolved);
848                     if (recordName.equals(resolved)) {
849                         // We followed a CNAME chain that was part of the response without any extra queries.
850                         cnameNeedsFollow = false;
851                         break;
852                     }
853                 } while (resolved != null);
854 
855                 if (resolved == null) {
856                     assert questionName.isEmpty() || questionName.charAt(questionName.length() - 1) == '.';
857 
858                     for (String searchDomain : parent.searchDomains()) {
859                         if (searchDomain.isEmpty()) {
860                             continue;
861                         }
862 
863                         final String fqdn;
864                         if (searchDomain.charAt(searchDomain.length() - 1) == '.') {
865                             fqdn = questionName + searchDomain;
866                         } else {
867                             fqdn = questionName + searchDomain + '.';
868                         }
869                         if (recordName.equals(fqdn)) {
870                             resolved = recordName;
871                             break;
872                         }
873                     }
874                     if (resolved == null) {
875                         if (logger.isDebugEnabled()) {
876                             logger.debug("{} Ignoring record {} for [{}: {}] as it contains a different name than " +
877                                             "the question name [{}]. Cnames: {}, Search domains: {}",
878                                     channel, r.toString(), response.id(), envelope.sender(),
879                                     questionName, cnames, parent.searchDomains());
880                         }
881                         continue;
882                     }
883                 }
884             }
885 
886             final T converted = convertRecord(r, hostname, additionals, parent.executor());
887             if (converted == null) {
888                 if (logger.isDebugEnabled()) {
889                     logger.debug("{} Ignoring record {} for [{}: {}] as the converted record is null. "
890                                     + "Hostname [{}], Additionals: {}",
891                             channel, r.toString(), response.id(),
892                             envelope.sender(), hostname, additionals);
893                 }
894                 continue;
895             }
896 
897             boolean shouldRelease = false;
898             // Check if we did determine we wanted to complete early before. If this is the case we want to not
899             // include the result
900             if (!completeEarly) {
901                 completeEarly = isCompleteEarly(converted);
902             }
903 
904             // Check if the promise was done already, and only if not add things to the finalResult. Otherwise lets
905             // just release things after we cached it.
906             if (!promise.isDone()) {
907                 // We want to ensure we do not have duplicates in finalResult as this may be unexpected.
908                 //
909                 // While using a LinkedHashSet or HashSet may sound like the perfect fit for this we will use an
910                 // ArrayList here as duplicates should be found quite unfrequently in the wild and we dont want to pay
911                 // for the extra memory copy and allocations in this cases later on.
912                 if (finalResult == null) {
913                     finalResult = new ArrayList<T>(8);
914                     finalResult.add(converted);
915                 } else if (isDuplicateAllowed() || !finalResult.contains(converted)) {
916                     finalResult.add(converted);
917                 } else {
918                     shouldRelease = true;
919                 }
920             } else {
921                 shouldRelease = true;
922             }
923 
924             cache(hostname, additionals, r, converted);
925             found = true;
926 
927             if (shouldRelease) {
928                 ReferenceCountUtil.release(converted);
929             }
930             // Note that we do not break from the loop here, so we decode/cache all A/AAAA records.
931         }
932 
933         if (found && !cnameNeedsFollow) {
934             // If we found the correct result we can just stop here without following any extra CNAME records in the
935             // response.
936             if (completeEarly) {
937                 this.completeEarly = true;
938             }
939             queryLifecycleObserver.querySucceed();
940         } else if (cnames.isEmpty()) {
941             queryLifecycleObserver.queryFailed(NO_MATCHING_RECORD_QUERY_FAILED_EXCEPTION);
942         } else {
943             queryLifecycleObserver.querySucceed();
944             // We also got a CNAME so we need to ensure we also query it.
945             onResponseCNAME(question, cnames, newDnsQueryLifecycleObserver(question), promise);
946         }
947     }
948 
949     private void onResponseCNAME(
950             DnsQuestion question, Map<String, String> cnames,
951             final DnsQueryLifecycleObserver queryLifecycleObserver,
952             Promise<List<T>> promise) {
953 
954         // Resolve the host name in the question into the real host name.
955         String resolved = question.name().toLowerCase(Locale.US);
956         boolean found = false;
957         while (!cnames.isEmpty()) { // Do not attempt to call Map.remove() when the Map is empty
958                                     // because it can be Collections.emptyMap()
959                                     // whose remove() throws a UnsupportedOperationException.
960             final String next = cnames.remove(resolved);
961             if (next != null) {
962                 found = true;
963                 resolved = next;
964             } else {
965                 break;
966             }
967         }
968 
969         if (found) {
970             followCname(question, resolved, queryLifecycleObserver, promise);
971         } else {
972             queryLifecycleObserver.queryFailed(CNAME_NOT_FOUND_QUERY_FAILED_EXCEPTION);
973         }
974     }
975 
976     private static Map<String, String> buildAliasMap(
977             String queryName, DnsResponse response, DnsCnameCache cache, EventLoop loop) {
978         final int answerCount = response.count(DnsSection.ANSWER);
979         Map<String, String> cnames = null;
980         for (int i = 0; i < answerCount; i ++) {
981             final DnsRecord r = response.recordAt(DnsSection.ANSWER, i);
982             final DnsRecordType type = r.type();
983             if (type != DnsRecordType.CNAME) {
984                 continue;
985             }
986 
987             if (!(r instanceof DnsRawRecord)) {
988                 continue;
989             }
990 
991             final ByteBuf recordContent = ((ByteBufHolder) r).content();
992             final String domainName = decodeDomainName(recordContent);
993             if (domainName == null) {
994                 continue;
995             }
996 
997             if (cnames == null) {
998                 cnames = new HashMap<String, String>(min(8, answerCount));
999             }
1000 
1001             String name = r.name().toLowerCase(Locale.US);
1002             String mapping = domainName.toLowerCase(Locale.US);
1003 
1004             // Cache the CNAME as well.
1005             String nameWithDot = hostnameWithDot(name);
1006             String mappingWithDot = hostnameWithDot(mapping);
1007             if (!nameWithDot.equalsIgnoreCase(mappingWithDot)) {
1008                 String queryNameWithDot = hostnameWithDot(queryName.toLowerCase(Locale.US));
1009                 // Only cache the CNAME if the owner is in the bailiwick of the original query name.
1010                 boolean inBailiwick = nameWithDot.equals(queryNameWithDot) ||
1011                         nameWithDot.endsWith("." + queryNameWithDot);
1012                 if (inBailiwick) {
1013                     cache.cache(nameWithDot, mappingWithDot, r.timeToLive(), loop);
1014                 }
1015                 cnames.put(name, mapping);
1016             }
1017         }
1018 
1019         return cnames != null? cnames : Collections.<String, String>emptyMap();
1020     }
1021 
1022     private void tryToFinishResolve(final DnsServerAddressStream nameServerAddrStream,
1023                                     final int nameServerAddrStreamIndex,
1024                                     final DnsQuestion question,
1025                                     final DnsQueryLifecycleObserver queryLifecycleObserver,
1026                                     final Promise<List<T>> promise,
1027                                     final Throwable cause) {
1028 
1029         // There are no queries left to try.
1030         if (!completeEarly && !queriesInProgress.isEmpty()) {
1031             queryLifecycleObserver.queryCancelled(allowedQueries);
1032 
1033             // There are still some queries in process, we will try to notify once the next one finishes until
1034             // all are finished.
1035             return;
1036         }
1037 
1038         // There are no queries left to try.
1039         if (finalResult == null) {
1040             if (nameServerAddrStreamIndex < nameServerAddrStream.size()) {
1041                 if (queryLifecycleObserver == NoopDnsQueryLifecycleObserver.INSTANCE) {
1042                     // If the queryLifecycleObserver has already been terminated we should create a new one for this
1043                     // fresh query.
1044                     query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question,
1045                           newDnsQueryLifecycleObserver(question), true, promise, cause);
1046                 } else {
1047                     query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, queryLifecycleObserver,
1048                           true, promise, cause);
1049                 }
1050                 return;
1051             }
1052 
1053             queryLifecycleObserver.queryFailed(NAME_SERVERS_EXHAUSTED_EXCEPTION);
1054 
1055             // .. and we could not find any expected records.
1056 
1057             // The following is of questionable benefit, but has been around for a while that
1058             // it may be risky to remove. Reference https://datatracker.ietf.org/doc/html/rfc8020
1059             // - If we receive NXDOMAIN we know the domain doesn't exist, any other lookup type is meaningless.
1060             // - If we receive SERVFAIL, and we attempt a CNAME that returns NOERROR with 0 answers, it may lead the
1061             //   call-site to invalidate previously advertised addresses.
1062             // Having said that, there is the case of DNS services that don't respect the protocol either
1063             // - An A lookup could result in NXDOMAIN but a CNAME may succeed with answers.
1064             // It's an imperfect world. Accept it.
1065             // Guarding it with a system property, as an opt-in functionality.
1066             if (TRY_FINAL_CNAME_ON_ADDRESS_LOOKUPS) {
1067                 // If cause != null we know this was caused by a timeout / cancel / transport exception. In this case we
1068                 // won't try to resolve the CNAME as we only should do this if we could not get the expected records
1069                 // because they do not exist and the DNS server did probably signal it.
1070                 final boolean isValidResponse =
1071                         cause == NXDOMAIN_CAUSE_QUERY_FAILED_EXCEPTION || cause == SERVFAIL_QUERY_FAILED_EXCEPTION;
1072                 if ((cause == null || isValidResponse) && !triedCNAME &&
1073                         (question.type() == DnsRecordType.A || question.type() == DnsRecordType.AAAA)) {
1074                     // As the last resort, try to query CNAME, just in case the name server has it.
1075                     triedCNAME = true;
1076 
1077                     query(hostname, DnsRecordType.CNAME, getNameServers(hostname), true, promise);
1078                     return;
1079                 }
1080             }
1081         } else {
1082             queryLifecycleObserver.queryCancelled(allowedQueries);
1083         }
1084 
1085         // We have at least one resolved record or tried CNAME as the last resort.
1086         finishResolve(promise, cause);
1087     }
1088 
1089     private void finishResolve(Promise<List<T>> promise, Throwable cause) {
1090         // If completeEarly was true we still want to continue processing the queries to ensure we still put everything
1091         // in the cache eventually.
1092         if (!completeEarly && !queriesInProgress.isEmpty()) {
1093             // If there are queries in progress, we should cancel it because we already finished the resolution.
1094             for (Iterator<Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> i = queriesInProgress.iterator();
1095                  i.hasNext();) {
1096                 Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> f = i.next();
1097                 i.remove();
1098 
1099                 f.cancel(false);
1100             }
1101         }
1102 
1103         if (finalResult != null) {
1104             if (!promise.isDone()) {
1105                 // Found at least one resolved record.
1106                 final List<T> result = filterResults(finalResult);
1107                 // Lets replace the previous stored result.
1108                 finalResult = Collections.emptyList();
1109                 if (!DnsNameResolver.trySuccess(promise, result)) {
1110                     for (T item : result) {
1111                         ReferenceCountUtil.safeRelease(item);
1112                     }
1113                 }
1114             } else {
1115                 // This should always be the case as we replaced the list once notify the promise with an empty one
1116                 // and never add to it again.
1117                 assert finalResult.isEmpty();
1118             }
1119             return;
1120         }
1121 
1122         // No resolved address found.
1123         final int maxAllowedQueries = parent.maxQueriesPerResolve();
1124         final int tries = maxAllowedQueries - allowedQueries;
1125         final StringBuilder buf = new StringBuilder(64);
1126 
1127         buf.append("Failed to resolve '").append(hostname).append("' ").append(Arrays.toString(expectedTypes));
1128         if (tries > 1) {
1129             if (tries < maxAllowedQueries) {
1130                 buf.append(" after ")
1131                    .append(tries)
1132                    .append(" queries ");
1133             } else {
1134                 buf.append(". Exceeded max queries per resolve ")
1135                 .append(maxAllowedQueries)
1136                 .append(' ');
1137             }
1138         }
1139         final UnknownHostException unknownHostException = new UnknownHostException(buf.toString());
1140         if (cause == null) {
1141             // Only cache if the failure was not because of an IO error / timeout that was caused by the query
1142             // itself.
1143             cache(hostname, additionals, unknownHostException);
1144         } else {
1145             unknownHostException.initCause(cause);
1146         }
1147         promise.tryFailure(unknownHostException);
1148     }
1149 
1150     static String decodeDomainName(ByteBuf in) {
1151         in.markReaderIndex();
1152         try {
1153             return DefaultDnsRecordDecoder.decodeName(in);
1154         } catch (CorruptedFrameException e) {
1155             // In this case we just return null.
1156             return null;
1157         } finally {
1158             in.resetReaderIndex();
1159         }
1160     }
1161 
1162     private DnsServerAddressStream getNameServers(String name) {
1163         DnsServerAddressStream stream = getNameServersFromCache(name);
1164         if (stream == null) {
1165             // We need to obtain a new stream from the parent DnsNameResolver if the hostname is not the same as
1166             // for the original query (for example we may follow CNAMEs). Otherwise let's just duplicate the
1167             // original nameservers so we correctly update the internal index
1168             if (name.equals(hostname)) {
1169                 return nameServerAddrs.duplicate();
1170             }
1171             return parent.newNameServerAddressStream(name);
1172         }
1173         return stream;
1174     }
1175 
1176     private void followCname(DnsQuestion question, String cname, DnsQueryLifecycleObserver queryLifecycleObserver,
1177                              Promise<List<T>> promise) {
1178         final DnsQuestion cnameQuestion;
1179         final DnsServerAddressStream stream;
1180         try {
1181             cname = cnameResolveFromCache(cnameCache(), cname);
1182             stream = getNameServers(cname);
1183             cnameQuestion = new DefaultDnsQuestion(cname, question.type(), dnsClass);
1184         } catch (Throwable cause) {
1185             queryLifecycleObserver.queryFailed(cause);
1186             PlatformDependent.throwException(cause);
1187             return;
1188         }
1189         query(stream, 0, cnameQuestion, queryLifecycleObserver.queryCNAMEd(cnameQuestion),
1190               true, promise, null);
1191     }
1192 
1193     private boolean query(String hostname, DnsRecordType type, DnsServerAddressStream dnsServerAddressStream,
1194                           boolean flush, Promise<List<T>> promise) {
1195         final DnsQuestion question;
1196         try {
1197             question = new DefaultDnsQuestion(hostname, type, dnsClass);
1198         } catch (Throwable cause) {
1199             // Assume a single failure means that queries will succeed. If the hostname is invalid for one type
1200             // there is no case where it is known to be valid for another type.
1201             promise.tryFailure(new IllegalArgumentException("Unable to create DNS Question for: [" + hostname + ", " +
1202                     type + ']', cause));
1203             return false;
1204         }
1205         query(dnsServerAddressStream, 0, question, newDnsQueryLifecycleObserver(question), flush, promise, null);
1206         return true;
1207     }
1208 
1209     private DnsQueryLifecycleObserver newDnsQueryLifecycleObserver(DnsQuestion question) {
1210         return parent.dnsQueryLifecycleObserverFactory().newDnsQueryLifecycleObserver(question);
1211     }
1212 
1213     private final class CombinedDnsServerAddressStream implements DnsServerAddressStream {
1214         private final InetSocketAddress replaced;
1215         private final DnsServerAddressStream originalStream;
1216         private final List<InetAddress> resolvedAddresses;
1217         private Iterator<InetAddress> resolved;
1218 
1219         CombinedDnsServerAddressStream(InetSocketAddress replaced, List<InetAddress> resolvedAddresses,
1220                                        DnsServerAddressStream originalStream) {
1221             this.replaced = replaced;
1222             this.resolvedAddresses = resolvedAddresses;
1223             this.originalStream = originalStream;
1224             resolved = resolvedAddresses.iterator();
1225         }
1226 
1227         @Override
1228         public InetSocketAddress next() {
1229             if (resolved.hasNext()) {
1230                 return nextResolved0();
1231             }
1232             InetSocketAddress address = originalStream.next();
1233             if (address.equals(replaced)) {
1234                 resolved = resolvedAddresses.iterator();
1235                 return nextResolved0();
1236             }
1237             return address;
1238         }
1239 
1240         private InetSocketAddress nextResolved0() {
1241             return parent.newRedirectServerAddress(resolved.next());
1242         }
1243 
1244         @Override
1245         public int size() {
1246             return originalStream.size() + resolvedAddresses.size() - 1;
1247         }
1248 
1249         @Override
1250         public DnsServerAddressStream duplicate() {
1251             return new CombinedDnsServerAddressStream(replaced, resolvedAddresses, originalStream.duplicate());
1252         }
1253     }
1254 
1255     /**
1256      * Holds the closed DNS Servers for a domain.
1257      */
1258     private static final class AuthoritativeNameServerList {
1259 
1260         private final String questionName;
1261 
1262         // We not expect the linked-list to be very long so a double-linked-list is overkill.
1263         private AuthoritativeNameServer head;
1264 
1265         private int nameServerCount;
1266 
1267         AuthoritativeNameServerList(String questionName) {
1268             this.questionName = questionName.toLowerCase(Locale.US);
1269         }
1270 
1271         void add(DnsRecord r) {
1272             if (r.type() != DnsRecordType.NS || !(r instanceof DnsRawRecord)) {
1273                 return;
1274             }
1275 
1276             // Only include servers that serve the correct domain.
1277             if (questionName.length() <  r.name().length()) {
1278                 return;
1279             }
1280 
1281             String recordName = r.name().toLowerCase(Locale.US);
1282 
1283             int dots = 0;
1284             for (int a = recordName.length() - 1, b = questionName.length() - 1; a >= 0; a--, b--) {
1285                 char c = recordName.charAt(a);
1286                 if (questionName.charAt(b) != c) {
1287                     return;
1288                 }
1289                 if (c == '.') {
1290                     dots++;
1291                 }
1292             }
1293 
1294             if (head != null && head.dots > dots) {
1295                 // We already have a closer match so ignore this one, no need to parse the domainName etc.
1296                 return;
1297             }
1298 
1299             final ByteBuf recordContent = ((ByteBufHolder) r).content();
1300             final String domainName = decodeDomainName(recordContent);
1301             if (domainName == null) {
1302                 // Could not be parsed, ignore.
1303                 return;
1304             }
1305 
1306             // We are only interested in preserving the nameservers which are the closest to our qName, so ensure
1307             // we drop servers that have a smaller dots count.
1308             if (head == null || head.dots < dots) {
1309                 nameServerCount = 1;
1310                 head = new AuthoritativeNameServer(dots, r.timeToLive(), recordName, domainName);
1311             } else if (head.dots == dots) {
1312                 AuthoritativeNameServer serverName = head;
1313                 while (serverName.next != null) {
1314                     serverName = serverName.next;
1315                 }
1316                 serverName.next = new AuthoritativeNameServer(dots, r.timeToLive(), recordName, domainName);
1317                 nameServerCount++;
1318             }
1319         }
1320 
1321         void handleWithAdditional(
1322                 DnsNameResolver parent, DnsRecord r, AuthoritativeDnsServerCache authoritativeCache) {
1323             // Just walk the linked-list and mark the entry as handled when matched.
1324             AuthoritativeNameServer serverName = head;
1325 
1326             String nsName = r.name();
1327             InetAddress resolved = decodeAddress(r, nsName, parent.isDecodeIdn());
1328             if (resolved == null) {
1329                 // Could not parse the address, just ignore.
1330                 return;
1331             }
1332 
1333             while (serverName != null) {
1334                 if (serverName.nsName.equalsIgnoreCase(nsName)) {
1335                     if (serverName.address != null) {
1336                         // We received multiple ADDITIONAL records for the same name.
1337                         // Search for the last we insert before and then append a new one.
1338                         while (serverName.next != null && serverName.next.isCopy) {
1339                             serverName = serverName.next;
1340                         }
1341                         AuthoritativeNameServer server = new AuthoritativeNameServer(serverName);
1342                         server.next = serverName.next;
1343                         serverName.next = server;
1344                         serverName = server;
1345 
1346                         nameServerCount++;
1347                     }
1348                     // We should replace the TTL if needed with the one of the ADDITIONAL record so we use
1349                     // the smallest for caching.
1350                     serverName.update(parent.newRedirectServerAddress(resolved), r.timeToLive());
1351 
1352                     // Cache the server now.
1353                     cache(serverName, authoritativeCache, parent.executor());
1354                     return;
1355                 }
1356                 serverName = serverName.next;
1357             }
1358         }
1359 
1360         // Now handle all AuthoritativeNameServer for which we had no ADDITIONAL record
1361         void handleWithoutAdditionals(
1362                 DnsNameResolver parent, DnsCache cache, AuthoritativeDnsServerCache authoritativeCache) {
1363             AuthoritativeNameServer serverName = head;
1364 
1365             while (serverName != null) {
1366                 if (serverName.address == null) {
1367                     // These will be resolved on the fly if needed.
1368                     cacheUnresolved(serverName, authoritativeCache, parent.executor());
1369 
1370                     // Try to resolve via cache as we had no ADDITIONAL entry for the server.
1371 
1372                     List<? extends DnsCacheEntry> entries = cache.get(serverName.nsName, null);
1373                     if (entries != null && !entries.isEmpty()) {
1374                         InetAddress address = entries.get(0).address();
1375 
1376                         // If address is null we have a resolution failure cached so just use an unresolved address.
1377                         if (address != null) {
1378                             serverName.update(parent.newRedirectServerAddress(address));
1379 
1380                             for (int i = 1; i < entries.size(); i++) {
1381                                 address = entries.get(i).address();
1382 
1383                                 assert address != null :
1384                                         "Cache returned a cached failure, should never return anything else";
1385 
1386                                 AuthoritativeNameServer server = new AuthoritativeNameServer(serverName);
1387                                 server.next = serverName.next;
1388                                 serverName.next = server;
1389                                 serverName = server;
1390                                 serverName.update(parent.newRedirectServerAddress(address));
1391 
1392                                 nameServerCount++;
1393                             }
1394                         }
1395                     }
1396                 }
1397                 serverName = serverName.next;
1398             }
1399         }
1400 
1401         private void cacheUnresolved(
1402                 AuthoritativeNameServer server, AuthoritativeDnsServerCache authoritativeCache, EventLoop loop) {
1403             // We still want to cached the unresolved address
1404             server.address = InetSocketAddress.createUnresolved(
1405                     server.nsName, DefaultDnsServerAddressStreamProvider.DNS_PORT);
1406 
1407             // Cache the server now.
1408             cache(server, authoritativeCache, loop);
1409         }
1410 
1411         private void cache(AuthoritativeNameServer server, AuthoritativeDnsServerCache cache, EventLoop loop) {
1412             // Cache NS record if not for a root server as we should never cache for root servers.
1413             if (server.isRootServer()) {
1414                 return;
1415             }
1416             // Bailiwick check (RFC 2181 §5.4.1): only cache a nameserver entry when its zone
1417             // equals the question name or is a subdomain of it. A server that is authoritative
1418             // for a child zone must not be trusted to supply authoritative NS records for a
1419             // parent zone, which would allow cache poisoning of the parent.
1420             if (!server.domainName.equals(questionName) &&
1421                     !server.domainName.endsWith("." + questionName)) {
1422                 return;
1423             }
1424             cache.cache(server.domainName, server.address, server.ttl, loop);
1425         }
1426 
1427         /**
1428          * Returns {@code true} if empty, {@code false} otherwise.
1429          */
1430         boolean isEmpty() {
1431             return nameServerCount == 0;
1432         }
1433 
1434         /**
1435          * Creates a new {@link List} which holds the {@link InetSocketAddress}es.
1436          */
1437         List<InetSocketAddress> addressList() {
1438             List<InetSocketAddress> addressList = new ArrayList<InetSocketAddress>(nameServerCount);
1439 
1440             AuthoritativeNameServer server = head;
1441             while (server != null) {
1442                 if (server.address != null) {
1443                     addressList.add(server.address);
1444                 }
1445                 server = server.next;
1446             }
1447             return addressList;
1448         }
1449     }
1450 
1451     private static final class AuthoritativeNameServer {
1452         private final int dots;
1453         private final String domainName;
1454         final boolean isCopy;
1455         final String nsName;
1456 
1457         private long ttl;
1458         private InetSocketAddress address;
1459 
1460         AuthoritativeNameServer next;
1461 
1462         AuthoritativeNameServer(int dots, long ttl, String domainName, String nsName) {
1463             this.dots = dots;
1464             this.ttl = ttl;
1465             this.nsName = nsName;
1466             this.domainName = domainName;
1467             isCopy = false;
1468         }
1469 
1470         AuthoritativeNameServer(AuthoritativeNameServer server) {
1471             dots = server.dots;
1472             ttl = server.ttl;
1473             nsName = server.nsName;
1474             domainName = server.domainName;
1475             isCopy = true;
1476         }
1477 
1478         /**
1479          * Returns {@code true} if its a root server.
1480          */
1481         boolean isRootServer() {
1482             return dots == 1;
1483         }
1484 
1485         /**
1486          * Update the server with the given address and TTL if needed.
1487          */
1488         void update(InetSocketAddress address, long ttl) {
1489             assert this.address == null || this.address.isUnresolved();
1490             this.address = address;
1491             this.ttl = min(this.ttl, ttl);
1492         }
1493 
1494         void update(InetSocketAddress address) {
1495             update(address, Long.MAX_VALUE);
1496         }
1497     }
1498 }