fix(nests): connect over a resolved IPv4 address while keeping hostname SNI

After switching the dev harness to `localhost`, the QUIC handshake still
stalled and the relay logged nothing at all — packets weren't reaching
it. `localhost` resolves to ::1 first on most systems, and IPv6 loopback
isn't reliably routable (Docker's IPv6 UDP port-forwarding silently
blackholes), so the UDP socket was connecting into a hole.

QuicWebTransportFactory.connect now resolves the authority host to a
concrete address preferring IPv4 for the UDP socket target, while still
passing the original hostname as the TLS SNI server name — so SNI keeps
matching the server certificate and stays a legal RFC 6066 host_name.

https://claude.ai/code/session_01PoQupfdoKU3ryQwLwTeXeM
This commit is contained in:
Claude
2026-05-14 21:51:35 +00:00
parent ed1f10c071
commit 6a60918cc6
@@ -112,7 +112,15 @@ class QuicWebTransportFactory(
bearerToken: String?,
): WebTransportSession {
val (host, port) = splitAuthority(authority)
val socket = UdpSocket.connect(host, port)
// Decouple the socket target from the TLS SNI name. The socket must
// reach a routable address — `localhost` commonly resolves to ::1
// first, and IPv6 loopback is not always routable (e.g. Docker's
// IPv6 UDP port-forwarding silently blackholes), so prefer an IPv4
// address for the actual connection. SNI, however, must stay the
// original hostname so it (a) matches the server's certificate and
// (b) is a legal RFC 6066 host_name rather than an IP literal.
val socketHost = resolvePreferIpv4(host)
val socket = UdpSocket.connect(socketHost, port)
val conn =
QuicConnection(
serverName = host,
@@ -373,6 +381,25 @@ class QuicWebTransportFactory(
return items.joinToString(", ") { "\"$it\"" }
}
/**
* Resolve [host] to a concrete address string, preferring IPv4. Returns
* an IPv4 literal when one exists, otherwise the first resolved address,
* otherwise [host] unchanged (already an IP literal, or resolution
* failed — let the socket layer surface the error). The returned IP
* literal is only used as the UDP socket target; the caller still passes
* the original [host] as the TLS SNI server name.
*/
private fun resolvePreferIpv4(host: String): String =
try {
val addrs = java.net.InetAddress.getAllByName(host)
(
addrs.firstOrNull { it is java.net.Inet4Address }
?: addrs.firstOrNull()
)?.hostAddress ?: host
} catch (_: java.net.UnknownHostException) {
host
}
private fun splitAuthority(authority: String): Pair<String, Int> {
val idx = authority.lastIndexOf(':')
if (idx <= 0) return authority to 443