From c36c9ccf43f62cbe6ee6902b499142ea7e7518cd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 13:37:02 +0000 Subject: [PATCH] fix(dns): treat trailing-dot FQDN form of localhost as loopback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RFC 1034: `localhost.` and `localhost` are the same name — the trailing dot just marks the FQDN form. Without this, an upstream answer of 127.0.0.1 for `localhost.` (or `relay.localhost.`) would get filtered as poison, breaking user-configured local relays addressed in FQDN form. --- .../vitorpamplona/amethyst/service/okhttp/SurgeDns.kt | 6 ++++-- .../amethyst/service/okhttp/SurgeDnsTest.kt | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/SurgeDns.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/SurgeDns.kt index 5e442efee4..126c3587e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/SurgeDns.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/SurgeDns.kt @@ -296,8 +296,10 @@ class SurgeDns( * already lowercased. */ private fun isLoopbackHostname(host: String): Boolean { - if (host == "localhost" || host.endsWith(".localhost")) return true - val literal = parseIpLiteral(host) ?: return false + // RFC 1034: a trailing dot is the FQDN form (e.g. `localhost.`), still the same name. + val name = host.trimEnd('.') + if (name == "localhost" || name.endsWith(".localhost")) return true + val literal = parseIpLiteral(name) ?: return false return literal.isLoopbackAddress || literal.isAnyLocalAddress } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/okhttp/SurgeDnsTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/okhttp/SurgeDnsTest.kt index 692116756a..d2578126b9 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/okhttp/SurgeDnsTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/okhttp/SurgeDnsTest.kt @@ -540,6 +540,15 @@ class SurgeDnsTest { assertEquals(listOf(ip("127.0.0.1")), dns.lookup("relay.localhost")) } + @Test + fun `trailing-dot FQDN form of localhost keeps loopback answers`() { + // RFC 1034: `localhost.` is the same name as `localhost`, just in FQDN form. + val upstream = CountingDns(mapOf("localhost." to listOf(ip("127.0.0.1")))) + val dns = SurgeDns(delegate = upstream) + + assertEquals(listOf(ip("127.0.0.1")), dns.lookup("localhost.")) + } + @Test fun `ipv4 loopback literal keeps loopback answers`() { val upstream = CountingDns(mapOf("127.0.0.1" to listOf(ip("127.0.0.1"))))