A boost-asio SSL/TLS TCP socket is implemented as an ssl::stream over a tcp::socket:
boost::asio::ssl::stream<boost::asio::ip::tcp::socket> ssl_socket;
In the TLS protocol, a cryptographically secure shutdown involves parties exchanging close_notify messages. Simply closing the lowest layer may make the session vulnerable to a truncation attack.
In boost asio ssl async_shutdown always finishes with an error? #Tanner Sansbury describes the SSL shutdown process in detail with a number of scenarios and proposes using an async_shutdown followed by an async_write to disconnect an SSL stream prior to closing the socket:
ssl_socket.async_shutdown(...);
const char buffer[] = "";
async_write(ssl_socket, buffer, [](...) { ssl_socket.close(); })
Performing an async_shutdown on an ssl::stream sends an SSL close_notify message and waits for a response from the other end. The purpose of writing to the stream after the async_shutdown is to be notified when async_shutdown has sent the close_notify so that the socket can be closed without waiting for the response. However, in the current (1.59) version of boost the call to async_write fails...
In How to gracefully shutdown a boost asio ssl client? #maxschlepzig proposes shutting down receiver of the underlying TCP socket:
ssl_socket.lowest_layer()::shutdown(tcp::socket::shutdown_receive);
This produces a short read error, and async_shutdown is called when it's detected in the error handler:
// const boost::system::error_code &ec
if (ec.category() == asio::error::get_ssl_category() &&
ec.value() == ERR_PACK(ERR_LIB_SSL, 0, SSL_R_SHORT_READ))
{
// -> not a real error:
do_ssl_async_shutdown();
}
Or cancelling the read/write operations on the socket and then calling SSL async shutdown, i.e.:
boost::system::error_code ec;
ssl_socket.cancel(ec);
ssl_socket.async_shutdown([](...) { ssl_socket.close(); };
I'm currently using this last method since it works with the current version of boost.
What is the correct/best way to securely disconnect a boost-asio SSL socket?
To securely disconnect, perform a shutdown operation and then close the underlying transport once shutdown has complete. Hence, the method you are currently using will perform a secure disconnect:
boost::system::error_code ec;
ssl_socket.cancel(ec);
ssl_socket.async_shutdown([](...) { ssl_socket.close(); };
Be aware that the current async_shutdown operation will be considered complete when either:
A close_notify has been received by the remote peer.
The remote peer closes the socket.
The operation has been cancelled.
Hence, if resources are bound to the lifetime of the socket or connection, then these resources will remain alive waiting for the remote peer to take action or until the operation is cancelled locally. However, waiting for a close_notify response is not required for a secure shutdown. If resources are bound to the connection, and locally the connection is considered dead upon sending a shutdown, then it may be worthwhile to not wait for the remote peer to take action:
ssl_socket.async_shutdown(...);
const char buffer[] = "";
async_write(ssl_socket, boost::asio::buffer(buffer),
[](...) { ssl_socket.close(); })
When a client sends a close_notify message, the client guarantees that the client will not send additional data across the secure connection. In essence, the async_write() is being used to detect when the client has sent a close_notify, and within the completion handler, will close the underlying transport, causing the async_shutdown() to complete with boost::asio::error::operation_aborted. As noted in the linked answer, the async_write() operation is expected to fail.
... as the write side of PartyA's SSL stream has closed, the async_write() operation will fail with an SSL error indicating the protocol has been shutdown.
if ((error.category() == boost::asio::error::get_ssl_category())
&& (SSL_R_PROTOCOL_IS_SHUTDOWN == ERR_GET_REASON(error.value())))
{
ssl_stream.lowest_layer().close();
}
The failed async_write() operation will then explicitly close the underlying transport, causing the async_shutdown() operation that is waiting for PartyB's close_notify to be cancelled.
I'm probably late to answer this but I want to report my experience.
This solution so far (using boost 1.78) did not produce any visible error on the client nor the server:
// sock type is boost::asio::ssl::stream<boost::asio::ip::tcp::socket>
sock->shutdown(ec);
sock->lowest_layer().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
sock->lowest_layer().cancel(ec);
sock->lowest_layer().close();
Sandbox server with: openssl s_server -cert server.crt -key server.key -4 -debug
With this solution the server gets this after the sock->shutdown(ec).
read from 0x55e5dff8c960 [0x55e5dff810f8] (19 bytes => 19 (0x13))
0000 - 44 bc 11 5b a9 b4 ee 51-48 e0 18 f7 99 a7 a8 a9 D..[...QH.......
0010 - 21 1a 60 !.`
DONE
shutting down SSL
CONNECTION CLOSED
Before I was using this code (used for both plain TCP and ssl socket)
sock->lowest_layer().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
sock->lowest_layer().cancel(ec);
sock->lowest_layer().close();
The old code, when leveraging ssl socket, produced this error on the server:
read from 0x55eb3d40b430 [0x55eb3d423513] (5 bytes => 0 (0x0))
ERROR
shutting down SSL
CONNECTION CLOSED
As mentioned before, to avoid this behavior a close_notify should be sent out by the client using ssl::stream::async_shutdown or ssl::stream::shutdown
The trick of async_write() could be useful in case you want to leverage the async_shutdown() function instead of the synchronous shutdown()
Related
I'm using asio to run a TCP server.
For each message the server receives, one or more responses is returned.
Most of the messages are simple returns but some are commands which will run an action, which can take up to 10 minutes then a returns a message (but only one action can run at a time).
I start my session function in a new thread, passing it a tcp::socket when a connection is made:
tcp::acceptor a(io_context, tcp::endpoint(tcp::v4(), port));
for (;;) {
std::thread(session, a.accept()).detach();
}
But after that the tcp::socket is "stuck" in the session function. I can't pass the socket anywhere else (without compilation errors so far) and the session needs to be complete because it:
Receives the message using socket.read_some()
Processes the message (and trigger an action if required)
Transmits a response using asio::write()
I need to be able to interrupt step 2 if a new message is received but without sharing the Socket I don't know how.
Whichever way I look at it, the socket can only be used by one thread so I'll either be waiting for a new message or waiting for a response to be generated - both of which would block eachother.
I am creating a client application that connects to a server using a an ssl Websocket connection and an ssl Http (Keep-Alive) connection and I am using boost::beast package to do the same. So as to detect a dead connection i have implemented a simple ping-pong mechanism. These all work fine, but an issue comes up when handling the ping-pong failure. The issue is as follows:
For testing my code i connected to the remote server, sent few messages and then turned off my wifi. As expected after a certain period it detected that it did not receive any message from the server and it tried to do an async_shutdown for the http connection and an async_close for the websocket connection. First thing i noticed was that both these calls block their respective strands until the wifi is back up.
And after the wifi is up, the application tries to reset the stream before reconnect:
void HttpKeepAliveConnection::recreateSocket()
{
_receivedPongForLastPing = true;
_sslContext.reset(new boost::asio::ssl::context({boost::asio::ssl::context::sslv23_client}));
_stream.reset(new HttpStream(_ioContext, *_sslContext));
}
And reset ws variable for websocket:
void WebsocketConnection::recreateSocket()
{
_receivedPongForLastPing = true;
_sslContext.reset(new boost::asio::ssl::context({boost::asio::ssl::context::sslv23_client}));
_ws.reset(new WebSocket(_ioContext, *_sslContext));
}
Unfortunately it fails at either on_connect or on_ssl_handshake. Following are my logs:
156 AsioConnectionBase.cpp:53 (2018-08-06 15:34:38.458536) [0x00007ffff601e700] : Started connect sequence. Connection Name: HttpKeepAliveConn
157 AsioConnectionBase.cpp:122 (2018-08-06 15:34:38.459802) [0x00007ffff481b700] : Failed establishing connection to destination. Connection failed. Connection Name: HttpKeepAliveConn. Host: xxxxxxxxx. Port: 443. Error: Operation canceled
158 APIManager.cpp:175 (2018-08-06 15:34:38.459886) [0x00007ffff481b700] : Received error callback from connection. Restarting connection in a sec. Connection Name: HttpKeepAliveConn
159 AsioConnectionBase.cpp:53 (2018-08-06 15:34:39.460009) [0x00007ffff481b700] : Started connect sequence. Connection Name: HttpKeepAliveConn
160 HttpKeepAliveConnection.cpp:32 (2018-08-06 15:34:39.460515) [0x00007ffff481b700] : Failed ssl handshake. Connection failed.Connection Name: HttpKeepAliveConn. Host: xxxxxxxxx. Port: 443. Error: Bad file descriptor
161 APIManager.cpp:175 (2018-08-06 15:34:39.460674) [0x00007ffff481b700] : Received error callback from connection. Restarting connection in a sec. Connection Name: HttpKeepAliveConn
So I have 2 questions:
How do we close a connection if internet is down and a proper tcp close is not possible.
Before reconnecting what are the variables in boost::beast (or for that matter boost::asio as boost::beast is built on top of asio) that needs to be reset
Have been stuck trying to debug this for couple of hours. Any help is appreciated
EDIT
So I figured out where I went wrong. Both Alan Birtles and Vinnie Falco were right. The way to close a dead ssl connection after your ping timer has expired (and none of the handlers have returned yet) is
In your timer handler
_stream->lowest_layer().close();
For websocket
_ws->lowest_layer().close();
Wait for one of your handlers (typically read handler) to return with error (typically boost::asio::error::operation_aborted error). From there, queue the start of the next reconnect. (Do not queue the reconnect immediately after step 1, it will result in memory issues that I faced. I know this is asio 101, but is easy to forget)
For resetting socket, all that is required is for the stream to be reset
_stream.reset(new HttpStream(_ioContext, _sslContext));
For websocket
_ws.reset(new WebSocket(_ioContext, _sslContext));
I don't think asio::ssl::stream can be used again after being closed.
How do we close a connection if internet is down and a proper tcp close is not possible.
Simply allow the socket or stream object to be destroyed.
We are using openssl 1.0.2k for our TLS related functionalities.
In one of our deployment the client is able to complete the TLS handshakes using TLSv1.2 and was able to send application data towards server.After some requests the TLS connections closed from the server side with the below error
"error:1408F10B:SSL routines:SSL3_GET_RECORD:wrong version number"
TLS handshake steps:
1. Client hello
2. Server Hello
3. Certificate,Certificate Request, Server hello done
4. Certificate,Client Key Exchange,Change Cipher spec,Encrypted handshake message
5. Change Cipher spec,Encrypted handshake message
6. Application data exchanges between client and server
7. Encrypted Alert(server to client)
8. Encrypted Alert( client to server
The error logs from server side says "error:1408F10B:SSL routines:SSL3_GET_RECORD:wrong version number"
Can you please let us know the cause for this issue. If the ssl version is mismatching then the handshake phase should not succeed right?
But in our case handshake is successful and after some application data transfer our server is failing with this error.
If the ssl version is mismatching then the handshake phase should not succeed right?
No. Any TLS packet have header, and header has TLS version inside:
(
byte - record_type
byte[2] - version
byte[2] - length
) header
byte[length] - encrypted or raw data
Header is always in raw, it is never encrypted. Even if during handshake client sent TLS 1.2 version in all TLS packets, he can send another version after handshake is finished. Or someone in between can modify network traffic. In this case OpenSSL throws described error.
In my case, I was using OpenSSL for client functionality.
I was calling SSL_set_connect_state after SSL_connect. It should be called before.
SSL_set_connect_state (for client only) cleans up all the state!
snippet:
void SSL_set_connect_state(SSL *s)
{
s->server = 0;
s->shutdown = 0;
ossl_statem_clear(s);
s->handshake_func = s->method->ssl_connect;
clear_ciphers(s);
}
In my case:
1) Client <-> Server handshake succeeded.
2) SSL_write from client side (client sending message to server) lead to exact same error as mentioned in question (on server side)
I looked at pkt dump on server side.
read from 0x2651570 [0x2656c63] (5 bytes => 5 (0x5)) .
0000 - 16 03 01 01 e2 .....
ERROR
139688140752544:error:1408F10B:SSL routines:SSL3_GET_RECORD:wrong >version number:s3_pkt.c:337:
1) 5 Bytes read in the above snipped is the size of SSL record. Server received data, and it attempted reading SSL record.
2) 1'st byte of the record is the SSL record type In this case ===> x16 => '22'
This itself is wrong, as far as server is concerned, handshake was successful and it was expecting application data. Instead it received data with SSL record for handshake, hence it was throwing the error.
A correct snippet of application data is as follows: 'x17' ==> 23
read from 0x2664f80 [0x2656c63] (5 bytes => 5 (0x5)) .
0000 - 17 03 03 00 1c
Since SSL_set_connect_state was called after connecting, client state was lost and SSL_write will attempt handshake if handshake wasnt performed before (client thought so as its state was lost!)
More data on these SSL records can be found here:
https://www.ibm.com/support/knowledgecenter/SSB23S_1.1.0.12/gtps7/s5rcd.html
Providing a MCVE is going to be hard, the scenario is the following:
a server written in c++ with boost asio offers some services
a client written in c++ with boost asio requests services
There are custom headers and most communication is done using multipart/form.
However, in the case where the server returns a 401 for an unauthorized access,
the client receives a broken pipe (system error 32).
AFAIK this happens when the server connection closes too early.
So, running into gdb, I can see that the problem is indeed the transition from the async_write which sends the request, to the async_read_until which reads the first line of the HTTP Header:
The connect routine sends the request from the client to the server:
boost::asio::async_write(*socket_.get(),
request_,
boost::bind(&asio_handler<http_socket>::write_request,
this,
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));
And the write_request callback, checks if the request was sent OK, and then reads the first line (until the first newline):
template <class T>
void asio_handler<T>::write_request(const boost::system::error_code & err,
const std::size_t bytes)
{
if (!err) {
// read until first newline
boost::asio::async_read_until(*socket_,
buffer_,
"\r\n",
boost::bind(&asio_handler::read_status_line,
this,
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));
}
else {
end(err);
}
}
The problem is that the end(err) is always called with a broken pipe (error code 32). Meaning, as far as I understand, that the server closed the connection. The server indeed closes the connection, but only after it has sent a message HTTP/1.1 401 Unauthorized.
using curl with the appropriate request, we do get the actual message/error before the server closes the connection
using our client written in C++/boost asio we only get the broken pipe and no data
only when the server leaves the connection open, do we get to the point of reading the error (401) but that defeats the purpose, since now the connection is left open.
I would really appreciate any hints or tips. I understand that without the code its hard to help, so I can add more source at any time.
EDIT:
If I do not check for errors between writing the request, and reading the server reply, then I do get the actual HTTP 401 error. However this seems counter-intuitive, and I am not sure why this happens or if it is supposed to happen.
The observed behavior is allowed per the HTTP specification.
A client or server may close the socket at anytime. The server can provide a response and close the connection before the client has finished transmitting the request. When writing the body, it is recommended that clients monitor the socket for an error or close notification. From the RFC 7230, HTTP/1.1: Message Syntax and Routing Section 6.5. Failures and Timeouts:
6.5. Failures and Timeouts
A client, server, or proxy MAY close the transport connection at any time. [...]
A client sending a message body SHOULD monitor the network connection for an error response while it is transmitting the request. If the client sees a response that indicates the server does not wish to receive the message body and is closing the connection, the client SHOULD immediately cease transmitting the body and close its side of the connection.
On a graceful connection closure, the server will send a response to the client before closing the underlying socket:
6.6. Tear-down
A server that sends a "close" connection option MUST initiate a close of the connection [...] after it sends the response containing "close". [...]
Given the above behaviors, there are three possible scenarios. The async_write() operation completes with:
success, indicating the request was written in full. The client may or may not have received the HTTP Response yet
an error, indicating the request was not written in full. If there is data available to be read on the socket, then it may contain the HTTP Response sent by the server before the connection terminated. The HTTP connection may have terminated gracefully
an error, indicating the request was not written in full. If there is no data available to be read on the socket, then the HTTP connection was not terminated gracefully
Consider either:
initiating the async_read() operation if the async_write() is successful or there is data available to be read
void write_request(
const boost::system::error_code & error,
const std::size_t bytes_transferred)
{
// The server may close the connection before the HTTP Request finished
// writing. In that case, the HTTP Response will be available on the
// socket. Only stop the call chain if an error occurred and no data is
// available.
if (error && !socket_->available())
{
return;
}
boost::asio::async_read_until(*socket_, buffer_, "\r\n", ...);
}
per the RFC recommendation, initiate the async_read() operation at the same time as the async_write(). If the server indicates the HTTP connection is closing, then the client would shutdown its send side of the socket. The additional state handling may not warrant the extra complexity
I have a small ssl client that I've programmed in boost 1.55 asio, and I'm trying to figure out why boost::asio::ssl::stream::async_shutdown() always fails. The client is very similar (almost identical) to the ssl client examples in the boost documentation, in that it goes through an boost::asio::ip::tcp::resolver::async_resolve() -> boost::asio::ssl::stream::async_connect() -> boost::asio::ssl::stream::async_handshake() callback sequence. All of this works as expected and the async_handshake() callback gets an all-clear boost::system::error_code.
From the async_handshake() callback, I call async_shutdown() (I don't transfer any data - this object is more for testing the handshake):
void ClientCertificateFinder::handle_handshake(const boost::system::error_code& e)
{
if ( !e )
{
m_socket.async_shutdown( boost::bind( &ClientCertificateFinder::handle_shutdown_after_success,
this,
boost::asio::placeholders::error ) );
}
else
{
m_handler( e, IssuerNameList() );
}
}
handle_shutdown_after_success() is then called, but always with an error? The error is value=2 in asio.misc, which is 'End of file'. I've tried this with a variety of ssl servers, and I always seem to get this asio.misc error. That this isn't an underlying openssl error suggests to me that I might be misusing asio in some way...?
Anyone know why this might be happening? I was under the impression that shutting down the connection with async_shutdown() was The Right Thing To Do, but I guess I could just call boost::asio::ssl::stream.lowestlayer().close() to close the socket out from under openssl if that's the expected way to do this (and indeed the asio ssl examples seem to indicate that this is the right way of shutting down).
For a cryptographically secure shutdown, both parties musts execute shutdown operations on the boost::asio::ssl::stream by either invoking shutdown() or async_shutdown() and running the io_service. If the operation completes with an error_code that does not have an SSL category and was not cancelled before part of the shutdown could occur, then the connection was securely shutdown and the underlying transport may be reused or closed. Simply closing the lowest layer may make the session vulnerable to a truncation attack.
The Protocol and Boost.Asio API
In the standardized TLS protocol and the non-standardized SSLv3 protocol, a secure shutdown involves parties exchanging close_notify messages. In terms of the Boost.Asio API, either party may initiate a shutdown by invoking shutdown() or async_shutdown(), causing a close_notify message to be sent to the other party, informing the recipient that the initiator will not send more messages on the SSL connection. Per the specification, the recipient must respond with a close_notify message. Boost.Asio does not automatically perform this behavior, and requires the recipient to explicitly invoke shutdown() or async_shutdown().
The specification permits the initiator of the shutdown to close their read side of the connection before receiving the close_notify response. This is used in cases where the application protocol does not wish to reuse the underlying protocol. Unfortunately, Boost.Asio does not currently (1.56) provide direct support for this capability. In Boost.Asio, the shutdown() operation is considered complete upon error or if the party has sent and received a close_notify message. Once the operation has completed, the application may reuse the underlying protocol or close it.
Scenarios and Error Codes
Once an SSL connection has been established, the following error codes occur during shutdown:
One party initiates a shutdown and the remote party closes or has already closed the underlying transport without shutting down the protocol:
The initiator's shutdown() operation will fail with an SSL short read error.
One party initiates a shutdown and waits for the remote party to shutdown the protocol:
The initiator's shutdown operation will complete with an error value of boost::asio::error::eof.
The remote party's shutdown() operation completes with success.
One party initiates a shutdown then closes the underlying protocol without waiting for the remote party to shutdown the protocol:
The initiator's shutdown() operation will be cancelled, resulting in an error of boost::asio::error::operation_aborted. This is the result of a workaround noted in the details below.
The remote party's shutdown() operation completes with success.
These various scenarios are captured in detailed below. Each scenario is illustrated with a swim-line like diagram, indicating what each party is doing at the exact same point in time.
PartyA invokes shutdown() after PartyB closes connection without negotiating shutdown.
In this scenario, PartyB violates the shutdown procedure by closing the underlying transport without first invoking shutdown() on the stream. Once the underlying transport has been closed, the PartyA attempts to initiate a shutdown().
PartyA | PartyB
-------------------------------------+----------------------------------------
ssl_stream.handshake(...); | ssl_stream.handshake(...);
... | ssl_stream.lowest_layer().close();
ssl_stream.shutdown(); |
PartyA will attempt to send a close_notify message, but the write to the underlying transport will fail with boost::asio::error::eof. Boost.Asio will explicitly map the underlying transport's eof error to an SSL short read error, as PartyB violated the SSL shutdown procedure.
if ((error.category() == boost::asio::error::get_ssl_category())
&& (ERR_GET_REASON(error.value()) == SSL_R_SHORT_READ))
{
// Remote peer failed to send a close_notify message.
}
PartyA invokes shutdown() then PartyB closes connection without negotiating shutdown.
In this scenario, PartyA initiates a shutdown. However, while PartyB receives the close_notify message, PartyB violates the shutdown procedure by never explicitly responding with a shutdown() before closing the underlying transport.
PartyA | PartyB
-------------------------------------+---------------------------------------
ssl_stream.handshake(...); | ssl_stream.handshake(...);
ssl_stream.shutdown(); | ...
| ssl_stream.lowest_layer().close();
As Boost.Asio's shutdown() operation is considered complete once a close_notify has been both sent and received or an error occurs, PartyA will send a close_notify then wait for a response. PartyB closes the underlying transport without sending a close_notify, violating the SSL protocol. PartyA's read will fail with boost::asio::error::eof, and Boost.Asio will map it to an SSL short read error.
PartyA initiates shutdown() and waits for PartyB to respond with a shutdown().
In this scenario, PartyA will initiate a shutdown and wait for PartyB to respond with a shutdown.
PartyA | PartyB
-------------------------------------+----------------------------------------
ssl_stream.handshake(...); | ssl_stream.handshake(...);
ssl_stream.shutdown(); | ...
... | ssl_stream.shutdown();
This is a fairly basic shutdown, where both parties send and receive a close_notify message. Once the shutdown has been negotiated by both parties, the underlying transport may either be reused or closed.
PartyA's shutdown operation will complete with an error value of boost::asio::error::eof.
PartyB's shutdown operation will complete with success.
PartyA initiates shutdown() but does not wait for PartyB to responsd.
In this scenario, PartyA will initiate a shutdown and then immediately close the underlying transport once close_notify has been sent. PartyA does not wait for PartyB to respond with a close_notify message. This type of negotiated shutdown is allowed per the specification and fairly common amongst implementations.
As mentioned above, Boost.Asio does not directly support this type of shutdown. Boost.Asio's shutdown() operation will wait for the remote peer to send its close_notify. However, it is possible to implement a workaround while still upholding the specification.
PartyA | PartyB
-------------------------------------+---------------------------------------
ssl_stream.handshake(...); | ssl_stream.handshake(...)
ssl_stream.async_shutdown(...); | ...
const char buffer[] = ""; | ...
async_write(ssl_stream, buffer, | ...
[](...) { ssl_stream.close(); }) | ...
io_service.run(); | ...
... | ssl_stream.shutdown();
PartyA will initiate an asynchronous shutdown operation and then initiate an asynchronous write operation. The buffer used for the write must be of a non-zero length (null character is used above); otherwise, Boost.Asio will optimize the write to a no-op. When the shutdown() operation runs, it will send close_notify to PartyB, causing SSL to close the write side of PartyA's SSL stream, and then asynchronously wait for PartyB's close_notify. However, as the write side of PartyA's SSL stream has closed, the async_write() operation will fail with an SSL error indicating the protocol has been shutdown.
if ((error.category() == boost::asio::error::get_ssl_category())
&& (SSL_R_PROTOCOL_IS_SHUTDOWN == ERR_GET_REASON(error.value())))
{
ssl_stream.lowest_layer().close();
}
The failed async_write() operation will then explicitly close the underlying transport, causing the async_shutdown() operation that is waiting for PartyB's close_notify to be cancelled.
Although PartyA performed a shutdown procedure permitted by the SSL specification, the shutdown() operation was explicitly cancelled when underlying transport was closed. Hence, the shutdown() operation's error code will have a value of boost::asio::error::operation_aborted.
PartyB's shutdown operation will complete with success.
In summary, Boost.Asio's SSL shutdown operations are a bit tricky. The inconstancies between the initiator and remote peer's error codes during proper shutdowns can make handling a bit awkward. As a general rule, as long as the error code's category is not an SSL category, then the protocol was securely shutdown.