boost asio ssl async_shutdown 总是以错误结束?

Posted

技术标签:

【中文标题】boost asio ssl async_shutdown 总是以错误结束?【英文标题】:boost asio ssl async_shutdown always finishes with an error? 【发布时间】:2014-10-24 14:16:03 【问题描述】:

我有一个在 boost 1.55 asio 中编写的小型 ssl 客户端,我试图弄清楚为什么 boost::asio::ssl::stream::async_shutdown() 总是失败。该客户端与 boost 文档中的 ssl 客户端示例非常相似(几乎相同),因为它经历了 boost::asio::ip::tcp::resolver::async_resolve() -> boost::asio::ssl::stream::async_connect() -> boost::asio::ssl::stream::async_handshake() 回调序列。所有这些都按预期工作,async_handshake() 回调得到一个完全清除的boost::system::error_code

async_handshake() 回调中,我调用async_shutdown()(我不传输任何数据 - 此对象更多用于测试握手):

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() 然后被调用,但总是出错?错误是 asio.misc 中的 value=2,即“文件结束”。我已经用各种 ssl 服务器尝试过这个,我似乎总是得到这个asio.misc 错误。这不是潜在的 openssl 错误,这表明我可能以某种方式滥用 asio...?

有人知道为什么会这样吗?我的印象是关闭与async_shutdown() 的连接是正确的事情,但我想我可以调用boost::asio::ssl::stream.lowestlayer().close() 从openssl 下关闭套接字,如果这是执行此操作的预期方式(确实如此) asio ssl 示例似乎表明这是正确的关闭方式)。

【问题讨论】:

可能相关:***.com/questions/22575315/… 我见过那个,但它似乎与 openssl 相关 - 特别是,他们收到的错误是类别 asio.ssl,而不是 asio.misc。 【参考方案1】:

对于加密安全关机,双方必须通过调用shutdown()async_shutdown() 并运行io_serviceboost::asio::ssl::stream 执行关机操作。如果操作以没有SSL category 的error_code 完成,并且在部分关闭可能发生之前未取消,则连接已安全关闭,并且可以重用或关闭底层传输。简单地关闭最低层可能会使会话容易受到truncation attack 的攻击。


协议和 Boost.Asio API

在标准化的TLS 协议和非标准化的SSLv3 协议中,安全关闭涉及各方交换close_notify 消息。在Boost.Asio API方面,任何一方都可以通过调用shutdown()async_shutdown()来发起关机,导致向对方发送close_notify消息,通知接收方发起者不会再发送更多消息在 SSL 连接上。根据规范,收件人必须回复close_notify 消息。 Boost.Asio 不会自动执行此行为,需要收件人显式调用shutdown()async_shutdown()

规范允许关闭的发起者在收到close_notify 响应之前关闭其读取端的连接。这用于应用程序协议不希望重用底层协议的情况。不幸的是,Boost.Asio 目前 (1.56) 不直接支持此功能。在 Boost.Asio 中,shutdown() 操作在发生错误或当事方已发送并接收到 close_notify 消息时被视为完成。操作完成后,应用程序可以重用底层协议或关闭它。

场景和错误代码

一旦建立 SSL 连接,关机时会出现以下错误代码:

一方发起关闭,另一方关闭或已经关闭底层传输但未关闭协议: 发起者的shutdown() 操作将失败并出现 SSL 短读取错误。 一方发起关闭并等待远程方关闭协议: 启动器的关闭操作将完成,错误值为boost::asio::error::eof。 远程方的shutdown()操作成功完成。 一方启动关闭,然后关闭底层协议,而不等待远程方关闭协议: 发起者的shutdown()操作将被取消,导致boost::asio::error::operation_aborted的错误。这是以下详细信息中提到的解决方法的结果。 远程方的shutdown()操作成功完成。

下面详细介绍了这些不同的场景。每个场景都用一个类似游泳线的图表来说明,表明每一方在完全相同的时间点做什么。

PartyAPartyB 关闭连接后调用shutdown() 而不协商关闭。

在这种情况下,PartyB 违反了关闭过程,即在未先在流上调用 shutdown() 的情况下关闭底层传输。一旦底层传输关闭,PartyA 会尝试发起shutdown()

 PartyA                              | PartyB
-------------------------------------+----------------------------------------
 ssl_stream.handshake(...);          | ssl_stream.handshake(...);
 ...                                 | ssl_stream.lowest_layer().close();
 ssl_stream.shutdown();              |

PartyA 将尝试发送close_notify 消息,但写入底层传输将失败并显示boost::asio::error::eof。 Boost.Asio 将 explicitly map 底层传输的 eof 错误转换为 SSL 短读取错误,因为 PartyB 违反了 SSL 关闭程序。

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 调用shutdown() 然后PartyB 关闭连接而不协商关闭。

在这种情况下,PartyA 启动关闭。然而,当 PartyB 收到 close_notify 消息时,PartyB 在关闭底层传输之前从未明确响应 shutdown(),从而违反了关闭程序。

 PartyA                              | PartyB
