NGINXにおけるQUICネットワーキングと暗号化の基礎

NGINX | 2023年4月19日

NGINXブログでQUICとHTTP/3について初めて取り上げたのは、4年前のことでした(!)。そして今、皆さまと同様に、F5もQUIC実装がまもなくNGINX Open Sourceのメインラインブランチに統合されることを心待ちにしています。長い開発期間を考えれば、これまでQUICについてあまり考える機会がなかったとしても無理はありません。

しかし現時点では、開発者やサイト管理者は、QUICによってネットワークに関する一部の詳細事項を担う主体が、オペレーティングシステムからNGINX(およびすべてのHTTPアプリ)へ移ることを理解しておく必要があります。ネットワークが専門外であっても、QUICを導入すれば、ネットワークについて考えることも(少なくとも多少は)業務の一部になります。

この記事では、わかりやすさを重視し、一部の詳細を簡略化するとともに本質的でない情報を省きながら、QUICで使用されるネットワークと暗号化の主要な概念を詳しく解説します。その過程で細かなニュアンスが失われる可能性はありますが、QUICをお使いの環境に効果的に導入するために十分な情報、または少なくとも知識を深めるための基礎を提供することを目的としています。

QUICについてまったく初めて学ぶ方は、まず以前の記事のいずれかを読み、概要動画をご覧になることをお勧めします。

 

QUICについてより詳細かつ包括的に理解するには、IETC QUICワーキンググループによる優れた文書「QUICトランスポートプロトコルの管理性」と、本書内でリンクされている補足資料を参照することを推奨します。

QUICのネットワークと暗号化が重要な理由

これまで、クライアントとNGINX間のネットワーク接続に関する込み入った詳細は、ほとんどのユーザーにとって特に重要ではありませんでした。というのも、HTTP/1.xおよびHTTP/2では、クライアントとNGINX間のTransmission Control Protocol(TCP)接続の確立をオペレーティングシステムが担うためです。NGINXは、確立された接続を使用するだけです。

しかし、QUICでは、接続の確立、検証、管理を担う主体が、基盤となるオペレーティングシステムからNGINXへと移ります。NGINXは、確立済みのTCP接続を受け取る代わりに、User Datagram Protocol(UDP)データグラムのストリームを受け取り、それをクライアント接続とストリームに解析する必要があります。また、パケット損失、接続の再開、輻輳制御への対応もNGINXが担うようになります。

さらに、QUICでは、接続の開始、バージョンネゴシエーション、暗号鍵交換が1つの接続確立処理に統合されています。また、TLS暗号化はQUIC+HTTP/3とTCP+HTTP/1+2のどちらでもおおむね同様に処理されますが、レイヤー4ロードバランサー、ファイアウォール、セキュリティアプライアンスなどのダウンストリームデバイスにとって重要となり得る相違点があります。

最終的に、これらの変更によって、NGINXの設定や運用をほとんど変更することなく、ユーザーはよりセキュアで高速かつ信頼性の高いエクスペリエンスを得られます。ただし、問題が発生した際に、自らに原因がないことを立証するまでの平均時間をできるだけ短縮するためにも、NGINX管理者はQUICとNGINXで何が起きているのかを少なくともある程度理解しておく必要があります。

(HTTP/3にはQUICが必要なため、この記事ではHTTPの動作に焦点を当てていますが、QUICは他のプロトコルにも使用できる点に留意してください。その好例が、RFC 9250「専用QUIC接続を介したDNS」で定義されているDNS over QUICです。)

以上を踏まえ、QUICネットワークの具体的な仕組みを見ていきましょう。

TCPとUDPの比較

QUICは、クライアントとサーバー間でHTTPアプリケーションデータを伝送するために使用される基盤ネットワークプロトコルに大きな変化をもたらします。

前述のとおり、HTTP Webアプリケーションデータの伝送には、従来からTCPが使用されてきました。TCPは、IPネットワーク上でデータを確実に配信できるよう設計されています。接続の確立とデータ受信の確認に関する明確で広く理解されたメカニズムに加え、信頼性が低く輻輳したネットワークで一般的に発生するパケット損失や遅延を管理するためのさまざまなアルゴリズムと技術を備えています。

