Boost.Asio SSL 线程安全

Boost.Asio SSL thread safety

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

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

SSL and Threads

SSL stream objects perform no locking of their own. Therefore, it is essential that all asynchronous SSL operations are performed in an implicit or explicit strand. Note that this means that no synchronisation is required (and so no locking overhead is incurred) in single threaded programs.

我很可能只会有一个 SSL 上下文,但我想知道 strand 由 SSL 上下文拥有还是由全球网络服务拥有更合适。

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

更新

这个答案的主旨受到 David Schwarz 的质疑,我非常尊重他在这方面的权威。

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

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

以下是当前记录的约束。

[旧答案文本如下]

Thread Safety


In general, it is safe to make concurrent use of distinct objects, but unsafe to make concurrent use of a single object. However, types such as io_service provide a stronger guarantee that it is safe to use a single object concurrently.

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

如果您使用某些特定的挂钩(如您提到的),那么您知道底层 SSL 库支持它并不重要。 only 告诉您 make ssl_context 线程感知可能并不难。

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

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

我认为这个话题有些混乱,因为有几件事需要澄清。让我们从断言 ::asio::ssl::context == SSL_CTX 开始。二者为一

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

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

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

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

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

无论如何,除了关于 SO 的此类评论和答案以及我自己的实践经验之外,很难在文档中找到这方面的具体证据,或者明确定义什么是线程安全和什么不是线程安全的界限。 boost::asio::ssl:context 就像 David 也指出的那样,只是 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 can safely be used in multi-threaded applications provided that at least two callback functions are set, locking_function and threadid_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 而言,这里的线程安全与 strands 无关,而与 openSSL 工作有关,因为 openSSL 被设计为在多线程程序中正确使用时工作。

回到最初的问题,现在解释了所有这些,David 在评论中也非常简单地给出了答案:

The most common method is to use one strand per SSL connection.

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

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

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

请注意,该项目正在开发中,因为我正在重写很多东西(dep 构建说明尚不存在),但就 MITM SSL 代码而言,一切都正常运行,因此您正在寻找一个完整的功能 class 和相关组件。

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

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

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

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

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

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