/* * Copyright 2014 The Netty Project * * The Netty Project licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package io.netty.resolver.dns; import io.netty.bootstrap.Bootstrap; import io.netty.channel.AddressedEnvelope; import io.netty.channel.ChannelFactory; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoop; import io.netty.channel.FixedRecvByteBufAllocator; import io.netty.channel.ReflectiveChannelFactory; import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.InternetProtocolFamily; import io.netty.handler.codec.dns.DatagramDnsQueryEncoder; import io.netty.handler.codec.dns.DatagramDnsResponse; import io.netty.handler.codec.dns.DatagramDnsResponseDecoder; import io.netty.handler.codec.dns.DnsQuestion; import io.netty.handler.codec.dns.DnsResponse; import io.netty.resolver.InetNameResolver; import io.netty.util.NetUtil; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.FastThreadLocal; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Promise; import io.netty.util.internal.OneTimeTask; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import java.net.IDN; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map.Entry; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import static io.netty.util.internal.ObjectUtil.checkNotNull; /** * A DNS-based {@link InetNameResolver}. */ public class DnsNameResolver extends InetNameResolver { private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsNameResolver.class); static final InetSocketAddress ANY_LOCAL_ADDR = new InetSocketAddress(0); private static final InternetProtocolFamily[] DEFAULT_RESOLVE_ADDRESS_TYPES = new InternetProtocolFamily[2]; static { // Note that we did not use SystemPropertyUtil.getBoolean() here to emulate the behavior of JDK. if (Boolean.getBoolean("java.net.preferIPv6Addresses")) { DEFAULT_RESOLVE_ADDRESS_TYPES[0] = InternetProtocolFamily.IPv6; DEFAULT_RESOLVE_ADDRESS_TYPES[1] = InternetProtocolFamily.IPv4; logger.debug("-Djava.net.preferIPv6Addresses: true"); } else { DEFAULT_RESOLVE_ADDRESS_TYPES[0] = InternetProtocolFamily.IPv4; DEFAULT_RESOLVE_ADDRESS_TYPES[1] = InternetProtocolFamily.IPv6; logger.debug("-Djava.net.preferIPv6Addresses: false"); } } private static final DatagramDnsResponseDecoder DECODER = new DatagramDnsResponseDecoder(); private static final DatagramDnsQueryEncoder ENCODER = new DatagramDnsQueryEncoder(); final DnsServerAddresses nameServerAddresses; final ChannelFuture bindFuture; final DatagramChannel ch; /** * Manages the {@link DnsQueryContext}s in progress and their query IDs. */ final DnsQueryContextManager queryContextManager = new DnsQueryContextManager(); /** * Cache for {@link #doResolve(String, Promise)} and {@link #doResolveAll(String, Promise)}. */ final ConcurrentMap> resolveCache = PlatformDependent.newConcurrentHashMap(); private final FastThreadLocal nameServerAddrStream = new FastThreadLocal() { @Override protected DnsServerAddressStream initialValue() throws Exception { return nameServerAddresses.stream(); } }; private final DnsResponseHandler responseHandler = new DnsResponseHandler(); private volatile long queryTimeoutMillis = 5000; // The default TTL values here respect the TTL returned by the DNS server and do not cache the negative response. private volatile int minTtl; private volatile int maxTtl = Integer.MAX_VALUE; private volatile int negativeTtl; private volatile int maxQueriesPerResolve = 3; private volatile boolean traceEnabled = true; private volatile InternetProtocolFamily[] resolveAddressTypes = DEFAULT_RESOLVE_ADDRESS_TYPES; private volatile boolean recursionDesired = true; private volatile int maxPayloadSize; private volatile boolean optResourceEnabled = true; /** * Creates a new DNS-based name resolver that communicates with the specified list of DNS servers. * * @param eventLoop the {@link EventLoop} which will perform the communication with the DNS servers * @param channelType the type of the {@link DatagramChannel} to create * @param nameServerAddresses the addresses of the DNS server. For each DNS query, a new stream is created from * this to determine which DNS server should be contacted for the next retry in case * of failure. */ public DnsNameResolver( EventLoop eventLoop, Class channelType, DnsServerAddresses nameServerAddresses) { this(eventLoop, channelType, ANY_LOCAL_ADDR, nameServerAddresses); } /** * Creates a new DNS-based name resolver that communicates with the specified list of DNS servers. * * @param eventLoop the {@link EventLoop} which will perform the communication with the DNS servers * @param channelType the type of the {@link DatagramChannel} to create * @param localAddress the local address of the {@link DatagramChannel} * @param nameServerAddresses the addresses of the DNS server. For each DNS query, a new stream is created from * this to determine which DNS server should be contacted for the next retry in case * of failure. */ public DnsNameResolver( EventLoop eventLoop, Class channelType, InetSocketAddress localAddress, DnsServerAddresses nameServerAddresses) { this(eventLoop, new ReflectiveChannelFactory(channelType), localAddress, nameServerAddresses); } /** * Creates a new DNS-based name resolver that communicates with the specified list of DNS servers. * * @param eventLoop the {@link EventLoop} which will perform the communication with the DNS servers * @param channelFactory the {@link ChannelFactory} that will create a {@link DatagramChannel} * @param nameServerAddresses the addresses of the DNS server. For each DNS query, a new stream is created from * this to determine which DNS server should be contacted for the next retry in case * of failure. */ public DnsNameResolver( EventLoop eventLoop, ChannelFactory channelFactory, DnsServerAddresses nameServerAddresses) { this(eventLoop, channelFactory, ANY_LOCAL_ADDR, nameServerAddresses); } /** * Creates a new DNS-based name resolver that communicates with the specified list of DNS servers. * * @param eventLoop the {@link EventLoop} which will perform the communication with the DNS servers * @param channelFactory the {@link ChannelFactory} that will create a {@link DatagramChannel} * @param localAddress the local address of the {@link DatagramChannel} * @param nameServerAddresses the addresses of the DNS server. For each DNS query, a new stream is created from * this to determine which DNS server should be contacted for the next retry in case * of failure. */ public DnsNameResolver( EventLoop eventLoop, ChannelFactory channelFactory, InetSocketAddress localAddress, DnsServerAddresses nameServerAddresses) { super(eventLoop); checkNotNull(channelFactory, "channelFactory"); checkNotNull(nameServerAddresses, "nameServerAddresses"); checkNotNull(localAddress, "localAddress"); this.nameServerAddresses = nameServerAddresses; bindFuture = newChannel(channelFactory, localAddress); ch = (DatagramChannel) bindFuture.channel(); setMaxPayloadSize(4096); } private ChannelFuture newChannel( ChannelFactory channelFactory, InetSocketAddress localAddress) { Bootstrap b = new Bootstrap(); b.group(executor()); b.channelFactory(channelFactory); b.handler(new ChannelInitializer() { @Override protected void initChannel(DatagramChannel ch) throws Exception { ch.pipeline().addLast(DECODER, ENCODER, responseHandler); } }); ChannelFuture bindFuture = b.bind(localAddress); bindFuture.channel().closeFuture().addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { clearCache(); } }); return bindFuture; } /** * Returns the minimum TTL of the cached DNS resource records (in seconds). * * @see #maxTtl() * @see #setTtl(int, int) */ public int minTtl() { return minTtl; } /** * Returns the maximum TTL of the cached DNS resource records (in seconds). * * @see #minTtl() * @see #setTtl(int, int) */ public int maxTtl() { return maxTtl; } /** * Sets the minimum and maximum TTL of the cached DNS resource records (in seconds). If the TTL of the DNS resource * record returned by the DNS server is less than the minimum TTL or greater than the maximum TTL, this resolver * will ignore the TTL from the DNS server and use the minimum TTL or the maximum TTL instead respectively. * The default value is {@code 0} and {@link Integer#MAX_VALUE}, which practically tells this resolver to respect * the TTL from the DNS server. * * @return {@code this} * * @see #minTtl() * @see #maxTtl() */ public DnsNameResolver setTtl(int minTtl, int maxTtl) { if (minTtl < 0) { throw new IllegalArgumentException("minTtl: " + minTtl + " (expected: >= 0)"); } if (maxTtl < 0) { throw new IllegalArgumentException("maxTtl: " + maxTtl + " (expected: >= 0)"); } if (minTtl > maxTtl) { throw new IllegalArgumentException( "minTtl: " + minTtl + ", maxTtl: " + maxTtl + " (expected: 0 <= minTtl <= maxTtl)"); } this.maxTtl = maxTtl; this.minTtl = minTtl; return this; } /** * Returns the TTL of the cache for the failed DNS queries (in seconds). The default value is {@code 0}, which * disables the cache for negative results. * * @see #setNegativeTtl(int) */ public int negativeTtl() { return negativeTtl; } /** * Sets the TTL of the cache for the failed DNS queries (in seconds). * * @return {@code this} * * @see #negativeTtl() */ public DnsNameResolver setNegativeTtl(int negativeTtl) { if (negativeTtl < 0) { throw new IllegalArgumentException("negativeTtl: " + negativeTtl + " (expected: >= 0)"); } this.negativeTtl = negativeTtl; return this; } /** * Returns the timeout of each DNS query performed by this resolver (in milliseconds). * The default value is 5 seconds. * * @see #setQueryTimeoutMillis(long) */ public long queryTimeoutMillis() { return queryTimeoutMillis; } /** * Sets the timeout of each DNS query performed by this resolver (in milliseconds). * * @return {@code this} * * @see #queryTimeoutMillis() */ public DnsNameResolver setQueryTimeoutMillis(long queryTimeoutMillis) { if (queryTimeoutMillis < 0) { throw new IllegalArgumentException("queryTimeoutMillis: " + queryTimeoutMillis + " (expected: >= 0)"); } this.queryTimeoutMillis = queryTimeoutMillis; return this; } /** * Returns the list of the protocol families of the address resolved by {@link #resolve(String)} * in the order of preference. * The default value depends on the value of the system property {@code "java.net.preferIPv6Addresses"}. * * @see #setResolveAddressTypes(InternetProtocolFamily...) */ public List resolveAddressTypes() { return Arrays.asList(resolveAddressTypes); } InternetProtocolFamily[] resolveAddressTypesUnsafe() { return resolveAddressTypes; } /** * Sets the list of the protocol families of the address resolved by {@link #resolve(String)}. * Usually, both {@link InternetProtocolFamily#IPv4} and {@link InternetProtocolFamily#IPv6} are specified in the * order of preference. To enforce the resolve to retrieve the address of a specific protocol family, specify * only a single {@link InternetProtocolFamily}. * * @return {@code this} * * @see #resolveAddressTypes() */ public DnsNameResolver setResolveAddressTypes(InternetProtocolFamily... resolveAddressTypes) { checkNotNull(resolveAddressTypes, "resolveAddressTypes"); final List list = new ArrayList(InternetProtocolFamily.values().length); for (InternetProtocolFamily f: resolveAddressTypes) { if (f == null) { break; } // Avoid duplicate entries. if (list.contains(f)) { continue; } list.add(f); } if (list.isEmpty()) { throw new IllegalArgumentException("no protocol family specified"); } this.resolveAddressTypes = list.toArray(new InternetProtocolFamily[list.size()]); return this; } /** * Sets the list of the protocol families of the address resolved by {@link #resolve(String)}. * Usually, both {@link InternetProtocolFamily#IPv4} and {@link InternetProtocolFamily#IPv6} are specified in the * order of preference. To enforce the resolve to retrieve the address of a specific protocol family, specify * only a single {@link InternetProtocolFamily}. * * @return {@code this} * * @see #resolveAddressTypes() */ public DnsNameResolver setResolveAddressTypes(Iterable resolveAddressTypes) { checkNotNull(resolveAddressTypes, "resolveAddressTypes"); final List list = new ArrayList(InternetProtocolFamily.values().length); for (InternetProtocolFamily f: resolveAddressTypes) { if (f == null) { break; } // Avoid duplicate entries. if (list.contains(f)) { continue; } list.add(f); } if (list.isEmpty()) { throw new IllegalArgumentException("no protocol family specified"); } this.resolveAddressTypes = list.toArray(new InternetProtocolFamily[list.size()]); return this; } /** * Returns {@code true} if and only if this resolver sends a DNS query with the RD (recursion desired) flag set. * The default value is {@code true}. * * @see #setRecursionDesired(boolean) */ public boolean isRecursionDesired() { return recursionDesired; } /** * Sets if this resolver has to send a DNS query with the RD (recursion desired) flag set. * * @return {@code this} * * @see #isRecursionDesired() */ public DnsNameResolver setRecursionDesired(boolean recursionDesired) { this.recursionDesired = recursionDesired; return this; } /** * Returns the maximum allowed number of DNS queries to send when resolving a host name. * The default value is {@code 8}. * * @see #setMaxQueriesPerResolve(int) */ public int maxQueriesPerResolve() { return maxQueriesPerResolve; } /** * Sets the maximum allowed number of DNS queries to send when resolving a host name. * * @return {@code this} * * @see #maxQueriesPerResolve() */ public DnsNameResolver setMaxQueriesPerResolve(int maxQueriesPerResolve) { if (maxQueriesPerResolve <= 0) { throw new IllegalArgumentException("maxQueriesPerResolve: " + maxQueriesPerResolve + " (expected: > 0)"); } this.maxQueriesPerResolve = maxQueriesPerResolve; return this; } /** * Returns if this resolver should generate the detailed trace information in an exception message so that * it is easier to understand the cause of resolution failure. The default value if {@code true}. */ public boolean isTraceEnabled() { return traceEnabled; } /** * Sets if this resolver should generate the detailed trace information in an exception message so that * it is easier to understand the cause of resolution failure. */ public DnsNameResolver setTraceEnabled(boolean traceEnabled) { this.traceEnabled = traceEnabled; return this; } /** * Returns the capacity of the datagram packet buffer (in bytes). The default value is {@code 4096} bytes. * * @see #setMaxPayloadSize(int) */ public int maxPayloadSize() { return maxPayloadSize; } /** * Sets the capacity of the datagram packet buffer (in bytes). The default value is {@code 4096} bytes. * * @return {@code this} * * @see #maxPayloadSize() */ public DnsNameResolver setMaxPayloadSize(int maxPayloadSize) { if (maxPayloadSize <= 0) { throw new IllegalArgumentException("maxPayloadSize: " + maxPayloadSize + " (expected: > 0)"); } if (this.maxPayloadSize == maxPayloadSize) { // Same value; no need to instantiate DnsClass and RecvByteBufAllocator again. return this; } this.maxPayloadSize = maxPayloadSize; ch.config().setRecvByteBufAllocator(new FixedRecvByteBufAllocator(maxPayloadSize)); return this; } /** * Enable the automatic inclusion of a optional records that tries to give the remote DNS server a hint about how * much data the resolver can read per response. Some DNSServer may not support this and so fail to answer * queries. If you find problems you may want to disable this. */ public DnsNameResolver setOptResourceEnabled(boolean optResourceEnabled) { this.optResourceEnabled = optResourceEnabled; return this; } /** * Returns the automatic inclusion of a optional records that tries to give the remote DNS server a hint about how * much data the resolver can read per response is enabled. */ public boolean isOptResourceEnabled() { return optResourceEnabled; } /** * Clears all the resolved addresses cached by this resolver. * * @return {@code this} * * @see #clearCache(String) */ public DnsNameResolver clearCache() { for (Iterator>> i = resolveCache.entrySet().iterator(); i.hasNext();) { final Entry> e = i.next(); i.remove(); cancelExpiration(e); } return this; } /** * Clears the resolved addresses of the specified host name from the cache of this resolver. * * @return {@code true} if and only if there was an entry for the specified host name in the cache and * it has been removed by this method */ public boolean clearCache(String hostname) { boolean removed = false; for (Iterator>> i = resolveCache.entrySet().iterator(); i.hasNext();) { final Entry> e = i.next(); if (e.getKey().equals(hostname)) { i.remove(); cancelExpiration(e); removed = true; } } return removed; } private static void cancelExpiration(Entry> e) { final List entries = e.getValue(); final int numEntries = entries.size(); for (int i = 0; i < numEntries; i++) { entries.get(i).cancelExpiration(); } } /** * Closes the internal datagram channel used for sending and receiving DNS messages, and clears all DNS resource * records from the cache. Attempting to send a DNS query or to resolve a domain name will fail once this method * has been called. */ @Override public void close() { ch.close(); } @Override protected EventLoop executor() { return (EventLoop) super.executor(); } @Override protected void doResolve(String inetHost, Promise promise) throws Exception { final byte[] bytes = NetUtil.createByteArrayFromIpAddressString(inetHost); if (bytes != null) { // The inetHost is actually an ipaddress. promise.setSuccess(InetAddress.getByAddress(bytes)); return; } final String hostname = hostname(inetHost); if (!doResolveCached(hostname, promise)) { doResolveUncached(hostname, promise); } } private boolean doResolveCached(String hostname, Promise promise) { final List cachedEntries = resolveCache.get(hostname); if (cachedEntries == null) { return false; } InetAddress address = null; Throwable cause = null; synchronized (cachedEntries) { final int numEntries = cachedEntries.size(); assert numEntries > 0; if (cachedEntries.get(0).cause() != null) { cause = cachedEntries.get(0).cause(); } else { // Find the first entry with the preferred address type. for (InternetProtocolFamily f : resolveAddressTypes) { for (int i = 0; i < numEntries; i++) { final DnsCacheEntry e = cachedEntries.get(i); if (f.addressType().isInstance(e.address())) { address = e.address(); break; } } } } } if (address != null) { setSuccess(promise, address); } else if (cause != null) { if (!promise.tryFailure(cause)) { logger.warn("Failed to notify failure to a promise: {}", promise, cause); } } else { return false; } return true; } private static void setSuccess(Promise promise, InetAddress result) { if (!promise.trySuccess(result)) { logger.warn("Failed to notify success ({}) to a promise: {}", result, promise); } } private void doResolveUncached(String hostname, Promise promise) { final DnsNameResolverContext ctx = new DnsNameResolverContext(this, hostname, promise) { @Override protected boolean finishResolve( Class addressType, List resolvedEntries) { final int numEntries = resolvedEntries.size(); for (int i = 0; i < numEntries; i++) { final InetAddress a = resolvedEntries.get(i).address(); if (addressType.isInstance(a)) { setSuccess(promise(), a); return true; } } return false; } }; ctx.resolve(); } @Override protected void doResolveAll(String inetHost, Promise> promise) throws Exception { final byte[] bytes = NetUtil.createByteArrayFromIpAddressString(inetHost); if (bytes != null) { // The unresolvedAddress was created via a String that contains an ipaddress. promise.setSuccess(Collections.singletonList(InetAddress.getByAddress(bytes))); return; } final String hostname = hostname(inetHost); if (!doResolveAllCached(hostname, promise)) { doResolveAllUncached(hostname, promise); } } private boolean doResolveAllCached(String hostname, Promise> promise) { final List cachedEntries = resolveCache.get(hostname); if (cachedEntries == null) { return false; } List result = null; Throwable cause = null; synchronized (cachedEntries) { final int numEntries = cachedEntries.size(); assert numEntries > 0; if (cachedEntries.get(0).cause() != null) { cause = cachedEntries.get(0).cause(); } else { for (InternetProtocolFamily f : resolveAddressTypes) { for (int i = 0; i < numEntries; i++) { final DnsCacheEntry e = cachedEntries.get(i); if (f.addressType().isInstance(e.address())) { if (result == null) { result = new ArrayList(numEntries); } result.add(e.address()); } } } } } if (result != null) { promise.trySuccess(result); } else if (cause != null) { promise.tryFailure(cause); } else { return false; } return true; } private void doResolveAllUncached(final String hostname, final Promise> promise) { final DnsNameResolverContext> ctx = new DnsNameResolverContext>(this, hostname, promise) { @Override protected boolean finishResolve( Class addressType, List resolvedEntries) { List result = null; final int numEntries = resolvedEntries.size(); for (int i = 0; i < numEntries; i++) { final InetAddress a = resolvedEntries.get(i).address(); if (addressType.isInstance(a)) { if (result == null) { result = new ArrayList(numEntries); } result.add(a); } } if (result != null) { promise().trySuccess(result); return true; } return false; } }; ctx.resolve(); } private static String hostname(String inetHost) { return IDN.toASCII(inetHost); } void cache(String hostname, InetAddress address, long originalTtl) { final int maxTtl = maxTtl(); if (maxTtl == 0) { return; } final int ttl = Math.max(minTtl(), (int) Math.min(maxTtl, originalTtl)); final List entries = cachedEntries(hostname); final DnsCacheEntry e = new DnsCacheEntry(hostname, address); synchronized (entries) { if (!entries.isEmpty()) { final DnsCacheEntry firstEntry = entries.get(0); if (firstEntry.cause() != null) { assert entries.size() == 1; firstEntry.cancelExpiration(); entries.clear(); } } entries.add(e); } scheduleCacheExpiration(entries, e, ttl); } void cache(String hostname, Throwable cause) { final int negativeTtl = negativeTtl(); if (negativeTtl == 0) { return; } final List entries = cachedEntries(hostname); final DnsCacheEntry e = new DnsCacheEntry(hostname, cause); synchronized (entries) { final int numEntries = entries.size(); for (int i = 0; i < numEntries; i ++) { entries.get(i).cancelExpiration(); } entries.clear(); entries.add(e); } scheduleCacheExpiration(entries, e, negativeTtl); } private List cachedEntries(String hostname) { List oldEntries = resolveCache.get(hostname); final List entries; if (oldEntries == null) { List newEntries = new ArrayList(); oldEntries = resolveCache.putIfAbsent(hostname, newEntries); entries = oldEntries != null? oldEntries : newEntries; } else { entries = oldEntries; } return entries; } private void scheduleCacheExpiration(final List entries, final DnsCacheEntry e, int ttl) { e.scheduleExpiration( ch.eventLoop(), new OneTimeTask() { @Override public void run() { synchronized (entries) { entries.remove(e); if (entries.isEmpty()) { resolveCache.remove(e.hostname()); } } } }, ttl, TimeUnit.SECONDS); } /** * Sends a DNS query with the specified question. */ public Future> query(DnsQuestion question) { return query(nextNameServerAddress(), question); } /** * Sends a DNS query with the specified question. */ public Future> query( DnsQuestion question, Promise> promise) { return query(nextNameServerAddress(), question, promise); } private InetSocketAddress nextNameServerAddress() { return nameServerAddrStream.get().next(); } /** * Sends a DNS query with the specified question using the specified name server list. */ public Future> query( InetSocketAddress nameServerAddr, DnsQuestion question) { return query0(checkNotNull(nameServerAddr, "nameServerAddr"), checkNotNull(question, "question"), ch.eventLoop().>newPromise()); } /** * Sends a DNS query with the specified question using the specified name server list. */ public Future> query( InetSocketAddress nameServerAddr, DnsQuestion question, Promise> promise) { return query0(checkNotNull(nameServerAddr, "nameServerAddr"), checkNotNull(question, "question"), checkNotNull(promise, "promise")); } private Future> query0( InetSocketAddress nameServerAddr, DnsQuestion question, Promise> promise) { final Promise> castPromise = cast(promise); try { new DnsQueryContext(this, nameServerAddr, question, castPromise).query(); return castPromise; } catch (Exception e) { return castPromise.setFailure(e); } } @SuppressWarnings("unchecked") private static Promise> cast(Promise promise) { return (Promise>) promise; } private final class DnsResponseHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try { final DatagramDnsResponse res = (DatagramDnsResponse) msg; final int queryId = res.id(); if (logger.isDebugEnabled()) { logger.debug("{} RECEIVED: [{}: {}], {}", ch, queryId, res.sender(), res); } final DnsQueryContext qCtx = queryContextManager.get(res.sender(), queryId); if (qCtx == null) { if (logger.isWarnEnabled()) { logger.warn("{} Received a DNS response with an unknown ID: {}", ch, queryId); } return; } qCtx.finish(res); } finally { ReferenceCountUtil.safeRelease(msg); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { logger.warn("{} Unexpected exception: ", ch, cause); } } }