TCPは信頼性の高い転送を実現する一方で、パフォーマンスとレイテンシーの面ではトレードオフがあります。また、TCPにはデータ暗号化機能が組み込まれていないため、別途実装する必要があります。さらに、HTTPトラフィックパターンの変化に応じてTCPを改善または拡張することも困難でした。TCPの処理はLinuxカーネルで行われるため、システム全体のパフォーマンスと安定性に予期しない影響が及ばないよう、変更は慎重に設計し、テストする必要があります。

もう1つの問題は、多くのシナリオで、クライアントとサーバー間のHTTPトラフィックが、ファイアウォールやロードバランサーなど、複数のTCP処理デバイス(総称して「ミドルボックス」)を経由することです。こうしたデバイスでは、TCP標準の変更への対応が遅れる場合があります。

一方、QUICはトランスポートプロトコルとしてUDPを使用します。UDPはTCPと同様にIPネットワーク経由でデータを伝送するよう設計されていますが、意図的にコネクションの確立と信頼性の高い配信を省いています。このようにオーバーヘッドが少ないため、UDPは信頼性よりも効率と速度が重視される多くのアプリケーションに適しています。

しかし、ほとんどのWebアプリケーションでは、信頼性の高いデータ配信が不可欠です。基盤となるUDPトランスポート層は信頼性の高いデータ配信を提供しないため、これらの機能はQUIC(またはアプリケーション自体)で提供する必要があります。この点において、QUICにはTCPよりも優れている点がいくつかあります。

  • QUICの処理はLinuxのユーザー空間で実行されるため、特定の処理で問題が発生しても、システム全体へのリスクを抑えられます。これにより、新機能を迅速に開発しやすくなります。
  • 前述の「ミドルボックス」は通常、UDPトラフィックを最小限しか処理しないため、QUICプロトコルの機能強化を制約しません。

QUICネットワーク構成の概要

QUICの「ストリーム」は、HTTP/3のリクエストまたはレスポンス(あるいはその他のアプリケーションデータ)を格納する論理オブジェクトです。ネットワークエンドポイント間で伝送する際は、図に示すように複数の論理レイヤーでカプセル化されます。

図1. QUICストリームの構造

外側から内側へ順に、論理レイヤーとオブジェクトは次のとおりです。

  • UDPデータグラム – 送信元ポートと宛先ポート(長さおよびチェックサムのデータを含む)を指定するヘッダーと、それに続く1つ以上のQUICパケットで構成されます。データグラムは、ネットワーク経由でクライアントからサーバーに送信される情報の単位です。
  • QUICパケット – 1つのQUICヘッダーと1つ以上のQUICフレームが含まれます。
  • QUICヘッダー – パケットに関するメタデータが含まれます。ヘッダーには次の2種類があります。
    • 接続の確立時に使用されるロングヘッダー。
    • 接続の確立後に使用されるショートヘッダー。接続ID、パケット番号、キーフェーズ(キーローテーションに対応するため、パケットの暗号化に使用されたキーを追跡するもの)などのデータが含まれます。パケット番号は、特定の接続とキーフェーズの組み合わせごとに一意であり、常に増加します。
  • フレーム – タイプ、ストリームID、オフセット、ストリームデータが含まれます。ストリームデータは複数のフレームに分散していますが、接続ID、ストリームID、オフセットを使用して、データの各チャンクを正しい順序に並べて組み立てることができます。
  • ストリーム – 単一のQUIC接続内における単方向または双方向のデータフローです。各QUIC接続は、それぞれ固有のストリームIDを持つ複数の独立したストリームをサポートできます。一部のストリームを含むQUICパケットが失われても、失われたパケットに含まれていないストリームの進行には影響しません(これは、HTTP/2で発生するヘッドオブラインブロッキングを回避するうえで極めて重要です)。ストリームは双方向にでき、どちらのエンドポイントからでも作成できます。

接続の確立

