feat(quic): client-initiated 1-RTT key update + dispatch ecn/blackhole/amplificationlimit
The runner's keyupdate testcase has TESTCASE_CLIENT=keyupdate (server runs plain transfer). The runner verifies the pcap shows BOTH sides emit packets in phase 1 — pre-fix our receive-only key-update path satisfied a server-initiated rotation but not this test, because aioquic's transfer-server doesn't rotate spontaneously. Result: 0 phase-1 packets either direction, "Expected to see packets sent with key phase 1 from both client and server". QuicConnection.initiateKeyUpdate() (now public) is the send-side analogue of commitKeyUpdate: derives next-phase secrets for both directions via HKDF-Expand-Label "quic ku", installs as live (reusing old HP keys per RFC §6.1), flips currentSendKeyPhase + currentReceiveKeyPhase together. The receive side has to roll too because the peer responds in the new phase — leaving currentReceive at 0 would force feedShortHeaderPacket to take the deriveNextPhase-then-commit path on the response and orphan the keys we just installed in previousReceiveProtection. InteropClient adds an `initiateKeyUpdate` flag to runTransferTest; the keyupdate dispatch sets it true. After awaitHandshake (TLS done, 1-RTT keys derived) the flag-flow polls briefly for status=CONNECTED (HANDSHAKE_DONE arrived → handshake confirmed per RFC 9001 §6.5 prerequisite) before calling initiateKeyUpdate, then sends the GET. The GET goes out in phase 1, the server mirrors phase 1 in its response, runner is satisfied. Also added ecn, amplificationlimit, blackhole to the runTransferTest dispatch (all reuse the plain-transfer flow; the runner verifies behaviour via pcap independent of any client-side dance). aioquic phase 3 result: ✓(retry, keyupdate, blackhole), ?(resumption, zerortt, ecn — feature gaps requiring session tickets, 0-RTT, and IP-layer ECT codepoints respectively), amplificationlimit blocked by a runner-side cert-gen bug on macOS (tr LC_CTYPE=C doesn't suppress UTF-8 errors, the chainlen=9 cert inflation step fails). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -209,10 +209,33 @@ fun main() {
|
||||
// ipv6 — same flow over an IPv6 socket;
|
||||
// JDK DatagramChannel.connect handles
|
||||
// the v6 address resolution natively.
|
||||
// ecn — runner verifies ECN-CE counts in
|
||||
// the pcap. Client just does a 100KB
|
||||
// transfer; the IP-layer ECT codepoint
|
||||
// is set by the sim and we don't
|
||||
// need to do anything special.
|
||||
// amplificationlimit — runner verifies server obeys 3x
|
||||
// amplification limit. Pure server
|
||||
// check — client does a normal
|
||||
// transfer (the runner sets
|
||||
// TESTCASE_CLIENT=transfer).
|
||||
// blackhole — sim drops ALL packets for several
|
||||
// seconds mid-transfer; client must
|
||||
// resume after blackhole ends. Our
|
||||
// PTO + retransmit handles this; the
|
||||
// runner sets TESTCASE_CLIENT=transfer.
|
||||
// keyupdate — server initiates a 1-RTT key update
|
||||
// mid-transfer (KEY_PHASE bit flips).
|
||||
// Our RFC 9001 §6 receive-side key
|
||||
// update lands the rotation; runner
|
||||
// verifies the pcap shows packets
|
||||
// in both phases. Server-side test
|
||||
// from our perspective.
|
||||
"handshake", "chacha20",
|
||||
"transfer", "http3", "multiplexing",
|
||||
"transferloss", "transfercorruption", "longrtt", "goodput", "crosstraffic",
|
||||
"retry", "ipv6",
|
||||
"ecn", "amplificationlimit", "blackhole",
|
||||
// NOTE: the runner does NOT have a `versionnegotiation` testcase
|
||||
// (its Available list excludes it). The :quic VN-handling code
|
||||
// (applyVersionNegotiation, FORCE_VERSION_NEGOTIATION constant)
|
||||
@@ -245,6 +268,28 @@ fun main() {
|
||||
)
|
||||
}
|
||||
|
||||
// keyupdate: same transfer flow but the client initiates a
|
||||
// RFC 9001 §6 1-RTT key update once the handshake is
|
||||
// confirmed. Runner verifies pcap shows BOTH client and
|
||||
// server emitting packets in phase 1 — without our side
|
||||
// initiating, aioquic's plain-transfer server doesn't
|
||||
// rotate spontaneously and the test fails with "Expected
|
||||
// to see packets sent with key phase 1 from both client
|
||||
// and server".
|
||||
"keyupdate" -> {
|
||||
runTransferTest(
|
||||
requests = requests,
|
||||
downloadsDir = downloadsDir,
|
||||
cipherSuites = cipherSuites,
|
||||
offeredAlpns = offeredAlpns,
|
||||
initialVersion = initialVersion,
|
||||
keyLogPath = keyLogPath,
|
||||
qlogDir = qlogDir,
|
||||
parallel = requests.split(Regex("\\s+")).count { it.isNotBlank() } > 1,
|
||||
initiateKeyUpdate = true,
|
||||
)
|
||||
}
|
||||
|
||||
// The runner reuses TESTCASE_CLIENT=multiconnect for the
|
||||
// handshakeloss + handshakecorruption tests (see
|
||||
// testcases_quic.py:746). Each URL must be fetched on a fresh
|
||||
@@ -294,6 +339,7 @@ private fun runTransferTest(
|
||||
keyLogPath: String?,
|
||||
qlogDir: File?,
|
||||
parallel: Boolean,
|
||||
initiateKeyUpdate: Boolean = false,
|
||||
): Int {
|
||||
val urls =
|
||||
requests
|
||||
@@ -407,6 +453,27 @@ private fun runTransferTest(
|
||||
// runs even when DEBUG=0 — this is a control-flow boundary,
|
||||
// not a hot-path trace.
|
||||
System.err.println("[boot] transfer mode: parallel=$parallel urls=${urls.size}")
|
||||
|
||||
// RFC 9001 §6 keyupdate testcase: the runner verifies the pcap
|
||||
// shows packets in BOTH key phases from BOTH sides. Without
|
||||
// initiating from our side, only the server's natural rotation
|
||||
// (if any) would show — aioquic's transfer-server doesn't
|
||||
// initiate, so we'd see only phase 0. Initiate after the
|
||||
// handshake is confirmed (HANDSHAKE_DONE → status=CONNECTED,
|
||||
// RFC 9001 §6.5 prerequisite) but BEFORE we send the GET so the
|
||||
// request itself is in phase 1 — the server's response then
|
||||
// mirrors phase 1, satisfying the runner's check. Brief poll
|
||||
// for status because awaitHandshake returns on TLS-done
|
||||
// (1-RTT keys derived) which is one ack ahead of HANDSHAKE_DONE
|
||||
// arriving.
|
||||
if (initiateKeyUpdate) {
|
||||
withTimeoutOrNull(2_000L) {
|
||||
while (conn.status != QuicConnection.Status.CONNECTED) delay(10)
|
||||
}
|
||||
conn.initiateKeyUpdate()
|
||||
System.err.println("[boot] keyupdate: client initiated rotation to phase 1")
|
||||
}
|
||||
|
||||
val outcome =
|
||||
withTimeoutOrNull(TRANSFER_TIMEOUT_SEC * 1_000L) {
|
||||
val responses =
|
||||
|
||||
Reference in New Issue
Block a user