- Notifications
You must be signed in to change notification settings - Fork 4k
core: lookup TXT records when doing name resolution #2912
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a1b29c3 56bf190 996f065 30c8475 a3b4306 005d342 c629229 03bab09 File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| | @@ -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,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; | ||
| 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; | ||
| | @@ -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 @@ 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. | ||
| | @@ -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); | ||
| Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So the TXT doesn't go anywhere? Contributor Author There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| | @@ -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); | ||
| | @@ -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) { | ||
| | @@ -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 { | ||
| Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be just a singleton object. Contributor Author There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
| Member There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. Contributor Author There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it was when I looked last :/
https://android.googlesource.com/platform/libcore/+/42cdd0bf7251074df8bf6dfa7f5d62a01c0a1ef6/ojluni/src/main/java/com/sun/jndi/dns/DnsContextFactory.java
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.