よく知られている「SYN / SYN-ACK / ACK」の3ウェイハンドシェイクにより、TCP接続が確立されます。

図2. TCP接続を確立する3ウェイハンドシェイク

QUIC接続の確立には同様の手順が必要ですが、より効率的です。また、暗号化ハンドシェイクの一環として、接続のセットアップにアドレス検証が組み込まれています。アドレス検証は、トラフィック増幅攻撃を防御します。この攻撃では、悪意のある攻撃者が、標的とする被害者の送信元アドレス情報を詐称したパケットをサーバーに送信します。攻撃者は、自ら生成できるパケットよりも数が多い、またはサイズが大きいパケットをサーバーから被害者に送信させ、膨大なトラフィックを発生させようとします。(詳細については、RFC 9000の「セクション8」(QUIC:UDPベースの多重化されたセキュアなトランスポート)を参照してください。)

接続の確立時に、クライアントとサーバーはそれぞれ独立した接続IDを提供します。これらはQUICヘッダーにエンコードされるため、クライアントの送信元IPアドレスに依存せず、接続を容易に識別できます。

ただし、QUIC接続の初期確立にはTLS暗号鍵の交換処理も含まれるため、サーバーにかかる計算負荷は、TCP接続の確立時に生成する単純な「SYN-ACK」応答よりも大きくなります。また、鍵交換処理が行われる前にクライアントのIPアドレスが検証されないため、分散型サービス拒否(DDoS)攻撃の潜在的な経路にもなります。

ただし、複雑な暗号処理を開始する前にクライアントのIPアドレスを検証するようNGINXを設定できます。そのためには、quic_retryディレクティブをonに設定します。この場合、NGINXはトークンを含む再試行パケットをクライアントに送信します。クライアントは、接続確立パケットにこのトークンを含める必要があります。

図3. 再試行パケットを使用しない場合と使用する場合のQUIC接続確立

このメカニズムはTCPの3ウェイハンドシェイクにやや似ており、特に重要な点として、クライアントが提示する送信元IPアドレスを実際に所有していることを確認します。このチェックがなければ、NGINXなどのQUICサーバーは、送信元IPアドレスを偽装した単純なDoS攻撃に対して脆弱になる可能性があります。(このような攻撃を緩和するもう1つのQUICメカニズムとして、すべての初期接続パケットを最低1,200バイトまでパディングする要件があります。これにより、パケット送信のコストが高くなります。)

さらに、再試行パケットは、クライアントに送信する接続IDに接続の詳細をエンコードすることで、TCP SYNフラッド攻撃に似た攻撃(開始されたものの完了していない大量のハンドシェイクがメモリに保持され、サーバーリソースが枯渇する攻撃)を緩和します。また、クライアントが後で提示する接続IDとトークンから接続情報を復元できるため、サーバー側で情報を保持する必要がないという利点もあります。この手法は、TCP SYN Cookieに相当します。さらに、NGINXなどのQUICサーバーは、接続の再開を高速化するために、クライアントからの今後の接続で使用できる有効期限付きトークンを提供できます。

接続IDを使用すると、接続を基盤となるトランスポート層から独立させることができるため、ネットワークの変更によって接続が切断されるのを回避できます。詳しくは「クライアントIPアドレスの変更を適切に管理する」を参照してください。

損失検出

接続が確立され(詳細は後述)、暗号化が有効になると、クライアントとNGINXの間でHTTPリクエストとレスポンスを双方向に送受信できます。UDPデータグラムも送受信されます。ただし、さまざまな要因により、一部のデータグラムが失われたり遅延したりする可能性があります。

