io.grpc.internal.DnsNameResolver Maven / Gradle / Ivy
/*
* Copyright 2015 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.base.Throwables;
import com.google.common.base.Verify;
import com.google.common.base.VerifyException;
import io.grpc.Attributes;
import io.grpc.EquivalentAddressGroup;
import io.grpc.NameResolver;
import io.grpc.ProxiedSocketAddress;
import io.grpc.ProxyDetector;
import io.grpc.Status;
import io.grpc.SynchronizationContext;
import io.grpc.internal.SharedResourceHolder.Resource;
import java.io.IOException;
import java.lang.reflect.Constructor;
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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
/**
* A DNS-based {@link NameResolver}.
*
* Each {@code A} or {@code AAAA} record emits an {@link EquivalentAddressGroup} in the list
* passed to {@link NameResolver.Listener2#onResult(ResolutionResult)}.
*
* @see DnsNameResolverProvider
*/
public class DnsNameResolver extends NameResolver {
private static final Logger logger = Logger.getLogger(DnsNameResolver.class.getName());
private static final String SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY = "clientLanguage";
private static final String SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY = "percentage";
private static final String SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY = "clientHostname";
private static final String SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY = "serviceConfig";
// From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
static final String SERVICE_CONFIG_PREFIX = "grpc_config=";
private static final Set SERVICE_CONFIG_CHOICE_KEYS =
Collections.unmodifiableSet(
new HashSet<>(
Arrays.asList(
SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY,
SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY,
SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY,
SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY)));
// From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
private static final String SERVICE_CONFIG_NAME_PREFIX = "_grpc_config.";
private static final String JNDI_PROPERTY =
System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_jndi", "true");
private static final String JNDI_LOCALHOST_PROPERTY =
System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_jndi_localhost", "false");
private static final String JNDI_TXT_PROPERTY =
System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_service_config", "false");
/**
* Java networking system properties name for caching DNS result.
*
* Default value is -1 (cache forever) if security manager is installed. If security manager is
* not installed, the ttl value is {@code null} which falls back to {@link
* #DEFAULT_NETWORK_CACHE_TTL_SECONDS gRPC default value}.
*
*
For android, gRPC doesn't attempt to cache; this property value will be ignored.
*/
@VisibleForTesting
static final String NETWORKADDRESS_CACHE_TTL_PROPERTY = "networkaddress.cache.ttl";
/** Default DNS cache duration if network cache ttl value is not specified ({@code null}). */
@VisibleForTesting
static final long DEFAULT_NETWORK_CACHE_TTL_SECONDS = 30;
@VisibleForTesting
static boolean enableJndi = Boolean.parseBoolean(JNDI_PROPERTY);
@VisibleForTesting
static boolean enableJndiLocalhost = Boolean.parseBoolean(JNDI_LOCALHOST_PROPERTY);
@VisibleForTesting
protected static boolean enableTxt = Boolean.parseBoolean(JNDI_TXT_PROPERTY);
private static final ResourceResolverFactory resourceResolverFactory =
getResourceResolverFactory(DnsNameResolver.class.getClassLoader());
@VisibleForTesting
final ProxyDetector proxyDetector;
/** Access through {@link #getLocalHostname}. */
private static String localHostname;
private final Random random = new Random();
protected volatile AddressResolver addressResolver = JdkAddressResolver.INSTANCE;
private final AtomicReference resourceResolver = new AtomicReference<>();
private final String authority;
private final String host;
private final int port;
/** Executor that will be used if an Executor is not provide via {@link NameResolver.Args}. */
private final Resource executorResource;
private final long cacheTtlNanos;
private final SynchronizationContext syncContext;
// Following fields must be accessed from syncContext
private final Stopwatch stopwatch;
protected boolean resolved;
private boolean shutdown;
private Executor executor;
/** True if using an executor resource that should be released after use. */
private final boolean usingExecutorResource;
private final ServiceConfigParser serviceConfigParser;
private boolean resolving;
// The field must be accessed from syncContext, although the methods on an Listener2 can be called
// from any thread.
private NameResolver.Listener2 listener;
protected DnsNameResolver(
@Nullable String nsAuthority,
String name,
Args args,
Resource executorResource,
Stopwatch stopwatch,
boolean isAndroid) {
checkNotNull(args, "args");
// 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.executorResource = executorResource;
// Must prepend a "//" to the name when constructing a URI, otherwise it will be treated as an
// opaque URI, thus the authority and host of the resulted URI would be null.
URI nameUri = URI.create("//" + checkNotNull(name, "name"));
Preconditions.checkArgument(nameUri.getHost() != null, "Invalid DNS name: %s", name);
authority = Preconditions.checkNotNull(nameUri.getAuthority(),
"nameUri (%s) doesn't have an authority", nameUri);
host = nameUri.getHost();
if (nameUri.getPort() == -1) {
port = args.getDefaultPort();
} else {
port = nameUri.getPort();
}
this.proxyDetector = checkNotNull(args.getProxyDetector(), "proxyDetector");
this.cacheTtlNanos = getNetworkAddressCacheTtlNanos(isAndroid);
this.stopwatch = checkNotNull(stopwatch, "stopwatch");
this.syncContext = checkNotNull(args.getSynchronizationContext(), "syncContext");
this.executor = args.getOffloadExecutor();
this.usingExecutorResource = executor == null;
this.serviceConfigParser = checkNotNull(args.getServiceConfigParser(), "serviceConfigParser");
}
@Override
public String getServiceAuthority() {
return authority;
}
@VisibleForTesting
protected String getHost() {
return host;
}
@Override
public void start(Listener2 listener) {
Preconditions.checkState(this.listener == null, "already started");
if (usingExecutorResource) {
executor = SharedResourceHolder.get(executorResource);
}
this.listener = checkNotNull(listener, "listener");
resolve();
}
@Override
public void refresh() {
Preconditions.checkState(listener != null, "not started");
resolve();
}
private List resolveAddresses() {
List extends InetAddress> addresses;
Exception addressesException = null;
try {
addresses = addressResolver.resolveAddress(host);
} catch (Exception e) {
addressesException = e;
Throwables.throwIfUnchecked(e);
throw new RuntimeException(e);
} finally {
if (addressesException != null) {
logger.log(Level.FINE, "Address resolution failure", addressesException);
}
}
// Each address forms an EAG
List servers = new ArrayList<>(addresses.size());
for (InetAddress inetAddr : addresses) {
servers.add(new EquivalentAddressGroup(new InetSocketAddress(inetAddr, port)));
}
return Collections.unmodifiableList(servers);
}
@Nullable
private ConfigOrError resolveServiceConfig() {
List txtRecords = Collections.emptyList();
ResourceResolver resourceResolver = getResourceResolver();
if (resourceResolver != null) {
try {
txtRecords = resourceResolver.resolveTxt(SERVICE_CONFIG_NAME_PREFIX + host);
} catch (Exception e) {
logger.log(Level.FINE, "ServiceConfig resolution failure", e);
}
}
if (!txtRecords.isEmpty()) {
ConfigOrError rawServiceConfig = parseServiceConfig(txtRecords, random, getLocalHostname());
if (rawServiceConfig != null) {
if (rawServiceConfig.getError() != null) {
return ConfigOrError.fromError(rawServiceConfig.getError());
}
@SuppressWarnings("unchecked")
Map verifiedRawServiceConfig = (Map) rawServiceConfig.getConfig();
return serviceConfigParser.parseServiceConfig(verifiedRawServiceConfig);
}
} else {
logger.log(Level.FINE, "No TXT records found for {0}", new Object[]{host});
}
return null;
}
@Nullable
private EquivalentAddressGroup detectProxy() throws IOException {
InetSocketAddress destination =
InetSocketAddress.createUnresolved(host, port);
ProxiedSocketAddress proxiedAddr = proxyDetector.proxyFor(destination);
if (proxiedAddr != null) {
return new EquivalentAddressGroup(proxiedAddr);
}
return null;
}
/**
* Main logic of name resolution.
*/
protected InternalResolutionResult doResolve(boolean forceTxt) {
InternalResolutionResult result = new InternalResolutionResult();
try {
result.addresses = resolveAddresses();
} catch (Exception e) {
if (!forceTxt) {
result.error =
Status.UNAVAILABLE.withDescription("Unable to resolve host " + host).withCause(e);
return result;
}
}
if (enableTxt) {
result.config = resolveServiceConfig();
}
return result;
}
private final class Resolve implements Runnable {
private final Listener2 savedListener;
Resolve(Listener2 savedListener) {
this.savedListener = checkNotNull(savedListener, "savedListener");
}
@Override
public void run() {
if (logger.isLoggable(Level.FINER)) {
logger.finer("Attempting DNS resolution of " + host);
}
InternalResolutionResult result = null;
try {
EquivalentAddressGroup proxiedAddr = detectProxy();
ResolutionResult.Builder resolutionResultBuilder = ResolutionResult.newBuilder();
if (proxiedAddr != null) {
if (logger.isLoggable(Level.FINER)) {
logger.finer("Using proxy address " + proxiedAddr);
}
resolutionResultBuilder.setAddresses(Collections.singletonList(proxiedAddr));
} else {
result = doResolve(false);
if (result.error != null) {
savedListener.onError(result.error);
return;
}
if (result.addresses != null) {
resolutionResultBuilder.setAddresses(result.addresses);
}
if (result.config != null) {
resolutionResultBuilder.setServiceConfig(result.config);
}
if (result.attributes != null) {
resolutionResultBuilder.setAttributes(result.attributes);
}
}
savedListener.onResult(resolutionResultBuilder.build());
} catch (IOException e) {
savedListener.onError(
Status.UNAVAILABLE.withDescription("Unable to resolve host " + host).withCause(e));
} finally {
final boolean succeed = result != null && result.error == null;
syncContext.execute(new Runnable() {
@Override
public void run() {
if (succeed) {
resolved = true;
if (cacheTtlNanos > 0) {
stopwatch.reset().start();
}
}
resolving = false;
}
});
}
}
}
@Nullable
static ConfigOrError parseServiceConfig(
List rawTxtRecords, Random random, String localHostname) {
List