Repository navigation
Keep draining TLS plaintext after a TLS 1.3 peer close_notify - #3731
Conversation
Motivation: When the plaintext output is cancelled, the GraphStage TLS engine drains plaintext it has already accepted. If a TLS 1.3 peer sent close_notify during that drain, unwrap returned CLOSED and the stage completed, discarding the accepted plaintext without sending it or its own close_notify, although TLS 1.3 close_notify only closes the peer's sending direction. Modification: - Stay in the inbound-closed drain when unwrap returns CLOSED while accepted plaintext is still being drained and the engine's outbound side is open. The drain then finishes and closes outbound as before. - Document that a TLS 1.3 close_notify does not end the drain, while a TLS 1.2 close_notify (which closes both SSLEngine directions) does. - Add real-SSLEngine tests that send close_notify during the drain for IgnoreComplete and EagerClose, on TLS 1.3 and TLS 1.2. Result: A TLS 1.3 peer that half-closes receives the accepted response followed by close_notify. TLS 1.2 behaviour is unchanged and terminates promptly. Tests: - sbt "stream-tests / Test / testOnly org.apache.pekko.stream.io.TlsGraphStageCancellationSpec": the two TLS 1.3 tests failed before the fix (partial response) and pass after it; 6/6 pass. - sbt "stream-tests / Test / testOnly org.apache.pekko.stream.io.Tls*": 281/281 passed. - Native scalafmt and git diff --check passed. References: Refs apache#3727
He-Pin
left a comment
There was a problem hiding this comment.
LGTM. Traced the state machine: in InboundClosedPhase the peer close_notify makes transportInput non-empty, so drainBeforeInboundClose goes false and doInbound reaches doUnwrap. Skipping completeOrFlush() there is safe because engine.isInboundDone then forces inboundHalfClosedReady/inboundHalfClosedCompleted, so the pump exits and the drain finishes via doOutbound -> closeOutbound -> FlushingOutboundPhase. No spin: phaseExecutable is false without transport demand, and OutputSlot.onPull re-runs the pump. The phase.toInt != InboundClosedPhase guard keeps AwaitingClosePhase and the bidirectional/outbound-closed paths unchanged. Extracting drainingAcceptedUserInput is the right call since drainBeforeInboundClose cannot express this state. Docs and both DSL scaladocs match the tested TLS 1.2 vs 1.3 behaviour.
Add apache#3727, apache#3728, apache#3731, apache#3734, apache#3738 (TLS), apache#3730 (fanout publisher performance) and apache#3733 (shard lease release logging). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add apache#3727, apache#3728, apache#3731, apache#3734, apache#3738 (TLS), apache#3730 (fanout publisher performance) and apache#3733 (shard lease release logging).
Motivation
#3727 made the GraphStage TLS engine (
pekko.stream.materializer.tls.engine = graph-stage) drain plaintext it has already accepted when the plaintext output is cancelled. If a TLS 1.3 peer sendsclose_notifyduring that drain,unwrapreturnsCLOSED,completeOrFlushcompletes the stage andpumpFinishedcancels the user input. The accepted plaintext is discarded, and our ownclose_notifyis never sent. In TLS 1.3,close_notifyonly closes the peer's sending direction, so the response could still have been delivered.Modification
doUnwrap, whenunwrapreturnsCLOSEDwhile the stage is in the inbound-closed drain, accepted plaintext remains and the engine's outbound side is still open, stay in the drain instead of completing. The drain then finishes through the existing path:closeOutbound, sendclose_notify, complete.close_notifydoes not end the drain, while a TLS 1.2close_notify(which closes bothSSLEnginedirections) does.TlsGraphStageCancellationSpecthat drive a real clientSSLEngineagainst the operator. The server accepts a response larger than its remaining ciphertext demand, its plaintext output is cancelled, and the client then sendsclose_notify. These run forIgnoreCompleteandEagerClose, on TLS 1.3 (full response expected) and TLS 1.2 (prompt termination with a partial prefix expected).Result
A TLS 1.3 peer that half-closes receives the accepted response followed by the server's
close_notify. TLS 1.2 behaviour is unchanged and still terminates promptly. The legacy actor engine is unaffected.Tests
sbt "stream-tests / Test / testOnly org.apache.pekko.stream.io.TlsGraphStageCancellationSpec": the two TLS 1.3 tests failed before the fix (partial response) and pass after it; 6/6 pass.sbt "stream-tests / Test / testOnly org.apache.pekko.stream.io.Tls*": 281/281 passed.git diff --checkpassed.References
Refs #3727