From 412b2a285c1579ce17790a8ce3ac42dcfd1ca873 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Apr 2026 20:10:35 +0000 Subject: [PATCH 1/3] fix(nests): point default moq.nostrnests.com URLs at port 4443 The public nostrnests deployment serves moq-auth + moq-relay on :4443; :443 is not the live endpoint. With the old defaults a freshly-created room could never mint a JWT, so the room screen parked on "Reconnecting" forever and the room never showed up in the nostrnests UI. --- .../amethyst/ui/actions/nestsServers/NestsServersScreen.kt | 2 +- .../ui/screen/loggedIn/nests/create/CreateNestViewModel.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/nestsServers/NestsServersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/nestsServers/NestsServersScreen.kt index 576ea5ddc..669f83a19 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/nestsServers/NestsServersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/nestsServers/NestsServersScreen.kt @@ -250,5 +250,5 @@ private fun NestsServerEntry( */ val DEFAULT_NESTS_SERVERS: List = listOf( - NestsServer(name = "nostrnests.com", baseUrl = "https://moq.nostrnests.com"), + NestsServer(name = "nostrnests.com", baseUrl = "https://moq.nostrnests.com:4443"), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestViewModel.kt index 9b6c0d662..c4c57e46d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestViewModel.kt @@ -356,7 +356,7 @@ class CreateNestViewModel : ViewModel() { * room here. Users can edit either field to point at their own * moq-auth / moq-relay pair. */ - const val DEFAULT_SERVICE_URL: String = "https://moq.nostrnests.com" - const val DEFAULT_ENDPOINT_URL: String = "https://moq.nostrnests.com" + const val DEFAULT_SERVICE_URL: String = "https://moq.nostrnests.com:4443" + const val DEFAULT_ENDPOINT_URL: String = "https://moq.nostrnests.com:4443" } } From 23fab05c8a5d559639de396ab8bf670d59afade5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Apr 2026 20:26:49 +0000 Subject: [PATCH 2/3] fix(net): prefer IPv4 over IPv6 for OkHttp + QUIC dial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Android emulator (and a number of dual-stack networks where the IPv6 path is broken) advertises working v6 connectivity but silently drops outbound packets. With the JDK / RFC 6724 default of AAAA-first, any host that publishes a stale or unreachable AAAA record — e.g. moq.nostrnests.com, whose Linode v6 currently doesn't accept connections — causes every HTTPS call and every QUIC handshake to fast-fail with ConnectException, parking the Nests room screen on "Reconnecting" indefinitely. Plug a small Dns wrapper into both OkHttp factories that puts Inet4Address entries ahead of v6 in the resolved list (v6 stays as fallback for genuinely v6-only hosts), and switch UdpSocket.connect from getByName to getAllByName + IPv4-first selection so the QUIC audio path makes the same choice as the HTTPS auth POST. --- .../amethyst/service/okhttp/Ipv4FirstDns.kt | 52 +++++++++++++++++++ .../service/okhttp/OkHttpClientFactory.kt | 1 + .../okhttp/OkHttpClientFactoryForRelays.kt | 1 + .../vitorpamplona/quic/transport/UdpSocket.kt | 12 ++++- 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/Ipv4FirstDns.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/Ipv4FirstDns.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/Ipv4FirstDns.kt new file mode 100644 index 000000000..074589096 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/Ipv4FirstDns.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.okhttp + +import okhttp3.Dns +import java.net.Inet4Address +import java.net.InetAddress + +/** + * System-DNS wrapper that puts IPv4 addresses ahead of IPv6 in the + * resolved list. OkHttp's [okhttp3.internal.connection.RouteSelector] + * iterates routes in order, so v4 is tried first; v6 is still kept + * as a fallback for genuinely v6-only hosts. + * + * Mitigates two real-world cases that surface as a "Reconnecting" + * loop on the Nests auth POST (and on plain media loads): + * - Android emulator: the emulator's userspace networking advertises + * IPv6 connectivity but its NAT consistently drops outbound v6 + * packets, so any host with an AAAA record times out / fast-fails. + * - Hosts whose AAAA record points at a stale or otherwise + * unreachable address while v4 still works (e.g. Linode VMs that + * lost v6 routing). System resolver may return AAAA-only on some + * networks; we can't recover that case here, but we can stop + * v6-first from breaking the dual-stack case. + */ +class Ipv4FirstDns : Dns { + override fun lookup(hostname: String): List { + val all = Dns.SYSTEM.lookup(hostname) + if (all.size <= 1) return all + val v4 = all.filterIsInstance() + if (v4.isEmpty() || v4.size == all.size) return all + return v4 + all.filterNot { it is Inet4Address } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt index 61f3255f6..3567dcbd9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt @@ -63,6 +63,7 @@ class OkHttpClientFactory( .Builder() .dispatcher(dispatcher) .connectionPool(connectionPool) + .dns(Ipv4FirstDns()) .eventListenerFactory(MediaCallEventListenerFactory(dispatcher, connectionPool)) .followRedirects(true) .followSslRedirects(true) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt index e4b81b31f..aba24457f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt @@ -55,6 +55,7 @@ class OkHttpClientFactoryForRelays( OkHttpClient .Builder() .dispatcher(myDispatcher) + .dns(Ipv4FirstDns()) .followRedirects(true) .followSslRedirects(true) .addInterceptor(DefaultContentTypeInterceptor(userAgent)) diff --git a/quic/src/jvmAndroid/kotlin/com/vitorpamplona/quic/transport/UdpSocket.kt b/quic/src/jvmAndroid/kotlin/com/vitorpamplona/quic/transport/UdpSocket.kt index fdd6420d5..cefa1bf78 100644 --- a/quic/src/jvmAndroid/kotlin/com/vitorpamplona/quic/transport/UdpSocket.kt +++ b/quic/src/jvmAndroid/kotlin/com/vitorpamplona/quic/transport/UdpSocket.kt @@ -92,7 +92,17 @@ actual class UdpSocket private constructor( port: Int, ): UdpSocket = withContext(Dispatchers.IO) { - val address = InetAddress.getByName(host) + // Prefer IPv4 over IPv6 when both are present. RFC 6724 / the JDK + // default put AAAA first, but on the Android emulator and many + // dual-stack networks the IPv6 path silently drops packets, so a + // QUIC handshake against the AAAA times out indefinitely while + // the A would succeed. Fall back to whatever the resolver gave + // when no IPv4 entry exists (genuinely v6-only host). + val all = InetAddress.getAllByName(host) + val address = + all.firstOrNull { it is java.net.Inet4Address } + ?: all.firstOrNull() + ?: throw java.net.UnknownHostException(host) val remote = InetSocketAddress(address, port) val channel = DatagramChannel.open() channel.configureBlocking(true) From 882a7b55011f733116b56768f1fb5e81f2ab4af8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Apr 2026 21:56:30 +0000 Subject: [PATCH 3/3] Revert "fix(net): prefer IPv4 over IPv6 for OkHttp + QUIC dial" This reverts commit 23fab05c8a5d559639de396ab8bf670d59afade5. --- .../amethyst/service/okhttp/Ipv4FirstDns.kt | 52 ------------------- .../service/okhttp/OkHttpClientFactory.kt | 1 - .../okhttp/OkHttpClientFactoryForRelays.kt | 1 - .../vitorpamplona/quic/transport/UdpSocket.kt | 12 +---- 4 files changed, 1 insertion(+), 65 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/Ipv4FirstDns.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/Ipv4FirstDns.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/Ipv4FirstDns.kt deleted file mode 100644 index 074589096..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/Ipv4FirstDns.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.service.okhttp - -import okhttp3.Dns -import java.net.Inet4Address -import java.net.InetAddress - -/** - * System-DNS wrapper that puts IPv4 addresses ahead of IPv6 in the - * resolved list. OkHttp's [okhttp3.internal.connection.RouteSelector] - * iterates routes in order, so v4 is tried first; v6 is still kept - * as a fallback for genuinely v6-only hosts. - * - * Mitigates two real-world cases that surface as a "Reconnecting" - * loop on the Nests auth POST (and on plain media loads): - * - Android emulator: the emulator's userspace networking advertises - * IPv6 connectivity but its NAT consistently drops outbound v6 - * packets, so any host with an AAAA record times out / fast-fails. - * - Hosts whose AAAA record points at a stale or otherwise - * unreachable address while v4 still works (e.g. Linode VMs that - * lost v6 routing). System resolver may return AAAA-only on some - * networks; we can't recover that case here, but we can stop - * v6-first from breaking the dual-stack case. - */ -class Ipv4FirstDns : Dns { - override fun lookup(hostname: String): List { - val all = Dns.SYSTEM.lookup(hostname) - if (all.size <= 1) return all - val v4 = all.filterIsInstance() - if (v4.isEmpty() || v4.size == all.size) return all - return v4 + all.filterNot { it is Inet4Address } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt index 3567dcbd9..61f3255f6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt @@ -63,7 +63,6 @@ class OkHttpClientFactory( .Builder() .dispatcher(dispatcher) .connectionPool(connectionPool) - .dns(Ipv4FirstDns()) .eventListenerFactory(MediaCallEventListenerFactory(dispatcher, connectionPool)) .followRedirects(true) .followSslRedirects(true) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt index aba24457f..e4b81b31f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt @@ -55,7 +55,6 @@ class OkHttpClientFactoryForRelays( OkHttpClient .Builder() .dispatcher(myDispatcher) - .dns(Ipv4FirstDns()) .followRedirects(true) .followSslRedirects(true) .addInterceptor(DefaultContentTypeInterceptor(userAgent)) diff --git a/quic/src/jvmAndroid/kotlin/com/vitorpamplona/quic/transport/UdpSocket.kt b/quic/src/jvmAndroid/kotlin/com/vitorpamplona/quic/transport/UdpSocket.kt index cefa1bf78..fdd6420d5 100644 --- a/quic/src/jvmAndroid/kotlin/com/vitorpamplona/quic/transport/UdpSocket.kt +++ b/quic/src/jvmAndroid/kotlin/com/vitorpamplona/quic/transport/UdpSocket.kt @@ -92,17 +92,7 @@ actual class UdpSocket private constructor( port: Int, ): UdpSocket = withContext(Dispatchers.IO) { - // Prefer IPv4 over IPv6 when both are present. RFC 6724 / the JDK - // default put AAAA first, but on the Android emulator and many - // dual-stack networks the IPv6 path silently drops packets, so a - // QUIC handshake against the AAAA times out indefinitely while - // the A would succeed. Fall back to whatever the resolver gave - // when no IPv4 entry exists (genuinely v6-only host). - val all = InetAddress.getAllByName(host) - val address = - all.firstOrNull { it is java.net.Inet4Address } - ?: all.firstOrNull() - ?: throw java.net.UnknownHostException(host) + val address = InetAddress.getByName(host) val remote = InetSocketAddress(address, port) val channel = DatagramChannel.open() channel.configureBlocking(true)