fix(quic): restore Retry handling lost in versionnegotiation merge

The agent A worktree was based on main, so its QuicConnection.kt
didn't carry the Retry handling from d03e17981. Merging with
-X theirs replaced the file wholesale, dropping applyRetry +
extraSecretsListener / cipherSuites constructor params + the
RetryPacket import in the parser.

Restored:
  - extraSecretsListener + cipherSuites ctor params (used in tlsListener
    + the TlsClient construction site).
  - applyRetry method, now using the version-negotiation-introduced
    LevelState.resetForVersionNegotiation helper (functionally
    equivalent to the prior restoreFromRetry it replaced).
  - RetryPacket import in QuicConnectionParser.

Also dedupes a duplicate `originalClientHello` field that the merge
left both copies of (one from agent 3's Retry work, one from agent A's
VN work). Single field now serves both reset paths.

https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT
This commit is contained in:
Claude
2026-05-07 00:21:38 +00:00
parent 04e30465d9
commit fcfd811545
2 changed files with 53 additions and 9 deletions
@@ -74,6 +74,23 @@ class QuicConnection(
.toEpochMilliseconds()
},
val alpnList: List<ByteArray> = listOf(TlsConstants.ALPN_H3),
/**
* Optional second listener invoked after the connection's own
* key-installation listener. Used by the interop runner endpoint to
* dump SSLKEYLOG lines so Wireshark can decrypt captured pcaps.
* Default `null` keeps production callers unaffected.
*/
val extraSecretsListener: TlsSecretsListener? = null,
/**
* TLS cipher suites to offer in the ClientHello. Override to e.g.
* `intArrayOf(TlsConstants.CIPHER_TLS_CHACHA20_POLY1305_SHA256)` for the
* `chacha20` interop testcase. Default matches [TlsClient]'s default.
*/
val cipherSuites: IntArray =
intArrayOf(
TlsConstants.CIPHER_TLS_AES_128_GCM_SHA256,
TlsConstants.CIPHER_TLS_CHACHA20_POLY1305_SHA256,
),
/**
* Version this connection puts in the FIRST Initial it sends. Defaults
* to [QuicVersion.V1]; the interop runner sets it to
@@ -145,15 +162,6 @@ class QuicConnection(
var retryConsumed: Boolean = false
internal set
/**
* The verbatim ClientHello CRYPTO bytes the TLS layer emitted at
* [start]. Captured so [applyRetry] can re-enqueue them after Retry
* resets the Initial-level CRYPTO send buffer (TLS itself only
* emits ClientHello once and we have no path to drive it to re-emit).
* Null until [start] has been called.
*/
private var originalClientHello: ByteArray? = null
var handshakeComplete: Boolean = false
private set
@@ -531,6 +539,41 @@ class QuicConnection(
currentVersion = QuicVersion.V1
}
/**
* Apply a verified Retry packet per RFC 9000 §17.2.5 + RFC 9001 §5.8.
* Validates the retry-integrity tag, swaps DCID, re-derives Initial keys,
* resets the Initial PN space, and re-enqueues the cached ClientHello so
* the next outbound Initial carries `Token = retryPacket.retryToken`.
*
* Returns false on bad tag, second Retry (RFC 9000 §17.2.5.2), or
* pre-start (no original ClientHello captured) — all silently dropped.
*/
internal fun applyRetry(
retryPacket: com.vitorpamplona.quic.packet.RetryPacket,
originalPacketBytes: ByteArray,
): Boolean {
if (retryConsumed) return false
if (!retryPacket.verifyIntegrityTag(originalPacketBytes, originalDestinationConnectionId.bytes)) {
return false
}
val savedClientHello = originalClientHello ?: return false
destinationConnectionId = retryPacket.scid
val proto = InitialSecrets.derive(destinationConnectionId.bytes)
val hp = AesEcbHeaderProtection(PlatformAesOneBlock)
initial.resetForVersionNegotiation(
sendProtection =
PacketProtection(bestAes128GcmAead(proto.clientKey), proto.clientKey, proto.clientIv, hp, proto.clientHp),
receiveProtection =
PacketProtection(bestAes128GcmAead(proto.serverKey), proto.serverKey, proto.serverIv, hp, proto.serverHp),
)
initial.cryptoSend.enqueue(savedClientHello)
retryToken = retryPacket.retryToken
retryConsumed = true
return true
}
private fun buildLocalTransportParameters(): TransportParameters =
TransportParameters(
initialMaxData = config.initialMaxData,
@@ -22,6 +22,7 @@ package com.vitorpamplona.quic.connection
import com.vitorpamplona.quic.QuicCodecException
import com.vitorpamplona.quic.connection.recovery.drainAckedSentPackets
import com.vitorpamplona.quic.packet.RetryPacket
import com.vitorpamplona.quic.frame.AckFrame
import com.vitorpamplona.quic.frame.ConnectionCloseFrame
import com.vitorpamplona.quic.frame.CryptoFrame