fix(net): prefer IPv4 over IPv6 for OkHttp + QUIC dial
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.
This commit is contained in:
@@ -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<InetAddress> {
|
||||||
|
val all = Dns.SYSTEM.lookup(hostname)
|
||||||
|
if (all.size <= 1) return all
|
||||||
|
val v4 = all.filterIsInstance<Inet4Address>()
|
||||||
|
if (v4.isEmpty() || v4.size == all.size) return all
|
||||||
|
return v4 + all.filterNot { it is Inet4Address }
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -63,6 +63,7 @@ class OkHttpClientFactory(
|
|||||||
.Builder()
|
.Builder()
|
||||||
.dispatcher(dispatcher)
|
.dispatcher(dispatcher)
|
||||||
.connectionPool(connectionPool)
|
.connectionPool(connectionPool)
|
||||||
|
.dns(Ipv4FirstDns())
|
||||||
.eventListenerFactory(MediaCallEventListenerFactory(dispatcher, connectionPool))
|
.eventListenerFactory(MediaCallEventListenerFactory(dispatcher, connectionPool))
|
||||||
.followRedirects(true)
|
.followRedirects(true)
|
||||||
.followSslRedirects(true)
|
.followSslRedirects(true)
|
||||||
|
|||||||
+1
@@ -55,6 +55,7 @@ class OkHttpClientFactoryForRelays(
|
|||||||
OkHttpClient
|
OkHttpClient
|
||||||
.Builder()
|
.Builder()
|
||||||
.dispatcher(myDispatcher)
|
.dispatcher(myDispatcher)
|
||||||
|
.dns(Ipv4FirstDns())
|
||||||
.followRedirects(true)
|
.followRedirects(true)
|
||||||
.followSslRedirects(true)
|
.followSslRedirects(true)
|
||||||
.addInterceptor(DefaultContentTypeInterceptor(userAgent))
|
.addInterceptor(DefaultContentTypeInterceptor(userAgent))
|
||||||
|
|||||||
@@ -92,7 +92,17 @@ actual class UdpSocket private constructor(
|
|||||||
port: Int,
|
port: Int,
|
||||||
): UdpSocket =
|
): UdpSocket =
|
||||||
withContext(Dispatchers.IO) {
|
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 remote = InetSocketAddress(address, port)
|
||||||
val channel = DatagramChannel.open()
|
val channel = DatagramChannel.open()
|
||||||
channel.configureBlocking(true)
|
channel.configureBlocking(true)
|
||||||
|
|||||||
Reference in New Issue
Block a user