Filter DNS results so they only contain the expected type when multiple types are present. (#7875)

Motivation:

Currently, if a DNS server returns a non-preferred address type before the preferred one, then both will be returned as the result, and when only taking a single one, this usually ends up being the non-preferred type. However, the JDK requires lookups to only return the preferred type when possible to allow for backwards compatibility.

To allow a client to be able to resolve the appropriate address when running on a machine that does not support IPv6 but the DNS server returns IPv6 addresses before IPv4 addresses when querying.

Modification:

Filter the returned records to the expected type when both types are present.

Result:

Allows a client to run on a machine with IPv6 disabled even when a server returns both IPv4 and IPv6 results. Netty-based code can be a drop-in replacement for JDK-based code in such circumstances.

This PR filters results before returning them to respect JDK expectations.
This commit is contained in:
Anuraag Agrawal 2018-04-19 17:52:22 +09:00 committed by Norman Maurer
parent 3ec29455af
commit 4cd39cc4b3
5 changed files with 105 additions and 2 deletions

View File

@ -19,6 +19,7 @@ import static io.netty.resolver.dns.DnsAddressDecoder.decodeAddress;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import io.netty.channel.EventLoop;
@ -61,6 +62,31 @@ final class DnsAddressResolveContext extends DnsResolveContext<InetAddress> {
return false;
}
@Override
List<InetAddress> filterResults(List<InetAddress> unfiltered) {
final Class<? extends InetAddress> inetAddressType = parent.preferredAddressType().addressType();
final int size = unfiltered.size();
int numExpected = 0;
for (int i = 0; i < size; i++) {
InetAddress address = unfiltered.get(i);
if (inetAddressType.isInstance(address)) {
numExpected++;
}
}
if (numExpected == size || numExpected == 0) {
// If all the results are the preferred type, or none of them are, then we don't need to do any filtering.
return unfiltered;
}
List<InetAddress> filtered = new ArrayList<InetAddress>(numExpected);
for (int i = 0; i < size; i++) {
InetAddress address = unfiltered.get(i);
if (inetAddressType.isInstance(address)) {
filtered.add(address);
}
}
return filtered;
}
@Override
void cache(String hostname, DnsRecord[] additionals,
DnsRecord result, InetAddress convertedResult) {

View File

@ -61,6 +61,11 @@ final class DnsRecordResolveContext extends DnsResolveContext<DnsRecord> {
return true;
}
@Override
List<DnsRecord> filterResults(List<DnsRecord> unfiltered) {
return unfiltered;
}
@Override
void cache(String hostname, DnsRecord[] additionals, DnsRecord result, DnsRecord convertedResult) {
// Do not cache.

View File

@ -31,6 +31,7 @@ import io.netty.handler.codec.dns.DnsRecordType;
import io.netty.handler.codec.dns.DnsResponse;
import io.netty.handler.codec.dns.DnsResponseCode;
import io.netty.handler.codec.dns.DnsSection;
import io.netty.util.NetUtil;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.FutureListener;
@ -142,6 +143,12 @@ abstract class DnsResolveContext<T> {
*/
abstract boolean containsExpectedResult(List<T> finalResult);
/**
* Returns a filtered list of results which should be the final result of DNS resolution. This must take into
* account JDK semantics such as {@link NetUtil#isIpV6AddressesPreferred()}.
*/
abstract List<T> filterResults(List<T> unfiltered);
/**
* Caches a successful resolution.
*/
@ -702,7 +709,7 @@ abstract class DnsResolveContext<T> {
if (finalResult != null) {
// Found at least one resolved record.
trySuccess(promise, finalResult);
trySuccess(promise, filterResults(finalResult));
return;
}

View File

@ -66,9 +66,12 @@ import java.net.UnknownHostException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -76,6 +79,7 @@ import java.util.Map.Entry;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@ -1177,6 +1181,67 @@ public class DnsNameResolverTest {
testRecursiveResolveCache(true);
}
@Test
public void testIpv4PreferredWhenIpv6First() throws Exception {
testResolvesPreferredWhenNonPreferredFirst0(ResolvedAddressTypes.IPV4_PREFERRED);
}
@Test
public void testIpv6PreferredWhenIpv4First() throws Exception {
testResolvesPreferredWhenNonPreferredFirst0(ResolvedAddressTypes.IPV6_PREFERRED);
}
private static void testResolvesPreferredWhenNonPreferredFirst0(ResolvedAddressTypes types) throws Exception {
final String name = "netty.com";
// This store is non-compliant, returning records of the wrong type for a query.
// It works since we don't verify the type of the result when resolving to deal with
// non-compliant servers in the wild.
List<Set<ResourceRecord>> records = new ArrayList<Set<ResourceRecord>>();
final String ipv6Address = "0:0:0:0:0:0:1:1";
final String ipv4Address = "1.1.1.1";
if (types == ResolvedAddressTypes.IPV4_PREFERRED) {
records.add(newAddressRecord(name, RecordType.AAAA, ipv6Address));
records.add(newAddressRecord(name, RecordType.A, ipv4Address));
} else {
records.add(newAddressRecord(name, RecordType.A, ipv4Address));
records.add(newAddressRecord(name, RecordType.AAAA, ipv6Address));
}
final Iterator<Set<ResourceRecord>> recordsIterator = records.iterator();
RecordStore arbitrarilyOrderedStore = new RecordStore() {
@Override
public Set<ResourceRecord> getRecords(QuestionRecord questionRecord) {
return recordsIterator.next();
}
};
TestDnsServer nonCompliantDnsServer = new TestDnsServer(arbitrarilyOrderedStore);
nonCompliantDnsServer.start();
try {
DnsNameResolver resolver = newResolver(types)
.maxQueriesPerResolve(2)
.nameServerProvider(new SingletonDnsServerAddressStreamProvider(
nonCompliantDnsServer.localAddress()))
.build();
InetAddress resolved = resolver.resolve("netty.com").syncUninterruptibly().getNow();
if (types == ResolvedAddressTypes.IPV4_PREFERRED) {
assertEquals(ipv4Address, resolved.getHostAddress());
} else {
assertEquals(ipv6Address, resolved.getHostAddress());
}
} finally {
nonCompliantDnsServer.stop();
}
}
private static Set<ResourceRecord> newAddressRecord(String name, RecordType type, String address) {
ResourceRecordModifier rm = new ResourceRecordModifier();
rm.setDnsClass(RecordClass.IN);
rm.setDnsName(name);
rm.setDnsTtl(100);
rm.setDnsType(type);
rm.put(DnsAttribute.IP_ADDRESS, address);
return Collections.singleton(rm.getEntry());
}
private static void testRecursiveResolveCache(boolean cache)
throws Exception {
final String hostname = "some.record.netty.io";

View File

@ -258,7 +258,7 @@ class TestDnsServer extends DnsServer {
private final Set<String> domains;
public TestRecordStore(Set<String> domains) {
private TestRecordStore(Set<String> domains) {
this.domains = domains;
}