Repository: mageddo/dns-proxy-server (java)

Issue:
Permanent error on AAAA request
### What is Happening / What is expected
DPS logs fault, associated to AAAA request on container name
Log entry:
```
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]: 08:28:44.878 [Thread-41      ] WAR c.m.d.server.dns.RequestHandlerDefault            l=93   m=solve0                          status=solverFailed, currentSolverTime=8, totalTime=8, solver=SolverDocker, query=query=AAAA:db.docker, eClass=NullPointerException, msg=null
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]: java.lang.NullPointerException: null
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at java.base@19.0.2/java.util.Objects.requireNonNull(Objects.java:233)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at java.base@19.0.2/java.util.Optional.of(Optional.java:113)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at java.base@19.0.2/java.util.stream.FindOps$FindSink$OfRef.get(FindOps.java:194)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at java.base@19.0.2/java.util.stream.FindOps$FindSink$OfRef.get(FindOps.java:191)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at java.base@19.0.2/java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:150)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at java.base@19.0.2/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at java.base@19.0.2/java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:647)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at com.mageddo.dnsproxyserver.docker.ContainerSolvingService.findBestMatch(ContainerSolvingService.java:50)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at com.mageddo.dnsproxyserver.server.dns.solver.SolverDocker.lambda$handle$0(SolverDocker.java:40)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at com.mageddo.dnsproxyserver.server.dns.solver.HostnameMatcher.match(HostnameMatcher.java:22)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at com.mageddo.dnsproxyserver.server.dns.solver.SolverDocker.handle(SolverDocker.java:39)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at com.mageddo.dnsproxyserver.server.dns.RequestHandlerDefault.solve0(RequestHandlerDefault.java:75)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at com.mageddo.dnsproxyserver.server.dns.solver.SolverCache.lambda$handleRes$0(SolverCache.java:38)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at com.mageddo.commons.caching.LruTTLCache.lambda$computeIfAbsentWithTTL$1(LruTTLCache.java:94)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at java.base@19.0.2/java.util.concurrent.ConcurrentHashMap.compute(ConcurrentHashMap.java:1916)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at com.mageddo.commons.caching.LruTTLCache.lambda$computeIfAbsentWithTTL$2(LruTTLCache.java:88)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at java.base@19.0.2/java.util.concurrent.ConcurrentHashMap.compute(ConcurrentHashMap.java:1916)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at com.mageddo.commons.caching.LruTTLCache.computeIfAbsentWithTTL(LruTTLCache.java:86)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at com.mageddo.dnsproxyserver.server.dns.solver.SolverCache.handleRes(SolverCache.java:36)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at com.mageddo.dnsproxyserver.server.dns.solver.SolverCache.handle(SolverCache.java:31)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at com.mageddo.dnsproxyserver.server.dns.RequestHandlerDefault.solve(RequestHandlerDefault.java:51)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at com.mageddo.dnsproxyserver.server.dns.RequestHandlerDefault.handle(RequestHandlerDefault.java:42)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at com.mageddo.dnsproxyserver.server.dns.UDPServer.handle(UDPServer.java:54)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at com.mageddo.dnsproxyserver.server.dns.UDPServer.lambda$start0$0(UDPServer.java:42)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at java.base@19.0.2/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:577)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at java.base@19.0.2/java.util.concurrent.FutureTask.run(FutureTask.java:317)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at java.base@19.0.2/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at java.base@19.0.2/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at java.base@19.0.2/java.lang.Thread.run(Thread.java:1589)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at org.graalvm.nativeimage.builder/com.oracle.svm.core.thread.PlatformThreads.threadStartRoutine(PlatformThreads.java:775)
Dec 07 11:28:44 pavlyuts.ru dns-proxy-server[448]:         at org.graalvm.nativeimage.builder/com.oracle.svm.core.posix.thread.PosixPlatformThreads.pthreadStartRoutine(PosixPlatformThreads.java:203)
```
To reproduce:
1. Setup Docker
2.  Setup DPS standaone mode and configure it
3. Create container with docker default network conection and name 'test'
4. `dig @127.0.0.1 test.docker AAAA`
5. Check DPS logs

