kTLS 进入 rustls 组织:把 TLS 的数据面交给内核
本文是对 ktls now under the rustls org 的整理与翻译。内容结构概览文章主题ktls现在归入rustls组织方便和rustls/tokio-rustls同步维护。kTLS 是什么Kernel TLS offload让内核接管 TLS 连接建立之后的数据加密、解密、分帧等工作。kTLS 不负责什么TLS 握手仍然要由用户态 TLS 库完成比如 hello、证书验证、密钥协商等。Rust 中自然选择 rustls作者的高层ktlscrate 依赖rustls因为 Rust 生态里它是主流 TLS 实现之一。核心难点一交接边界TLS frame、TCP segment、read/recvmsg缓冲区边界并不对齐rustls 可能已经解密了一些数据。核心难点二导出秘密信息内核要继续加解密就必须拿到 session keys、sequence numbers 等信息。rustls 默认不暴露这些秘密这是安全设计但 kTLS 场景需要一个显式 opt-in 的导出机制。API 协商过程作者和 rustls 维护者花时间设计了双方都满意的 API。当前 ktls API接收rustls::ClientConnection或rustls::ServerConnection返回一个能透明做 TLS 的TcpStream以及一段已经被 rustls 解密但还没交给上层的数据。示例流程生成测试证书配置rustls::ServerConfig启用 secret extraction设置 ALPN完成tokio-rustls握手再调用ktls::config_ktls_server。CorkStream 的作用帮助在 TLS message 边界停止读取避免交接时多读。为什么要转到 rustls orgrustlsAPI 会演进tokio-rustls和ktls必须跟着更新在同一组织下能减少版本滞后。协作意义未来可以更容易协调rustls、tokio-rustls、ktls的同步发布。作者态度感谢 Dirkjan 提议接收ktls作者自己也会继续参与维护。这篇文章很短只有几分钟阅读时间但它背后的技术点挺有意思。作者宣布自己维护的两个 Rust cratektls和ktls-sys现在已经转到rustlsGitHub 组织下面。这件事表面上像是一次仓库归属调整实际背后牵涉到 Rust TLS 生态、Linux kTLS、用户态 TLS 库和内核网络栈之间的协作问题。简单说rustls 负责完成 TLS 握手 ktls 把握手之后的数据加解密交给 Linux 内核 tokio-rustls 负责 async Rust 里的 TLS I/O 适配这三者如果版本不同步API 演进不协调就会让维护者和用户都很难受。把ktls放进rustls组织可以让相关包更容易同步发布也能减少“rustls 更新了但 ktls / tokio-rustls 还没跟上”的滞后。一、kTLS 是什么kTLS全称可以理解为 Kernel TLS。它的核心思想是TLS 连接建立之后把后续数据传输阶段的加密、解密、record framing 等工作交给内核处理。注意这个限定很重要连接建立之后。TLS 分两大部分握手阶段 数据传输阶段握手阶段包括ClientHello ServerHello 密钥协商 证书验证 EncryptedExtensions Finished ALPN 协商这些东西仍然需要用户态 TLS 实现完成比如rustls。kTLS 不负责帮你完成 TLS 握手。它不是一个完整 TLS 库。它更像是当用户态 TLS 库已经完成握手、已经得到会话密钥之后把“接下来怎么加密/解密每个 TLS record”的任务交给内核。换句话说kTLS 不是取代rustls而是接在rustls后面。可以粗略理解成普通 TLS 应用 - rustls 加密 - TCP socket - 内核 - 网卡 kTLS 应用 - TCP socket - 内核负责 TLS record 加密 - 网卡如果网卡支持相关 offload后续甚至可以让更多工作继续下沉到网卡。这对高吞吐网络服务很有吸引力。因为在大量数据传输时TLS record 的加密、解密、分帧会占用 CPU。如果这些工作能交给内核甚至交给硬件就可能减少用户态和内核态之间的数据搬运与加密开销。二、kTLS 不负责握手为什么还要 rustls文章明确说对于 TLS handshake 本身仍然需要用户态 TLS 实现。在 Rust 生态里自然选择就是rustls。rustls是 Rust 写的 TLS 库目标是安全、现代、避免 OpenSSL 这类 C 依赖。作者的高层ktlscrate 依赖rustls原因也很自然先让rustls完成 TLS 握手然后把后续连接交给 kTLS。也就是说流程不是kTLS 直接接收 TCP 连接并完成 TLS而是TCP 连接建立 rustls 完成 TLS 握手 从 rustls 中导出内核需要的会话信息 配置 Linux kTLS 后续数据通过内核 kTLS 透明加解密这里真正难的不是“调用一个 setsockopt”。难的是如何安全、正确地从用户态 TLS 库切换到内核 TLS。三、交接的第一个难点边界不一定对齐文章里有一个非常关键的细节TLS frames、TCP segments、以及read/recvmsg写入应用 buffer 的边界并不一定对齐。这句话值得展开一下。很多人写网络代码时会不自觉把几个概念混在一起一次应用层 read 一个 TCP segment 一个 TLS record 一个 HTTP request但它们不是同一个东西。TCP 是字节流。TLS 在 TCP 字节流上定义自己的 record。应用调用read时操作系统可能给你一段任意长度的数据。这个长度不保证刚好等于一个 TLS record也不保证刚好等于一个 HTTP message。所以在 rustls 完成握手、准备把连接交给内核 kTLS 时可能已经从 TCP socket 里读了一些数据其中有些数据已经被 rustls 解密了。如果这部分数据直接丢掉上层协议就会缺数据。如果把已经被 rustls 解密的数据又交给内核处理就会重复处理。因此ktls的 API 不能只是“给你一个 TcpStream”。它还要处理交接时已经被用户态 TLS 解出来、但还没被上层应用消费的数据。这就是为什么当前 API 会返回两样东西TcpStream Vecu8 already-decrypted data这个Vecu8就是交接时已经被 rustls 解密好的剩余数据。这类细节很容易被高层 API 隐藏但在协议栈边界非常重要。四、交接的第二个难点导出 session keys 和 sequence numberskTLS 要让内核继续处理 TLS record就必须知道一些秘密信息。至少包括会话密钥 加密算法相关参数 record sequence number 方向信息 cipher-specific state具体要导出的数据取决于协商出来的 cipher suite。但问题是rustls默认不愿意把这些秘密暴露出来。这不是坏事。恰恰相反这是安全设计。TLS 库持有 session secrets。正常情况下这些 secrets 不应该随便被应用代码拿到。暴露得越多误用和泄漏风险越高。但 kTLS 是一个特殊场景为了让内核接管后续加解密必须把这些信息交给内核。于是就需要一个显式的、受控的、双方都能接受的 API。文章提到作者和rustls维护者当时花了一些时间设计这个接口。最后 API 在 rustls 里落地。这个过程不是“开个字段 public 就完事”而是要考虑安全性、正确性、未来扩展性和维护者能否接受。这就是系统编程里常见的难点你需要突破抽象边界但不能把抽象边界炸掉。五、当前 ktls API 怎么用文章给了一个来自作者 HTTP/12 实现loona的代码片段。简化后流程大概是这样。第一步生成测试用证书letcertified_keyrcgen::generate_simple_self_signed(vec![localhost.to_string()]).unwrap();letcrtcertified_key.cert.der();letkeycertified_key.key_pair.serialize_der();这里是为了测试方便用自签证书。第二步配置rustls::ServerConfigletmutserver_configServerConfig::builder().with_no_client_auth().with_single_cert(vec![crt.clone()],PrivatePkcs8KeyDer::from(key.clone()).into(),).unwrap();第三步开启 key log。server_config.key_logArc::new(rustls::KeyLogFile::new());这不是 kTLS 必需的但调试时很有用。它会读取SSLKEYLOGFILE环境变量把 secrets 写进去。这样 Wireshark 可以用这个文件解密 TLS 流量便于调试。第四步显式启用 secret extractionserver_config.enable_secret_extractiontrue;这是 kTLS 需要的关键开关。默认情况下不导出秘密。你必须显式 opt in告诉 rustls我知道自己在做什么我需要把 secrets 提取出来交给内核。第五步配置 ALPNserver_config.alpn_protocolsvec![bh2.to_vec(),bhttp/1.1.to_vec()];ALPN 用来在 TLS 握手时协商应用层协议比如 HTTP/2 的h2或 HTTP/1.1。第六步用tokio-rustls建立 TLS 连接letacceptortokio_rustls::TlsAcceptor::from(Arc::new(server_config));letstream:TcpStreamtodo!(TCP listen/accept logic goes here);letstreamCorkStream::new(stream);letstreamacceptor.accept(stream).await?;这里的CorkStream很关键。注释里说它能在两个 TLS message 的边界处停止读取。这是为了后面配置 kTLS 时不要在用户态多读过头。第七步从 rustls session 中拿到 ALPN 结果letscstream.get_ref().1;letalpn_protosc.alpn_protocol().and_then(|p|std::str::from_utf8(p).ok().map(|s|s.to_string()));这就是常规tokio-rustls流程里能做的事握手完成后看双方协商出来的协议是什么。第八步真正配置 kTLSletstreamktls::config_ktls_server(stream).await?;这一步会提取 rustls 的 secrets配置 Linux kTLS把 TLS 数据面交给内核。最后拿回原始内容let(drained,stream)stream.into_raw();letdraineddrained.unwrap_or_default();这里的drained就是前面说过的rustls 已经解密但还没交给应用层的那段数据。最终应用拿到一个TcpStream它后续可以“透明地做 TLS”。也就是说对上层来说它像一个普通 stream但实际写入的数据会由内核 kTLS 加密读出的数据会由内核 kTLS 解密。六、为什么 API 返回 TcpStream 和 Vec这个 API 设计非常值得单独讲。文章说今天ktls的 API 接收一个 rustlsClientConnection或ServerConnection然后返回TcpStream Vecu8 already-decrypted data这背后反映了两个事实。第一kTLS 连接接管之后应用不再通过 rustls 的 stream wrapper 读写而是直接拿回底层TcpStream。这个TcpStream已经被内核配置成 TLS ULP也就是 Upper-Layer Protocol。后续对它读写内核会处理 TLS record。第二交接时不能假设没有剩余数据。由于 rustls 可能已经读取并解密了一部分应用数据这部分必须被返回给调用者。否则上层协议会丢字节。如果你写的是 HTTP/2 实现这一点尤其重要。HTTP/2 的连接前导、SETTINGS frame、HEADERS frame 等都可能紧跟在 TLS 握手之后到达。网络边界不会等你优雅切换实现。多读一点、少读一点都可能影响上层状态机。所以Vecu8不是奇怪的附属品而是一个必要的交接缓冲。七、为什么 ktls 要和 rustls 同步维护文章的最后一节叫 Coordinating collaborating。核心问题是rustls会演进。TLS 实现是安全敏感代码。随着维护者发现更好、更安全、更正确、更灵活的接口API 会变化。这是好事。安全库不应该为了表面稳定而拒绝改进。但对tokio-rustls、ktls这类依赖 rustls 内部能力的 crate 来说这就意味着必须跟着更新。过去作者曾经对rustls新版本发布后tokio-rustls几个月没有对应版本感到不满。他最近在网上抱怨过这件事。维护者告诉他这种情况以后不太会再发生因为rustls和tokio-rustls现在已经在同一个 GitHub organization 下面。然后维护者还提出也可以把ktls收进来。这样做的好处很直接rustls 更新时可以同时考虑 ktls。 tokio-rustls 更新时可以一起协调。 ktls 需要适配新 API 时不再孤立等待。 三个包可以更容易同步 release。 维护者之间的沟通成本更低。 用户遇到版本兼容问题的概率更小。这就是这篇文章标题的含义ktlsnow under therustlsorg。这不是品牌迁移而是生态维护策略。八、为什么这件事对 Rust 网络生态重要从表面看kTLS 是一个偏小众、偏底层的功能。普通 Web 应用用不到它。你用 axum、reqwest、hyper、tonic完全可以不关心 kTLS。绝大多数服务的瓶颈也不一定在 TLS 加解密。但对高性能网络服务来说这件事很有意义。如果你维护的是高吞吐 HTTP server 反向代理 CDN 边缘节点 大文件传输服务 视频平台 自定义 HTTP/1 HTTP/2 实现 网关或负载均衡器那么 TLS 数据面的 CPU 成本、内核态/用户态切换、buffer 复制、sendfile / splice / zero-copy 路线都会变得重要。kTLS 的吸引力在于它可能让 TLS 和内核网络栈更好地结合。比如传统 TLS 在用户态完成加密后再 write 到 socket。这样很多内核优化路径会变复杂因为内核看到的是已经加密后的字节而 TLS record 边界和应用数据边界也在用户态处理。kTLS 把 TLS record 处理下沉到内核后一些内核网络能力更容易发挥作用。具体收益取决于系统、内核版本、网卡、cipher、工作负载和实现细节但方向是明确的减少用户态 TLS 数据面负担让内核或硬件处理更多传输细节。而 Rust 网络生态如果想参与这类底层优化就需要rustls、tokio-rustls、ktls之间有稳定协作。九、这不是“把 TLS 交给内核就更安全”这里需要注意一个点kTLS 不是无条件更安全也不是“内核做就一定更好”。它是一个性能和架构工具。用户态 TLS 库比如rustls有自己的安全边界和内存安全优势。把数据面交给内核后内核也会进入信任边界。你要相信 Linux kTLS 实现正确配置正确cipher 支持正确状态交接正确。同时导出 session secrets 本身就是敏感操作。虽然它是为了交给内核但这仍然意味着用户态代码需要访问原本被 TLS 库严密封装的信息。所以enable_secret_extraction true这种显式开关很重要。它让调用者清楚知道我正在启用一个高级功能它需要导出 TLS secrets。这类 API 不应该默认开启也不应该无感发生。正确设计应该是默认安全 高级能力显式启用 调用者能看到边界 错误路径清楚 交接数据不丢失这也是 rustls 维护者和作者花时间打磨 API 的原因。十、CorkStream一个容易忽略但很关键的细节文章代码里有一个CorkStreamletstreamCorkStream::new(stream);letstreamacceptor.accept(stream).await?;注释说它能在两个 TLS message 的边界之间停止读取。真正的配置逻辑在后面的config_ktls_{client,server}里完成。这背后仍然是边界问题。假设 rustls 正在握手。它从 TCP stream 中读取数据。TCP 是字节流所以一次 read 可能不只读到握手数据还可能顺手读到握手之后的应用数据。如果 rustls 多读了它可能已经解密了一些 application data。那交给 kTLS 时就必须把这些数据单独交给上层。CorkStream 的作用就是尽量控制读取边界让交接更干净。即使如此API 仍然要返回drained数据因为不能假设完全没有多读。这个细节很底层但很重要。很多协议栈 bug 都来自“我以为边界会对齐”。现实里边界经常不对齐。TCP segment 不等于 TLS record TLS record 不等于应用层消息 一次 read 不等于一次 write 用户态 buffer 不等于协议边界kTLS 交接就是这些边界问题的集中体现。十一、和作者其他文章的关联如果你读过作者之前的文章比如 HTTP crash course、Futures Nostalgia、loona 相关内容就会发现这篇短文是同一条线上的一小段更新。作者一直对网络栈很感兴趣HTTP/1.1 HTTP/2 hyper / h2 Tower Service async Rust io_uring rustls kTLS他在写自己的 HTTP/12 实现loona也一直关注如何把 Rust 网络服务做得更透明、更可观察、更可控。kTLS 正好连接了几个点用户态 TLSrustls 异步 I/Otokio-rustls 内核网络栈Linux kTLS HTTP 实现loona 性能优化减少用户态 TLS 数据面成本所以这篇文章虽然短但其实是一个长期探索方向的节点Rust 网络生态不仅要有安全的 TLS 实现还要能和内核能力接起来服务高性能场景。十二、为什么放到同一个 GitHub 组织很实际开源生态里仓库归属并不是纯行政问题。如果几个 crate 关系很紧密但在不同组织、不同维护节奏、不同 release 流程下就会出现很多协调成本。比如rustls 发布新版本 内部 API 或配置结构改变 tokio-rustls 需要适配 ktls 也需要适配 某个 crate 先发了另一个没发 用户 dependency graph 卡住 下游库只能 pin 旧版本 issue 开得到处都是这些问题不一定是技术难题但很消耗维护者精力。把它们放进同一个组织后不代表所有问题消失但至少有几个好处维护者权限更容易协调 release 可以一起计划 CI 和测试矩阵更容易共享 版本兼容策略更容易讨论 用户知道这些 crate 属于同一个生态范围尤其是ktls这种紧贴rustlsAPI 的 crate和rustls脱节太久会非常麻烦。作者感谢 Dirkjan 提议接收ktls。同时他也说明自己仍然会在旁边继续处理需要处理的事情。这不是“作者扔掉项目”而是“项目进入更适合它的位置”。十三、这篇文章真正想表达什么这篇文章很短但背后有三层意思。第一层是技术解释kTLS 让内核接管 TLS 连接握手之后的数据面但握手仍然由用户态 TLS 库完成。第二层是 API 设计为了让内核从 rustls 手里接过 TLS 状态需要安全地导出 session secrets 和 sequence numbers还要处理 rustls 已经解密但未交给上层的数据。第三层是生态协作ktls强依赖rustls而rustls和tokio-rustls都会演进。把它们放进同一个组织能减少版本滞后让同步 release 更容易。所以这不是一个“仓库搬家通知”那么简单。它其实是 Rust TLS / 网络生态成熟的一小步。十四、对实际开发的启发第一kTLS 不是 TLS 库。它不做握手不做证书验证不负责 ALPN 协商。你仍然需要rustls这样的用户态 TLS 实现。第二协议栈交接必须处理残留数据。TLS record、TCP segment、应用 read buffer 不对齐。用户态 TLS 可能已经解密了一些数据。API 必须显式返回这些 drained data。第三导出 secrets 必须显式 opt in。enable_secret_extraction true这种设计是必要的。安全敏感信息不能默认暴露。第四底层性能优化依赖生态协作。kTLS、rustls、tokio-rustls 不是孤立的。一个包 API 变化另一个包就要跟着适配。组织和 release 流程本身也是工程问题。第五小 crate 也需要归属感。如果一个 crate 的价值建立在另一个核心项目上把它放到同一组织下可能更健康。这样维护者、用户和下游项目都更容易理解它的定位。第六Rust 网络生态正在走向更底层。从安全 TLS到 async TLS再到 kTLSRust 不只是写应用层 Web 服务也在逐步覆盖高性能网络服务需要的系统接口。十五、总结这篇文章宣布作者维护的ktls和ktls-sys两个 crate现在已经归入rustlsGitHub organization。它们的目标是把 Linux Kernel TLS offload 暴露给 Rust 使用者。kTLS 的作用不是完成 TLS 握手而是在 TLS 握手完成后让内核接管后续数据传输阶段的加密、解密、record framing 等工作。TLS 握手仍然需要用户态 TLS 实现比如rustls。握手阶段包括 hello、密钥协商、证书验证、encrypted extensions、ALPN 等。完成这些后用户态 TLS 库掌握了连接所需的秘密信息kTLS 才有机会继续接管数据面。作者的高层ktlscrate 依赖rustls。这是自然选择因为 Rust 生态里rustls是重要 TLS 实现。难点在于内核要从rustls手里继续加解密必须拿到 session keys、sequence numbers 等状态。不同 cipher 需要导出的数据不同而rustls默认会把这些秘密藏得很好这是安全设计。为了 kTLS需要一个显式 opt-in 的 API让调用者明确启用 secret extraction。作者和 rustls 维护者花时间设计了双方都满意的接口并在 rustls 中落地。另一个难点是交接边界。TLS frames、TCP segments、read/recvmsg写入 buffer 的边界并不对齐。rustls 完成握手时可能已经从 TCP stream 中多读并解密了一些应用数据。因此ktls的 API 不能只返回一个TcpStream还必须返回一段已经解密但未被上层消费的数据。当前 API 接收 rustls 的ClientConnection或ServerConnection然后返回一个TcpStream以及一个Vecu8后者就是 drained data。文章里的示例来自作者自己的 HTTP/12 实现loona。示例先生成自签证书配置rustls::ServerConfig设置KeyLogFile方便 Wireshark 调试再显式设置enable_secret_extraction true并配置 ALPN 为h2和http/1.1。随后用tokio-rustls的TlsAcceptor完成 TLS 握手。中间套了一层CorkStream用于在 TLS message 边界停止读取减少交接时多读的复杂性。握手完成后可以从 rustls session 中读取协商出的 ALPN 协议然后调用ktls::config_ktls_server(stream).await?配置 kTLS。最后通过into_raw()拿到 drained data 和后续透明做 TLS 的TcpStream。文章最后解释了为什么ktls要进入rustls组织。rustls会持续演进API 会根据更安全、更正确、更灵活的设计而变化。每次 rustls 发布新版本tokio-rustls和ktls都要适配。过去作者曾对rustls更新后tokio-rustls几个月没有同步发布感到不满。后来维护者告诉他rustls和tokio-rustls已经在同一个 GitHub organization 下这种滞后以后不太会再发生。维护者也提议接收ktls这样未来可以更容易协调rustls、tokio-rustls和ktls的同步发布。这件事的意义不只是“仓库搬家”。它说明 Rust TLS 生态在往更紧密的协作方向发展。rustls负责安全的用户态 TLStokio-rustls负责 async I/O 适配ktls负责连接建立后把数据面交给 Linux 内核。对普通应用来说kTLS 可能暂时不重要但对高吞吐 HTTP server、反向代理、CDN 边缘节点、大文件传输服务、视频平台、自定义 HTTP/12 实现来说这类能力很有价值。未来 Rust 网络服务如果想更深入利用内核和硬件 offloadrustls、tokio-rustls、ktls这样的协同就会越来越重要。