NGINX.COM
Web Server Load Balancing with NGINX Plus

本文转载自 The New Stack

QUIC 是 HTTP/3 协议的标准传输层,但当开始在 NGINX 中构建对 QUIC 的支持时,我们很快就遇到了意想不到的挑战。问题的核心就是 OpenSSL 库对 QUIC 的支持 — 或者更确切地说,该库缺乏对 QUIC 的支持。虽然 BoringSSL、QuicTLS 和 LibreSSL 等其他 SSL 库支持 QUIC,但都不如 OpenSSL 常用,OpenSSL 实际上是互联网安全传输的标准库。

NGINX 作为高性能代理和 Web 服务器一直享有盛誉,但我们在类 QUIC 实现方面的选择似乎受到了不可控力的限制。面对看似不可能跳出的困境,我们另辟蹊径,在 OpenSSL 层之上新增了一项能力,用作它与 NGINX 之间的接口。这样,我们就可以充分利用 OpenSSL 市场领先的速度和安全性以及最新的 Web 技术进步。我们将此特性称为 OpenSSL 兼容层,从 NGINX 1.25.0 版本开始提供。

下面我们来深入了解一下整个过程中的技术分析和考虑,以及 OpenSSL 兼容层的优势和局限。

 

QUIC 握手

QUIC 数据包保护基于 TLS 1.3。这意味着 QUIC 的握手过程类似于 TLS 1.3,包括使用对称密码和握手密钥交换期间生成的密钥进行数据包加密。值得一提的是,有了密钥之后,QUIC 数据包加密就很简单了,所以在此不再赘述,而是重点探讨 QUIC 握手本身。

在与 TCP 建立 TLS 连接时,我们只需调用 SSL_do_handshake() 即可,后者读写握手 TLS 记录。不过,QUIC 握手消息流并未封装在 TLS 记录中,而是被嵌入到 QUIC CRYPTO 帧中,需要 SSL 库进行特殊处理。BoringSSL 和 OpenSSL 采用不同的 QUIC 握手方式,下面我们来更详细地了解一下这两种方法。

使用 BoringSSL 进行 QUIC 握手

BoringSSL 支持通过额外函数和回调绕过 TCP I/O 和 TLS 记录封装。通过这种方法,我们将原始 TLS 握手消息流传入和传出 SSL 库(只负责握手),同时实现数据包和帧封装。从理论上讲,这种方法支持最高效的 QUIC 实现。

使用 OpenSSL 进行 QUIC 握手

OpenSSL 的目标是将握手、QUIC 流、拥塞控制等所有 QUIC 内部机制隐藏在库中。通过这种方法,我们使用支持 QUIC 的标准 TLS API 来返回引用 QUIC 连接或数据流的 SSL* 对象。

不过,有人担心在 OpenSSL 中实现整个 QUIC 协议涉及大量工作。鉴于大部分工作似乎与 TLS 无关,因此过长的开发时间成为 OpenSSL 方法的一大问题。目前的路线图计划为 3.4 版本提供全面的 QUIC 支持,但可能需要一段时间才能发布并面向用户提供。有鉴于此,我们建议使用 BoringSSL QUIC API。

 

BoringSSL QUIC API

下面我们来了解一下 BoringSSL QUIC API 及其工作原理,并探讨它为何不是 NGINX QUIC 实现的理想解决方案。以下库目前支持此 API:

:QuicTLS 是一个 OpenSSL 分支,提供类似于 BoringSSL 的 QUIC API。

BoringSSL QUIC API 的主要函数包括:

  • SSL_set_quic_method() — 设置点击此处输入 text.callbacks set_read_secret()set_write_secret(),以启用用于 QUIC 数据包加密和解密的加密密钥,以及用于访问原始传出握手消息的回调 add_handshake_data()
  • SSL_provide_quic_data() — 传递 QUIC 握手过程中传入的原始握手消息。
  • SSL_set_quic_transport_params()SSL_get_peer_quic_transport_params() — 设置并检索 QUIC 传输参数。

在服务器端实现 QUIC 时,使用 SSL_set_quic_method() 设置回调后,我们需要为每个传入的 CRYPTO 帧调用 SSL_provide_quic_data(),然后再调用 SSL_do_handshake(),如下代码所示。在执行 SSL_do_handshake() 期间,库将调用 add_handshake_data() 回调。 传给回调的数据应封装成 CRYPTO 帧并发送给对等端。该库还将调用带有密钥的 set_read_secret()set_write_secret() 回调,以便在握手和应用级别加密并解密 QUIC 数据包。

 

不过,支持 BoringSSL QUIC API 的库并不像 OpenSSL 那样普及。大多数 Linux 和 NGINX 用户已经通过软件包安装了 OpenSSL。从源代码构建另一个 SSL 库并将其安装到每台需要 QUIC 支持的服务器上,这不是一个适合所有人的实用解决办法。当时由于选择有限,我们决定构建 OpenSSL 兼容层,现在每个官方 NGINX 发行版都默认提供该层。此特性支持用户使用现有 OpenSSL 库,通过 QUIC 运行 NGINX。

 

