在给 Cloudflare 的 boringtun 分支打了两年多、累计超过 70 个 PR 的补丁之后,零信任网络厂商 Firezone 终于放弃了维持 100% 上游 API 兼容的尝试,于 2026 年 10 月 31 日正式开源了用 Rust 编写的用户态 WireGuard 状态机实现 sanstun(v0.1.0 版本于 10 月 30 日发布,采用 Apache-2.0 许可证)。与人们熟悉的 Go 语言版 wireguard-go 或 boringtun 相比,sanstun 最激进的改变是彻底采用 sans-IO 架构——把网络读写、系统时钟、随机数生成以及内部缓冲全部从核心库里剥离,只留下一个纯粹的协议状态机。

这一重构直接解决了 Rust 网络编程里的一个顽疾,即如何在不引入内部锁的前提下实现单隧道内的多核并行加解密,同时也让全系统确定性仿真测试成为可能。但剥离副作用是有代价的:sanstun 把最容易出错的密码学安全边界推给了上层调用者,而且这个初出茅庐的 v0.1.0 版本目前尚未经过第三方安全审计。

两年 70 个补丁换来的决裂:内部缓冲与系统时钟为何成了累赘

跨平台客户端和容器化网关高度依赖用户态 WireGuard 实现,Firezone 早期顺理成章地选择了 Cloudflare 用 Rust 编写的 boringtun(sanstun 的 NOTICE.txt 中至今保留着源自 BoringTun 的 BSD-3-Clause 许可证声明)。但在长达两年的生产环境打磨中,Firezone 发现传统网络库将状态与副作用打包的做法,正在拖垮系统的可测试性与吞吐上限,原有的 firezone/boringtun 分支也随着 sanstun 的发布被正式弃用。

历经 70 个补丁后剥离内部缓冲与系统时钟(示意图)
历经 70 个补丁后剥离内部缓冲与系统时钟(示意图)

