安全断开asio SSL套接字的正确方法是什么?

12
一个基于 boost-asio 的 SSL/TLS TCP 套接字是通过在 tcp::socket 上实现一个 ssl::stream 来完成的:
boost::asio::ssl::stream<boost::asio::ip::tcp::socket> ssl_socket;

TLS协议中,加密安全关闭涉及各方交换close_notify消息。简单地关闭最低层可能会使会话容易受到截断攻击的影响。
boost asio ssl async_shutdown always finishes with an error?中,@Tanner Sansbury详细描述了SSL关闭过程,并提出在关闭套接字之前使用async_shutdown后跟async_write来断开SSL流的建议。
ssl_socket.async_shutdown(...);
const char buffer[] = "";
async_write(ssl_socket, buffer, [](...) { ssl_socket.close(); }) 

执行 ssl::stream 上的 async_shutdown 会发送 SSL close_notify 消息并等待对端响应。在 async_shutdown 后向流写入内容的目的是为了在未等待响应即可关闭套接字时得到通知。然而,在当前 (1.59) 版本的 boost 中,调用 async_write 失败...
如何优雅地关闭 boost asio ssl 客户端? 中,@maxschlepzig 建议关闭底层 TCP 套接字的接收器。
ssl_socket.lowest_layer()::shutdown(tcp::socket::shutdown_receive);

这会产生一个“短读取”错误,并且在错误处理程序中检测到时调用“async_shutdown”函数:
// 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();
}

取消套接字上的读/写操作,然后调用SSL异步关闭,即:

