【问题标题】:Boost.Asio SSL thread safetyBoost.Asio SSL 线程安全
【发布时间】:2015-04-14 19:22:12
【问题描述】:

我是创建一个 所有 我的 SSL 套接字共享的链,还是每个 SSL 上下文一个链(由任何关联的套接字共享)?

Boost.Asio SSL 文档说明了这一点,但没有提及上下文。我认为这意味着我必须对所有内容只使用一个链,但我认为这是在 OpenSSL 支持多线程之前编写的。

SSL 和线程

SSL 流对象不执行自己的锁定。因此,所有异步 SSL 操作都必须在隐式或显式链中执行。请注意,这意味着在单线程程序中不需要同步(因此不会产生锁定开销)。

我很可能只有一个 SSL 上下文,但我想知道由 SSL 上下文或全局网络服务拥有链是否更合适。

我确实为CRYPTO_set_locking_callback 提供了一个处理程序,以防万一。

【问题讨论】:

  • 所以我很清楚...您建议在多线程程序中使用 OpenSSL 没有它的锁?您可能还想查看CRYPTO_lock - OpenSSL thread support
  • 最常用的方法是每个 SSL 连接使用一个链。
  • AFAIK boost::asio::ssl 钩子在 openSSL 指定的所有锁定机制中必须按照其自己的文档使用。
  • 我认为您可能也混淆了文档的措辞。它说的是 SSL 流,而不是 SSL 上下文。您是在询问共享上下文还是共享对 SSL 流的并发访问? David 也提出了这一点,请查看 ASIO 中的客户端/服务器示例。当一个新客户端连接时,它会获得处理程序给出的自己的session。该会话类专门用于客户端及其流,其中通常还有一个成员链。因此,每个连接一根。

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


【解决方案1】:

我认为这个线程有些混乱,因为有一些事情需要澄清。让我们从断言::asio::ssl::context == SSL_CTX 开始。两者合而为一。

其次,当使用boost::asio::ssl 时,除非您在绕过内部init 对象的地方做一些疯狂的事情,否则您无需手动设置加密锁定回调。正如您在源代码here 中看到的那样,这是为您完成的。

实际上,这样做可能会导致问题,因为init 对象的析构函数在假设它们已在内部完成此工作的情况下运行。对最后一点持保留态度,因为我没有深入审查过这一点。

第三,我相信您将 SSL 流与 SSL 上下文混淆了。为简单起见,将 SSL 流视为套接字,将 SSL 上下文视为套接字可用于各种 SSL 功能的单独对象,例如与特定协商密钥的握手,或作为服务器,提供有关您的服务器证书的信息到已连接的客户端,以便您可以与客户端握手。

提到链归结为防止对一个特定流(套接字)的可能同时 IO,而不是上下文。显然,尝试在同一个套接字上同时从同一个缓冲区读取和写入将是一个问题。因此,当您向各种::asio::async_X 方法提供由链包裹的完成处理程序时,您将强制执行特定的排序以防止上述情况。您可以在this answer 中阅读更多内容,该内容由比我了解更多的人提供。

现在就上下文而言,David Schwartz 在 cmets 和他写的另一个我需要挖掘的答案中指出,上下文本身的全部目的是提供有助于跨多个 SSL 流实现 SSL 功能的信息。考虑到它们的预期目的,他似乎暗示它们本质上必须是线程安全的。我相信他可能是在::asio::ssl::context 的上下文中说话,只是因为::asio::ssl 正确使用线程安全回调的方式,或者他只是在多线程程序中正确使用openSSL 的上下文中说话。

无论如何,除了关于 SO 的此类 cmets 和答案以及我自己的实践经验之外,很难在文档中找到具体的证据,或者明确定义线程安全与非线程安全之间的界限。 boost::asio::ssl:context 是,正如大卫也指出的那样,只是 SSL_CTX 的一个非常薄的包装。我还要补充一点,它旨在为使用底层结构提供更“c++ ish”的感觉。它的设计可能也是为了将::asio::ssl 和底层实现库解耦,但它没有实现这一点,两者是紧密结合的。 David 再次正确地提到这个瘦包装器的文档记录很差,必须查看实现才能获得洞察力。

