feat(quic-interop): wire peer-uni-stream drainer + versionnegotiation
Two integrations from overnight agents: 1. agent C — peer-uni-stream drainer for the H3 multiplexing fix. Http3GetClient.init() now takes a CoroutineScope and calls conn.drainPeerInitiatedUniStreamsIntoBlackHole(scope) to consume + discard the server's three uni streams (control, qpack-encoder, qpack-decoder). RFC 9114 §6.2 mandates the client read these; pre-fix their bytes accumulated in 64-chunk per-stream channels until parser overflow tore down the connection with INTERNAL_ERROR ~4.5s into a multiplexing run. 2. agent A — versionnegotiation testcase wired into dispatch. Adds the testcase to the runTransferTest route and threads QuicVersion.FORCE_VERSION_NEGOTIATION as the initial version when testcase=='versionnegotiation'. The QuicConnection's RFC 9000 §6 VN flow (applyVersionNegotiation) takes over from there, retrying with v1 once the server replies with its supported list. https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT
This commit is contained in:
+13
-1
@@ -22,6 +22,7 @@ package com.vitorpamplona.quic.interop.runner
|
||||
|
||||
import com.vitorpamplona.quic.QuicWriter
|
||||
import com.vitorpamplona.quic.connection.QuicConnection
|
||||
import com.vitorpamplona.quic.connection.drainPeerInitiatedUniStreamsIntoBlackHole
|
||||
import com.vitorpamplona.quic.http3.Http3Frame
|
||||
import com.vitorpamplona.quic.http3.Http3FrameReader
|
||||
import com.vitorpamplona.quic.http3.Http3FrameType
|
||||
@@ -29,6 +30,7 @@ import com.vitorpamplona.quic.http3.Http3Settings
|
||||
import com.vitorpamplona.quic.http3.Http3StreamType
|
||||
import com.vitorpamplona.quic.qpack.QpackDecoder
|
||||
import com.vitorpamplona.quic.qpack.QpackEncoder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.collect
|
||||
|
||||
/** Common shape for the two interop GET clients (HTTP/3 and HQ-interop). */
|
||||
@@ -59,7 +61,7 @@ data class GetResponse(
|
||||
class Http3GetClient(
|
||||
private val conn: QuicConnection,
|
||||
) : GetClient {
|
||||
suspend fun init() {
|
||||
suspend fun init(scope: CoroutineScope) {
|
||||
// Control stream: type-0x00 prefix followed by a SETTINGS frame
|
||||
// (empty body is legal — RFC 9114 §7.2.4).
|
||||
val control = conn.openUniStream()
|
||||
@@ -81,6 +83,16 @@ class Http3GetClient(
|
||||
val w3 = QuicWriter()
|
||||
w3.writeVarint(Http3StreamType.QPACK_DECODER)
|
||||
qpackDec.send.enqueue(w3.toByteArray())
|
||||
|
||||
// RFC 9114 §6.2: the server opens its own three uni streams
|
||||
// (control, qpack encoder, qpack decoder). Their bytes accumulate
|
||||
// in per-stream `incomingChannel`s (capacity 64); without an
|
||||
// active consumer the channel saturates and `:quic` tears down
|
||||
// the connection with INTERNAL_ERROR. We don't actually use the
|
||||
// dynamic table or care about the server's settings, so drain
|
||||
// and discard. Without this, multiplexing testcase fails after
|
||||
// ~4.5s with "consumer overflowed" tear-down.
|
||||
conn.drainPeerInitiatedUniStreamsIntoBlackHole(scope)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+15
-2
@@ -89,6 +89,16 @@ fun main() {
|
||||
else -> null
|
||||
}
|
||||
|
||||
// For the versionnegotiation testcase the runner expects us to send
|
||||
// an Initial advertising a version the server doesn't support, then
|
||||
// honor its VN response by retrying with v1. agent A's
|
||||
// QuicVersion.FORCE_VERSION_NEGOTIATION drives that flow.
|
||||
val initialVersion =
|
||||
when (testcase) {
|
||||
"versionnegotiation" -> com.vitorpamplona.quic.packet.QuicVersion.FORCE_VERSION_NEGOTIATION
|
||||
else -> com.vitorpamplona.quic.packet.QuicVersion.V1
|
||||
}
|
||||
|
||||
// ALPN selection. Different servers configure different ALPNs PER
|
||||
// testcase, with no consistent convention:
|
||||
// - quic-go-qns — strictly hq-interop for non-http3 tests
|
||||
@@ -131,13 +141,14 @@ fun main() {
|
||||
"handshake", "chacha20", "handshakeloss",
|
||||
"transfer", "http3", "multiplexing",
|
||||
"transferloss", "transfercorruption", "longrtt", "goodput", "crosstraffic",
|
||||
"retry", "ipv6",
|
||||
"retry", "ipv6", "versionnegotiation",
|
||||
-> {
|
||||
runTransferTest(
|
||||
requests = requests,
|
||||
downloadsDir = downloadsDir,
|
||||
cipherSuites = cipherSuites,
|
||||
offeredAlpns = offeredAlpns,
|
||||
initialVersion = initialVersion,
|
||||
keyLogPath = keyLogPath,
|
||||
parallel = (testcase == "multiplexing"),
|
||||
)
|
||||
@@ -168,6 +179,7 @@ private fun runTransferTest(
|
||||
downloadsDir: File,
|
||||
cipherSuites: IntArray?,
|
||||
offeredAlpns: List<Alpn>,
|
||||
initialVersion: Int,
|
||||
keyLogPath: String?,
|
||||
parallel: Boolean,
|
||||
): Int {
|
||||
@@ -204,6 +216,7 @@ private fun runTransferTest(
|
||||
config = QuicConnectionConfig(),
|
||||
tlsCertificateValidator = PermissiveCertificateValidator(),
|
||||
alpnList = offeredAlpns.map { it.wireBytes },
|
||||
initialVersion = initialVersion,
|
||||
cipherSuites =
|
||||
cipherSuites
|
||||
?: intArrayOf(
|
||||
@@ -235,7 +248,7 @@ private fun runTransferTest(
|
||||
val client: GetClient =
|
||||
when (negotiated) {
|
||||
"h3" -> {
|
||||
Http3GetClient(conn).also { it.init() }
|
||||
Http3GetClient(conn).also { it.init(scope) }
|
||||
}
|
||||
|
||||
"hq-interop" -> {
|
||||
|
||||
Reference in New Issue
Block a user