DPS config file
```
{
  "version": 2,
  "remoteDnsServers": [ ],
  "envs": [
    {
      "name": "",
      "hostnames": [ ]
    }
  ],
  "activeEnv": "",
  "webServerPort": 5380,
  "dnsServerPort": 53,
  "logLevel": "INFO",
  "logFile": "console",
  "registerContainerNames": true,
  "domain": "docker",
  "dpsNetwork": false,
  "dpsNetworkAutoConnect": false,
  "defaultDns": false,
  "hostMachineHostname" : "host.docker",
  "serverProtocol": "UDP_TCP",
  "dockerHost": null,
  "resolvConfOverrideNameServers": false,
  "noRemoteServers": true,
  "noEntriesResponseCode": 3
}

```
### Specs
* OS: Debian 11, Oracle Linux 8 
* Docker Version: 24.0.7
* DPS Version: 3.15.13-snapshot


Pull request description:
DockerSolver: Leading with no IPV6 to answer 
To fix #428 

Patch:
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 43a76fb3..a0400cc3 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -1,3 +1,6 @@
+## 3.16.3
+* Fixed NPE when no IPV6 is found to answer to the client #428
+
 ## 3.16.2
 * Fixing Native Image Reflection JSON Parsing #437
 
diff --git a/gradle.properties b/gradle.properties
index fcca0f4f..4f738183 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,1 +1,1 @@
-version=3.16.2-snapshot
+version=3.16.3-snapshot
diff --git a/src/main/java/com/mageddo/dnsproxyserver/docker/ContainerSolvingService.java b/src/main/java/com/mageddo/dnsproxyserver/docker/ContainerSolvingService.java
index 789acd87..0fc04f88 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/docker/ContainerSolvingService.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/docker/ContainerSolvingService.java
@@ -14,7 +14,6 @@ import javax.inject.Inject;
 import javax.inject.Singleton;
 import java.util.Collection;
 import java.util.LinkedHashSet;
