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

Issue:
Support regex or mid-level subdomain wildcard DNS
### What is expected

Be able to provide a `HOSTNAMES=subdomain.*.domain.tld` env variable to a container on a network with a DPS container and have DPS match `subdomain.some.domain.tld`, `subdomain.other.domain.tld`, `subdomain.mid-level.domain.tld`, `subdomain.subdomain.domain.tld`, for example.

Alternatively, you could support regex. For example:
- `HOSTNAMES=subdomain\..*\.domain.tld` to match questions that start with `subdomain.` and end with `.domain.tld`
- `HOSTNAMES=subdomain\.[a-z]+-test.tld` to match questions that start with `subdomain.` and where the domain starts with alpha chars and ends in `-test`

Currently, I only need the former option but the regex option would be more flexible (and maybe easier to implement?).

### What is Happening

DPS isn't able to parse mid-level wildcards or regex. Logs report `Not solver for the question {URL}`.

### Changes (Optional)


Pull request description:
Support for regex on hostname
Fixes #245 

Patch:
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index e713f994..fa9f8a9d 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -1,3 +1,6 @@
+## 3.14.0
+* Support for regex on localdb solver and docker container hostname and HOSTNAMES env
+
 ## 3.13.1
 * Caching remote solved hostnames for 5 minutes
 * Caching not found hostnames for 1 hour
diff --git a/docs/content/2-features/solving-hostnames-wildcard/_index.en.md b/docs/content/2-features/solving-hostnames-wildcard/_index.en.md
index c9c1e483..68289432 100644
--- a/docs/content/2-features/solving-hostnames-wildcard/_index.en.md
+++ b/docs/content/2-features/solving-hostnames-wildcard/_index.en.md
@@ -3,11 +3,15 @@ title: Wildcards hostnames Solving
 weight: 3
 ---
 
+## Wildcard
+
 If you register a hostname with `.` at start, then all subdomains will solve to that container/local storage entry
 
 Example
 
-	docker run --rm --hostname .mageddo.com nginx:latest
+```
+$ docker run --rm --hostname .mageddo.com nginx:latest
+```
 
 Now all **mageddo.com** subdomains will solve to that nginx container
 
@@ -19,7 +23,9 @@ Address:	172.17.0.4#53
 Non-authoritative answer:
 Name:	site1.mageddo.com
 Address: 172.17.0.5
+```
 
+```
 $ nslookup mageddo.com
 Server:		172.17.0.4
 Address:	172.17.0.4#53
@@ -27,5 +33,55 @@ Address:	172.17.0.4#53
 Non-authoritative answer:
 Name:	mageddo.com
 Address: 172.17.0.5
+```
+
+## RegEx
+
+From DPS `3.14` regex is also supported on localstorage or docker container hostname/HOSTNAMES env,
+the pattern is `/${REGEX}/`, everything around the slashes is considered as regex
+
+**Example:**
+
+Running a sample container
+```bash
+$ docker run --rm --name nginx1 --hostname '/batata\.[a-z]+\.com/' -e 'HOSTNAMES=/\d+\.acme\.com/,/sub\.acme\.com/' nginx
+```
+
+Solving
+```
+$ nslookup batata.whatever.com
+Server:		127.0.0.1
+Address:	127.0.0.1#53
+
+Non-authoritative answer:
+Name:	batata.whatever.com
+Address: 172.17.0.4
+```
+
+```
+$ nslookup xpto.acme.com
+Server:		127.0.0.1
+Address:	127.0.0.1#53
+
+** server can't find xpto.acme.com: NXDOMAIN
+```
+
+```
+$ nslookup 123.acme.com
+Server:		127.0.0.1
+Address:	127.0.0.1#53
+
+Non-authoritative answer:
+Name:	123.acme.com
+Address: 172.17.0.4
+```
 
 ```