-------------------------------------+---------------------------------------
 ssl_stream.handshake(...);          | ssl_stream.handshake(...);
 ssl_stream.shutdown();              | ...
                                     | ssl_stream.lowest_layer().close();

由于 Boost.Asio 的 shutdown() 操作在 close_notify 已发送和接收或发生错误时被视为完成,因此 PartyA 将发送 close_notify 然后等待响应。 PartyB 关闭底层传输而不发送close_notify,违反了 SSL 协议。 PartyA 的读取将失败并显示 boost::asio::error::eof,Boost.Asio 会将其映射到 SSL 短读取错误。

PartyA 发起shutdown() 并等待PartyB 回复shutdown()

在这种情况下,PartyA 将启动关闭并等待 PartyB 响应关闭。

 PartyA                              | PartyB
-------------------------------------+----------------------------------------
 ssl_stream.handshake(...);          | ssl_stream.handshake(...);
 ssl_stream.shutdown();              | ...
 ...                                 | ssl_stream.shutdown();

这是一个相当基本的关闭,双方发送和接收close_notify 消息。一旦双方协商关闭,底层传输可能会被重用或关闭。

甲方的关机操作将完成,错误值为boost::asio::error::eofPartyB 的关机操作将成功完成。

PartyA 发起shutdown() 但不等待PartyB 响应。

在这种情况下,PartyA 将启动关闭,然后在发送close_notify 后立即关闭底层传输。 PartyA 不会等待 PartyB 回复 close_notify 消息。规范允许这种类型的协商关闭,并且在实现中相当普遍。

如上所述,Boost.Asio 不直接支持这种类型的关闭。 Boost.Asio 的shutdown() 操作将等待远程对等方发送其close_notify。但是,可以在仍然遵守规范的情况下实施变通方法。

 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会先发起异步关机操作,然后再发起异步写操作。用于写入的缓冲区必须是非零长度(上面使用空字符);否则,Boost.Asio 会将写入优化为无操作。当shutdown()操作运行时,会向PartyB发送close_notify,导致SSL关闭PartyA的SSL流的写端,然后异步等待PartyBclose_notify。但是,由于 PartyA 的 SSL 流的写入端已关闭,async_write() 操作将失败并显示 SSL 错误,指示协议已关闭。

if ((error.category() == boost::asio::error::get_ssl_category())
     && (SSL_R_PROTOCOL_IS_SHUTDOWN == ERR_GET_REASON(error.value())))

  ssl_stream.lowest_layer().close();

失败的async_write() 操作将显式关闭底层传输,导致正在等待PartyBclose_notifyasync_shutdown() 操作被取消。

虽然 PartyA 执行了 SSL 规范允许的关闭过程,但当底层传输关闭时,shutdown() 操作被显式取消。因此,shutdown() 操作的错误代码将具有 boost::asio::error::operation_aborted 的值。 PartyB 的关机操作将成功完成。

总的来说,Boost.Asio 的 SSL 关闭操作有点棘手。在正确关闭期间发起方和远程对等方的错误代码之间的不一致可能会使处理有点尴尬。作为一般规则,只要错误代码的类别不是 SSL 类别,协议就会被安全关闭。

【讨论】:

优秀的答案,未来的参考!谢谢! 在花了几十个小时试图弄清楚如何处理 ssl::stream 对象的优雅关闭之后,这终于给了我想要的答案。 Christopher 应该在 asio 文档中添加一个特殊部分来澄清。 我担心示例代码,它具有误导性,因为它将位于堆栈上的 1 字节缓冲区的地址传递给async_write。如果函数在 async_write 完成之前返回,这可能会导致崩溃。相反,应该使用这个调用:async_write(ssl_stream, boost::asio::null_buffers, [](...) ssl_stream.close(); ) @Vinnie Fair 点。这些图并不是为了表示一个堆栈,而是为了捕捉事件的顺序(如果它是一个堆栈,io_service.run() 将使缓冲区保持活动状态)。我认为使用null_buffers 更改了检测 SSL 流的本地写入端何时关闭的所需条件(SSL_write 失败并出现关闭错误),因为ssl_stream.async_write_somenull_buffers 将被优化为无操作,从不尝试SSL_write 似乎PartyA initiates shutdown() but does not wait for PartyB to responsd. 版本不会使写操作失败,至少这是处理程序得到的error_code 所暗示的。是success

以上是关于boost asio ssl async_shutdown 总是以错误结束?的主要内容,如果未能解决你的问题,请参考以下文章

boost/asio/ssl 抛出“未定义的引用”错误

如何接受boost :: asio :: ssl :: stream 作为boost :: asio :: ip :: tcp :: socket类型的参数

boost asio ssl async_shutdown 总是以错误结束?

多线程应用程序上的 boost::asio::ssl 访问冲突

为啥 Boost.Asio SSL 请求返​​回 405 Not Allowed?

boost::asio::async_write 写入 ssl::stream 成功但服务器未获取