四年前,NGINX 博客首次提及 QUIC 和 HTTP/3,现在 QUIC 实现终于即将合并到 NGINX 开源版的主线版中,和大家一样,我们对此也翘首以待。鉴于整个等待过程比较长,因此如果您之前没有仔细研究过 QUIC,也是可以理解的。
但现在,作为开发人员或网站管理员,您有必要了解 QUIC 如何将一些网络连接的具体操作从操作系统迁移到 NGINX(以及所有 HTTP 应用)。即使网络连接并非您的职责所在,但采用 QUIC 即意味着网络工作(至少在某种程度上)已是您的份内工作。
本文深入探讨了 QUIC 涉及的关键网络连接和加密概念,为清晰起见,我们简化了一些细节并省略了不必要的信息。虽然这样做可能会忽略一些细节,但我们的目的是为您提供足够的信息,以便您在自己的环境中有效使用 QUIC,或者至少能够帮助您入门。
如果您对 QUIC 完全不了解,建议您先阅读我们之前的相关博文并观看我们的概述视频。
如欲更细致、更全面地了解 QUIC,强烈推荐您阅读 IETC QUIC 工作组精心编制的《QUIC 传输协议的可管理性》文档及本文中链接的其他材料。
为何要关注 QUIC 网络连接和加密问题?
目前,大多数用户都无需特别了解客户端和 NGINX 之间网络连接的详尽细节。毕竟,在 HTTP/1.x 和 HTTP/2 中,操作系统负责设置客户端和 NGINX 之间的传输控制协议 (TCP) 连接。NGINX 只是使用已建立的连接。
但在 QUIC 中,创建、验证和管理连接的职责从底层操作系统转移给了 NGINX。NGINX 现在不再接收已建立的 TCP 连接,而是接收用户数据报协议 (UDP) 数据报流,然后将其解析为客户端连接和数据流。NGINX 现在还负责处理丢包、重连和拥塞控制。
此外,QUIC 将连接发起、版本协商和加密密钥交换都整合到了单个连接建立操作中。虽然 QUIC+HTTP/3 和 TCP+HTTP/1+2 的 TLS 加密处理方式大致相同,但对于四层负载均衡器、防火墙和安全设备等下游设备而言,可能具有显著差别。
最终,在这些变化的综合作用下,用户体验的安全性、速度和可靠性得到了提升,而 NGINX 的配置或操作却没有太大改变。不过,NGINX 管理员至少需要了解 QUIC 和 NGINX 的一些基本知识,即便只是为了在出现问题时尽可能地缩短平均故障调查时间。
(值得一提的是,虽然本文重点介绍了 HTTP 操作,因为 HTTP/3 需要使用 QUIC,但 QUIC 也可以用于其他协议。典型示例为 DNS over QUIC——在 RFC 9250 中被定义为“基于专用 QUIC 连接的 DNS”。)
下面让我们深入了解一些 QUIC 网络连接细节。
TCP 与 UDP 的比较
QUIC 对用于在客户端和服务器之间传输 HTTP 应用数据的底层网络协议进行了重大调整。
如前所述,TCP 是一种用于传输 HTTP Web 应用数据的协议,旨在通过 IP 网络可靠地传输数据。它不仅拥有定义明确、易于理解的机制来建立连接和确认数据接收,而且还采用了各种算法和技术来管理不可靠和拥挤的网络上常见的丢包和延迟问题。
虽然 TCP 可确保传输可靠性,但在性能和延迟方面有所折衷。此外,数据加密没有内置于 TCP 中,必须单独实施。考虑到不断变化的 HTTP 流量模式,改进或扩展 TCP 也很困难,因为 TCP 处理在 Linux 内核中执行,任何更改都必须经过仔细设计和测试,以免对整个系统的性能和稳定性带来意想不到的影响。
另一个问题是,在许多情况下,客户端和服务器之间的 HTTP 流量流经多个 TCP 处理设备,如防火墙或负载均衡器(统称为“中间盒”),而中间盒实现 TCP 标准变更的速度可能会很慢。
因此,QUIC 使用 UDP 作为传输协议。UDP 能够像 TCP 一样通过 IP 网络传输数
据,但特意舍弃了连接建立和可靠交付功能,这种精简使其适用于大量更注重效率和速度而非可靠性的应用。
然而,对于大多数 Web 应用来说,可靠的数据传输至关重要。由于底层 UDP 传输层无法提供可靠的数据传输,因此这些功能需要由 QUIC(或应用本身)来提供。幸运的是,QUIC 在这方面比 TCP 更具优势:
- QUIC 处理在 Linux 用户空间中执行,其中某项特定操作的问题对整个系统构成的风险较小。这有助于新功能的快速开发。
- 上述“中间盒”通常只对 UDP 流量进行最低程度的处理,因此不会限制对 QUIC 协议的增强。
QUIC 网络简析
QUIC 数据流是包含 HTTP/3 请求或响应(或任何其他应用数据)的逻辑对象。为了在网络端点之间进行传输,它们被封装在多个逻辑层内,如图所示。
从外向内有以下逻辑层和对象:
- UDP 数据报 – 包含一个指定源端口和目标端口(以及长度与校验和数据)的请求头,后跟一个或多个 QUIC 数据包。数据报是信息通过网络从客户端向服务器传输的单位。
- QUIC 数据包 – 包含一个 QUIC 请求头和一个或多个 QUIC 数据帧。
-
QUIC 请求头 – 包含关于数据包的元数据。请求头有两种:
- 长请求头,在建立连接时使用。
- 短请求头,在连接建立后使用。它包含(除其他数据以外)连接 ID、数据包编号和关键短语(用于跟踪哪些密钥被用于加密数据包,以支持密钥轮换)。对于特定连接和关键短语来说,数据包编号是独一无二的(并且不断增加)。
- 数据帧 – 包含类型、数据流 ID、偏移量和流式数据。流式数据分散在多个数据帧中,但可以使用连接 ID、数据流 ID 和偏移量进行组合,从而以正确的顺序呈现数据块。
- 数据流 – 单个 QUIC 连接内的单向或双向数据流。每个 QUIC 连接均可支持多个独立的数据流,每个数据流都有自己的数据流 ID。如果某个包含一些数据流的 QUIC 数据包丢失,不在所丢数据包之中的数据流的进度不受影响(这是避免 HTTP/2 队头阻塞问题的关键。)数据流可以是双向并由任意端点创建。
连接建立
通过熟悉的 SYN
/ SYN-ACK
/ ACK
三次握手建立 TCP 连接:
QUIC 连接的建立遵循类似的步骤,但效率更高。它还将地址验证作为加密握手的一部分包含在连接设置中。地址验证可防范流量放大攻击,阻止攻击者向服务器发送包含针对目标受害者伪造的源地址信息的数据包。攻击者希望服务器向受害者生成比攻击者自身所能生成的数据包更多或更大的数据包,从而导致超出受害者处理能力的流量激增。(详情请见 RFC 9000 第 8 节“QUIC:基于 UDP 的多路复用和安全传输”。)
在建立连接的过程中,客户端和服务器提供独立的连接 ID,这些 ID 在 QUIC 请求头中进行编码,提供与客户端源 IP 地址无关的简单连接标识。
但由于 QUIC 连接的初始建立还需要执行 TLS 加密密钥交换操作,因此对于服务器来说,其计算工作量要高于 TCP 连接建立期间生成的简单 SYN-ACK
响应。这也为分布式拒绝服务 (DDoS) 攻击提供了一个潜在向量,因为在进行密钥交换操作之前不会验证客户端 IP 地址。
但您可通过将 quic_retry
指令设置为 on
,配置 NGINX 在复杂的加密操作开始之前验证客户端 IP 地址。在这种情况下,NGINX 会向客户端发送一个包含令牌的重试数据包,客户端必须将该令牌添加到连接设置数据包中。
该机制有点类似于 TCP 的三次握手,关键是要确定客户端拥有其所提供的源 IP 地址。如果不进行该检查,NGINX 等 QUIC 服务器可能很容易遭到使用伪造源 IP 地址的 DoS 攻击。(另一种可缓解此类攻击的 QUIC 机制是要求所有初始连接数据包都必须包含至少 1,200 个字节,以增加发送这些数据包的成本。)
此外,重试数据包可将连接细节编码到其发送给客户端的连接 ID 中,从而缓解类似于 TCP SYN
洪水攻击的攻击(其中服务器资源被存储在内存中的大量已打开但未完成的握手耗尽);这还有一个好处,即无需保留服务器端信息,因为可使用客户端随后提交的连接 ID 和令牌复原连接信息。该技术类似于 TCP SYN
cookie。此外,NGINX 等 QUIC 服务器可提供一个会过期的令牌,以用于日后来自客户端的连接,从而加速连接恢复。
使用连接 ID 能够让连接独立于底层传输层,因此网络连接变更不会导致连接中断。这一点将在下文“平滑地管理客户端 IP 地址变更”一节中详细论述。
丢包检测
在建立连接(并启用加密,如下详述)后,HTTP 请求和响应可以在客户端和 NGINX 之间来回流动。UDP 数据报不断被发送和接收。然而,许多因素可能会导致其中一些数据报丢失或延迟。
TCP 具有复杂的机制来确认数据包交付,检测丢包或延迟,并管理丢失数据包的重传,从而向应用层交付正确排序的完整数据。UDP 缺乏这种机制,因此是在 QUIC 层实施拥塞控制和丢包检测。
- 客户端和服务器都会对其接收的每个 QUIC 数据包发送明确的确认消息(尽管只包含低优先级数据帧的数据包不会得到立即确认)。
-
如果包含需要可靠传输的数据帧的数据包在设定的超时时间后没有得到确认,就会被视为丢失。
超时时间取决于数据包中的内容。例如,对于建立加密和设置连接所需的数据包,超时时间较短,因为这关乎 QUIC 握手性能。
- 当一个数据包被视为丢失时,丢失的数据帧将通过带有新序列号的新数据包进行重传。
- 数据包接收方使用数据包上的数据流 ID 和偏移量,以正确的顺序组合传输的数据。数据包编号只决定发送顺序,而非数据包的组合次序。
- 由于接收端的数据组合与传输顺序无关,因此某个数据包的丢失或延迟只影响其中包含的单个数据流,而不会影响连接中的所有数据流。这消除了影响 HTTP/1.x 和 HTTP/2 的队头阻塞问题,因为数据流不属于传输层。
丢包检测的完整描述不在本入门手册论述范围内。请参考 RFC 9002“QUIC 丢包检测和拥塞控制”,详细了解超时确定机制以及传输中允许的未确认数据量。
平滑地管理客户端 IP 地址变更
客户端 IP 地址(在应用会话的上下文中被称为“源 IP 地址”)在会话期间可能会发生变化,例如当 VPN 或网关更改其公共地址或智能手机用户离开 WiFi 覆盖区域时,会强制切换到蜂窝网络。此外,网络管理员通常为 UDP 流量设置的超时时间要短于 TCP 连接,这导致网络地址转换 (NAT) 重绑定的几率增加。
QUIC 提供了两种机制来减少可能发生的中断:客户端可以主动通知服务器其地址将发生变化,服务器也可以平滑地处理客户端地址的意外变更。由于连接 ID 在整个转换过程中保持一致,因此未确认的数据帧可以被重传到新的 IP 地址。
在 QUIC 会话期间更改源 IP 地址可能会影响下游负载均衡器(或其他四层网络连接组件),因为这些负载均衡器(或其他四层网络组件)使用源 IP 地址和端口来确定哪台上游服务器将接收特定 UDP 数据报。为了确保正确的流量管理,四层网络设备的提供商将需要对它们进行更新以处理 QUIC 连接 ID。如欲进一步了解未来负载均衡和 QUIC,请参见 IETF 草案“QUIC-LB:生成可路由的 QUIC 连接 ID”。
加密
在连接建立中,我们提到初始 QUIC 握手不只是建立连接。不同于 TCP 的 TLS 握手,在 UDP 中,密钥和 TLS 1.3 加密参数的交换也是初始连接的一部分。该功能消除了多次交换,并在客户端恢复之前连接时实现了零往返时间 (0-RTT)。
除了将加密握手整合到连接建立流程中之外,QUIC 还比 TCP+TLS 加密了更多的元数据。甚至在交换密钥之前,初始连接数据包就已加密;尽管窃听者仍能够破解密钥,但与未加密的数据包相比,需要下更大的功夫。这有助于更好地保护数据,例如攻击者和网络审查人员关注的服务器名称指示符 (SNI)。图 5 显示了与 TCP+TLS 相比,QUIC 加密了更多的敏感元数据(红色)。
QUIC 有效载荷中的所有数据均已使用 TLS 1.3 加密。这有两个优点:禁止使用过时、易受攻击的密码套件和散列算法,以及必须使用向前保密 (FS) 密钥交换机制。向前保密可防止攻击者解密数据,即使攻击者捕获了私钥和流量副本。
低 RTT 和零 RTT 连接可缩短延迟
减少在传输任何应用数据之前必须在客户端和服务器之间完成的通信往返次数可提高应用性能,特别是在具有较高延迟的网络上。
TLS 1.3 通过单次往返即可建立加密连接,并实现了连接恢复的零次往返,但对于 TCP 来说,这意味着必须在 TLS Client Hello 之前进行握手。
通过将加密操作与连接设置合在一起进行,QUIC 实现了真正的 0-RTT 连接重建,其中客户端可以在第一个 QUIC 数据包中发送请求,这能够消除第一个请求之前连接建立的初次往返,从而缩短延迟。
在本例中,客户端发送了一个 HTTP 请求,该请求使用前一个连接中所用的参数进行了加密,而且为了验证地址,还包含了一个由服务器在之前连接中提供的令牌。
遗憾的是,0-RTT 连接恢复不提供向前保密,因此初始客户端请求并未像交换中的其他流量那样经过安全加密。除第一个请求以外的所有其他请求和响应均受向前保密保护。但问题是,初始请求也极易遭受重放攻击,即攻击者可以捕获初始请求并多次向服务器重放它。
对于许多应用和网站来说,0-RTT 连接恢复带来的性能提升超出了这些潜在的漏洞风险,但这需要您自己进行权衡。
NGINX 默认禁用该功能。如要启用该功能,请将 ssl_early_data
指令设置为 on
。
使用 Alt-Svc
请求头从 HTTP/1.1 转变为 HTTP/3
几乎所有的客户端(尤其是浏览器)都通过 TCP/TLS 建立初始连接。如果服务器支持 QUIC+HTTP/3,则会返回一个 Alt-Svc
请求头中包含 h3
参数的 HTTP/1.1 响应。然后,客户端可以选择是使用 QUIC+HTTP/3 还是继续使用早期版本 HTTP。(值得一提的是,Alt-Svc
请求头[定义请见 RFC 7838]早于 QUIC 出现,也可用于其他目的。)
Alt-Svc
请求头告知客户端,备用主机、协议或端口(或它们的组合)提供相同的服务。此外,客户端还可以得知此服务可用时长的保守预估。
举例:
Alt-Svc: h3=":443" |
HTTP/3 在该服务器的 443 端口上可用 |
Alt-Svc: h3="new.example.com:8443" |
HTTP/3 在服务器 new.example.com 的 8443 端口上可用 |
Alt-Svc: h3=":8443"; ma=600 |
HTTP/3 在此服务器的 8443 端口上可用,并且至少在 10 分钟内可用 |
虽然不是强制要求,但在大多数情况下,服务器都会被配置为在与 TCP+TLS 相同的端口上响应 QUIC 连接。
如要将 NGINX 配置为包含 Alt-Svc
请求头,请使用 add_header
指令。在本例中,$server_port
变量意味着 NGINX 在客户端发送 TCP+TLS 请求的端口上接受 QUIC 连接,其中 86,400 表示 24 小时:
add_header Alt-Svc 'h3=":$server_port"; ma=86400';
结语
本文提供了有关 QUIC 的入门知识,希望能够帮助您大致了解 QUIC 关键网络连接和加密操作。
如欲更全面地了解如何针对 QUIC + HTTP/3 配置 NGINX,请阅读我们的博文《NGINX QUIC+HTTP/3 预览版的二进制包现已发布》,或者观看我们的网络研讨会《亲自体验 NGINX 和 QUIC+HTTP/3》。
如欲详细了解 QUIC+HTTP/3 的所有 NGINX 指令以及有关安装预构建二进制文件或从源代码进行构建的完整说明,请访问 NGINX QUIC 网站。