资讯动态

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

发布时间:2026/9/11 2:24:00 来源:尧图企业网站定制
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 的数据流具有以下两个特性直接催生了接收缓冲区模块文档开篇即定义了它的职责乱序到达STREAM 帧可能以任意顺序到达只有等更低 offset 的数据全部到齐后上层才能获得连续的数据流非同步消费数据包可能在应用程序调用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_RSTREAMQUIC 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 APIFront End I/O APISSL_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 DepacketizerReceive 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 ControlReceive 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 LayerReceive 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()共用直观地反映了列表 可选环形缓冲区的双态读取通过ossl_sframe_list_peek()迭代窥探列表头部连续帧若帧数据指针非空仍引用自数据包直接memcpy复制给应用若指针为空则从环形缓冲区按逻辑 offset 取数据ring_buf_get_ptr()必要时处理环回跨越max_len l分支读完后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 上限不能用来限制数据包缓冲区的内存占用。防御策略达到上限后回退到复制解决思路是双层防御一旦达到未处理解密数据包的上限就回退为从解密数据包中复制数据对应ossl_quic_rstream_move_to_rbuf()未来还可能考虑当收到部分重叠且一帧不是另一帧子集的流数据帧时直接回退到复制模式文档标注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),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价