インフラエンジニアなら気になるQUICのロードバランサ (方式編)

Jun-ya Kato
nttlabs
Published in
18 min readAug 24, 2021

--

図1: QUICコネクションを振り分けるロードバランサ

はじめに

本記事では、バックエンドのWebサーバへリクエストを振り分ける装置の意味でのロードバランサ(図1)について、QUIC対応の議論状況を紹介します。方式編と実装編にわけて二編を予定しており、本稿は方式についての解説です。

IETFでは、F5 Networksとマイクロソフトから提案されたロードバランシング方式が議論されています。本稿では下記のインターネットドラフトをQUIC-LBと表記します。

QUIC-LB: Generating Routable QUIC Connection IDs
https://datatracker.ietf.org/doc/html/draft-ietf-quic-load-balancers

執筆時点の -07 をベースとしますが、ドラフトですので今後の議論次第で改版が続きます。あらかじめご承知おきください。

なお、過去の記事においてQUIC対応Nginxの実装が、eBPFを使ってLinuxカーネルのSO_REUSEPORT機能を拡張する様子を紹介しました。ホスト内で複数プロセスへCPUを割り当てる手法の一つと言えるものでしたが、本稿の対象はUDP/IPパケットを振り分けながら転送を行う装置です。

リバースプロキシによるL7終端方式

QUIC-LBとは異なる方式ですが関連する重要な話題です。2021年7月21日 GCP Japan TeamのブログでロードバランサのHTTP/3対応が案内されました。公式の紹介動画ドキュメントを見ると、フロントに置かれたリバースプロキシがHTTP/3接続を受け付けてQUICコネクションを終端し、バックエンドに向けてHTTPリクエストをTCPに載せ替えて再送信する構成が取れます(図2)。

図2: L7終端によるロードバランサ

クライアントからみてQUICコネクションを張るエンドホストは、リバースプロキシ(L7ロードバランサ)となります。本形態ではTLS処理をWebサーバからオフロードしてロードバランサで暗号を解いたり、WebサーバをHTTP/3に対応させる必要がない点がメリットです。しかし、エンド-to-エンドの暗号化や低遅延など、HTTP/3, QUICの効果を最大限に発揮するには、クライアントとWebサーバ間で直接コネクションを確立するのが望ましいと考えられます。

トランスポートレイヤ(L4)のロードバランサとQUIC利用時の問題

もうひとつだけQUIC-LBの方式にふれる前に、TCPのL4ロードバランサを使ってTLSをつなぐ場合を考えます。TCPコネクションをロードバランサの前後でスプリットしますが、TLSはCONNECTメソッドを延長して、クライアントとWebサーバ間で直接ネゴシエーションを行います(図2)。

図2: TLSをエンド-to-エンドでネゴシエーションする形態(DSRなしの構成例)

QUICコネクションはTLS/TCPと同様に、クライアントとWebサーバのエンドホスト間でネゴシエーションを行いますので、QUICロードバランサはQUICパケット(=UDPパケットのペイロード)を改変することなく適切なWebサーバに向けて転送することが必要です。UDPのL4ロードバランサでは、4タプル情報(送信先IPアドレス, 送信元IPアドレス, 送信先ポート番号, 送信元ポート番号)を使ってフローを認識して振り分けますが、QUICでは同一のQUICコネクションでも、UDPレイヤの4タプル情報が変化するため、4タプルのフロー識別とQUICコネクションが必ずしも一致しません。例えばクライアントのIPアドレスや、送信元ポート番号が変化しても同一のQUICコネクションを維持することが可能です。変化のトリガとなるネゴシエーションは暗号化されており、のぞき見ることができないため、コネクションとUDPレイヤの4タプル情報の対応づけを追跡できません。

なお、ロードバランサの転送が最適では なくてもWebサーバ間で目的のサーバまで転送する方式や、コネクションマイグレーションを無効化する方法なども考えられます。RFC 9000 “5.2.3 Considerations for Simple Load Balancers” 節ではロードバランサに関する制約や考慮事項を言及していますが、QUIC-LBは制約の解決を試みる方式と言えます。

QUIC-LB の提案方式の概略

