Drain QUIC handshake outputs instead of pushing to a delegate - #15
Merged
Conversation
Motivation: NIOSSLQUICHandshake delivered its SSL_QUIC_METHOD outputs — the read and write traffic secrets and the handshake bytes to send — through NIOSSLQUICDelegate, the only delegate protocol in NIOSSL. The library's other customization points are closures (NIOSSLCustomVerificationCallback, the PSK callbacks) or, for its one multi-method protocol, EventLoopFuture-returning (NIOSSLCustomPrivateKey); there is no async and no other delegate. The handshake is otherwise a synchronous pull driver: advance() returns a State, alerts are thrown from it, and the peer transport parameters and negotiated protocol are pulled properties. The delegate was the lone push channel, and a pattern foreign to the library — friction for landing this upstream. Design discussion: #14. Modifications: The set_read_secret / set_write_secret / add_handshake_data trampolines now stash their outputs on the handshake rather than calling a delegate, the way send_alert already stashes its alert. advance() leaves them in place; the caller drains them with drainSecrets() (one Secret per BoringSSL callback, tagged read or write) and drainHandshakeData() (the per-level bytes to send) — the drive-then-drain shape SSLConnection and ByteBufferBIO already use to drive BoringSSL internally. The initializer drops its delegate parameter, NIOSSLQUICDelegate is removed, and NIOSSLQUICRole moves into the handshake file. How this is verified: the in-process handshake tests drive both endpoints through the pull API — completion, matching secrets, ALPN, transport parameters, the post-handshake and fatal-alert paths, and certificate and hostname verification — via a small Endpoint harness that drains after each advance(). Result: NIOSSLQUICHandshake is a uniform sans-I/O pull driver with no callbacks: drive advance(), drain what it produced, feed what it wants. No EventLoop, no delegate, no retain cycle. https://claude.ai/code/session_01Cu2dW2zXuAnwDRaiHr7feR
ydnar
marked this pull request as ready for review
June 18, 2026 05:45
ydnar
pushed a commit
to alta/swift-nio-quic
that referenced
this pull request
Jun 18, 2026
alta/swift-nio-ssl#15 merged into the fork's main (e65c0ba), so point the dependency back at `branch: "main"` and bump the Package.resolved pin from the temporary `quic-handshake-pull-outputs` branch to that commit. No code change. https://claude.ai/code/session_01Cu2dW2zXuAnwDRaiHr7feR
This was referenced Jun 18, 2026
ydnar
added a commit
to alta/swift-nio-quic
that referenced
this pull request
Jun 18, 2026
Motivation: The swift-nio-ssl fork reshaped NIOSSLQUICHandshake from a delegate that pushes its outputs into a sans-I/O pull driver (alta/swift-nio-ssl#15): advance() produces the traffic secrets and the handshake bytes to send, and the caller drains them. TLSHandshake conformed to the now-removed NIOSSLQUICDelegate, so it adopts the new shape. Modifications: TLSHandshake no longer conforms to NIOSSLQUICDelegate. After each advance() it drains the handshake: drainSecrets() installs the read and write packet protection keys (retaining the application-level secrets to seed key updates, RFC 9001 § 6.1), and drainHandshakeData() queues the per-level bytes to send as CRYPTO frames. The handshake initializer drops its delegate argument, and the former setReadSecret / setWriteSecret / writeHandshakeData callbacks fold into installSecret(_:) and the drain loop. Timing is unchanged: the secrets are installed when the drain runs, immediately after advance() returns, exactly where the delegate callbacks fired. The swift-nio-ssl dependency tracks branch main, with Package.resolved bumped to the squash of alta/swift-nio-ssl#15. How this is verified: the full unit suite passes against the new fork API, including the loopback real-socket handshakes and the in-process connection and transport simulators. Result: TLSHandshake drives the fork as a pull driver — advance, drain the secrets and handshake data, feed CRYPTO — with no delegate and no behavior change on the wire.
ydnar
added a commit
that referenced
this pull request
Jun 30, 2026
Motivation: NIOSSLQUICHandshake delivered its SSL_QUIC_METHOD outputs — the read and write traffic secrets and the handshake bytes to send — through NIOSSLQUICDelegate, the only delegate protocol in NIOSSL. The library's other customization points are closures (NIOSSLCustomVerificationCallback, the PSK callbacks) or, for its one multi-method protocol, EventLoopFuture-returning (NIOSSLCustomPrivateKey); there is no async and no other delegate. The handshake is otherwise a synchronous pull driver: advance() returns a State, alerts are thrown from it, and the peer transport parameters and negotiated protocol are pulled properties. The delegate was the lone push channel, and a pattern foreign to the library — friction for landing this upstream. Design discussion: #14. Modifications: The set_read_secret / set_write_secret / add_handshake_data trampolines now stash their outputs on the handshake rather than calling a delegate, the way send_alert already stashes its alert. advance() leaves them in place; the caller drains them with drainSecrets() (one Secret per BoringSSL callback, tagged read or write) and drainHandshakeData() (the per-level bytes to send) — the drive-then-drain shape SSLConnection and ByteBufferBIO already use to drive BoringSSL internally. The initializer drops its delegate parameter, NIOSSLQUICDelegate is removed, and NIOSSLQUICRole moves into the handshake file. How this is verified: the in-process handshake tests drive both endpoints through the pull API — completion, matching secrets, ALPN, transport parameters, the post-handshake and fatal-alert paths, and certificate and hostname verification — via a small Endpoint harness that drains after each advance(). Result: NIOSSLQUICHandshake is a uniform sans-I/O pull driver with no callbacks: drive advance(), drain what it produced, feed what it wants. No EventLoop, no delegate, no retain cycle.
ydnar
added a commit
that referenced
this pull request
Jul 17, 2026
Motivation: NIOSSLQUICHandshake delivered its SSL_QUIC_METHOD outputs — the read and write traffic secrets and the handshake bytes to send — through NIOSSLQUICDelegate, the only delegate protocol in NIOSSL. The library's other customization points are closures (NIOSSLCustomVerificationCallback, the PSK callbacks) or, for its one multi-method protocol, EventLoopFuture-returning (NIOSSLCustomPrivateKey); there is no async and no other delegate. The handshake is otherwise a synchronous pull driver: advance() returns a State, alerts are thrown from it, and the peer transport parameters and negotiated protocol are pulled properties. The delegate was the lone push channel, and a pattern foreign to the library — friction for landing this upstream. Design discussion: #14. Modifications: The set_read_secret / set_write_secret / add_handshake_data trampolines now stash their outputs on the handshake rather than calling a delegate, the way send_alert already stashes its alert. advance() leaves them in place; the caller drains them with drainSecrets() (one Secret per BoringSSL callback, tagged read or write) and drainHandshakeData() (the per-level bytes to send) — the drive-then-drain shape SSLConnection and ByteBufferBIO already use to drive BoringSSL internally. The initializer drops its delegate parameter, NIOSSLQUICDelegate is removed, and NIOSSLQUICRole moves into the handshake file. How this is verified: the in-process handshake tests drive both endpoints through the pull API — completion, matching secrets, ALPN, transport parameters, the post-handshake and fatal-alert paths, and certificate and hostname verification — via a small Endpoint harness that drains after each advance(). Result: NIOSSLQUICHandshake is a uniform sans-I/O pull driver with no callbacks: drive advance(), drain what it produced, feed what it wants. No EventLoop, no delegate, no retain cycle.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
NIOSSLQUICHandshakedelivered itsSSL_QUIC_METHODoutputs — the read and write traffic secrets and the handshake bytes to send — throughNIOSSLQUICDelegate, the only delegate protocol in NIOSSL. The library's other customization points are closures (NIOSSLCustomVerificationCallback, the PSK callbacks) or, for its one multi-method protocol,EventLoopFuture-returning (NIOSSLCustomPrivateKey); there is noasyncand no other delegate. The handshake is otherwise a synchronous pull driver:advance()returns aState, alerts are thrown from it, and the peer transport parameters and negotiated protocol are pulled properties. The delegate was the lone push channel, and a pattern foreign to the library — friction for landing this upstream. Design discussion in #14.Modifications
The
set_read_secret/set_write_secret/add_handshake_datatrampolines now stash their outputs on the handshake rather than calling a delegate, the waysend_alertalready stashes its alert.advance()leaves them in place; the caller drains them after each step:drainSecrets() -> [Secret]— oneSecretper BoringSSL callback, carrying the level, cipher suite, direction (.read/.write), and bytes.drainHandshakeData() -> [(level:data:)]— the per-level bytes to send in CRYPTO frames.This is the drive-then-drain shape
SSLConnectionandByteBufferBIOalready use to drive BoringSSL internally (doHandshake()returns a state; the BIO buffers output that the caller pulls). The initializer drops itsdelegateparameter,NIOSSLQUICDelegateis removed, andNIOSSLQUICRolemoves into the handshake file.How this is verified: the in-process handshake tests drive both endpoints through the pull API — completion, matching traffic secrets, ALPN, transport parameters, the post-handshake and fatal-alert paths, and certificate and hostname verification — via a small
Endpointharness that drains after eachadvance().Result
NIOSSLQUICHandshakeis a uniform sans-I/O pull driver with no callbacks: driveadvance(), drain what it produced, feed what it wants. NoEventLoop, no delegate, no retain cycle.This is the smallest change that implements option C from #14, scoped to the handshake outputs and independent of certificate verification (#13, which reframes onto this once it lands). The
swift-nio-quicconsumer is unaffected until its pin is bumped: that migration —TLSHandshakedraining instead of conforming toNIOSSLQUICDelegate— is the paired follow-up PR.Generated by Claude Code