0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

パッチ70本で見えた WireGuard の限界——sanstun が捨てたものと、残したもの

0
Posted at

70本の修正が示した、フォーク運用の重さ

画像: www.firezone.dev より
画像: www.firezone.dev より

Firezone が sanstun を紹介した記事は、単に「新しい WireGuard 実装が出た」という話ではありません。出発点は boringtun のフォークでした。最初は「ちょうどいい実装」として機能していたものの、使い続けるうちに bug fix と API 追加が積み上がり、上流に戻せる形を保つために既存 API 互換を守り続ける必要が出てきます。ここで既に、保守の難しさが始まっていました。

互換性を維持したまま要件を変えるのは、見た目以上に厄介です。たとえば Firezone は、時間の扱いを完全に sans-IO にするため、Instant を受け取る _at 系 API を増やしました。また内部バッファリングも段階的に外していきます。最初は「送る先がまだなくてもライブラリが溜めておけば便利」に見えますが、実際にはどこかで drain する責任が発生し、余計な確保も増え、バッファサイズの調整まで必要になります。便利さが、設計の重さに変わる瞬間です。

この話が面白いのは、WireGuard というプロトコル自体がもともと軽快に見えて、実装側では逆に「状態」「時刻」「乱数」「再送」「バッファ」が強く絡み合う点です。WireGuard は UDP の上に乗るため、1 パケットごとの扱いは単純でも、セッションの開始や再キーイング、cookie 応答、再送制御まで含めると、ただの暗号化ライブラリでは済みません。Firezone が 70 件超の PR を重ねたという事実は、その複雑さが表に出た結果だと読めます。

しかもその 70 件超は、単なる依存更新ではありません。エッジケース修正、API の追加、設定可能項目の増加が混ざっています。つまり「バグを直したら終わり」ではなく、「直すたびに API が現実に引っ張られる」状態です。Rust のように型で安全性を積み上げたい言語ほど、このズレは目立ちます。型の力で守れる部分がある一方で、設計そのものが古いと、型はむしろ不自然な継ぎ足しを隠しきれません。

sans-IO にすると、何がライブラリの外へ押し出されるのか

sanstun は Firezone の説明では「A sans-IO, parallel-friendly, userspace WireGuard implementation in Rust」とされています。ここでの sans-IO は、I/O と副作用を、状態やビジネスロジックからきれいに分ける設計思想です。I/O にはネットワークやファイルだけでなく、時刻や乱数も含まれます。要するに「外の世界」とやり取りする部分を外に出し、内部は純粋な状態機械として扱うやり方です。

この分離が効くのは、テストと合成です。副作用が少ないコンポーネントは、決まった入力に対して決まった出力を返しやすく、複雑な通信処理でも再現しやすい。Firezone の sans-IO 解説では、通常の async 実装が抱える「function coloring」の問題にも触れています。ある関数が async になると、その呼び出し元も async に寄っていく。依存の都合で、呼び出し階層全体が巻き込まれるわけです。ネットワークライブラリでは、これが「I/O の都合でロジックが縛られる」状態を生みます。

sanstun は、その縛りを API の形で解きにいっています。README では、プロトコル状態にはロックもアトミックカウンタもなく、実際の暗号処理は別スレッドに逃がせると説明されています。さらに docs.rs の例では、Interface::new、new_tunnel、receive、begin_encapsulate、begin_decapsulate、commit_decapsulate といった分かれ方が見えます。つまり「何を決めるか」と「どこで UDP を送るか」を分けている。ライブラリは状態遷移だけを示し、実際の送受信はイベントループ側に任せる形です。

従来型の実装だと、送る・受ける・時刻を読む・再送を予約する、がひとまとめになりがちです。するとテストではソケットやタイマーを握り込む必要があり、実運用では見えにくいタイミング依存のバグが混ざります。sanstun が狙うのは、その不透明さを減らすことです。ただし、ここで重要なのは「I/O を消した」わけではない点です。I/O は消えず、ライブラリの外に追い出されます。その結果、次に問うべきは「外に出した後、どの責務をどこまで持たせるのか」です。

WireGuard の都合が、そのまま API の形になる

