この記事は後半です。前半は Qiita にあります: パッチ70本で見えた WireGuard の限界——sanstun が捨てたものと、残したもの

画像: mullvad.net より
sanstun の紹介文で目を引くのは、単なる機能追加ではなく、状態機械そのものを API として組み直した点です。Interface と Tunnel に責務を分け、受信は Interface::receive に集約する。生の UDP datagram をどこからでも突っ込める形にしないのは、WireGuard の都合そのものです。
WireGuard は handshake initiation を誰でも投げられます。受信側は、相手が本当に正当な peer かを見極める前に、DH を含む重い処理をある程度進めざるを得ません。だからこそ、受信入口で rate limit を必ず通し、cookie reply もそこで返す。sanstun はこの制約を隠さず、型と公開 API の配置で前面に出しています。HandshakeInitiation のような型はコンストラクタが crate-private で、Interface::receive を通さずに作れないため、順番を飛ばした呼び出しをしにくい。設計の狙いは「便利にすること」より、「壊れた使い方をしにくくすること」にあります。
sans-IO という言葉は、I/O を消すという意味ではありません。むしろ逆で、I/O をライブラリの外へ追い出す設計です。時刻、乱数、ネットワーク送受信、さらには副作用に相当する処理まで、内部の状態機械から切り離す。Firezone の説明では、これがテストしやすさと合成しやすさにつながるとされています。
ただし、外に出した責務は消えません。docs.rs の「Caller responsibilities」に並んでいる内容が、そのまま実運用での注意点です。復号したパケットの source IP を許可リストと照合すること、正しい tunnel を選んで暗号化すること、復号後の IP パケットをパースして長さやパディングを確認すること。PendingDecapsulate を無制限にキューへ積まないことも重要です。ライブラリが軽くなるほど、アプリケーションは「どこまでを信じるか」を自分で決める必要があります。
ここが sanstun の面白いところです。便利な抽象化に見えて、実際には責務の境界線をかなり厳密に引いています。Tunnel::begin_encapsulate はパケットを暗号化する準備だけをし、実際の送信はイベントループが担う。poll_timeout や handle_timeout も同じです。ライブラリは「何をすべきか」を返し、実行は外側がやる。設計の重心が完全に分離されているので、見た目以上に規律が要ります。
README が掲げる parallel-friendly は、かなり正確な表現です。WireGuard はトンネル間で状態を共有しないため、複数 peer の処理を並列化しやすい。sanstun も internal buffering を持たず、暗号処理を別スレッドへ逃がしやすい設計になっています。
難しいのは単一トンネルの中です。WireGuard は layer 3 で動くので、ひとつのセッションに複数の通信フローが乗ります。しかもデータパケットにはリプレイ対策の単調増加カウンタがあり、セッション単位の状態なので、素直に複数スレッドから触れば同期が要ります。sanstun はここで帳簿付けと暗号処理を分けました。begin_encapsulate はセッションを確認してカウンタを予約し、タイマー状態を進めるだけで、返ってくる PendingEncapsulate(docs.rs の説明では「カウンタ予約済みの、暗号化待ちのデータメッセージ」)は Send なので、重い encrypt を別スレッドで実行できます。受信側も begin_decapsulate で初回のリプレイ確認、decrypt を別スレッド、commit_decapsulate で確定という分け方です。Interface と Tunnel の状態は持ち主のスレッドに残るので、rate limiter はロックもアトミックカウンタも要りません。ただし並列化の仕方そのものはライブラリが決めず、呼び出し側の設計です。
また、公式の説明では now には単調時計を使い、Interface::new に渡す Unix 時刻も再起動をまたいで巻き戻してはいけないとされ、seed も十分なエントロピーが求められます。sans-IO は deterministic になりやすいぶん、同じ seed なら同じ鍵や nonce が出る。テストではありがたいですが、本番で雑に扱うと危ない。楽になるのは実装の見通しであって、暗号の厳しさまで薄まるわけではありません。
このライブラリを自分が使うなら、まず「ネットワークの外側に置きたい核」に向いているかを見ます。たとえば、UDP の受信処理や再送制御をアプリ本体から剥がし、イベントループで一元管理したいときです。逆に、単に WireGuard をすぐ動かしたいだけなら、ここまで設計を背負う必要はありません。sanstun は“簡単に使える完成品”というより、“間違えにくい部品”に近いです。
Firezone が 70 件超の修正を積んだ末に選んだのは、互換性を守りながら継ぎ足す道ではなく、責務の線引きを引き直す道でした。その判断は、WireGuard の実装が抱える本当の複雑さが、暗号そのものより境界設計にあることを示しています。sanstun の価値は、実装を新しくしたことではなく、何をライブラリに残し、何を外へ出すかを言語化したことにあります。