OpenSSL QUIC 流接收缓冲区(Stream Receive Buffers)模块设计解析
2026/9/11 2:23:54 网站建设 项目流程

OpenSSL QUIC 流接收缓冲区(Stream Receive Buffers)模块设计解析

【免费下载链接】opensslGeneral purpose TLS and crypto library项目地址: https://gitcode.com/GitHub_Trending/ope/openssl

导读

本文围绕 OpenSSL 仓库中 QUIC 实现的设计文档 stream-receive-buffers.md 展开,深入解析 QUIC 接收方向的核心存储模块:Stream Receive Buffers(流接收缓冲区)。该模块负责暂存解包后得到的 STREAM 帧数据,直到应用程序通过SSL_read()读取为止,是连接"RX 解包器"与"前端 I/O API"之间的关键数据中转站。读完本文,你将掌握该模块的 MVP 需求、SSL_set_max_stored_stream_data()SSL_set_max_unprocessed_packet_data()两个新增 API 的设计意图、QUIC_RSTREAM/SFRAME_LIST的底层数据结构与不变量,以及针对恶意对端内存放大攻击的防御策略。

模块定位:为什么 QUIC 需要专门的接收缓冲区

与 TCP 不同,QUIC 的数据流具有以下两个特性,直接催生了接收缓冲区模块(文档开篇即定义了它的职责):

  1. 乱序到达:STREAM 帧可能以任意顺序到达,只有等更低 offset 的数据全部到齐后,上层才能获得连续的数据流;
  2. 非同步消费:数据包可能在应用程序调用SSL_read()之前就已经到达并被解密,必须先暂存起来。

因此,文档明确给出了该模块的使命:"This is a QUIC specific module that retains the received stream data until the application reads it with SSL_read() or any future stream read calls."—— 它是 QUIC 特有的、用于保留已接收流数据直到应用读取的模块。

从仓库源码看,这一职责由QUIC_RSTREAM(QUIC Receive Stream Manager)承担。在 include/internal/quic_stream.h 中,QUIC_RSTREAM被描述为"responsible for storing the received stream data frames until the application is able to read the data",并且每个可接收数据的流(单向接收流,或双向流的接收方向)都会实例化一个。

MVP 阶段的核心需求

设计文档为 MVP(最小可行产品)阶段识别出以下需求,它们是理解后续所有设计决策的出发点:

需求说明
乱序暂存携带流帧的数据包任意顺序到达时,必须存储数据,直到所有更早 offset 的数据都收到
读取前暂存数据包可能早于应用调用SSL_read()到达,必须先存储
存储上限与流控应用应能设置存储数据量上限;利用流控限制对端不要发送更多数据。否则恶意对端可通过无限发送流入的流数据帧触发 DoS
读取后释放数据经SSL_read()交给应用后,即可释放存储,并抬高流控上限
重叠帧处理对端重传时会重建流数据帧,实现必须正确处理与先前帧部分或完全重叠的帧

其中"存储上限 + 流控联动"是安全关键点:文档明确指出"Without the flow control limit a rogue peer could trigger a DoS via unlimited flow of incoming stream data frames"(没有流控限制,恶意对端可以通过无限制的流入流数据帧触发 DoS)。

可选的零拷贝(单次复制)需求

MVP 之外,设计文档还提出了一个"理想态"的可选需求:

为了支持未来流读取调用的单次复制操作,接收数据时不应将数据从解密后的数据包中复制出来存储。实际存储的仅是一个由 offset、length、数据指针组成的列表,外加一个指向存储实际帧数据的解密 QUIC 数据包的指针。

这条需求决定了整个实现的形态:默认采用"引用而非拷贝"策略——SFRAME_LIST中的每个条目并不持有数据副本,而是持有指向解密数据包内数据的指针,并通过引用计数控制数据包的生命周期。这一点在 quic_sf_list.h 与 quic_rstream.c 中得到了完整印证(详见下文实现细节)。

新增的公开 API

设计文档提出了两个面向应用的新增 API,用于解决"存储多少"与"未处理数据包占多少内存"这两个问题:

int SSL_set_max_stored_stream_data(SSL *stream, size_t length);
  • 作用:调整stream上当前的数据流控上限,允许在应用读取之前存储length字节的 QUIC 流数据;
  • 联动行为:OpenSSL 会在应用读取已存储数据时,自动、恰当地发送 MAX_STREAM_DATA 帧,无需应用干预。
int SSL_set_max_unprocessed_packet_data(SSL *connection, size_t length);
  • 作用:设置connection允许分配的未处理 QUIC 数据包数据量上限(字节数);
  • 该接口与下文"Other considerations(其他考量)"一节直接相关——它是应对恶意对端内存放大攻击时,从"零拷贝引用"回退到"复制"策略的触发开关。

需要说明的是,这两个 API 目前仍停留在设计阶段:在全仓库范围内检索,二者仅出现在 stream-receive-buffers.md 设计文档中,尚未在公开头文件中落地。此外文档中一处笔误将第三个函数写为SSL_set_max_unprocessed_quic_packet_data()(与第二个 API 含义相同),阅读源码时需注意区分。