WireGuard のプロトコルは、見た目よりずっと設計制約が強いです。公式の protocol では、メッセージ種別は handshake initiation、handshake response、cookie reply、encrypted data の 4 種類。セッション開始では Diffie-Hellman 交換が走り、cookie 機構で DoS を抑えます。しかも cookie の rate limit は「個々の tunnel」ではなく「インターフェース全体」にかかる。つまり、受信側は「誰から来たか」を雑に信じてはいけません。

この制約が API に落ちると、パケットを受けた瞬間に何でもかんでもトンネルへ渡す、という作りは危険になります。sanstun の docs.rs では、Interface::receive が唯一の公開パース入口になっています。生の UDP datagram はまずここで header の解析、MAC の確認、rate limit の適用、cookie reply の生成まで通され、その後に ReceivedMessage として型付きで返されます。ハンドシェイクの開始は peer_static_public で関連付けられ、そこから先を Tunnel に渡す構造です。

この振る舞いは、WireGuard で特に問題になる「高価な計算を遠隔から好きに起動させない」ためのものです。Firezone の記事でも、受信側は不正な handshake initiation かどうかを知る前に DH の自分の側の計算をしてしまう、と説明されています。攻撃者が大量の偽 initiation を投げれば、CPU を持っていかれる。だから sanstun は、受信入口で rate limiter を通さない経路を用意しない。型で「まだ処理してはいけないもの」を作りにくくしているわけです。

一方で、これは使う側に責任を押し返します。docs.rs の「Caller responsibilities」には、cryptokey routing、inner IP packet validation、bounded queues、monotonic clock などが並びます。たとえば暗号化されたからといって、その送信元 IP を許可したことにはなりません。復号後の IP パケットが本当に正しい長さか、パディングをどう扱うかもアプリ側です。sanstun は WireGuard の面倒な境界を減らしますが、ネットワーク製品として必要な判断はまだ残しています。では、その「残る責任」が実運用でどれくらい効いてくるのか。ここが次の焦点です。

並列化しやすい、でも楽になるとは限らない

sanstun の README が強く押し出しているのは、parallel-friendly という性格です。WireGuard はトンネルごとに共有状態を持たないので、異なる peer への処理を並列化しやすい。さらに internal buffering をなくしたことで、暗号処理を後段のスレッドへ回しやすくなっています。これは、ただ速いというだけではなく、バッチ処理や多コア活用を前提にしやすい設計です。

ただし、並列にできるからといって、アプリ全体が単純になるわけではありません。WireGuard は layer 3 で動くため、ひとつのセッションに多くの通信フローが乗ります。同じ宛先に向かう流れは、結局ひとつのセッション上で順に処理されやすい。Firezone のブログは、peer ごとのトンネルを別スレッドで扱っても、同じホストやサブネットへ向かうフローはすべて順番に処理され、WireGuard のデータ用暗号 ChaCha20-Poly1305 を CPU がこなす速さで頭打ちになると指摘しています。1 本のトンネルの中の処理をどう分けるのかが問題になります。

さらに、sans-IO には「便利なことが起きない」側面もあります。たとえば receive が cookie reply を返したら、送信するのはイベントループです。PendingDecapsulate は生成は軽いが復号は重いので、キューを無制限に積むとメモリを食います。時刻も単調増加でなければならない。乱数も十分なエントロピーを持つ seed を渡さないといけない。ライブラリがやってくれることが減る分、アプリケーションは「いつ何を投げるか」をきちんと作らなければなりません。

ここで面白い比較対象になるのが、WireGuard を Rust で扱う他の流れです。Mullvad は wireguard-go から Rust 実装の GotaTun に移行し、Android での crash rate が 0.40% から 0.01% へ下がったと公表しました。Mullvad はその理由として、Go 実装由来のクラッシュと、Rust と Go をまたぐ FFI のデバッグの難しさを挙げています。別の実装ですが、やはり「境界をどう持つか」が製品品質に効いた例です。sanstun も同じく、実装そのものより境界設計で評価されるタイプのライブラリです。ここまで来ると、残る疑問はひとつです。Firezone は、なぜ「フォークを積み増す」方向ではなく、API を引き直す方向に振れたのか。その答えは、70 本超の修正の中身を見ないと見えてきません。

👉 続き(核心はここから)は PaPoo で読めます: https://papoo.work/doc/7574137f9294a716

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?