TCPには、パケットの配信を確認し、パケットの損失や遅延を検出して、損失したパケットの再送を管理する複雑な仕組みがあります。これにより、正しい順序で欠落のないデータがアプリケーション層に配信されます。UDPにはこの機能がないため、輻輳制御と損失検出はQUIC層に実装されています。

  • クライアントとサーバーはどちらも、受信したQUICパケットごとに明示的な確認応答を送信します(ただし、優先度の低いフレームのみを含むパケットには、すぐには確認応答を返しません)。
  • 信頼性の高い配信が必要なフレームを含むパケットが、所定のタイムアウト期間を過ぎても確認応答されない場合、そのパケットは失われたと見なされます。タイムアウト期間はパケットの内容によって異なります。たとえば、暗号化の確立と接続のセットアップに必要なパケットは、QUICハンドシェイクのパフォーマンスに不可欠であるため、タイムアウト期間が短く設定されます。
  • パケットが失われたと判断されると、欠落したフレームは、新しいシーケンス番号が付与された新しいパケットで再送信されます。
  • パケットの受信側は、パケットに含まれるストリームIDとオフセットを使用して、送信されたデータを正しい順序で組み立てます。パケット番号が示すのは送信順序のみであり、パケットを組み立てる順序ではありません。
  • 受信側でのデータの組み立ては送信順序に依存しないため、パケットが失われたり遅延したりしても、影響を受けるのはそのパケットに含まれる個々のストリームだけであり、接続内のすべてのストリームではありません。これにより、ヘッドオブラインブロッキングの問題が解消されます。この問題は、ストリームがトランスポート層に含まれないため、HTTP/1.xおよびHTTP/2で発生します。

損失検出の詳細な説明は、この入門書の範囲外です。タイムアウトを決定するメカニズムと、転送中に許容される未確認応答データ量の詳細については、RFC 9002「QUIC Loss Detection and Congestion Control」を参照してください。

クライアントIPアドレスの変更を円滑に管理

クライアントのIPアドレス(アプリケーションセッションのコンテキストでは「送信元IPアドレス」と呼ばれます)は、セッション中に変更されることがあります。たとえば、VPNやゲートウェイのパブリックアドレスが変更された場合や、スマートフォンユーザーがWi-Fiの利用可能な場所から離れ、セルラーネットワークへの切り替えが必要になった場合です。また、従来、ネットワーク管理者はUDPトラフィックのタイムアウトをTCP接続より短く設定してきたため、ネットワークアドレス変換(NAT)の再バインディングが発生する可能性が高くなります。

QUICには、こうした変更によって生じる可能性のある通信の中断を軽減するための2つの仕組みがあります。クライアントは、アドレスが変更されることを事前にサーバーへ通知できます。また、サーバーは、クライアントのアドレスが予期せず変更された場合でも適切に処理できます。移行中も接続IDは変わらないため、確認応答を受信していないフレームを新しいIPアドレスに再送信できます。

QUICセッション中に送信元IPアドレスが変更されると、送信元IPアドレスとポートを使用して、特定のUDPデータグラムを受信するアップストリームサーバーを決定するダウンストリームのロードバランサー(またはその他のレイヤー4ネットワークコンポーネント)で問題が生じる可能性があります。トラフィックを適切に管理するには、レイヤー4ネットワークデバイスのプロバイダーが、QUIC接続IDを処理できるようにデバイスを更新する必要があります。ロードバランシングとQUICの今後について詳しくは、IETFドラフト「QUIC‑LB:ルーティング可能なQUIC接続IDの生成」をご覧ください。

暗号化

「接続の確立」では、最初のQUICハンドシェイクが単に接続を確立するだけではないことに触れました。TCPのTLSハンドシェイクとは異なり、UDPでは、鍵とTLS 1.3の暗号化パラメーターの交換が初回接続の一環として行われます。この機能により、複数回のやり取りが不要になり、クライアントが以前の接続を再開する際にゼロラウンドトリップタイム(0-RTT)が可能になります。

図4. TCP+TLS/1.3とQUICの暗号化ハンドシェイクの比較

QUICは、暗号化ハンドシェイクを接続確立プロセスに組み込むだけでなく、TCP+TLSよりも広範なメタデータを暗号化します。鍵交換が行われる前でも、最初の接続パケットは暗号化されています。盗聴者が鍵を導出することは依然として可能ですが、暗号化されていないパケットの場合よりも手間がかかります。これにより、攻撃者と国家レベルの検閲者の双方にとって関心の対象となるServer Name Indicator(SNI)などのデータが、より適切に保護されます。図5は、QUICがTCP+TLSよりも多くの潜在的な機密メタデータ(赤色)を暗号化する仕組みを示しています。

