fix(quic-interop): offer both h3 + hq-interop ALPNs, dispatch by negotiated

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
This commit is contained in:
Claude
2026-05-07 00:10:19 +00:00
parent 038eb18617
commit acfe815e1f
@@ -89,16 +89,20 @@ fun main() {
else -> null else -> null
} }
// ALPN per quic-interop-runner convention. Most testcases use // ALPN selection. Different servers configure different ALPNs PER
// `hq-interop` (HTTP/0.9 over QUIC, no H3/QPACK). Only the `http3` and // testcase, with no consistent convention:
// `multiplexing` testcases use `h3`. quic-go-qns enforces this strictly // - quic-go-qns — strictly hq-interop for non-http3 tests
// (CRYPTO_ERROR 0x178 / TLS no_application_protocol on mismatch); // - aioquic-qns — accepts either
// aioquic and picoquic accept either. Match the per-test convention // - picoquic-qns — strictly h3 for ALL testcases
// so we work everywhere. //
val alpn = // 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) { when (testcase) {
"http3", "multiplexing" -> Alpn.H3 "http3", "multiplexing" -> listOf(Alpn.H3)
else -> Alpn.HQ_INTEROP else -> listOf(Alpn.HQ_INTEROP, Alpn.H3)
} }
val code = val code =
@@ -133,7 +137,7 @@ fun main() {
requests = requests, requests = requests,
downloadsDir = downloadsDir, downloadsDir = downloadsDir,
cipherSuites = cipherSuites, cipherSuites = cipherSuites,
alpn = alpn, offeredAlpns = offeredAlpns,
keyLogPath = keyLogPath, keyLogPath = keyLogPath,
parallel = (testcase == "multiplexing"), parallel = (testcase == "multiplexing"),
) )
@@ -163,7 +167,7 @@ private fun runTransferTest(
requests: String, requests: String,
downloadsDir: File, downloadsDir: File,
cipherSuites: IntArray?, cipherSuites: IntArray?,
alpn: Alpn, offeredAlpns: List<Alpn>,
keyLogPath: String?, keyLogPath: String?,
parallel: Boolean, parallel: Boolean,
): Int { ): Int {
@@ -199,7 +203,7 @@ private fun runTransferTest(
serverName = host, serverName = host,
config = QuicConnectionConfig(), config = QuicConnectionConfig(),
tlsCertificateValidator = PermissiveCertificateValidator(), tlsCertificateValidator = PermissiveCertificateValidator(),
alpnList = listOf(alpn.wireBytes), alpnList = offeredAlpns.map { it.wireBytes },
cipherSuites = cipherSuites =
cipherSuites cipherSuites
?: intArrayOf( ?: intArrayOf(
@@ -221,10 +225,27 @@ private fun runTransferTest(
return@runBlocking "handshake_failed" 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 = val client: GetClient =
when (alpn) { when (negotiated) {
Alpn.H3 -> Http3GetClient(conn).also { it.init() } "h3" -> {
Alpn.HQ_INTEROP -> HqInteropGetClient(conn) 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" val authority = if (port == 443) host else "$host:$port"