core: lookup TXT records when doing name resolution

This commit is contained in:
Carl Mastrangelo 2017-04-20 14:11:48 -07:00 committed by GitHub
parent e576c4cb4c
commit b7833dab05
2 changed files with 301 additions and 51 deletions

View File

@ -31,6 +31,8 @@
package io.grpc.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import io.grpc.Attributes;
@ -41,15 +43,22 @@ import io.grpc.internal.SharedResourceHolder.Resource;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.InitialDirContext;
/**
* A DNS-based {@link NameResolver}.
@ -57,9 +66,19 @@ import javax.annotation.concurrent.GuardedBy;
* <p>Each {@code A} or {@code AAAA} record emits an {@link EquivalentAddressGroup} in the list
* passed to {@link NameResolver.Listener#onUpdate}
*
* @see DnsNameResolverFactory
* @see DnsNameResolverProvider
*/
class DnsNameResolver extends NameResolver {
final class DnsNameResolver extends NameResolver {
private static final Logger logger = Logger.getLogger(DnsNameResolver.class.getName());
private static final boolean isJndiAvailable = jndiAvailable();
@VisibleForTesting
static boolean enableJndi = false;
private DelegateResolver delegateResolver = pickDelegateResolver();
private final String authority;
private final String host;
private final int port;
@ -83,7 +102,6 @@ class DnsNameResolver extends NameResolver {
Resource<ExecutorService> executorResource) {
// TODO: if a DNS server is provided as nsAuthority, use it.
// https://www.captechconsulting.com/blogs/accessing-the-dusty-corners-of-dns-with-java
this.timerServiceResource = timerServiceResource;
this.executorResource = executorResource;
// Must prepend a "//" to the name when constructing a URI, otherwise it will be treated as an
@ -128,7 +146,6 @@ class DnsNameResolver extends NameResolver {
private final Runnable resolutionRunnable = new Runnable() {
@Override
public void run() {
InetAddress[] inetAddrs;
Listener savedListener;
synchronized (DnsNameResolver.this) {
// If this task is started by refresh(), there might already be a scheduled task.
@ -149,10 +166,10 @@ class DnsNameResolver extends NameResolver {
savedListener.onAddresses(Collections.singletonList(server), Attributes.EMPTY);
return;
}
ResolutionResults resolvedInetAddrs;
try {
inetAddrs = getAllByName(host);
} catch (UnknownHostException e) {
resolvedInetAddrs = delegateResolver.resolve(host);
} catch (Exception e) {
synchronized (DnsNameResolver.this) {
if (shutdown) {
return;
@ -168,8 +185,7 @@ class DnsNameResolver extends NameResolver {
}
// Each address forms an EAG
ArrayList<EquivalentAddressGroup> servers = new ArrayList<EquivalentAddressGroup>();
for (int i = 0; i < inetAddrs.length; i++) {
InetAddress inetAddr = inetAddrs[i];
for (InetAddress inetAddr : resolvedInetAddrs.addresses) {
servers.add(new EquivalentAddressGroup(new InetSocketAddress(inetAddr, port)));
}
savedListener.onAddresses(servers, Attributes.EMPTY);
@ -192,12 +208,6 @@ class DnsNameResolver extends NameResolver {
}
};
// To be mocked out in tests
@VisibleForTesting
InetAddress[] getAllByName(String host) throws UnknownHostException {
return InetAddress.getAllByName(host);
}
@GuardedBy("this")
private void resolve() {
if (resolving || shutdown) {
@ -226,4 +236,150 @@ class DnsNameResolver extends NameResolver {
final int getPort() {
return port;
}
private DelegateResolver pickDelegateResolver() {
JdkResolver jdkResolver = new JdkResolver();
if (isJndiAvailable && enableJndi) {
return new CompositeResolver(jdkResolver, new JndiResolver());
}
return jdkResolver;
}
/**
* Forces the resolver. This should only be used by testing code.
*/
@VisibleForTesting
void setDelegateResolver(DelegateResolver delegateResolver) {
this.delegateResolver = delegateResolver;
}
/**
* Returns whether the JNDI DNS resolver is available. This is accomplished by looking up a
* particular class. It is believed to be the default (only?) DNS resolver that will actually be
* used. It is provided by the OpenJDK, but unlikely Android. Actual resolution will be done by
* using a service provider when a hostname query is present, so the {@code DnsContextFactory}
* may not actually be used to perform the query. This is believed to be "okay."
*/
@VisibleForTesting
@SuppressWarnings("LiteralClassName")
static boolean jndiAvailable() {
try {
Class.forName("javax.naming.directory.InitialDirContext");
Class.forName("com.sun.jndi.dns.DnsContextFactory");
} catch (ClassNotFoundException e) {
logger.log(Level.FINE, "Unable to find JNDI DNS resolver, skipping", e);
return false;
}
return true;
}
/**
* Common interface between the delegate resolvers used by DnsNameResolver.
*/
@VisibleForTesting
abstract static class DelegateResolver {
abstract ResolutionResults resolve(String host) throws Exception;
}
/**
* Describes the results from a DNS query.
*/
@VisibleForTesting
static final class ResolutionResults {
final List<InetAddress> addresses;
final List<String> txtRecords;
ResolutionResults(List<InetAddress> addresses, List<String> txtRecords) {
this.addresses = Collections.unmodifiableList(checkNotNull(addresses, "addresses"));
this.txtRecords = Collections.unmodifiableList(checkNotNull(txtRecords, "txtRecords"));
}
}
/**
* A composite DNS resolver that uses both the JDK and JNDI resolvers as delegate. It is
* expected that two DNS queries will be executed, with the second one being from JNDI.
*/
@VisibleForTesting
static final class CompositeResolver extends DelegateResolver {
private final DelegateResolver jdkResovler;
private final DelegateResolver jndiResovler;
CompositeResolver(DelegateResolver jdkResovler, DelegateResolver jndiResovler) {
this.jdkResovler = jdkResovler;
this.jndiResovler = jndiResovler;
}
@Override
ResolutionResults resolve(String host) throws Exception {
ResolutionResults jdkResults = jdkResovler.resolve(host);
List<InetAddress> addresses = jdkResults.addresses;
List<String> txtRecords = Collections.emptyList();
try {
ResolutionResults jdniResults = jndiResovler.resolve(host);
txtRecords = jdniResults.txtRecords;
} catch (Exception e) {
logger.log(Level.SEVERE, "Failed to resolve TXT results", e);
}
return new ResolutionResults(addresses, txtRecords);
}
}
/**
* The default name resolver provided with the JDK. This is unable to lookup TXT records, but
* provides address ordering sorted according to RFC 3484. This is true on OpenJDK, because it
* in turn calls into libc which sorts addresses in order of reachability.
*/
@VisibleForTesting
static final class JdkResolver extends DelegateResolver {
@Override
ResolutionResults resolve(String host) throws Exception {
return new ResolutionResults(
Arrays.asList(InetAddress.getAllByName(host)),
Collections.<String>emptyList());
}
}
/**
* A resolver that uses JNDI. This class is capable of looking up both addresses
* and text records, but does not provide ordering guarantees. It is currently not used for
* address resolution.
*/
@VisibleForTesting
static final class JndiResolver extends DelegateResolver {
private static final String[] rrTypes = new String[]{"TXT"};
@Override
ResolutionResults resolve(String host) throws NamingException {
InitialDirContext dirContext = new InitialDirContext();
javax.naming.directory.Attributes attrs = dirContext.getAttributes("dns:///" + host, rrTypes);
List<InetAddress> addresses = new ArrayList<InetAddress>();
List<String> txtRecords = new ArrayList<String>();
NamingEnumeration<? extends Attribute> rrGroups = attrs.getAll();
try {
while (rrGroups.hasMore()) {
Attribute rrEntry = rrGroups.next();
assert Arrays.asList(rrTypes).contains(rrEntry.getID());
NamingEnumeration<?> rrValues = rrEntry.getAll();
try {
while (rrValues.hasMore()) {
String rrValue = (String) rrValues.next();
txtRecords.add(rrValue);
}
} finally {
rrValues.close();
}
}
} finally {
rrGroups.close();
}
return new ResolutionResults(addresses, txtRecords);
}
}
}

View File

@ -31,6 +31,7 @@
package io.grpc.internal;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
@ -40,22 +41,30 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import com.google.common.base.MoreObjects;
import com.google.common.collect.Iterables;
import io.grpc.Attributes;
import io.grpc.EquivalentAddressGroup;
import io.grpc.NameResolver;
import io.grpc.Status;
import io.grpc.internal.DnsNameResolver.DelegateResolver;
import io.grpc.internal.DnsNameResolver.ResolutionResults;
import io.grpc.internal.SharedResourceHolder.Resource;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -75,6 +84,7 @@ public class DnsNameResolverTest {
private final DnsNameResolverProvider provider = new DnsNameResolverProvider();
private final FakeClock fakeClock = new FakeClock();
private final FakeClock fakeExecutor = new FakeClock();
private MockResolver mockResolver = new MockResolver();
private final Resource<ScheduledExecutorService> fakeTimerServiceResource =
new Resource<ScheduledExecutorService>() {
@Override
@ -108,9 +118,21 @@ public class DnsNameResolverTest {
@Captor
private ArgumentCaptor<Status> statusCaptor;
private DnsNameResolver newResolver(String name, int port) {
DnsNameResolver dnsResolver = new DnsNameResolver(
null,
name,
Attributes.newBuilder().set(NameResolver.Factory.PARAMS_DEFAULT_PORT, port).build(),
fakeTimerServiceResource,
fakeExecutorResource);
dnsResolver.setDelegateResolver(mockResolver);
return dnsResolver;
}
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
DnsNameResolver.enableJndi = true;
}
@After
@ -143,21 +165,23 @@ public class DnsNameResolverTest {
@Test
public void resolve() throws Exception {
InetAddress[] answer1 = createAddressList(2);
InetAddress[] answer2 = createAddressList(1);
List<InetAddress> answer1 = createAddressList(2);
List<InetAddress> answer2 = createAddressList(1);
String name = "foo.googleapis.com";
MockResolver resolver = new MockResolver(name, 81, answer1, answer2);
DnsNameResolver resolver = newResolver(name, 81);
mockResolver.addAnswer(answer1).addAnswer(answer2);
resolver.start(mockListener);
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener).onAddresses(resultCaptor.capture(), any(Attributes.class));
assertEquals(name, resolver.invocations.poll());
assertEquals(name, mockResolver.invocations.poll());
assertAnswerMatches(answer1, 81, resultCaptor.getValue());
assertEquals(0, fakeClock.numPendingTasks());
resolver.refresh();
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener, times(2)).onAddresses(resultCaptor.capture(), any(Attributes.class));
assertEquals(name, resolver.invocations.poll());
assertEquals(name, mockResolver.invocations.poll());
assertAnswerMatches(answer2, 81, resultCaptor.getValue());
assertEquals(0, fakeClock.numPendingTasks());
@ -168,12 +192,13 @@ public class DnsNameResolverTest {
public void retry() throws Exception {
String name = "foo.googleapis.com";
UnknownHostException error = new UnknownHostException(name);
InetAddress[] answer = createAddressList(2);
MockResolver resolver = new MockResolver(name, 81, error, error, answer);
List<InetAddress> answer = createAddressList(2);
DnsNameResolver resolver = newResolver(name, 81);
mockResolver.addAnswer(error).addAnswer(error).addAnswer(answer);
resolver.start(mockListener);
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener).onError(statusCaptor.capture());
assertEquals(name, resolver.invocations.poll());
assertEquals(name, mockResolver.invocations.poll());
Status status = statusCaptor.getValue();
assertEquals(Status.Code.UNAVAILABLE, status.getCode());
assertSame(error, status.getCause());
@ -187,7 +212,7 @@ public class DnsNameResolverTest {
fakeClock.forwardNanos(1);
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener, times(2)).onError(statusCaptor.capture());
assertEquals(name, resolver.invocations.poll());
assertEquals(name, mockResolver.invocations.poll());
status = statusCaptor.getValue();
assertEquals(Status.Code.UNAVAILABLE, status.getCode());
assertSame(error, status.getCause());
@ -202,7 +227,7 @@ public class DnsNameResolverTest {
assertEquals(0, fakeClock.numPendingTasks());
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener).onAddresses(resultCaptor.capture(), any(Attributes.class));
assertEquals(name, resolver.invocations.poll());
assertEquals(name, mockResolver.invocations.poll());
assertAnswerMatches(answer, 81, resultCaptor.getValue());
verifyNoMoreInteractions(mockListener);
@ -212,12 +237,13 @@ public class DnsNameResolverTest {
public void refreshCancelsScheduledRetry() throws Exception {
String name = "foo.googleapis.com";
UnknownHostException error = new UnknownHostException(name);
InetAddress[] answer = createAddressList(2);
MockResolver resolver = new MockResolver(name, 81, error, answer);
List<InetAddress> answer = createAddressList(2);
DnsNameResolver resolver = newResolver(name, 81);
mockResolver.addAnswer(error).addAnswer(answer);
resolver.start(mockListener);
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener).onError(statusCaptor.capture());
assertEquals(name, resolver.invocations.poll());
assertEquals(name, mockResolver.invocations.poll());
Status status = statusCaptor.getValue();
assertEquals(Status.Code.UNAVAILABLE, status.getCode());
assertSame(error, status.getCause());
@ -230,7 +256,7 @@ public class DnsNameResolverTest {
// Refresh cancelled the retry
assertEquals(0, fakeClock.numPendingTasks());
verify(mockListener).onAddresses(resultCaptor.capture(), any(Attributes.class));
assertEquals(name, resolver.invocations.poll());
assertEquals(name, mockResolver.invocations.poll());
assertAnswerMatches(answer, 81, resultCaptor.getValue());
verifyNoMoreInteractions(mockListener);
@ -240,12 +266,13 @@ public class DnsNameResolverTest {
public void shutdownCancelsScheduledRetry() throws Exception {
String name = "foo.googleapis.com";
UnknownHostException error = new UnknownHostException(name);
MockResolver resolver = new MockResolver(name, 81, error);
DnsNameResolver resolver = newResolver(name, 81);
mockResolver.addAnswer(error);
resolver.start(mockListener);
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener).onError(statusCaptor.capture());
assertEquals(name, resolver.invocations.poll());
assertEquals(name, mockResolver.invocations.poll());
Status status = statusCaptor.getValue();
assertEquals(Status.Code.UNAVAILABLE, status.getCode());
assertSame(error, status.getCause());
@ -260,6 +287,64 @@ public class DnsNameResolverTest {
verifyNoMoreInteractions(mockListener);
}
@Test(timeout = 10000)
public void jdkResolverWorks() throws Exception {
DnsNameResolver.DelegateResolver resolver = new DnsNameResolver.JdkResolver();
ResolutionResults results = resolver.resolve("localhost");
// Just check that *something* came back.
assertThat(results.addresses).isNotEmpty();
assertThat(results.txtRecords).isNotNull();
}
@Test(timeout = 10000)
public void jndiResolverWorks() throws Exception {
Assume.assumeTrue(DnsNameResolver.jndiAvailable());
DnsNameResolver.DelegateResolver resolver = new DnsNameResolver.JndiResolver();
ResolutionResults results = null;
try {
results = resolver.resolve("localhost");
} catch (javax.naming.NameNotFoundException e) {
Assume.assumeNoException(e);
}
assertThat(results.addresses).isEmpty();
assertThat(results.txtRecords).isNotNull();
}
@Test
public void compositeResolverPrefersJdkAddressJndiTxt() throws Exception {
MockResolver jdkDelegate = new MockResolver();
MockResolver jndiDelegate = new MockResolver();
DelegateResolver resolver = new DnsNameResolver.CompositeResolver(jdkDelegate, jndiDelegate);
List<InetAddress> jdkAnswer = createAddressList(2);
jdkDelegate.addAnswer(jdkAnswer, Arrays.asList("jdktxt"));
List<InetAddress> jdniAnswer = createAddressList(2);
jndiDelegate.addAnswer(jdniAnswer, Arrays.asList("jnditxt"));
ResolutionResults results = resolver.resolve("abc");
assertThat(results.addresses).containsExactlyElementsIn(jdkAnswer).inOrder();
assertThat(results.txtRecords).containsExactly("jnditxt");
}
@Test
public void compositeResolverSkipsAbsentJndi() throws Exception {
MockResolver jdkDelegate = new MockResolver();
MockResolver jndiDelegate = null;
DelegateResolver resolver = new DnsNameResolver.CompositeResolver(jdkDelegate, jndiDelegate);
List<InetAddress> jdkAnswer = createAddressList(2);
jdkDelegate.addAnswer(jdkAnswer);
ResolutionResults results = resolver.resolve("abc");
assertThat(results.addresses).containsExactlyElementsIn(jdkAnswer).inOrder();
assertThat(results.txtRecords).isEmpty();
}
private void testInvalidUri(URI uri) {
try {
provider.newNameResolver(uri, NAME_RESOLVER_PARAMS);
@ -278,47 +363,56 @@ public class DnsNameResolverTest {
private byte lastByte = 0;
private InetAddress[] createAddressList(int n) throws UnknownHostException {
InetAddress[] list = new InetAddress[n];
private List<InetAddress> createAddressList(int n) throws UnknownHostException {
List<InetAddress> list = new ArrayList<InetAddress>(n);
for (int i = 0; i < n; i++) {
list[i] = InetAddress.getByAddress(new byte[] {127, 0, 0, ++lastByte});
list.add(InetAddress.getByAddress(new byte[] {127, 0, 0, ++lastByte}));
}
return list;
}
private static void assertAnswerMatches(
InetAddress[] addrs, int port, List<EquivalentAddressGroup> results) {
assertEquals(addrs.length, results.size());
for (int i = 0; i < addrs.length; i++) {
List<InetAddress> addrs, int port, List<EquivalentAddressGroup> results) {
assertEquals(addrs.size(), results.size());
for (int i = 0; i < addrs.size(); i++) {
EquivalentAddressGroup addrGroup = results.get(i);
InetSocketAddress socketAddr =
(InetSocketAddress) Iterables.getOnlyElement(addrGroup.getAddresses());
assertEquals("Addr " + i, port, socketAddr.getPort());
assertEquals("Addr " + i, addrs[i], socketAddr.getAddress());
assertEquals("Addr " + i, addrs.get(i), socketAddr.getAddress());
}
}
private class MockResolver extends DnsNameResolver {
final LinkedList<Object> answers = new LinkedList<Object>();
final LinkedList<String> invocations = new LinkedList<String>();
private static class MockResolver extends DnsNameResolver.DelegateResolver {
private final Queue<Object> answers = new LinkedList<Object>();
private final Queue<String> invocations = new LinkedList<String>();
MockResolver(String name, int defaultPort, Object ... answers) {
super(null, name, Attributes.newBuilder().set(
NameResolver.Factory.PARAMS_DEFAULT_PORT, defaultPort).build(), fakeTimerServiceResource,
fakeExecutorResource);
for (Object answer : answers) {
this.answers.add(answer);
}
MockResolver addAnswer(List<InetAddress> addresses) {
return addAnswer(addresses, null);
}
MockResolver addAnswer(List<InetAddress> addresses, List<String> txtRecords) {
answers.add(
new ResolutionResults(
addresses,
MoreObjects.firstNonNull(txtRecords, Collections.<String>emptyList())));
return this;
}
MockResolver addAnswer(UnknownHostException ex) {
answers.add(ex);
return this;
}
@SuppressWarnings("unchecked") // explosions acceptable.
@Override
InetAddress[] getAllByName(String host) throws UnknownHostException {
ResolutionResults resolve(String host) throws Exception {
invocations.add(host);
Object answer = answers.poll();
if (answer instanceof UnknownHostException) {
throw (UnknownHostException) answer;
}
return (InetAddress[]) answer;
return (ResolutionResults) answer;
}
}
}