図5. QUICはTCP+TLSよりも多くの機密性の高いメタデータを暗号化

QUICペイロード内のすべてのデータは、TLS 1.3を使用して暗号化されます。これには2つの利点があります。古く脆弱な暗号スイートとハッシュアルゴリズムは使用できず、前方秘匿性(FS)を備えた鍵交換メカニズムが必須となります。前方秘匿性により、攻撃者が秘密鍵とトラフィックのコピーを入手した場合でも、データの復号を防止できます。

低RTT接続とゼロRTT接続でレイテンシを低減

アプリケーションデータの送信を開始するまでにクライアントとサーバー間で必要となるラウンドトリップの回数を減らすことで、特にレイテンシーの高いネットワークにおいて、アプリケーションのパフォーマンスが向上します。

TLS 1.3では、暗号化接続の確立に必要なラウンドトリップが1回、接続の再開には0回となりました。ただし、TCPではTLS Client Helloの前にハンドシェイクを行う必要があります。

QUICは暗号処理と接続確立を組み合わせることで、真の0‑RTT接続再確立を実現し、クライアントは最初のQUICパケットでリクエストを送信できます。これにより、最初のリクエストの前に接続を確立するための初回ラウンドトリップが不要になり、レイテンシーが低減します。

図6. TCP+TLSとQUICで接続を再確立するために必要なメッセージの比較

この場合、クライアントは、以前の接続で使用したパラメーターで暗号化したHTTPリクエストを送信します。また、アドレス検証のために、以前の接続時にサーバーから提供されたトークンを含めます。

残念ながら、0‑RTT接続再開では前方秘匿性が確保されないため、クライアントからの最初のリクエストは、その通信における他のトラフィックほど強固には暗号化されません。最初のリクエストより後のリクエストとレスポンスは、前方秘匿性によって保護されます。さらに問題となり得るのは、最初のリクエストがリプレイ攻撃に対しても脆弱であることです。攻撃者は最初のリクエストを取得し、サーバーに対して何度も再送できます。

多くのアプリケーションやWebサイトでは、0‑RTT接続再開によるパフォーマンス向上が、こうした潜在的な脆弱性を上回るメリットをもたらします。ただし、採用するかどうかはご自身で判断する必要があります。

この機能は、NGINXではデフォルトで無効になっています。有効にするには、ssl_early_dataディレクティブをonに設定します。

Moving from HTTP/1.1 to HTTP/3 with the Alt-Svcヘッダーを使用したHTTP/1.1からHTTP/3への移行

ほぼすべてのクライアント(特にブラウザー)は、最初にTCP/TLS経由で接続します。サーバーがQUIC+HTTP/3をサポートしている場合、h3パラメーターをAlt-Svcヘッダーに含むHTTP/1.1レスポンスを返すことで、そのことをクライアントに通知します。その後、クライアントはQUIC+HTTP/3を使用するか、以前のバージョンのHTTPを引き続き使用するかを選択します。(なお、Alt-SvcヘッダーはRFC 7838で定義されており、QUICより前から存在し、ほかの目的にも使用できます。)

図7. Alt-Svcヘッダーを使用して接続をHTTP/1.1からHTTP/3に切り替える仕組み

The Alt-Svcヘッダーは、同じサービスが別のホスト、プロトコル、ポート、またはそれらの組み合わせで利用可能であることをクライアントに通知します。また、このサービスが引き続き利用可能であると見なせる期間もクライアントに通知できます。

例:

Alt-Svc: h3=":443" このサーバーではポート 443でHTTP/3を利用できますAlt-Svc: h3="new.example.com:8443" サーバーnew.example.comではポート 8443でHTTP/3を利用できますAlt-Svc: h3=":8443"; ma=600 このサーバーではポート 8443でHTTP/3を利用でき、少なくとも10分間は利用可能です