第一个痛点是系统时间耦合。为了排查极其隐蔽的分布式边缘 Bug,Firezone 需要对整个网络系统做确定性仿真测试(Deterministic Simulation Testing)。如果底层库直接调用操作系统的时钟,仿真器就无法快进或精确重放超时逻辑。Firezone 当初向自己维护的 boringtun 分支提交的首批大改(firezone/boringtun#5),就是给所有涉及时间的函数硬加上 _at 后缀并传入 Instant 参数。

第二个痛点是看似贴心的内部缓冲机制。当上层应用尝试发送数据包而隧道尚未完成握手时,boringtun 会自动在库内部把包缓存起来并触发会话建立。这种设计带来了三个工程包袱:

  • 反直觉的排空契约.为了把积压在内部的缓冲区排空,调用方必须向 decapsulate 函数传入一个零长度的数据报并在循环中反复调用。
  • 额外的内存分配.库内部的隐式缓冲不可避免地增加了堆内存分配开销。
  • 容量难以按需调优.底层库写死的缓冲策略无法适应千差万别的上层业务流量特征。

sanstun 的解法非常干脆,直接砍掉内部缓冲。发送数据包时只需调用 Tunnel::begin_encapsulate,如果当前没有活跃会话,由调用方自己在外部决定怎么缓存数据包,并用 Tunnel::initiate_handshake 显式发起握手。

架构解耦对比:从传统耦合库到 sanstun 状态机 boringtun(传统耦合模式) 隐式内部缓冲(需零长度包排空) 绑死系统时钟与随机数副作用 可变借用锁定状态与加解密 结果:难以仿真测试,单隧道多核扩展受限 sanstun(sans-IO 纯状态机) 零内部缓冲,内存控制权归还调用方 外部注入时间与熵源,支持确定性仿真 三段式 API 剥离记账与密码学计算 结果:主线程无锁记账,工作线程并行加解密

绕开 Rust 借用检查器的手术刀:单隧道如何跑满多核

在协议层面,WireGuard 不同隧道(Tunnel)之间不共享状态,按对端节点做多线程并行非常容易。但如果两条高带宽节点之间只建了一条 WireGuard 隧道,所有五元组流量都挤在同一个会话里,单核 CPU 的 ChaCha20-Poly1305 对称加解密速度就会成为死瓶颈。

为了防止重放攻击,WireGuard 要求每个数据包携带单调递增的包计数器(Packet Counter)。在传统的 Rust 实现里,更新计数器状态和执行加解密被绑在同一个需要可变借用(&mut self)的函数中;只要一个线程还在算加密,其他线程就无法触碰这个隧道状态,除非在库内部塞满锁和原子变量。

sanstun 用了一把非常精巧的手术刀:把极轻量的状态记账与极耗时的密码学运算彻底拆开。

发送数据时,主线程调用 Tunnel::begin_encapsulate 并不执行加密,它只检查会话、预留一个包计数器并推进定时器状态,随后立刻归还隧道的可变借用,返回一个实现了 Send trait 的 PendingEncapsulate 结构体。调用方可以把这个结构体直接扔给后台线程池去跑真正的 encrypt。接收端同样被拆成 begin_decapsulate、跨线程 decrypt 和主线程 commit_decapsulate 三个阶段。

1280 字节数据包各阶段耗时对比(Criterion 微基准) 发送记账 (begin_encapsulate) 20.8 ns(仅占 2%) 发送加密 (encrypt 可跨线程) 933.5 ns 接收预检 (begin_decapsulate) 37.7 ns 接收解密 (decrypt 可跨线程) 914.9 ns 注:接收端最后还需 17.7 ns 的 commit_decapsulate 状态提交;记账耗时不随包大小增长。

根据代码仓库 benches/data_path.rs 中基于 Criterion 框架的微基准测试,无论是 64 字节还是 1,280 字节的负载,begin_encapsulate 的记账耗时都稳定在 20.8 纳秒左右;而在 1,280 字节负载下,真正的 encrypt 耗时高达 933.5 纳秒(64 字节时为 226.2 纳秒)。记账开销只占整个发包加密流程的 2%。

这也让 Interface 和 Tunnel 的核心状态得以保持纯单线程运行,内部的防 DoS 速率限制器连一把锁或一个原子计数器都不需要。

但这里有一个原博客没有展开的工程现实:单靠组件级微基准测试里的 45 倍理论上限,还不能直接等同于真实网络里的吞吐暴涨。sanstun 的基准测试目前尚未包含 TUN/UDP 套接字读写、跨线程工作队列调度,也没有给出与早已内置并行加解密 worker 的 wireguard-go 的端到端吞吐量与延迟对比。在低负载场景下,把任务派发给外部工作线程的队列调度开销,反而可能拉高单包的尾延迟;这也是为什么 sanstun 保留了在单线程内直接内联调用 encrypt 的接口。


编译期防呆与外移的安全契约:谁该用、谁先别碰

在 API 安全性上,sanstun 充分展现了 Rust 类型系统的优势。WireGuard 的握手阶段需要进行开销不小的 Diffie-Hellman 密钥交换,攻击者一旦获知服务端公钥,就可以伪造源 IP 发起洪水攻击耗尽服务端 CPU;协议因此设计了基于接口层级的 Cookie 速率限制机制。

编译期强制限流入口与移交给调用方的四项安全重担(示意图)
编译期强制限流入口与移交给调用方的四项安全重担(示意图)

为了防止开发者在调用时漏掉限流检查,sanstun 把 HandshakeInitiation 等消息类型的构造函数全部设为包内私有(crate-private)。外部代码即便看到了 Tunnel::handle_handshake_initiation 这个公开函数,也无法直接构造参数去调用它,唯一合法的路径是先把原始数据报塞进 Interface::receive。这个函数会在底层强制执行速率限制、解析高 24 位的隧道索引,并在高负载时直接生成 Cookie 回复,从编译期就堵死了绕过限流器的可能。

状态机越纯粹,留给调用方的安全考卷就越残酷。

然而,当 sanstun 把所有 I/O 和环境副作用清理出门后,原本由传统网络库兜底的安全重担也一并转移到了集成方肩上。翻看其 GitHub 仓库的文档可以发现,调用者如果要在生产环境接入 sanstun,必须自己填平几道致命的安全鸿沟:

  • 风险.sanstun 的 README 明确声明该项目目前尚未接受正式的第三方安全审计(formal security audit),且其安全策略仅支持最新发布的版本,不会向旧版本回溯移植安全补丁。

除了审计空白,集成方还必须独立承担四项核心安全职责:

  1. 严苛的高熵随机数种子调用方必须提供密码学安全的随机数生成器。一旦在初始化或会话中重用了随机数种子,会导致临时密钥、Nonce 和 Cookie 重复,直接击穿整个加密体系。
  2. 强制执行 Cryptokey 路由校验sanstun 不负责检查允许的 IP 列表(Allowed IPs),调用方必须在外部自行实现并强制校验路由表。
  3. 验证解密后的内层 IP 包解密完成后,上层业务必须自行校验内部 IP 头部的合法性,防止欺骗流量注入。
  4. 保证单调递增的时间输入外部传入的 Instant 时间戳必须严格单调递增,否则会搞乱握手重传与密钥轮换计时器。

对于自研 SD-WAN、游戏加速器或容器网络的 Rust 基础设施团队来说,sanstun 提供了一个极其干净、不绑死 Tokio 等特定异步运行时的底座,甚至可以轻松塞进基于 io_uring 的自研事件循环里。但如果你所在的团队只需要一个开箱即用的 VPN 组件,既没有全系统确定性仿真的需求,也没有把握独立写对随机数熵源管理、Cryptokey 路由校验和无锁多线程队列调度,那么在 sanstun 完成独立第三方安全审计并补齐端到端性能实测之前,继续留在成熟的 wireguard-go 或经过多年验证的现有方案里,依然是更稳妥的选择。