こんにちは!SRE兼クラウドアーキテクトの私です。
日頃からAWSやGCPといったメガクラウドの海を泳ぎ、Kubernetesの複雑なネットワーク(CiliumやCalico、Envoyなど)のパケットを毎日追いかけていると、「おっ、今回は綺麗なパケットの形をしてるねえ」なんて、変なところでエンジニアとしてのロマンを感じてしまったりします。
さて、クラウドインフラを触っていると必ずと言っていいほどお世話になるのが「ロードバランサー」ですよね。中でもAWSの NLB(Network Load Balancer) は、毎秒数百万件もの超高速なTCP/UDPトラフィックをさばく、まさにインフラ界の超特急です。
でも、このNLB、一つだけ「悩ましい性質」を持っているんです。
それは、「TCPレイヤー(レイヤー4)で動くから、HTTPの X-Forwarded-For ヘッダーみたいにクライアントの本当のIPアドレスをこっそり教えることができない」 という点です。
「えっ、じゃあバックエンドのサーバーからは、いつもNLBのIPアドレスしか見えないの?」
「アクセスログが全部NLBのIPになっちゃうじゃん!」
そう思ったそこのあなた。素晴らしい着眼点です!
その問題を美しく、かつエレガントに解決してくれるのが、今回ご紹介する 「Proxy Protocol v2(プロキシプロトコルv2)」 なんです。
今回は、このプロキシプロトコルの裏側にある「バイナリヘッダーの秘密」を、身近な例えを交えながら、一歩ずつ優しく紐解いていきましょう!
—
1. なぜNLBを通すと「元のIPアドレス」が分からなくなるの?
まずは、私たちが普段使っているロードバランサーの世界を、身近な「郵便配達」に例えて考えてみましょう。
例えば、あなたがネット通販で商品を買ったとします。
- あなた(クライアント):「海の向こうのAさん」
- NLB(ロードバランサー):「近所の郵便局の配達スタッフ」
- バックエンドのサーバー(EC2やコンテナ):「あなたのおうち」
HTTP(Webブラウザなど)の世界では、お馴染みの X-Forwarded-For という「付箋(メモ書き)」を荷物の表面にペタッと貼ることができます。「この荷物はもともとAさんから預かりましたよ」と書いておけるわけです。だから、おうちの人は誰から届いたのかすぐに分かります。
しかし、NLBが扱う TCP(レイヤー4)の世界 は、いわば「中身が見えない頑丈なダンボール箱」をそのまま高速でリレーしていくようなものです。
NLBは、届いたダンボール箱を一度自分で受け取り、中身のTCPパケットの宛先書き換え(NAT)を行って、バックエンドのサーバーへポイッと投げ直します。この時、箱の差出人欄には「NLBの住所」が上書きされてしまうため、バックエンドのサーバーから見ると「あれ? いつも配達員さん(NLB)からしか荷物が届かないぞ?」となってしまうのです。
非HTTPのプロトコル(データベースの通信や、独自のTCPベースのゲームサーバーなど)では、この「誰から来たか分からない問題」が深刻なボトルネックになります。セキュリティの観点から「アクセス元のIPアドレスでアクセス制限(IPホワイトリスト)をかけたい!」という要件もよくありますよね。
そこで登場するのが、Proxy Protocol です。
—
2. Proxy Protocol v2 ってなに?(手紙の「二重封筒」の仕組み)
Proxy Protocolは、HaProxy(エイチエープロキシ)という有名なソフトウェアのチームが考案した、非常にシンプルかつ天才的な仕組みです。
先ほどの郵便配達の例えで言うと、「荷物を包むダンボール箱の中に、本当の差出人情報が書かれた小さな手紙をそっと忍ばせておく」 というアプローチです。
1. クライアントがNLBに接続する。
2. NLBは、バックエンドのサーバーに新しいTCPコネクションを張る。
3. そのコネクションの最初の一撃(最初のパケット)として、独自の「お助けデータ(バイナリヘッダー)」をこっそり先頭に割り込ませて送信する。
4. バックエンドのサーバーは、その最初の数バイトを読み解いて、「なーんだ、本当のクライアントはあそこのIPアドレスだったんだな」と把握する。
このプロトコルには、人間が読める文字で書かれた v1 と、コンピュータ同士が高速に処理できるバイナリ(0と1の塊)で書かれた効率的な v2 があります。現代のクラウド環境では、より安全で高速な v2 を使うのがデファクトスタンダードです。
—
3. ちょっと覗いてみよう!Proxy Protocol v2 のバイナリヘッダー構造
「バイナリ」と言われると、一気に難しく感じてしまうかもしれませんが、怖がらなくて大丈夫です。一歩ずつ分解していきましょう。
Proxy Protocol v2のヘッダーは、合計で 16バイトの「シグネチャー(合言葉)」 と、それに続く 可変長の「データ部分」 で構成されています。
全体像をイメージしやすいように、お弁当箱に例えてみましょう。
+-----------------------------------+-----------------------------------+
| 16バイトの固定シグネチャー | ヘッダーの長さや送信元・宛先情報など |
| (「私はProxy Protocol v2です!」) | (実際のIPやポートのバイナリデータ) |
+-----------------------------------+-----------------------------------+
① 16バイトの合言葉(シグネチャー)
通信が始まった瞬間、パケットの先頭には必ず以下の16バイトのバイト列(Hex値)が流れます。これが来たら、バックエンドのサーバーは「おっ、Proxy Protocol v2の荷物だな」と気づきます。
- 16進数表現:
0x0D 0x0A 0x0D 0x0A 0x00 0x0D 0x0A 0x51 0x55 0x49 0x54 0x0A(後半に “QUIT” というマジックワードが入っているのがちょっとユニークですね)
② コントロールバイト(1バイト)
シグネチャーの直後にある1バイトで、「今回の通信はIPv4なの? IPv6なの? それともTCPなの?」といったステータスを伝えます。
③ アドレスファミリーとプロトコル(1バイト)
通信の種類を示します。例えば 0x11 なら「IPv4 over TCP」、0x21 なら「IPv6 over TCP」といった具合です。
④ データ長(2バイト)
この後に続く、IPアドレスやポート番号のデータが「あと何バイトあるか」を示します。
⑤ 実際のIPアドレスとポート番号
IPv4の場合、以下の情報がギュッと詰め込まれています。
- 送信元IPアドレス(4バイト)
- 宛先IPアドレス(4バイト)
- 送信元ポート番号(2バイト)
- 宛先ポート番号(2バイト)
人間が読むときは、パケット解析ツール(Wiresharkなど)が自動的に 192.0.2.1 のようなおなじみの形に変換して見せてくれますが、パケットの裏側ではこのように綺麗にパッキングされて流れているんですね。
—
4. AWS NLBで Proxy Protocol v2 を有効にする方法
理屈が分かったところで、実務での設定方法を見てみましょう。
AWS環境であれば、TerraformやAWS CLIを使って一発で有効化できます。
今回は、実務でよく使われる Terraform のコード例をご紹介しますね。ターゲットグループ(Target Group)を作成する際に、ひと手間加えるだけです。
# AWS NLBのターゲットグループを作成するTerraformの例
resource "aws_lb_target_group" "app_tcp_tg" {
name = "app-tcp-target-group"
port = 443
protocol = "TCP"
vpc_id = "vpc-xxxxxxxxxxxxxxxxx"
target_type = "ip" # ECS on FargateやKubernetesなどでよく使われるIPターゲット
# ★ここでProxy Protocol v2を有効化します!
proxy_protocol_v2 = true
# ヘルスチェックの設定
health_check {
enabled = true
protocol = "TCP"
port = "443"
interval = 30
healthy_threshold = 3
unhealthy_threshold = 3
}
tags = {
Environment = "production"
System = "core-api"
}
}
たったこれだけの設定で、NLBはバックエンドへ向かうすべてのTCPコネクションの先頭に、あのバイナリヘッダーを優しく添えてくれるようになります。
—
5. バックエンド側(アプリケーションやNginxなど)での受け止め方
NLB側でProxy Protocolを有効にしたら、当然ながら受け取る側のサーバー(バックエンド)も、そのヘッダーを解釈できるように設定してあげる必要があります。
もしバックエンドに Nginx を置いている場合は、listen ディレктиブに proxy_protocol を追記するだけです。
# Nginxの設定例( /etc/nginx/nginx.conf など)
server {
listen 443 ssl proxy_protocol; # ← ここに「proxy_protocol」と書き添える!
listen [::]:443 ssl proxy_protocol;
server_name api.example.com;
# NLBからの接続であることを信頼し、Proxy ProtocolからクライアントIPを復元する
set_real_ip_from 10.0.0.0/16; # VPCのCIDRブロックやNLBのプライベートIP範囲を指定
real_ip_header proxy_protocol;
location / {
proxy_pass http://my_backend_service;
# バックエンドのアプリケーションへ正しいクライアントIPを伝えるための設定
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
この設定をしておけば、Nginxは自動的に先頭のバイナリヘッダーを「パクッ」と美味しく食べ(解釈し)、本来のクライアントIPアドレスを $remote_addr 変数にセットしてくれます。あとはいつも通り、バックエンドのアプリケーション(Node.js, Python, Goなど)で普通にIPアドレスを取得すればOKです!
—
6. 現場のSREが教える!ハマりどころとトラブルシューティング
最後に、現場で実際にやりがちな「落とし穴」をいくつかシェアしておきますね。これを避けるだけで、夜間呼び出しの確率がグッと下がります。
① バックエンドが対応していないのにNLB側だけ有効にしてしまう
一番多いのがこれです。NLB側で proxy_protocol_v2 = true にしたのに、バックエンドのサーバー(Nginxや独自アプリ)がそのバイナリヘッダーを想定していない場合、アプリ側は「なんだこの意味不明な最初の16バイトは!?」とパニックになり、コネクションが即座に切断されてしまいます(HTTP 502やコネクションリセットの嵐になります)。
必ず「NLB側を有効にする前に、バックエンド側の受け入れ準備を済ませる」、あるいは同時にデプロイするようにしましょう。
② ヘルスチェックの罠
TCPのヘルスチェックを行っている場合、NLBからのヘルスチェックパケットにもProxy Protocolのヘッダーが付与されることがあります。バックエンドのアプリケーションやロードバランサーの設定によっては、ヘルスチェック用ポートと通常のトラフィック用ポートを分けるか、ヘルスチェック側でもProxy Protocolを正しく解釈できる仕組みが必要です。
—
まとめ
いかがでしたでしょうか?
「Proxy Protocol v2」という名前を聞いたときは、なんだか宇宙科学のように難しそうに感じたかもしれませんが、中身を紐解いてみると 「TCPという素っ気ない通信の中に、クライアントの身分証(IPとポート)をこっそり同封する、先人の知恵が詰まった素敵な工夫」 だということが分かっていただけたかと思います。
クラウドやコンテナのネットワークは、こうした「目に見えないパケットのキャッチボール」の積み重ねで成り立っています。仕組みを一つひとつ優しく解きほぐしていけば、どんな複雑なアーキテクチャも必ず自分の手でコントロールできるようになりますよ。
それでは、また次回のインフラ探訪でお会いしましょう!快適なクラウドライフを!
コメント