Repository: eclipse-vertx/vert.x

Problem statement:
DNS search domains resolution
Implements DNS search domains for the AddressResolver



Hints:

Patch:
diff --git a/src/main/generated/io/vertx/core/dns/AddressResolverOptionsConverter.java b/src/main/generated/io/vertx/core/dns/AddressResolverOptionsConverter.java
--- a/src/main/generated/io/vertx/core/dns/AddressResolverOptionsConverter.java
+++ b/src/main/generated/io/vertx/core/dns/AddressResolverOptionsConverter.java
@@ -45,6 +45,9 @@ public static void fromJson(JsonObject json, AddressResolverOptions obj) {
     if (json.getValue("maxQueries") instanceof Number) {
       obj.setMaxQueries(((Number)json.getValue("maxQueries")).intValue());
     }
+    if (json.getValue("ndots") instanceof Number) {
+      obj.setNdots(((Number)json.getValue("ndots")).intValue());
+    }
     if (json.getValue("optResourceEnabled") instanceof Boolean) {
       obj.setOptResourceEnabled((Boolean)json.getValue("optResourceEnabled"));
     }
@@ -54,6 +57,12 @@ public static void fromJson(JsonObject json, AddressResolverOptions obj) {
     if (json.getValue("rdFlag") instanceof Boolean) {
       obj.setRdFlag((Boolean)json.getValue("rdFlag"));
     }
+    if (json.getValue("searchDomains") instanceof JsonArray) {
+      json.getJsonArray("searchDomains").forEach(item -> {
+        if (item instanceof String)
+          obj.addSearchDomain((String)item);
+      });
+    }
     if (json.getValue("servers") instanceof JsonArray) {
       json.getJsonArray("servers").forEach(item -> {
         if (item instanceof String)
@@ -73,9 +82,17 @@ public static void toJson(AddressResolverOptions obj, JsonObject json) {
       json.put("hostsValue", obj.getHostsValue().getBytes());
     }
     json.put("maxQueries", obj.getMaxQueries());
+    json.put("ndots", obj.getNdots());
     json.put("optResourceEnabled", obj.isOptResourceEnabled());
     json.put("queryTimeout", obj.getQueryTimeout());
     json.put("rdFlag", obj.getRdFlag());
+    if (obj.getSearchDomains() != null) {
+      json.put("searchDomains", new JsonArray(
+          obj.getSearchDomains().
+              stream().
+              map(item -> item).
+              collect(java.util.stream.Collectors.toList())));
+    }
     if (obj.getServers() != null) {
       json.put("servers", new JsonArray(
           obj.getServers().
diff --git a/src/main/java/examples/CoreExamples.java b/src/main/java/examples/CoreExamples.java
--- a/src/main/java/examples/CoreExamples.java
+++ b/src/main/java/examples/CoreExamples.java
@@ -328,6 +328,13 @@ public void configureHosts() {
     );
   }
 
+  public void configureSearchDomains() {
+    Vertx vertx = Vertx.vertx(new VertxOptions().
+        setAddressResolverOptions(
+            new AddressResolverOptions().addSearchDomain("foo.com").addSearchDomain("bar.com"))
+    );
+  }
+
   public void deployVerticleWithDifferentWorkerPool(Vertx vertx) {
     vertx.deployVerticle("the-verticle", new DeploymentOptions().setWorkerPoolName("the-specific-pool"));
   }
diff --git a/src/main/java/io/vertx/core/dns/AddressResolverOptions.java b/src/main/java/io/vertx/core/dns/AddressResolverOptions.java
--- a/src/main/java/io/vertx/core/dns/AddressResolverOptions.java
+++ b/src/main/java/io/vertx/core/dns/AddressResolverOptions.java
@@ -22,6 +22,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * Configuration options for Vert.x hostname resolver. The resolver uses the local <i>hosts</i> file and performs
@@ -48,6 +49,8 @@ public class AddressResolverOptions {
   public static final int DEFAULT_QUERY_TIMEOUT = 5000;
   public static final int DEFAULT_MAX_QUERIES = 3;
   public static final boolean DEFAULT_RD_FLAG = true;
+  public static final List<String> DEFAULT_SEACH_DOMAINS = null;
+  public static final int DEFAULT_NDOTS = 1;
 
   private String hostsPath;
   private Buffer hostsValue;
@@ -59,6 +62,8 @@ public class AddressResolverOptions {
   private long queryTimeout;
   private int maxQueries;
   private boolean rdFlag;
+  private List<String> searchDomains;
+  private int ndots;
 
   public AddressResolverOptions() {
     servers = DEFAULT_SERVERS;
@@ -69,6 +74,8 @@ public AddressResolverOptions() {
     queryTimeout = DEFAULT_QUERY_TIMEOUT;
     maxQueries = DEFAULT_MAX_QUERIES;
     rdFlag = DEFAULT_RD_FLAG;
+    searchDomains = null;
+    ndots = DEFAULT_NDOTS;
   }
 
   public AddressResolverOptions(AddressResolverOptions other) {
@@ -82,6 +89,8 @@ public AddressResolverOptions(AddressResolverOptions other) {
     this.queryTimeout = other.queryTimeout;
     this.maxQueries = other.maxQueries;
     this.rdFlag = other.rdFlag;
+    this.searchDomains = other.searchDomains != null ? new ArrayList<>(other.searchDomains) : null;
+    this.ndots = other.ndots;
   }
 
   public AddressResolverOptions(JsonObject json) {
@@ -200,6 +209,9 @@ public int getCacheMinTimeToLive() {
    * @return a reference to this, so the API can be used fluently
    */
   public AddressResolverOptions setCacheMinTimeToLive(int cacheMinTimeToLive) {
+    if (cacheMinTimeToLive < 0) {
+      throw new IllegalArgumentException("cacheMinTimeToLive must be >= 0");
+    }
     this.cacheMinTimeToLive = cacheMinTimeToLive;
     return this;
   }
@@ -219,6 +231,9 @@ public int getCacheMaxTimeToLive() {
    * @return a reference to this, so the API can be used fluently
    */
   public AddressResolverOptions setCacheMaxTimeToLive(int cacheMaxTimeToLive) {
+    if (cacheMaxTimeToLive < 0) {
+      throw new IllegalArgumentException("cacheMaxTimeToLive must be >= 0");
+    }
     this.cacheMaxTimeToLive = cacheMaxTimeToLive;
     return this;
   }
@@ -239,6 +254,9 @@ public int getCacheNegativeTimeToLive() {
    * @return a reference to this, so the API can be used fluently
    */
   public AddressResolverOptions setCacheNegativeTimeToLive(int cacheNegativeTimeToLive) {
+    if (cacheNegativeTimeToLive < 0) {
+      throw new IllegalArgumentException("cacheNegativeTimeToLive must be >= 0");
+    }
     this.cacheNegativeTimeToLive = cacheNegativeTimeToLive;
     return this;
   }
@@ -257,6 +275,9 @@ public long getQueryTimeout() {
    * @return a reference to this, so the API can be used fluently
    */
   public AddressResolverOptions setQueryTimeout(long queryTimeout) {
+    if (queryTimeout < 1) {
+      throw new IllegalArgumentException("queryTimeout must be > 0");
+    }
     this.queryTimeout = queryTimeout;
     return this;
   }
@@ -275,6 +296,9 @@ public int getMaxQueries() {
    * @return a reference to this, so the API can be used fluently
    */
   public AddressResolverOptions setMaxQueries(int maxQueries) {
+    if (maxQueries < 1) {
+      throw new IllegalArgumentException("maxQueries must be > 0");
+    }
     this.maxQueries = maxQueries;
     return this;
   }
@@ -297,6 +321,61 @@ public AddressResolverOptions setRdFlag(boolean rdFlag) {
     return this;
   }
 
+  /**
+   * @return the list of search domains
+   */
+  public List<String> getSearchDomains() {
+    return searchDomains;
+  }
+
+  /**
+   * Set the lists of DNS search domains.
+   * <p/>
+   * When the search domain list is null, the effective search domain list will be populated using
+   * the system DNS search domains.
+   *
+   * @param searchDomains the search domains
+   */
+  public AddressResolverOptions setSearchDomains(List<String> searchDomains) {
+    this.searchDomains = searchDomains;
+    return this;
+  }
+
+  /**
+   * Add a DNS search domain.
+   *
+   * @param searchDomain the search domain to add
+   * @return a reference to this, so the API can be used fluently
+   */
+  public AddressResolverOptions addSearchDomain(String searchDomain) {
+    if (searchDomains == null) {
+      searchDomains = new ArrayList<>();
+    }
+    searchDomains.add(searchDomain);
+    return this;
+  }
+
+  /**
+   * @return the ndots value
+   */
+  public int getNdots() {
+    return ndots;
+  }
+
+  /**
+   * Set the ndots value used when resolving using search domains, the default value is {@code 1}.
+   *
+   * @param ndots the new ndots value
+   * @return a reference to this, so the API can be used fluently
+   */
+  public AddressResolverOptions setNdots(int ndots) {
+    if (ndots < 1) {
+      throw new IllegalArgumentException("ndots must be > 0");
+    }
+    this.ndots = ndots;
+    return this;
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) return true;
@@ -309,6 +388,8 @@ public boolean equals(Object o) {
     if (queryTimeout != that.queryTimeout) return false;
     if (maxQueries != that.maxQueries) return false;
     if (rdFlag != that.rdFlag) return false;
+    if (!Objects.equals(searchDomains, that.searchDomains)) return false;
+    if (ndots != that.ndots) return false;
     return servers != null ? servers.equals(that.servers) : that.servers == null;
   }
 
@@ -321,6 +402,8 @@ public int hashCode() {
     result = 31 * result + cacheNegativeTimeToLive;
     result = 31 * result + Long.hashCode(queryTimeout);
     result = 31 * result + maxQueries;
+    result = 31 * result + (searchDomains != null ? searchDomains.hashCode() : 0);
+    result = 31 * result + ndots;
     result = 31 * result + Boolean.hashCode(rdFlag);
     return result;
   }
diff --git a/src/main/java/io/vertx/core/dns/impl/fix/DnsNameResolver.java b/src/main/java/io/vertx/core/dns/impl/fix/DnsNameResolver.java
--- a/src/main/java/io/vertx/core/dns/impl/fix/DnsNameResolver.java
+++ b/src/main/java/io/vertx/core/dns/impl/fix/DnsNameResolver.java
@@ -38,8 +38,10 @@
 import io.netty.resolver.dns.*;
 import io.netty.util.NetUtil;
 import io.netty.util.ReferenceCountUtil;
+import io.netty.util.concurrent.DefaultPromise;
 import io.netty.util.concurrent.FastThreadLocal;
 import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.FutureListener;
 import io.netty.util.concurrent.Promise;
 import io.netty.util.internal.UnstableApi;
 import io.netty.util.internal.logging.InternalLogger;
@@ -48,6 +50,7 @@
 import java.net.IDN;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -115,6 +118,8 @@ protected DnsServerAddressStream initialValue() throws Exception {
   private final int maxPayloadSize;
   private final boolean optResourceEnabled;
   private final HostsFileEntriesResolver hostsFileEntriesResolver;
+  private final List<String> searchDomains;
+  private final int ndots;
 
   /**
    * Creates a new DNS-based name resolver that communicates with the specified list of DNS servers.
@@ -134,6 +139,8 @@ protected DnsServerAddressStream initialValue() throws Exception {
    * @param maxPayloadSize the capacity of the datagram packet buffer
    * @param optResourceEnabled if automatic inclusion of a optional records is enabled
    * @param hostsFileEntriesResolver the {@link HostsFileEntriesResolver} used to check for local aliases
+   * @param searchDomains TODO
+   * @param ndots TODO
    */
   public DnsNameResolver(
       EventLoop eventLoop,
@@ -148,7 +155,9 @@ public DnsNameResolver(
       boolean traceEnabled,
       int maxPayloadSize,
       boolean optResourceEnabled,
-      HostsFileEntriesResolver hostsFileEntriesResolver) {
+      HostsFileEntriesResolver hostsFileEntriesResolver,
+      List<String> searchDomains,
+      int ndots) {
 
     super(eventLoop);
     checkNotNull(channelFactory, "channelFactory");
@@ -163,6 +172,8 @@ public DnsNameResolver(
     this.optResourceEnabled = optResourceEnabled;
     this.hostsFileEntriesResolver = checkNotNull(hostsFileEntriesResolver, "hostsFileEntriesResolver");
     this.resolveCache = resolveCache;
+    this.searchDomains = checkNotNull(searchDomains, "searchDomains");
+    this.ndots = checkPositive(ndots, "ndots");
 
     bindFuture = newChannel(channelFactory, localAddress);
     ch = (DatagramChannel) bindFuture.channel();
@@ -369,28 +380,87 @@ private static void setSuccess(Promise<InetAddress> promise, InetAddress result)
     }
   }
 
+  private boolean searchDomains(String hostname) {
+    if (searchDomains.isEmpty() || (hostname.length() > 0 && hostname.charAt(hostname.length() - 1) == '.')) {
+      return false;
+    } else {
+      int idx = hostname.length();
+      int dots = 0;
+      while (idx-- > 0) {
+        if (hostname.charAt(idx) == '.') {
+          dots++;
+        }
+        if (dots >= ndots) {
+          return false;
+        }
+      }
+      return true;
+    }
+  }
+
   private void doResolveUncached(String hostname,
                                  Promise<InetAddress> promise,
                                  DnsCache resolveCache) {
-    final DnsNameResolverContext<InetAddress> ctx =
-        new DnsNameResolverContext<InetAddress>(this, hostname, promise, resolveCache) {
-          @Override
-          protected boolean finishResolve(
-              Class<? extends InetAddress> addressType, List<DnsCacheEntry> resolvedEntries) {
+    doResolveUncached(hostname, promise, resolveCache, searchDomains.size() > 0 && !hostname.endsWith("."));
 
-            final int numEntries = resolvedEntries.size();
-            for (int i = 0; i < numEntries; i++) {
-              final InetAddress a = resolvedEntries.get(i).address();
-              if (addressType.isInstance(a)) {
-                setSuccess(promise(), a);
-                return true;
+  }
+  private void doResolveUncached(String hostname,
+                                 Promise<InetAddress> promise,
+                                 DnsCache resolveCache, boolean trySearchDomain) {
+    if (trySearchDomain) {
+      Promise<InetAddress> original = promise;
+      promise = new DefaultPromise<>(executor());
+      FutureListener<InetAddress> globalListener = future -> {
+        if (future.isSuccess()) {
+          original.setSuccess(future.getNow());
+        } else {
+          FutureListener<InetAddress> sdListener = new FutureListener<InetAddress>() {
+            int count;
+            @Override
+            public void operationComplete(Future<InetAddress> future) throws Exception {
+              if (future.isSuccess()) {
+                original.trySuccess(future.getNow());
+              } else {
+                if (count < searchDomains.size()) {
+                  String searchDomain = searchDomains.get(count++);
+                  Promise<InetAddress> p = new DefaultPromise<>(executor());
+                  doResolveUncached(hostname + "." + searchDomain, p, resolveCache, false);
+                  p.addListener(this);
+                } else {
+                  original.tryFailure(future.cause());
+                }
               }
             }
-            return false;
-          }
-        };
+          };
+          future.addListener(sdListener);
+        }
+      };
+      promise.addListener(globalListener);
+    }
+    if (searchDomains(hostname)) {
+      promise.tryFailure(new UnknownHostException(hostname));
+    } else {
+      final DnsNameResolverContext<InetAddress> ctx =
+          new DnsNameResolverContext<InetAddress>(this, hostname, promise, resolveCache) {
+            @Override
+            protected boolean finishResolve(
+                Class<? extends InetAddress> addressType, List<DnsCacheEntry> resolvedEntries) {
+
+              final int numEntries = resolvedEntries.size();
+              for (int i = 0; i < numEntries; i++) {
+                final InetAddress a = resolvedEntries.get(i).address();
+                if (addressType.isInstance(a)) {
+                  setSuccess(promise(), a);
+                  return true;
+                }
+              }
+              return false;
+            }
+          };
 
-    ctx.resolve();
+
+      ctx.resolve();
+    }
   }
 
   @Override
diff --git a/src/main/java/io/vertx/core/dns/impl/fix/DnsNameResolverBuilder.java b/src/main/java/io/vertx/core/dns/impl/fix/DnsNameResolverBuilder.java
--- a/src/main/java/io/vertx/core/dns/impl/fix/DnsNameResolverBuilder.java
+++ b/src/main/java/io/vertx/core/dns/impl/fix/DnsNameResolverBuilder.java
@@ -25,7 +25,10 @@
 import io.netty.util.internal.InternalThreadLocalMap;
 import io.netty.util.internal.UnstableApi;
 
+import java.lang.reflect.Method;
 import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
@@ -37,6 +40,25 @@
 @UnstableApi
 public final class DnsNameResolverBuilder {
 
+  private static final List<String> DEFAULT_SEACH_DOMAINS;
+
+  static {
+    ArrayList<String> searchDomains = new ArrayList<>();
+    try {
+      Class<?> configClass = Class.forName("sun.net.dns.ResolverConfiguration");
+      Method open = configClass.getMethod("open");
+      Method nameservers = configClass.getMethod("searchlist");
+      Object instance = open.invoke(null);
+
+      @SuppressWarnings("unchecked")
+      List<String> list = (List<String>) nameservers.invoke(instance);
+      searchDomains.addAll(list);
+    } catch (Exception ignore) {
+      // Failed to get the system name search domain list.
+    }
+    DEFAULT_SEACH_DOMAINS = Collections.unmodifiableList(searchDomains);
+  }
+
   private final EventLoop eventLoop;
   private ChannelFactory<? extends DatagramChannel> channelFactory;
   private InetSocketAddress localAddress = DnsNameResolver.ANY_LOCAL_ADDR;
@@ -53,6 +75,8 @@ public final class DnsNameResolverBuilder {
   private int maxPayloadSize = 4096;
   private boolean optResourceEnabled = true;
   private HostsFileEntriesResolver hostsFileEntriesResolver = HostsFileEntriesResolver.DEFAULT;
+  private List<String> searchDomains = DEFAULT_SEACH_DOMAINS;
+  private int ndots = 1;
 
   /**
    * Creates a new builder.
@@ -301,6 +325,24 @@ public DnsNameResolverBuilder hostsFileEntriesResolver(HostsFileEntriesResolver
     return this;
   }
 
+  public List<String> searchDomains() {
+    return searchDomains;
+  }
+
+  public DnsNameResolverBuilder searchDomains(List<String> searchDomains) {
+    this.searchDomains = searchDomains;
+    return this;
+  }
+
+  public int ndots() {
+    return ndots;
+  }
+
+  public DnsNameResolverBuilder ndots(int ndots) {
+    this.ndots = ndots;
+    return this;
+  }
+
   /**
    * Returns a new {@link DnsNameResolver} instance.
    *
@@ -328,6 +370,8 @@ public DnsNameResolver build() {
         traceEnabled,
         maxPayloadSize,
         optResourceEnabled,
-        hostsFileEntriesResolver);
+        hostsFileEntriesResolver,
+        searchDomains,
+        ndots);
   }
 }
diff --git a/src/main/java/io/vertx/core/dns/impl/fix/package-info.java b/src/main/java/io/vertx/core/dns/impl/fix/package-info.java
--- a/src/main/java/io/vertx/core/dns/impl/fix/package-info.java
+++ b/src/main/java/io/vertx/core/dns/impl/fix/package-info.java
@@ -1,5 +1,10 @@
 /**
  * fixed dns resolver to be removed when Netty with https://github.com/netty/netty/pull/5413
  * is released
+ *
+ * added dns search domain for doResolveUncached (https://github.com/netty/netty/issues/4665)
+ * not implemented:
+ * - doResolveAllUncached as we don't need it
+ * - /etc/resolv.conf parsing to determine "domain" and "options/ndots"
  */
 package io.vertx.core.dns.impl.fix;
\ No newline at end of file
diff --git a/src/main/java/io/vertx/core/impl/AddressResolver.java b/src/main/java/io/vertx/core/impl/AddressResolver.java
--- a/src/main/java/io/vertx/core/impl/AddressResolver.java
+++ b/src/main/java/io/vertx/core/impl/AddressResolver.java
@@ -18,6 +18,7 @@
 
 import io.netty.channel.socket.nio.NioDatagramChannel;
 import io.netty.resolver.AddressResolverGroup;
+import io.netty.resolver.DefaultAddressResolverGroup;
 import io.netty.resolver.HostsFileParser;
 import io.netty.resolver.InetSocketAddressResolver;
 import io.netty.resolver.dns.DnsServerAddresses;
@@ -49,89 +50,103 @@
  */
 public class AddressResolver {
 
+  private static final String DISABLE_DNS_RESOLVER_PROP_NAME = "vertx.disableDnsResolver";
+  private static final boolean DISABLE_DNS_RESOLVER = Boolean.getBoolean(DISABLE_DNS_RESOLVER_PROP_NAME);
+
   private final Vertx vertx;
   private final AddressResolverGroup<InetSocketAddress> resolverGroup;
 
   public AddressResolver(VertxImpl vertx, AddressResolverOptions options) {
-    DnsNameResolverBuilder builder = new DnsNameResolverBuilder(vertx.createEventLoopContext(null, null, new JsonObject(), Thread.currentThread().getContextClassLoader()).nettyEventLoop());
-    builder.channelType(NioDatagramChannel.class);
-    if (options != null) {
-      List<String> dnsServers = options.getServers();
-      if (dnsServers != null && dnsServers.size() > 0) {
-        List<InetSocketAddress> serverList = new ArrayList<>();
-        for (String dnsServer : dnsServers) {
-          int sep = dnsServer.indexOf(':');
-          String ipAddress;
-          int port;
-          if (sep != -1) {
-            ipAddress = dnsServer.substring(0, sep);
-            port = Integer.parseInt(dnsServer.substring(sep + 1));
-          } else {
-            ipAddress = dnsServer;
-            port = 53;
+
+    if (!DISABLE_DNS_RESOLVER) {
+      DnsNameResolverBuilder builder = new DnsNameResolverBuilder(vertx.createEventLoopContext(null, null, new JsonObject(), Thread.currentThread().getContextClassLoader()).nettyEventLoop());
+      builder.channelType(NioDatagramChannel.class);
+      if (options != null) {
+        List<String> dnsServers = options.getServers();
+        if (dnsServers != null && dnsServers.size() > 0) {
+          List<InetSocketAddress> serverList = new ArrayList<>();
+          for (String dnsServer : dnsServers) {
+            int sep = dnsServer.indexOf(':');
+            String ipAddress;
+            int port;
+            if (sep != -1) {
+              ipAddress = dnsServer.substring(0, sep);
+              port = Integer.parseInt(dnsServer.substring(sep + 1));
+            } else {
+              ipAddress = dnsServer;
+              port = 53;
+            }
+            try {
+              serverList.add(new InetSocketAddress(InetAddress.getByAddress(NetUtil.createByteArrayFromIpAddressString(ipAddress)), port));
+            } catch (UnknownHostException e) {
+              throw new VertxException(e);
+            }
+          }
+          DnsServerAddresses nameServerAddresses = DnsServerAddresses.sequential(serverList);
+          builder.nameServerAddresses(nameServerAddresses);
+        }
+
+
+        Map<String, InetAddress> entries;
+        if (options.getHostsPath() != null) {
+          File file = vertx.resolveFile(options.getHostsPath()).getAbsoluteFile();
+          try {
+            if (!file.exists() || !file.isFile()) {
+              throw new IOException();
+            }
+            entries = HostsFileParser.parse(file);
+          } catch (IOException e) {
+            throw new VertxException("Cannot read hosts file " + file.getAbsolutePath());
           }
+        } else if (options.getHostsValue() != null) {
           try {
-            serverList.add(new InetSocketAddress(InetAddress.getByAddress(NetUtil.createByteArrayFromIpAddressString(ipAddress)), port));
-          } catch (UnknownHostException e) {
-            throw new VertxException(e);
+            entries = HostsFileParser.parse(new StringReader(options.getHostsValue().toString()));
+          } catch (IOException e) {
+            throw new VertxException("Cannot read hosts config ", e);
           }
+        } else {
+          entries = HostsFileParser.parseSilently();
         }
-        DnsServerAddresses nameServerAddresses = DnsServerAddresses.sequential(serverList);
-        builder.nameServerAddresses(nameServerAddresses);
-      }
-
 
-      Map<String, InetAddress> entries;
-      if (options.getHostsPath() != null) {
-        File file = vertx.resolveFile(options.getHostsPath()).getAbsoluteFile();
+        // When localhost is missing we just resolve it and add it
         try {
-          if (!file.exists() || !file.isFile()) {
-            throw new IOException();
+          if (!entries.containsKey("localhost")) {
+            entries.put("localhost", InetAddress.getByName("localhost"));
           }
-          entries = HostsFileParser.parse(file);
-        } catch (IOException e) {
-          throw new VertxException("Cannot read hosts file " + file.getAbsolutePath());
+        } catch (UnknownHostException ignore) {
         }
-      } else if (options.getHostsValue() != null) {
-        try {
-          entries = HostsFileParser.parse(new StringReader(options.getHostsValue().toString()));
-        } catch (IOException e) {
-          throw new VertxException("Cannot read hosts config ", e);
+        builder.hostsFileEntriesResolver(entries::get);
+
+        builder.optResourceEnabled(options.isOptResourceEnabled());
+        builder.ttl(options.getCacheMinTimeToLive(), options.getCacheMaxTimeToLive());
+        builder.negativeTtl(options.getCacheNegativeTimeToLive());
+        builder.queryTimeoutMillis(options.getQueryTimeout());
+        builder.maxQueriesPerResolve(options.getMaxQueries());
+        builder.recursionDesired(options.getRdFlag());
+
+        if (options.getSearchDomains() != null) {
+          builder.searchDomains(options.getSearchDomains());
+          builder.ndots(options.getNdots());
         }
-      } else {
-        entries = HostsFileParser.parseSilently();
       }
 
-      // When localhost is missing we just resolve it and add it
-      try {
-        if (!entries.containsKey("localhost")) {
-          entries.put("localhost", InetAddress.getByName("localhost"));
+      resolverGroup = new AddressResolverGroup<InetSocketAddress>() {
+        @Override
+        protected io.netty.resolver.AddressResolver<InetSocketAddress> newResolver(EventExecutor executor) throws Exception {
+          DnsNameResolver resolver = builder.build();
+          return new InetSocketAddressResolver(executor, resolver) {
+            @Override
+            public void close() {
+              // Workaround for bug https://github.com/netty/netty/issues/2545
+              resolver.close();
+            }
+          };
         }
-      } catch (UnknownHostException ignore) {
-      }
-      builder.hostsFileEntriesResolver(entries::get);
-
-      builder.optResourceEnabled(options.isOptResourceEnabled());
-      builder.ttl(options.getCacheMinTimeToLive(), options.getCacheMaxTimeToLive());
-      builder.negativeTtl(options.getCacheNegativeTimeToLive());
-      builder.queryTimeoutMillis(options.getQueryTimeout());
-      builder.maxQueriesPerResolve(options.getMaxQueries());
-      builder.recursionDesired(options.getRdFlag());
+      };
+    } else {
+      resolverGroup = DefaultAddressResolverGroup.INSTANCE;
     }
 
-    this.resolverGroup = new AddressResolverGroup<InetSocketAddress>() {
-      @Override
-      protected io.netty.resolver.AddressResolver<InetSocketAddress> newResolver(EventExecutor executor) throws Exception {
-        DnsNameResolver resolver = builder.build();
-        return new InetSocketAddressResolver(executor, resolver) {
-          @Override
-          public void close() {
-            // Workaround for bug https://github.com/netty/netty/issues/2545
-            resolver.close();
-          }
-        };
-      }
-    };
     this.vertx = vertx;
   }
 
diff --git a/src/main/java/io/vertx/core/package-info.java b/src/main/java/io/vertx/core/package-info.java
--- a/src/main/java/io/vertx/core/package-info.java
+++ b/src/main/java/io/vertx/core/package-info.java
@@ -1238,6 +1238,20 @@
  * {@link examples.CoreExamples#configureHosts}
  * ----
  *
+ * By default the resolver will use the system DNS search domains from the environment. Alternatively an explicit search domain
+ * list can be provided:
+ *
+ * [source,$lang]
+ * ----
+ * {@link examples.CoreExamples#configureSearchDomains()}
+ * ----
+ *
+ * When a search domain list is used, the threshold for the number of dots is {@code 1}, it can be configured with
+ * {@link io.vertx.core.dns.AddressResolverOptions#setNdots(int)}.
+ *
+ * NOTE: sometimes it can be desirable to use the JVM built-in resolver, the JVM system property
+ * _-Dvertx.disableDnsResolver=true_ activates this behavior
+ *
  * == High Availability and Fail-Over
  *
  * Vert.x allows you to run your verticles with high availability (HA) support. In that case, when a vert.x


Test patch:
diff --git a/src/test/java/io/vertx/test/core/HostnameResolutionTest.java b/src/test/java/io/vertx/test/core/HostnameResolutionTest.java
--- a/src/test/java/io/vertx/test/core/HostnameResolutionTest.java
+++ b/src/test/java/io/vertx/test/core/HostnameResolutionTest.java
@@ -35,6 +35,7 @@
 import io.vertx.core.net.NetServer;
 import io.vertx.core.net.NetServerOptions;
 import io.vertx.test.fakedns.FakeDNSServer;
+import org.apache.directory.server.dns.DnsServer;
 import org.junit.Test;
 
 import java.io.File;
@@ -43,7 +44,9 @@
 import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -101,6 +104,10 @@ public void testAsyncResolveFail() throws Exception {
 
   @Test
   public void testNet() throws Exception {
+    testNet("vertx.io");
+  }
+
+  private void testNet(String hostname) throws Exception {
     NetClient client = vertx.createNetClient();
     NetServer server = vertx.createNetServer().connectHandler(so -> {
       so.handler(buff -> {
@@ -110,11 +117,11 @@ public void testNet() throws Exception {
     });
     try {
       CountDownLatch listenLatch = new CountDownLatch(1);
-      server.listen(1234, "vertx.io", onSuccess(s -> {
+      server.listen(1234, hostname, onSuccess(s -> {
         listenLatch.countDown();
       }));
       awaitLatch(listenLatch);
-      client.connect(1234, "vertx.io", onSuccess(so -> {
+      client.connect(1234, hostname, onSuccess(so -> {
         Buffer buffer = Buffer.buffer();
         so.handler(buffer::appendBuffer);
         so.closeHandler(v -> {
@@ -168,24 +175,72 @@ public void testOptions() {
     assertEquals(AddressResolverOptions.DEFAULT_QUERY_TIMEOUT, options.getQueryTimeout());
     assertEquals(AddressResolverOptions.DEFAULT_MAX_QUERIES, options.getMaxQueries());
     assertEquals(AddressResolverOptions.DEFAULT_RD_FLAG, options.getRdFlag());
+    assertEquals(AddressResolverOptions.DEFAULT_NDOTS, options.getNdots());
+    assertEquals(AddressResolverOptions.DEFAULT_SEACH_DOMAINS, options.getSearchDomains());
 
     boolean optResourceEnabled = TestUtils.randomBoolean();
     List<String> servers = Arrays.asList("1.2.3.4", "5.6.7.8");
     int minTTL = TestUtils.randomPositiveInt();
-    int maxTTL = minTTL + TestUtils.randomPositiveInt();
+    int maxTTL = minTTL + 1000;
     int negativeTTL = TestUtils.randomPositiveInt();
     int queryTimeout = 1 + TestUtils.randomPositiveInt();
     int maxQueries = 1 + TestUtils.randomPositiveInt();
     boolean rdFlag = TestUtils.randomBoolean();
+    int ndots = TestUtils.randomPositiveInt();
+    List<String> searchDomains = new ArrayList<>();
+    for (int i = 0;i < 2;i++) {
+      searchDomains.add(TestUtils.randomAlphaString(15));
+    }
 
     assertSame(options, options.setOptResourceEnabled(optResourceEnabled));
     assertSame(options, options.setServers(new ArrayList<>(servers)));
+    assertSame(options, options.setCacheMinTimeToLive(0));
     assertSame(options, options.setCacheMinTimeToLive(minTTL));
+    try {
+      options.setCacheMinTimeToLive(-1);
+      fail("Should throw exception");
+    } catch (IllegalArgumentException e) {
+      // OK
+    }
+    assertSame(options, options.setCacheMaxTimeToLive(0));
     assertSame(options, options.setCacheMaxTimeToLive(maxTTL));
+    try {
+      options.setCacheMaxTimeToLive(-1);
+      fail("Should throw exception");
+    } catch (IllegalArgumentException e) {
+      // OK
+    }
+    assertSame(options, options.setCacheNegativeTimeToLive(0));
     assertSame(options, options.setCacheNegativeTimeToLive(negativeTTL));
+    try {
+      options.setCacheNegativeTimeToLive(-1);
+      fail("Should throw exception");
+    } catch (IllegalArgumentException e) {
+      // OK
+    }
     assertSame(options, options.setQueryTimeout(queryTimeout));
+    try {
+      options.setQueryTimeout(0);
+      fail("Should throw exception");
+    } catch (IllegalArgumentException e) {
+      // OK
+    }
     assertSame(options, options.setMaxQueries(maxQueries));
+    try {
+      options.setMaxQueries(0);
+      fail("Should throw exception");
+    } catch (IllegalArgumentException e) {
+      // OK
+    }
     assertSame(options, options.setRdFlag(rdFlag));
+    assertSame(options, options.setSearchDomains(searchDomains));
+    assertSame(options, options.setNdots(ndots));
+    try {
+      options.setNdots(0);
+      fail("Should throw exception");
+    } catch (IllegalArgumentException e) {
+      // OK
+    }
 
     assertEquals(optResourceEnabled, options.isOptResourceEnabled());
     assertEquals(servers, options.getServers());
@@ -195,6 +250,8 @@ public void testOptions() {
     assertEquals(queryTimeout, options.getQueryTimeout());
     assertEquals(maxQueries, options.getMaxQueries());
     assertEquals(rdFlag, options.getRdFlag());
+    assertEquals(ndots, options.getNdots());
+    assertEquals(searchDomains, options.getSearchDomains());
 
     // Test copy and json copy
     AddressResolverOptions copy = new AddressResolverOptions(options);
@@ -208,6 +265,8 @@ public void testOptions() {
     options.setQueryTimeout(AddressResolverOptions.DEFAULT_QUERY_TIMEOUT);
     options.setMaxQueries(AddressResolverOptions.DEFAULT_MAX_QUERIES);
     options.setRdFlag(AddressResolverOptions.DEFAULT_RD_FLAG);
+    options.setNdots(AddressResolverOptions.DEFAULT_NDOTS);
+    options.setSearchDomains(AddressResolverOptions.DEFAULT_SEACH_DOMAINS);
 
     assertEquals(optResourceEnabled, copy.isOptResourceEnabled());
     assertEquals(servers, copy.getServers());
@@ -217,6 +276,8 @@ public void testOptions() {
     assertEquals(queryTimeout, copy.getQueryTimeout());
     assertEquals(maxQueries, copy.getMaxQueries());
     assertEquals(rdFlag, copy.getRdFlag());
+    assertEquals(ndots, copy.getNdots());
+    assertEquals(searchDomains, copy.getSearchDomains());
 
     assertEquals(optResourceEnabled, jsonCopy.isOptResourceEnabled());
     assertEquals(servers, jsonCopy.getServers());
@@ -226,6 +287,8 @@ public void testOptions() {
     assertEquals(queryTimeout, jsonCopy.getQueryTimeout());
     assertEquals(maxQueries, jsonCopy.getMaxQueries());
     assertEquals(rdFlag, jsonCopy.getRdFlag());
+    assertEquals(ndots, jsonCopy.getNdots());
+    assertEquals(searchDomains, jsonCopy.getSearchDomains());
   }
 
   @Test
@@ -239,6 +302,8 @@ public void testDefaultJsonOptions() {
     assertEquals(AddressResolverOptions.DEFAULT_QUERY_TIMEOUT, options.getQueryTimeout());
     assertEquals(AddressResolverOptions.DEFAULT_MAX_QUERIES, options.getMaxQueries());
     assertEquals(AddressResolverOptions.DEFAULT_RD_FLAG, options.getRdFlag());
+    assertEquals(AddressResolverOptions.DEFAULT_SEACH_DOMAINS, options.getSearchDomains());
+    assertEquals(AddressResolverOptions.DEFAULT_NDOTS, options.getNdots());
   }
 
   @Test
@@ -320,6 +385,7 @@ public void testResolveMissingLocalhost() throws Exception {
     InetAddress localhost = InetAddress.getByName("localhost");
 
     // Set a dns resolver that won't resolve localhost
+    dnsServer.stop();
     dnsServer = FakeDNSServer.testResolveASameServer("127.0.0.1");
     dnsServer.start();
     dnsServerAddress = (InetSocketAddress) dnsServer.getTransports()[0].getAcceptor().getLocalAddress();
@@ -372,4 +438,188 @@ public void testResolveMissingLocalhost() throws Exception {
     });
     test3.get(10, TimeUnit.SECONDS);
   }
+
+  @Test
+  public void testSearchDomain() throws Exception {
+
+    Map<String, String> records = new HashMap<>();
+    records.put("host1.foo.com", "127.0.0.1");
+    records.put("host1", "127.0.0.2");
+    records.put("host3", "127.0.0.3");
+    records.put("host4.sub.foo.com", "127.0.0.4");
+    records.put("host5.sub.foo.com", "127.0.0.5");
+    records.put("host5.sub", "127.0.0.6");
+
+    dnsServer.stop();
+    dnsServer = FakeDNSServer.testResolveA(records);
+    dnsServer.start();
+    VertxInternal vertx = (VertxInternal) vertx(new VertxOptions().setAddressResolverOptions(
+        new AddressResolverOptions().
+            addServer(dnsServerAddress.getAddress().getHostAddress() + ":" + dnsServerAddress.getPort()).
+            setOptResourceEnabled(false).
+            addSearchDomain("foo.com")
+    ));
+
+    // host1 resolves host1.foo.com with foo.com search domain
+    CountDownLatch latch1 = new CountDownLatch(1);
+    vertx.resolveAddress("host1", onSuccess(resolved -> {
+      assertEquals("127.0.0.1", resolved.getHostAddress());
+      latch1.countDown();
+    }));
+    awaitLatch(latch1);
+
+    // "host1." absolute query
+    CountDownLatch latch2 = new CountDownLatch(1);
+    vertx.resolveAddress("host1.", onSuccess(resolved -> {
+      assertEquals("127.0.0.2", resolved.getHostAddress());
+      latch2.countDown();
+    }));
+    awaitLatch(latch2);
+
+    // "host2" not resolved
+    CountDownLatch latch3 = new CountDownLatch(1);
+    vertx.resolveAddress("host2", onFailure(cause -> {
+      assertTrue(cause instanceof UnknownHostException);
+      latch3.countDown();
+    }));
+    awaitLatch(latch3);
+
+    // "host3" does not contain a dot or is not absolute
+    CountDownLatch latch4 = new CountDownLatch(1);
+    vertx.resolveAddress("host3", onFailure(cause -> {
+      assertTrue(cause instanceof UnknownHostException);
+      latch4.countDown();
+    }));
+    awaitLatch(latch4);
+
+    // "host3." does not contain a dot but is absolute
+    CountDownLatch latch5 = new CountDownLatch(1);
+    vertx.resolveAddress("host3.", onSuccess(resolved -> {
+      assertEquals("127.0.0.3", resolved.getHostAddress());
+      latch5.countDown();
+    }));
+    awaitLatch(latch5);
+
+    // "host4.sub" contains a dot but not resolved then resolved to "host4.sub.foo.com" with "foo.com" search domain
+    CountDownLatch latch6 = new CountDownLatch(1);
+    vertx.resolveAddress("host4.sub", onSuccess(resolved -> {
+      assertEquals("127.0.0.4", resolved.getHostAddress());
+      latch6.countDown();
+    }));
+    awaitLatch(latch6);
+
+    // "host5.sub" contains a dot and is resolved
+    CountDownLatch latch7 = new CountDownLatch(1);
+    vertx.resolveAddress("host5.sub", onSuccess(resolved -> {
+      assertEquals("127.0.0.6", resolved.getHostAddress());
+      latch7.countDown();
+    }));
+    awaitLatch(latch7);
+  }
+
+  @Test
+  public void testMultipleSearchDomain() throws Exception {
+
+    Map<String, String> records = new HashMap<>();
+    records.put("host1.foo.com", "127.0.0.1");
+    records.put("host2.bar.com", "127.0.0.2");
+    records.put("host3.bar.com", "127.0.0.3");
+    records.put("host3.foo.com", "127.0.0.4");
+
+    dnsServer.stop();
+    dnsServer = FakeDNSServer.testResolveA(records);
+    dnsServer.start();
+    VertxInternal vertx = (VertxInternal) vertx(new VertxOptions().setAddressResolverOptions(
+        new AddressResolverOptions().
+            addServer(dnsServerAddress.getAddress().getHostAddress() + ":" + dnsServerAddress.getPort()).
+            setOptResourceEnabled(false).
+            addSearchDomain("foo.com").
+            addSearchDomain("bar.com")
+    ));
+
+    // "host1" resolves via the "foo.com" search path
+    CountDownLatch latch1 = new CountDownLatch(1);
+    vertx.resolveAddress("host1", onSuccess(resolved -> {
+      assertEquals("127.0.0.1", resolved.getHostAddress());
+      latch1.countDown();
+    }));
+    awaitLatch(latch1);
+
+    // "host3" resolves via the "bar.com" search path
+    CountDownLatch latch2 = new CountDownLatch(1);
+    vertx.resolveAddress("host2", onSuccess(resolved -> {
+      assertEquals("127.0.0.2", resolved.getHostAddress());
+      latch2.countDown();
+    }));
+    awaitLatch(latch2);
+
+    // "host3" resolves via the the "foo.com" search path as it is the first one
+    CountDownLatch latch3 = new CountDownLatch(1);
+    vertx.resolveAddress("host3", onSuccess(resolved -> {
+      assertEquals("127.0.0.4", resolved.getHostAddress());
+      latch3.countDown();
+    }));
+    awaitLatch(latch3);
+
+    // "host4" does not resolve
+    vertx.resolveAddress("host4", onFailure(cause -> {
+      assertTrue(cause instanceof UnknownHostException);
+      testComplete();
+    }));
+
+    await();
+  }
+
+  @Test
+  public void testSearchDomainWithNdots2() throws Exception {
+
+    Map<String, String> records = new HashMap<>();
+    records.put("host1.sub.foo.com", "127.0.0.1");
+    records.put("host2.sub.foo.com", "127.0.0.2");
+    records.put("host2.sub", "127.0.0.3");
+
+    dnsServer.stop();
+    dnsServer = FakeDNSServer.testResolveA(records);
+    dnsServer.start();
+    VertxInternal vertx = (VertxInternal) vertx(new VertxOptions().setAddressResolverOptions(
+        new AddressResolverOptions().
+            addServer(dnsServerAddress.getAddress().getHostAddress() + ":" + dnsServerAddress.getPort()).
+            setOptResourceEnabled(false).
+            addSearchDomain("foo.com").
+            addSearchDomain("bar.com").
+            setNdots(2)
+    ));
+
+    CountDownLatch latch1 = new CountDownLatch(1);
+    vertx.resolveAddress("host1.sub", onSuccess(resolved -> {
+      assertEquals("127.0.0.1", resolved.getHostAddress());
+      latch1.countDown();
+    }));
+    awaitLatch(latch1);
+
+    // "host2.sub" is resolved with the foo.com search domain as ndots = 2
+    CountDownLatch latch2 = new CountDownLatch(1);
+    vertx.resolveAddress("host2.sub", onSuccess(resolved -> {
+      assertEquals("127.0.0.2", resolved.getHostAddress());
+      latch2.countDown();
+    }));
+    awaitLatch(latch2);
+
+  }
+
+  @Test
+  public void testNetSearchDomain() throws Exception {
+    Map<String, String> records = new HashMap<>();
+    records.put("host1.foo.com", "127.0.0.1");
+    dnsServer.stop();
+    dnsServer = FakeDNSServer.testResolveA(records);
+    dnsServer.start();
+    vertx = vertx(new VertxOptions().setAddressResolverOptions(
+        new AddressResolverOptions().
+            addServer(dnsServerAddress.getAddress().getHostAddress() + ":" + dnsServerAddress.getPort()).
+            setOptResourceEnabled(false).
+            addSearchDomain("foo.com")
+    ));
+    testNet("host1");
+  }
 }
diff --git a/src/test/java/io/vertx/test/fakedns/FakeDNSServer.java b/src/test/java/io/vertx/test/fakedns/FakeDNSServer.java
--- a/src/test/java/io/vertx/test/fakedns/FakeDNSServer.java
+++ b/src/test/java/io/vertx/test/fakedns/FakeDNSServer.java
@@ -42,8 +42,11 @@
 import org.apache.mina.transport.socket.DatagramSessionConfig;
 
 import java.io.IOException;
+import java.util.Collections;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * @author <a href="mailto:nmaurer@redhat.com">Norman Maurer</a>
@@ -61,19 +64,22 @@ private FakeDNSServer(RecordStore store) {
   }
 
   public static FakeDNSServer testResolveA(final String ipAddress) {
+    return testResolveA(Collections.singletonMap("dns.vertx.io", ipAddress));
+  }
+
+  public static FakeDNSServer testResolveA(Map<String, String> entries) {
     return new FakeDNSServer(new RecordStore() {
       @Override
       public Set<ResourceRecord> getRecords(QuestionRecord questionRecord) throws org.apache.directory.server.dns.DnsException {
-        Set<ResourceRecord> set = new HashSet<>();
-
-        ResourceRecordModifier rm = new ResourceRecordModifier();
-        rm.setDnsClass(RecordClass.IN);
-        rm.setDnsName("dns.vertx.io");
-        rm.setDnsTtl(100);
-        rm.setDnsType(RecordType.A);
-        rm.put(DnsAttribute.IP_ADDRESS, ipAddress);
-        set.add(rm.getEntry());
-        return set;
+        return entries.entrySet().stream().map(entry -> {
+          ResourceRecordModifier rm = new ResourceRecordModifier();
+          rm.setDnsClass(RecordClass.IN);
+          rm.setDnsName(entry.getKey());
+          rm.setDnsTtl(100);
+          rm.setDnsType(RecordType.A);
+          rm.put(DnsAttribute.IP_ADDRESS, entry.getValue());
+          return rm.getEntry();
+        }).collect(Collectors.toSet());
       }
     });
   }