
2026/10/31 6:30
sanstun:Rust で実装された、sans-IO を採用した並列性に優れた WireGuard 実装
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Sanstun は、非推奨の
firezone/boringtun フォークを代替する、WireGuard 状態機械の最適化された Rust 実装(IO なし)をリリースしました。この新しい設計は、並列処理に適した慣用 API を使用して I/O および副作用(時間と乱数を含む)をコアビジネスロジックから分離することで、使いやすさ、セキュリティ向上、パフォーマンスを優先しています。主な利点には、プロトコル不変性をマッピングする型を通じたコンパイル時安全性、Instant オブジェクトによる時間処理を通じた正確なシステムシミュレーション、Diffie-Hellman 鍵交換およびクッキー管理を通じた DoS 攻撃に対する内蔵防御が含まれます。API は明示的に記録業務(例:カプセル化の開始)と重い暗号化/復号化を分離することで同期ボトルネックを回避し、暗号処理をワーカースレッドにオフロードした場合のベンチマークでは 45 倍以上のスピードアップの可能性を示しています。ユーザーは現在、パケットを手動でバッファリングする必要があり、Tunnel::initiate_handshake を通じてハンドシェイクを開始する必要がありますが、この設計はプライベートハンドシェイクコンスストラクタ(Interface::receive のみアクセス可能)を介してコンパイル時にレート制限および正しいトンネルルートを強制します。このアプローチにより、以前のフォークで問題となった直感的ではないドレインや設定できないバッファサイズのなどの内部バッファリング問題は解消されます。Sanstun パターンは Rust に限定されず、様々なプログラミング言語での実装にも有益である可能性があります。Apache 2.0 ライセンスの下で crates.io で利用可能であり、非推奨の firezone/boringtun フォークから移行する過程で GitHub の issue および PR を通じて貢献を招集しています。本文
sanstun:理念に基づいた Rust 製の WireGuard® ステートマシン実装
本日、sanstun を正式に発表いたします。これは sans-IO という理念に基づいた、Rust 言語による WireGuard® ステートマシンの実装です。
- 目的: 使いやすさ、セキュリティ、パフォーマンスの向上を目指しています。
- 設計方針: 並列処理(Parallelism)に最適化され、Rust 特有の慣習(idiomatic)に沿った API で設計されています。
sans-IO とは何か?
sans-IO の核となる思想は、システム設計において**「入出力(IO)」と「副次的な効果(side-effects)」**を、「状態」と「業務ロジック」から明確に分離することです。
- IO の定義: ファイルやネットワーク操作だけでなく、時間や乱数といった要素も含まれます。
- 設計上の利点:
- これらの要素が副作用を持たないため、コンポーネント同士は非常に良好に合成(compose)できます。
- テストも比較的容易に行えます。
- 「関数カラーリング」といった問題を回避でき、Rust の所有性ルールとも親和性が高いです。
注記: この記事のコード例は Rust 言語を用いていますが、sans-IO そのものは Rust に特化した概念ではありません。そのパターンや思想はあらゆるプログラミング言語に適用可能です。詳細は sans-IO に関する投稿をご参照ください。
沿革
Firezone の当初構築において WireGuard 実装として boringtun を採用し、要求をほぼ満たしていましたが、時間の経過とともにバグ修正や API 拡張のためのパッチをフォーク版に次々と適用する状況となりました。
- 当初の目標: パッチを上流(upstream)にも還元できるよう、既存 API の維持。
- 課題: 製品要件が進化するにつれ、上流との 100% API 互換性 を保つことが難しくなりました。
sans-IO への移行と内部バッファリングの問題
boringtun に施した最初の重大な変更の一つは、時間処理部分を完全に sans-IO にすることでした(firezone/boringtun#5)。これにより、
_at という接尾辞を持つすべての機能に対して新しい API が追加されました。
boringtun には当初から有効化されていた「内部バッファリング」ですが、以下のような問題を抱えていました:
- 排出(Drain)の必要性: バッファ内容の排出が必要で、ゼロ長パケットを持つ
の呼び出しで解決しているが直感的ではありません。decapsulate - メモリ割り当て: 追加のメモリ割り当てが発生します。
- 柔軟性の欠如: バッファサイズの設定・調整が困難です。
これらの問題は**「ライブラリ内でバッファリングを一切行わないこと」**という解決策を指し示しています。これにより、インターフェースと実装の双方が簡素化されました。
- 適用されたパッチや修正の一覧は非常に長くなっています(依存関係更新を除き 70 のプルリクエスト 以上)。
- これらはエッジケースの修正、新しい API の追加、設定可能にする変更などを含みます。
boringtun を 2 年以上使用し、パッチを適用してきた結果、ユーザー空間における WireGuard 実装の全体像となるべき API が明確となり、それが sanstun として存在します。
より優れた sans-IO API
sans-IO API の核心となる構築要素は以下の通りです:
機能別の API メソッド
| カテゴリ | メソッド名 |
|---|---|
| 入力パケットの処理 | |
| 出力パケットの処理 | |
| タイマー更新の処理 | |
| イベントの処理 | |
- 設計上の工夫: 内部バッファリングがないため、特定の API をループで呼び出す必要がありません。API がコンパクトであり、sans-IO モデル全体に格好良く適合しています。
- ハンドシェイク制御: パケット送信には
を呼び出します。セッションが存在しない場合はパケットをバッファリングするかどうかに任せ、新しいセッションを作成するにはハンドシェイクを送信します。任意のタイミングでTunnel::begin_encapsulate
で新たなハンドシェイクを開始可能です。Tunnel::initiate_handshake
WireGuard プロトコルとセキュリティ
WireGuard のメッセージ形式は 4 つのタイプをサポートしています:
- ハンドシェイク開始(handshake init)
- ハンドシェイク応答(handshake response)
- クッキー応答(cookie reply)
- 暗号化されたデータ
セキュリティ上の課題:DoS 攻撃への対策
WireGuard の設計により、エンドポイントを開示することが DoS(サービス拒否)の機会を発生させないよう確保されています。
- 問題点: エンドポイントの公開鍵を知っている者は、偽のハンドシェイク開始メッセージを作成し、入力される UDP パケットのソース IP を捏造するのが容易です。
- 防御コスト: 受信側が供給されたハンドシェイクデータが偽物であることを識別するには、DH 演算の自らの半分を実行する必要がありますが、その時点で CPU リソースはすでに消費されています。
- 攻撃手法: 十分な数のハンドシェイク開始メッセージを送信することで、受信側は関連しないハンドシェイクの処理にすべての CPU サイクルを費やす可能性があります(CPU 枯渇攻撃)。
防御策:クッキー機構
- WireGuard はリモートホストが主張するソース IP の所有権を証明させるクッキー機構を指定しています。
- これにより、ソース IP 単位でのハンドシェイクレート制限が可能となります(単一のトンネルだけでなく全体インターフェースに適用されます)。
- ハンドシェイク完了後、両方のピアは対称暗号を使用して暗号化されたデータパケットを交換できます。
トンネルとセッション管理
- ハンドシェイクの一部として、ピアはそれぞれが選択したセッションインデックスも交換します。
- これらのインデックスにより、トンネル間のマルチプレックス化ならびにトンネル内の複数のセッションの実行が可能になります。
プロトコルの不変性を API 設計へマッピング
Rust のような言語で作業する場合、型を使ってコンパイル時に不変性を強制し、バグを発生する前に防ぐことが重要です。
- 例: メッセージに対してレート制限器を適用することを忘れると、通常動作下では問題が表面化しません。
- 目標: 優れた API 設計は、壊れたものを構築することを不可能にし、プロトコルの仕組みを理解する道筋を示します。
一つのインターフェース、多数のトンネル
WireGuard インターフェースは単一の鍵で識別され、トンネルは二つのインターフェース間の接続を意味します。
: インターフェース(単一鍵)Interface
: トンネル(二インターフェース間の接続)Tunnel
パケット送信時はどのトンネルを通すか指定し、入力パケット受信時はどのトンネルに属するかを特定する必要があります。sanstun ではこれらの概念が直接的に
Interface と Tunnel タイプに対応しています。
処理フロー
すべての入力パケットは
を通ります。これは生のデータグラムを解析する唯一の公開 API です。
このメソッドは以下の処理を行います:Interface::receive
- パケットを解析する
- レート制限を適用する
(各メッセージタイプの変数を持つ)またはクッキー応答を返すReceivedMessage
これらは対応するトンネルの機能に直接マッピングされます(例:
Tunnel::handle_handshake_initiation)。
重要ルール: すべての UDP データグラムは
を通ります。すべてのチェックが通過するまで、メッセージは対応するトンネルに転送されません。Interface::receive
処理ステップ:
: 負荷下でのクッキー応答 ->datagramInterface::receiveparse headerverify MACcheck cookierate limit source
メッセージのルティングルール:
: イニシエーターの静的鍵によるルーティングHandshakeInitiation
:HandshakeResponse
(例:5) で識別TunnelId
:CookieReply
(例:5) で識別TunnelId
:TransportData
(例:7) -> 対応するTunnelId
に転送(例:3)Tunnel
型安全性の保証:
などの型のコンストラクタはすべて クレート固有(crate-private) です。HandshakeInitiation
を呼び出すためには、まずTunnel::handle_handshake_initiation
を経由して該当する型を取得する必要があります。Interface::receive- この設計により、アプリケーション側がレート制限器を直接適用しなくても保護されます。
並列化可能な暗号処理
sanstun の最も優れた機能の一つは、パケットの暗号化および復号化のための並列処理に最適化された APIです。
背景と課題
- WireGuard はトンネル間で状態を共有しません。ピア間での実装並列化は容易ですが、単一トンネル内の並列化は困難です。
- ボトルネック: ワークスレッドで処理しても、CPU パフォーマンスによって制限されます(特に ChaCha20-Poly1305 アルゴリズム)。
- 同期問題: 再生攻撃を防ぐためにパケットカウンターが含まれており、これへのアクセスには同期が必要です。
解決策:事務処理と暗号化の分離
sanstun は**事務処理(bookkeeping)**を実際の暗号化・復号処理から分離しています。
// 例:事務処理のみを行う関数 pub fn begin_encapsulate(...) -> PendingEncapsulate { // セッション確認、カウンター予約、タイマー状態を進める }
impl PendingEncapsulate { pub fn encrypt<'a>(self, plaintext: &[u8], dst: &'a mut [u8]) -> Result<DataMessage<'a>, Error> { // ... 実際には何も暗号化されていない(ここでも事務処理のみ) // 暗号化は別のスレッドで実行される準備を完了する } }
はPendingEncapsulate
トrait を実装しており、他のスレッドに渡して主要な処理を実行できます。Send
パフォーマンスベンチマーク
sanstun リポジトリで cargo bench data_path` を実行した結果:
64 バイトパケット
| 処理項目 | 平均時間 (ns) |
|---|---|
| ~20.8 |
| ~226.2 |
| ~37.6 |
| ~205.8 |
| ~17.5 |
1280 バイトパケット
| 処理項目 | 平均時間 (ns) |
|---|---|
| ~20.8 |
| ~933.5 |
| ~37.6 |
| ~914.9 |
| ~17.7 |
分析:
- 事務処理部分(
等)はパケットサイズに比例して増大しません。begin_encapsulate - 暗号化/復号化のみがパケットサイズに依存します。
- 1280 バイトの場合、事務処理部分は全体の**わずか 2%**に過ぎません。
- この設計により、理論的には45 倍以上のパフォーマンス向上が可能です(実際にはスレッド渡しのコストなどで達成困難ですが)。
アーキテクチャ例
暗号処理をワーカースレッドへオフロードする
3 つのスレッドを持つシーケンス図:
- 所有者スレッド: インターフェースとトンネル状態を保持。事務処理のみ実行。
- ワーカー 1: 入力
-> 暗号化 (934 ns) -> DataMessage -> 出力PendingEncapsulate
x2begin_encapsulate - ワーカー 2: 入力
-> 復号化 (915 ns) -> Decrypted -> 出力PendingDecapsulate
x2begin_decapsulate
sans-IO ライブラリの sanstun は、あなたがどのように暗号処理を並列化したいかを気にしません。シンプルさを保ちたい場合は、そのまま
encrypt と decrypt をインラインで呼び出すことも可能です。
達成目標:
周辺での一切を単一スレッドで保ちつつも並列化可能な暗号処理を実現することです。これにより、内部のレート制限器がロックやアトミックカウンターなしに動作し、Rust のバローチェッカー(borrow checker)を活用しながら複数のトンネルの状態を統合できます。Interface
おわりに
sanstun によって、firezone/boringtun にて行ったフォーク版は現在廃止されました。 WireGuard を基盤としたアプリケーション(VPN、メッシュネットワークなど)を開発する皆様にとって、sanstun が作業を容易にすることを願っています。
- 入手先: crates.io
- ライセンス: Apache 2.0 (オープンソース)
- コードリポジトリ: GitHub(※実際の GitHub URL はリンク切れの場合あり、プロジェクトページを参照)
問題提起やプルリクエストの歓迎いたします!