-import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
@@ -40,16 +39,18 @@ public class ContainerSolvingService {
 
   private final DockerDAO dockerDAO;
   private final DockerNetworkDAO networkDAO;
+  private final MatchingContainerService matchingContainerService;
 
   public Entry findBestMatch(HostnameQuery host) {
     final var stopWatch = StopWatch.createStarted();
-    final var matchedContainers = this.findMatchingContainers(host);
+    final var matchedContainers = this.matchingContainerService.findMatchingContainers(host);
     final var foundIp = matchedContainers
       .stream()
       .map(it -> this.findBestIpMatch(it, host.getVersion()))
+      .filter(Objects::nonNull)
       .findFirst()
       .orElse(null);
-    final var hostnameMatched = !matchedContainers.isEmpty();
+    final var hostnameMatched = !matchedContainers.isEmpty() && foundIp != null;
     log.trace(
       "status=findDone, host={}, found={}, hostnameMatched={}, time={}",
       host, foundIp, hostnameMatched, stopWatch.getTime()
@@ -159,11 +160,4 @@ public class ContainerSolvingService {
       .collect(Collectors.toCollection(LinkedHashSet::new));
   }
 
-  List<InspectContainerResponse> findMatchingContainers(HostnameQuery host) {
-    return this.dockerDAO.findActiveContainers()
-      .stream()
-      .map(it -> this.dockerDAO.inspect(it.getId()))
-      .filter(ContainerHostnameMatcher.buildPredicate(host))
-      .toList();
-  }
 }
diff --git a/src/main/java/com/mageddo/dnsproxyserver/docker/MatchingContainerService.java b/src/main/java/com/mageddo/dnsproxyserver/docker/MatchingContainerService.java
new file mode 100644
index 00000000..d9d6fd69
--- /dev/null
+++ b/src/main/java/com/mageddo/dnsproxyserver/docker/MatchingContainerService.java
@@ -0,0 +1,26 @@
+package com.mageddo.dnsproxyserver.docker;
+
+import com.github.dockerjava.api.command.InspectContainerResponse;
+import com.mageddo.dnsproxyserver.server.dns.solver.HostnameQuery;
+import lombok.RequiredArgsConstructor;
+
+import javax.enterprise.inject.Default;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.util.List;
+
+@Default
+@Singleton
+@RequiredArgsConstructor(onConstructor = @__({@Inject}))
+public class MatchingContainerService {
+
+  private final DockerDAO dockerDAO;
+
+  List<InspectContainerResponse> findMatchingContainers(HostnameQuery host) {
+    return this.dockerDAO.findActiveContainers()
+      .stream()
+      .map(it -> this.dockerDAO.inspect(it.getId()))
+      .filter(ContainerHostnameMatcher.buildPredicate(host))
+      .toList();
+  }
+}


Test patch:
diff --git a/src/test/java/com/mageddo/dnsproxyserver/docker/ContainerSolvingServiceCompTest.java b/src/test/java/com/mageddo/dnsproxyserver/docker/ContainerSolvingServiceCompTest.java
index f1951789..3a63e188 100644
--- a/src/test/java/com/mageddo/dnsproxyserver/docker/ContainerSolvingServiceCompTest.java
+++ b/src/test/java/com/mageddo/dnsproxyserver/docker/ContainerSolvingServiceCompTest.java
@@ -1,23 +1,21 @@
 package com.mageddo.dnsproxyserver.docker;
 
 import com.mageddo.dnsproxyserver.di.Context;
-import testing.templates.IpTemplates;
-import testing.templates.docker.InspectContainerResponseTemplates;
-import testing.templates.docker.NetworkTemplates;
+import com.mageddo.dnsproxyserver.server.dns.solver.HostnameQuery;
 import com.mageddo.net.IP;
 import dagger.sheath.InjectMock;
 import dagger.sheath.junit.DaggerTest;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
+import testing.templates.IpTemplates;
+import testing.templates.docker.InspectContainerResponseTemplates;
+import testing.templates.docker.NetworkTemplates;
 
 import javax.inject.Inject;
+import java.util.List;
 
-import static testing.templates.docker.InspectContainerResponseTemplates.ngixWithDefaultBridgeNetworkOnly;
-import static testing.templates.docker.InspectContainerResponseTemplates.ngixWithIpv4DefaultBridgeAndIpv6CustomBridgeNetwork;
-import static testing.templates.docker.InspectContainerResponseTemplates.ngixWithIpv6CustomBridgeNetwork;
-import static testing.templates.docker.InspectContainerResponseTemplates.ngixWithIpv6DefaultBridgeNetworkOnly;
-import static testing.templates.docker.InspectContainerResponseTemplates.ngixWithIpv6DefaultIp;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.mockito.ArgumentMatchers.anyString;
@@ -25,6 +23,11 @@ import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static testing.templates.docker.InspectContainerResponseTemplates.ngixWithDefaultBridgeNetworkOnly;
+import static testing.templates.docker.InspectContainerResponseTemplates.ngixWithIpv4DefaultBridgeAndIpv6CustomBridgeNetwork;
+import static testing.templates.docker.InspectContainerResponseTemplates.ngixWithIpv6CustomBridgeNetwork;
+import static testing.templates.docker.InspectContainerResponseTemplates.ngixWithIpv6DefaultBridgeNetworkOnly;
+import static testing.templates.docker.InspectContainerResponseTemplates.ngixWithIpv6DefaultIp;
 
 @DaggerTest(component = Context.class)
 class ContainerSolvingServiceCompTest {
@@ -35,6 +38,9 @@ class ContainerSolvingServiceCompTest {
   @InjectMock
   DockerNetworkDAO dockerNetworkDAO;
 
+  @InjectMock
+  MatchingContainerService matchingContainerService;
+
   @Inject
   ContainerSolvingService containerSolvingService;
 
@@ -182,5 +188,25 @@ class ContainerSolvingServiceCompTest {
 
   }
 
+  @Test
+  void mustLeadWithNoneIPV6ReturnedFromDockerSolver() {
+    // arrange
+    final var hostnameQuery = HostnameQuery.of("nginx-2.dev", IP.Version.IPV6);
+    final var inspect = ngixWithDefaultBridgeNetworkOnly();
+
+    doReturn(List.of(inspect))
+      .when(this.matchingContainerService)
+      .findMatchingContainers(eq(hostnameQuery));
+
+    // act
+    final var ip = this.containerSolvingService.findBestMatch(hostnameQuery);
+
+    // assert
+    assertNotNull(ip);
+    assertFalse(ip.isHostnameMatched());
+    assertNull(ip.getIp());
+
+  }
+
 
 }