tech.ydb.shaded.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