Skip to content

Drain QUIC handshake outputs instead of pushing to a delegate - #15

Merged
ydnar merged 1 commit into
mainfrom
quic-handshake-pull-outputs
Jun 18, 2026
Merged

Drain QUIC handshake outputs instead of pushing to a delegate#15
ydnar merged 1 commit into
mainfrom
quic-handshake-pull-outputs

Conversation

@ydnar

@ydnar ydnar commented Jun 18, 2026

Copy link
Copy Markdown

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 in #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 after each step:

  • drainSecrets() -> [Secret] — one Secret per 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 SSLConnection and ByteBufferBIO already use to drive BoringSSL internally (doHandshake() returns a state; the BIO buffers output that the caller pulls). 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 traffic 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 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-quic consumer is unaffected until its pin is bumped: that migration — TLSHandshake draining instead of conforming to NIOSSLQUICDelegate — is the paired follow-up PR.


Generated by Claude Code

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
ydnar marked this pull request as ready for review June 18, 2026 05:45
@ydnar
ydnar merged commit e65c0ba into main Jun 18, 2026
44 of 45 checks passed
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
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.
@ydnar
ydnar deleted the quic-handshake-pull-outputs branch August 5, 2026 17:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants