【実務・中級編】 iptablesを用いたカスタムNATインスタンスのSNAT/MASQUERADE設定 – クラウド&コンテナネットワーク実践ガイド

クラウド全盛期のいま、マネージドなNATゲートウェイ(AWSのNAT GatewayやGCPのCloud NATなど)は非常に便利です。「ボタンひとつ、あるいはTerraformの一行でセキュアなアウトバウンド通信路が手に入る」——素晴らしいことですよね。

しかし、現場で数々の修羅場をくぐり抜けてきたSREなら、一度はこんな壁にぶ当たったことがあるはずです。「マネージドNATだとコストが想定外にかかる」「特定のプロトコルや複雑なルーティング制御、あるいはパケットキャプチャレベルでのデバッグが必要になった」……そんなとき、私たちの古巣であり最強のツールである 「LinuxベースのカスタムNATインスタンス(EC2/GCEなど)」 が再び脚光を浴びます。

今回は、Linuxカーネルの心臓部である Netfilter と iptables を操り、プライベートサブネットの苦学生たち(インスタンス群)をインターネットという荒波へ送り出すための SNAT と MASQUERADE の設定術を、パケットの挙動から実務的なTipsまで徹底的に解説します。

—

1. なぜ「NAT」が必要なのか? ネットワークの基本とRFCの原則

プライベートサブネットにあるインスタンスから外部のWeb API(例えば、StripeやSendGridなど)を叩くとき、パケットの宛先IPアドレスはグローバルIPになります。しかし、送信元IPアドレスは 10.0.x.x や 192.168.x.x といった RFC 1918(プライベートIPアドレスの割り当て) で定義されたローカルなアドレスのままです。

このままパケットをインターネットへ送り出しても、ルーターや宛先のサーバーは「返り値のパケットをどこに飛ばせばいいのか(10.0.1.100 なんてグローバルにユニークではないアドレスへは返せない)」が分からないため、パケットは闇に消えます。

そこで登場するのが NAT(Network Address Translation) です。
プライベートIPを、NATインスタンスが持つパブリックIP(Elastic IPなど)へと書き換え(Source NAT)、インターネット側からのレスポンスを受け取ったら再び元のプライベートIPに書き戻して元のインスタンスへ届ける——この一連の魔法を、Linuxのカーネル空間で実現するのが iptables です。

—

2. 通信フローの全体像:パケットは内核(カーネル)でどう書き換わるか?

プライベートインスタンスから外部へHTTPリクエストが飛ぶとき、パケットの旅路は次のようなシーケンスをたどります。

[Private Instance] (10.0.1.50:54321)
       │
       ▼  (外向きパケット送出)
[NAT Instance: eth0(10.0.1.1: 内側), eth1(203.0.113.50: 外側)]
       │  ※ iptablesのNATテーブル・POSTROUTINGチェインで書き換え発生!
       ▼
[Internet / Target API Server] (送信元が 203.0.113.50 に見えている)

1. プライベートインスタンス が curl https://api.example.com を実行。

  • 送信元: 10.0.1.50:54321 → 宛先: 198.51.100.10:443

2. NATインスタンス の eth0(内側インターフェース)がパケットを受信。
3. Linuxカーネルのルーティングテーブルに従い、パケットが外側へ転送される直前、iptables の POSTROUTING チェインに到達。
4. ここで SNAT(またはMASQUERADE) が発動し、パケットの送信元IPアドレスがNATインスタンスのグローバルIP 203.0.113.50 に書き換わる。同時に、コネクション追跡機構(conntrack)がこのマッピングを記憶する。
5. インターネット側のサーバーは、送信元が 203.0.113.50:xxxx であると認識してレスポンスを返す。
6. レスポンスがNATインスタンスの eth1 に到着すると、カーネルは conntrack テーブルを参照し、元の宛先(10.0.1.50:54321)にパケットを巻き戻してプライベートインスタンスへ返却する。

この一連の処理において、肝となるのが iptables の設定です。

—

3. 実践!SNATとMASQUERADEの設定構文

カスタムNATインスタンスを構築する際、OS側のIPフォワーディングの有効化が大前提となります。まずはこれを済ませておきましょう。

# カーネルパラメータをいじって、IPパケットの転送(Forwarding)を許可する
sudo sysctl -w net.ipv4.ip_forward=1

# 再起動後も永続化させるために設定ファイルに追記
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.d/99-ip-forward.conf

それでは、本題である iptables の設定です。よく混同される SNAT と MASQUERADE の違いを押さえつつ、それぞれの使い分けを見ていきましょう。

パターンA: 固定グローバルIPを使う場合の SNAT

Elastic IP(EIP)など、NATインスタンスのパブリック側インターフェースのIPアドレスが固定されている場合に用います。処理のオーバーヘッドが少なく、パフォーマンス面で有利です。

# eth1 はインターネット側に接続されたパブリックインターフェース
# 203.0.113.50 はこのインスタンスにアタッチされた固定のパブリックIP
sudo iptables -t nat -A POSTROUTING -o eth1 -j SNAT --to-source 203.0.113.50

パターンB: 動的IPや柔軟性を重視する場合の MASQUERADE

