Repository: netty/netty
Issue: Allow ndots=0 in DnsNameResolver and search domains
Labels: defect

Motivation:

The `ndots = 0` is a valid value for ndots, it means that when using a non dotted name, the resolution should first try using a search and if it fails then use subdomains. Currently it is not allowed.

Docker compose uses this when wiring up containers as names have usually no dots inside.. An example of docker compose `resolv.conf`:

```
search local
nameserver 127.0.0.11
options ndots:0
```

Modification:

Modify `DnsNameResolver` to accept `ndots = 0` and handle the case in the resolution procedure. In this case a direct search is done and then a fallback on the search path is performed.

Result:

The `ndots = 0` case is implemented.


Issue comments:
Fixed by https://github.com/netty/netty/pull/5571


Pull request review comments:
Shouldnt this stay outside of the rose block?

I don't understand what you mean.

Outside of the else block.... Thanks to autocorrect

no, it's the same behavior than in the foreach loop that returns immediately after calling internalResolve.

got it.

call resolver.close() at the end of the test ?


Patch that resolved the issue:
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java
index 6f2e4192d76..b6148ddc89b 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java
@@ -191,7 +191,7 @@ public DnsNameResolver(
         this.hostsFileEntriesResolver = checkNotNull(hostsFileEntriesResolver, "hostsFileEntriesResolver");
         this.resolveCache = resolveCache;
         this.searchDomains = checkNotNull(searchDomains, "searchDomains").clone();
-        this.ndots = checkPositive(ndots, "ndots");
+        this.ndots = checkPositiveOrZero(ndots, "ndots");
 
         Bootstrap b = new Bootstrap();
         b.group(executor());
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java
index ffba72547c0..477ccab99e4 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java
@@ -321,6 +321,7 @@ public DnsNameResolverBuilder searchDomains(Iterable<String> searchDomains) {
 
   /**
    * Set the number of dots which must appear in a name before an initial absolute query is made.
+   * The default value is {@code 1}.
    *
    * @param ndots the ndots value
    * @return {@code this}
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java
index fabb9da7e8e..08ba335a6ad 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java
@@ -123,14 +123,18 @@ public void operationComplete(Future<T> future) throws Exception {
                     }
                 }
             });
-            int dots = 0;
-            for (int idx = hostname.length() - 1; idx >= 0; idx--) {
-                if (hostname.charAt(idx) == '.' && ++dots >= parent.ndots()) {
-                    internalResolve(promise);
-                    return;
+            if (parent.ndots() == 0) {
+                internalResolve(promise);
+            } else {
+                int dots = 0;
+                for (int idx = hostname.length() - 1; idx >= 0; idx--) {
+                    if (hostname.charAt(idx) == '.' && ++dots >= parent.ndots()) {
+                        internalResolve(promise);
+                        return;
+                    }
                 }
+                promise.tryFailure(new UnknownHostException(hostname));
             }
-            promise.tryFailure(new UnknownHostException(hostname));
         }
     }
 


Test patch:
diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/SearchDomainTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/SearchDomainTest.java
index c3e677b4c9a..da89173848c 100644
--- a/resolver-dns/src/test/java/io/netty/resolver/dns/SearchDomainTest.java
+++ b/resolver-dns/src/test/java/io/netty/resolver/dns/SearchDomainTest.java
@@ -48,6 +48,7 @@ private DnsNameResolverBuilder newResolver() {
 
     private TestDnsServer dnsServer;
     private EventLoopGroup group;
+    private DnsNameResolver resolver;
 
     @Before
     public void before() {
@@ -60,6 +61,9 @@ public void destroy() {
             dnsServer.stop();
             dnsServer = null;
         }
+        if (resolver != null) {
+            resolver.close();
+        }
         group.shutdownGracefully();
     }
 
@@ -77,7 +81,7 @@ public void testResolve() throws Exception {
         dnsServer = new TestDnsServer(store);
         dnsServer.start();
 
-        DnsNameResolver resolver = newResolver().searchDomains(Collections.singletonList("foo.com")).build();
+        resolver = newResolver().searchDomains(Collections.singletonList("foo.com")).build();
 
         String a = "host1.foo.com";
         String resolved = assertResolve(resolver, a);
@@ -124,7 +128,7 @@ public void testResolveAll() throws Exception {
         dnsServer = new TestDnsServer(store);
         dnsServer.start();
 
-        DnsNameResolver resolver = newResolver().searchDomains(Collections.singletonList("foo.com")).build();
+        resolver = newResolver().searchDomains(Collections.singletonList("foo.com")).build();
 
         String a = "host1.foo.com";
         List<String> resolved = assertResolveAll(resolver, a);
@@ -169,7 +173,7 @@ public void testMultipleSearchDomain() throws Exception {
         dnsServer = new TestDnsServer(store);
         dnsServer.start();
 
-        DnsNameResolver resolver = newResolver().searchDomains(Arrays.asList("foo.com", "bar.com")).build();
+        resolver = newResolver().searchDomains(Arrays.asList("foo.com", "bar.com")).build();
 
         // "host1" resolves via the "foo.com" search path
         String resolved = assertResolve(resolver, "host1");
@@ -198,7 +202,7 @@ public void testSearchDomainWithNdots2() throws Exception {
         dnsServer = new TestDnsServer(store);
         dnsServer.start();
 
-        DnsNameResolver resolver = newResolver().searchDomains(Collections.singleton("foo.com")).ndots(2).build();
+        resolver = newResolver().searchDomains(Collections.singleton("foo.com")).ndots(2).build();
 
         String resolved = assertResolve(resolver, "host1.sub");
         assertEquals(store.getAddress("host1.sub.foo.com"), resolved);
@@ -208,6 +212,32 @@ public void testSearchDomainWithNdots2() throws Exception {
         assertEquals(store.getAddress("host2.sub.foo.com"), resolved);
     }
 
+    @Test
+    public void testSearchDomainWithNdots0() throws Exception {
+        Set<String> domains = new HashSet<String>();
+        domains.add("host1");
+        domains.add("host1.foo.com");
+        domains.add("host2.foo.com");
+
+        TestDnsServer.MapRecordStoreA store = new TestDnsServer.MapRecordStoreA(domains);
+        dnsServer = new TestDnsServer(store);
+        dnsServer.start();
+
+        resolver = newResolver().searchDomains(Collections.singleton("foo.com")).ndots(0).build();
+
+        // "host1" resolves directly as ndots = 0
+        String resolved = assertResolve(resolver, "host1");
+        assertEquals(store.getAddress("host1"), resolved);
+
+        // "host1.foo.com" resolves to host1.foo
+        resolved = assertResolve(resolver, "host1.foo.com");
+        assertEquals(store.getAddress("host1.foo.com"), resolved);
+
+        // "host2" resolves to host2.foo.com with the foo.com search domain
+        resolved = assertResolve(resolver, "host2");
+        assertEquals(store.getAddress("host2.foo.com"), resolved);
+    }
+
     private void assertNotResolve(DnsNameResolver resolver, String inetHost) throws InterruptedException {
         Future<InetAddress> fut = resolver.resolve(inetHost);
         assertTrue(fut.await(10, TimeUnit.SECONDS));