与其他 QUIC 实现模块的接口

设计文档按模块边界给出了 Receive Buffers 的交互矩阵,这些接口关系在仓库源码中大多已有实现佐证。

前端 I/O API(Front End I/O API)

  • SSL_read():从存储缓冲区复制数据(若可用),并最终触发对已无引用价值的未处理数据包的释放;
  • SSL_peek()SSL_pending()SSL_has_pending():窥探存储缓冲区,获取已存储数据的信息。

对应的底层能力在 quic_stream.h 的QUIC_RSTREAMAPI 中均有体现:

int ossl_quic_rstream_read(QUIC_RSTREAM *qrs, unsigned char *buf, size_t size, size_t *readbytes, int *fin); int ossl_quic_rstream_peek(QUIC_RSTREAM *qrs, unsigned char *buf, size_t size, size_t *readbytes, int *fin); int ossl_quic_rstream_available(QUIC_RSTREAM *qrs, size_t *avail, int *fin);

值得注意的是,源码还提供了文档未细述的ossl_quic_rstream_get_record()/ossl_quic_rstream_release_record()一对接口(见 quic_stream.h),用于零拷贝读取:前者返回第一个可读数据块的起始指针与长度,后者在应用消费后释放(可部分释放)该记录——这正是文档中"single copy operation"愿景的实现雏形。

RX 解包器(RX Depacketizer)

Receive Buffers 模块通过ssl_queue_data()回调获取流数据。在 quic_rx_depack.c 中,解包器解析出 STREAM 帧后调用:

ossl_quic_rstream_queue_data(rstream, parent_pkt, f.offset, f.data, f.len, 0);

(同一文件中还有一处携带fin标志的调用,见 quic_rx_depack.c。)可以看到数据交付时连同parent_pkt(解密数据包)一起传入,为"引用而非拷贝"提供了数据源。

模块还使用ossl_qrx_pkt_wrap_up_ref()ossl_qrx_pkt_wrap_release()函数对包含未处理数据的解密数据包进行引用计数与释放。在 quic_stream.h 中对ossl_quic_rstream_queue_data()有明确注释:"Thepkt_wraprefcount is incremented if thedatais queued directly without copying"(若数据被直接引用入队而不复制,则递增 pkt_wrap 引用计数)。

流控(Flow Control)

Receive Buffers 模块为流控模块提供合适的值,用于发送 MAX_DATA 与 MAX_STREAM_DATA 帧(文档标注Details TBD,即细节待定)。从实现看,流控联动已在读取路径上落实:quic_rstream.c 中ossl_quic_rstream_read()在完成数据消费后,会调用ossl_quic_rxfc_on_retire(qrs->rxfc, *readbytes, rtt)向流控模块汇报已消费字节数,从而触发流控上限的更新——这正是文档"数据被读取后流控上限可抬高"这一需求的代码级印证。RTT 则由statm(统计模块)查询,用于计算合适的流控窗口更新时机。

QUIC 读记录层(QUIC Read Record Layer)

Receive Buffers 模块需要知道:何时应停止持有解密数据包、转而复制流数据——即达到SSL_set_max_unprocessed_quic_packet_data()所设上限时。文档标注此处Details TBD

从源码结构可以推断,这一"回退复制"逻辑由ossl_quic_rstream_move_to_rbuf()实现:它将SFRAME_LIST中所有帧的数据从数据包复制到内部环形缓冲区(ring buffer),使数据包可被释放,详见下文。

实现细节:QUIC_RSTREAM 与 SFRAME_LIST

设计文档给出了核心数据结构的设计:

QUIC_RSTREAM对象在SFRAME_LIST结构中保存接收到的流数据。这是一个部分重叠(从不完全重叠)数据帧的有序列表。每个列表项持有一个指向已接收数据包包装器的指针,用于引用计数,并在应用读取流数据后正确释放数据包数据。

SFRAME_LIST的定义与不变量在 include/internal/quic_sf_list.h 中完整给出:

typedef struct sframe_list_st { STREAM_FRAME *head, *tail; /* Is the tail frame final. */ unsigned int fin; /* Number of stream frames in the list. */ size_t num_frames; /* Offset of data not yet dropped */ uint64_t offset; /* Is head locked ? */ int head_locked; /* Cleanse data on release? */ int cleanse; } SFRAME_LIST;

头文件注释中声明的不变量(Invariant)正是设计文档要求的精确化表达:

  • 列表中的帧按起始/结束边界排序(sorted by the start and end bounds);
  • 不存在完全重叠的帧,也不存在被另一帧完全包含的帧(no fully overlapping frames or frames that would be fully encompassed by another frame);
  • 任何帧不允许 start > end;
  • 范围start 包含、end 排除(range start is inclusive, end is exclusive),以便标记空帧;
  • offset 指针永远不会越过第一帧内部。

设计文档补充了两个关键操作语义:

  • 插入不变量:每个列表项的range.start/range.end都大于前一项的对应值,该不变量在插入重叠流帧时得到保证,冗余帧会被释放;
  • 尾部插入优化:列表末尾的插入做了优化——在无丢包的理想情况下,新帧总是追加到末尾。

