🌟

QUIC-LBを読んでみる

2023/05/14に公開

注意事項

本記事は自分の研究用の記録をメインの目的として残しています。
中には仕様が定かではないソースや自分の勉強不足からくる誤解が書いてある可能性があります。あらかじめご了承ください。

Introduction

今研究でMultipath QUICについて調べていて、クライアント側の経路を増やして通信する、といった技術に興味を持っています。
今の通信端末がWi-Fiとセルラー回線の両方が使えるんだから両方同時に使ったら帯域有効活用できるよね!っていう発想で作られたMultipath TCPのQUIC版ですね。
こちらについては後日別の記事にするとして、クライアントの性能が上がったら一般的にサーバはどのようにするのが適切でしょうか?
自分がパッと思いつく限りで言うとロードバランサ(負荷分散装置)がありますね。
今回はdraft-16になって未だ標準化に向けて作業が進めらているQUIC-LBについて勉強してみます。

Basic knowledges

ロードバランサの種類

主要なものにはアプリケーションロードバランサ(L7-LB)とネットワークロードバランサ(L4-LB)の二つがあります
AWSにもElastic Load BalancerがALBとNLBとして展開されてますね(そこ、CLBはとか言わない)

ネットワークロードバランサ

  • 処理する階層:トランスポート層
  • 処理内容:単純な振り分け (後述)
  • 判断材料: ポートとIPアドレス,(プロトコル)

アプリケーションロードバランサ

  • 処理する階層:アプリケーション層
  • 処理内容: リクエストに応じた処理の振り分け(HTTPリクエストの中身までをみて判断)
  • 判断材料: URL, Cookie, Header

ロードバランサの挙動

では簡単にロードバランサの挙動をみていきます
まずはネットワークロードバランサです

L4-LBはクライアントからの最初のSYNパケットを受け取った時点でパケットの内容から振り分けのための判断材料(IP/Port)が手に入リます。
この時点でLBはサーバの割り当てを決定し、サーバに対してパケットをそのまま送り、以降もLBを中継してパケットは変わらずやり取りされます。

次にアプリケーションロードバランサです。

L7-LBはHTTPパケットを受け取らないと判断材料が手に入りません
そのため、HTTPパケットが流れてくるまではLBはサーバの代理としてやり取りをします。
HTTPパケットが流れてくると判断が可能なので、サーバを決定し、LB-Server間でハンドシェイクをした後にリクエストを送ります。

ロードバランサのアルゴリズム

L7はセッション維持とか色々含めてアルゴリズムがたくさんあるようなので省略します。
AWS ELBのスティッキーセッションなんかもこの辺が元になっているんですかね🤔

L4-LBの振り分けの方法には静的なアルゴリズムと動的なアルゴリズムがあります。
以下にいくつかの例を示します

静的ロードバランシング (サーバの状態を加味せず振り分け)

  • ラウンドロビン方式 待機してるサーバに順番に振り分け
  • ハッシュ方式 IPやポートからハッシュを生成して振り分け

動的ロードバランシング(サーバの状態を加味して振り分け)

  • リーストコネクション方式 コネクション保持数が少ないところに振り分け
  • 最小応答時間方式 応答時間が少ないところに振り分け
  • リソースベース方式 サーバの負荷に応じて振り分け

QUICでL4-LBを使える?

QUICはステートフルなコネクションを構築し、同じCIDを持つパケットは同じサーバに分けてあげる必要があります。
ラウンドロビン方式ではコネクションを処理するサーバが変わってしまうため、そもそもハンドシェイクすら成功しないようです(運よく特定のサーバのみに振り分けられたら話は変わるかもだけど...)

ハッシュ方式は上手くいきそうだが、
以下の状況を加味しないといけないため、QUICの通信を続ける上では弊害が起こりそうです

  • ハッシュの再計算が起こる場合
  • 振り分け先のサーバが増減した場合や設定変更した場合
    これにより、
  • コネクションマイグレーションが使えない
  • クライアントのIPやポートが変わってもQUICはCMできるが、これらが変わるとLBは振り分けを再設定してしまい、元のサーバと通信できない
    などの弊害が起こりそうです。

先述の動的アルゴリズムも、振り分けにIP/Portを使っている以上、これらの問題は回避できないと言うことですね。

HTTP/3ならば...

HTTP/3を使うということであれば、L7ロードバランサをプロキシのように使えば上手い振り分けが可能無ようです。
2021年にGCPが提案したリバースプロキシを利用した方法を以下に示します
(2022年にAWSもCloudfrontで対応したけど似たような方法かと思います)
LBやCDNをリバースプロキシとしてリクエストをオフロードし、プロキシとServer間で接続を再構築する形ですね。

(LB-Server間はTLSと書いてますが、別にLBで終端してしまっても問題ないですね)

GCPのドキュメントを見てみると
クライアントからロードバランサはHTTP/3が使える

ロードバランサからバックエンドは以下の通り、HTTP/3の記述がないですね

QUIC-LB Generating Routable QUIC Connection IDs

QUIC-LBはL4でのロードバランサを提案するものです。
QUIC-LBの基本的な設計思想は、
LBとサーバであらかじめ情報の共有しとこ!サーバはConnection ID(CID)にその情報を組み込んでクライアントとやり取りしてくれたらLBは共有情報を元に振り分けを識別できるから!
というものです。

