Detect CNAME loops in the CNAME cache while trying to resolve (#10221)

Motivation:

We need to detect CNAME loops during lookup the DnsCnameCache as otherwise we may try to follow cnames forever.

Modifications:

- Correctly detect CNAME loops in the cache
- Add unit test

Result:

Fixes https://github.com/netty/netty/issues/10220
This commit is contained in:
Norman Maurer 2020-04-28 10:47:10 +02:00
parent ae95e9c3d6
commit fe20550a22
2 changed files with 71 additions and 23 deletions

View File

@ -265,15 +265,52 @@ abstract class DnsResolveContext<T> {
return name + '.';
}
private void internalResolve(String name, Promise<List<T>> promise) {
for (;;) {
// Resolve the final name from the CNAME cache until there is nothing to follow anymore. This also
// guards against loops in the cache but early return once a loop is detected.
private String cnameResolveFromCache(String name) {
DnsCnameCache cnameCache = cnameCache();
String first = cnameCache.get(hostnameWithDot(name));
if (first == null) {
// Nothing in the cache at all
return name;
}
String second = cnameCache.get(hostnameWithDot(first));
if (second == null) {
// Nothing else to follow, return first match.
return first;
}
if (first.equals(second)) {
// Loop detected.... early return.
return first;
}
return cnameResolveFromCacheLoop(cnameCache, first, second);
}
private String cnameResolveFromCacheLoop(DnsCnameCache cnameCache, String first, String mapping) {
// Detect loops using a HashSet. We use this as last resort implementation to reduce allocations in the most
// common cases.
Set<String> cnames = new HashSet<>(4);
cnames.add(first);
cnames.add(mapping);
String name = mapping;
// Resolve from cnameCache() until there is no more cname entry cached.
String mapping = cnameCache().get(hostnameWithDot(name));
if (mapping == null) {
while ((mapping = cnameCache.get(hostnameWithDot(name))) != null) {
if (!cnames.add(mapping)) {
// Follow CNAME from cache would loop. Lets break here.
break;
}
name = mapping;
}
return name;
}
private void internalResolve(String name, Promise<List<T>> promise) {
// Resolve from cnameCache() until there is no more cname entry cached.
name = cnameResolveFromCache(name);
try {
DnsServerAddressStream nameServerAddressStream = getNameServers(name);
@ -936,24 +973,7 @@ abstract class DnsResolveContext<T> {
private void followCname(DnsQuestion question, String cname, DnsQueryLifecycleObserver queryLifecycleObserver,
Promise<List<T>> promise) {
Set<String> cnames = null;
for (;;) {
// Resolve from cnameCache() until there is no more cname entry cached.
String mapping = cnameCache().get(hostnameWithDot(cname));
if (mapping == null) {
break;
}
if (cnames == null) {
// Detect loops.
cnames = new HashSet<>(2);
}
if (!cnames.add(cname)) {
// Follow CNAME from cache would loop. Lets break here.
break;
}
cname = mapping;
}
cname = cnameResolveFromCache(cname);
DnsServerAddressStream stream = getNameServers(cname);
final DnsQuestion cnameQuestion;

View File

@ -2176,6 +2176,34 @@ public class DnsNameResolverTest {
}
}
@Test
public void testCNAMELoopInCache() throws Throwable {
expectedException.expect(UnknownHostException.class);
DnsNameResolver resolver = null;
try {
DnsNameResolverBuilder builder = newResolver()
.recursionDesired(false)
.resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY)
.maxQueriesPerResolve(16)
.nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer.localAddress()));
resolver = builder.build();
// Add a CNAME loop into the cache
String name = "somehost.netty.io.";
String name2 = "cname.netty.io.";
resolver.cnameCache().cache(name, name2, Long.MAX_VALUE, resolver.executor());
resolver.cnameCache().cache(name2, name, Long.MAX_VALUE, resolver.executor());
resolver.resolve(name).syncUninterruptibly().getNow();
} catch (CompletionException e) {
throw e.getCause();
} finally {
if (resolver != null) {
resolver.close();
}
}
}
@Test
public void testSearchDomainQueryFailureForSingleAddressTypeCompletes() throws Throwable {
expectedException.expect(UnknownHostException.class);