From acfe815e1f77c2c42cf6d20b4723fcb69449650d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 00:10:19 +0000 Subject: [PATCH] fix(quic-interop): offer both h3 + hq-interop ALPNs, dispatch by negotiated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit (e5bbf8509) switched ALPN per testcase based on quic-interop-runner convention (h3 for http3/multiplexing, hq-interop otherwise). That broke picoquic, which had been 4/4 green: picoquic-qns strictly registers h3 ALPN and rejects hq-interop. Different servers disagree on which ALPN they configure for the same testcase: - quic-go-qns — strictly hq-interop for non-http3 - aioquic-qns — accepts either - picoquic-qns — strictly h3 for all testcases There's no per-testcase convention all peers honor. Right move is the TLS-spec-supported one: offer BOTH h3 and hq-interop in the ClientHello, let the server pick whichever matches its config, then dispatch the GET client by `tls.negotiatedAlpn` after the handshake completes. http3 and multiplexing still restrict to h3 only since they exercise H3 framing. Brings picoquic back to fully green and should unblock quic-go for the non-http3 testcases. https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT --- .../quic/interop/runner/InteropClient.kt | 51 +++++++++++++------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/quic/interop/src/main/kotlin/com/vitorpamplona/quic/interop/runner/InteropClient.kt b/quic/interop/src/main/kotlin/com/vitorpamplona/quic/interop/runner/InteropClient.kt index 04c42f415..ad21a95a7 100644 --- a/quic/interop/src/main/kotlin/com/vitorpamplona/quic/interop/runner/InteropClient.kt +++ b/quic/interop/src/main/kotlin/com/vitorpamplona/quic/interop/runner/InteropClient.kt @@ -89,16 +89,20 @@ fun main() { else -> null } - // ALPN per quic-interop-runner convention. Most testcases use - // `hq-interop` (HTTP/0.9 over QUIC, no H3/QPACK). Only the `http3` and - // `multiplexing` testcases use `h3`. quic-go-qns enforces this strictly - // (CRYPTO_ERROR 0x178 / TLS no_application_protocol on mismatch); - // aioquic and picoquic accept either. Match the per-test convention - // so we work everywhere. - val alpn = + // ALPN selection. Different servers configure different ALPNs PER + // testcase, with no consistent convention: + // - quic-go-qns — strictly hq-interop for non-http3 tests + // - aioquic-qns — accepts either + // - picoquic-qns — strictly h3 for ALL testcases + // + // Solution: offer BOTH `h3` and `hq-interop` in the ClientHello. TLS + // ALPN negotiation lets the server pick whichever matches its config. + // For testcases that REQUIRE H3 framing (http3, multiplexing) we + // restrict to h3 so any server that picks hq-interop fails fast. + val offeredAlpns = when (testcase) { - "http3", "multiplexing" -> Alpn.H3 - else -> Alpn.HQ_INTEROP + "http3", "multiplexing" -> listOf(Alpn.H3) + else -> listOf(Alpn.HQ_INTEROP, Alpn.H3) } val code = @@ -133,7 +137,7 @@ fun main() { requests = requests, downloadsDir = downloadsDir, cipherSuites = cipherSuites, - alpn = alpn, + offeredAlpns = offeredAlpns, keyLogPath = keyLogPath, parallel = (testcase == "multiplexing"), ) @@ -163,7 +167,7 @@ private fun runTransferTest( requests: String, downloadsDir: File, cipherSuites: IntArray?, - alpn: Alpn, + offeredAlpns: List, keyLogPath: String?, parallel: Boolean, ): Int { @@ -199,7 +203,7 @@ private fun runTransferTest( serverName = host, config = QuicConnectionConfig(), tlsCertificateValidator = PermissiveCertificateValidator(), - alpnList = listOf(alpn.wireBytes), + alpnList = offeredAlpns.map { it.wireBytes }, cipherSuites = cipherSuites ?: intArrayOf( @@ -221,10 +225,27 @@ private fun runTransferTest( return@runBlocking "handshake_failed" } + // Dispatch the GET client by what the server actually picked. + // If the server didn't pick or picked something unrecognized, + // fall back to HQ-interop (simpler, more permissive). + val negotiated = + conn.tls.negotiatedAlpn + ?.decodeToString() + .orEmpty() val client: GetClient = - when (alpn) { - Alpn.H3 -> Http3GetClient(conn).also { it.init() } - Alpn.HQ_INTEROP -> HqInteropGetClient(conn) + when (negotiated) { + "h3" -> { + Http3GetClient(conn).also { it.init() } + } + + "hq-interop" -> { + HqInteropGetClient(conn) + } + + else -> { + System.err.println("unrecognized negotiated ALPN '$negotiated'; defaulting to hq-interop") + HqInteropGetClient(conn) + } } val authority = if (port == 443) host else "$host:$port"