diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 0bb52e219..4df26069b 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,9 +1,12 @@ -## 5.0.5 +## 5.1.0 +* Changed resolv.conf handling format to make it compatible with alpine linux see [#627][5_1_0_1][1]. * Refactoring Linux amd64 static build to work on Github Actions * Creating the docs for config v3 * Fixing Hostname Entry Update API * Fixing intermittent unit test +[5_1_0_1]: https://github.com/mageddo/dns-proxy-server/issues/627 + ## 5.0.0 * Refactoring config module to support config v3 * Activating Config v3 for beta testing diff --git a/gradle.properties b/gradle.properties index e11546ed0..af1ed0e58 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=5.0.5-snapshot +version=5.1.0-snapshot diff --git a/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/DnsConfiguratorDefault.java b/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/DnsConfiguratorDefault.java index 84aed460f..5397e128f 100644 --- a/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/DnsConfiguratorDefault.java +++ b/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/DnsConfiguratorDefault.java @@ -37,7 +37,7 @@ public void configure(IpAddr addr) { final var serversBefore = this.findNetworkDnsServers(network); this.serversBefore.put(network, serversBefore); final var success = this.updateDnsServers(network, - Collections.singletonList(addr.getRawIP()) + Collections.singletonList(addr.getIpAsText()) ); log.debug("status=configuring, network={}, serversBefore={}, success={}", network, this.serversBefore, success diff --git a/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/DnsConfiguratorLinux.java b/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/DnsConfiguratorLinux.java index 8173d4d30..2f16b62df 100644 --- a/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/DnsConfiguratorLinux.java +++ b/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/DnsConfiguratorLinux.java @@ -58,7 +58,7 @@ public void configure(IpAddr addr) { if (confFile.isResolvconf()) { final var overrideNameServers = this.isOverrideNameServersActive(); - ResolvconfConfigurator.process(confFile.getPath(), addr, overrideNameServers); + ResolvconfConfiguratorV2.process(confFile.getPath(), addr, overrideNameServers); } else if (confFile.isResolved()) { this.configureResolved(addr, confFile); } else { @@ -82,7 +82,7 @@ public void restore() { final var confFile = this.getConfFile(); if (confFile.isResolvconf()) { - ResolvconfConfigurator.restore(confFile.getPath()); + ResolvconfConfiguratorV2.restore(confFile.getPath()); } else if (confFile.isResolved()) { ResolvedConfigurator.restore(confFile.getPath()); tryRestartResolved(); diff --git a/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/LinuxResolverConfDetector.java b/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/LinuxResolverConfDetector.java index 9438052fc..81ed0b3f2 100644 --- a/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/LinuxResolverConfDetector.java +++ b/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/LinuxResolverConfDetector.java @@ -8,6 +8,7 @@ import com.mageddo.dnsproxyserver.dnsconfigurator.linux.ResolvFile.Type; public class LinuxResolverConfDetector { + public static Type detect(Path path) { if (isSystemdResolved(path)) { diff --git a/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/ResolvconfConfigurator.java b/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/ResolvconfConfigurator.java index 6fc5bd9d1..9dc169e79 100644 --- a/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/ResolvconfConfigurator.java +++ b/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/ResolvconfConfigurator.java @@ -8,6 +8,10 @@ import com.mageddo.dnsproxyserver.utils.Dns; import com.mageddo.net.IpAddr; +/** + * @deprecated deprecated because of #627, use {@link ResolvconfConfiguratorV2} + */ +@Deprecated public class ResolvconfConfigurator { public static void process(Path confFile, IpAddr addr) { diff --git a/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/ResolvconfConfiguratorV2.java b/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/ResolvconfConfiguratorV2.java new file mode 100644 index 000000000..9c0ab58fe --- /dev/null +++ b/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/ResolvconfConfiguratorV2.java @@ -0,0 +1,422 @@ +package com.mageddo.dnsproxyserver.dnsconfigurator.linux; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; + +import com.mageddo.dnsproxyserver.utils.Dns; +import com.mageddo.net.IpAddr; + +public class ResolvconfConfiguratorV2 { + + private static final String BEGIN_ENTRIES = "# BEGIN dps-entries"; + private static final String END_ENTRIES = "# END dps-entries"; + private static final String BEGIN_COMMENTS = "# BEGIN dps-comments"; + private static final String END_COMMENTS = "# END dps-comments"; + + private static final String DPS_ENTRY_SUFFIX = "# dps-entry"; + private static final String DPS_COMMENT_SUFFIX = "# dps-comment"; + private static final String LINE_BREAK = "\n"; + + public static void process(final Path confFile, final IpAddr addr) { + process(confFile, addr, true); + } + + public static void process(final Path confFile, final IpAddr addr, + final boolean overrideNameServers) { + + Dns.validateIsDefaultPort(addr); + + final var dns = parseDnsAddress(addr.getIpAsText()); + final var content = readNormalized(confFile); + final var cleaned = removeDpsArtifacts(content); + + final var output = overrideNameServers + ? buildOverrideOutput(dns.address(), cleaned) + : buildNonOverrideOutput(dns.address(), cleaned); + + writeString(confFile, output); + } + + public static void restore(final Path confFile) { + final var content = readNormalized(confFile); + final var restored = restoreFromContent(content); + writeString(confFile, restored); + } + + // ------------------------------------------------------------------------- + // Build outputs + // ------------------------------------------------------------------------- + + private static String buildOverrideOutput( + final String dpsNameserverHost, + final CleanedContent cleaned + ) { + final var nameserversToComment = + collectNameserversToComment(dpsNameserverHost, cleaned); + + final var out = new StringBuilder(); + + append(out, BEGIN_ENTRIES); + append(out, nameserverLine(dpsNameserverHost)); + append(out, END_ENTRIES); + + if (nameserversToComment.isEmpty()) { + return out.toString(); + } + + append(out, ""); + append(out, BEGIN_COMMENTS); + + for (final var ns : nameserversToComment) { + append(out, commentedNameserverLine(ns)); + } + + append(out, END_COMMENTS); + + return out.toString(); + } + + private static void append(final StringBuilder out, final String value) { + out.append(value) + .append(LINE_BREAK); + } + + private static String buildNonOverrideOutput( + final String dpsNameserverHost, final CleanedContent cleaned + ) { + + final var lines = cleaned.originalLines(); + final var insertionIndex = indexAfterHeaderComments(lines); + final var out = new ArrayList<>(lines.subList(0, insertionIndex)); + + ensureBlankLine(out); + out.add(BEGIN_ENTRIES); + out.add(nameserverLine(dpsNameserverHost)); + out.add(END_ENTRIES); + out.add(""); + + final var remainderStart = skipBlankLines(lines, insertionIndex); + out.addAll(lines.subList(remainderStart, lines.size())); + + trimTrailingBlankLines(out); + return joinLines(out) + LINE_BREAK; + } + + private static List collectNameserversToComment( + final String dpsNameserverHost, final CleanedContent cleaned + ) { + final var nameservers = new LinkedHashSet(); + + // inline "# ... # dps-comment" captured during cleanup + nameservers.addAll(cleaned.inlineCommentCandidates()); + + // remaining active nameservers in the file + for (final var line : cleaned.originalLines()) { + final var ns = extractActiveNameserver(line); + if (ns != null) { + nameservers.add(ns); + } + } + + nameservers.remove(dpsNameserverHost); + return new ArrayList<>(nameservers); + } + + private static int indexAfterHeaderComments(final List lines) { + int i = 0; + while (i < lines.size()) { + final var trimmed = lines.get(i) + .trim(); + if (!trimmed.startsWith("#")) { + break; + } + if (isDpsMarker(trimmed)) { + break; + } + i++; + } + return i; + } + + private static int skipBlankLines(final List lines, final int startIndex) { + int i = startIndex; + while (i < lines.size() && lines.get(i) + .isBlank()) { + i++; + } + return i; + } + + private static void ensureBlankLine(final List lines) { + if (lines.isEmpty()) { + return; + } + if (!lines.getLast() + .isBlank()) { + lines.add(""); + } + } + + // ------------------------------------------------------------------------- + // Remove / Restore DPS artifacts + // ------------------------------------------------------------------------- + + private static CleanedContent removeDpsArtifacts(final String normalizedContent) { + final var lines = splitLines(normalizedContent); + + final var cleaned = new ArrayList(); + final var inlineCommentCandidates = new LinkedHashSet(); + + boolean insideEntriesBlock = false; + boolean insideCommentsBlock = false; + + for (final var line : lines) { + final var trimmed = line.trim(); + + if (trimmed.equals(BEGIN_ENTRIES)) { + insideEntriesBlock = true; + continue; + } + if (trimmed.equals(END_ENTRIES)) { + insideEntriesBlock = false; + continue; + } + if (trimmed.equals(BEGIN_COMMENTS)) { + insideCommentsBlock = true; + continue; + } + if (trimmed.equals(END_COMMENTS)) { + insideCommentsBlock = false; + continue; + } + + // drop managed blocks completely (both entries and comments) + if (insideEntriesBlock || insideCommentsBlock) { + continue; + } + + // drop inline dps-entry + if (isInlineDpsEntry(line)) { + continue; + } + + // capture inline dps-comment and drop the line from output + if (isInlineDpsComment(line)) { + final var restored = restoreInlineDpsComment(line); + final var ns = restored == null ? null : extractActiveNameserver(restored); + if (ns != null) { + inlineCommentCandidates.add(ns); + } + continue; + } + + cleaned.add(line); + } + + trimLeadingBlankLines(cleaned); + trimTrailingBlankLines(cleaned); + + return new CleanedContent(cleaned, new ArrayList<>(inlineCommentCandidates)); + } + + private static String restoreFromContent(final String normalizedContent) { + final var lines = splitLines(normalizedContent); + final var restored = new ArrayList(); + + boolean insideEntriesBlock = false; + boolean insideCommentsBlock = false; + + for (final var line : lines) { + final var trimmed = line.trim(); + + if (trimmed.equals(BEGIN_ENTRIES)) { + insideEntriesBlock = true; + continue; + } + if (trimmed.equals(END_ENTRIES)) { + insideEntriesBlock = false; + continue; + } + if (trimmed.equals(BEGIN_COMMENTS)) { + insideCommentsBlock = true; + continue; + } + if (trimmed.equals(END_COMMENTS)) { + insideCommentsBlock = false; + continue; + } + + if (insideEntriesBlock) { + continue; + } + + if (insideCommentsBlock) { + final var restoredLine = uncommentNameserverIfPresent(line); + if (restoredLine != null && !restoredLine.isBlank()) { + restored.add(restoredLine); + } + continue; + } + + if (isInlineDpsEntry(line)) { + continue; + } + + if (isInlineDpsComment(line)) { + final var restoredLine = restoreInlineDpsComment(line); + if (restoredLine != null && !restoredLine.isBlank()) { + restored.add(restoredLine); + } + continue; + } + + if (!line.isBlank()) { + restored.add(line); + } + } + + return joinLines(restored) + LINE_BREAK; + } + + private static DnsAddress parseDnsAddress(final String addr) { + return new DnsAddress(addr); + } + + // ------------------------------------------------------------------------- + // Nameserver line parsing + // ------------------------------------------------------------------------- + + private static String extractActiveNameserver(final String line) { + final var trimmed = line.trim(); + if (!trimmed.startsWith("nameserver")) { + return null; + } + final var parts = trimmed.split("\\s+"); + return parts.length >= 2 ? parts[1].trim() : null; + } + + private static String uncommentNameserverIfPresent(final String line) { + var trimmed = line.trim(); + if (!trimmed.startsWith("#")) { + return null; + } + trimmed = trimmed.substring(1) + .trim(); + return trimmed.startsWith("nameserver") ? trimmed : null; + } + + private static String restoreInlineDpsComment(final String line) { + // "# nameserver 8.8.8.8 # dps-comment" -> "nameserver 8.8.8.8" + final var withoutSuffix = line.replace(DPS_COMMENT_SUFFIX, "") + .trim(); + var trimmed = withoutSuffix.trim(); + if (trimmed.startsWith("#")) { + trimmed = trimmed.substring(1) + .trim(); + } + return trimmed; + } + + private static String nameserverLine(final String host) { + return "nameserver " + host; + } + + private static String commentedNameserverLine(final String host) { + return "# " + nameserverLine(host); + } + + // ------------------------------------------------------------------------- + // DPS markers / inline markers + // ------------------------------------------------------------------------- + + private static boolean isDpsMarker(final String trimmedLine) { + return trimmedLine.equals(BEGIN_ENTRIES) + || trimmedLine.equals(END_ENTRIES) + || trimmedLine.equals(BEGIN_COMMENTS) + || trimmedLine.equals(END_COMMENTS); + } + + private static boolean isInlineDpsEntry(final String line) { + return line.contains(DPS_ENTRY_SUFFIX); + } + + private static boolean isInlineDpsComment(final String line) { + return line.contains(DPS_COMMENT_SUFFIX); + } + + // ------------------------------------------------------------------------- + // IO / text utils + // ------------------------------------------------------------------------- + + private static String readNormalized(final Path path) { + return normalizeNewlines(readFileOrEmpty(path)); + } + + private static String readFileOrEmpty(final Path path) { + try { + if (!Files.exists(path)) { + return ""; + } + return Files.readString(path); + } catch (final IOException e) { + throw new UncheckedIOException("Failed to read file: " + path, e); + } + } + + private static void writeString(final Path path, final String content) { + try { + Files.writeString(path, content); + } catch (final IOException e) { + throw new UncheckedIOException("Failed to write file: " + path, e); + } + } + + private static String normalizeNewlines(final String s) { + return s.replace("\r\n", LINE_BREAK) + .replace("\r", LINE_BREAK); + } + + private static List splitLines(final String normalizedContent) { + if (normalizedContent.isEmpty()) { + return List.of(); + } + return List.of(normalizedContent.split(LINE_BREAK, -1)); + } + + private static String joinLines(final List lines) { + if (lines.isEmpty()) { + return ""; + } + final var out = new StringBuilder(); + for (int i = 0; i < lines.size(); i++) { + out.append(lines.get(i)); + if (i + 1 < lines.size()) { + out.append(LINE_BREAK); + } + } + return out.toString(); + } + + private static void trimLeadingBlankLines(final List lines) { + while (!lines.isEmpty() && lines.getFirst() + .isBlank()) { + lines.removeFirst(); + } + } + + private static void trimTrailingBlankLines(final List lines) { + while (!lines.isEmpty() && lines.getLast() + .isBlank()) { + lines.removeLast(); + } + } + + private record DnsAddress(String address) {} + + private record CleanedContent(List originalLines, List inlineCommentCandidates) {} +} diff --git a/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/ResolvedConfigurator.java b/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/ResolvedConfigurator.java index 94914a3c2..03e09b27d 100644 --- a/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/ResolvedConfigurator.java +++ b/src/main/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/ResolvedConfigurator.java @@ -20,9 +20,9 @@ public static void configure(Path confFile, IpAddr addr) { private static String formatAddr(IpAddr addr) { if (Dns.isDefaultPortOrNull(addr)) { - return addr.getRawIP(); + return addr.getIpAsText(); } - return String.format("%s:%s", addr.getRawIP(), addr.getPort()); + return String.format("%s:%s", addr.getIpAsText(), addr.getPort()); } public static void restore(Path confFile) { diff --git a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/mapper/ResolverMapper.java b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/mapper/ResolverMapper.java index 43086b92e..be6e42ebb 100644 --- a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/mapper/ResolverMapper.java +++ b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/mapper/ResolverMapper.java @@ -34,6 +34,6 @@ public static ResolverStats toResolverStats(Resolver resolver, CircuitStatus sta } public static InetSocketAddress toInetSocketAddress(IpAddr addr) { - return InetAddresses.toSocketAddress(addr.getRawIP(), addr.getPortOrDef(53)); + return InetAddresses.toSocketAddress(addr.getIpAsText(), addr.getPortOrDef(53)); } } diff --git a/src/main/java/com/mageddo/net/IpAddr.java b/src/main/java/com/mageddo/net/IpAddr.java index 76d9339bb..6ba721808 100644 --- a/src/main/java/com/mageddo/net/IpAddr.java +++ b/src/main/java/com/mageddo/net/IpAddr.java @@ -29,12 +29,12 @@ public int getPortOrDef(int def) { @Override public String toString() { if (this.port == null) { - return this.getRawIP(); + return this.getIpAsText(); } return String.format("%s:%d", this.ip, this.port); } - public String getRawIP() { + public String getIpAsText() { return this.ip.toText(); } diff --git a/src/main/java/com/mageddo/net/NetExecutorWatchdog.java b/src/main/java/com/mageddo/net/NetExecutorWatchdog.java index f9fe5568e..6bfca7012 100644 --- a/src/main/java/com/mageddo/net/NetExecutorWatchdog.java +++ b/src/main/java/com/mageddo/net/NetExecutorWatchdog.java @@ -28,7 +28,7 @@ public CompletableFuture watch(IpAddr pingAddr, CompletableFuture futu int pingTimeoutInMs) { final var pingFuture = this.threadPool.submit( - () -> Networks.ping(pingAddr.getRawIP(), pingAddr.getPort(), pingTimeoutInMs) + () -> Networks.ping(pingAddr.getIpAsText(), pingAddr.getPort(), pingTimeoutInMs) ); boolean mustCheckPing = true; diff --git a/src/test/java/com/mageddo/dnsproxyserver/dnsconfigurator/DnsConfiguratorDefaultTest.java b/src/test/java/com/mageddo/dnsproxyserver/dnsconfigurator/DnsConfiguratorDefaultTest.java index d87ead0bc..50a93bfdc 100644 --- a/src/test/java/com/mageddo/dnsproxyserver/dnsconfigurator/DnsConfiguratorDefaultTest.java +++ b/src/test/java/com/mageddo/dnsproxyserver/dnsconfigurator/DnsConfiguratorDefaultTest.java @@ -60,7 +60,7 @@ void mustConfigureDNSServer() { this.configurator.configure(addr); // assert - verify(this.configurator).updateDnsServers(eq(network), eq(singletonList(addr.getRawIP()))); + verify(this.configurator).updateDnsServers(eq(network), eq(singletonList(addr.getIpAsText()))); assertEquals("{WI-FI=[8.8.8.8]}", this.configurator.getServersBefore() .toString() ); diff --git a/src/test/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/DnsConfiguratorLinuxTest.java b/src/test/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/DnsConfiguratorLinuxTest.java index 999bde8e4..3f4febedb 100644 --- a/src/test/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/DnsConfiguratorLinuxTest.java +++ b/src/test/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/DnsConfiguratorLinuxTest.java @@ -46,9 +46,10 @@ void mustConfigureDpsServerOnEmptyFileAsResolvconf(@TempDir Path tmpDir) throws this.configurator.configure(ip); // assert - assertEquals( - """ - nameserver 10.10.0.1 # dps-entry + assertEquals(""" + # BEGIN dps-entries + nameserver 10.10.0.1 + # END dps-entries """, Files.readString(resolvFile) ); diff --git a/src/test/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/ResolvconfConfiguratorV2Test.java b/src/test/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/ResolvconfConfiguratorV2Test.java new file mode 100644 index 000000000..309279be7 --- /dev/null +++ b/src/test/java/com/mageddo/dnsproxyserver/dnsconfigurator/linux/ResolvconfConfiguratorV2Test.java @@ -0,0 +1,264 @@ +package com.mageddo.dnsproxyserver.dnsconfigurator.linux; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import testing.templates.IpAddrTemplates; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ResolvconfConfiguratorV2Test { + + @Test + void mustConfigureDpsServerOnEmptyFile(@TempDir Path tmpDir) throws Exception { + + // arrrange + final var resolvFile = Files.createTempFile(tmpDir, "resolv", ".conf"); + + // act + ResolvconfConfiguratorV2.process(resolvFile, IpAddrTemplates.local()); + + // assert + assertEquals(""" + # BEGIN dps-entries + nameserver 10.10.0.1 + # END dps-entries + """, + Files.readString(resolvFile) + ); + + } + + + @Test + void mustCleanUpDpsCommentsAndEntriesBeforeApply(@TempDir Path tmpDir) throws Exception { + + // arrrange + final var resolvFile = tmpDir.resolve("resolv.conf"); + final var ip = IpAddrTemplates.local(); + Files.writeString(resolvFile, """ + # BEGIN dps-entries + nameserver 10.10.0.6 + # END dps-entries + + # BEGIN dps-comments + # nameserver 5.5.5.5 + # END dps-comments + + nameserver 5.5.5.5 # dps-entry + # nameserver 8.8.8.8 # dps-comment + # nameserver 8.8.4.4 # dps-comment + + nameserver 8.8.8.8 + """); + + // act + ResolvconfConfiguratorV2.process(resolvFile, ip); + + // assert + assertEquals( + """ + # BEGIN dps-entries + nameserver 10.10.0.1 + # END dps-entries + + # BEGIN dps-comments + # nameserver 8.8.8.8 + # nameserver 8.8.4.4 + # END dps-comments + """, + Files.readString(resolvFile) + ); + + } + + @Test + void mustCommentExistingServerAndSetupPassedConf(@TempDir Path tmpDir) throws Exception { + + // arrrange + final var resolvFile = tmpDir.resolve("resolv.conf"); + final var ip = IpAddrTemplates.local(); + Files.writeString(resolvFile, "nameserver 8.8.8.8"); + + // act + ResolvconfConfiguratorV2.process(resolvFile, ip); + + // assert + assertEquals( + """ + # BEGIN dps-entries + nameserver 10.10.0.1 + # END dps-entries + + # BEGIN dps-comments + # nameserver 8.8.8.8 + # END dps-comments + """, + Files.readString(resolvFile) + ); + + } + + + @Test + void mustUseAlreadyExistentDpsServerLine(@TempDir Path tmpDir) throws Exception { + + // arrrange + final var resolvFile = tmpDir.resolve("resolv.conf"); + final var ip = IpAddrTemplates.local(); + Files.writeString(resolvFile, "nameserver 8.8.8.8\nnameserver 4.4.4.4 # dps-entry"); + + // act + ResolvconfConfiguratorV2.process(resolvFile, ip); + // assert + assertEquals( + """ + # BEGIN dps-entries + nameserver 10.10.0.1 + # END dps-entries + + # BEGIN dps-comments + # nameserver 8.8.8.8 + # END dps-comments + """, + Files.readString(resolvFile) + ); + + } + + @Test + void mustRestoreOriginalResolvconf(@TempDir Path tmpDir) throws Exception { + + // arrange + final var resolvFile = tmpDir.resolve("resolv.conf"); + + Files.writeString(resolvFile, """ + # Provided by test + # nameserver 7.7.7.7 + # nameserver 8.8.8.8 # dps-comment + nameserver 9.9.9.9 # dps-entry + + # BEGIN dps-entries + nameserver 10.10.0.1 + # END dps-entries + + # BEGIN dps-comments + # nameserver 8.8.4.4 + # END dps-comments + """ + ); + + // act + ResolvconfConfiguratorV2.restore(resolvFile); + + // assert + assertEquals( + """ + # Provided by test + # nameserver 7.7.7.7 + nameserver 8.8.8.8 + nameserver 8.8.4.4 + """, + Files.readString(resolvFile) + ); + + } + + @Test + void wontConfigurePortsDifferentFrom53(@TempDir Path tmpDir) throws Exception { + + // arrrange + final var addr = IpAddrTemplates.localPort54(); + final var resolvFile = tmpDir.resolve("resolv.conf"); + + // act + final var ex = assertThrows(IllegalArgumentException.class, () -> { + ResolvconfConfiguratorV2.process(resolvFile, addr); + } + ); + + // assert + final var msg = ex.getMessage(); + assertTrue(msg.contains("requires dns server port to"), msg); + + } + + @Test + void mustNotCommentFollowingNameServersWhenNameserversOverrideIsDisabled(@TempDir Path tmpDir) + throws Exception { + + // arrange + final var resolvFile = tmpDir.resolve("resolv.conf"); + final var ip = IpAddrTemplates.local(); + + Files.writeString(resolvFile, """ + # Provided by test + nameserver 7.7.7.7 + # nameserver 8.8.8.8 + nameserver 8.8.4.4 + """ + ); + + // act + ResolvconfConfiguratorV2.process(resolvFile, ip, false); + + // assert + assertEquals( + """ + # Provided by test + + # BEGIN dps-entries + nameserver 10.10.0.1 + # END dps-entries + + nameserver 7.7.7.7 + # nameserver 8.8.8.8 + nameserver 8.8.4.4 + """, + Files.readString(resolvFile) + ); + } + + + @Test + void mustCreateExactlyOneDpsEntryEvenWhenCalledTwice(@TempDir Path tmpDir) + throws Exception { + + // arrange + final var resolvFile = tmpDir.resolve("resolv.conf"); + final var ip = IpAddrTemplates.local(); + + Files.writeString(resolvFile, """ + # Provided by test + nameserver 7.7.7.7 + # nameserver 8.8.8.8 + nameserver 8.8.4.4 + """ + ); + + // act + ResolvconfConfiguratorV2.process(resolvFile, ip, false); + ResolvconfConfiguratorV2.process(resolvFile, ip, false); + + // assert + assertEquals( + """ + # Provided by test + + # BEGIN dps-entries + nameserver 10.10.0.1 + # END dps-entries + + nameserver 7.7.7.7 + # nameserver 8.8.8.8 + nameserver 8.8.4.4 + """, + Files.readString(resolvFile) + ); + } +}