如果您开始深入研究实现,有一种相当简单的方法可以找出在上下文方面什么是线程安全的,什么不是线程安全的。您可以在ssl_lib.c 等来源中搜索CRYPTO_LOCK_SSL_CTX

int SSL_CTX_set_generate_session_id(SSL_CTX *ctx, GEN_SESSION_CB cb)
{
    CRYPTO_w_lock(CRYPTO_LOCK_SSL_CTX);
    ctx->generate_session_id = cb;
    CRYPTO_w_unlock(CRYPTO_LOCK_SSL_CTX);
    return 1;
}

如您所见,使用了CRYPTO_w_lock,这使我们回到了有关openSSL和线程的官方页面here,其中指出:

OpenSSL 可以安全地用于提供的多线程应用程序中 至少设置了两个回调函数,locking_functionthreadid_func

现在我们回到这个答案第一段中链接的asio/ssl/detail/impl/openssl_init.ipp源代码,我们在哪里看到:

do_init()
  {
    ::SSL_library_init();
    ::SSL_load_error_strings();        
    ::OpenSSL_add_all_algorithms();

    mutexes_.resize(::CRYPTO_num_locks());
    for (size_t i = 0; i < mutexes_.size(); ++i)
      mutexes_[i].reset(new boost::asio::detail::mutex);
    ::CRYPTO_set_locking_callback(&do_init::openssl_locking_func);
    ::CRYPTO_set_id_callback(&do_init::openssl_id_func);

#if !defined(SSL_OP_NO_COMPRESSION) \
  && (OPENSSL_VERSION_NUMBER >= 0x00908000L)
    null_compression_methods_ = sk_SSL_COMP_new_null();
#endif // !defined(SSL_OP_NO_COMPRESSION)
       // && (OPENSSL_VERSION_NUMBER >= 0x00908000L)
  }

当然要注意:

CRYPTO_set_locking_callback
CRYPTO_set_id_callback

所以至少就::asio::ssl::context 而言,这里的线程安全与链无关,而与openSSL 工作有关的一切都与openSSL 的工作有关,因为openSSL 旨在在多线程程序中正确使用时工作。

回到最初的问题,现在已经解释了所有这些,大卫也在 cmets 中非常简单地给出了答案:

最常用的方法是每个 SSL 连接使用一个链。

以提供example.com 内容的HTTPS 服务器为例。服务器有一个单独的上下文,配置了诸如example.com 的证书之类的信息。客户端连接,此上下文用于所有连接的客户端以执行握手等。您将连接的客户端包装在一个新的 session 对象中,您可以在其中处理该客户端。在此会话中,您将拥有一个单链,无论是隐式还是显式,来保护 socket,而不是上下文。

虽然我无论如何都不是专家,并且我欢迎对此答案进行更正,但我已将我对这些主题的所有了解都应用到开源透明过滤 HTTPS 代理中。总行数超过 17K 的 cmets 与代码的比率略高于 50%,所以我所知道的一切都写在那里(无论是对还是错;))。如果您想查看这些东西的实际示例,您可以查看TlsCapableHttpBridge.hpp 源,它在每个主机、每个连接的基础上充当客户端和服务器。

服务器上下文和证书被欺骗/生成一次,并在平移多个线程的所有客户端之间共享。唯一完成的手动锁定是在上下文的存储和检索期间。网桥的每 side 有一根线,一根用于真正的下游客户端套接字,一根用于上游服务器连接,尽管它们在技术上甚至不是必需的,因为操作顺序无论如何都会创建一个隐式线.

请注意,该项目正在开发中,因为我正在重写很多东西(dep build 指令尚不存在),但就 MITM SSL 代码而言,一切都是功能性的,因此您正在查看一个功能齐全的类和相关的组件。