QUIC-LBは、ロードバランサとエンドホストのWebサーバが、あらかじめQUICコネクションに関する情報を共有しておき、ロードバランサにQUICコネクションを識別させる方法を提案しています(図3)。共有する情報はコネクションIDにWebサーバを示すサーバIDを埋め込む方法です。クライアントが送信して、ロードバランサに到着するQUICパケットの宛先コネクションIDに、振り分け先のWebサーバを示すサーバIDの情報を埋め込ませるようにします。ロードバランサは事前に、どのような方法で埋め込まれた手順を知っていれば、サーバIDを取り出すことができますし、知らなければランダムのIDにしか見えません。ロードバランサはサーバIDを、コネクション状態に依存しない演算で取り出すことができるため、ほとんどステートを持たず(low state)に動作します。

図3: エンド間でコネクションを成立させるQUIC-LB方式

少々わかりにくいので、以下、具体例を使って解説します。サーバIDの埋め込み方式は複数ありますが、もっとも簡単に理解できるPlain Text方式を図4を使って解説します。

図4: Plain Text方式によるサーバIDの埋め込みの様子

ロードバランサとWebサーバ間の共有情報 ★1

あらかじめロードバランサとWebサーバ間で、サーバIDの長さを共有します。図4の例では2バイトです。サーバIDはコネクションIDの2バイト目および3バイト目に埋め込みます。先頭の1バイト目には特別な意味がありますが、図4のシーケンス解説でふれませんので、のちの記述をお待ち下さい。

処理★2 @ QUIC-LB

接続確立のため、クライアントが最初に送信してきた Initialパケットを受信したロードバランサは、サーバのどれか一つを選んで転送します。このときは4タプル情報を元に転送先のWebサーバを選びます。いわゆる一般的なL4ロードバランサの動きです。

処理★3 @ Webサーバ

Webサーバは事前に共有した情報にしたがってSCID (Source Connection ID)を生成し、クライアントへ応答するInitialパケットに埋め込みます。図4の例では、2バイトのサーバIDは 1234 (16進数表記)で、それを2および3バイト目に埋め込んだSCIDは 001234abcdefabcd としています。

処理★4 @ QUIC-LB

クライアントはDCID (Destination Connection ID)が 001234abcdefabcd であるQUICパケットを送信してきます。ロードバランサは共有情報を使い、コネクションIDの2および3バイト目を読み取ってサーバIDを取り出して、QUICパケットを含んだUDPパケットをサーバIDに対応付けられたWebサーバへ転送します。 なお、ロードバランサにはサーバIDとWebサーバ (IPアドレスとポート番号)の対応関係が事前に設定されているとします。

★1~★4 の過程でわかることは、ロードバランサは個別のQUICコネクション状態を保持することなく、クライアントとWebサーバ間のエンド-to-エンドでQUICコネクションを成立させながら、ステートレスな振り分けを実現していることです。

QUIC-LB: コネクションIDへのサーバIDの埋め込み方式

Plain Text方式は最も単純な埋め込み方式ですがセキュリティ上の問題があります。サーバIDの長さはクライアントに教えることはありませんし、ロードバランサとWebサーバが同一組織による運用するなら外部に公開する必要もありません。しかし、Plain Text方式は比較的容易にサーバIDの推測が可能であり、バックエンドにある特定のWebサーバだけを狙い打ちするDoS攻撃が成立します。そこで、QUIC-LBではよりセキュアな方式としてサーバIDの長さと同時に暗号鍵の情報を共有する方式を提案しています。サーバIDと共有鍵にAES暗号を適用した結果をコネクションIDへ埋め込むことで、コネクションIDを見ても容易には解読できない状況とします。

QUIC-LBでは、AES暗号の適用方式の違いで2つの方式を提案しており、

  • Stream Cipher CID Algorithm
  • Block Cipher CID Algorithm

前者は計算量やデータ量が小さくでき、コネクションIDも短く設定できる。後者はより強度が得られるが、計算量や大きくなりコネクションIDも17バイト以上必要となるトレードオフがあり、要件に応じて選択することになります。QUIC-LBでの転送処理はステートレスとは言え、パケット単位の暗号解読処理となるので、負荷の違いは重要なのかもしれません。

QUIC-LB: Retryパケットの代理応答によるDDoS防御 (クライアントのIPアドレス検証)

