【ITニュース解説】Five bugs that only exist in a reverse proxy (and the Rust that fixed them)
2026年09月29日に「Dev.to」が公開したITニュース「Five bugs that only exist in a reverse proxy (and the Rust that fixed them)」について初心者にもわかりやすく解説しています。
ITニュース概要
Rust製リバースプロキシ「ferryman-edge」の開発で、HTTPバージョン不一致やルーティング誤りなど、5つの特有なバグに直面した。クライアントとアップストリーム間の複雑な挙動から生じる問題を具体的に解説し、その解決策と、プロキシ設計における重要な注意点を紹介する。
ITニュース解説
今回のニュース記事では、Rustというプログラミング言語で開発された「ferryman-edge」というリバースプロキシが経験した、特にプロキシならではの興味深いバグとその解決策について詳しく解説されている。システムエンジニアを目指す皆さんにとって、これはシステムが実際にどのように動いているのか、そして開発の現場でどんな問題に直面し、どう解決していくのかを学ぶ良い機会となるだろう。
まず、リバースプロキシとは何かから説明しよう。ウェブサイトやアプリケーションにアクセスする際、通常はクライアント(例えばウェブブラウザ)が直接サーバーに接続する。しかし、リバースプロキシはクライアントとサーバーの間に位置し、クライアントからのリクエストを一度受け取り、それを適切なサーバー(バックエンドサーバーと呼ばれることが多い)に転送する役割を果たす。これにより、セキュリティの向上、負荷分散、キャッシュによるパフォーマンス向上など、様々なメリットが得られる。ferryman-edgeは、このリバースプロキシの中でも、HTTP/HTTPSリクエストを処理する「レイヤー7(L7)」プロキシに分類される。
ferryman-edgeにはいくつかの重要な機能が組み込まれている。一つは「mTLS(Mutual TLS)」で、これはクライアントとサーバーの両方が互いに証明書を提示し、厳密に認証し合う仕組みだ。これにより、通信のセキュリティが格段に向上する。次に「JWT(JSON Web Token)」による認証機能があり、クライアントが提示するトークンが有効かどうかを検証する。この検証結果はキャッシュされるため、効率的に処理できる。さらに、不正なアクセスを防ぐための「レートリミット」機能がある。これは、例えば特定のユーザーが短時間に送れるリクエストの数を制限するものだ。そして、バックエンドサーバーの負荷を軽減し、システム全体の安定性を保つための「サーキットブレーカー」機能も備わっている。これは、もしバックエンドサーバーが応答しなくなったりエラーを連発したりした場合に、一時的にそのサーバーへのリクエストを遮断し、システム全体が停止するのを防ぐ仕組みだ。ちょうど電気回路のブレーカーが、過電流が流れたときに回路を遮断して家電を守るように機能する。
さて、このような多機能なリバースプロキシを開発する中で、どのような問題が発生したのだろうか。記事では五つのバグが紹介されている。
一つ目のバグは、HTTPプロトコルのバージョンの不一致に関するものだった。ferryman-edgeは、クライアントからのリクエストに対してHTTP/2とHTTP/1.1の両方に対応していると応答する。現代の多くのウェブブラウザやツールはデフォルトでHTTP/2を使用しようとする。しかし、プロキシがリクエストを転送するバックエンドサーバーがHTTP/1.1しか対応していなかった場合、HTTP/2形式のリクエストをHTTP/1.1の接続で送ろうとしてしまい、バックエンド側でエラーが発生し、クライアントには「502 Bad Gateway」というエラーが返されていた。これは、プロキシがクライアントとバックエンドの間で、プロトコルバージョンを適切に調整していなかったために起こった問題だ。解決策は、バックエンドサーバーへ送るリクエストのHTTPバージョンを、常にHTTP/1.1に「ダウングレード」するよう修正することだった。また、同様にバックエンドからの応答のバージョンもクライアントに合わせて調整する必要があった。
二つ目のバグは、サーキットブレーカーとルーティングの組み合わせで発生した。リバースプロキシは、リクエストのURLパスに基づいて、どのバックエンドサーバーに転送するかを決定する「ルーティング」を行う。ferryman-edgeでは、URLパスの最も長い部分が一致するルールを優先してルーティングするように設計されていた。しかし、もし特定のサービスに対応するバックエンドのサーキットブレーカーが作動してリクエストを一時的に遮断した場合、プロキシは代わりに「/」のような、より一般的な「キャッチオール」ルート(どんなパスにもマッチするルート)を選んでしまい、本来とは全く異なるバックエンドにリクエストを転送してしまう可能性があった。これは、あるサービスのトラフィックが誤って別のサービスのバックエンドに送られてしまうという、非常に危険な状況だ。この問題の解決策は、まず最も具体的なルーティングルールを決定し、その次にそのルートが現在利用可能かどうか(サーキットブレーカーが開いていないかなど)を確認する、という処理の順番を入れ替えることだった。これにより、サーキットブレーカーが作動している場合は適切に503エラー(サービス利用不可)を返すようになり、意図しないルーティングを防げるようになった。
三つ目のバグは、サーキットブレーカーの「半開」状態での処理に関するものだ。サーキットブレーカーが一度作動して(「開いた」状態)、しばらく時間が経過すると、プロキシはバックエンドサーバーが回復したかどうかを確認するため、一度だけリクエストを通す「半開」状態に移行する。このとき、たくさんのリクエストが同時に来ても、実際にバックエンドサーバーに送られるリクエストは「たった一つ」だけにする必要がある。記事の最初の実装では、この「たった一つ」を保証するために、状態を示す単純な値を共有していたが、これは複数のリクエストがほぼ同時にその値を読み書きしようとした場合に、誤って二つ以上のリクエストが「半開」状態を認識し、バックエンドへ送られてしまう可能性があった。このような問題を「競合状態」と呼ぶ。解決策は、状態の変更時刻という、より具体的な情報を使って、誰が最初にリクエストを通す権利を得たかを厳密に判断する仕組みを導入することだった。これにより、多数のリクエストが同時に来ても、必ず一つだけがバックエンドに送られることが保証された。また、この機能をテストするために、複数のリクエストを同時に発生させ、一つだけが許可されることを確認するテストが開発されたことも述べられている。
四つ目のバグは、クライアント側のエラーがバックエンドサーバーの不具合として誤認識されてしまうという、プロキシ特有の問題だった。例えば、クライアントが非常に大きなファイルをアップロードしようとしたり、アップロードの途中で通信を切断したりすることがある。これらの問題は、バックエンドサーバーが悪いわけではなく、あくまでクライアント側の都合で発生したエラーだ。しかし、ferryman-edgeの初期の実装では、このようなクライアント起因のエラーも、すべてバックエンドサーバーが不健全になった兆候だと判断し、サーキットブレーカーを作動させてしまっていた。結果として、悪意のある、または単に動作が不安定なクライアントが、特定のサービスのサーキットブレーカーを意図せずに、あるいは故意に開かせてしまい、他の正常なクライアントがそのサービスを利用できなくなるという問題が発生した。この問題の解決策は、エラーが本当にバックエンドサーバー側で発生したものなのか、それともクライアント側の問題なのかを、エラーの原因を詳細に分析して区別することだった。具体的には、エラーの連鎖をたどって、それが例えばデータサイズの制限超過や、クライアントによる接続切断といった「ユーザーエラー」に起因するものかを識別するロジックを導入した。また、アップロードのタイムアウト処理についても改善が加えられ、クライアントからのデータ受信にかかる時間と、バックエンドへのリクエスト処理にかかる時間を別々に計測することで、クライアント側の問題でバックエンドが不健全だと判断されないように修正された。
五つ目のバグは、HTTPヘッダーの処理順序のミスに関するものだった。ferryman-edgeは、JWT認証が成功した後、バックエンドサーバーに対して、どのテナント(利用者)からのリクエストかを伝えるために「x-ferryman-tenant」というカスタムヘッダーを追加する。しかし、HTTPの仕様には、セキュリティや効率のために、プロキシが特定のヘッダーを転送する前に削除すべき、というルールがある(これらを「ホップバイホップヘッダー」と呼ぶ)。初期の実装では、プロキシが新しいヘッダーを追加してから、不要なヘッダーを削除する処理が走っていたため、もしクライアントが「Connection: x-ferryman-tenant」というヘッダーを送ってきた場合、プロキシがせっかく追加した「x-ferryman-tenant」ヘッダーが、その直後の削除処理によって消されてしまうという問題が発生した。解決策は、ヘッダーの削除処理を先に行い、その後にプロキシが追加したいヘッダーを付与するという、処理の順序を入れ替える単純なものだった。この問題はHTTP/1.1に特有のもので、HTTP/2ではConnectionヘッダーが許可されていないため影響を受けない。
これらの主要なバグの他にも、いくつかの細かな改善や発見があった。例えば、非同期処理を扱うためのtokio::select!マクロのガード条件が一度しか評価されないという落とし穴や、JWTライブラリのデフォルト設定では「発行者(iss)」などの必須情報が不足しているトークンも受け入れてしまう問題、Linuxのプロセス名が短縮されるためにpgrepコマンドで正確にプロセスを特定できない問題、Dockerイメージのビルド環境と実行環境のライブラリバージョンの不一致などが挙げられている。
最後に、これらのバグ修正と機能改善を経て、ferryman-edgeの性能測定結果も示されている。例えば、JWTトークンの検証では、キャッシュが効いている場合は約0.68マイクロ秒、キャッシュなしの場合は約150マイクロ秒と、キャッシュの大きな効果が示されている。また、システムのメモリ使用量も約16MBと非常に低いことが確認されており、効率的な動作が期待できる。
このニュース記事は、リバースプロキシというシステムの中間にあるがゆえに発生する複雑な問題や、並行処理における落とし穴、そして堅牢なシステムを構築するために徹底したテストがどれほど重要かを示している。システムエンジニアを目指す皆さんにとって、このような実際の開発現場で起こる問題と、それを解決するための思考プロセスを知ることは、非常に貴重な経験となるだろう。そして、Rustのような安全性を重視するプログラミング言語が、このような複雑なシステム開発において、どのように役立つかの一端も垣間見える。