クラウドのオートスケーリングやDHCP環境などで、パブリックIPが変動する可能性がある場合に用います。インターフェースからIPが外れたり変わったりしても、カーネルが自動的に現在のIPを拾って書き換えてくれます。

# -o (out-interface) で指定したインターフェースのIPを自動で動的割り当て(マスカレード)する
sudo iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE

> シニアからの実務Tips:
> 通常のWeb API通信であれば MASQUERADE でも大きな性能差はありませんが、極限までパケット処理のオーバーヘッドを削りたい高スループットな環境では、固定IPを指定した SNAT の方が conntrack のルックアップやアドレス解決のわずかな手足を削れるため好まれます。

—

4. 設定を保存する(ディストリビューション別の罠)

iptables のコマンドは、OSを再起動すると すべて消え去ります。これがインフラエンジニアの夜間呼出しランキング上位常連のトラップです。しっかりと永続化設定を行いましょう。

Ubuntu / Debian 系の場合

iptables-persistent パッケージを使用するのが最も確実です。

# インストール(途中で現在のルールを保存するか聞かれるので「Yes」を選択)
sudo apt-get update && sudo apt-get install -y iptables-persistent

# 後から手動でルールを変更・追加した場合は、以下のコマンドで保存を忘れないこと!
sudo netfilter-persistent save

RHEL / Amazon Linux 2 / Rocky Linux 系の場合

iptables-services を利用します。

# インストールとサービスの有効化
sudo yum install -y iptables-services
sudo systemctl enable iptables
sudo systemctl start iptables

# ルールを変更した後の保存コマンド
sudo service iptables save

—

5. 動作確認とデバッグの極意

設定を終えたら、プライベートサブネット上のインスタンスから実際に通信テストを行います。ここでは、実務でよく使う curl やPython、Node.jsなどのコード例を交えて、パケットが意図した通りに世界へ出ていっているかを確認します。

1. プライベートインスタンスからの疎通確認 (curl)

自分のIPアドレスをエコーバックしてくれる外部サービス(https://httpbin.org/ip など)を叩いてみます。

# プライベートインスタンス内で実行
curl -s https://httpbin.org/ip

期待される出力:

{
  "origin": "203.0.113.50"
}

ここで表示されるIPが、プライベートインスタンス自身のIPではなく、NATインスタンスのパブリックIP(例: 203.0.113.50) になっていればSNATは成功です!

2. バックエンドAPIのクライアントコード例(Python)

例えば、Pythonの requests ライブラリを使って外部のWeb APIを叩く場合も、背後でこのNAT機構がシームレスに動いています。

import requests
import sys

def call_external_api():
    api_url = "https://api.ipify.org?format=json"
    
    try:
        # プライベートサブネットからNAT経由で外部APIへリクエスト送信
        response = requests.get(api_url, timeout=5)
        response.raise_for_status()
        
        print(f"API呼び出し成功! 見えているグローバルIP: {response.json().get('ip')}")
        
    except requests.exceptions.RequestException as e:
        print(f"API呼び出しに失敗しました: {e}", file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    call_external_api()

—

6. ハマりどころと現場のトラブルシューティング

最後に、現場で泣きを見るポイントと、その解決のためのデバッグ手順を共有します。

トラブル1: 「パケットが全く飛んでいかない、タイムアウトする」

  • 原因の多く: NATインスタンスのOS側で ip_forward が有効になっていないか、クラウド側のセキュリティグループ/ネットワークACLでパケットの往来がブロックされています。
  • 確認手法:
# NATインスタンス上でパケットが転送されているかカウンターを確認する
  sudo iptables -t nat -v -L POSTROUTING -n

pkts や bytes のカウンターが 0 のまま増えていない場合、パケットがそもそも POSTROUTING チェインに到達していません。ルーティングテーブル(プライベートインスタンス側のデフォルトゲートウェイがNATインスタンスに向いているか)を疑いましょう。

トラブル2: 「特定のAPIだけ極端に応答が遅い、または切断される」

  • 原因の多く: MTU(Maximum Transmission Unit)のミスマッチ です。トンネリングや仮想ネットワーク(VPC内)を経由する際、パケットサイズが大きすぎるとNATインスタンスで断片化(Fragmentation)が発生し、Path MTU Discoveryがブロックされている環境だと通信がスタックします。
  • 解決手法: NATインスタンスの FORWARD チェインで、MSS(Maximum Segment Size)を強制的にクランプ(調整)するルールを追加します。
sudo iptables -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

この一行を入れるだけで、謎の「Web API通信が途中で固まる現象」が嘘のように解決することが多々あります。お守り代わりに覚えておいて損はありません。

—

おわりに

マネージドサービス全盛の時代にあえて iptables によるカスタムNATを触ることは、レトロフューチャーなアプローチに見えるかもしれません。しかし、パケットのルーティング、Netfilterのフック、そして conntrack の挙動を深く理解しているエンジニアは、いざという時の障害切り分けにおいて圧倒的な強さを発揮します。

「クラウドの裏側で、パケットがどう書き換わり、どう世界へ羽ばたいているか」——その解像度を一段上げるための第一歩として、ぜひ今日の知識を検証環境で試してみてください。それでは、良きSREライフを!

コメント

タイトルとURLをコピーしました