【讨论】:

    【解决方案2】:

    更新

    这个答案的要点受到了 David Schwarz 的质疑,我非常尊重他在这一领域的权威。

    有理由期​​望 ssl 上下文可以在线程之间共享 - 至少对于某些操作,如果只是为了促进 SSL 会话恢复。

    我认为 David 在 OpenSSL 使用 SSL 上下文方面有经验。 Boost ASIO 依次使用 that(至少在我知道的所有平台上)。因此,要么 David 写一个答案来分享他的知识,要么你/我必须花一些时间阅读 OpenSSL 文档和 Boost Asio 源代码来找出适用于 Boost Asio 的ssl::context 用法的有效约束。

    以下是当前记录的约束。

    [后面是旧答案]

    Thread Safety


    一般来说,并发使用不同的对象是安全的,但并发使用单个对象是不安全的。但是,诸如 io_service 之类的类型提供了更强的保证,即并发使用单个对象是安全的。

    从逻辑上讲,由于文档没有特别提到 ssl_context 类的线程安全性,因此您必须得出结论,它不是。

    如果您使用某些特定的钩子(如您提到的),您知道底层 SSL 库支持这一点并不重要。这only 告诉您制作 ssl_context 线程感知可能并不难。

    但在您(与库开发人员合作)提供此补丁之前,它不可用。

    长话短说,您可以从单个链访问每个 ssl_context

    【讨论】:

    • 这意味着您不能支持 SSL 会话恢复,或者您必须始终在同一链中使用所有 SSL 连接。
    • @DavidSchwartz 如果您能解释这是如何发生的,我很乐意纠正。我已经多次看到这个问题被问到,并且每次都调查了文档和代码(包括这次),我同意答案似乎不令人满意。也许有了您的论点,我们可以向 Asio 开发人员提出问题/请求?
    • 您认为第三种选择是什么?如果您对所有 SSL 连接使用相同的 SSL 上下文,那么您声称您需要一个用于所有 SSL 连接的单链。如果您使用不同的 SSL 上下文,则会话恢复将不起作用,因为会话缓存是 SSL 上下文的一部分。
    • @DavidSchwartz 我没有其他选择。只是对 Boost Asio 中似乎存在的限制或缺乏共享上下文的文档感到好奇。由于没有这样的文件,我没有信心声称它没问题。一方面,我不知道会话恢复
    • @sehe 是的,大约 3 年前,我使用 boost python 将 mitmproxy 带入了一个 C++ 应用程序来使用它。当时我正在开发一个内容过滤器,所以我需要更强大的东西并且必须自己推出,但我对此几乎一无所知,哈哈。所以我一直在学习并使用 boost::asio 制作一个,所以它是可移植的。内置了对 adblock plus 过滤器和 CSS 选择器的支持(在 html 内容类型上使用 gumbo 解析器和 CSS 选择器库)。
    【解决方案3】:

    我会说这取决于您的协议是什么样的。如果是 HTTP,则无需使用(显式)链,因为您不会并行读取和写入套接字。

    其实会出问题的,是这样的代码:

    void func()
    {
        async_write(...);
        async_read(...);
    }
    

    因为这里 - 如果你的 io_service() 有一个与之关联的线程池 - 实际的读取和写入可以由多个线程并行执行。

    如果每个 io_service 只有一个线程,则不需要线程。例如,如果您正在实施 HTTP,情况也是如此。在 HTTP 中,由于协议的布局,您不会并行读取和写入套接字。你从客户端读取一个请求——尽管这可能在几个异步调用中完成——然后你以某种方式处理请求和标头,然后你(异步与否)发送你的回复。

    您也可以在 ASIO 的 strand 文档中阅读几乎相同的内容。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-11-21
      • 1970-01-01
      • 1970-01-01
      • 2021-07-12
      相关资源
      最近更新 更多