必須ではありませんが、ほとんどの場合、サーバーはTCP+TLSと同じポートでQUIC接続に応答するように構成されています。

NGINXでAlt-Svcヘッダーを追加するように設定するには、add_headerディレクティブを使用します。この例では、$server_port変数は、クライアントがTCP+TLSリクエストを送信したポートでNGINXがQUIC接続を受け付けることを意味し、86,400は24時間を表します。

add_header Alt-Svc 'h3=":$server_port"; ma=86400';

まとめ

このブログでは、QUICの基礎をわかりやすく解説し、QUICで使用される主要なネットワーク処理と暗号化処理を理解するために必要な概要を紹介します。

QUIC + HTTP/3向けのNGINX設定について詳しくは、ブログ記事「プレビュー版NGINX QUIC+HTTP/3実装のバイナリパッケージを公開」をお読みいただくか、ウェビナー「NGINXとQUIC+HTTP/3を実際に使ってみる」をご覧ください。QUIC+HTTP/3向けのすべてのNGINXディレクティブの詳細と、ビルド済みバイナリのインストールまたはソースからのビルドに関する詳しい手順については、「NGINX QUIC Webページ」をご覧ください。


共有
タグ: Tech, F5 NGINX

著者について

Robert Haynes
Robert Haynesテクニカルマーケティングマネージャー

Robert Haynesのその他のブログ記事

関連するブログ記事

チュートリアル:NGINXを使用してアプリケーション向けにOpenTelemetryを構成する
NGINX | 2024/01/18

チュートリアル:NGINXを使用してアプリケーション向けにOpenTelemetryを構成する

OpenTelemetryを使用してNGINXサーバーの状態と健全性をより明確に把握することで、問題のトラブルシューティングを行い、Webアプリケーションのパフォーマンスを向上できます。

AkitaのNGINX Plus認定モジュールでAPIエンドポイントの問題を特定・修正
NGINX | 2023/05/31

AkitaのNGINX Plus認定モジュールでAPIエンドポイントの問題を特定・修正

Akita SoftwareのAPIオブザーバビリティ向け新しいNGINX Plus Certified Moduleを活用して、APIエンドポイントの問題をより迅速に特定し、修正できます。

JavaプロジェクトをスキャフォールディングしてKubernetesネイティブアプリを構築する方法
NGINX | 2023/02/09

JavaプロジェクトをスキャフォールディングしてKubernetesネイティブアプリを構築する方法

Red HatのシニアプリンシパルデベロッパーアドボケイトであるDaniel Oh氏が、Javaプロジェクトをゼロから構築するためのひな形を作成し、Kubernetesネイティブアプリケーションとしてビルドする方法を解説します。

F5 NGINXaaS for Azureのご紹介:Microsoft Azureでネイティブに利用できるSaaS型ロードバランシング
NGINX | 2022/05/24

F5 NGINXaaS for Azureのご紹介:Microsoft Azureでネイティブに利用できるSaaS型ロードバランシング

NGINXaaS for Azureは、NGINX PlusをAzureネイティブのSaaSとして利用できるサービスです。NGINXの設定を容易にリフト&シフトできるため、クラウド導入とモダンアプリの展開を加速できます。

SELinuxでNGINXおよびNGINX Plusを使用する
NGINX | 2018/08/17

SELinuxでNGINXおよびNGINX Plusを使用する

Red Hat Enterprise Linux(RHEL)および関連ディストリビューションでSecurity-Enhanced Linux(SELinux)を有効にすると、デフォルト設定によりNGINXとNGINX Plusの一部の操作が制限されます。この記事では、すべての機能を利用できるようにSELinuxの設定を変更する方法について説明します。

NGINX PlusによるAWS Auto Scalingグループの負荷分散
NGINX | 2017/03/06

NGINX PlusによるAWS Auto Scalingグループの負荷分散

新しいnginx-asg-sync統合ソフトウェアにより、NGINX PlusはAWS Auto Scalingグループの負荷を直接分散できるため、クラウドでの負荷分散がさらに柔軟になります