SSL的间歇性问题,使用Boost / Asio

时间:2014-12-01 17:19:45

标签: c++ multithreading ssl openssl boost-asio

问题:

我看到间歇性问题导致SSL连接被丢弃。我能够在我的测试环境中重现这个问题(50k调用中有1个失败)。我正在测试的软件是SMTP / POP3 / IMAP服务器。

在SMTP客户端和服务器之间来回发送几条命令后,成功执行握手后,SSL连接将被删除。

我的服务器软件(在Windows上运行)使用以下方式编写:

  • 提升:1.56.0
  • OpenSSL:1.0.1j

客户端应用程序使用.NET3.5,SslStream

编写

该应用程序与HTTP Server 3 in the Boost examples

非常相似

我正在制作的电话如下:

boost::asio::async_read_until(ssl_socket_, receive_buffer_, delimitor, strand_.wrap(AsyncReadCompletedFunction));

在AsyncReadCompletedFunction中,错误包含以下内容:

  • error_code :: value = 67567722
  • error_code :: message =块类型不是01

有时错误是:

  • error_code :: value = 67530866
  • error_code :: message = padding check failed

或者:

  • error_code :: value = 336265225
  • error_code :: message = PEM lib

这些错误由OpenSSL设置。

隔离

  1. 当将调用io_service :: run()的线程数从15减少到1时,问题就会消失。
  2. 我没有看到非SSL连接的问题。
  3. 我已经确保所有对Asio的调用都经过了一次。对async_write / async_read的调用和完成处理程序都在一个strand中执行,如下所示。

    std::shared_ptr<TCPConnection> shared_this = shared_from_this();
    
    strand_.post([AsyncWriteCompletedFunction, send_buffer_, shared_this]
    {
            boost::asio::async_write (shared_this->ssl_socket_, send_buffer_, shared_this->strand_.wrap(AsyncWriteCompletedFunction));
    });
    
  4. 我测试了多个变体,例如只是将处理程序包装在一个链中,而只是在一个链中调用boost :: asio :: async_write。

  5. 为了进行测试,我介绍了一个recursive_mutex,我在做任何事情之前放置了一个scoped_lock。连接类中的所有方法都是通过获取此锁来开始的,因此没有两个方法同时执行。
  6. 我已经在我的方法中引入了同步日志记录文件,并确认没有重叠的读/写调用:

    2014-12-01 13:12:45.362: Session 2254, Starting AsyncWrite - Call to boost::asio::async_write
    2014-12-01 13:12:45.362: Session 2254, AsyncWriteCompleted - Write completion handler called
    2014-12-01 13:12:45.362: Session 2254, Starting AsyncRead - Call to boost::asio::async_read
    2014-12-01 13:12:45.362: Session 2254, AsyncReadCompleted - Read completion handler called, with error:
            The read operation failed. Bytes transferred: 0 Remote IP: 127.0.0.1, Session: 2254, Code: 67567722, Message: block type is not 01
    
  7. 我认为这可能是我用来运行测试的客户端中的错误。但它是单线程的,并使用具有同步调用的循环实现。我已经仔细检查了.NET Framework 3.5中SslStream类的文档,但是也看不到任何错误(客户端代码非常简单)。

  8. 测试在Windows XP Service Pack 3上运行。我正在其他操作系统上运行测试:es,但到目前为止,我没有看到那里。

    编辑1:

    我已经安装了Wireshark来跟踪SMTP流量。我看到以下内容:

    1. 握手成功完成。
    2. 服务器发送欢迎信息(“220 HI ESMTP”)
    3. 客户端发送命令EHLO test
    4. 服务器使用其支持的命令进行响应
    5. 客户端发送命令AUTH LOGIN
    6. 服务器请求用户名
    7. 客户端发送用户名。这是最后的第二行。
    8. 服务器断开客户端连接。 (RST)。
    9. 查看服务器日志时,我可以看到服务器从未收到过步骤7中发送的客户端用户名。 async_read操作已完成,并且调用了完成处理程序,错误代码为336265225:PEM lib“

      Wireshark trace 我已经没有想法进行测试了。有人建议应该尝试什么?

1 个答案:

答案 0 :(得分:1)

此问题是由Asio SSL实施中的错误引起的。向Asio Github issue tracker报告了它。

当OpenSSL发生错误时,会将错误代码推送到队列中。单次调用OpenSSL可能会导致多个错误代码被推送到队列。例如,OpenSSL中的低级组件可能会将一个错误推送到队列,而另一个更高级别的组件可能会推送另一个错误。 (它可以被视为一堆错误)。

这些队列链接到该线程。在Windows的Asio imlplementation中,这是使用thread-id(int)完成的。所以每个线程都有自己的包含错误的队列。

Asio执行SSL功能后,只会从队列中删除第一个错误。如果已将多个项目推送到队列,则剩余的项目将被留下。这样做的结果是,当Asio稍后调用SSL函数并检查队列时,它可能会从上一个操作中获取错误,并认为当前操作已出现故障,即使它没有。

有时当Asio调用OpenSSL时,它会先清除队列。例如,函数add_certificate_authority通过执行:: ERR_clear_error()开始。似乎其他人有同样的问题并声称这是正确的解决方案。但是Asio中的其他功能,例如async_read不会这样做。

运行我的测试套件时发现了我的问题。测试套件包含许多测试错误处理的测试,例如通过向SSL服务器发送无效数据,尝试使用密码无效的私钥等等,以及这里开始的乐趣: / p>

我的测试套件会触发代码:

 context.set_password_callback(callbackfunc)
 context.use_private_key_file(myfile, boost::asio::ssl::context::pem)

回调函数将返回错误的密码(以测试是否正确处理)。 use_private_key_file将调用SSL_CTX_use_PrivateKey_file,将两个错误推送到队列。但asios实现use_private_key_file只删除第一项。 use_private_key_file正确地向我的应用程序代码报告错误。

我的应用程序代码检测到这一点,并触发重新启动服务组件(清理,重新启动线程等)。实际的Windows进程将继续运行。

一段时间后,当我调用async_read时,该调用可能恰好在一个线程上执行,该线程与我在一小时前调用use_private_key_file的线程具有相同的ID。它是真正不同的线程(原始线程终止),但它们碰巧具有相同的Thread-Id。在Asio调用SSL_read之后,它将检查错误队列中当前线程id并检索先前在use_private_key_file调用中报告的错误。

这就是async_read如何成功但仍然失败,错误指的是完全不同的东西。