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, buildAliasMap(envelope.content(), cnameCache(), parent.executor()),
626                                     queryLifecycleObserver, promise);
627                     return;
628                 }
629 
630                 for (DnsRecordType expectedType : expectedTypes) {
631                     if (type == expectedType) {
632                         onExpectedResponse(question, envelope, queryLifecycleObserver, promise);
633                         return;
634                     }
635                 }
636 
637                 queryLifecycleObserver.queryFailed(UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION);
638                 return;
639             }
640 
641             // Retry with the next server if the server did not tell us that the domain does not exist.
642             if (code != DnsResponseCode.NXDOMAIN) {
643                 query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question,
644                       queryLifecycleObserver.queryNoAnswer(code), true, promise, cause(code));
645             } else {
646                 queryLifecycleObserver.queryFailed(NXDOMAIN_QUERY_FAILED_EXCEPTION);
647 
648                 // Try with the next server if is not authoritative for the domain.
649                 //
650                 // From https://tools.ietf.org/html/rfc1035 :
651                 //
652                 //   RCODE        Response code - this 4 bit field is set as part of
653                 //                responses.  The values have the following
654                 //                interpretation:
655                 //
656                 //                ....
657                 //                ....
658                 //
659                 //                3               Name Error - Meaningful only for
660                 //                                responses from an authoritative name
661                 //                                server, this code signifies that the
662                 //                                domain name referenced in the query does
663                 //                                not exist.
664                 //                ....
665                 //                ....
666                 if (!res.isAuthoritativeAnswer()) {
667                     query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question,
668                             newDnsQueryLifecycleObserver(question), true, promise, cause(code));
669                 } else {
670                     // Failed with NX cause - distinction between NXDOMAIN vs a timeout
671                     tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question,
672                             queryLifecycleObserver, promise, NXDOMAIN_CAUSE_QUERY_FAILED_EXCEPTION);
673                 }
674             }
675         } finally {
676             ReferenceCountUtil.safeRelease(envelope);
677         }
678     }
679 
680     /**
681      * Handles a redirect answer if needed and returns {@code true} if a redirect query has been made.
682      */
683     private boolean handleRedirect(
684             DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
685             final DnsQueryLifecycleObserver queryLifecycleObserver, Promise<List<T>> promise) {
686         final DnsResponse res = envelope.content();
687 
688         // Check if we have answers, if not this may be an non authority NS and so redirects must be handled.
689         if (res.count(DnsSection.ANSWER) == 0) {
690             AuthoritativeNameServerList serverNames = extractAuthoritativeNameServers(question.name(), res);
691             if (serverNames != null) {
692                 int additionalCount = res.count(DnsSection.ADDITIONAL);
693 
694                 AuthoritativeDnsServerCache authoritativeDnsServerCache = authoritativeDnsServerCache();
695                 for (int i = 0; i < additionalCount; i++) {
696                     final DnsRecord r = res.recordAt(DnsSection.ADDITIONAL, i);
697 
698                     if (r.type() == DnsRecordType.A && !parent.supportsARecords() ||
699                         r.type() == DnsRecordType.AAAA && !parent.supportsAAAARecords()) {
700                         continue;
701                     }
702 
703                     // We may have multiple ADDITIONAL entries for the same nameserver name. For example one AAAA and
704                     // one A record.
705                     serverNames.handleWithAdditional(parent, r, authoritativeDnsServerCache);
706                 }
707 
708                 // Process all unresolved nameservers as well.
709                 serverNames.handleWithoutAdditionals(parent, resolveCache(), authoritativeDnsServerCache);
710 
711                 List<InetSocketAddress> addresses = serverNames.addressList();
712 
713                 // Give the user the chance to sort or filter the used servers for the query.
714                 DnsServerAddressStream serverStream = parent.newRedirectDnsServerStream(
715                         question.name(), addresses);
716 
717                 if (serverStream != null) {
718                     query(serverStream, 0, question,
719                           queryLifecycleObserver.queryRedirected(new DnsAddressStreamList(serverStream)),
720                           true, promise, null);
721                     return true;
722                 }
723             }
724         }
725         return false;
726     }
727 
728     private static Throwable cause(final DnsResponseCode code) {
729         assert code != null;
730         if (SERVFAIL.intValue() == code.intValue()) {
731             return SERVFAIL_QUERY_FAILED_EXCEPTION;
732         } else if (NXDOMAIN.intValue() == code.intValue()) {
733             return NXDOMAIN_CAUSE_QUERY_FAILED_EXCEPTION;
734         }
735 
736         return null;
737     }
738 
739     private static final class DnsAddressStreamList extends AbstractList<InetSocketAddress> {
740 
741         private final DnsServerAddressStream duplicate;
742         private List<InetSocketAddress> addresses;
743 
744         DnsAddressStreamList(DnsServerAddressStream stream) {
745             duplicate = stream.duplicate();
746         }
747 
748         @Override
749         public InetSocketAddress get(int index) {
750             if (addresses == null) {
751                 DnsServerAddressStream stream = duplicate.duplicate();
752                 addresses = new ArrayList<InetSocketAddress>(size());
753                 for (int i = 0; i < stream.size(); i++) {
754                     addresses.add(stream.next());
755                 }
756             }
757             return addresses.get(index);
758         }
759 
760         @Override
761         public int size() {
762             return duplicate.size();
763         }
764 
765         @Override
766         public Iterator<InetSocketAddress> iterator() {
767             return new Iterator<InetSocketAddress>() {
768                 private final DnsServerAddressStream stream = duplicate.duplicate();
769                 private int i;
770 
771                 @Override
772                 public boolean hasNext() {
773                     return i < stream.size();
774                 }
775 
776                 @Override
777                 public InetSocketAddress next() {
778                     if (!hasNext()) {
779                         throw new NoSuchElementException();
780                     }
781                     i++;
782                     return stream.next();
783                 }
784 
785                 @Override
786                 public void remove() {
787                     throw new UnsupportedOperationException();
788                 }
789             };
790         }
791     }
792 
793     /**
794      * Returns the {@code {@link AuthoritativeNameServerList} which were included in {@link DnsSection#AUTHORITY}
795      * or {@code null} if non are found.
796      */
797     private static AuthoritativeNameServerList extractAuthoritativeNameServers(String questionName, DnsResponse res) {
798         int authorityCount = res.count(DnsSection.AUTHORITY);
799         if (authorityCount == 0) {
800             return null;
801         }
802 
803         AuthoritativeNameServerList serverNames = new AuthoritativeNameServerList(questionName);
804         for (int i = 0; i < authorityCount; i++) {
805             serverNames.add(res.recordAt(DnsSection.AUTHORITY, i));
806         }
807         return serverNames.isEmpty() ? null : serverNames;
808     }
809 
810     private void onExpectedResponse(
811             DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
812             final DnsQueryLifecycleObserver queryLifecycleObserver, Promise<List<T>> promise) {
813 
814         // We often get a bunch of CNAMES as well when we asked for A/AAAA.
815         final DnsResponse response = envelope.content();
816         final Map<String, String> cnames = buildAliasMap(response, cnameCache(), parent.executor());
817         final int answerCount = response.count(DnsSection.ANSWER);
818 
819         boolean found = false;
820         boolean completeEarly = this.completeEarly;
821         boolean cnameNeedsFollow = !cnames.isEmpty();
822         for (int i = 0; i < answerCount; i ++) {
823             final DnsRecord r = response.recordAt(DnsSection.ANSWER, i);
824             final DnsRecordType type = r.type();
825             boolean matches = false;
826             for (DnsRecordType expectedType : expectedTypes) {
827                 if (type == expectedType) {
828                     matches = true;
829                     break;
830                 }
831             }
832 
833             if (!matches) {
834                 continue;
835             }
836 
837             final String questionName = question.name().toLowerCase(Locale.US);
838             final String recordName = r.name().toLowerCase(Locale.US);
839 
840             // Make sure the record is for the questioned domain.
841             if (!recordName.equals(questionName)) {
842                 Map<String, String> cnamesCopy = new HashMap<String, String>(cnames);
843                 // Even if the record's name is not exactly same, it might be an alias defined in the CNAME records.
844                 String resolved = questionName;
845                 do {
846                     resolved = cnamesCopy.remove(resolved);
847                     if (recordName.equals(resolved)) {
848                         // We followed a CNAME chain that was part of the response without any extra queries.
849                         cnameNeedsFollow = false;
850                         break;
851                     }
852                 } while (resolved != null);
853 
854                 if (resolved == null) {
855                     assert questionName.isEmpty() || questionName.charAt(questionName.length() - 1) == '.';
856 
857                     for (String searchDomain : parent.searchDomains()) {
858                         if (searchDomain.isEmpty()) {
859                             continue;
860                         }
861 
862                         final String fqdn;
863                         if (searchDomain.charAt(searchDomain.length() - 1) == '.') {
864                             fqdn = questionName + searchDomain;
865                         } else {
866                             fqdn = questionName + searchDomain + '.';
867                         }
868                         if (recordName.equals(fqdn)) {
869                             resolved = recordName;
870                             break;
871                         }
872                     }
873                     if (resolved == null) {
874                         if (logger.isDebugEnabled()) {
875                             logger.debug("{} Ignoring record {} for [{}: {}] as it contains a different name than " +
876                                             "the question name [{}]. Cnames: {}, Search domains: {}",
877                                     channel, r.toString(), response.id(), envelope.sender(),
878                                     questionName, cnames, parent.searchDomains());
879                         }
880                         continue;
881                     }
882                 }
883             }
884 
885             final T converted = convertRecord(r, hostname, additionals, parent.executor());
886             if (converted == null) {
887                 if (logger.isDebugEnabled()) {
888                     logger.debug("{} Ignoring record {} for [{}: {}] as the converted record is null. "
889                                     + "Hostname [{}], Additionals: {}",
890                             channel, r.toString(), response.id(),
891                             envelope.sender(), hostname, additionals);
892                 }
893                 continue;
894             }
895 
896             boolean shouldRelease = false;
897             // Check if we did determine we wanted to complete early before. If this is the case we want to not
898             // include the result
899             if (!completeEarly) {
900                 completeEarly = isCompleteEarly(converted);
901             }
902 
903             // Check if the promise was done already, and only if not add things to the finalResult. Otherwise lets
904             // just release things after we cached it.
905             if (!promise.isDone()) {
906                 // We want to ensure we do not have duplicates in finalResult as this may be unexpected.
907                 //
908                 // While using a LinkedHashSet or HashSet may sound like the perfect fit for this we will use an
909                 // ArrayList here as duplicates should be found quite unfrequently in the wild and we dont want to pay
910                 // for the extra memory copy and allocations in this cases later on.
911                 if (finalResult == null) {
912                     finalResult = new ArrayList<T>(8);
913                     finalResult.add(converted);
914                 } else if (isDuplicateAllowed() || !finalResult.contains(converted)) {
915                     finalResult.add(converted);
916                 } else {
917                     shouldRelease = true;
918                 }
919             } else {
920                 shouldRelease = true;
921             }
922 
923             cache(hostname, additionals, r, converted);
924             found = true;
925 
926             if (shouldRelease) {
927                 ReferenceCountUtil.release(converted);
928             }
929             // Note that we do not break from the loop here, so we decode/cache all A/AAAA records.
930         }
931 
932         if (found && !cnameNeedsFollow) {
933             // If we found the correct result we can just stop here without following any extra CNAME records in the
934             // response.
935             if (completeEarly) {
936                 this.completeEarly = true;
937             }
938             queryLifecycleObserver.querySucceed();
939         } else if (cnames.isEmpty()) {
940             queryLifecycleObserver.queryFailed(NO_MATCHING_RECORD_QUERY_FAILED_EXCEPTION);
941         } else {
942             queryLifecycleObserver.querySucceed();
943             // We also got a CNAME so we need to ensure we also query it.
944             onResponseCNAME(question, cnames, newDnsQueryLifecycleObserver(question), promise);
945         }
946     }
947 
948     private void onResponseCNAME(
949             DnsQuestion question, Map<String, String> cnames,
950             final DnsQueryLifecycleObserver queryLifecycleObserver,
951             Promise<List<T>> promise) {
952 
953         // Resolve the host name in the question into the real host name.
954         String resolved = question.name().toLowerCase(Locale.US);
955         boolean found = false;
956         while (!cnames.isEmpty()) { // Do not attempt to call Map.remove() when the Map is empty
957                                     // because it can be Collections.emptyMap()
958                                     // whose remove() throws a UnsupportedOperationException.
959             final String next = cnames.remove(resolved);
960             if (next != null) {
961                 found = true;
962                 resolved = next;
963             } else {
964                 break;
965             }
966         }
967 
968         if (found) {
969             followCname(question, resolved, queryLifecycleObserver, promise);
970         } else {
971             queryLifecycleObserver.queryFailed(CNAME_NOT_FOUND_QUERY_FAILED_EXCEPTION);
972         }
973     }
974 
975     private static Map<String, String> buildAliasMap(DnsResponse response, DnsCnameCache cache, EventLoop loop) {
976         final int answerCount = response.count(DnsSection.ANSWER);
977         Map<String, String> cnames = null;
978         for (int i = 0; i < answerCount; i ++) {
979             final DnsRecord r = response.recordAt(DnsSection.ANSWER, i);
980             final DnsRecordType type = r.type();
981             if (type != DnsRecordType.CNAME) {
982                 continue;
983             }
984 
985             if (!(r instanceof DnsRawRecord)) {
986                 continue;
987             }
988 
989             final ByteBuf recordContent = ((ByteBufHolder) r).content();
990             final String domainName = decodeDomainName(recordContent);
991             if (domainName == null) {
992                 continue;
993             }
994 
995             if (cnames == null) {
996                 cnames = new HashMap<String, String>(min(8, answerCount));
997             }
998 
999             String name = r.name().toLowerCase(Locale.US);
1000             String mapping = domainName.toLowerCase(Locale.US);
1001 
1002             // Cache the CNAME as well.
1003             String nameWithDot = hostnameWithDot(name);
1004             String mappingWithDot = hostnameWithDot(mapping);
1005             if (!nameWithDot.equalsIgnoreCase(mappingWithDot)) {
1006                 cache.cache(nameWithDot, mappingWithDot, r.timeToLive(), loop);
1007                 cnames.put(name, mapping);
1008             }
1009         }
1010 
1011         return cnames != null? cnames : Collections.<String, String>emptyMap();
1012     }
1013 
1014     private void tryToFinishResolve(final DnsServerAddressStream nameServerAddrStream,
1015                                     final int nameServerAddrStreamIndex,
1016                                     final DnsQuestion question,
1017                                     final DnsQueryLifecycleObserver queryLifecycleObserver,
1018                                     final Promise<List<T>> promise,
1019                                     final Throwable cause) {
1020 
1021         // There are no queries left to try.
1022         if (!completeEarly && !queriesInProgress.isEmpty()) {
1023             queryLifecycleObserver.queryCancelled(allowedQueries);
1024 
1025             // There are still some queries in process, we will try to notify once the next one finishes until
1026             // all are finished.
1027             return;
1028         }
1029 
1030         // There are no queries left to try.
1031         if (finalResult == null) {
1032             if (nameServerAddrStreamIndex < nameServerAddrStream.size()) {
1033                 if (queryLifecycleObserver == NoopDnsQueryLifecycleObserver.INSTANCE) {
1034                     // If the queryLifecycleObserver has already been terminated we should create a new one for this
1035                     // fresh query.
1036                     query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question,
1037                           newDnsQueryLifecycleObserver(question), true, promise, cause);
1038                 } else {
1039                     query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, queryLifecycleObserver,
1040                           true, promise, cause);
1041                 }
1042                 return;
1043             }
1044 
1045             queryLifecycleObserver.queryFailed(NAME_SERVERS_EXHAUSTED_EXCEPTION);
1046 
1047             // .. and we could not find any expected records.
1048 
1049             // The following is of questionable benefit, but has been around for a while that
1050             // it may be risky to remove. Reference https://datatracker.ietf.org/doc/html/rfc8020
1051             // - If we receive NXDOMAIN we know the domain doesn't exist, any other lookup type is meaningless.
1052             // - If we receive SERVFAIL, and we attempt a CNAME that returns NOERROR with 0 answers, it may lead the
1053             //   call-site to invalidate previously advertised addresses.
1054             // Having said that, there is the case of DNS services that don't respect the protocol either
1055             // - An A lookup could result in NXDOMAIN but a CNAME may succeed with answers.
1056             // It's an imperfect world. Accept it.
1057             // Guarding it with a system property, as an opt-in functionality.
1058             if (TRY_FINAL_CNAME_ON_ADDRESS_LOOKUPS) {
1059                 // If cause != null we know this was caused by a timeout / cancel / transport exception. In this case we
1060                 // won't try to resolve the CNAME as we only should do this if we could not get the expected records
1061                 // because they do not exist and the DNS server did probably signal it.
1062                 final boolean isValidResponse =
1063                         cause == NXDOMAIN_CAUSE_QUERY_FAILED_EXCEPTION || cause == SERVFAIL_QUERY_FAILED_EXCEPTION;
1064                 if ((cause == null || isValidResponse) && !triedCNAME &&
1065                         (question.type() == DnsRecordType.A || question.type() == DnsRecordType.AAAA)) {
1066                     // As the last resort, try to query CNAME, just in case the name server has it.
1067                     triedCNAME = true;
1068 
1069                     query(hostname, DnsRecordType.CNAME, getNameServers(hostname), true, promise);
1070                     return;
1071                 }
1072             }
1073         } else {
1074             queryLifecycleObserver.queryCancelled(allowedQueries);
1075         }
1076 
1077         // We have at least one resolved record or tried CNAME as the last resort.
1078         finishResolve(promise, cause);
1079     }
1080 
1081     private void finishResolve(Promise<List<T>> promise, Throwable cause) {
1082         // If completeEarly was true we still want to continue processing the queries to ensure we still put everything
1083         // in the cache eventually.
1084         if (!completeEarly && !queriesInProgress.isEmpty()) {
1085             // If there are queries in progress, we should cancel it because we already finished the resolution.
1086             for (Iterator<Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> i = queriesInProgress.iterator();
1087                  i.hasNext();) {
1088                 Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> f = i.next();
1089                 i.remove();
1090 
1091                 f.cancel(false);
1092             }
1093         }
1094 
1095         if (finalResult != null) {
1096             if (!promise.isDone()) {
1097                 // Found at least one resolved record.
1098                 final List<T> result = filterResults(finalResult);
1099                 // Lets replace the previous stored result.
1100                 finalResult = Collections.emptyList();
1101                 if (!DnsNameResolver.trySuccess(promise, result)) {
1102                     for (T item : result) {
1103                         ReferenceCountUtil.safeRelease(item);
1104                     }
1105                 }
1106             } else {
1107                 // This should always be the case as we replaced the list once notify the promise with an empty one
1108                 // and never add to it again.
1109                 assert finalResult.isEmpty();
1110             }
1111             return;
1112         }
1113 
1114         // No resolved address found.
1115         final int maxAllowedQueries = parent.maxQueriesPerResolve();
1116         final int tries = maxAllowedQueries - allowedQueries;
1117         final StringBuilder buf = new StringBuilder(64);
1118 
1119         buf.append("Failed to resolve '").append(hostname).append("' ").append(Arrays.toString(expectedTypes));
1120         if (tries > 1) {
1121             if (tries < maxAllowedQueries) {
1122                 buf.append(" after ")
1123                    .append(tries)
1124                    .append(" queries ");
1125             } else {
1126                 buf.append(". Exceeded max queries per resolve ")
1127                 .append(maxAllowedQueries)
1128                 .append(' ');
1129             }
1130         }
1131         final UnknownHostException unknownHostException = new UnknownHostException(buf.toString());
1132         if (cause == null) {
1133             // Only cache if the failure was not because of an IO error / timeout that was caused by the query
1134             // itself.
1135             cache(hostname, additionals, unknownHostException);
1136         } else {
1137             unknownHostException.initCause(cause);
1138         }
1139         promise.tryFailure(unknownHostException);
1140     }
1141 
1142     static String decodeDomainName(ByteBuf in) {
1143         in.markReaderIndex();
1144         try {
1145             return DefaultDnsRecordDecoder.decodeName(in);
1146         } catch (CorruptedFrameException e) {
1147             // In this case we just return null.
1148             return null;
1149         } finally {
1150             in.resetReaderIndex();
1151         }
1152     }
1153 
1154     private DnsServerAddressStream getNameServers(String name) {
1155         DnsServerAddressStream stream = getNameServersFromCache(name);
1156         if (stream == null) {
1157             // We need to obtain a new stream from the parent DnsNameResolver if the hostname is not the same as
1158             // for the original query (for example we may follow CNAMEs). Otherwise let's just duplicate the
1159             // original nameservers so we correctly update the internal index
1160             if (name.equals(hostname)) {
1161                 return nameServerAddrs.duplicate();
1162             }
1163             return parent.newNameServerAddressStream(name);
1164         }
1165         return stream;
1166     }
1167 
1168     private void followCname(DnsQuestion question, String cname, DnsQueryLifecycleObserver queryLifecycleObserver,
1169                              Promise<List<T>> promise) {
1170         final DnsQuestion cnameQuestion;
1171         final DnsServerAddressStream stream;
1172         try {
1173             cname = cnameResolveFromCache(cnameCache(), cname);
1174             stream = getNameServers(cname);
1175             cnameQuestion = new DefaultDnsQuestion(cname, question.type(), dnsClass);
1176         } catch (Throwable cause) {
1177             queryLifecycleObserver.queryFailed(cause);
1178             PlatformDependent.throwException(cause);
1179             return;
1180         }
1181         query(stream, 0, cnameQuestion, queryLifecycleObserver.queryCNAMEd(cnameQuestion),
1182               true, promise, null);
1183     }
1184 
1185     private boolean query(String hostname, DnsRecordType type, DnsServerAddressStream dnsServerAddressStream,
1186                           boolean flush, Promise<List<T>> promise) {
1187         final DnsQuestion question;
1188         try {
1189             question = new DefaultDnsQuestion(hostname, type, dnsClass);
1190         } catch (Throwable cause) {
1191             // Assume a single failure means that queries will succeed. If the hostname is invalid for one type
1192             // there is no case where it is known to be valid for another type.
1193             promise.tryFailure(new IllegalArgumentException("Unable to create DNS Question for: [" + hostname + ", " +
1194                     type + ']', cause));
1195             return false;
1196         }
1197         query(dnsServerAddressStream, 0, question, newDnsQueryLifecycleObserver(question), flush, promise, null);
1198         return true;
1199     }
1200 
1201     private DnsQueryLifecycleObserver newDnsQueryLifecycleObserver(DnsQuestion question) {
1202         return parent.dnsQueryLifecycleObserverFactory().newDnsQueryLifecycleObserver(question);
1203     }
1204 
1205     private final class CombinedDnsServerAddressStream implements DnsServerAddressStream {
1206         private final InetSocketAddress replaced;
1207         private final DnsServerAddressStream originalStream;
1208         private final List<InetAddress> resolvedAddresses;
1209         private Iterator<InetAddress> resolved;
1210 
1211         CombinedDnsServerAddressStream(InetSocketAddress replaced, List<InetAddress> resolvedAddresses,
1212                                        DnsServerAddressStream originalStream) {
1213             this.replaced = replaced;
1214             this.resolvedAddresses = resolvedAddresses;
1215             this.originalStream = originalStream;
1216             resolved = resolvedAddresses.iterator();
1217         }
1218 
1219         @Override
1220         public InetSocketAddress next() {
1221             if (resolved.hasNext()) {
1222                 return nextResolved0();
1223             }
1224             InetSocketAddress address = originalStream.next();
1225             if (address.equals(replaced)) {
1226                 resolved = resolvedAddresses.iterator();
1227                 return nextResolved0();
1228             }
1229             return address;
1230         }
1231 
1232         private InetSocketAddress nextResolved0() {
1233             return parent.newRedirectServerAddress(resolved.next());
1234         }
1235 
1236         @Override
1237         public int size() {
1238             return originalStream.size() + resolvedAddresses.size() - 1;
1239         }
1240 
1241         @Override
1242         public DnsServerAddressStream duplicate() {
1243             return new CombinedDnsServerAddressStream(replaced, resolvedAddresses, originalStream.duplicate());
1244         }
1245     }
1246 
1247     /**
1248      * Holds the closed DNS Servers for a domain.
1249      */
1250     private static final class AuthoritativeNameServerList {
1251 
1252         private final String questionName;
1253 
1254         // We not expect the linked-list to be very long so a double-linked-list is overkill.
1255         private AuthoritativeNameServer head;
1256 
1257         private int nameServerCount;
1258 
1259         AuthoritativeNameServerList(String questionName) {
1260             this.questionName = questionName.toLowerCase(Locale.US);
1261         }
1262 
1263         void add(DnsRecord r) {
1264             if (r.type() != DnsRecordType.NS || !(r instanceof DnsRawRecord)) {
1265                 return;
1266             }
1267 
1268             // Only include servers that serve the correct domain.
1269             if (questionName.length() <  r.name().length()) {
1270                 return;
1271             }
1272 
1273             String recordName = r.name().toLowerCase(Locale.US);
1274 
1275             int dots = 0;
1276             for (int a = recordName.length() - 1, b = questionName.length() - 1; a >= 0; a--, b--) {
1277                 char c = recordName.charAt(a);
1278                 if (questionName.charAt(b) != c) {
1279                     return;
1280                 }
1281                 if (c == '.') {
1282                     dots++;
1283                 }
1284             }
1285 
1286             if (head != null && head.dots > dots) {
1287                 // We already have a closer match so ignore this one, no need to parse the domainName etc.
1288                 return;
1289             }
1290 
1291             final ByteBuf recordContent = ((ByteBufHolder) r).content();
1292             final String domainName = decodeDomainName(recordContent);
1293             if (domainName == null) {
1294                 // Could not be parsed, ignore.
1295                 return;
1296             }
1297 
1298             // We are only interested in preserving the nameservers which are the closest to our qName, so ensure
1299             // we drop servers that have a smaller dots count.
1300             if (head == null || head.dots < dots) {
1301                 nameServerCount = 1;
1302                 head = new AuthoritativeNameServer(dots, r.timeToLive(), recordName, domainName);
1303             } else if (head.dots == dots) {
1304                 AuthoritativeNameServer serverName = head;
1305                 while (serverName.next != null) {
1306                     serverName = serverName.next;
1307                 }
1308                 serverName.next = new AuthoritativeNameServer(dots, r.timeToLive(), recordName, domainName);
1309                 nameServerCount++;
1310             }
1311         }
1312 
1313         void handleWithAdditional(
1314                 DnsNameResolver parent, DnsRecord r, AuthoritativeDnsServerCache authoritativeCache) {
1315             // Just walk the linked-list and mark the entry as handled when matched.
1316             AuthoritativeNameServer serverName = head;
1317 
1318             String nsName = r.name();
1319             InetAddress resolved = decodeAddress(r, nsName, parent.isDecodeIdn());
1320             if (resolved == null) {
1321                 // Could not parse the address, just ignore.
1322                 return;
1323             }
1324 
1325             while (serverName != null) {
1326                 if (serverName.nsName.equalsIgnoreCase(nsName)) {
1327                     if (serverName.address != null) {
1328                         // We received multiple ADDITIONAL records for the same name.
1329                         // Search for the last we insert before and then append a new one.
1330                         while (serverName.next != null && serverName.next.isCopy) {
1331                             serverName = serverName.next;
1332                         }
1333                         AuthoritativeNameServer server = new AuthoritativeNameServer(serverName);
1334                         server.next = serverName.next;
1335                         serverName.next = server;
1336                         serverName = server;
1337 
1338                         nameServerCount++;
1339                     }
1340                     // We should replace the TTL if needed with the one of the ADDITIONAL record so we use
1341                     // the smallest for caching.
1342                     serverName.update(parent.newRedirectServerAddress(resolved), r.timeToLive());
1343 
1344                     // Cache the server now.
1345                     cache(serverName, authoritativeCache, parent.executor());
1346                     return;
1347                 }
1348                 serverName = serverName.next;
1349             }
1350         }
1351 
1352         // Now handle all AuthoritativeNameServer for which we had no ADDITIONAL record
1353         void handleWithoutAdditionals(
1354                 DnsNameResolver parent, DnsCache cache, AuthoritativeDnsServerCache authoritativeCache) {
1355             AuthoritativeNameServer serverName = head;
1356 
1357             while (serverName != null) {
1358                 if (serverName.address == null) {
1359                     // These will be resolved on the fly if needed.
1360                     cacheUnresolved(serverName, authoritativeCache, parent.executor());
1361 
1362                     // Try to resolve via cache as we had no ADDITIONAL entry for the server.
1363 
1364                     List<? extends DnsCacheEntry> entries = cache.get(serverName.nsName, null);
1365                     if (entries != null && !entries.isEmpty()) {
1366                         InetAddress address = entries.get(0).address();
1367 
1368                         // If address is null we have a resolution failure cached so just use an unresolved address.
1369                         if (address != null) {
1370                             serverName.update(parent.newRedirectServerAddress(address));
1371 
1372                             for (int i = 1; i < entries.size(); i++) {
1373                                 address = entries.get(i).address();
1374 
1375                                 assert address != null :
1376                                         "Cache returned a cached failure, should never return anything else";
1377 
1378                                 AuthoritativeNameServer server = new AuthoritativeNameServer(serverName);
1379                                 server.next = serverName.next;
1380                                 serverName.next = server;
1381                                 serverName = server;
1382                                 serverName.update(parent.newRedirectServerAddress(address));
1383 
1384                                 nameServerCount++;
1385                             }
1386                         }
1387                     }
1388                 }
1389                 serverName = serverName.next;
1390             }
1391         }
1392 
1393         private static void cacheUnresolved(
1394                 AuthoritativeNameServer server, AuthoritativeDnsServerCache authoritativeCache, EventLoop loop) {
1395             // We still want to cached the unresolved address
1396             server.address = InetSocketAddress.createUnresolved(
1397                     server.nsName, DefaultDnsServerAddressStreamProvider.DNS_PORT);
1398 
1399             // Cache the server now.
1400             cache(server, authoritativeCache, loop);
1401         }
1402 
1403         private static void cache(AuthoritativeNameServer server, AuthoritativeDnsServerCache cache, EventLoop loop) {
1404             // Cache NS record if not for a root server as we should never cache for root servers.
1405             if (!server.isRootServer()) {
1406                 cache.cache(server.domainName, server.address, server.ttl, loop);
1407             }
1408         }
1409 
1410         /**
1411          * Returns {@code true} if empty, {@code false} otherwise.
1412          */
1413         boolean isEmpty() {
1414             return nameServerCount == 0;
1415         }
1416 
1417         /**
1418          * Creates a new {@link List} which holds the {@link InetSocketAddress}es.
1419          */
1420         List<InetSocketAddress> addressList() {
1421             List<InetSocketAddress> addressList = new ArrayList<InetSocketAddress>(nameServerCount);
1422 
1423             AuthoritativeNameServer server = head;
1424             while (server != null) {
1425                 if (server.address != null) {
1426                     addressList.add(server.address);
1427                 }
1428                 server = server.next;
1429             }
1430             return addressList;
1431         }
1432     }
1433 
1434     private static final class AuthoritativeNameServer {
1435         private final int dots;
1436         private final String domainName;
1437         final boolean isCopy;
1438         final String nsName;
1439 
1440         private long ttl;
1441         private InetSocketAddress address;
1442 
1443         AuthoritativeNameServer next;
1444 
1445         AuthoritativeNameServer(int dots, long ttl, String domainName, String nsName) {
1446             this.dots = dots;
1447             this.ttl = ttl;
1448             this.nsName = nsName;
1449             this.domainName = domainName;
1450             isCopy = false;
1451         }
1452 
1453         AuthoritativeNameServer(AuthoritativeNameServer server) {
1454             dots = server.dots;
1455             ttl = server.ttl;
1456             nsName = server.nsName;
1457             domainName = server.domainName;
1458             isCopy = true;
1459         }
1460 
1461         /**
1462          * Returns {@code true} if its a root server.
1463          */
1464         boolean isRootServer() {
1465             return dots == 1;
1466         }
1467 
1468         /**
1469          * Update the server with the given address and TTL if needed.
1470          */
1471         void update(InetSocketAddress address, long ttl) {
1472             assert this.address == null || this.address.isUnresolved();
1473             this.address = address;
1474             this.ttl = min(this.ttl, ttl);
1475         }
1476 
1477         void update(InetSocketAddress address) {
1478             update(address, Long.MAX_VALUE);
1479         }
1480     }
1481 }