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