boost::system::error_code ec;
ssl_socket.cancel(ec);
ssl_socket.async_shutdown([](...) { ssl_socket.close(); };

目前我正在使用这种方法,因为它与当前版本的boost兼容。

如何正确/最佳地安全断开boost-asio SSL套接字?


1
这取决于情况。如果你收到了一个 close_notify 通知,你并不需要回复一个。 - user207421
2
是的,你是的,但在发送 close_notify 后,无需等待接收到它。 - kenba
3个回答

8

要进行安全断开连接,执行关闭操作,然后在关闭完成后关闭底层传输。因此,您当前正在使用的方法将执行安全断开连接:

boost::system::error_code ec;
ssl_socket.cancel(ec);
ssl_socket.async_shutdown([](...) { ssl_socket.close(); };

请注意,当前的async_shutdown操作将在以下任一情况下被视为完成:
  • 远程对等方已收到close_notify
  • 远程对等方关闭了套接字。
  • 该操作已被取消。
因此,如果资源绑定到套接字或连接的生命周期,则这些资源将保持活动状态,等待远程对等方采取行动或直到本地取消操作。但是,为了安全关闭,不需要等待close_notify响应。如果资源绑定到连接,并且在本地发送关机后连接被认为已死亡,则不等待远程对等方采取行动可能是值得的。
ssl_socket.async_shutdown(...);
const char buffer[] = "";
async_write(ssl_socket, boost::asio::buffer(buffer),
    [](...) { ssl_socket.close(); })

当客户端发送一个close_notify消息时,客户端保证不会在安全连接上发送其他数据。实质上,async_write()被用于检测客户端何时发送了close_notify,并且在完成处理程序中,将关闭底层传输,导致async_shutdown()boost::asio::error::operation_aborted完成。如链接的答案所述,预计async_write()操作将失败。

... 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.


1
谢谢@Tanner,我完全同意一旦客户端发送了“close notify”,它就不必等待响应,而且我确实像你在这里和你的(优秀的)链接答案中描述的那样使用了async_shutdown后跟一个async_write。不幸的是,在使用boost 1.59时,现在调用async_write会在engine.ipp中崩溃,而不是使用SSL_R_PROTOCOL_IS_SHUTDOWN回调!那么最好如何断开SSL套接字而不崩溃? - kenba
1
@kenba 我会尽力找时间调查一下。这个应用程序是否保证所有异步操作都在同一个隐式或显式的线程中执行?经常情况下,Boost.Asio SSL 的崩溃是违反并发要求的结果。 - Tanner Sansbury
请接受我的诚挚道歉,@TannerSansbury。我完全支持您计划提交的更改SSL关闭以不等待对等方的“close_notify”的PR。然而,请注意,Chris Kohlhoff在GitHub上的账户已经超过3个月没有活动了。我不知道他是忙于准备将asio包含在C++17中还是什么?我只希望他没事。就我而言,我对您的尊重比以往任何时候都要大。 - kenba
5
使用 async_shutdown() 时同时启动 async_write() 的意图是为了检测本地数据流写入端关闭的时间,从而使得可以释放资源而无需等待远程对等方关闭他们的 SSL 数据流的一侧。如果等待 async_shutdown() 完成,那么 async_write() 将不再必要。 - Tanner Sansbury
4
我遇到了kenba提到的问题,在engine.ipp中发生了崩溃,只有在多个线程运行ioservice::run时才会出现。正如Tanner Sansbury所指出的那样,这是由于违反了SSL并发性问题所致。解决方法是将关闭和写入操作绑定: ssl_socket.async_shutdown(strand_.wrap(..) ); const char buffer[] = ""; async_write(ssl_socket_, boost::asio::buffer(buffer), strand_.wrap(....)); - 希望这对某人有所帮助。 - Benjamin Close
显示剩余2条评论

2
我可能回答晚了,但我想报告一下我的经验。 到目前为止,这个解决方案(使用boost 1.78)在客户端和服务器上都没有产生任何可见的错误:
// 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();

沙盒服务器使用:openssl s_server -cert server.crt -key server.key -4 -debug

使用这个解决方案后,服务器在 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

之前我使用了这段代码(用于普通TCP和SSL套接字)

sock->lowest_layer().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);

sock->lowest_layer().cancel(ec);

sock->lowest_layer().close();

旧代码在利用ssl socket时,在服务器上产生了这个错误:

read from 0x55eb3d40b430 [0x55eb3d423513] (5 bytes => 0 (0x0))
ERROR
shutting down SSL
CONNECTION CLOSED

如前所述,为避免此行为,客户端应使用ssl::stream::async_shutdownssl::stream::shutdown发送close_notify

async_write()的技巧可能在您想利用异步async_shutdown()函数而不是同步shutdown()时非常有用


你是否测试过这个程序在连接不正常关闭的服务器上的表现? - Edward Kigwana

1
我只是想在这里添加我的回应,以帮助那些使用较新版本的Boost的人。 在我的情况下,我正在使用Boost 1.82.0,并且我已经实现了@tanner-sansbury提出的第一个选项,其中使用了ssl_stream.async_shutdownssl_stream.lowest_layer()->close()的组合。
客户端和服务器端的代码看起来完全相同(只有日志消息和可选的变量名更改):
tls_socket.lowest_layer().cancel();  // Cancels all ongoing operations
// Sends a 'close_notify' message to the server
tls_socket_ptr_.async_shutdown([this](const boost::system::error_code& ec)
    {           
        timer_->cancel();
        if (!ec)
        {
            std::cout << "CloseSocket - Cleanly shutdown SSL stream. Now we'll shutdown the socket..." << std::endl;
            ClosePlainTCPSocket();
        }
        else
        {
            std::cout << "BrainClient::CloseSocket - Something went wrong while shutting down SSL stream! Exitting anyway..." << std::endl;
        }
    });


void ClosePlainTCPSocket()
{
    boost::system::error_code tcp_socket_shutdown_ec;
    tls_socket.lowest_layer().shutdown(
        boost::asio::ip::tcp::socket::shutdown_both, tcp_socket_shutdown_ec);

    if (!tcp_socket_shutdown_ec)
    {
        std::cout << "BrainClient::ClosePlainTCPSocket - TCP socket was successfully shutdown, closing socket..." << std::endl;
        tls_socket_ptr_.lowest_layer().close();
    }
}


此外,我在上述代码后面添加了一个boost::asio::steady_timer,这样我就不会永远等待异步关闭完成。如果计时器超时,我会报告一个错误,并尝试使用shutdown()同步关闭服务器端。
如果代码中有任何错误,请原谅,因为我只取了部分代码来尽可能简化示例。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接