Correctly detect and handle CNAME loops. (#8691)

Motivation:

We do not correctly detect loops when follow CNAMEs and so may try to follow it without any success.

Modifications:

- Correctly detect CNAME loops
- Do not cache CNAME entries which point to itself
- Add unit test.

Result:

Fixes https://github.com/netty/netty/issues/8687.
This commit is contained in:
Norman Maurer 2019-01-14 08:17:44 +01:00 committed by GitHub
parent 6fdd7fcddb
commit 82ec6ba815
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 65 additions and 3 deletions

View File

@ -48,6 +48,7 @@ import java.util.AbstractList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@ -651,10 +652,11 @@ abstract class DnsResolveContext<T> {
// Make sure the record is for the questioned domain. // Make sure the record is for the questioned domain.
if (!recordName.equals(questionName)) { if (!recordName.equals(questionName)) {
Map<String, String> cnamesCopy = new HashMap<String, String>(cnames);
// Even if the record's name is not exactly same, it might be an alias defined in the CNAME records. // Even if the record's name is not exactly same, it might be an alias defined in the CNAME records.
String resolved = questionName; String resolved = questionName;
do { do {
resolved = cnames.get(resolved); resolved = cnamesCopy.remove(resolved);
if (recordName.equals(resolved)) { if (recordName.equals(resolved)) {
break; break;
} }
@ -749,9 +751,13 @@ abstract class DnsResolveContext<T> {
String mapping = domainName.toLowerCase(Locale.US); String mapping = domainName.toLowerCase(Locale.US);
// Cache the CNAME as well. // Cache the CNAME as well.
cache.cache(hostnameWithDot(name), hostnameWithDot(mapping), r.timeToLive(), loop); String nameWithDot = hostnameWithDot(name);
String mappingWithDot = hostnameWithDot(mapping);
if (!nameWithDot.equalsIgnoreCase(mappingWithDot)) {
cache.cache(nameWithDot, mappingWithDot, r.timeToLive(), loop);
cnames.put(name, mapping); cnames.put(name, mapping);
} }
}
return cnames != null? cnames : Collections.<String, String>emptyMap(); return cnames != null? cnames : Collections.<String, String>emptyMap();
} }
@ -875,12 +881,21 @@ abstract class DnsResolveContext<T> {
private void followCname(DnsQuestion question, String cname, DnsQueryLifecycleObserver queryLifecycleObserver, private void followCname(DnsQuestion question, String cname, DnsQueryLifecycleObserver queryLifecycleObserver,
Promise<List<T>> promise) { Promise<List<T>> promise) {
Set<String> cnames = null;
for (;;) { for (;;) {
// Resolve from cnameCache() until there is no more cname entry cached. // Resolve from cnameCache() until there is no more cname entry cached.
String mapping = cnameCache().get(hostnameWithDot(cname)); String mapping = cnameCache().get(hostnameWithDot(cname));
if (mapping == null) { if (mapping == null) {
break; break;
} }
if (cnames == null) {
// Detect loops.
cnames = new HashSet<String>(2);
}
if (!cnames.add(cname)) {
// Follow CNAME from cache would loop. Lets break here.
break;
}
cname = mapping; cname = mapping;
} }

View File

@ -96,6 +96,7 @@ import static io.netty.handler.codec.dns.DnsRecordType.AAAA;
import static io.netty.handler.codec.dns.DnsRecordType.CNAME; import static io.netty.handler.codec.dns.DnsRecordType.CNAME;
import static io.netty.resolver.dns.DnsServerAddresses.sequential; import static io.netty.resolver.dns.DnsServerAddresses.sequential;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.hamcrest.Matchers.greaterThan; 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;
@ -2135,6 +2136,52 @@ public class DnsNameResolverTest {
} }
} }
@Test
public void testFollowCNAMELoop() throws IOException {
expectedException.expect(UnknownHostException.class);
TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() {
@Override
public Set<ResourceRecord> getRecords(QuestionRecord question) {
Set<ResourceRecord> records = new LinkedHashSet<ResourceRecord>(4);
records.add(new TestDnsServer.TestResourceRecord("x." + question.getDomainName(),
RecordType.A, Collections.<String, Object>singletonMap(
DnsAttribute.IP_ADDRESS.toLowerCase(), "10.0.0.99")));
records.add(new TestDnsServer.TestResourceRecord(
"cname2.netty.io", RecordType.CNAME,
Collections.<String, Object>singletonMap(
DnsAttribute.DOMAIN_NAME.toLowerCase(), "cname.netty.io")));
records.add(new TestDnsServer.TestResourceRecord(
"cname.netty.io", RecordType.CNAME,
Collections.<String, Object>singletonMap(
DnsAttribute.DOMAIN_NAME.toLowerCase(), "cname2.netty.io")));
records.add(new TestDnsServer.TestResourceRecord(
question.getDomainName(), RecordType.CNAME,
Collections.<String, Object>singletonMap(
DnsAttribute.DOMAIN_NAME.toLowerCase(), "cname.netty.io")));
return records;
}
});
dnsServer2.start();
DnsNameResolver resolver = null;
try {
DnsNameResolverBuilder builder = newResolver()
.recursionDesired(false)
.resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY)
.maxQueriesPerResolve(16)
.nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress()));
resolver = builder.build();
resolver.resolveAll("somehost.netty.io").syncUninterruptibly().getNow();
} finally {
dnsServer2.stop();
if (resolver != null) {
resolver.close();
}
}
}
@Test @Test
public void testSearchDomainQueryFailureForSingleAddressTypeCompletes() { public void testSearchDomainQueryFailureForSingleAddressTypeCompletes() {
expectedException.expect(UnknownHostException.class); expectedException.expect(UnknownHostException.class);