记录层
TLS 连接上流动的是记录(record):5 字节的头(内容类型、版本、长度)加一段载荷,载荷在密钥就位后是 AEAD 密文。握手消息、告警和应用数据都装在记录里,一条握手消息可以拆进几条记录,几条握手消息也可以挤进一条记录。这一部分讲 rustls 怎么把字节变成记录、把记录变成消息,以及反方向怎么走,也就是记录主线在记录这一层的全部细节:writer().write_all(b"...") 的明文怎么分片、加密、排队,write_tls 交出去的字节在对端怎么经 read_tls 进缓冲、被 process_new_packets 切开、原地解密,再从 reader() 读出来。连接对象和 sans-IO 的调用循环见 sans-IO,同一次握手逐条记录的抓取见一次握手。
代码分布(tag v/0.23.46,.rs 行数,含文件内的单元测试):msgs/codec.rs 404 行、msgs/macros.rs 329 行、msgs/base.rs 258 行、msgs/handshake.rs 3266 行(各种握手消息和扩展);去帧在 msgs/deframer/ 三个文件共 1059 行,消息类型在 msgs/message/ 三个文件共 854 行;record_layer.rs 358 行、crypto/cipher.rs 364 行;发送侧的 msgs/fragmenter.rs 232 行、vecbuf.rs 377 行;把它们串起来的是 conn.rs(1335 行)和 common_state.rs(1136 行)。
- 编解码:
Reader加Codec<'a>是所有线上结构的读写接口,读出来的值可以借用输入;长度前缀先写0xff占位、Drop时回填;enum_builder!给每个协议枚举加Unknown,extension_struct!把扩展列表变成一个全是Option字段的结构,未知扩展在 ClientHello 里丢弃、在 HelloRetryRequest 里报错,重复扩展在解码时就拒绝。 - 去帧与缓冲:
read_tls是密文进入 rustls 的唯一一次拷贝,之后切记录、解密、拼接握手消息都在同一块DeframerVecBuffer里原地进行;跨记录的握手消息靠HandshakeDeframer记下区间、用copy_within挪到一起,超过 64 KiB 直接拒绝;应用数据解密后拷进received_plaintext,后者超过 16 KiB 时read_tls报错。 - 加密与序列号:
RecordLayer两个方向各一个加解密器和序列号,nonce 是 IV 异或序列号;TLS 1.3 的真实内容类型藏在密文最后一个字节,rustls 发送时不填充、接收时剥掉填充;AES-GCM 一把密钥最多加密 2^24 条记录:TLS 1.3 用最后一个可用的序列号发 KeyUpdate 换密钥,TLS 1.2 到上限时发 close_notify。 - 发送路径:
MessageFragmenter按 16 KiB(或max_fragment_size减 5)切明文;握手完成前的写入整块缓存在sendable_plaintext,之后每条记录一个Vec进sendable_tls,write_tls一次最多交出 64 条;set_buffer_limit默认 64 KiB,满了write返回Ok(0),write_all报WriteZero。
flowchart TB w["writer write"] --> frag["MessageFragmenter"] frag --> enc["encrypt_outgoing"] enc --> tls["sendable_tls"] tls --> wt["write_tls"] wt --> rt["对端 read_tls"] rt --> buf["DeframerVecBuffer"] buf --> it["DeframerIter"] it --> dec["decrypt_incoming"] dec --> hs["HandshakeDeframer"] dec --> plain["received_plaintext"] hs --> codec["Codec 解析"] plain --> rd["reader read"]
四页的实验都在 exp-rustls/rec-lab(M2 Max、macOS 27、rustc 1.99.0,release),连接在内存里握手,不开套接字。各页的「实测」用 ring provider,吞吐对比在发送路径的末尾同时测 ring 与 aws-lc-rs。