espress_streamは、実用性を維持した上で、必要最小限の機能を持つストリーム通信のプロトコルです。 また、それをできるだけ単純に実装したものでもあります。
必要最小限の機能は以下のように設定しました。
ネットワークインターフェイス層としては任意のデータグラム通信を想定しています。 linux用実装においてはこれをUDPとすることで、インターネット通信を可能にしています。
実装においては、組み込み機器を含む様々なアーキテクチャーへの対応を考慮し、以下のような要素を備えることとしました。
接続は1RTT (ラウンドトリップタイム)で行なわれます。 この中で鍵交換が行なわれ、鍵が共有されます。
espress_streamではサーバー、クライアント認証は行なっていません。 サーバー、クライアント認証はアプリケーションが行なうことを前提としています。 接続パケットには任意のデータが搭載できるようになっています。 アプリケーションはこれを利用することで、サーバー、クライアント認証の処理を接続時に進めておくことができます。
切断は1/2RTTで行なわれますが、これにより切断が行なわれるのは切断パケット送信側の送信チャンネルだけです。 espress_streamプロトコルでは、1セッション内の送受信チャンネル各々の切断は独立しておこなわれます。
切断がたった1つのパケット送信によって行なわれるために、無視できない確率で切断通知の不達が発生します。 espress_streamではある一定期間(現在の実装では10秒)データが受信されないと、その受信チャンネルは切断されたと認識することになっています。 これにより、切断パケットの受信がなくとも、受信側は受信チャンネルを能動的に切断することができます。
送信チャンネルの接続を維持したいが送信するデータがない場合はある一定間隔(現在の実装では1秒間隔)でゼロサイズのデータを送信することで切断を抑制します。
このような無受信期間タイムアウトによる切断検出と無送信データ期間のビーコン送信による活線維持は、espress_stream実装内で自動的に行なわれるので、アプリケーションは意識する必要はありません。
espress_streamではビーコン送信を含むデータの送信に対応する応答が一定時間(現在の実装では10秒)受信されないと、通信が途絶したと判断することも行っています。 これはTCPにおけるキープアライブ1の機能と同等のものです。 通信途絶は送信チャンネル側のエラーとして検出されますが、受信チャンネル側もエラーとします。 通信途絶は送信チャンネル側での検出のため、送信チャンネルが切断されている場合、検出されません。 もし、送信チャンネルが切断されている状態で通信途絶が発生した場合、受信チャンネルの切断として検出されます。
通信途絶の検出もespress_stream実装内で自動的に行われます。通信途絶の検出はアプリケーションに通知され、送受信等のAPI実行結果にエラーとして反映されます。
接続の識別はコネクションIDによってなされ、ネットワークアドレスには依存しません。
このため、espress_streamは一つのネットワークアドレスから一つのネットワークアドレスへの接続を同時に複数生成することができます。 一つのアドレスにサーバーとクライアントを共存させることも可能です。 ネットワーク接続のマイグレーションも可能です。
接続パケット以外のパケットにはカウンター番号が付けられています。 カウンター番号はパケットが送信されるたびにインクリメントされます。 再送パケットにおいてもインクリメントされます。
これにより、送信パケットの一意性を保障し、計測RTTの正確性とパケットの保安性を高めます。
カウンター番号は32ビットの整数で、ラップアラウンドします。 このため、保安性に必要な一意性に問題が発生しますが、これはラップアラウンドするたびに鍵拡張アルゴリズムにより共有鍵を更新することで解決しています。
フロー制御はTCPと同じスライディングウィンドウ方式によって実現されています2。 ただし、パケットの単純化、実装の容易化を目的にセレクティブACK3は用いられていません。 遅延ACK2も同様に用いられていません。
現在の実装ではNagleアルゴリズム2は用いられていません。 送信バッファーにMTUを満たすサイズのデータが記録されないと実際のデータ送信が行なわないので、アプリケーションは適宜データの排出を指示する必要があります。
TCPと同様の再送信タイムアウト2による再転送と高速再転送4を行ないます。
再送信タイムアウト時間もlinuxのTCP実装で用いられているのと同じアルゴリズムで、計測RTTから動的に求められています。
輻輳ウィンドウを輻輳発生状態によって調整して制御しています4。 現在の実装で用いられている輻輳ウィンドウを調整するアルゴリズムはNew Reno5です。
New Renoは若干古い方式ですが、実装が簡単なため採用されました。
接続時に交換される公開鍵により双方のノードに共通の鍵を生成します。 鍵交換にはECDHアルゴリズム、楕円曲線にはCurve25519を使用しています6。
共有鍵から暗号とメッセージ認証のための鍵を生成します。 鍵拡張にはSHA2-2567を用いたHKDF関数8を使用しています。
接続時に送信パケットの暗号、メッセージ認証に用いる鍵拡張のためのソルトを両ノードが互いに送信します。 互いに送信するソルトは独立した乱数値です。 このため、各々のノードが送信するパケットには異なる鍵列が用いられます。
暗号化は送信データおよびそのメタ情報に対して行なわれています。 暗号にはブロック暗号AES2569を暗号利用モードCTR10で使用しています。 CTRで用いるナンスには送信データパケットのヘッダーを使用しています。
全てのパケットにはメッセージ認証が付けられます。 メッセージ認証にはSHA2-2567を用いたHMAC11をETMで使用しています。 メッセージ認証によりパケットの偽造、誤りを検出します。
スレッドの使用、未使用が選択できるようになっています。
スレッド使用時はposixにおけるpthread_t、pthread_create、pthread_joinに相当するstruct thread、thread_create、thread_joinが定義され、使用されます。 スレッド起動、停止は集中的に行われており、スレッド操作のオーバーヘッドを減らしています。 (linux用実装ではAES256-CTRの処理を行う部分にOpenMPを使用しています。 このため、espress_stream実装のソースコードで明示的に起動、停止されるスレッド以外のスレッドが起動、停止されています。)
スレッド使用時に生成されるスレッドからは、ライブラリー内の各種モジュールを作動させるルーチンが呼び出されます。 スレッド未使用時には、タイムアウトを持ち、処理の完了を待つAPIからこれらのルーチンが適宜呼び出されます。 このため、アプリケーションプログラマーは、大抵の場合、スレッド未使用時もスレッド使用時と変わらない感覚でAPIを使用できます。
ヒープの使用、未使用が選択できるようになっています。
ヒープ使用時は標準ライブラリーのmalloc、freeが使用されます。 ヒープの確保は、前述のスレッドと同様、集中的に行われ、低効率のヒープに対しても性能低下が発生しにくくなっています。
ヒープ未使用時には、ライブラリーの設定値として、使用するネットワークインターフェイスとセッションの最大数を指定します。 各々の最大数に必要なメモリーはライブラリー内で静的に確保され、適宜使用されます。 このため、ヒープ未使用時においてもヒープ使用時とAPI使用方法が変わることはありません。
特定のアーキテクチャーへの対応は基本実装に対して適宜モジュールを入れ替えることで実現します。
基本実装はlinux上で動作するように作られていますが、アーキテクチャー依存部は以下にしめす小規模なものになっています。
これらを動作させたいアーキテクチャー用のものに入れ替えることで任意のアーキテクチャー上でespress_streamを動作させることができます。
更に、必要に応じて以下のモジュールを入れ替えることで、システムに最適化することができます。
その他、espress_stream_block_cipher (ブロック暗号部)、espress_stream_hash (ハッシュ生成部)等に対してシステムに同等の機能が存在する場合、それらを利用するようなモジュールに入れ替えることで、より最適化することができる可能性があります。