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:
@@ -74,6 +74,23 @@ class QuicConnection(
|
|||||||
.toEpochMilliseconds()
|
.toEpochMilliseconds()
|
||||||
},
|
},
|
||||||
val alpnList: List<ByteArray> = listOf(TlsConstants.ALPN_H3),
|
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
|
* Version this connection puts in the FIRST Initial it sends. Defaults
|
||||||
* to [QuicVersion.V1]; the interop runner sets it to
|
* to [QuicVersion.V1]; the interop runner sets it to
|
||||||
@@ -145,15 +162,6 @@ class QuicConnection(
|
|||||||
var retryConsumed: Boolean = false
|
var retryConsumed: Boolean = false
|
||||||
internal set
|
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
|
var handshakeComplete: Boolean = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -531,6 +539,41 @@ class QuicConnection(
|
|||||||
currentVersion = QuicVersion.V1
|
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 =
|
private fun buildLocalTransportParameters(): TransportParameters =
|
||||||
TransportParameters(
|
TransportParameters(
|
||||||
initialMaxData = config.initialMaxData,
|
initialMaxData = config.initialMaxData,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ package com.vitorpamplona.quic.connection
|
|||||||
|
|
||||||
import com.vitorpamplona.quic.QuicCodecException
|
import com.vitorpamplona.quic.QuicCodecException
|
||||||
import com.vitorpamplona.quic.connection.recovery.drainAckedSentPackets
|
import com.vitorpamplona.quic.connection.recovery.drainAckedSentPackets
|
||||||
|
import com.vitorpamplona.quic.packet.RetryPacket
|
||||||
import com.vitorpamplona.quic.frame.AckFrame
|
import com.vitorpamplona.quic.frame.AckFrame
|
||||||
import com.vitorpamplona.quic.frame.ConnectionCloseFrame
|
import com.vitorpamplona.quic.frame.ConnectionCloseFrame
|
||||||
import com.vitorpamplona.quic.frame.CryptoFrame
|
import com.vitorpamplona.quic.frame.CryptoFrame
|
||||||
|
|||||||
Reference in New Issue
Block a user