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    *   http://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.channel.socket.InternetProtocolFamily;
21  import io.netty.handler.codec.dns.DnsClass;
22  import io.netty.handler.codec.dns.DnsQuestion;
23  import io.netty.handler.codec.dns.DnsResource;
24  import io.netty.handler.codec.dns.DnsResponse;
25  import io.netty.handler.codec.dns.DnsResponseDecoder;
26  import io.netty.handler.codec.dns.DnsType;
27  import io.netty.util.CharsetUtil;
28  import io.netty.util.ReferenceCountUtil;
29  import io.netty.util.concurrent.Future;
30  import io.netty.util.concurrent.FutureListener;
31  import io.netty.util.concurrent.Promise;
32  import io.netty.util.internal.StringUtil;
33  
34  import java.net.Inet4Address;
35  import java.net.Inet6Address;
36  import java.net.InetAddress;
37  import java.net.InetSocketAddress;
38  import java.net.UnknownHostException;
39  import java.util.ArrayList;
40  import java.util.Collections;
41  import java.util.HashMap;
42  import java.util.IdentityHashMap;
43  import java.util.Iterator;
44  import java.util.List;
45  import java.util.Locale;
46  import java.util.Map;
47  import java.util.Set;
48  
49  final class DnsNameResolverContext {
50  
51      private static final int INADDRSZ4 = 4;
52      private static final int INADDRSZ6 = 16;
53  
54      private static final FutureListener<DnsResponse> RELEASE_RESPONSE = new FutureListener<DnsResponse>() {
55          @Override
56          public void operationComplete(Future<DnsResponse> future) {
57              if (future.isSuccess()) {
58                  future.getNow().release();
59              }
60          }
61      };
62  
63      private final DnsNameResolver parent;
64      private final Promise<InetSocketAddress> promise;
65      private final String hostname;
66      private final int port;
67      private final int maxAllowedQueries;
68      private final InternetProtocolFamily[] resolveAddressTypes;
69  
70      private final Set<Future<DnsResponse>> queriesInProgress =
71              Collections.newSetFromMap(new IdentityHashMap<Future<DnsResponse>, Boolean>());
72      private List<InetAddress> resolvedAddresses;
73      private StringBuilder trace;
74      private int allowedQueries;
75      private boolean triedCNAME;
76  
77      DnsNameResolverContext(DnsNameResolver parent, String hostname, int port, Promise<InetSocketAddress> promise) {
78          this.parent = parent;
79          this.promise = promise;
80          this.hostname = hostname;
81          this.port = port;
82  
83          maxAllowedQueries = parent.maxQueriesPerResolve();
84          resolveAddressTypes = parent.resolveAddressTypesUnsafe();
85          allowedQueries = maxAllowedQueries;
86      }
87  
88      void resolve() {
89          for (InternetProtocolFamily f: resolveAddressTypes) {
90              final DnsType type;
91              switch (f) {
92              case IPv4:
93                  type = DnsType.A;
94                  break;
95              case IPv6:
96                  type = DnsType.AAAA;
97                  break;
98              default:
99                  throw new Error();
100             }
101 
102             query(parent.nameServerAddresses, new DnsQuestion(hostname, type));
103         }
104     }
105 
106     private void query(Iterable<InetSocketAddress> nameServerAddresses, final DnsQuestion question) {
107         if (allowedQueries == 0 || promise.isCancelled()) {
108             return;
109         }
110 
111         allowedQueries --;
112 
113         final Future<DnsResponse> f = parent.query(nameServerAddresses, question);
114         queriesInProgress.add(f);
115 
116         f.addListener(new FutureListener<DnsResponse>() {
117             @Override
118             public void operationComplete(Future<DnsResponse> future) throws Exception {
119                 queriesInProgress.remove(future);
120 
121                 if (promise.isDone()) {
122                     return;
123                 }
124 
125                 try {
126                     if (future.isSuccess()) {
127                         onResponse(question, future.getNow());
128                     } else {
129                         addTrace(future.cause());
130                     }
131                 } finally {
132                     tryToFinishResolve();
133                 }
134             }
135         });
136     }
137 
138     void onResponse(final DnsQuestion question, final DnsResponse response) {
139         final DnsType type = question.type();
140         try {
141             if (type == DnsType.A || type == DnsType.AAAA) {
142                 onResponseAorAAAA(type, question, response);
143             } else if (type == DnsType.CNAME) {
144                 onResponseCNAME(question, response);
145             }
146         } finally {
147             ReferenceCountUtil.safeRelease(response);
148         }
149     }
150 
151     private void onResponseAorAAAA(DnsType qType, DnsQuestion question, DnsResponse response) {
152         // We often get a bunch of CNAMES as well when we asked for A/AAAA.
153         final Map<String, String> cnames = buildAliasMap(response);
154 
155         boolean found = false;
156         for (DnsResource r: response.answers()) {
157             final DnsType type = r.type();
158             if (type != DnsType.A && type != DnsType.AAAA) {
159                 continue;
160             }
161 
162             final String qName = question.name().toLowerCase(Locale.US);
163             final String rName = r.name().toLowerCase(Locale.US);
164 
165             // Make sure the record is for the questioned domain.
166             if (!rName.equals(qName)) {
167                 // Even if the record's name is not exactly same, it might be an alias defined in the CNAME records.
168                 String resolved = qName;
169                 do {
170                     resolved = cnames.get(resolved);
171                     if (rName.equals(resolved)) {
172                         break;
173                     }
174                 } while (resolved != null);
175 
176                 if (resolved == null) {
177                     continue;
178                 }
179             }
180 
181             final ByteBuf content = r.content();
182             final int contentLen = content.readableBytes();
183             if (contentLen != INADDRSZ4 && contentLen != INADDRSZ6) {
184                 continue;
185             }
186 
187             final byte[] addrBytes = new byte[contentLen];
188             content.getBytes(content.readerIndex(), addrBytes);
189 
190             try {
191                 InetAddress resolved = InetAddress.getByAddress(hostname, addrBytes);
192                 if (resolvedAddresses == null) {
193                     resolvedAddresses = new ArrayList<InetAddress>();
194                 }
195                 resolvedAddresses.add(resolved);
196                 found = true;
197             } catch (UnknownHostException e) {
198                 // Should never reach here.
199                 throw new Error(e);
200             }
201         }
202 
203         if (found) {
204             return;
205         }
206 
207         addTrace(response.sender(), "no matching " + qType + " record found");
208 
209         // We aked for A/AAAA but we got only CNAME.
210         if (!cnames.isEmpty()) {
211             onResponseCNAME(question, response, cnames, false);
212         }
213     }
214 
215     private void onResponseCNAME(DnsQuestion question, DnsResponse response) {
216         onResponseCNAME(question, response, buildAliasMap(response), true);
217     }
218 
219     private void onResponseCNAME(
220             DnsQuestion question, DnsResponse response, Map<String, String> cnames, boolean trace) {
221 
222         // Resolve the host name in the question into the real host name.
223         final String name = question.name().toLowerCase(Locale.US);
224         String resolved = name;
225         boolean found = false;
226         for (;;) {
227             String next = cnames.get(resolved);
228             if (next != null) {
229                 found = true;
230                 resolved = next;
231             } else {
232                 break;
233             }
234         }
235 
236         if (found) {
237             followCname(response.sender(), name, resolved);
238         } else if (trace) {
239             addTrace(response.sender(), "no matching CNAME record found");
240         }
241     }
242 
243     private static Map<String, String> buildAliasMap(DnsResponse response) {
244         Map<String, String> cnames = null;
245         for (DnsResource r: response.answers()) {
246             final DnsType type = r.type();
247             if (type != DnsType.CNAME) {
248                 continue;
249             }
250 
251             String content = decodeDomainName(r.content());
252             if (content == null) {
253                 continue;
254             }
255 
256             if (cnames == null) {
257                 cnames = new HashMap<String, String>();
258             }
259 
260             cnames.put(r.name().toLowerCase(Locale.US), content.toLowerCase(Locale.US));
261         }
262 
263         return cnames != null? cnames : Collections.<String, String>emptyMap();
264     }
265 
266     void tryToFinishResolve() {
267         if (!queriesInProgress.isEmpty()) {
268             // There are still some queries we did not receive responses for.
269             if (gotPreferredAddress()) {
270                 // But it's OK to finish the resolution process if we got a resolved address of the preferred type.
271                 finishResolve();
272             }
273 
274             // We did not get any resolved address of the preferred type, so we can't finish the resolution process.
275             return;
276         }
277 
278         // There are no queries left to try.
279         if (resolvedAddresses == null) {
280             // .. and we could not find any A/AAAA records.
281             if (!triedCNAME) {
282                 // As the last resort, try to query CNAME, just in case the name server has it.
283                 triedCNAME = true;
284                 query(parent.nameServerAddresses, new DnsQuestion(hostname, DnsType.CNAME, DnsClass.IN));
285                 return;
286             }
287         }
288 
289         // We have at least one resolved address or tried CNAME as the last resort..
290         finishResolve();
291     }
292 
293     private boolean gotPreferredAddress() {
294         if (resolvedAddresses == null) {
295             return false;
296         }
297 
298         final int size = resolvedAddresses.size();
299         switch (resolveAddressTypes[0]) {
300         case IPv4:
301             for (int i = 0; i < size; i ++) {
302                 if (resolvedAddresses.get(i) instanceof Inet4Address) {
303                     return true;
304                 }
305             }
306             break;
307         case IPv6:
308             for (int i = 0; i < size; i ++) {
309                 if (resolvedAddresses.get(i) instanceof Inet6Address) {
310                     return true;
311                 }
312             }
313             break;
314         }
315 
316         return false;
317     }
318 
319     private void finishResolve() {
320         if (!queriesInProgress.isEmpty()) {
321             // If there are queries in progress, we should cancel it because we already finished the resolution.
322             for (Iterator<Future<DnsResponse>> i = queriesInProgress.iterator(); i.hasNext();) {
323                 Future<DnsResponse> f = i.next();
324                 i.remove();
325 
326                 if (!f.cancel(false)) {
327                     f.addListener(RELEASE_RESPONSE);
328                 }
329             }
330         }
331 
332         if (resolvedAddresses != null) {
333             // Found at least one resolved address.
334             for (InternetProtocolFamily f: resolveAddressTypes) {
335                 switch (f) {
336                 case IPv4:
337                     if (finishResolveWithIPv4()) {
338                         return;
339                     }
340                     break;
341                 case IPv6:
342                     if (finishResolveWithIPv6()) {
343                         return;
344                     }
345                     break;
346                 }
347             }
348         }
349 
350         // No resolved address found.
351         int tries = maxAllowedQueries - allowedQueries;
352         UnknownHostException cause;
353         if (tries > 1) {
354             cause = new UnknownHostException(
355                     "failed to resolve " + hostname + " after " + tries + " queries:" +
356                     trace);
357         } else {
358             cause = new UnknownHostException("failed to resolve " + hostname + ':' + trace);
359         }
360 
361         promise.tryFailure(cause);
362     }
363 
364     private boolean finishResolveWithIPv4() {
365         final List<InetAddress> resolvedAddresses = this.resolvedAddresses;
366         final int size = resolvedAddresses.size();
367 
368         for (int i = 0; i < size; i ++) {
369             InetAddress a = resolvedAddresses.get(i);
370             if (a instanceof Inet4Address) {
371                 promise.trySuccess(new InetSocketAddress(a, port));
372                 return true;
373             }
374         }
375 
376         return false;
377     }
378 
379     private boolean finishResolveWithIPv6() {
380         final List<InetAddress> resolvedAddresses = this.resolvedAddresses;
381         final int size = resolvedAddresses.size();
382 
383         for (int i = 0; i < size; i ++) {
384             InetAddress a = resolvedAddresses.get(i);
385             if (a instanceof Inet6Address) {
386                 promise.trySuccess(new InetSocketAddress(a, port));
387                 return true;
388             }
389         }
390 
391         return false;
392     }
393 
394     /**
395      * Adapted from {@link DnsResponseDecoder#readName(ByteBuf)}.
396      */
397     static String decodeDomainName(ByteBuf buf) {
398         buf.markReaderIndex();
399         try {
400             int position = -1;
401             int checked = 0;
402             int length = buf.writerIndex();
403             StringBuilder name = new StringBuilder(64);
404             for (int len = buf.readUnsignedByte(); buf.isReadable() && len != 0; len = buf.readUnsignedByte()) {
405                 boolean pointer = (len & 0xc0) == 0xc0;
406                 if (pointer) {
407                     if (position == -1) {
408                         position = buf.readerIndex() + 1;
409                     }
410                     buf.readerIndex((len & 0x3f) << 8 | buf.readUnsignedByte());
411                     // check for loops
412                     checked += 2;
413                     if (checked >= length) {
414                         // Name contains a loop; give up.
415                         return null;
416                     }
417                 } else {
418                     name.append(buf.toString(buf.readerIndex(), len, CharsetUtil.UTF_8)).append('.');
419                     buf.skipBytes(len);
420                 }
421             }
422 
423             if (position != -1) {
424                 buf.readerIndex(position);
425             }
426 
427             if (name.length() == 0) {
428                 return null;
429             }
430 
431             return name.substring(0, name.length() - 1);
432         } finally {
433             buf.resetReaderIndex();
434         }
435     }
436 
437     private void followCname(
438             InetSocketAddress nameServerAddr, String name, String cname) {
439 
440         if (trace == null) {
441             trace = new StringBuilder(128);
442         }
443 
444         trace.append(StringUtil.NEWLINE);
445         trace.append("\tfrom ");
446         trace.append(nameServerAddr);
447         trace.append(": ");
448         trace.append(name);
449         trace.append(" CNAME ");
450         trace.append(cname);
451 
452         query(parent.nameServerAddresses, new DnsQuestion(cname, DnsType.A, DnsClass.IN));
453         query(parent.nameServerAddresses, new DnsQuestion(cname, DnsType.AAAA, DnsClass.IN));
454     }
455 
456     private void addTrace(InetSocketAddress nameServerAddr, String msg) {
457         if (trace == null) {
458             trace = new StringBuilder(128);
459         }
460 
461         trace.append(StringUtil.NEWLINE);
462         trace.append("\tfrom ");
463         trace.append(nameServerAddr);
464         trace.append(": ");
465         trace.append(msg);
466     }
467 
468     private void addTrace(Throwable cause) {
469         if (trace == null) {
470             trace = new StringBuilder(128);
471         }
472 
473         trace.append(StringUtil.NEWLINE);
474         trace.append("Caused by: ");
475         trace.append(cause);
476     }
477 }