Handle early disconnects before SSL handshake#596
Conversation
|
|
||
| connection.addHandlerFirst(new EnsureSubscribersCompleteChannelHandler(this.requestSink)); | ||
| connection.addHandlerLast(new EnsureSubscribersCompleteChannelHandler(this.requestSink)); | ||
| connection.addHandlerLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE - 5, 1, 4, -4, 0)); |
There was a problem hiding this comment.
Keeps the same handler order. The SSL adapter is already added first.
| InternalLogger logger = InternalLoggerFactory.getInstance(ReactorNettyClient.class); | ||
| if (logger.isTraceEnabled()) { | ||
| pipeline.addFirst(LoggingHandler.class.getSimpleName(), | ||
| new LoggingHandler(ReactorNettyClient.class, LogLevel.TRACE)); | ||
| } |
There was a problem hiding this comment.
Trace logging handler also added on channel init, so it logs all lifecycle events.
| completeHandshakeExceptionally(e); | ||
| } else { | ||
| completeHandshake(); | ||
| ctx.channel().pipeline().remove(this); |
There was a problem hiding this comment.
Noticed this path as well, which could be reached if the server is ssl=off and the client is sslmode=prefer. Probably not tested currently, but the adapter would still be in the pipeline and fail subsequent reads.
mp911de
left a comment
There was a problem hiding this comment.
Thanks a lot. Closing the potential gaps that can leave a connection lingering makes sense. I left a few comments to provide guidance toward some changes.
| public void handlerAdded(ChannelHandlerContext ctx) { | ||
| public void channelActive(ChannelHandlerContext ctx) { | ||
| Mono.from(SSLRequest.INSTANCE.encode(this.alloc)).subscribe(ctx::writeAndFlush); | ||
| ctx.fireChannelActive(); |
There was a problem hiding this comment.
We can resort to calling super.channelActive().
There was a problem hiding this comment.
Yes, wasn't sure what would be preferred. Updated in d0bbd9d.
| // If we receive channel inactive before removing this handler, then the inbound has closed early. | ||
| PostgresqlSslException e = new PostgresqlSslException("Connection closed during SSL negotiation"); | ||
| completeHandshakeExceptionally(e); | ||
| ctx.fireChannelInactive(); |
There was a problem hiding this comment.
let's call super.channelInactive()
|
|
||
| @Override | ||
| public void handlerAdded(ChannelHandlerContext ctx) { | ||
| public void channelActive(ChannelHandlerContext ctx) { |
There was a problem hiding this comment.
Do we require the same changes in SSLTunnelHandlerAdapter?
There was a problem hiding this comment.
SSLTunnelHandlerAdapter immediately swaps itself for the underlying SslHandler, which already handles the disconnects, so shouldn't need any changes. Added a similar integration test for tunnel mode: 6de187d
| return tcpClient.connect().flatMap(it -> { | ||
|
|
||
| ChannelPipeline pipeline = it.channel().pipeline(); | ||
| return tcpClient.doOnChannelInit((observer, channel, remoteAddress) -> { |
There was a problem hiding this comment.
Using doOnChannelInit does make sense to avoid the gap of having a closed channel.
|
|
||
| return Mono.empty(); | ||
| private static Mono<Void> getSslHandshake(Channel channel) { | ||
| Mono<Void> sslHandshake = channel.attr(SSL_HANDSHAKE_KEY).getAndSet(null); |
There was a problem hiding this comment.
Instead of an attribute we could walk the handler pipeline and obtain the handshake mono from the AbstractPostgresSSLHandlerAdapter object.
There was a problem hiding this comment.
As mentioned in original comment, since the handshake mono is under connect.flatMap and the SSL adapter swaps itself for the underlying SslHandler, the SSL adapter can already be removed from the pipeline at this point. Definitely for SSLTunnelHandlerAdapter, which swaps on handler added. Assuming it's possible for SSLSessionHandlerAdapter as well, based on timing. Or am I missing something? Maybe if the SSL tunnel adapter doesn't swap for the SSL handler until after connection, and it's clear that the adapters will still be in the pipeline.
There was a problem hiding this comment.
This is an indication that the idea of removing the SSL handler isn't ideal. The SSLSessionHandlerAdapter performs one-time-only ops by sending the SSL request upon connect and reading the result afterwards. If we introduce state to the handler to remember that we've sent the request and that we've consumed its result, then we could skip the respective activity in channelActive and channelRead and keep the handler within the pipeline.
There was a problem hiding this comment.
Right, not removing the handlers is the alternative. So if leaving them in the pipeline, as noop handlers after the negotiation, is preferred, then I'll update to that.
0ba6b07 to
ba6b2f4
Compare
ba6b2f4 to
d40226f
Compare
|
@mp911de do you have time for another review here? Would be great to have this fix in a release to use. |
|
Thank you for your contribution. That's merged, polished, and backported now. |
Refs #595
Handle the inbound connection closing before SSL negotiation and always complete the SSL handshake future.
At first, only added the channel inactive lifecycle event in
SSLSessionHandlerAdapter. But with the channel pipeline being updated after connect, found that the inbound could already be closed before the handler was added (could check if it's active) but the channel could also be unregistered and outside the event loop and then the handler added event would be pending and never called. So reworked the pipeline changes so that the SSL adapter is added on channel init, so all lifecycle events are received. Pass the handshake Mono via a channel attribute — not sure if there's a better way, and the SSL adapter may already have swapped itself for the underlying SSL handler.Simple integration test for checking that disconnects are completed exceptionally, simulating what we've seen with Google Cloud SQL. Before these changes, this test will hang. No test specifically for the races between the disconnects and the handler lifecycle events, but have tested these changes manually under load.