NIOSSL: spurious uncleanShutdown error

at the end of every websocket connection, the TLS handler is always hitting the uncleanShutdown error, even though as far as i can tell, i am not doing anything wrong at the websocket layer. the uncleanShutdown error happens before channelInactive(context:), channelUnregistered(context:), and handlerRemoved(context:).

i also cloned the Vapor WebsocketKit module, and modified it to log all errors as well, and it has the same problem. it just wasn’t noticeable because for some reason, Vapor suppresses all the errors.

my websocket implementation yields an error to its AsyncThrowingStream<T, Failure>.Continuation whenever the errorCaught(context:error:) method gets called, which means that every websocket connection terminates with a thrown error. what am i doing wrong here?

uncleanShutdown is a case of NIO being extremely general and letting you figure out whether the information it's signalling actually matters.

A TLS connection has a "clean shutdown" mechanism. This is achieved by having the side that wants to initiate a close sending a CLOSE_NOTIFY alert on the TLS connection. The side that receives this alert should immediately send its own CLOSE_NOTIFY alert. Once that's done, it's safe to tear the TCP connection down.

NIO throws an uncleanShutdown error if this dance has not occurred: that is, if the underlying TCP connection dies without a bidirectional CLOSE_NOTIFY. We do this out of an abundance of caution: it is not terribly hard for an on-path attacker to launch a truncation attack against your TLS connection by injecting TCP FIN or RST packets into the stream. This is not something that TLS can protect against. As a result, NIO tells you if there was a risk that a truncation attack was launched in the form of this uncleanShutdown error.

You can safely ignore and suppress this error if you have clear in-band signalling that you have not suffered a truncation attack. Specifically, if you get a nice clean websocket shutdown with close frames in either direction, you know the connection was closed cleanly and you can safely ignore this error.

Why is this so common? Sadly, most TLS-using endpoints don't bother with a clean shutdown, particularly when they use HTTP. This is partly because the inner protocol has length tracking, and partly because they don't really know any better. NIO is pedantic on this point: we always send CLOSE_NOTIFY.

2 Likes

i’m seeing this when i initiate the disconnect. does this mean i should be sending this alert from my end? if so, how do i do this?

You will be: NIO doesn’t give you an option about it. In this case it means that the remote end didn’t send their CLOSE_NOTIFY in response, so we don’t really know that we got all the bytes they sent.

Just in case others are following this thread and are curious about the HTTP situation, AsyncHTTPClient also doesn't handle this correctly. A while ago I wrote up an issue with some more info.

1 Like