これからの説明をわかりやすくするために、以下の例を儲けてみます。
例えば学生課と学生が書類のやり取りをする時、学生が教授に学籍番号を教えておけば先生を介してやり取りできますね。(学生課がClient, M先生がLB,ゼミ生がServers,学籍番号がServer ID)

さて、これがQUIC-LBで言うPlain-text方式ですね。(後述します)
以前のdraftでは記載されているのに途中から記載がなくなってますね。わかりずらいので元に戻して欲しいです。

Plain text方式

plain textは、サーバの識別番号を平文でCIDに組み込んで共有する方法です。
課題として比較的容易にサーバIDの予測が可能なため、サーバIDが判明すると特定のサーバに対してのDoSが成立する、と言うものがあります
先ほどの例をネットワークの図に落とし込んでみます。

どうやってサーバの情報をCIDに埋め込む?

先に述べたように、ただ平文で埋め込むだけでは比較的容易にサーバIDを予想されてしまいます。
そこで以下を考えました
LBとサーバの間でサーバIDだけではなくAESの共有鍵等を共有して、暗号化したものをCIDに埋め混んで通信しよう!

ここにクライアントはそこに関与しないので鍵を知る必要はないし、LBでCIDから取った暗号済みサーバIDを復号し、該当サーバに送るだけでいいので、リスクは低減されます。

この方式は以下のような変遷を辿ってきました。

  • draft-7の段階だとAESの適用方式に応じてストリーム暗号方式、ブロック暗号方式を提案
  • draft-10でEncrypted Short、Encrypted Longに名称変更 (前者はアルゴリズムも)
  • draft-11以降はEncrypted Short CIDアルゴリズムを一般的な使用例として定義した

サーバの4パス暗号化アルゴリズム

まずはこの図を見てください。
FOはファーストオクテットといい、この後説明します。
ServerIDはサーバ達とLBを取り持つ構成エージェントというものから与えられます。
Nonceは長さだけ構成エージェントから指定され、サーバがランダムに生成します。
これらの情報から、共通鍵を持っているLBだから復号できるCIDを生成するわけですね。

Encrypted short CID
先ほどの図を辿ってみます

  • ServerID+nonceをplain text_cidとしてこれを奇数偶数で分割、left_0 right_0に
  • left_0を暗号化してright_0とXORしてright_1を作成
  • right_1を暗号化してleft_0とXORしてleft_1を作成
  • left_1を暗号化してright_1とXORしてright_2を作成
  • right_2を暗号化してleft_1とXORしてleft_2を作成
  • left_2+right_2をcyphertextとして,FOとくっつけてCID決定

という感じですね。4回似た動作が行われているので4パス暗号化アルゴリズムと言われています

補足事項
draft-14から生成CIDが類似しないように、ある条件下で各パスの後にright(left)の先頭4bitをクリアする処理が入ります。(詳しくはRFCをみてみてください。)

また、先ほども出た話題ですが、nonceは構成エージェントによって長さが決定され、サーバは最初にnonceはランダムで生成し、CIDの生成毎にnonceをインクリメントして使うなりしなければいけません。(そうしないと鍵が漏れる可能性があるようです)

また、Encrypted Long CIDは特殊な事例に場合分けされました。
nonce長とサーバーID長の合計がちょうど16オクテットの時
1パスアルゴリズムを使用してCIDを生み出すようです。詳しくはRFCをどうぞ

ロードバランサとしての役割を果たすために

AWS ALBのリスナールールを扱ったことがある方ならイメージがつきやすいかもしれませんが、先ほどの4パスアルゴリズムの適用方法もLBにおけるルールの一つです。
例えばサーバとLBの共有情報(AES鍵,ServerID,nonce長etc)による埋め込みルールが変更された場合を考えてみます。
ネットワークには変更前のルールで生成されたCID、変更後のCIDがあります。
このようなものをどのように管理すればいいでしょうか?

QUIC-LBではサーバが生成するCIDの第一オクテット(8バイト)を使い、この中にそれらの管理フラグを盛り込みます。
このオクテットの最初の2ビットがConfig Rotationと言われ、バージョンの管理に用いられます。
0b00 , 0b01 , 0b10は世代管理
0b11はフェイルオーバ用
のようです。

ここまで出てきたものをネットワークの構造的に表してみるとこんな感じですね

QUIC-LB Connection ID {
    First Octet (8),
    Plaintext Block (40..152),
}

Plaintext Block {
    Server ID (8..),
    Nonce (32..),
}

First Octet {
Config Rotation(2)、
CID LenまたはRandom Bits(6)
}

(QUICの仕様では最初の接続でDCIDはクライアントがランダムに生成するため、偶然Config Rotationとならないように注意が必要なようです)

まとめ

以上が現状のQUIC-LBの大きな設計と仕組みでした。(多分)
このほか、QUICの色々な考慮事項があるようなので、今後もそこについての議論が続いていくのかと思います。
既存のインフラとの整合性、セキュリティなどを考慮した上で新しいアーキテクチャを提案するのは大変ですね。それでは!

参考文献

https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/network/introduction.html
https://datatracker.ietf.org/doc/html/draft-ietf-quic-load-balancers
https://medium.com/nttlabs/quic-load-balancer-design-82c5fbae8305
https://asnokaze.hatenablog.com/entry/2019/12/30/201331

Discussion