基于 TLS API 的 QUIC API

由于 QUIC 使用与 TLS 1.3 相同的握手消息,因此首先想到的是在常规 OpenSSL TLS API 之上创建一个实现 BoringSSL QUIC API 的层。但此举会使代码复杂化,并带来以下挑战:

  1. 将原始 TLS 握手消息传入和传出 OpenSSL
  2. 从 OpenSSL 中获取握手和应用加密级别的密钥
  3. 将 QUIC 传输参数传入和传出 OpenSSL

下文将介绍 NGINX 如何应对这些挑战。请注意,为清晰起见,所提供的代码段均经过简化。如欲获取完整的源代码,请点击此处

 

握手消息

从 OpenSSL 连接中获取输入和输出的最简单方法是使用 BIO,这是 OpenSSL 提供的用于基本 I/O 抽象的 API。有了这个 API,就可以构建(并解析)包含握手消息的 TLS 记录,让 OpenSSL 误以为它已经建立了真正的 TLS 连接。虽然这种方法在处理 ClientHello 和 ServerHello 等初始明文数据包时非常有效,但 OpenSSL 要求后续请求和响应 TLS 记录都经过加密。这意味着我们需要重新加密所有初始输入后的 TLS 记录(就像真正的客户端那样),并在将数据传递给 QUIC 层之前解密输出。

值得庆幸的是,有更好的解决方案来解决第二个问题,无需解密。OpenSSL 提供了一个使用 SSL_set_msg_callback() 设置消息回调的选项,可返回未加密的 TLS 握手消息输出。使用该回调时,我们只需设置 null BIO 即可忽略所有握手输出。

不过,对于输入似乎没有有效的绕过办法。这意味着我们需要在初始输入 TLS 记录后按以下方式对其进行加密:

 

加密密钥

BoringSSL QUIC API 的一个有用功能是设置回调 set_read_secret()set_write_secret(),它们能够返回用于在不同级别加密和解密 QUIC 数据包的密钥。同样,我们可以使用 SSL_CTX_set_keylog_callback() 从 OpenSSL 获取这些密钥。该函数设置的回调接收 TLS 会话期间生成的密钥。密钥的名称和值以文本形式传递。这与 Wireshark 等数据包分析器解密会话时通常所用的方法相同。

要实现 QUIC,我们需要以下密钥:

  • CLIENT_HANDSHAKE_TRAFFIC_SECRET
  • SERVER_HANDSHAKE_TRAFFIC_SECRET
  • CLIENT_TRAFFIC_SECRET_0
  • SERVER_TRAFFIC_SECRET_0

密钥获取方式如下:

 

传输参数

BoringSSL QUIC API 提供了两个函数,用于获取和设置 QUIC 传输参数。虽然 OpenSSL 不提供这类函数,但具有多个通用函数,可处理新的 TLS 扩展。其中一个函数是 SSL_CTX_add_custom_ext(),使用方式如下:

 

0-RTT 问题

如前所述,QUIC 握手和 TLS 握手的主要区别在于握手消息的封装方式。但遗憾的是,这并非唯一的区别。在 QUIC 中禁止使用一种特殊的 TLS 握手消息:EndOfEarlyData 消息。出于这个原因,以及 QUIC 0-RTT 的处理方式与 TLS 不同,我们只能处理没有任何早期数据的 QUIC 握手。一种解决方案是在从 QUIC 握手转换时,将 EndOfEarlyData 注入 TLS 握手。但这样做会改变 TLS 会话脚本哈希,进而导致 Finished MAC 和 PSK Binder 出错。虽然可以重新计算这些值,但如此一来会大大加剧代码的复杂性。我们目前正在努力解决这一问题。截至目前,0-RTT 已在 OpenSSL 兼容层中禁用。

 

结语

NGINX 团队和社区始终追求不断创新。当面临没有理想解决方案可用的技术挑战时,我们就会发挥所长:新创一种方法,并与全球同仁共享。开放透明是 NGINX 的核心理念。我们围绕 OpenSSL 兼容层就是这么做的,很高兴能与大家分享。

请访问 nginx.orgnginx.org.cn,了解我们在此特性及其他创新方面取得的进展。同时,欢迎大家微信添加小 N 助手(微信号:nginxoss)加入社区微信群,在群内提出问题、给出建议和发表看法。

Hero image
免费白皮书:
NGINX 企阅版全解析

助力企业用户规避开源治理风险,应对开源使用挑战

关于作者

Roman Arutyunyan

首席软件工程师

关于作者

Michael Vernik

高级产品经理

关于 F5 NGINX

F5, Inc. 是备受欢迎的开源软件 NGINX 背后的商业公司。我们为现代应用的开发和交付提供一整套技术。我们的联合解决方案弥合了 NetOps 和 DevOps 之间的横沟,提供从代码到用户的多云应用服务。访问 nginx-cn.net 了解更多相关信息。