sanstun:一个用 Rust 编写的无 I/O、支持并行的 WireGuard 实现
摘要
Firezone 发布了 sanstun,一个用 Rust 编写的 sans-IO、支持并行的用户态 WireGuard 状态机实现,旨在取代其长期维护的 boringtun 分支,提供更简洁、更安全且更易测试的 API。
暂无内容
查看缓存全文
缓存时间: 2026/10/31 00:16
# sanstun 简介:用 Rust 实现的 sans-IO、便于并行处理的用户态 WireGuard 来源:https://www.firezone.dev/blog/introducing-sanstun 今天,我们宣布推出`sanstun` (https://github.com/firezone/sanstun):一个采用 sans\-IO 设计的 Rust WireGuard® 状态机实现,通过便于并行处理且符合 Rust 惯用风格的 API,优化易用性、安全性和性能。 ## 什么是 sans\-IO? (https://www.firezone.dev/blog/introducing-sanstun#what-is-sans-io) sans\-IO 的理念是,在设计系统时,将 IO 和副作用与状态及“业务逻辑”清晰地分离。sans\-IO 中的 IO 不仅指文件或网络 IO,还包括时间和随机性等事物!由于没有副作用,sans\-IO 组件非常易于组合,也相对容易测试。这种设计还避免了函数染色等问题,并且与 Rust 的所有权规则配合良好。本文中的代码示例和参考资料使用 Rust,但 sans\-IO 完全不是 Rust 专属的。它的模式和理念可以应用于任何语言。若想更深入地了解,我们的 sans\-IO 文章 (https://www.firezone.dev/blog/sans-io)是一个很好的起点。 ## 历史 (https://www.firezone.dev/blog/introducing-sanstun#history) 最初构建 Firezone 时,我们选择了`boringtun` (https://github.com/cloudflare/boringtun)作为 WireGuard 实现。`boringtun`基本上满足了我们的需求,但随着时间推移,我们发现自己不断地为分叉版本打补丁,既修复错误,也添加 API。我们的目标是让这些补丁仍能合并到上游,因此始终保持现有 API 的行为不变。我们发现,随着产品需求的发展,与上游保持 100% 的 API 兼容性变得越来越困难。我们对`boringtun`所做的最早几项重大改动之一,就是让它在时间处理方面完全采用 sans\-IO 设计 \(firezone/boringtun\#5 (https://github.com/firezone/boringtun/pull/5)\)。这意味着为所有相关操作添加带有`\_at`后缀的新 API,用于接收一个`Instant`。如果想准确模拟整个系统,sans\-IO 时间处理就非常重要。请参阅这篇博客文章 (https://www.firezone.dev/blog/finding-bugs-you-didnt-think-to-test-for),了解我们如何实现这一点。我们还为内部缓冲添加了功能开关。起初,当尚未建立会话 \(或会话已失效\) 而你尝试发送数据包时,`boringtun`会自动建立会话,这看起来很方便。然而,内部缓冲会带来几个问题:1. 缓冲区终究需要排空。`boringtun`的解决方法是要求你使用长度为零的数据报调用`decapsulate`。这相当不直观。2. 它会产生额外的内存分配 \(至少`boringtun`中的实现方式如此\)。3. 缓冲区大小应当可以配置和调优。所有这些问题都指向同一个解决方案:不要在库中进行缓冲。这样可以简化双方的接口和实现。补丁和修复的完整列表 (https://github.com/cloudflare/boringtun/compare/master...firezone:boringtun:master)相当长。即使排除依赖升级,也有 70 多个 PR。其中一些修复了我们发现的边界情况,另一些则添加了新 API,或让某些方面变得可配置。在使用并修补`boringtun`两年多之后,我们已经非常清楚用户态 WireGuard 实现的整体 API 应该是什么样子。如今,这套 API 已经以`sanstun`的形式实现。 ## 更好的 sans\-IO API (https://www.firezone.dev/blog/introducing-sanstun#a-better-sans-io-api) sans\-IO API 目前的核心构成如下:- 处理入站数据包:- `Tunnel::handle\_handshake\_initiation` - `Tunnel::handle\_handshake\_response` - `Tunnel::handle\_cookie\_reply` - `Tunnel::begin\_decapsulate`和`Tunnel::commit\_decapsulate` - 处理出站数据包:`Tunnel::begin\_encapsulate` - 处理定时器更新:`Tunnel::handle\_timeout`和`Tunnel::poll\_timeout` - 处理事件:`Tunnel::poll\_event` 没有内部缓冲,就不会存在要求你循环调用某些 API 的接口约定,这与整体 sans\-IO 模型更加契合。要发送数据包,只需调用`Tunnel::begin\_encapsulate`。如果尚未建立会话,是否缓冲数据包由你决定。创建新会话意味着需要发送握手消息。你可以随时使用`Tunnel::initiate\_handshake`发起新的握手。这些 API 的设计经过了深思熟虑,快速回顾一下 WireGuard 协议后,你就会更容易理解。 ## WireGuard 协议 (https://www.firezone.dev/blog/introducing-sanstun#wireguard-protocol) WireGuard 的消息格式支持 4 种消息类型:握手发起、握手响应、Cookie 回复和加密数据。每个 WireGuard 会话都以 Diffie\-Hellman 密钥交换开始,这是一项开销不小的操作。WireGuard 的设计确保暴露端点不会带来 DoS 攻击的机会。任何知道端点公钥的人都可以轻易构造虚假的握手发起消息,并伪造传入 UDP 数据包的源 IP。接收端的 WireGuard 实现必须先执行自己那一半的 DH 运算,才能判断所提供的握手数据是伪造的。但此时,CPU 资源已经消耗掉了。只要发送足够多的握手发起消息,接收端就会将所有 CPU 周期用于处理无关的握手。为防止这种情况,WireGuard 规定了一种 Cookie 机制,强制远端证明其拥有所声称的源 IP,从而能够按源 IP 对握手进行速率限制。这些速率限制作用于整个 WireGuard 接口,而不仅仅是单条隧道。握手完成后,双方即可使用对称密码交换加密数据包。在握手过程中,双方还会交换各自在本地选择的会话索引。这些索引用于区分不同隧道,以及同一隧道内的多个会话。 ## 将协议不变量映射到 API 设计 (https://www.firezone.dev/blog/introducing-sanstun#mapping-protocol-invariants-to-api-design) 如果你使用 Rust 这样的语言,很可能会喜欢使用类型。而善用类型,意味着在编译期强制保证不变量,让某些错误根本无法随软件发布。例如,忘记对某条消息应用速率限制会相当糟糕,因为这个问题在正常运行时不会显现。同样,将传入数据包路由到错误的隧道,也只有在生产环境中才会暴露。良好的 API 设计让你无法构建出有这类缺陷的系统,同时引导你理解协议的工作原理。 ## 一个接口,多条隧道 (https://www.firezone.dev/blog/introducing-sanstun#one-interface-many-tunnels) 一个 WireGuard 接口由一个密钥标识,而一条隧道则是两个接口之间的连接。因此,每次发送数据包时,我们都需要指定通过哪条隧道发送;收到数据包时,则需要判断它属于哪条隧道。在`sanstun`中,这些概念直接对应`Interface`和`Tunnel`类型。所有入站数据包都会经过`Interface::receive`。这是唯一解析原始数据报的公共 API。它会解析数据包、应用速率限制,然后返回一个`ReceivedMessage`,其中每种 WireGuard 消息类型都对应一个枚举变体;如果接口负载过高,则会返回一个需要发回的 Cookie 回复。这些消息随后会直接映射到`Tunnel`上的对应函数:`Tunnel::handle\_handshake\_initiation`等。握手发起消息根据发起方的静态公钥进行路由。所有其他消息都会通过`TunnelId`指明应当被分发到*哪条*`Tunnel`。一个接口,多条隧道每个 UDP 数据报都经由 Interface::receive 进入。只有通过所有检查后,消息才会被转发到其所属的 Tunnel。数据报Cookie 回复负载过高Interface::receive解析头部验证 MAC检查 Cookie按源地址限制速率HandshakeInitiationA 的静态密钥HandshakeResponseTunnelId 5CookieReplyTunnelId 5TransportDataTunnelId 7Tunnel 3对等方 Ahandle\_handshake\_initiationTunnel 5对等方 Bhandle\_handshake\_responsehandle\_cookie\_replyTunnel 7对等方 Cbegin\_decapsulate 每个入站数据报都必须经过`Interface::receive`。在内部,`sanstun`会在这里为你处理很多事情。它会解析隧道索引并提取高 24 位,应用速率限制,生成 Cookie 回复,并验证握手消息。关键在于,`HandshakeInitiation`等类型的构造函数都仅在 crate 内部可见。尽管`Tunnel`公开了`handle\_handshake\_initiation`函数,但调用它的唯一方式,是先经过`Interface::receive`来获取相应类型。而`Interface::receive`会通过应用速率限制器来保护你的应用,整个过程无需你操心。 ## 并行加密与解密 (https://www.firezone.dev/blog/introducing-sanstun#parallel-crypto) `sanstun`最出色的特性之一,是它为数据包加密和解密提供了便于并行处理的 API。在协议层面,WireGuard 不会在隧道之间共享任何状态。因此,在不同对等方之间并行执行 WireGuard 实现非常容易。那么,在单条隧道内并行处理又如何呢?WireGuard 工作在第 3 层,因此与另一个对等方建立的单个会话会承载许多独立网络流的流量。即使我们使用不同线程处理通往不同对等方的隧道,所有发往同一主机或子网的网络流 \(即不同的五元组\) 仍会被顺序处理,其速度实际上受限于 CPU 执行 ChaCha20\-Poly1305 的速度;后者正是 WireGuard 用于数据包的密码算法。为了防止传输路径上的恶意观察者进行重放,WireGuard 数据包带有一个单调递增的数据包计数器。这个计数器的作用域是单个会话,因此,如果从多个线程同时访问该状态,就需要进行同步。为避免这一点,`sanstun`将状态维护与实际的加密、解密操作分离。调用`Tunnel::begin\_encapsulate`实际上不会加密任何内容。它会检查当前会话,预留一个数据包计数值,推进定时器状态,然后返回一个`PendingEncapsulate`:`` impl PendingEncapsulate { pub fn encrypt<'a>(self, plaintext: &[u8], dst: &'a mut [u8]) -> Result, Error> { // ... } } `` `PendingEncapsulate`实现了`Send`,因此可以转移到另一个线程上执行大部分工作。在`sanstun`仓库中运行`cargo bench data\_path`,即可看到这种拆分的效果:`` data_path/64B/begin_encapsulate time: [20.785 ns 20.828 ns 20.869 ns] data_path/64B/encrypt time: [226.15 ns 226.22 ns 226.29 ns] data_path/64B/begin_decapsulate time: [37.600 ns 37.675 ns 37.744 ns] data_path/64B/decrypt time: [205.78 ns 205.83 ns 205.87 ns] data_path/64B/commit_decapsulate time: [17.486 ns 17.529 ns 17.575 ns] data_path/1280B/begin_encapsulate time: [20.785 ns 20.828 ns 20.869 ns] data_path/1280B/encrypt time: [933.49 ns 933.54 ns 933.59 ns] data_path/1280B/begin_decapsulate time: [37.606 ns 37.681 ns 37.751 ns] data_path/1280B/decrypt time: [914.81 ns 914.89 ns 914.97 ns] data_path/1280B/commit_decapsulate time: [17.657 ns 17.707 ns 17.764 ns] `` 正如预期,状态维护部分 \(`begin\_encapsulate`、`begin\_decapsulate`和`commit\_decapsulate`\) 的耗时不会随数据包大小增加而增长。耗时随数据包大小增长的是`encrypt`和`decrypt`函数。当数据包大小为 1280 字节时,加密一个新数据包所需的状态维护工作仅占 2%。如果将实际的加密工作移到另一个线程,这表明理论上可能获得超过 45 倍的加速!实际中很难达到 45 倍的有效加速,因为这个估算忽略了将`PendingEncapsulate`传递到另一个线程的成本,并且假设不会引入任何其他开销。尽管如此,能够将加密与解密工作卸载到工作线程,仍然可以显著提升吞吐量。将加密与解密工作卸载到工作线程一张包含三个线程的时序图:持有 Interface 和 Tunnel 状态的所有者线程,以及位于其两侧的两个工作线程。工作线程 1所有者线程Interface, Tunnel工作线程 2出站入站PendingEncapsulate n加密934 nsDataMessage nPendingEncapsulate n\+1加密934 nsDataMessage n\+1begin\_encapsulate ×2每次 21 nsPendingDecapsulate m解密915 nsDecrypted mPendingDecapsulate m\+1解密915 nsDecrypted m\+1begin\_decapsulate ×2每次 38 nscommit\_decapsulate ×2重放检查,每次 18 ns 所有者线程只负责状态维护,而加密与解密在工作线程上运行。作为一个 sans\-IO 库,`sanstun`不关心你具体如何并行执行加密与解密。如果想保持简单,也可以直接在当前执行流程中调用`encrypt`和`decrypt`,完全不必考虑并行化。我们设计`sanstun`的 API 时,最想实现的是支持并行加密与解密,同时让围绕`Interface`的所有操作保持单线程。这样,内部速率限制器无需锁或原子计数器就能运行,还能利用 Rust 的借用检查器,同时整合来自多条隧道的状态。 ## 结语 (https://www.firezone.dev/blog/introducing-sanstun#closing) 随着`sanstun`的推出,我们在`firezone/boringtun` (https://github.com/firezone/boringtun)维护的分叉版本现已弃用。如果你正在基于 WireGuard 构建产品,无论是 VPN、网状网络,还是完全不同的东西,我们都希望`sanstun`能让你的工作轻松一些。它已在 crates\.io (https://crates.io/crates/sanstun)上发布。`sanstun`采用 Apache 2\.0 许可证开源。欢迎前往 GitHub (https://github.com/firezone/sanstun)查看,也欢迎提交问题和拉取请求!
相似文章
开放 WireGuard 端点
本文宣布 UDP Gateway 的新功能,包括接受任何客户端而无需预注册的开放 WireGuard 端点,以及用于事件驱动工作流的异步 Lambda 调用。
Plan 9 的 Wireguard
WireGuard 正在被移植到 Plan 9 操作系统,为这一小众操作系统带来现代 VPN 功能。
@fhwofjow51260: 推荐一个开源的 Rust 探针,叫 monitor。hub 空转 6.8 MiB,agent 1.8 MiB,两百个节点同时在线也才 10.8 MiB,占一颗核的百分之二。 设计很极端:agent 不监听端口、不执行指令,面板失守也接管不…
推荐一个开源的 Rust 探针 monitor,agent 仅 1.8 MiB、两百节点在线也只需 10.8 MiB,采用极端安全设计:不监听端口、不执行指令、不写文件,且不含 web terminal 与远程 SSH,支持主题商店与 Telegram/Webhook 通知。
宣布推出Rust版Zstandard
Trifecta Tech Foundation 宣布首次发布 libzstd-rs-sys,这是一个纯 Rust 实现的 Zstandard 压缩格式,可作为 C 参考实现的直接替代品,在轻微性能损失下提供更好的可移植性和内存安全性。
@paulmillr:关于此事的更多背景 + Tailscale 使用的是 WireGuard。Tailnet Lock 确保 Tailscale 自身无法向用户账户添加新设…
Paul Miller 提供了关于 Tailscale 与 WireGuard 使用的更多背景:Tailnet Lock 可防止 Tailscale 自行为用户账户添加新设备,且 Tailscale 软件开源可审计,但缺少后量子加密支持;他建议用户也可自建 WireGuard 服务器。