本節の内容は、TCP SYN CookieのQUIC版とも言える仕組みです。TCPにおいて、送信元IPアドレスを詐称して大量のSYNパケットを送りつけるDDoS攻撃があります。WebサーバがSYNを受け取った直後すぐに状態管理(コントロールブロック)のためのメモリリソースを確保するとTCPスタックのリソース枯渇を招きます。対策として、TCP SYN Cookieと呼ばれる手法があり、SYNを受け取ってもすぐに状態を作らず、ランダムなCookieの値を応答のSYN+ACKのシーケンス番号に埋め込みクライアントに送り返します。実在するクライアントからのみCookieを含むACKが返答されるので、Webサーバはクライアントの存在性(IPアドレスが正しいこと)を確認した後に、始めて状態を作ることができます。

QUICには同様な仕組みが組み込まれており、Retryパケットとトークンを使ってクライアントのIPアドレスを検証する仕組みがあります。RFC9000の8.1.2節での規定です。トークンとTCP SYN cookieでは格納される情報は厳密には異なりますが、クライアントの存在性を確認するため、Webサーバからクライアントへ情報を送りつけ、クライアントから返事があれば正当とみなす観点では同じです。

QUIC-LBでは、ロードバランサがWebサーバに代わってRetryパケットの送信を代行する仕組みが盛り込まれています。ロードバランサは負荷分散だけでなくDDoSの検知と防御の仕組みが搭載されることが多いですが、大量のInitialによるフラッド攻撃も想定しています。攻撃を検知した際は、Webサーバへの転送は抑止し、ロードバランサがトークンを含んだRetryパケットをクライアントに送り返し、実在するクライアントであるかを確認します。

ロードバランサがWebサーバに代わってトークンを送るには、あらかじめトークン情報を共有しておく必要がありますが、QUIC-LBでは双方で暗号化したトークン情報を共有する方法を提案しています。共有方法に関してWebサーバの状態を極力保持しないステートレス方式と、積極的に保持するステートありの二つの方式を提案しています。

コネクションIDの先頭バイトへの意味付け

コネクションIDの先頭の1バイト目は、ロードバランサとWebサーバ間のみで通じる特別な意味付けをしています。クライアントの動きには影響を与えません。

# コネクションIDの先頭の1バイト目を 2bits : 6bits に分けて意味づけFirst Octet of Connection ID {
Config Rotation (CR) (2 bits)
CID Len or Random (6 bits)
}

Config Rotation (CR)

コネクションIDの先頭の2ビットがConfig Rotation (CR)です。2ビットあり、ロードバランサとWebサーバへ共有情報をデプロイする際、三代まで世代管理ができます(00, 01, 10)。11はデプロイ失敗時にデフォルト動作(4タプル転送)に戻すために使います。世代を取り入れることでロードバランサとWebサーバ間の共有情報を段階的に変更できます。

例えば、Webサーバの増設が行われサーバIDの埋め込みルールを変更したとします。過去のQUICコネクションが継続しているところで、いきなり切り替えてしまうと既存のコネクションの切断が発生してしまいます。そこで変更中を表す世代を設けます。変更前から存在していたQUICコネクションには従前の埋め込みルールを維持しつつ、新規のQUICコネクションは変更中であること明示して、ロードバランサとWebサーバの双方に新旧を区別させます。古いコネクションが終了したら、WebサーバはコネクションIDの先頭2ビットを00に戻して埋め込みルール変更を完了します。QUICではコネクションを維持したまま、いつでもコネクションIDを変更できる性質をつかっています。

CID Len or Random (6 bits)

本ビットはWebサーバの動作を補助するヒント情報であり、ロードバランサが転送処理のため参照することはありません。QUICショートヘッダには宛先コネクションIDの長さの情報を含まず、ショートヘッダだけを見てもどこまでがコネクションIDなのか判別できません、すでにエンドホスト間ネゴシエーションで合意しているので、ヘッダ内に長さを埋め込む必要がないためです。

しかし、Webサーバで暗号アクセラレータなどのハードウェア処理の適用の前段で、コネクションIDの長さの検索処理がネックになると言及しています。コネクションIDの長さ(正確には1だけ小さい値)を埋め込んでおけば、ショートヘッダでも即座にコネクションIDを特定することができ、ハードウェア処理へ移行できます。長さを埋め込まない選択もでき、その場合は6ビットを単にランダムな値で満たします。

コネクションIDの計算ライブラリと計算例