源码级的实现印证

ssl/quic/quic_rstream.c 是设计文档对应模块的落地实现,其核心结构为:

struct quic_rstream_st { SFRAME_LIST fl; QUIC_RXFC *rxfc; OSSL_STATM *statm; UINT_RANGE head_range; struct ring_buf rbuf; };

可见一个QUIC_RSTREAM实例内部包含:流帧列表fl(面向数据包的引用式存储)、流控句柄rxfc、统计句柄statm、当前被锁定的头部记录范围head_range,以及用于回退复制模式的环形缓冲区rbuf。构造函数ossl_quic_rstream_new()会同时初始化SFRAME_LIST并预分配环形缓冲区(quic_rstream.c)。

读取路径的实现逻辑(read_internal(),被ossl_quic_rstream_read()ossl_quic_rstream_peek()共用)直观地反映了"列表 + 可选环形缓冲区"的双态读取:

  1. 通过ossl_sframe_list_peek()迭代窥探列表头部连续帧;
  2. 若帧数据指针非空(仍引用自数据包),直接memcpy复制给应用;若指针为空,则从环形缓冲区按逻辑 offset 取数据(ring_buf_get_ptr()),必要时处理环回跨越(max_len < l分支);
  3. 读完后(drop语义,即read()路径)调用ossl_sframe_list_drop_frames()丢弃已消费帧,并同步ring_buf_cpop_range()弹出环形缓冲区数据。

而"回退到复制"模式的核心是ossl_quic_rstream_move_to_rbuf()(quic_rstream.c):它通过ossl_sframe_list_move_data()配合write_at_ring_buf_cb回调,把列表中所有帧的数据写入环形缓冲区(按逻辑 offset 写,ring_buf_write_at()),此后帧的数据指针变为 NULL,解包器即可释放相应数据包。这与设计文档"fall back to copying the data off the decrypted packet buffer once we reach a limit on unprocessed decrypted packets"的表述完全对应。

其他考量:恶意对端与内存放大攻击

设计文档用一整节篇幅分析了零拷贝策略带来的安全挑战,这也是理解整套设计的关键。

二次方内存放大问题

由于对端被允许重建流数据帧,且实现目标是单次复制(引用数据包而非拷贝),恶意对端可以这样攻击:

1st frame - offset 0 length 1000, 2nd frame - offset 1 length 1000, 3rd frame - offset 2 length 1000, and so on.

即每次只把 offset 偏移 1 字节、长度仍为 1000 的重叠帧。由于每个帧都要保留其数据包(引用),我们不得不为所有这些帧保留数据包数据,实际上使流数据流控上限呈二次方增长。文档并强调:"And this is not the only way how a rogue peer could make us occupy much more data than what is allowed"(这不是恶意对端让占用内存超出流控上限的唯一方式)。

为什么 MAX_DATA 无法兜底

一个直觉上的解法是用连接级 MAX_DATA 流控限制数据包缓冲区大小,但文档明确指出这是行不通的:

  • MAX_DATA 流控上限被定义为连接内所有流允许发送的数据总量,而非内存上限;
  • 数据包缓冲区包含的内容远不止流帧(还有 ACK、CRYPTO 等其他帧),尤其面对恶意对端时;
  • 因此MAX_DATA 上限不能用来限制数据包缓冲区的内存占用

防御策略:达到上限后回退到复制

解决思路是双层防御

  1. 一旦达到未处理解密数据包的上限,就回退为从解密数据包中复制数据(对应ossl_quic_rstream_move_to_rbuf());
  2. 未来还可能考虑:当收到部分重叠且一帧不是另一帧子集的流数据帧时,直接回退到复制模式(文档标注might also consider,属前瞻性设想)。

此外,MVP 阶段由于只支持单个双向流接收数据,文档给出了一个简化约束:该流的 MAX_DATA 流控上限应等于 MAX_STREAM_DATA 上限(因为只有一条流,连接级与流级上限天然一致)。

总结

Stream Receive Buffers 是 OpenSSL QUIC 实现中连接"解包"与"应用读取"的承重墙:它以SFRAME_LIST有序重叠帧列表为核心数据结构,默认以"引用解密数据包"的方式实现零拷贝暂存,通过QUIC_RSTREAM暴露读、窥探、记录锁/释放与环形缓冲区迁移等内部 API,并与流控模块联动完成 MAX_STREAM_DATA 的自动更新。面对恶意对端的重叠帧内存放大攻击,设计采用"达到未处理数据包上限即回退复制"的防御策略,并以 MVP 单流场景下的 MAX_DATA == MAX_STREAM_DATA 简化流控约束。

如需深入,建议继续阅读同目录下的 quic-overview.md(模块全景)、rx-depacketizer.md(数据来源侧)与 quic-fc.md(流控侧),并结合 include/internal/quic_stream.h、include/internal/quic_sf_list.h 与 ssl/quic/quic_rstream.c 阅读实现。

【免费下载链接】opensslGeneral purpose TLS and crypto library项目地址: https://gitcode.com/GitHub_Trending/ope/openssl

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询