Skip to content
188 changes: 172 additions & 16 deletions core/src/main/java/io/grpc/internal/DnsNameResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,25 +43,42 @@
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't seem like Android has javax.naming. Or at least the older ones don't. Have you tested on Android?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw the DnsContextFactory, which would imply that it does, but I also see things like this though: https://issues.apache.org/jira/browse/LOG4J2-703

So that may be newer versions have it. But I also didn't see javax.naming at https://developer.android.com/reference/packages.html

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it is absent. Progaurd complains about refs to missing javax.naming classes, but not the com.sun ones. I think if I check for both, it will properly disable itself.

import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.InitialDirContext;

/**
* A DNS-based {@link NameResolver}.
*
* <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;
Expand All @@ -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
Expand Down Expand Up @@ -128,7 +146,6 @@ public final synchronized void refresh() {
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.
Expand All @@ -149,10 +166,10 @@ public void run() {
savedListener.onAddresses(Collections.singletonList(server), Attributes.EMPTY);
return;
}

ResolutionResults resolvedInetAddrs;
try {
inetAddrs = getAllByName(host);
} catch (UnknownHostException e) {
resolvedInetAddrs = delegateResolver.resolve(host);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the TXT doesn't go anywhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently No. There are no consumers yet.

} catch (Exception e) {
synchronized (DnsNameResolver.this) {
if (shutdown) {
return;
Expand All @@ -168,8 +185,7 @@ public void run() {
}
// 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);
Expand All @@ -192,12 +208,6 @@ public void run() {
}
};

// 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) {
Expand Down Expand Up @@ -226,4 +236,150 @@ public final synchronized void shutdown() {
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be just a singleton object.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack


@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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to catch any exceptions from this, so that a failure to get TXT doesn't hose everything?

We may want to discuss with others what should happen if we get A/AAAA but have I/O errors durring TXT.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it, but it should be fairly loud. Not honoring the service config if present is something a user would want to fix. It shouldn't reach this point if the JNDI resolver isn't present.


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);
}
}
}
Loading