3 ポイント 投稿者 GN⁺ 2024-07-05 | 1件のコメント | WhatsAppで共有
  • Firezoneでは Rust を使い、Androidスマートフォン、macOSコンピュータ、Linuxサーバー上でスケーラブルかつ安全なリモートアクセスを構築している
  • connlib という接続ライブラリを使って、ネットワーク接続と WireGuard トンネルを管理している
  • 何度もの反復の末に sans-IO という設計にたどり着き、高速で徹底したテスト、深いカスタマイズ性、高い信頼性を実現した

connlib は Rust で書かれており、sans-IO 設計に従っている

  • Rust の速度とメモリ安全性のおかげで、ネットワークサービスの構築に適している
  • tokio ランタイム、tungstenite WebSockets、boringtun WireGuard 実装、rustls による API トラフィックの暗号化などを利用
  • sans-IO 設計では、あちこちでソケットを通じてバイト列を送受信する代わりに、純粋な状態機械としてプロトコルを実装する

Rust の非同期モデルと「関数の色付け」論争

  • 非同期関数は、他の非同期関数からしか呼び出せない
  • 非同期関数の深い階層にある関数は、それを呼び出すすべての関数も非同期関数にしなければならない
  • このため、依存関係が非同期かどうかに無関心なコードを書きたい人にとっては問題になりうる

sans-IO の紹介

  • sans-IO の中核となる考え方は、OOP の世界における依存性逆転の原則に似ている
  • ポリシー(何をするか)は、実装の詳細(どうやるか)に依存すべきではない
  • Transmit 構造体を使ってデータを送信する代わりに、Transmit を放出する

依存性逆転の適用

  • Transmit 構造体を使ってデータを送信する代わりに、Transmit を放出する
  • イベントループは副作用を実装し、実際に UdpSocket::send を呼び出す

状態機械

  • STUN バインディング要求の状態機械ダイアグラムは、SentReceived の2つの状態を持つ
  • StunBinding 構造体と関連関数を定義して、状態機械を実装する

イベントループ

  • イベントループは状態機械を駆動し、poll_transmithandle_input を使ってデータを処理する

時間の抽象化

  • poll_timeouthandle_timeout API を使って、時間ベースの要件を処理する

sans-IO の前提

  • sans-IO 設計は、依存関係が非同期かどうかの判断をアプリケーション側に委ねる
  • sans-IO 設計は組み合わせやすく、柔軟な API を提供し、テストしやすく、Rust の機能ともよく合う

組み合わせやすさ

  • StunBinding の API は、ほとんどのネットワークプロトコルに適用できる
  • Firezone の snownet ライブラリは、ICE と WireGuard を組み合わせることで、ネットワーク設定に関係なく動作する「魔法のような」IP トンネルを提供する

柔軟な API

  • イベントループを自分で書くことで、コードのチューニングが可能になり、保守もしやすくなる

高速なテスト

  • sans-IO のコードには副作用がないため、テストが非常に容易
  • Firezone では参照用の状態機械を実装し、connlib の実際の状態と比較するテストを行っている

エッジケースと IO 失敗

  • sans-IO 設計は、プロトコル実装を実際の IO 副作用から分離し、エッジケースやエラー処理を容易にする

Rust + sans-IO: ベストマッチ?

  • Rust は所有権と可変性を明示的にモデル化するため、sans-IO 設計と相性がよい
  • sans-IO 設計では &mut を自由に使って状態変更を表現し、async Rust とは異なり同期 API だけを使う

欠点

  • イベントループを自分で書くと、微妙なバグが入り込むことがある
  • 順次的なワークフローでは、より多くのコードが必要になることがある
  • Rust コミュニティでは、sans-IO 設計はまだ広く使われていない

まとめ

  • sans-IO コードは最初こそ見慣れないが、慣れると非常に楽しい
  • Rust は状態機械をモデル化するための優れた道具を提供する
  • sans-IO 設計は、エラー処理を入力処理の一部として強制するため、ネットワーキングコードを書く正しい方法のように感じられる

GN⁺の意見

  • sans-IO 設計は Rust の所有権モデルとよく合い、ネットワークプロトコル実装に非常に適している
  • イベントループを自分で書くことで、コードの柔軟性と保守性が高まる
  • テストしやすいため、安定したコードを書くうえで大いに役立つ
  • ただし Rust コミュニティで広く使われていないため、関連ライブラリが不足している可能性がある
  • 新しい技術を導入する際は、学習コストとコミュニティの支援を考慮する必要がある

1件のコメント

 
GN⁺ 2024-07-05
Hacker Newsの意見
  • Rustにasync/await構文が導入される前は、手動で状態機械を実装していた

    • Rustのasync/await構文のおかげで生産性が大幅に向上した
    • Rustのasyncは自動的に状態機械へ変換され、I/O地点で値を保存してくれる
  • VT100ライブラリを書きながら、Rustのカプセル化パターンの問題に気づいた

    • カプセル化にこだわりすぎることが問題を引き起こす
    • コンピュータは入力、データ変換、出力を行う機械であることを思い出した
  • チャネルを使ってデータを送る設計との比較

    • コードが複雑になる
    • メッセージ型を手動で実装しなければならない
    • 送信側を明示的に提供する必要がある
    • ネットワーク送信に失敗すると結果を得られない
    • ただし便利な点もある
  • Haskellエコシステムには、ロジックと実行を分離するという考え方がある

    • tokio::select! 呼び出しをどのようにカプセル化したのかは言及されていない
    • sans-IOスタイルでカプセル化された関数の実装に関心があった
  • Rustのasync関数は状態機械にコンパイルされる

    • sans-ioとasyncを組み合わせようとする試みがあったのか気になる
    • 主な問題は使い勝手とPinの扱いである
  • 状態を公開すれば、async関数は「純粋」になりうる

    • OpenSSLをasync Rustにバインドしようと試みた
  • Firezoneは驚くべきツールだ

    • Rust-libp2pと似たパターンを見つけた
  • コンパイラがasyncコードをsans ioへ自動変換できればよいのにと思う

    • 手動変換はエラーが起きやすい
  • 記事とコメントを読んで、hexagonalまたはports/adaptersアーキテクチャスタイルを再発明したように思えた

  • 実際のトラフィックがゲートウェイを通過するのか、それとも接続設定にだけ使われるのか気になる