Correctly handle hostnames with and without trailing dot in the DefaultDnsCache and use it for searchdomains. (#8181)
Motivation: We should ensure we return the same cached entries for the hostname and hostname ending with dot. Beside this we also should use it for the searchdomains as well. Modifications: - Internally always use hostname with a dot as a key and so ensure we correctly handle it in the cache. - Also query the cache for each searchdomain - Add unit tests Result: Use the same cached entries for hostname with and without trailing dot. Query the cache for each searchdomain query as well
This commit is contained in:
parent
56eb1e92cc
commit
f22781f176
@ -19,6 +19,7 @@ import io.netty.channel.EventLoop;
|
|||||||
import io.netty.handler.codec.dns.DnsRecord;
|
import io.netty.handler.codec.dns.DnsRecord;
|
||||||
import io.netty.util.concurrent.ScheduledFuture;
|
import io.netty.util.concurrent.ScheduledFuture;
|
||||||
import io.netty.util.internal.PlatformDependent;
|
import io.netty.util.internal.PlatformDependent;
|
||||||
|
import io.netty.util.internal.StringUtil;
|
||||||
import io.netty.util.internal.UnstableApi;
|
import io.netty.util.internal.UnstableApi;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
@ -115,7 +116,7 @@ public class DefaultDnsCache implements DnsCache {
|
|||||||
@Override
|
@Override
|
||||||
public boolean clear(String hostname) {
|
public boolean clear(String hostname) {
|
||||||
checkNotNull(hostname, "hostname");
|
checkNotNull(hostname, "hostname");
|
||||||
Entries entries = resolveCache.remove(hostname);
|
Entries entries = resolveCache.remove(appendDot(hostname));
|
||||||
return entries != null && entries.clearAndCancel();
|
return entries != null && entries.clearAndCancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,7 +131,7 @@ public class DefaultDnsCache implements DnsCache {
|
|||||||
return Collections.<DnsCacheEntry>emptyList();
|
return Collections.<DnsCacheEntry>emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Entries entries = resolveCache.get(hostname);
|
Entries entries = resolveCache.get(appendDot(hostname));
|
||||||
return entries == null ? null : entries.get();
|
return entries == null ? null : entries.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,7 +145,8 @@ public class DefaultDnsCache implements DnsCache {
|
|||||||
if (maxTtl == 0 || !emptyAdditionals(additionals)) {
|
if (maxTtl == 0 || !emptyAdditionals(additionals)) {
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
cache0(e, Math.max(minTtl, Math.min(MAX_SUPPORTED_TTL_SECS, (int) Math.min(maxTtl, originalTtl))), loop);
|
cache0(appendDot(hostname), e,
|
||||||
|
Math.max(minTtl, Math.min(MAX_SUPPORTED_TTL_SECS, (int) Math.min(maxTtl, originalTtl))), loop);
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,25 +161,25 @@ public class DefaultDnsCache implements DnsCache {
|
|||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
cache0(e, Math.min(MAX_SUPPORTED_TTL_SECS, negativeTtl), loop);
|
cache0(appendDot(hostname), e, Math.min(MAX_SUPPORTED_TTL_SECS, negativeTtl), loop);
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cache0(DefaultDnsCacheEntry e, int ttl, EventLoop loop) {
|
private void cache0(String hostname, DefaultDnsCacheEntry e, int ttl, EventLoop loop) {
|
||||||
Entries entries = resolveCache.get(e.hostname());
|
Entries entries = resolveCache.get(hostname);
|
||||||
if (entries == null) {
|
if (entries == null) {
|
||||||
entries = new Entries(e);
|
entries = new Entries(e);
|
||||||
Entries oldEntries = resolveCache.putIfAbsent(e.hostname(), entries);
|
Entries oldEntries = resolveCache.putIfAbsent(hostname, entries);
|
||||||
if (oldEntries != null) {
|
if (oldEntries != null) {
|
||||||
entries = oldEntries;
|
entries = oldEntries;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entries.add(e);
|
entries.add(e);
|
||||||
|
|
||||||
scheduleCacheExpiration(e, ttl, loop);
|
scheduleCacheExpiration(hostname, e, ttl, loop);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleCacheExpiration(final DefaultDnsCacheEntry e,
|
private void scheduleCacheExpiration(final String hostname, final DefaultDnsCacheEntry e,
|
||||||
int ttl,
|
int ttl,
|
||||||
EventLoop loop) {
|
EventLoop loop) {
|
||||||
e.scheduleExpiration(loop, new Runnable() {
|
e.scheduleExpiration(loop, new Runnable() {
|
||||||
@ -192,7 +194,7 @@ public class DefaultDnsCache implements DnsCache {
|
|||||||
// completely fine to remove the entry even if the TTL is not reached yet.
|
// completely fine to remove the entry even if the TTL is not reached yet.
|
||||||
//
|
//
|
||||||
// See https://github.com/netty/netty/issues/7329
|
// See https://github.com/netty/netty/issues/7329
|
||||||
Entries entries = resolveCache.remove(e.hostname);
|
Entries entries = resolveCache.remove(hostname);
|
||||||
if (entries != null) {
|
if (entries != null) {
|
||||||
entries.clearAndCancel();
|
entries.clearAndCancel();
|
||||||
}
|
}
|
||||||
@ -239,10 +241,6 @@ public class DefaultDnsCache implements DnsCache {
|
|||||||
return cause;
|
return cause;
|
||||||
}
|
}
|
||||||
|
|
||||||
String hostname() {
|
|
||||||
return hostname;
|
|
||||||
}
|
|
||||||
|
|
||||||
void scheduleExpiration(EventLoop loop, Runnable task, long delay, TimeUnit unit) {
|
void scheduleExpiration(EventLoop loop, Runnable task, long delay, TimeUnit unit) {
|
||||||
assert expirationFuture == null : "expiration task scheduled already";
|
assert expirationFuture == null : "expiration task scheduled already";
|
||||||
expirationFuture = loop.schedule(task, delay, unit);
|
expirationFuture = loop.schedule(task, delay, unit);
|
||||||
@ -338,4 +336,8 @@ public class DefaultDnsCache implements DnsCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String appendDot(String hostname) {
|
||||||
|
return StringUtil.endsWith(hostname, '.') ? hostname : hostname + '.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import java.util.List;
|
|||||||
import io.netty.channel.EventLoop;
|
import io.netty.channel.EventLoop;
|
||||||
import io.netty.handler.codec.dns.DnsRecord;
|
import io.netty.handler.codec.dns.DnsRecord;
|
||||||
import io.netty.handler.codec.dns.DnsRecordType;
|
import io.netty.handler.codec.dns.DnsRecordType;
|
||||||
|
import io.netty.util.concurrent.Promise;
|
||||||
|
|
||||||
final class DnsAddressResolveContext extends DnsResolveContext<InetAddress> {
|
final class DnsAddressResolveContext extends DnsResolveContext<InetAddress> {
|
||||||
|
|
||||||
@ -84,4 +85,12 @@ final class DnsAddressResolveContext extends DnsResolveContext<InetAddress> {
|
|||||||
void cache(String hostname, DnsRecord[] additionals, UnknownHostException cause) {
|
void cache(String hostname, DnsRecord[] additionals, UnknownHostException cause) {
|
||||||
resolveCache.cache(hostname, additionals, cause, parent.ch.eventLoop());
|
resolveCache.cache(hostname, additionals, cause, parent.ch.eventLoop());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void doSearchDomainQuery(String hostname, Promise<List<InetAddress>> nextPromise) {
|
||||||
|
// Query the cache for the hostname first and only do a query if we could not find it in the cache.
|
||||||
|
if (!parent.doResolveAllCached(hostname, additionals, nextPromise, resolveCache)) {
|
||||||
|
super.doSearchDomainQuery(hostname, nextPromise);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -790,7 +790,7 @@ public class DnsNameResolver extends InetNameResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean doResolveAllCached(String hostname,
|
boolean doResolveAllCached(String hostname,
|
||||||
DnsRecord[] additionals,
|
DnsRecord[] additionals,
|
||||||
Promise<List<InetAddress>> promise,
|
Promise<List<InetAddress>> promise,
|
||||||
DnsCache resolveCache) {
|
DnsCache resolveCache) {
|
||||||
|
@ -97,7 +97,7 @@ abstract class DnsResolveContext<T> {
|
|||||||
private final int dnsClass;
|
private final int dnsClass;
|
||||||
private final DnsRecordType[] expectedTypes;
|
private final DnsRecordType[] expectedTypes;
|
||||||
private final int maxAllowedQueries;
|
private final int maxAllowedQueries;
|
||||||
private final DnsRecord[] additionals;
|
final DnsRecord[] additionals;
|
||||||
|
|
||||||
private final Set<Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> queriesInProgress =
|
private final Set<Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> queriesInProgress =
|
||||||
Collections.newSetFromMap(
|
Collections.newSetFromMap(
|
||||||
@ -164,7 +164,8 @@ abstract class DnsResolveContext<T> {
|
|||||||
final String initialHostname = startWithoutSearchDomain ? hostname : hostname + '.' + searchDomains[0];
|
final String initialHostname = startWithoutSearchDomain ? hostname : hostname + '.' + searchDomains[0];
|
||||||
final int initialSearchDomainIdx = startWithoutSearchDomain ? 0 : 1;
|
final int initialSearchDomainIdx = startWithoutSearchDomain ? 0 : 1;
|
||||||
|
|
||||||
doSearchDomainQuery(initialHostname, new FutureListener<List<T>>() {
|
final Promise<List<T>> searchDomainPromise = parent.executor().newPromise();
|
||||||
|
searchDomainPromise.addListener(new FutureListener<List<T>>() {
|
||||||
private int searchDomainIdx = initialSearchDomainIdx;
|
private int searchDomainIdx = initialSearchDomainIdx;
|
||||||
@Override
|
@Override
|
||||||
public void operationComplete(Future<List<T>> future) {
|
public void operationComplete(Future<List<T>> future) {
|
||||||
@ -175,7 +176,9 @@ abstract class DnsResolveContext<T> {
|
|||||||
if (DnsNameResolver.isTransportOrTimeoutError(cause)) {
|
if (DnsNameResolver.isTransportOrTimeoutError(cause)) {
|
||||||
promise.tryFailure(new SearchDomainUnknownHostException(cause, hostname));
|
promise.tryFailure(new SearchDomainUnknownHostException(cause, hostname));
|
||||||
} else if (searchDomainIdx < searchDomains.length) {
|
} else if (searchDomainIdx < searchDomains.length) {
|
||||||
doSearchDomainQuery(hostname + '.' + searchDomains[searchDomainIdx++], this);
|
Promise<List<T>> newPromise = parent.executor().newPromise();
|
||||||
|
newPromise.addListener(this);
|
||||||
|
doSearchDomainQuery(hostname + '.' + searchDomains[searchDomainIdx++], newPromise);
|
||||||
} else if (!startWithoutSearchDomain) {
|
} else if (!startWithoutSearchDomain) {
|
||||||
internalResolve(promise);
|
internalResolve(promise);
|
||||||
} else {
|
} else {
|
||||||
@ -184,6 +187,7 @@ abstract class DnsResolveContext<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
doSearchDomainQuery(initialHostname, searchDomainPromise);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,12 +217,10 @@ abstract class DnsResolveContext<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doSearchDomainQuery(String hostname, FutureListener<List<T>> listener) {
|
void doSearchDomainQuery(String hostname, Promise<List<T>> nextPromise) {
|
||||||
DnsResolveContext<T> nextContext = newResolverContext(parent, hostname, dnsClass, expectedTypes,
|
DnsResolveContext<T> nextContext = newResolverContext(parent, hostname, dnsClass, expectedTypes,
|
||||||
additionals, nameServerAddrs);
|
additionals, nameServerAddrs);
|
||||||
Promise<List<T>> nextPromise = parent.executor().newPromise();
|
|
||||||
nextContext.internalResolve(nextPromise);
|
nextContext.internalResolve(nextPromise);
|
||||||
nextPromise.addListener(listener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void internalResolve(Promise<List<T>> promise) {
|
private void internalResolve(Promise<List<T>> promise) {
|
||||||
|
@ -172,4 +172,30 @@ public class DefaultDnsCacheTest {
|
|||||||
group.shutdownGracefully();
|
group.shutdownGracefully();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDotHandling() throws Exception {
|
||||||
|
InetAddress addr1 = InetAddress.getByAddress(new byte[] { 10, 0, 0, 1 });
|
||||||
|
InetAddress addr2 = InetAddress.getByAddress(new byte[] { 10, 0, 0, 2 });
|
||||||
|
EventLoopGroup group = new DefaultEventLoopGroup(1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
EventLoop loop = group.next();
|
||||||
|
final DefaultDnsCache cache = new DefaultDnsCache(1, 100, 100);
|
||||||
|
cache.cache("netty.io", null, addr1, 10000, loop);
|
||||||
|
cache.cache("netty.io.", null, addr2, 10000, loop);
|
||||||
|
|
||||||
|
List<? extends DnsCacheEntry> entries = cache.get("netty.io", null);
|
||||||
|
assertEquals(2, entries.size());
|
||||||
|
assertEntry(entries.get(0), addr1);
|
||||||
|
assertEntry(entries.get(1), addr2);
|
||||||
|
|
||||||
|
List<? extends DnsCacheEntry> entries2 = cache.get("netty.io.", null);
|
||||||
|
assertEquals(2, entries2.size());
|
||||||
|
assertEntry(entries2.get(0), addr1);
|
||||||
|
assertEntry(entries2.get(1), addr2);
|
||||||
|
} finally {
|
||||||
|
group.shutdownGracefully();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -96,13 +96,7 @@ import static org.hamcrest.Matchers.greaterThan;
|
|||||||
import static org.hamcrest.Matchers.hasSize;
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
import static org.hamcrest.Matchers.instanceOf;
|
import static org.hamcrest.Matchers.instanceOf;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.*;
|
||||||
import static org.junit.Assert.assertNotEquals;
|
|
||||||
import static org.junit.Assert.assertNotNull;
|
|
||||||
import static org.junit.Assert.assertNull;
|
|
||||||
import static org.junit.Assert.assertThat;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
import static org.junit.Assert.fail;
|
|
||||||
|
|
||||||
public class DnsNameResolverTest {
|
public class DnsNameResolverTest {
|
||||||
|
|
||||||
@ -1746,8 +1740,7 @@ public class DnsNameResolverTest {
|
|||||||
String hostname, DnsRecord[] additionals, Throwable cause, EventLoop loop) {
|
String hostname, DnsRecord[] additionals, Throwable cause, EventLoop loop) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
}).authoritativeDnsServerCache(new DnsCache() {
|
||||||
.authoritativeDnsServerCache(new DnsCache() {
|
|
||||||
@Override
|
@Override
|
||||||
public void clear() {
|
public void clear() {
|
||||||
authoritativeLatch.countDown();
|
authoritativeLatch.countDown();
|
||||||
@ -1779,4 +1772,53 @@ public class DnsNameResolverTest {
|
|||||||
resolveLatch.await();
|
resolveLatch.await();
|
||||||
authoritativeLatch.await();
|
authoritativeLatch.await();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testResolveACachedWithDot() {
|
||||||
|
final DnsCache cache = new DefaultDnsCache();
|
||||||
|
DnsNameResolver resolver = newResolver(ResolvedAddressTypes.IPV4_ONLY)
|
||||||
|
.resolveCache(cache).build();
|
||||||
|
|
||||||
|
try {
|
||||||
|
String domain = DOMAINS.iterator().next();
|
||||||
|
String domainWithDot = domain + '.';
|
||||||
|
|
||||||
|
resolver.resolve(domain).syncUninterruptibly();
|
||||||
|
List<? extends DnsCacheEntry> cached = cache.get(domain, null);
|
||||||
|
List<? extends DnsCacheEntry> cached2 = cache.get(domainWithDot, null);
|
||||||
|
|
||||||
|
assertEquals(1, cached.size());
|
||||||
|
assertSame(cached, cached2);
|
||||||
|
} finally {
|
||||||
|
resolver.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testResolveACachedWithDotSearchDomain() throws Exception {
|
||||||
|
final TestDnsCache cache = new TestDnsCache(new DefaultDnsCache());
|
||||||
|
TestDnsServer server = new TestDnsServer(Collections.singleton("test.netty.io"));
|
||||||
|
server.start();
|
||||||
|
DnsNameResolver resolver = newResolver(ResolvedAddressTypes.IPV4_ONLY)
|
||||||
|
.searchDomains(Collections.singletonList("netty.io"))
|
||||||
|
.nameServerProvider(new SingletonDnsServerAddressStreamProvider(server.localAddress()))
|
||||||
|
.resolveCache(cache).build();
|
||||||
|
try {
|
||||||
|
resolver.resolve("test").syncUninterruptibly();
|
||||||
|
|
||||||
|
assertNull(cache.cacheHits.get("test.netty.io"));
|
||||||
|
|
||||||
|
List<? extends DnsCacheEntry> cached = cache.cache.get("test.netty.io", null);
|
||||||
|
List<? extends DnsCacheEntry> cached2 = cache.cache.get("test.netty.io.", null);
|
||||||
|
assertEquals(1, cached.size());
|
||||||
|
assertSame(cached, cached2);
|
||||||
|
|
||||||
|
resolver.resolve("test").syncUninterruptibly();
|
||||||
|
List<? extends DnsCacheEntry> entries = cache.cacheHits.get("test.netty.io");
|
||||||
|
assertFalse(entries.isEmpty());
|
||||||
|
} finally {
|
||||||
|
resolver.close();
|
||||||
|
server.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user