F5 Networksが、GitHubリポジトリでコネクションIDを計算するアルゴリズムをライブラリ化して提供しています。提供範囲はコネクションIDの計算機能だけであり、ロードバランサとしてのパケット転送の機能はありません。

コネクションIDを算出するサンプルコードが添付されており、下記の三つの方式ごとに計算結果を見ることができます。コードはBIG-IPの一部を抜き出したものに見え、F5 Networksでは装置への実装をすすめていそうです。

  • PCID: Plain Text CID
  • SCID: Stream Cipher CID (Source Connection ID ではないことに注意)
  • BCID: Block Cipher CID

コードをビルドするとlb_testというバイナリができます。Webサーバが自身のSCID (Source Connection ID)を算出するとき演算結果を見せてくれます。これは図4の★3の処理に相当する部分です。

lb_test の出力結果の読み方

■PCID: Plain Text CIDの例

1:#./lb_test
2: ...
3:PCID LB configuration: cr_bits 0x0 length_self_encoding: n
sid_len 2
4:cid 30e198 sid e198 su
5:cid 15fb7619 sid fb76 su 19
6:cid 3dacfd487d sid acfd su 487d
7:cid 34828366865a sid 8283 su 66865a
8:cid 14d17343900634 sid d173 su 43900634

3行目に設定が表示されます。sid_lenはサーバIDの長さで、この例では2バイトの場合を取り上げています。ここがロードバランサと共有する情報です。cr_bits 0x0, length_self_encoding: n は先頭2ビットはConfig Rotationが00(2進表記), 続く6ビットは長さを埋め込まないのでランダムに満たした意味です。4~8行目のcidの先頭1バイト目の構造に反映されています。先頭2ビットはすべて00であること注意してください。

続く2および3バイト目の値は、Plain Text方式では各行に表示されているサーバID(sid)の値と一致していることが明確にわかります。最後のsu はserver usedの意味で、振り分け先のWebサーバ上でコネクション識別に使う部分になります。4行目だけsuはありませんが、1つだけと解釈します。

■BCID: Block Stream Cipher CIDの例

1:#./lb_test
2: ...
3: BCID LB configuration: cr_bits 0x0 length_self_encoding: y
sid_len 1 key 1cdb833ce074d05a87fcaa9c00261c64
4: cid 10b0305f384a148bc8d46ec4020c3465cb sid 23
su 3585a11a6d747a1faf6e89dd222066
5: cid 107229d120611490fd8995f6ab0c30890a sid 34
su 1109e3f3de68e1a0310945a99fa9e1
6: cid 1061090df6db3d46411a2521ccc2b3e773 sid 75
su ce140f9c1fad1f0678d68d6f73abd6
7: cid 10dbf87f29011ca10a838d9244e4b152a2 sid 09
su 68d5a15c53baf07119e99d7249ce5a
8: cid 10548af170bcba6a72fdd34e339e4a16a3 sid d0
su 256132ee3fa5d400c7f6d32a7b592e

3行目に表示が設定がロードバランサとWebサーバの間で共有する情報です。sid_lenがサーバIDの長さで1バイト、秘密鍵key1cdb883ce074d05a87fcaa9c00261c64 である二つの情報となっています。コネクションIDの2~17バイト目の部分は、サーバIDを秘密鍵で暗号化した結果から生成されています。サーバIDはわずか1バイトですが、cidをみても推測することはできませんし、特定のWebサーバを狙いたい攻撃者が、そこに届くようなコネクションIDを恣意的に作り出すこともできません。

なお、cr_bits 0x0, length_self_encoding: yの意味は先頭2ビットのConfig Rotationが00(2進表記), 続く6ビットには(コネクションID - 1)の長さを埋め込んでいます。10(hex)=00-010000(bin) なので、コネクションIDの長さは16バイト+1=17バイトであることがわかります。

方式編のまとめ

IETFで議論が進むQUICのロードバランス方式について方式を紹介しました。QUICコネクションをクライアントとバックエンドにあるWebサーバ間で成立させつつ、かつ、コネクションに関してほぼステートレスに振り分ける方式でした。まだ標準化は議論中の状況ですが、今後実装も進むことを期待します。次回の実装編では、QUIC-LBの実装や、ロードバランサの基礎技術となっているフロー識別の観点から見たQUICコネクションについて解説します。

--

--