fix(quic-interop): handshake/chacha20 testcases must transfer the file
Two coupled bugs in our endpoint that the runner just surfaced: 1. The runner mounts \$CLIENT_DOWNLOADS to /downloads as a Docker volume (per quic-interop-runner's docker-compose.yml `client.volumes`). There is no DOWNLOADS env var. Our endpoint was reading \$DOWNLOADS, getting null, and (for handshake/chacha20) skipping any download path. Hard-code /downloads. 2. The handshake / chacha20 / handshakeloss testcases don't just verify the handshake completes — they also require the requested file at /downloads/<basename>. The runner's _check_files validator reported "Missing files: ['intense-tremendous-firefighter']" while our client side reported `handshake ok`. Route handshake-flavor testcases through runTransferTest so the full H3 GET pipeline runs. `chacha20` keeps the cipher-suite override (ChaCha20-only ClientHello); `handshakeloss` reuses the same H3 flow against a lossy sim. Also drops the now-dead runHandshakeTest + parseFirstTarget helpers. https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT
This commit is contained in:
+33
-111
@@ -66,13 +66,16 @@ fun main() {
|
|||||||
|
|
||||||
val testcase = System.getenv("TESTCASE")?.trim().orEmpty()
|
val testcase = System.getenv("TESTCASE")?.trim().orEmpty()
|
||||||
val requests = System.getenv("REQUESTS")?.trim().orEmpty()
|
val requests = System.getenv("REQUESTS")?.trim().orEmpty()
|
||||||
val downloads = System.getenv("DOWNLOADS")?.takeIf { it.isNotBlank() }
|
// The runner mounts $CLIENT_DOWNLOADS to /downloads as a Docker volume
|
||||||
|
// (see quic-interop-runner's docker-compose.yml `client.volumes`). It
|
||||||
|
// does NOT export a DOWNLOADS env var. Hard-code the mount path.
|
||||||
|
val downloadsDir = File("/downloads")
|
||||||
val keyLogPath = System.getenv("SSLKEYLOGFILE")?.takeIf { it.isNotBlank() }
|
val keyLogPath = System.getenv("SSLKEYLOGFILE")?.takeIf { it.isNotBlank() }
|
||||||
|
|
||||||
System.err.println("== quic-interop client ==")
|
System.err.println("== quic-interop client ==")
|
||||||
System.err.println("testcase: $testcase")
|
System.err.println("testcase: $testcase")
|
||||||
System.err.println("requests: $requests")
|
System.err.println("requests: $requests")
|
||||||
System.err.println("downloads: ${downloads ?: "(unset)"}")
|
System.err.println("downloads dir: ${downloadsDir.absolutePath} (exists=${downloadsDir.isDirectory})")
|
||||||
System.err.println("sslkeylogfile: ${keyLogPath ?: "(unset)"}")
|
System.err.println("sslkeylogfile: ${keyLogPath ?: "(unset)"}")
|
||||||
|
|
||||||
val cipherSuites =
|
val cipherSuites =
|
||||||
@@ -83,40 +86,30 @@ fun main() {
|
|||||||
|
|
||||||
val code =
|
val code =
|
||||||
when (testcase) {
|
when (testcase) {
|
||||||
// Both `handshake` and `chacha20` only require the handshake to
|
// The runner's `handshake` / `chacha20` / `handshakeloss` testcases
|
||||||
// complete; `chacha20` adds the constraint that we offered only
|
// require BOTH a successful handshake AND the requested file
|
||||||
// ChaCha20-Poly1305 (verified by the runner via tshark on the
|
// transferred to /downloads — the validator checks file presence
|
||||||
// sim's pcap, decrypted using SSLKEYLOGFILE). `handshakeloss`
|
// via _check_files() AND that exactly 1 handshake happened on the
|
||||||
// is the same client behaviour against a lossy sim.
|
// wire via _count_handshakes(). `chacha20` adds the constraint
|
||||||
"handshake", "chacha20", "handshakeloss" -> {
|
// that we offered only ChaCha20-Poly1305 (verified by the runner
|
||||||
runHandshakeTest(requests, cipherSuites, keyLogPath)
|
// via tshark on the sim's pcap decrypted using SSLKEYLOGFILE).
|
||||||
}
|
// `handshakeloss` is the same client behaviour against a lossy sim.
|
||||||
|
//
|
||||||
// `transfer` and `http3` both fetch every URL in REQUESTS and
|
// `transfer` / `http3` / `multiplexing` and the network-condition
|
||||||
// write each body to $DOWNLOADS/<basename>. `multiplexing`
|
// aliases (transferloss / transfercorruption / longrtt / goodput
|
||||||
// additionally requires the requests to be sent in parallel on
|
// / crosstraffic) run the same H3 GET pipeline; the runner just
|
||||||
// separate streams (the runner verifies via tshark that the
|
// varies the sim configuration around them.
|
||||||
// streams overlap in time). The remaining aliases run the same
|
"handshake", "chacha20", "handshakeloss",
|
||||||
// client logic against different sim configurations:
|
|
||||||
// transferloss — random packet loss
|
|
||||||
// transfercorruption — random bit-flips (recovery via AEAD AUTH FAIL → drop + retransmit)
|
|
||||||
// longrtt — emulated high-latency link
|
|
||||||
// goodput — throughput floor under default sim
|
|
||||||
// crosstraffic — competing UDP flows on the same link
|
|
||||||
"transfer", "http3", "multiplexing",
|
"transfer", "http3", "multiplexing",
|
||||||
"transferloss", "transfercorruption", "longrtt", "goodput", "crosstraffic",
|
"transferloss", "transfercorruption", "longrtt", "goodput", "crosstraffic",
|
||||||
-> {
|
-> {
|
||||||
if (downloads == null) {
|
runTransferTest(
|
||||||
System.err.println("DOWNLOADS env var required for $testcase")
|
requests = requests,
|
||||||
EXIT_FAIL
|
downloadsDir = downloadsDir,
|
||||||
} else {
|
cipherSuites = cipherSuites,
|
||||||
runTransferTest(
|
keyLogPath = keyLogPath,
|
||||||
requests = requests,
|
parallel = (testcase == "multiplexing"),
|
||||||
downloadsDir = File(downloads),
|
)
|
||||||
keyLogPath = keyLogPath,
|
|
||||||
parallel = (testcase == "multiplexing"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
@@ -126,74 +119,10 @@ fun main() {
|
|||||||
exitProcess(code)
|
exitProcess(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun runHandshakeTest(
|
|
||||||
requests: String,
|
|
||||||
cipherSuites: IntArray?,
|
|
||||||
keyLogPath: String?,
|
|
||||||
): Int {
|
|
||||||
val target =
|
|
||||||
parseFirstTarget(requests) ?: run {
|
|
||||||
System.err.println("no parseable target in REQUESTS")
|
|
||||||
return EXIT_FAIL
|
|
||||||
}
|
|
||||||
val (host, port) = target
|
|
||||||
System.err.println("handshake target: $host:$port")
|
|
||||||
|
|
||||||
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
||||||
val outcome =
|
|
||||||
runBlocking {
|
|
||||||
val socket =
|
|
||||||
try {
|
|
||||||
UdpSocket.connect(host, port)
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
return@runBlocking "udp_failed: ${t.message ?: t::class.simpleName}"
|
|
||||||
}
|
|
||||||
val keyLogger = keyLogPath?.let { SslKeyLogger(File(it)) }
|
|
||||||
val conn =
|
|
||||||
QuicConnection(
|
|
||||||
serverName = host,
|
|
||||||
config = QuicConnectionConfig(),
|
|
||||||
tlsCertificateValidator = PermissiveCertificateValidator(),
|
|
||||||
cipherSuites =
|
|
||||||
cipherSuites
|
|
||||||
?: intArrayOf(
|
|
||||||
TlsConstants.CIPHER_TLS_AES_128_GCM_SHA256,
|
|
||||||
TlsConstants.CIPHER_TLS_CHACHA20_POLY1305_SHA256,
|
|
||||||
),
|
|
||||||
extraSecretsListener = keyLogger?.listener,
|
|
||||||
)
|
|
||||||
val driver = QuicConnectionDriver(conn, socket, scope)
|
|
||||||
driver.start()
|
|
||||||
|
|
||||||
val handshake =
|
|
||||||
withTimeoutOrNull(HANDSHAKE_TIMEOUT_SEC * 1_000L) {
|
|
||||||
runCatching { conn.awaitHandshake() }
|
|
||||||
}
|
|
||||||
val result =
|
|
||||||
when {
|
|
||||||
handshake == null -> "timeout"
|
|
||||||
handshake.isSuccess -> "ok"
|
|
||||||
else -> "failed: ${handshake.exceptionOrNull()?.message ?: "?"}"
|
|
||||||
}
|
|
||||||
runCatching { driver.close() }
|
|
||||||
conn.tls.clientRandom?.let { keyLogger?.flush(it) }
|
|
||||||
delay(50)
|
|
||||||
result
|
|
||||||
}
|
|
||||||
scope.cancel()
|
|
||||||
|
|
||||||
return if (outcome == "ok") {
|
|
||||||
System.err.println("handshake ok")
|
|
||||||
EXIT_OK
|
|
||||||
} else {
|
|
||||||
System.err.println("handshake $outcome")
|
|
||||||
EXIT_FAIL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runTransferTest(
|
private fun runTransferTest(
|
||||||
requests: String,
|
requests: String,
|
||||||
downloadsDir: File,
|
downloadsDir: File,
|
||||||
|
cipherSuites: IntArray?,
|
||||||
keyLogPath: String?,
|
keyLogPath: String?,
|
||||||
parallel: Boolean,
|
parallel: Boolean,
|
||||||
): Int {
|
): Int {
|
||||||
@@ -229,6 +158,12 @@ private fun runTransferTest(
|
|||||||
serverName = host,
|
serverName = host,
|
||||||
config = QuicConnectionConfig(),
|
config = QuicConnectionConfig(),
|
||||||
tlsCertificateValidator = PermissiveCertificateValidator(),
|
tlsCertificateValidator = PermissiveCertificateValidator(),
|
||||||
|
cipherSuites =
|
||||||
|
cipherSuites
|
||||||
|
?: intArrayOf(
|
||||||
|
TlsConstants.CIPHER_TLS_AES_128_GCM_SHA256,
|
||||||
|
TlsConstants.CIPHER_TLS_CHACHA20_POLY1305_SHA256,
|
||||||
|
),
|
||||||
extraSecretsListener = keyLogger?.listener,
|
extraSecretsListener = keyLogger?.listener,
|
||||||
)
|
)
|
||||||
val driver = QuicConnectionDriver(conn, socket, scope)
|
val driver = QuicConnectionDriver(conn, socket, scope)
|
||||||
@@ -293,19 +228,6 @@ private fun runTransferTest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseFirstTarget(requests: String): Pair<String, Int>? {
|
|
||||||
val first = requests.split(Regex("\\s+")).firstOrNull { it.isNotBlank() } ?: return null
|
|
||||||
val uri =
|
|
||||||
try {
|
|
||||||
URI(first)
|
|
||||||
} catch (_: Throwable) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val host = uri.host ?: return null
|
|
||||||
val port = uri.port.takeIf { it > 0 } ?: 443
|
|
||||||
return host to port
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes [NSS Key Log Format](https://firefox-source-docs.mozilla.org/security/nss/legacy/key_log_format/index.html)
|
* Writes [NSS Key Log Format](https://firefox-source-docs.mozilla.org/security/nss/legacy/key_log_format/index.html)
|
||||||
* lines so Wireshark can decrypt the sim's captured pcap.
|
* lines so Wireshark can decrypt the sim's captured pcap.
|
||||||
|
|||||||
Reference in New Issue
Block a user