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

Issue:
DNS resolution performance improvements
### Introduction
Most time spent on DNS resolution on DPS is caused by hostnames which won't get solved cause they are fake hostnames made by `search` feature (described on #336 ) or just a remote hostname query which got an timeout or spent a bit more time than expected to solve, it decreases DPS performance considerably.

### Goals
Improve DPS cache intelligence on remote solved names so  DPS will be much faster and stable:

✅ 1- I read is a common approach to DNS Servers to cache names for more time than specified on the TTL 

In general people expects the server name address to be updated in some hours, then if DPS caches found hostnames for at least 5 minutes and don't found for 1 hour, it might speed up a lot.

[ref 1][1]
> By default, all [proxied records](https://developers.cloudflare.com/dns/manage-dns-records/reference/proxied-dns-records/) have a TTL of Auto, which is set to 300 seconds.

[ref 2][2]
> A change to a DNS record—for example, changing the IP address defined for a specific hostname—can take up to 72 hours to propagate worldwide, although it typically takes a few hours

[ref 3][3]
>  No geral, a propagação das alterações do DNS ocorre dentro de algumas horas. No entanto, pode levar até 48 horas para que a propagação alcance toda a internet

[ref 4][4]
> Most registrars set the TTL to 24 hours IIRC, so for 24 hours some people will see the old address and some will see the new one and by 24 hours after the change everyone should have the new address, with some instead using a lower value like 4 hours.

✅  2- DPS must improve the remote resolution to make distinguish when a name not exists or when it just got an resolution timeout then name which not exists must be cached for a bigger period of time, maybe 1 hour?

✅ 3- Create a mechanism so the user can flush the cache

### Non Goals

todo

[1]: https://developers.cloudflare.com/dns/manage-dns-records/reference/ttl/
[2]: https://ns1.com/resources/dns-propagation#:~:text=DNS%20propagation%20is%20the%20time,typically%20takes%20a%20few%20hours.
[3]: https://br.godaddy.com/help/quais-fatores-afetam-o-tempo-de-propagacao-de-dns-1746
[4]: https://serverfault.com/questions/45470/how-long-will-a-dns-change-take

Pull request:
DNS resolution performance improvements and minor fixes
Related to #370 

Patch:
diff --git a/.gitignore b/.gitignore
index 1cc0da78..4b91c22f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -70,3 +70,4 @@ examples/tmp
 issues
 /files
 tmp/
+Poc.java
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 74f6e978..06e08325 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -1,3 +1,11 @@
+## 3.13.0
+* Caching remote solved hostnames for 5 minutes
+* Caching not found hostnames for 1 hour
+* Gui interface to clear the cache
+* Not documented api to get cache size and values
+* Increased remote timeout to 10 seconds to make sure won't get easlily get timeout on remote server slowness
+* Order interfaces by index when choosing an IP as the machine IP, put loopback at the end of the list. 
+
 ## 3.12.1
 * Binding UDP server to anylocalhost just as TCP this way DPS can be used on any interface.
 * Disabling mac as the binary isn't working macos-latest, see #341
diff --git a/app/src/components/Home.js b/app/src/components/Home.js
index 6e7f85b1..18e1c46b 100644
--- a/app/src/components/Home.js
+++ b/app/src/components/Home.js
@@ -1,6 +1,6 @@
 import EnvPicker from './EnvPicker';
 import EnvForm from './EnvForm';
-import NavBar from './NavBar';
+import {NavBar} from './NavBar';
 import React from 'react'
 import $ from 'jquery';
 import {RecordForm} from './RecordForm.js'
diff --git a/app/src/components/NavBar.js b/app/src/components/NavBar.js
index 82cd117f..a4062f47 100644
--- a/app/src/components/NavBar.js
+++ b/app/src/components/NavBar.js
@@ -1,29 +1,46 @@
 import React from 'react';
+import {Settings} from './Settings.js'
 
-const NavBar = () => (
-	<nav className="navbar navbar-inverse navbar-fixed-top" >
-		<a className="navbar-brand" href="#">DNS Proxy Server</a>
-		<button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
-			aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
-			<span className="navbar-toggler-icon"></span>
-		</button>
-		<div className="collapse navbar-collapse" id="navbarNav">
-			<ul className="navbar-nav pull-right">
-				<li className="nav-item active">
-					<a className="nav-link" href="#">Home <span className="sr-only">(current)</span></a>
-				</li>
-				<li className="nav-item">
-					<a className="nav-link" href="#">Features</a>
-				</li>
-				<li className="nav-item">
-					<a className="nav-link" href="#">Pricing</a>
-				</li>
-				<li className="nav-item">
-					<a className="nav-link disabled" href="#">Disabled</a>
-				</li>
-			</ul>
-		</div>
-	</nav>
-);
+export class NavBar extends React.Component {
+  constructor(props) {
+    super();
+    this.state = {};
+    this.props = props;
+  }
 
-export default NavBar;
+  componentDidMount() {
+
+  }
+
+  openSettings(){
+    window.$('#settings-modal').modal()
+  }
+
+  render() {
+    return (
+      <>
+        <nav className="navbar navbar-expand-lg navbar-dark bg-dark navbar-inverse navbar-fixed-top" >
+          <a className="navbar-brand" href="#">DNS Proxy Server</a>
+          <button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
+                  aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
+            <span className="navbar-toggler-icon"></span>
+          </button>
+          <div className="collapse navbar-collapse" id="navbarNav">
+            <ul className="navbar-nav mr-auto pull-right">
+              <li className="nav-item active">
+                <a className="nav-link" href="#">Home <span className="sr-only">(current)</span></a>
+              </li>
+              <li className="nav-item">
+                <a className="nav-link" onClick={(e) => this.openSettings()} href="#">Settings</a>
+              </li>
+              <li className="nav-item">
+                <a className="nav-link" target="_blank" href="http://mageddo.github.io/dns-proxy-server/latest/en/2-features/">Features / Docs</a>
+              </li>
+            </ul>
+          </div>
+        </nav>
+        <Settings />
+      </>
+    );
+  }
+}
diff --git a/app/src/components/Settings.js b/app/src/components/Settings.js
new file mode 100644
index 00000000..7b70e66c
--- /dev/null
+++ b/app/src/components/Settings.js
@@ -0,0 +1,73 @@
+import React from 'react';
+import $ from "jquery";
+
+export class Settings extends React.Component {
+  constructor(props) {
+    super();
+    this.state = {};
+    this.props = props;
+  }
+
+  componentDidMount() {
+
+  }
+
+  clearCache(){
+    window.$.ajax({
+      method: "DELETE",
+      url: "/v1/caches",
+      error: response => {
+        window.$.notify({
+          title: 'Ops!',
+          message: 'An error ocurred',
+        }, {
+          type: 'danger'
+        });
+      },
+      success: () => {
+        window.$.notify({
+          message: `Cache cleared`
+        }, {
+          type: 'success'
+        });
+      },
+    });
+  }
+
+  render() {
+    return (
+      <>
+        <div className="modal" tabIndex="-1" role="dialog" id="settings-modal" style={{zIndex: 9999}}>
+          <div className="modal-dialog" role="document">
+            <div className="modal-content">
+              <div className="modal-header">
+                <h5 className="modal-title">Settings</h5>
+                <button
+                  type="button" className="close" data-dismiss="modal" aria-label="Close"
+                >
+                  <span aria-hidden="true">&times;</span>
+                </button>
+              </div>
+              <div className="modal-body">
+                <form>
+                  <div className="form-group">
+                    <button
+                      type="button" className="btn btn-info" id="btnClearCache"
+                      onClick={(e) => this.clearCache()}
+                    >
+                      <i className="fa fa-trash"></i>
+                    </button>&nbsp;
+                    <label htmlFor="formGroupExampleInput">Clear Cache</label>
+                  </div>
+                </form>
+              </div>
+              <div className="modal-footer">
+                <button type="button" className="btn btn-secondary" data-dismiss="modal">Close</button>
+              </div>
+            </div>
+          </div>
+        </div>
+      </>
+    );
+  }
+}
diff --git a/build.gradle b/build.gradle
index 10f3babb..ee5dcc79 100644
--- a/build.gradle
+++ b/build.gradle
@@ -36,7 +36,7 @@ dependencies {
   implementation 'org.graalvm.sdk:graal-sdk:22.3.1'
   implementation('jakarta.enterprise:jakarta.enterprise.cdi-api:2.0.2')
   implementation('jakarta.ws.rs:jakarta.ws.rs-api:2.1.6')
-  implementation('com.mageddo.commons:commons-lang:0.1.6')
+  implementation('com.mageddo.commons:commons-lang:0.1.7')
   implementation('org.apache.commons:commons-exec:1.3')
 
   implementation('ch.qos.logback:logback-classic:1.4.5')
diff --git a/gradle.properties b/gradle.properties
index 72bc992c..9e6e9fed 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,1 +1,1 @@
-version=3.12.1
+version=3.13.0-snapshot
diff --git a/src/main/java/com/mageddo/dnsproxyserver/quarkus/QuarkusConfig.java b/src/main/java/com/mageddo/dnsproxyserver/quarkus/QuarkusConfig.java
index c823a5ef..921562fa 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/quarkus/QuarkusConfig.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/quarkus/QuarkusConfig.java
@@ -1,11 +1,9 @@
 package com.mageddo.dnsproxyserver.quarkus;
 
-import com.mageddo.dnsproxyserver.config.Config;
 import com.mageddo.dnsproxyserver.config.Configs;
 import com.mageddo.dnsproxyserver.server.dns.IpAddr;
 import com.mageddo.dnsproxyserver.server.dns.solver.RemoteResolvers;
 import com.mageddo.dnsproxyserver.utils.InetAddresses;
-import com.mageddo.logback.LogbackUtils;
 import dagger.Module;
 import dagger.Provides;
 import org.xbill.DNS.Resolver;
@@ -15,8 +13,6 @@ import javax.enterprise.inject.Produces;
 import java.time.Duration;
 import java.util.function.Function;
 
-import static org.apache.commons.lang3.StringUtils.equalsIgnoreCase;
-
 @Module
 public class QuarkusConfig {
 
@@ -34,7 +30,7 @@ public class QuarkusConfig {
   public Function<IpAddr, Resolver> getResolverProvider() {
     return it -> {
       final var resolver = new SimpleResolver(InetAddresses.toSocketAddress(it.getRawIP(), it.getPortOrDef(53)));
-      resolver.setTimeout(Duration.ofMillis(300));
+      resolver.setTimeout(Duration.ofSeconds(10)); // default is 10 seconds
       return resolver;
     };
   }
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/Messages.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/Messages.java
index 8c117757..95a20c48 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/Messages.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/Messages.java
@@ -1,6 +1,7 @@
 package com.mageddo.dnsproxyserver.server.dns;
 
 import com.mageddo.dnsproxyserver.config.Config;
+import com.mageddo.dnsproxyserver.server.dns.solver.Response;
 import com.mageddo.dnsproxyserver.utils.Ips;
 import lombok.SneakyThrows;
 import org.xbill.DNS.ARecord;
@@ -19,6 +20,13 @@ import java.util.Optional;
 
 public class Messages {
 
+  public static final long DEFAULT_TTL = 30L;
+  public static final Duration DEFAULT_TTL_DURATION = Duration.ofSeconds(DEFAULT_TTL);
+
+  public static String simplePrint(Response res) {
+    return simplePrint(res.getMessage());
+  }
+
   public static String simplePrint(Message message) {
     if (message == null) {
       return null;
@@ -65,7 +73,7 @@ public class Messages {
   }
 
   public static Message aAnswer(Message query, String ip) {
-    return aAnswer(query, ip, 30L);
+    return aAnswer(query, ip, DEFAULT_TTL);
   }
 
   public static Message aAnswer(Message query, String ip, final long ttl) {
@@ -177,7 +185,7 @@ public class Messages {
   /**
    * Set the id of the query into the response, se the response will match if the query;
    */
-  public static Message matchId(Message req, Message res) {
+  public static Message idMatches(Message req, Message res) {
     final var reqId = req.getHeader().getID();
     res.getHeader().setID(reqId);
     return res;
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/RequestHandlerDefault.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/RequestHandlerDefault.java
index 896ecd39..da452222 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/RequestHandlerDefault.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/RequestHandlerDefault.java
@@ -1,5 +1,6 @@
 package com.mageddo.dnsproxyserver.server.dns;
 
+import com.mageddo.dnsproxyserver.server.dns.solver.Response;
 import com.mageddo.dnsproxyserver.server.dns.solver.Solver;
 import com.mageddo.dnsproxyserver.server.dns.solver.SolversCache;
 import com.mageddo.dnsproxyserver.utils.Classes;
@@ -56,7 +57,7 @@ public class RequestHandlerDefault implements RequestHandler {
     }
   }
 
-  Message solve0(Message reqMsg) {
+  Response solve0(Message reqMsg) {
     final var stopWatch = StopWatch.createStarted();
     for (final var solver : this.solvers) {
       stopWatch.split();
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/UDPServer.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/UDPServer.java
index 02a691f9..7154fc0a 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/UDPServer.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/UDPServer.java
@@ -55,7 +55,6 @@ public class UDPServer {
       final var resData = res.toWire();
 
       server.send(new DatagramPacket(resData, resData.length, datagram.getSocketAddress()));
-
       log.debug(
         "status=success, query={}, res={}, serverAddr={}, clientAddr={}, dataLength={}, datagramLength={}",
         Messages.simplePrint(query), Messages.simplePrint(res),
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/CacheEntry.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/CacheEntry.java
new file mode 100644
index 00000000..e626aa67
--- /dev/null
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/CacheEntry.java
@@ -0,0 +1,15 @@
+package com.mageddo.dnsproxyserver.server.dns.solver;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+
+@Data
+@Accessors(chain = true)
+public class CacheEntry {
+  private String key;
+  private Duration ttl;
+  private LocalDateTime createdAt;
+}
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/Response.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/Response.java
new file mode 100644
index 00000000..4accf616
--- /dev/null
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/Response.java
@@ -0,0 +1,46 @@
+package com.mageddo.dnsproxyserver.server.dns.solver;
+
+import com.mageddo.dnsproxyserver.server.dns.Messages;
+import lombok.Builder;
+import lombok.NonNull;
+import lombok.Value;
+import org.xbill.DNS.Message;
+
+import java.time.Duration;
+
+@Value
+@Builder(toBuilder = true)
+public class Response {
+
+  /**
+   * The effective response.
+   */
+  @NonNull
+  private Message message;
+
+  /**
+   * the calculated ttl, can be the specified on the message or calculated to a different one.
+   */
+  @NonNull
+  private Duration ttl;
+
+  public static Response of(Message message){
+    return of(message, Messages.DEFAULT_TTL_DURATION);
+  }
+
+  public static Response of(Message message, Duration ttl){
+    return Response
+      .builder()
+      .message(message)
+      .ttl(ttl)
+      .build();
+  }
+
+  public Response withMessage(Message msg) {
+    return this
+      .toBuilder()
+      .message(msg)
+      .build()
+      ;
+  }
+}
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/Solver.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/Solver.java
index 6666c48e..529eba63 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/Solver.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/Solver.java
@@ -5,7 +5,7 @@ import org.xbill.DNS.Message;
 
 public interface Solver {
 
-  Message handle(Message query);
+  Response handle(Message query);
 
   default String name() {
     return ClassUtils.getSimpleName(getClass());
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverDelegate.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverDelegate.java
index 438a2670..20cd52a5 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverDelegate.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverDelegate.java
@@ -9,6 +9,7 @@ import org.xbill.DNS.Message;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
+import java.time.Duration;
 
 /**
  * When {@link SolverLocalDB} finds a wildcard hostname, delegate the found hostname to this class.
@@ -20,7 +21,7 @@ public class SolverDelegate {
 
   private final SolverProvider solverProvider;
 
-  public Message solve(Message query, Config.Entry entry){
+  public Response solve(Message query, Config.Entry entry) {
     log.debug("status=solvingCnameIp, source={}, target={}", entry.getHostname(), entry.getTarget());
 
     final var cnameAnswer = cnameAnswer(query, entry);
@@ -30,11 +31,11 @@ public class SolverDelegate {
       final var res = solver.handle(question);
       if (res != null) {
         log.debug("status=cnameARecordSolved, host={}, r={}", entry.getHostname(), Messages.simplePrint(res));
-        return Messages.combine(res, cnameAnswer);
+        return res.withMessage(Messages.combine(res.getMessage(), cnameAnswer));
       }
     }
-    // answer only the cname, without the matching IP when a IP is not found
-    return cnameAnswer;
+    // answer only the cname, without the matching IP when no IP is found
+    return Response.of(cnameAnswer, Duration.ofSeconds(entry.getTtl()));
   }
 
   static Message cnameAnswer(Message query, Config.Entry entry) {
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverDocker.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverDocker.java
index 361579d2..94ed7979 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverDocker.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverDocker.java
@@ -1,7 +1,7 @@
 package com.mageddo.dnsproxyserver.server.dns.solver;
 
-import com.mageddo.dnsproxyserver.docker.DockerDAO;
 import com.mageddo.dnsproxyserver.docker.ContainerSolvingService;
+import com.mageddo.dnsproxyserver.docker.DockerDAO;
 import com.mageddo.dnsproxyserver.server.dns.Messages;
 import com.mageddo.dnsproxyserver.server.dns.Wildcards;
 import lombok.AllArgsConstructor;
@@ -20,7 +20,7 @@ public class SolverDocker implements Solver {
   private final DockerDAO dockerDAO;
 
   @Override
-  public Message handle(Message query) {
+  public Response handle(Message query) {
 
     if (!this.dockerDAO.isConnected()) {
       log.trace("status=dockerDisconnected");
@@ -31,7 +31,7 @@ public class SolverDocker implements Solver {
     for (final var host : Wildcards.buildHostAndWildcards(askedHost)) {
       final var ip = this.containerSolvingService.findBestHostIP(host);
       if (ip != null) {
-        return Messages.aAnswer(query, ip);
+        return Response.of(Messages.aAnswer(query, ip));
       }
     }
 
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverLocalDB.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverLocalDB.java
index 06ed5dfa..a77ada79 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverLocalDB.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverLocalDB.java
@@ -13,6 +13,7 @@ import org.xbill.DNS.Message;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
+import java.time.Duration;
 
 @Slf4j
 @Singleton
@@ -23,7 +24,7 @@ public class SolverLocalDB implements Solver {
   private final SolverDelegate solverDelegate;
 
   @Override
-  public Message handle(Message query) {
+  public Response handle(Message query) {
 
     final var stopWatch = StopWatch.createStarted();
 
@@ -50,7 +51,10 @@ public class SolverLocalDB implements Solver {
       );
 
       if (Type.is(entry.getType(), Type.A, Type.AAAA)) {
-        return Messages.aAnswer(query, entry.getIp(), entry.getTtl());
+        return Response.of(
+          Messages.aAnswer(query, entry.getIp(), entry.getTtl()),
+          Duration.ofSeconds(entry.getTtl())
+        );
       }
       return this.solverDelegate.solve(query, entry);
     }
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverMock.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverMock.java
index 03dc8760..c9154a9f 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverMock.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverMock.java
@@ -23,11 +23,11 @@ public class SolverMock implements Solver {
   }
 
   @Override
-  public Message handle(Message query) {
+  public Response handle(Message query) {
     final var hostname = Messages.findQuestionHostname(query);
     for (final var entry : this.mocks) {
       if (entry.getKey().equalsIgnoreCase(hostname.getValue())) {
-        return Messages.aAnswer(query, entry.getValue().raw());
+        return Response.of(Messages.aAnswer(query, entry.getValue().raw()));
       }
     }
     return null;
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverRemote.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverRemote.java
index 5785cea7..e4f819f0 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverRemote.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverRemote.java
@@ -1,6 +1,6 @@
 package com.mageddo.dnsproxyserver.server.dns.solver;
 
-import lombok.AllArgsConstructor;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.ClassUtils;
 import org.xbill.DNS.Message;
@@ -10,18 +10,22 @@ import org.xbill.DNS.Resolver;
 import javax.inject.Inject;
 import javax.inject.Singleton;
 import java.io.IOException;
+import java.time.Duration;
 
 import static com.mageddo.dnsproxyserver.server.dns.Messages.simplePrint;
 
 @Slf4j
 @Singleton
-@AllArgsConstructor(onConstructor = @__({@Inject}))
+@RequiredArgsConstructor(onConstructor = @__({@Inject}))
 public class SolverRemote implements Solver {
 
+  public static final Duration DEFAULT_SUCCESS_TTL = Duration.ofMinutes(5);
+  public static final Duration DEFAULT_NXDOMAIN_TTL = Duration.ofMinutes(60);
+
   private final RemoteResolvers delegate;
 
   @Override
-  public Message handle(Message query) {
+  public Response handle(Message query) {
     Message lastErrorMsg = null;
     for (int i = 0; i < this.delegate.resolvers().size(); i++) {
       final Resolver resolver = this.delegate.resolvers().get(i);
@@ -29,7 +33,7 @@ public class SolverRemote implements Solver {
         final var res = resolver.send(query);
         if (res.getRcode() == Rcode.NOERROR) {
           log.trace("status=found, i={}, req={}, res={}, server={}", i, simplePrint(query), simplePrint(res), resolver);
-          return res;
+          return Response.of(res, DEFAULT_SUCCESS_TTL);
         } else {
           lastErrorMsg = res;
           log.trace("status=notFound, i={}, req={}, res={}, server={}", i, simplePrint(query), simplePrint(res), resolver);
@@ -48,6 +52,9 @@ public class SolverRemote implements Solver {
         );
       }
     }
-    return lastErrorMsg;
+    if (lastErrorMsg == null) {
+      return null;
+    }
+    return Response.of(lastErrorMsg, DEFAULT_NXDOMAIN_TTL);
   }
 }
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverSystem.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverSystem.java
index 224e4e5e..f95e74f4 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverSystem.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverSystem.java
@@ -18,7 +18,7 @@ public class SolverSystem implements Solver {
   private final DockerDAO dockerDAO;
 
   @Override
-  public Message handle(Message query) {
+  public Response handle(Message query) {
     final var hostname = Messages.findQuestionHostname(query);
     final var config = Configs.getInstance();
     if (hostname.isEqualTo(config.getHostMachineHostname())) { // fixme fazer case com hostname + search domain
@@ -28,7 +28,7 @@ public class SolverSystem implements Solver {
         return null;
       }
       log.debug("status=solvingHostMachineName, host={}, ip={}", hostname, ip);
-      return Messages.aAnswer(query, ip.raw());
+      return Response.of(Messages.aAnswer(query, ip.raw()), Messages.DEFAULT_TTL_DURATION);
     }
     return null;
   }
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolversCache.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolversCache.java
index 85dafe2a..3bac6c22 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolversCache.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/SolversCache.java
@@ -1,6 +1,7 @@
 package com.mageddo.dnsproxyserver.server.dns.solver;
 
 import com.mageddo.commons.caching.LruTTLCache;
+import com.mageddo.commons.lang.Objects;
 import com.mageddo.commons.lang.tuple.Pair;
 import com.mageddo.dnsproxyserver.server.dns.Messages;
 import lombok.RequiredArgsConstructor;
@@ -10,7 +11,9 @@ import org.xbill.DNS.Message;
 import javax.inject.Inject;
 import javax.inject.Singleton;
 import java.time.Duration;
-import java.util.Optional;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
 import java.util.UUID;
 import java.util.function.Function;
 
@@ -24,23 +27,23 @@ public class SolversCache {
 
   private final LruTTLCache cache = new LruTTLCache(2048, Duration.ofSeconds(5), false);
 
-  public Message handle(Message query, Function<Message, Message> delegate) {
+  public Message handle(Message query, Function<Message, Response> delegate) {
     final var key = buildKey(query);
     final var res = this.cache.computeIfAbsent0(key, (k) -> {
       log.trace("status=lookup, key={}, req={}", key, Messages.simplePrint(query));
       final var _res = delegate.apply(query);
       if (_res == null) {
-        log.debug("status=noAnswer, k={}", k);
+        log.debug("status=noAnswer, action=cant-cache, k={}", k);
         return null;
       }
-      final var ttl = Messages.findTTL(_res);
+      final var ttl = _res.getTtl();
       log.debug("status=hotload, k={}, ttl={}, simpleMsg={}", k, ttl, Messages.simplePrint(query));
       return Pair.of(_res, ttl);
     });
-    return Optional
-      .ofNullable(res)
-      .map(it -> Messages.matchId(query, it))
-      .orElse(null);
+    if (res == null) {
+      return null;
+    }
+    return Objects.mapOrNull(res.getMessage(), it -> Messages.idMatches(query, it));
   }
 
   static String buildKey(Message reqMsg) {
@@ -48,7 +51,27 @@ public class SolversCache {
     return String.format("%s-%s", type != null ? type : UUID.randomUUID(), findQuestionHostname(reqMsg));
   }
 
-  public int getSize(){
+  public int getSize() {
     return this.cache.getSize();
   }
+
+  public void clear() {
+    this.cache.clear();
+  }
+
+  public Map<String, CacheEntry> asMap() {
+    final var m = this.cache.asMap();
+    final var tmpMap = new HashMap<String, CacheEntry>();
+    final var keys = new HashSet<>(m.keySet());
+    for (final String k : keys) {
+      final var v = m.get(k);
+      final var entry = new CacheEntry()
+        .setKey(k)
+        .setTtl(v.getTtl())
+        .setCreatedAt(v.getCreatedAt());
+      tmpMap.put(k, entry);
+    }
+    return tmpMap;
+  }
+
 }
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/rest/CacheResource.java b/src/main/java/com/mageddo/dnsproxyserver/server/rest/CacheResource.java
index 10683449..9b0d943d 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/rest/CacheResource.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/rest/CacheResource.java
@@ -6,31 +6,22 @@ import com.mageddo.http.WebServer;
 import lombok.RequiredArgsConstructor;
 
 import javax.inject.Inject;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
+import javax.inject.Singleton;
 import javax.ws.rs.core.Response;
 import java.util.Map;
 
 import static com.mageddo.http.codec.Encoders.encodeJson;
 
-@Path("/v1/caches")
+@Singleton
 @RequiredArgsConstructor(onConstructor = @__({@Inject}))
 public class CacheResource implements HttpMapper {
 
   private final SolversCache cache;
 
-  @GET
-  @Path("/size")
-  @Produces(MediaType.APPLICATION_JSON)
-  public Object findCaches() {
-    return Map.of("size", this.cache.getSize());
-  }
-
   @Override
   public void map(WebServer server) {
-    server.map(
+
+    server.get(
       "/v1/caches/size",
       exchange -> encodeJson(
         exchange,
@@ -38,6 +29,27 @@ public class CacheResource implements HttpMapper {
         Map.of("size", this.cache.getSize())
       )
     );
+
+    server.delete(
+      "/v1/caches",
+      exchange -> {
+        this.cache.clear();
+        encodeJson(
+          exchange,
+          Response.Status.OK,
+          Map.of("size", this.cache.getSize())
+        );
+      }
+    );
+
+    server.get(
+      "/v1/caches",
+      exchange -> encodeJson(
+        exchange,
+        Response.Status.OK,
+        this.cache.asMap()
+      )
+    );
   }
 
 }
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/rest/reqres/CacheEntryResV1.java b/src/main/java/com/mageddo/dnsproxyserver/server/rest/reqres/CacheEntryResV1.java
new file mode 100644
index 00000000..115e4323
--- /dev/null
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/rest/reqres/CacheEntryResV1.java
@@ -0,0 +1,33 @@
+package com.mageddo.dnsproxyserver.server.rest.reqres;
+
+import com.mageddo.dnsproxyserver.server.dns.solver.CacheEntry;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Data
+@Accessors(chain = true)
+public class CacheEntryResV1 {
+
+  private String key;
+  private String ttl;
+  private String createdAt;
+
+  public static List<CacheEntryResV1> of(List<CacheEntry> entries) {
+    return entries
+      .stream()
+      .map(CacheEntryResV1::of)
+      .collect(Collectors.toList());
+  }
+
+  public static CacheEntryResV1 of(CacheEntry entry) {
+    return new CacheEntryResV1()
+      .setKey(entry.getKey())
+      .setTtl(String.valueOf(entry.getTtl()))
+      .setCreatedAt(String.valueOf(entry.getCreatedAt()))
+      ;
+  }
+
+}
diff --git a/src/main/java/com/mageddo/net/Networks.java b/src/main/java/com/mageddo/net/Networks.java
index bea0994b..91c3ac4a 100644
--- a/src/main/java/com/mageddo/net/Networks.java
+++ b/src/main/java/com/mageddo/net/Networks.java
@@ -25,10 +25,13 @@ public class Networks {
   public static List<IP> findMachineIps() {
     return findInterfaces()
       .stream()
+      .sorted(Comparator.comparingInt(NetworkInterface::getIndex))
       .flatMap(NetworkInterface::inetAddresses)
-      .filter(it -> it.getAddress().length == IP.BYTES)
+      .filter(it -> it.getAddress().length == IP.BYTES) // todo needs a filter to exclude virtual network cards
       .map(it -> IP.of(it.getHostAddress()))
-      .sorted(Comparator.comparing(it -> it.raw().startsWith("127") ? Integer.MAX_VALUE : 0))
+      .sorted(Comparator.comparing(it -> {
+        return it.raw().startsWith("127") ? Integer.MAX_VALUE : 0;
+      }))
       .toList()
       ;
   }


Test patch:
diff --git a/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverRemoteCompTest.java b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/RemoteResolversCompTest.java
similarity index 95%
rename from src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverRemoteCompTest.java
rename to src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/RemoteResolversCompTest.java
index 1fdaa147..11fac25c 100644
--- a/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverRemoteCompTest.java
+++ b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/RemoteResolversCompTest.java
@@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 
 
 @DaggerTest(component = Context.class, eventsHandler = Events.class)
-class SolverRemoteCompTest {
+class RemoteResolversCompTest {
 
   @Inject
   RemoteResolvers remoteResolvers;
diff --git a/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverDelegateTest.java b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverDelegateTest.java
index e5a1b462..9bc90130 100644
--- a/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverDelegateTest.java
+++ b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverDelegateTest.java
@@ -44,7 +44,7 @@ class SolverDelegateTest {
 
     // assert
     assertNotNull(response);
-    validResponse(response);
+    validResponse(response.getMessage());
 
   }
 }
diff --git a/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverLocalDBCompTest.java b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverLocalDBCompTest.java
index 8c9a3939..67b475d5 100644
--- a/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverLocalDBCompTest.java
+++ b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverLocalDBCompTest.java
@@ -64,7 +64,7 @@ class SolverLocalDBCompTest {
     assertNotNull(res);
     assertEquals(
         "acme.com.    45  IN  A  10.10.0.1",
-        Messages.detailedPrint(res)
+        Messages.detailedPrint(res.getMessage())
     );
 
   }
@@ -92,7 +92,7 @@ class SolverLocalDBCompTest {
     assertNotNull(res);
     assertEquals(
         "www.acme.com.    45  IN  CNAME  acme.com. | acme.com.    30  IN  A  192.168.1.8",
-        Messages.detailedPrint(res)
+        Messages.detailedPrint(res.getMessage())
     );
 
   }
@@ -112,7 +112,7 @@ class SolverLocalDBCompTest {
     assertNotNull(res);
     assertEquals(
       "acme.com.    45  IN  A  10.10.0.1",
-      Messages.detailedPrint(res)
+      Messages.detailedPrint(res.getMessage())
     );
 
   }
diff --git a/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverLocalDBTest.java b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverLocalDBTest.java
index e41cf6fd..5828b8f5 100644
--- a/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverLocalDBTest.java
+++ b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverLocalDBTest.java
@@ -49,7 +49,7 @@ class SolverLocalDBTest {
     assertNotNull(res);
     assertEquals(
       "acme.com.    45  IN  A  10.10.0.1",
-      Messages.detailedPrint(res)
+      Messages.detailedPrint(res.getMessage())
     );
 
     verify(this.solver).findEntryTo(hostname);
diff --git a/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverRemoteTest.java b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverRemoteTest.java
new file mode 100644
index 00000000..6ef2377d
--- /dev/null
+++ b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverRemoteTest.java
@@ -0,0 +1,102 @@
+package com.mageddo.dnsproxyserver.server.dns.solver;
+
+import com.mageddo.dnsproxyserver.templates.MessageTemplates;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.xbill.DNS.Resolver;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+
+@ExtendWith(MockitoExtension.class)
+class SolverRemoteTest {
+
+  @Mock
+  Resolver resolver;
+
+  @Mock
+  RemoteResolvers resolvers;
+
+  @Spy
+  @InjectMocks
+  SolverRemote solverRemote;
+
+  @Test
+  void mustCacheSolvedQueryFor5Minutes() throws Exception {
+    // arrange
+    final var query = MessageTemplates.acmeAQuery();
+    final var answer = MessageTemplates.buildAAnswer(query);
+
+    doReturn(answer)
+      .when(this.resolver)
+      .send(any())
+    ;
+
+    doReturn(List.of(this.resolver))
+      .when(this.resolvers)
+      .resolvers()
+    ;
+
+    // act
+    final var res = this.solverRemote.handle(query);
+
+    // assert
+    assertEquals(SolverRemote.DEFAULT_SUCCESS_TTL, res.getTtl());
+  }
+
+  @Test
+  void mustCacheNxDomainQueryFor1Hour() throws Exception {
+    // arrange
+    final var query = MessageTemplates.acmeAQuery();
+    final var answer = MessageTemplates.buildNXAnswer(query);
+
+    doReturn(answer)
+      .when(this.resolver)
+      .send(any())
+    ;
+
+    doReturn(List.of(this.resolver))
+      .when(this.resolvers)
+      .resolvers()
+    ;
+
+    // act
+    final var res = this.solverRemote.handle(query);
+
+    // assert
+    assertEquals(SolverRemote.DEFAULT_NXDOMAIN_TTL, res.getTtl());
+  }
+
+  @Test
+  void mustReturnNullWhenGetTimeout() throws Exception {
+    // arrange
+    final var query = MessageTemplates.acmeAQuery();
+
+    doThrow(new IOException("Deu ruim"))
+      .when(this.resolver)
+      .send(any())
+    ;
+
+    doReturn(List.of(this.resolver))
+      .when(this.resolvers)
+      .resolvers()
+    ;
+
+    // act
+    final var res = this.solverRemote.handle(query);
+
+    // assert
+    assertNull(res);
+  }
+
+}
diff --git a/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverSystemTest.java b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverSystemCompTest.java
similarity index 92%
rename from src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverSystemTest.java
rename to src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverSystemCompTest.java
index 78290d50..0dc90a07 100644
--- a/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverSystemTest.java
+++ b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverSystemCompTest.java
@@ -19,7 +19,7 @@ import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
 
 @DaggerTest(component = Context.class, eventsHandler = Events.class)
-class SolverSystemTest {
+class SolverSystemCompTest {
 
   @InjectMock
   DockerDAO dockerDAO;
@@ -42,7 +42,7 @@ class SolverSystemTest {
     final var res = this.solver.handle(query);
 
     // assert
-    final var answer = Messages.findFirstAnswerRecordStr(res);
+    final var answer = Messages.findFirstAnswerRecordStr(res.getMessage());
     assertThat(answer, CoreMatchers.containsString(hostname));
     assertEquals("host.docker.\t\t30\tIN\tA\t192.168.0.1", answer);
 
diff --git a/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolversCacheTest.java b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolversCacheTest.java
index 3644093d..19abe450 100644
--- a/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolversCacheTest.java
+++ b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolversCacheTest.java
@@ -24,7 +24,7 @@ class SolversCacheTest {
     final var req = MessageTemplates.acmeAQuery();
 
     // act
-    final var res = this.cache.handle(req, message -> Messages.aAnswer(message, "0.0.0.0"));
+    final var res = this.cache.handle(req, message -> Response.of(Messages.aAnswer(message, "0.0.0.0")));
 
     // assert
     assertNotNull(res);
diff --git a/src/test/java/com/mageddo/dnsproxyserver/templates/MessageTemplates.java b/src/test/java/com/mageddo/dnsproxyserver/templates/MessageTemplates.java
index 42c92812..413107d0 100644
--- a/src/test/java/com/mageddo/dnsproxyserver/templates/MessageTemplates.java
+++ b/src/test/java/com/mageddo/dnsproxyserver/templates/MessageTemplates.java
@@ -22,7 +22,11 @@ public class MessageTemplates {
     return buildAQuestionFor(ACME_HOSTNAME + ".");
   }
 
-  public static Message buildAAnswer(Message m) {
-    return Messages.aAnswer(m, IpTemplates.LOCAL);
+  public static Message buildAAnswer(Message query) {
+    return Messages.aAnswer(query, IpTemplates.LOCAL);
+  }
+
+  public static Message buildNXAnswer(Message query) {
+    return Messages.nxDomain(query);
   }
 }
diff --git a/src/test/java/com/mageddo/dnsproxyserver/templates/docker/SolverTemplates.java b/src/test/java/com/mageddo/dnsproxyserver/templates/docker/SolverTemplates.java
index d1ef65ee..12336872 100644
--- a/src/test/java/com/mageddo/dnsproxyserver/templates/docker/SolverTemplates.java
+++ b/src/test/java/com/mageddo/dnsproxyserver/templates/docker/SolverTemplates.java
@@ -1,6 +1,7 @@
 package com.mageddo.dnsproxyserver.templates.docker;
 
 import com.mageddo.dnsproxyserver.server.dns.Messages;
+import com.mageddo.dnsproxyserver.server.dns.solver.Response;
 import com.mageddo.dnsproxyserver.server.dns.solver.Solver;
 
 import java.util.List;
@@ -8,7 +9,7 @@ import java.util.List;
 public class SolverTemplates {
   public static List<Solver> mockTo192() {
     return List.of(reqMsg -> {
-      return Messages.aAnswer(reqMsg, "192.168.1.8");
+      return Response.of(Messages.aAnswer(reqMsg, "192.168.1.8"));
     });
   }
 }