こんにちは。今日も元気にプロダクション環境のパケットを追いかけていますか? シニアネットワークエンジニアの「おやじ」です。
私たちが日々、何気なく使っているメガクラウド(AWSやGCP)やKubernetes。その裏側では、目にも留まらぬ速さでパケットが行き交い、ヘッダーが書き換えられています。
インフラを構築していると、必ずぶつかる壁があります。それが「パブリックIP宛てに届いたリクエストが、なぜプライベートサブネット内にある特定のサーバーへ正しくルーティングされるのか?」という疑問です。
その中核を担う技術が、今回解説するDNAT(Destination Network Address Translation:宛先NAT)です。
教科書的な説明だけで終わらせるつもりはありません。今回は、RFCの基本仕様から、パケットがゲートウェイを通過する瞬間のヘッダーの変化、メガクラウドやKubernetesでの実装、そしてLinuxの実機を使ったデバッグ手順まで、現場で本当に役立つディープな知識を共有します。
コーヒーでも飲みながら、じっくりとパケットの旅に付き合ってください。
—
1. RFC仕様から紐解くDNATの原理
まずは、曖昧になりがちな定義をRFC(Request for Comments)の観点から整理しましょう。
NATの基本仕様は、主に [RFC 3022 (Traditional IP Network Address Translator)](https://datatracker.ietf.org/doc/html/rfc3022) や [RFC 2663 (IP Network Address Translator (NAT) Terminology and Considerations)](https://datatracker.ietf.org/doc/html/rfc2663) で規定されています。
一般的に「NAT」と聞くと、プライベートIPをパブリックIPに変換してインターネットへ出ていく「SNAT(Source NAT)」を思い浮かべる人が多いかもしれません。しかし、インターネット側からのインバウンド通信を受け付けるために不可欠なのがDNATです。
DNATの核心:宛先情報の書き換え
DNATの本質は極めてシンプルです。
「パケットの『宛先(Destination)IPアドレス』および『宛先ポート番号』を、ルーターやゲートウェイが動的または静的に書き換えてフォワーディングする」
[クライアント] (192.0.2.10)
│
│ (宛先: 203.0.113.80:80) ── インターネットを通過
▼
[NATゲートウェイ] (外側: 203.0.113.80 / 内側: 10.0.0.1)
│
│ ★ここでDNAT実行!宛先を 10.0.1.50:8080 に書き換え
▼
[Webサーバー] (10.0.1.50:8080)
この変換を行うために、NATデバイスは「ステートテーブル(コネクション追跡テーブル:conntrack)」と呼ばれるメモリ上のマップを維持します。これがないと、サーバーから戻ってきた返事(復路のパケット)を、元の送信元クライアントへ正しく送り返す(逆変換する)ことができなくなります。
—
2. パケットが駆け巡る通信フロー(往路と復路のシーケンス)
DNATの動作を完全に理解するには、パケットが往路(インバウンド)と復路(アウトバウンド)でどのように変化するかを追うのが一番の近道です。
以下のシナリオを想定してみましょう。
- 送信元(クライアント):
192.0.2.10(ポート54321) - NATデバイス(パブリックIP):
203.0.113.80(ポート80) - 宛先(Webサーバー):
10.0.1.50(ポート8080)
往路と復路のパケット遷移
クライアント (192.0.2.10) NATデバイス (203.0.113.80) Webサーバー (10.0.1.50)
│ │ │
│ [往路: パケットA] │ │
├─────────────────────────────────────>│ │
│ 送信元: 192.0.2.10:54321 │ │
│ 宛先: 203.0.113.80:80 │ │
│ │ ★ DNAT実行 & コネクション登録 │
│ │ 「203.0.113.80:80 ⇄ 10.0.1.50:8080」│
│ │ │
│ │ [往路: パケットB (変換後)] │
│ ├─────────────────────────────────────>│
│ │ 送信元: 192.0.2.10:54321 │
│ │ 宛先: 10.0.1.50:8080 │
│ │ │
│ │ │ (アプリケーション処理)
│ │ │
│ │ [復路: パケットC] │
│ │<─────────────────────────────────────┤
│ │ 送信元: 10.0.1.50:8080 │
│ │ 宛先: 192.0.2.10:54321 │
│ │ │
│ │ ★ ステートテーブルを参照して逆変換 │
│ │ (送信元IPを元のパブリックIPに) │
│ │ │
│ [復路: パケットD (逆変換後)] │ │
│<─────────────────────────────────────┤ │
│ 送信元: 203.0.113.80:80 │ │
│ 宛先: 192.0.2.10:54321 │ │
重要なポイント:Symmetric Routing(対称ルーティング)の維持
ここで絶対に忘れてはならないのが、「復路のパケットも必ず同じNATデバイスを通らなければならない」というルールです。
もし、WebサーバーがNATデバイスを経由せずに直接インターネットにパケットを返してしまうと(これを Asymmetric Routing:非対称ルーティング と呼びます)、クライアントには 10.0.1.50:8080 という「見覚えのないIP」からパケットが届くことになります。クライアントのOSは安全のため、この身に覚えのないコネクションのパケットを即座に破棄(RST)してしまいます。
DNATを設計する際は、「戻りのルートが正しくNATデバイスを向いているか」を常に意識する必要があります。
—
3. メガクラウドとKubernetesにおけるDNATの実装パターン
私たちの主戦場であるクラウドやコンテナ環境では、DNATはどのように抽象化されているでしょうか。代表的な2つのパターンを見てみましょう。
パターンA:AWSにおけるDNAT(Internet GatewayとNLB)
AWSを例に挙げると、DNATはいくつかのレイヤーで自動的に実行されています。
1. Internet Gateway (IGW) による1:1 NAT:
パブリックサブネット内のEC2インスタンスに「Elastic IP(EIP)」を付与している場合、EC2自体は自分のプライベートIP(例: 10.0.1.10)しか知りません。
EIP(例: 54.xx.xx.xx)宛てのパケットが IGW を通過する瞬間、IGW が宛先をプライベートIPに書き換えます(これが静的DNATです)。
2. Network Load Balancer (NLB):
NLB は、ターゲットグループが「IPタイプ」か「インスタンスタイプ」かによって挙動が変わりますが、基本的にはクライアントの送信元IPを維持したまま、宛先IPをターゲットのプライベートIPにDNATして転送します。
パターンB:Kubernetes(kube-proxy / iptablesモード)
Kubernetesクラスター内部での通信制御は、DNATの巨大な応用例です。
ClusterIP や NodePort といった Service 宛ての通信は、各ノードの kube-proxy が書き換えた iptables(または IPVS)のルールによって、バックエンドの Pod のIPへとDNATされます。
以下は、kube-proxy が生成する iptables の簡易的なパケット処理イメージです。
[パケット到着] ──> PREROUTING チェーン ──> KUBE-SERVICES チェーン
│
├─ (マッチ) ──> KUBE-SVC-XXXX (Service宛て)
│ │
│ └─> KUBE-SEP-YYYY (特定のPod宛てにDNAT)
Kubernetes内では、このDNATが毎秒数千、数万回と超高速で実行され、Podの動的なスケールイン/アウトに対応しています。
—
4. 実践編:LinuxによるDNATの構築と動作検証
座学はここまでです。実際にLinuxサーバー(Ubuntu/Debian想定)をNATルーターに仕立て上げ、iptables を使ってDNATを構築してみましょう。
このハンズオン環境を理解することで、ブラックボックスだったクラウドのロードバランサーやルーターの内部挙動が手に取るようにわかるようになります。
システム構成
- クライアント機: 任意の端末
- NATルーター(Linux):
- 外側インターフェース(
eth0):203.0.113.80(パブリック模擬) - 内側インターフェース(
eth1):10.0.1.1 - バックエンドWebサーバー:
10.0.1.50(ポート8080でPythonの簡易サーバーを起動)
—
Step 1: バックエンドWebサーバーの起動
まずはバックエンド(10.0.1.50)で、リクエストの内容をそのまま返す簡単なPython製Webサーバーを起動しておきます。
# echo_server.py
# バックエンドサーバー上で実行
from http.server import SimpleHTTPRequestHandler, HTTPServer
class MyHandler(SimpleHTTPRequestHandler):
def do_GET(self):
# クライアントの接続情報をコンソールに出力
print(f"★リクエストを受信しました! 送信元: {self.client_address}")
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"Hello from Backend behind DNAT!\n")
if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", 8080), MyHandler)
print("Webサーバーをポート 8080 で起動中...")
server.serve_forever()
—
Step 2: NATルーターでのDNAT設定 (iptables)
次に、NATルーター(Linux)にログインし、パケット転送を有効にして iptables ルールを適用します。
# 1. LinuxカーネルでのIPフォワーディング(パケット転送)を有効化
sudo sysctl -w net.ipv4.ip_forward=1
# 2. iptablesのルールをクリア(検証用クリーン環境を作るため)
sudo iptables -F
sudo iptables -t nat -F
# 3. DNATルールの追加
# 外側インターフェース(eth0)のポート80番に届いたTCPパケットの宛先を、
# プライベートサブネット内の 10.0.1.50:8080 に書き換える
sudo iptables -t nat -A PREROUTING \
-i eth0 \
-p tcp --dport 80 \
-j DNAT --to-destination 10.0.1.50:8080
# 4. FORWARDチェーンの許可(DNATされたパケットの通過を許可)
sudo iptables -A FORWARD \
-i eth0 -o eth1 \
-p tcp -d 10.0.1.50 --dport 8080 \
-j ACCEPT
# [重要] 5. 復路パケットのためのSNAT(マスカレード)設定
# バックエンドサーバーのデフォルトゲートウェイがこのNATルーターを向いていない場合、
# または送信元IPをNATルーターのものに偽装(SNAT)したい場合に必要
sudo iptables -t nat -A POSTROUTING \
-o eth1 \
-j MASQUERADE
> シニアのアドバイス:
> 上記の MASQUERADE(SNAT)を有効にすると、バックエンドサーバーに届くリクエストの送信元IPが「クライアントのIP」ではなく「NATルーターのIP(10.0.1.1)」に書き換わります。
> もしクライアントの生IPをバックエンドで取得したい場合は、この MASQUERADE を外し、バックエンドサーバーのデフォルトルート(デフォルトゲートウェイ)自体を 10.0.1.1 に設定してください。これぞネットワーク設計の妙味です。
—
Step 3: クライアントからの接続テスト
準備が整いました。クライアント端末から、NATルーターのパブリックIP(ポート 80)に向けて curl を投げてみましょう。
# クライアント端末から実行
curl -i http://203.0.113.80:80/
期待される出力:
HTTP/1.0 200 OK
Server: BaseHTTP/0.6 Python/3.x
Date: Wed, 23 Oct 2024 12:00:00 GMT
Content-Type: text/plain
Hello from Backend behind DNAT!
見事にプライベートサブネット内のPythonサーバーからレスポンスが返ってきました!
—
5. 現場で役立つインバウンド制御とトラブルシューティング
実務の運用フェーズでは、予期せぬトラブルやセキュリティ要件の追加が必ず発生します。ここでは、シニアエンジニアが現場で実践しているトラブルシューティングのノウハウを伝授します。
セキュリティの鉄則:DNATと送信元IP制限の併用
「DNATを設定したら、インターネット中の誰からでもアクセスできるようになってしまった」ではセキュリティ事故の元です。特定のオフィスやパートナー企業のIPからのみアクセスを許可するよう、iptables やクラウドのセキュリティグループでインバウンドを絞り込みましょう。
# 送信元が「198.51.100.0/24」からの通信のみDNATを許可する設定
sudo iptables -t nat -A PREROUTING \
-i eth0 \
-s 198.51.100.0/24 \
-p tcp --dport 80 \
-j DNAT --to-destination 10.0.1.50:8080
コネクショントラッキング(conntrack)のデバッグ
「通信が途中で切れる」「NATが動作していないようだ」というときは、Linuxのコネクション追跡状態をリアルタイムで監視しましょう。conntrack コマンド(conntrack-tools パッケージ)は、ネットワークエンジニアの「レントゲン写真」です。
# コネクション追跡情報をリアルタイムでダンプ
sudo conntrack -E -p tcp --dport 8080
パケットが届くと、以下のようなステート遷移がリアルタイムに表示されます。
[NEW] tcp 6 120 SYN_SENT src=192.0.2.10 dst=203.0.113.80 sport=54321 dport=80 [UNREPLIED] src=10.0.1.50 dst=192.0.2.10 sport=8080 dport=54321
[UPDATE] tcp 6 60 SYN_RECV src=192.0.2.10 dst=203.0.113.80 sport=54321 dport=80 src=10.0.1.50 dst=192.0.2.10 sport=8080 dport=54321
[UPDATE] tcp 6 432000 ESTABLISHED src=192.0.2.10 dst=203.0.113.80 sport=54321 dport=80 src=10.0.1.50 dst=192.0.2.10 sport=8080 dport=54321
ここで、src と dst が往路と復路でどのようにマッピング(アドレス変換)されているかを完璧に把握できます。
tcpdumpによるパケットキャプチャ
最後の武器は、やはり tcpdump です。NATルーターの外側(eth0)と内側(eth1)で同時にキャプチャをかけ、パケットが書き換わっているかを確認します。
# 外側インターフェースでポート80を監視
sudo tcpdump -nnni eth0 port 80
# 内側インターフェースでポート8080への変換後パケットを監視
sudo tcpdump -nnni eth1 port 8080
外側で dst 203.0.113.80 だったものが、内側で dst 10.0.1.50 に変わって流れていれば、DNATは正常に動作しています。もし内側にパケットが流れていなければ、iptables のフィルタリングルールか、カーネルのフォワーディング設定(ip_forward)を疑いましょう。
—
6. まとめ:抽象化の裏にある物理を意識しよう
現代のクラウドサービスは非常に優秀です。コンソールで数クリックするだけで、あるいはマニフェストファイルを kubectl apply するだけで、裏側でDNATが自動的に構成されます。
しかし、ひとたび大規模な通信遅延や、パケットドロップ、ルーティングの不整合といったトラブルが発生すると、ブラックボックス化されたインフラは牙を剥きます。
「今、パケットのアドレス書き換えはどこで行われているのか?」
「戻りのパケットは、本当に同じゲートウェイを通って帰れるのか?」
この視点を常に持ち、conntrack や tcpdump を叩けるエンジニアこそが、現場で最も信頼されるトラブルシューターになれるのです。
今回の知識が、あなたの次回のインフラ設計とデバッグの助けになれば幸いです。それでは、また次の現場でお会いしましょう!
コメント