如何优雅地关闭 boost asio ssl 客户端?

2022-01-07 00:00:00 ssl openssl c++ boost boost-asio

客户端执行一些 ssl::stream::async_read_some()/ssl::stream::async_write() 调用和一些点需要退出,即需要关闭连接.

调用 ssl::stream<tcp_socket>::lowest_layer().close() 有效,但是(正如预期的那样)服务器(一个 openssl s_server -state ... 命令)在关闭连接时报告错误.

查看 API 的正确方法似乎是调用 ssl::stream::async_shutdown().

现在基本上有两种情况需要关机:

1) 客户端在 async_read_some() 回调中并对来自服务器的退出"命令做出反应.从那里调用 async_shutdown() 会在关闭回调中产生短读"错误.

这令人惊讶,但在谷歌搜索之后这似乎是正常行为 - 人们似乎必须检查它是否是一个真正的错误或不是这样的:

//const boost::system::error_code &ecif (ec.category() == asio::error::get_ssl_category() &&ec.value() == ERR_PACK(ERR_LIB_SSL, 0, SSL_R_SHORT_READ)) {//->不是真正的错误,只是正常的 TLS 关闭}

TLS 服务器似乎很高兴,不过 - 它报告:

完成关闭 SSL连接已关闭

2) async_read_some() 处于活动状态 - 但用户决定退出客户端(例如,通过来自 stdin 的命令).当从该上下文调用 async_shutdown() 时,会发生以下情况:

  • async_read_some() 回调执行时带有短读"错误代码 - 现在是预期的
  • async_shutdown() 回调执行时带有解密失败或记录错误 mac 错误代码 - 这是意外的

服务端不报错.

因此我的问题是如何使用 boost asio 正确关闭 TLS 客户端.

解决方案

解决第二个上下文中解密失败或错误记录 mac"错误代码的一种方法是:

a) 来自 stdin 处理程序调用:

ssl::stream::lowest_layer()::shutdown(tcp::socket::shutdown_receive)

b) 这导致 async_read_some() 回调以短读"错误"代码执行

c) 在那个错误"条件下的回调中 async_shutdown() 被调用:

//const boost::system::error_code &ecif (ec.category() == asio::error::get_ssl_category() &&ec.value() == ERR_PACK(ERR_LIB_SSL, 0, SSL_R_SHORT_READ)) {//->不是真正的错误:do_ssl_async_shutdown();}

d) async_shutdown() 回调执行时带有一个短读"错误代码,我们最终从这里调用:

<前>ssl::stream::lowest_layer()::close()

这些步骤导致连接关闭,客户端或服务器端没有任何奇怪的错误消息.

例如,当使用 openssl s_server -state ... 作为服务器时,它会报告 sutdown:

<前>SSL3 警报读取:警告:关闭通知完毕关闭 SSL连接已关闭接受

(最后一行是因为该命令接受新连接)

替代方案

代替lowest_layer()::shutdown(tcp::socket::shutdown_receive) 我们也可以调用

ssl::stream::lowest_layer()::cancel()

启动适当的关闭.它具有相同的效果,即它产生预定的 async_read_some() 回调的执行(但带有 operation_aborted 错误代码).因此,可以从那里调用 async_shutdown():

if (ec.value() == asio::error::operation_aborted) {cout<<"(不是真正的错误)
";do_async_ssl_shutdown();}

The client does some ssl::stream<tcp_socket>::async_read_some()/ssl::stream<tcp_socket>::async_write() calls and at some point needs to exit, i.e. it needs to shutdown the connection.

Calling ssl::stream<tcp_socket>::lowest_layer().close() works, but (as it is expected) the server (a openssl s_server -state ... command) reports an error on closing the connection.

Looking at the API the right way seems to be to call ssl::stream<tcp_socket>::async_shutdown().

Now there are basically 2 situation where a shutdown is needed:

1) Client is in the async_read_some() callback and reacts on a 'quit' command from the server. Calling from there async_shutdown() yields a 'short read' error in the shutdown callback.

This is surprising but after googling around this seems to be normal behaviour - one seem to have to check if it is a real error or not like this:

// 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, just a normal TLS shutdown
}

The TLS server seems to be happy, though - it reports:

DONE
shutting down SSL
CONNECTION CLOSED

2) A async_read_some() is active - but a user decides to exit the client (e.g. via a command from stdin). When calling async_shutdown() from that context following happens:

  • the async_read_some() callback is executed with a 'short read' error code - kind of expected now
  • the async_shutdown() callback is executed with a decryption failed or bad record mac error code - this is unexpected

The server side does not report an error.

Thus my question how to properly shutdown a TLS client with boost asio.

解决方案

One way to resolve the 'decryption failed or bad record mac' error code from the 2nd context is:

a) from inside the stdin handler call:

ssl::stream<tcp_socket>::lowest_layer()::shutdown(tcp::socket::shutdown_receive)

b) this results in the async_read_some() callback getting executed with a 'short read' 'error' code

c) in that callback under that 'error' condition async_shutdown() is called:

// 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();
}

d) the async_shutdown() callback is executed with a 'short read' error code, from where we finally call:

ssl::stream::lowest_layer()::close()

These steps result in a connection shutdown without any weird error messages on the client or server side.

For example, when using openssl s_server -state ... as server it reports on sutdown:

SSL3 alert read:warning:close notify
DONE
shutting down SSL
CONNECTION CLOSED
ACCEPT

(the last line is because the command accepts new connections)

Alternative

Instead of lowest_layer()::shutdown(tcp::socket::shutdown_receive) we can also call

ssl::stream<tcp_socket>::lowest_layer()::cancel()

to initiate a proper shutdown. It has the same effect, i.e. it yields the execution of the scheduled async_read_some() callback (but with operation_aborted error code). Thus, one can call async_shutdown() from there:

if (ec.value() == asio::error::operation_aborted) {
  cout << "(not really an error)
";
  do_async_ssl_shutdown();
}

相关文章