feat(quic-interop): scaffold quic-interop-runner endpoint module

Adds :quic-interop, a JVM-only module at quic/interop/ that implements
the quic-interop-runner Docker contract so we can drive matrix tests
against aioquic / quiche / picoquic / msquic / ngtcp2 / etc. as a
bug-finding harness.

Phase 0 supports only the `handshake` testcase; everything else returns
127 so the runner skips rather than fails. SSLKEYLOGFILE / QLOGDIR
plumbing is deferred to Phase 1.

https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT
This commit is contained in:
Claude
2026-05-06 21:08:02 +00:00
parent e144226eee
commit 1586c0cced
7 changed files with 333 additions and 0 deletions
@@ -0,0 +1,139 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quic.interop.runner
import com.vitorpamplona.quic.connection.QuicConnection
import com.vitorpamplona.quic.connection.QuicConnectionConfig
import com.vitorpamplona.quic.connection.QuicConnectionDriver
import com.vitorpamplona.quic.tls.PermissiveCertificateValidator
import com.vitorpamplona.quic.transport.UdpSocket
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import java.net.URI
import kotlin.system.exitProcess
/**
* Endpoint that implements the [quic-interop-runner](https://github.com/quic-interop/quic-interop-runner)
* Docker contract. Reads the env-var protocol the runner pushes into each
* client container and dispatches by [TESTCASE].
*
* Exit codes:
* - `0` — testcase passed
* - `1` — testcase failed (bug in our impl, OR partner)
* - `127` — testcase not implemented (runner skips, doesn't fail)
*/
private const val EXIT_OK = 0
private const val EXIT_FAIL = 1
private const val EXIT_UNSUPPORTED = 127
private const val HANDSHAKE_TIMEOUT_SEC = 10L
fun main() {
val role = System.getenv("ROLE") ?: "client"
if (role != "client") {
System.err.println("server role not implemented")
exitProcess(EXIT_UNSUPPORTED)
}
val testcase = System.getenv("TESTCASE")?.trim().orEmpty()
val requests = System.getenv("REQUESTS")?.trim().orEmpty()
System.err.println("== quic-interop client ==")
System.err.println("testcase: $testcase")
System.err.println("requests: $requests")
val code =
when (testcase) {
"handshake" -> runHandshakeTest(requests)
else -> EXIT_UNSUPPORTED
}
exitProcess(code)
}
private fun runHandshakeTest(requests: 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 conn =
QuicConnection(
serverName = host,
config = QuicConnectionConfig(),
tlsCertificateValidator = PermissiveCertificateValidator(),
)
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() }
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 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
}