+$ nslookup sub.acme.com
+Server:		127.0.0.1
+Address:	127.0.0.1#53
+
+Non-authoritative answer:
+Name:	sub.acme.com
+Address: 172.17.0.4
+```
diff --git a/gradle.properties b/gradle.properties
index 9f7fdb7f..09670f26 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,1 +1,1 @@
-version=3.13.1
+version=3.14.0-snapshot
diff --git a/src/main/java/com/mageddo/dnsproxyserver/config/ConfigDAO.java b/src/main/java/com/mageddo/dnsproxyserver/config/ConfigDAO.java
index 1965e530..3e978fe4 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/config/ConfigDAO.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/config/ConfigDAO.java
@@ -1,6 +1,6 @@
 package com.mageddo.dnsproxyserver.config;
 
-import com.mageddo.dnsproxyserver.server.dns.Hostname;
+import com.mageddo.dnsproxyserver.server.dns.solver.HostnameQuery;
 
 import java.util.List;
 
@@ -10,7 +10,7 @@ public interface ConfigDAO {
 
   Config.Env findEnv(String env);
 
-  Config.Entry findEntryForActiveEnv(Hostname hostname);
+  Config.Entry findEntryForActiveEnv(HostnameQuery hostname);
 
   void addEntry(String env, Config.Entry entry);
 
diff --git a/src/main/java/com/mageddo/dnsproxyserver/config/ConfigDAOJson.java b/src/main/java/com/mageddo/dnsproxyserver/config/ConfigDAOJson.java
index 3393ed58..e134f02c 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/config/ConfigDAOJson.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/config/ConfigDAOJson.java
@@ -6,8 +6,7 @@ import com.mageddo.dnsproxyserver.config.entrypoint.JsonConfigs;
 import com.mageddo.dnsproxyserver.config.entrypoint.predicate.JsonEnvPredicate;
 import com.mageddo.dnsproxyserver.config.predicate.EntryPredicate;
 import com.mageddo.dnsproxyserver.config.predicate.EnvPredicate;
-import com.mageddo.dnsproxyserver.server.dns.Hostname;
-import com.mageddo.dnsproxyserver.server.dns.Hostnames;
+import com.mageddo.dnsproxyserver.server.dns.solver.HostnameQuery;
 import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
@@ -40,11 +39,11 @@ public class ConfigDAOJson implements ConfigDAO {
   }
 
   @Override
-  public Config.Entry findEntryForActiveEnv(Hostname hostname) {
+  public Config.Entry findEntryForActiveEnv(HostnameQuery hostname) {
     final var env = this.findActiveEnv();
     return env.getEntries()
       .stream()
-      .filter(it -> Hostnames.matches(hostname, it.getHostname()))
+      .filter(it -> hostname.matches(it.getHostname()))
       .findFirst()
       .orElse(null);
   }
diff --git a/src/main/java/com/mageddo/dnsproxyserver/docker/ContainerHostnameMatcher.java b/src/main/java/com/mageddo/dnsproxyserver/docker/ContainerHostnameMatcher.java
index 2bbf4f1d..c5615e9c 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/docker/ContainerHostnameMatcher.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/docker/ContainerHostnameMatcher.java
@@ -3,7 +3,7 @@ package com.mageddo.dnsproxyserver.docker;
 import com.github.dockerjava.api.command.InspectContainerResponse;
 import com.mageddo.dnsproxyserver.config.Config;
 import com.mageddo.dnsproxyserver.config.Configs;
-import com.mageddo.dnsproxyserver.server.dns.Hostname;
+import com.mageddo.dnsproxyserver.server.dns.solver.HostnameQuery;
 import org.apache.commons.lang3.BooleanUtils;
 
 import java.util.List;
@@ -15,15 +15,15 @@ import static com.mageddo.dnsproxyserver.docker.DpsContainer.findHostnamesFromEn
 
 public class ContainerHostnameMatcher {
 
-  public static boolean test(final InspectContainerResponse inspect, final Hostname host, final Config config) {
+  public static boolean test(final InspectContainerResponse inspect, final HostnameQuery host, final Config config) {
     return buildPredicate(host, config).test(inspect);
   }
 
-  static Predicate<InspectContainerResponse> buildPredicate(final Hostname host) {
+  static Predicate<InspectContainerResponse> buildPredicate(final HostnameQuery host) {
     return buildPredicate(host, Configs.getInstance());
   }
 
-  static Predicate<InspectContainerResponse> buildPredicate(final Hostname host, final Config config) {
+  static Predicate<InspectContainerResponse> buildPredicate(final HostnameQuery host, final Config config) {
     return container -> {
 
       final List<Predicate<InspectContainerResponse>> predicates = List.of(
@@ -44,16 +44,22 @@ public class ContainerHostnameMatcher {
     return BooleanUtils.isTrue(config.getRegisterContainerNames());
   }
 
-  public static boolean hostnameMatches(InspectContainerResponse c, Hostname host) {
-    return host.isEqualTo(findContainerHostname(c.getConfig()));
+  public static boolean hostnameMatches(InspectContainerResponse c, HostnameQuery host) {
+    return host.matches(findContainerHostname(c.getConfig()));
   }
 
-  public static boolean hostnamesEnvMatches(InspectContainerResponse c, Hostname host) {
-    return findHostnamesFromEnv(c.getConfig().getEnv()).contains(host);
+  public static boolean hostnamesEnvMatches(InspectContainerResponse c, HostnameQuery hostnameQuery) {
+    return findHostnamesFromEnv(c.getConfig().getEnv())
+      .stream()
+      .anyMatch(hostnameQuery::matches)
+      ;
   }
 
-  public static boolean serviceOrContainerNameMatches(InspectContainerResponse c, Hostname host, Config config) {
+  public static boolean serviceOrContainerNameMatches(InspectContainerResponse c, HostnameQuery hostQuery, Config config) {
     return isRegisterContainerNames(config)
-      && buildHostnamesFromServiceOrContainerNames(c, config.getDomain()).contains(host);
+      && buildHostnamesFromServiceOrContainerNames(c, config.getDomain())
+      .stream()
+      .anyMatch(hostQuery::matches)
+      ;
   }
 }
diff --git a/src/main/java/com/mageddo/dnsproxyserver/docker/ContainerSolvingService.java b/src/main/java/com/mageddo/dnsproxyserver/docker/ContainerSolvingService.java
index 185800d4..5f1376a9 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/docker/ContainerSolvingService.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/docker/ContainerSolvingService.java
@@ -1,8 +1,8 @@
 package com.mageddo.dnsproxyserver.docker;
 
 import com.github.dockerjava.api.command.InspectContainerResponse;
+import com.mageddo.dnsproxyserver.server.dns.solver.HostnameQuery;
 import com.mageddo.net.Networks;
-import com.mageddo.dnsproxyserver.server.dns.Hostname;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
@@ -39,7 +39,7 @@ public class ContainerSolvingService {
   private final DockerDAO dockerDAO;
   private final DockerNetworkDAO networkDAO;
 
-  public String findBestHostIP(Hostname host) {
+  public String findBestHostIP(HostnameQuery host) {
     final var stopWatch = StopWatch.createStarted();
     final var matchedContainers = this.findMatchingContainers(host);
     final var foundIp = matchedContainers
@@ -132,12 +132,11 @@ public class ContainerSolvingService {
       .collect(Collectors.toCollection(LinkedHashSet::new));
   }
 
-  private List<InspectContainerResponse> findMatchingContainers(Hostname host) {
+  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/Docker.java b/src/main/java/com/mageddo/dnsproxyserver/docker/Docker.java
index 0204f6f4..b4dba0ce 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/docker/Docker.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/docker/Docker.java
@@ -6,6 +6,7 @@ import com.mageddo.dnsproxyserver.server.dns.Hostname;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 
+import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -30,7 +31,9 @@ public class Docker {
       .of(
         buildFromContainerName(container, domain),
         buildFromServiceName(container, domain)
-      ).map(Hostname::of)
+      )
+      .filter(Objects::nonNull)
+      .map(Hostname::of)
       .collect(Collectors.toSet())
       ;
   }
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/Hostname.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/Hostname.java
index e0313e74..10352dd0 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/Hostname.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/Hostname.java
@@ -28,6 +28,10 @@ public class Hostname {
     return Objects.equals(this.value, hostname.value);
   }
 
+  public String getCanonicalValue() {
+    return this.value;
+  }
+
   public String getValue() {
     return this.rawValue;
   }
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/Hostnames.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/Hostnames.java
index f2d82cfa..40753312 100644
--- a/src/main/java/com/mageddo/dnsproxyserver/server/dns/Hostnames.java
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/Hostnames.java
@@ -15,7 +15,4 @@ public class Hostnames {
     return s + HOSTNAME_TERMINATE_CHAR;
   }
 
-  public static boolean matches(Hostname expected, String actual) {
-    return expected.isEqualTo(actual);
-  }
 }
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/HostnameMatcher.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/HostnameMatcher.java
new file mode 100644
index 00000000..4d32933f
--- /dev/null
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/HostnameMatcher.java
@@ -0,0 +1,26 @@
+package com.mageddo.dnsproxyserver.server.dns.solver;
+
+import com.mageddo.dnsproxyserver.server.dns.Hostname;
+
+import java.util.List;
+import java.util.function.Function;
+
+public class HostnameMatcher {
+
+  public static <T> T match(Hostname hostname, Function<HostnameQuery, T> hostnameProviderFn) {
+
+    final var wildcardHostname = HostnameQuery.ofWildcard(hostname);
+    final var regexHostname = HostnameQuery.ofRegex(hostname);
+    final var queries = List.of(wildcardHostname, regexHostname);
+
+    for (final var host : queries) {
+      final var ip = hostnameProviderFn.apply(host);
+      if (ip != null) {
+        return ip;
+      }
+    }
+
+    return null;
+  }
+
+}
diff --git a/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/HostnameQuery.java b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/HostnameQuery.java
new file mode 100644
index 00000000..b6cce54b
--- /dev/null
+++ b/src/main/java/com/mageddo/dnsproxyserver/server/dns/solver/HostnameQuery.java
@@ -0,0 +1,80 @@
+package com.mageddo.dnsproxyserver.server.dns.solver;
+
+import com.mageddo.dnsproxyserver.server.dns.Hostname;
+import com.mageddo.dnsproxyserver.server.dns.Wildcards;
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.NonNull;
+import lombok.Value;
+import org.apache.commons.lang3.StringUtils;
+
+@Value
+@Builder
+@EqualsAndHashCode
+public class HostnameQuery {
+
+  public static final String REGEX_TAG = "/";
+
+  @NonNull
+  private final Hostname hostname;
+
+  private final boolean useWildcards;
+
+  private final boolean useRegex;
+
+  public static HostnameQuery of(Hostname hostname) {
+    return of(hostname, false, false);
+  }
+
+  public static HostnameQuery ofWildcard(String hostname) {
+    return ofWildcard(Hostname.of(hostname));
+  }
+
+  public static HostnameQuery ofWildcard(Hostname hostname) {
+    return of(hostname, true, false);
+  }
+
+  public static HostnameQuery ofRegex(String hostname) {
+    return ofRegex(Hostname.of(hostname));
+  }
+
+  public static HostnameQuery ofRegex(Hostname hostname) {
+    return of(hostname, false, true);
+  }
+
+  public static HostnameQuery of(Hostname hostname, boolean wildcards, boolean regex) {
+    return HostnameQuery
+      .builder()
+      .hostname(hostname)
+      .useWildcards(wildcards)
+      .useRegex(regex)
+      .build();
+  }
+
+  public static HostnameQuery of(String hostname) {
+    return of(Hostname.of(hostname));
+  }
+
+  public boolean matches(Hostname hostname) {
+    return matches(hostname.getCanonicalValue());
+  }
+
+  public boolean matches(String hostnamePattern) {
+    if (this.useWildcards) {
+      final var hostnames = Wildcards.buildHostAndWildcards(this.hostname);
+      for (final var host : hostnames) {
+        if (host.isEqualTo(hostnamePattern)) {
+          return true;
+        }
+      }
+      return false;
+    }
+    if (this.useRegex && hostnamePattern.startsWith(REGEX_TAG) && hostnamePattern.endsWith(REGEX_TAG)) {
+      return this.hostname
+        .getCanonicalValue()
+        .matches(StringUtils.substringBetween(hostnamePattern, REGEX_TAG, REGEX_TAG))
+        ;
+    }
+    return this.hostname.isEqualTo(hostnamePattern);
+  }
+}
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 94ed7979..ffdc25b9 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,10 +1,10 @@
 package com.mageddo.dnsproxyserver.server.dns.solver;
 
+import com.mageddo.commons.lang.Objects;
 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;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.xbill.DNS.Message;
 
@@ -13,7 +13,7 @@ import javax.inject.Singleton;
 
 @Slf4j
 @Singleton
-@AllArgsConstructor(onConstructor = @__({@Inject}))
+@RequiredArgsConstructor(onConstructor = @__({@Inject}))
 public class SolverDocker implements Solver {
 
   private final ContainerSolvingService containerSolvingService;
@@ -28,14 +28,11 @@ public class SolverDocker implements Solver {
     }
 
     final var askedHost = Messages.findQuestionHostname(query);
-    for (final var host : Wildcards.buildHostAndWildcards(askedHost)) {
-      final var ip = this.containerSolvingService.findBestHostIP(host);
-      if (ip != null) {
-        return Response.of(Messages.aAnswer(query, ip));
-      }
-    }
+    return HostnameMatcher.match(askedHost, hostname -> {
+      final var ip = this.containerSolvingService.findBestHostIP(hostname);
+      return Objects.mapOrNull(ip, (it) -> Response.of(Messages.aAnswer(query, ip)));
+    });
 
-    return null;
   }
 
 }
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 a77ada79..55ea91ae 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
@@ -3,9 +3,7 @@ package com.mageddo.dnsproxyserver.server.dns.solver;
 import com.mageddo.dnsproxyserver.config.Config;
 import com.mageddo.dnsproxyserver.config.Config.Entry.Type;
 import com.mageddo.dnsproxyserver.config.ConfigDAO;
-import com.mageddo.dnsproxyserver.server.dns.Hostname;
 import com.mageddo.dnsproxyserver.server.dns.Messages;
-import com.mageddo.dnsproxyserver.server.dns.Wildcards;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.time.StopWatch;
@@ -35,19 +33,19 @@ public class SolverLocalDB implements Solver {
     }
 
     final var askedHost = Messages.findQuestionHostname(query);
-    for (final var host : Wildcards.buildHostAndWildcards(askedHost)) {
+    final var res = HostnameMatcher.match(askedHost, hostname -> {
       stopWatch.split();
-      final var entry = this.findEntryTo(host);
+      final var entry = this.findEntryTo(hostname);
       if (entry == null) {
         log.trace(
-            "status=partialNotFound, askedHost={}, time={}",
-            askedHost, stopWatch.getTime() - stopWatch.getSplitTime()
+          "status=partialNotFound, askedHost={}, time={}",
+          askedHost, stopWatch.getTime() - stopWatch.getSplitTime()
         );
-        continue;
+        return null;
       }
       log.trace(
-          "status=found, type={}, askedHost={}, time={}, totalTime={}",
-          entry.getType(), askedHost, stopWatch.getTime() - stopWatch.getSplitTime(), stopWatch.getTime()
+        "status=found, type={}, askedHost={}, time={}, totalTime={}",
+        entry.getType(), askedHost, stopWatch.getTime() - stopWatch.getSplitTime(), stopWatch.getTime()
       );
 
       if (Type.is(entry.getType(), Type.A, Type.AAAA)) {
@@ -57,13 +55,15 @@ public class SolverLocalDB implements Solver {
         );
       }
       return this.solverDelegate.solve(query, entry);
+    });
+    if (res != null) {
+      return res;
     }
-
     log.trace("status=notFound, askedHost={}, totalTime={}", askedHost, stopWatch.getTime());
     return null;
   }
 
-  Config.Entry findEntryTo(Hostname host) {
+  Config.Entry findEntryTo(HostnameQuery host) {
     return this.configDAO.findEntryForActiveEnv(host);
   }
 


Test patch:
diff --git a/src/test/java/com/mageddo/dnsproxyserver/config/ConfigDAOJsonTest.java b/src/test/java/com/mageddo/dnsproxyserver/config/ConfigDAOJsonTest.java
index 8cf8534d..0c6dd800 100644
--- a/src/test/java/com/mageddo/dnsproxyserver/config/ConfigDAOJsonTest.java
+++ b/src/test/java/com/mageddo/dnsproxyserver/config/ConfigDAOJsonTest.java
@@ -1,6 +1,6 @@
 package com.mageddo.dnsproxyserver.config;
 
-import com.mageddo.dnsproxyserver.server.dns.Hostname;
+import com.mageddo.dnsproxyserver.server.dns.solver.HostnameQuery;
 import com.mageddo.dnsproxyserver.templates.EnvTemplates;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -23,7 +23,7 @@ class ConfigDAOJsonTest {
   @Test
   void mustDoCaseInsensitiveFind() {
     // arrange
-    final var hostname = Hostname.of(EnvTemplates.MAGEDDO_COM);
+    final var hostname = HostnameQuery.of(EnvTemplates.MAGEDDO_COM);
     final var env = EnvTemplates.buildWithCamelCaseHost();
 
     doReturn(env)
diff --git a/src/test/java/com/mageddo/dnsproxyserver/docker/ContainerHostnameMatcherTest.java b/src/test/java/com/mageddo/dnsproxyserver/docker/ContainerHostnameMatcherTest.java
index 98442823..42ebc8de 100644
--- a/src/test/java/com/mageddo/dnsproxyserver/docker/ContainerHostnameMatcherTest.java
+++ b/src/test/java/com/mageddo/dnsproxyserver/docker/ContainerHostnameMatcherTest.java
@@ -1,7 +1,9 @@
 package com.mageddo.dnsproxyserver.docker;
 
 import com.mageddo.dnsproxyserver.config.Configs;
-import com.mageddo.dnsproxyserver.server.dns.Hostname;
+import com.mageddo.dnsproxyserver.server.dns.solver.HostnameQuery;
+import com.mageddo.dnsproxyserver.templates.HostnameQueryTemplates;
+import com.mageddo.dnsproxyserver.templates.HostnameTemplates;
 import com.mageddo.dnsproxyserver.templates.docker.InspectContainerResponseTemplates;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -18,10 +20,10 @@ class ContainerHostnameMatcherTest {
   }
 
   @Test
-  void mustSolveFromContainerHostnameButNoDomain(){
+  void mustSolveFromContainerHostnameWithoutNoDomain(){
     // arrange
     final var inspect = InspectContainerResponseTemplates.buildWithHostnameAndWithoutDomain();
-    final var hostname = Hostname.of("nginx-2.dev");
+    final var hostname = HostnameQueryTemplates.nginxWildcard();
     final var config = Configs.getInstance();
 
     // act
@@ -32,10 +34,10 @@ class ContainerHostnameMatcherTest {
   }
 
   @Test
-  void mustSolveFromContainerHostnameAndDomain(){
+  void mustSolveFromContainerHostnameWithDomain(){
     // arrange
     final var inspect = InspectContainerResponseTemplates.buildWithHostnameAndDomain("acme.com", "local");
-    final var hostname = Hostname.of("acme.com.local");
+    final var hostname = HostnameQueryTemplates.acmeComLocal();
     final var config = Configs.getInstance();
 
     // act
@@ -49,7 +51,7 @@ class ContainerHostnameMatcherTest {
   void mustSolveFromContainerHostnameEnv(){
     // arrange
     final var inspect = InspectContainerResponseTemplates.build();
-    final var hostname = Hostname.of("nginx.com.br");
+    final var hostname = HostnameQueryTemplates.nginxComBrWildcard();
     final var config = Configs.getInstance();
 
     // act
@@ -59,11 +61,28 @@ class ContainerHostnameMatcherTest {
     assertTrue(test, String.valueOf(hostname));
   }
 
+  @Test
+  void mustSolveRegexFromContainerHostnameEnv(){
+    // arrange
+    final var inspect = InspectContainerResponseTemplates.buildWithHostnamesEnv("/nginx.+/,/acme.+/");
+    final var hostname = HostnameQuery.ofRegex(HostnameTemplates.NGINX_COM_BR);
+    final var hostnameAcme = HostnameQuery.ofRegex(HostnameTemplates.NGINX_COM_BR);
+    final var config = Configs.getInstance();
+
+    // act
+    final var testNginx = ContainerHostnameMatcher.test(inspect, hostname, config);
+    final var testAcme = ContainerHostnameMatcher.test(inspect, hostnameAcme, config);
+
+    // assert
+    assertTrue(testNginx, String.valueOf(hostname));
+    assertTrue(testAcme, String.valueOf(hostnameAcme));
+  }
+
   @Test
   void mustSolveFromContainerName(){
     // arrange
     final var inspect = InspectContainerResponseTemplates.build();
-    final var hostname = Hostname.of("laughing_swanson.docker");
+    final var hostname = HostnameQuery.ofWildcard("laughing_swanson.docker");
     final var config = Configs.build(new String[]{"--register-container-names"});
 
     // act
@@ -79,7 +98,7 @@ class ContainerHostnameMatcherTest {
   void mustSolveFromServiceName(){
     // arrange
     final var inspect = InspectContainerResponseTemplates.build();
-    final var hostname = Hostname.of("nginx-service.docker");
+    final var hostname = HostnameQuery.of("nginx-service.docker");
     final var config = Configs.build(new String[]{"--register-container-names"});
 
     // act
@@ -96,7 +115,7 @@ class ContainerHostnameMatcherTest {
   void mustNOTSolveFromServiceNameWhenFeatureIsDisabled(){
     // arrange
     final var inspect = InspectContainerResponseTemplates.build();
-    final var hostname = Hostname.of("shibata.docker");
+    final var hostname = HostnameQuery.of("shibata.docker");
     final var config = Configs.getInstance();
 
     // act
diff --git a/src/test/java/com/mageddo/dnsproxyserver/server/CacheResourceCompTest.java b/src/test/java/com/mageddo/dnsproxyserver/server/CacheResourceCompTest.java
index f79d1c80..bc1f475b 100644
--- a/src/test/java/com/mageddo/dnsproxyserver/server/CacheResourceCompTest.java
+++ b/src/test/java/com/mageddo/dnsproxyserver/server/CacheResourceCompTest.java
@@ -1,18 +1,18 @@
 package com.mageddo.dnsproxyserver.server;
 
 import com.mageddo.dnsproxyserver.config.Configs;
-import com.mageddo.dnsproxyserver.di.Context;
-import testing.Events;
+import testing.ContextSupplier;
 import dagger.sheath.junit.DaggerTest;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import testing.Events;
 
 import javax.ws.rs.core.Response;
 
 import static io.restassured.RestAssured.given;
 import static org.hamcrest.CoreMatchers.equalTo;
 
-@DaggerTest(component = Context.class, eventsHandler = Events.class)
+@DaggerTest(initializer = ContextSupplier.class, eventsHandler = Events.class)
 class CacheResourceCompTest {
 
   @BeforeEach
diff --git a/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/HostnameQueryTest.java b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/HostnameQueryTest.java
new file mode 100644
index 00000000..5bd2000c
--- /dev/null
+++ b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/HostnameQueryTest.java
@@ -0,0 +1,84 @@
+package com.mageddo.dnsproxyserver.server.dns.solver;
+
+import com.mageddo.dnsproxyserver.templates.HostnameQueryTemplates;
+import com.mageddo.dnsproxyserver.templates.HostnameTemplates;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class HostnameQueryTest {
+
+  @Test
+  void mustMatchWildcard(){
+
+    // arrange
+    final var acme = HostnameQueryTemplates.acmeComWildcard();
+
+    // act
+    final var matches = acme.matches(HostnameTemplates.COM_WILDCARD);
+
+    // assert
+    assertTrue(matches);
+
+  }
+
+  @Test
+  void mustMatchWildcardWhenUsingSubdomain(){
+
+    // arrange
+    final var acme = HostnameQueryTemplates.orangeAcmeComWildcard();
+
+    // act
+    final var matches = acme.matches(acme.getHostname());
+
+    // assert
+    assertTrue(matches);
+
+  }
+
+  @Test
+  void mustMatchExactHostname(){
+
+    // arrange
+    final var acme = HostnameQueryTemplates.acmeComWildcard();
+
+    // act
+    final var matches = acme.matches(acme.getHostname());
+
+    // assert
+    assertTrue(matches);
+
+  }
+
+  @Test
+  void mustMatchRegex(){
+
+    // arrange
+    final var acme = HostnameQuery.ofRegex(HostnameTemplates.ACME_HOSTNAME);
+    final var queryRegex = "/[a-z]+\\.com/";
+
+    // act
+    final var matches = acme.matches(queryRegex);
+
+    // assert
+    assertTrue(matches);
+
+  }
+
+  @Test
+  void mustRespectRegexTemplate(){
+
+    // arrange
+    final var hostname = HostnameQuery.ofRegex("a");
+    final var queryRegex = ".";
+
+    // act
+    final var matches = hostname.matches(queryRegex);
+
+    // assert
+    assertFalse(matches);
+
+  }
+
+}
diff --git a/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverDockerTest.java b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverDockerTest.java
index 4b3798c4..4b28b63c 100644
--- a/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverDockerTest.java
+++ b/src/test/java/com/mageddo/dnsproxyserver/server/dns/solver/SolverDockerTest.java
@@ -1,25 +1,25 @@
 package com.mageddo.dnsproxyserver.server.dns.solver;
 
-import com.mageddo.dnsproxyserver.docker.DockerDAO;
 import com.mageddo.dnsproxyserver.docker.ContainerSolvingService;
-import com.mageddo.dnsproxyserver.server.dns.Hostname;
+import com.mageddo.dnsproxyserver.docker.DockerDAO;
+import com.mageddo.dnsproxyserver.templates.HostnameTemplates;
 import com.mageddo.dnsproxyserver.templates.MessageTemplates;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
-import org.mockito.Mock.Strictness;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
 
 @ExtendWith(MockitoExtension.class)
 class SolverDockerTest {
 
-  @Mock(strictness = Strictness.WARN)
+  @Mock
   ContainerSolvingService containerSolvingService;
 
   @Mock
@@ -35,10 +35,9 @@ class SolverDockerTest {
   @Test
   void mustSolveExactHostname() {
     // arrange
-
     final var query = MessageTemplates.acmeAQuery();
     final var ip = "0.0.0.0";
-    final var hostname = Hostname.of(MessageTemplates.ACME_HOSTNAME);
+    final var hostname = HostnameQuery.ofWildcard(HostnameTemplates.ACME_HOSTNAME);
 
     doReturn(true)
       .when(this.dockerDAO)
@@ -56,31 +55,7 @@ class SolverDockerTest {
 
     final var resText = res.toString();
     assertTrue(resText.contains(ip), resText);
+    verify(this.containerSolvingService).findBestHostIP(hostname);
   }
 
-  @Test
-  void mustSolveWildcardHostname() {
-    // arrange
-
-    final var query = MessageTemplates.acmeAQuery();
-    final var ip = "0.0.0.0";
-    final var hostname = Hostname.of("." + MessageTemplates.ACME_HOSTNAME);
-
-    doReturn(true)
-      .when(this.dockerDAO)
-      .isConnected()
-    ;
-    doReturn(ip)
-      .when(this.containerSolvingService)
-      .findBestHostIP(eq(hostname));
-
-    // act
-    final var res = this.solver.handle(query);
-
-    // assert
-    assertNotNull(res);
-
-    final var resText = res.toString();
-    assertTrue(resText.contains(ip), resText);
-  }
 }
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 5828b8f5..819c5f1d 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
@@ -1,8 +1,8 @@
 package com.mageddo.dnsproxyserver.server.dns.solver;
 
-import com.mageddo.dnsproxyserver.server.dns.Hostname;
 import com.mageddo.dnsproxyserver.server.dns.Messages;
 import com.mageddo.dnsproxyserver.templates.EntryTemplates;
+import com.mageddo.dnsproxyserver.templates.HostnameTemplates;
 import com.mageddo.dnsproxyserver.templates.MessageTemplates;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -12,9 +12,9 @@ import org.mockito.junit.jupiter.MockitoExtension;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
 @ExtendWith(MockitoExtension.class)
@@ -29,13 +29,8 @@ class SolverLocalDBTest {
 
     // arrange
     final var query = MessageTemplates.acmeAQuery();
-    final var hostname = Hostname.of(MessageTemplates.ACME_HOSTNAME);
-    final var wildcardHostName = Hostname.of(".com");
-
-    doReturn(null)
-      .when(this.solver)
-      .findEntryTo(any())
-    ;
+    final var hostname = HostnameQuery.of(HostnameTemplates.ACME_HOSTNAME);
+    final var wildcardHostName = HostnameQuery.ofWildcard(hostname.getHostname());
 
     doReturn(EntryTemplates.acmeAAAA())
       .when(this.solver)
@@ -52,7 +47,7 @@ class SolverLocalDBTest {
       Messages.detailedPrint(res.getMessage())
     );
 
-    verify(this.solver).findEntryTo(hostname);
+    verify(this.solver, never()).findEntryTo(hostname);
     verify(this.solver).findEntryTo(wildcardHostName);
 
   }
diff --git a/src/test/java/com/mageddo/dnsproxyserver/templates/HostnameQueryTemplates.java b/src/test/java/com/mageddo/dnsproxyserver/templates/HostnameQueryTemplates.java
new file mode 100644
index 00000000..7f42c17f
--- /dev/null
+++ b/src/test/java/com/mageddo/dnsproxyserver/templates/HostnameQueryTemplates.java
@@ -0,0 +1,26 @@
+package com.mageddo.dnsproxyserver.templates;
+
+import com.mageddo.dnsproxyserver.server.dns.solver.HostnameQuery;
+
+public class HostnameQueryTemplates {
+
+  public static HostnameQuery acmeComWildcard(){
+    return HostnameQuery.ofWildcard(HostnameTemplates.ACME_HOSTNAME);
+  }
+
+  public static HostnameQuery orangeAcmeComWildcard(){
+    return HostnameQuery.ofWildcard(HostnameTemplates.ORANGE_ACME_HOSTNAME);
+  }
+
+  public static HostnameQuery nginxWildcard() {
+    return HostnameQuery.ofWildcard("nginx-2.dev");
+  }
+
+  public static HostnameQuery acmeComLocal() {
+    return HostnameQuery.ofWildcard("acme.com.local");
+  }
+
+  public static HostnameQuery nginxComBrWildcard() {
+    return HostnameQuery.ofWildcard(HostnameTemplates.NGINX_COM_BR);
+  }
+}
diff --git a/src/test/java/com/mageddo/dnsproxyserver/templates/HostnameTemplates.java b/src/test/java/com/mageddo/dnsproxyserver/templates/HostnameTemplates.java
new file mode 100644
index 00000000..507d4375
--- /dev/null
+++ b/src/test/java/com/mageddo/dnsproxyserver/templates/HostnameTemplates.java
@@ -0,0 +1,8 @@
+package com.mageddo.dnsproxyserver.templates;
+
+public class HostnameTemplates {
+  public static final String ORANGE_ACME_HOSTNAME = "orange.acme.com";
+  public static final String ACME_HOSTNAME = "acme.com";
+  public static final String COM_WILDCARD = ".com";
+  public static final String NGINX_COM_BR = "nginx.com.br";
+}
diff --git a/src/test/java/com/mageddo/dnsproxyserver/templates/MessageTemplates.java b/src/test/java/com/mageddo/dnsproxyserver/templates/MessageTemplates.java
index 413107d0..1b6341cf 100644
--- a/src/test/java/com/mageddo/dnsproxyserver/templates/MessageTemplates.java
+++ b/src/test/java/com/mageddo/dnsproxyserver/templates/MessageTemplates.java
@@ -8,9 +8,9 @@ import org.xbill.DNS.Name;
 import org.xbill.DNS.Record;
 import org.xbill.DNS.Type;
 
-public class MessageTemplates {
+import static com.mageddo.dnsproxyserver.templates.HostnameTemplates.ACME_HOSTNAME;
 
-  public static final String ACME_HOSTNAME = "acme.com";
+public class MessageTemplates {
 
   @SneakyThrows
   public static Message buildAQuestionFor(String hostname) {
diff --git a/src/test/java/com/mageddo/dnsproxyserver/templates/docker/InspectContainerResponseTemplates.java b/src/test/java/com/mageddo/dnsproxyserver/templates/docker/InspectContainerResponseTemplates.java
index 55adf83c..5e2b252f 100644
--- a/src/test/java/com/mageddo/dnsproxyserver/templates/docker/InspectContainerResponseTemplates.java
+++ b/src/test/java/com/mageddo/dnsproxyserver/templates/docker/InspectContainerResponseTemplates.java
@@ -21,6 +21,12 @@ public class InspectContainerResponseTemplates {
     config.put("Domainname", domain);
     return parse(tree);
   }
+  public static InspectContainerResponse buildWithHostnamesEnv(String hostname) {
+    final var tree = buildTree(NGINX);
+    final var config = (ObjectNode) tree.at("/Config");
+    config.putArray("Env").add("HOSTNAMES=" + hostname);
+    return parse(tree);
+  }
 
   public static InspectContainerResponse withDpsLabel() {
     return build();
diff --git a/src/test/java/testing/ContextSupplier.java b/src/test/java/testing/ContextSupplier.java
new file mode 100644
index 00000000..e8eea95c
--- /dev/null
+++ b/src/test/java/testing/ContextSupplier.java
@@ -0,0 +1,14 @@
+package testing;
+
+import com.mageddo.dnsproxyserver.config.Configs;
+import com.mageddo.dnsproxyserver.di.Context;
+
+import java.util.function.Supplier;
+
+public class ContextSupplier implements Supplier<Context> {
+  @Override
+  public Context get() {
+    Configs.getInstance(new String[]{"--web-server-port=9944"});
+    return Context.create();
+  }
+}
diff --git a/src/test/java/testing/Events.java b/src/test/java/testing/Events.java
index 5adfe1ed..35ed57dc 100644
--- a/src/test/java/testing/Events.java
+++ b/src/test/java/testing/Events.java
@@ -1,5 +1,6 @@
 package testing;
 
+import com.mageddo.dnsproxyserver.config.Configs;
 import com.mageddo.dnsproxyserver.di.Context;
 import dagger.sheath.EventHandler;
 import io.restassured.RestAssured;
@@ -10,9 +11,10 @@ import lombok.extern.slf4j.Slf4j;
 public class Events implements EventHandler<Context> {
   @Override
   public void afterSetup(Context component) {
-    log.info("status=startingDPS");
+    final var config = Configs.getInstance();
+    log.info("status=startingDPS, port={}", config.getWebServerPort());
     component.start();
-    RestAssured.port = 5380;
+    RestAssured.port = config.getWebServerPort();
     RestAssured.config = RestAssured
         .config()
         .httpClient(