NATによるプライベートIPv4の通信
経路選択とアドレス変換をそれぞれ確認し、戻りのパケットが元のクライアントへ対応する仕組みを説明します。
説明
プライベートIPv4しか持たない資源は、internet gatewayへ経路を向けるだけではインターネット向けIPv4通信を成立させられません。public NAT gatewayを使う構成では、private subnetの経路、NATの配置先の経路、アドレスとポートの変換を順に確認します。
10.0.1.10:49152から198.51.100.20:443へ接続する固定例を使います。経路を修正した後、変換前、NATのprivate IPへ変換した段階、外側のIPへ変換した段階のパケットをtcpdumpで読みます。送信と応答を同じ対応表で追います。
経路選択とアドレス変換をそれぞれ確認し、戻りのパケットが元のクライアントへ対応する仕組みを説明します。
1二つのサブネットと経路表
private subnetにはpublic IPを持たないクライアント、public subnetにはNATがあります。構成図と初期の経路を読みます。
cat topology.txtサブネット・NAT・外側IPの対応を読む
cat routes.json初期の宛先経路を確認する初期のprivate経路は0.0.0.0/0をigwへ向けており、図のNATを通りません。経路表は宛先CIDRと転送先の組です。
2より具体的な宛先経路を選ぶ
VPC内の10.0.2.20と外部の198.51.100.20について、privateの経路表から一致する最長のプレフィックスを選びます。
python3 route.py lookup --dst 10.0.2.20VPC内の宛先を検索する
python3 route.py lookup --dst 198.51.100.20外部の宛先を検索する内部宛ては10.0.0.0/16のlocal、外部宛ては0.0.0.0/0のigwです。デフォルト経路があっても、より具体的な内部経路が優先されます。
3経路だけでは足りない条件
外部宛ての経路とクライアントのアドレス条件を照合します。
python3 route.py trace外部宛ての経路とアドレス条件を確認するigwを選びますが、10.0.1.10にpublic IPv4の対応がなくblockedです。private IPv4のまま外へ出せると考えず、変換を担当する経路を確認します。
4privateの出口をNATへ向ける
修正案ではprivateのデフォルト経路をnat-1へ変更します。public側のデフォルト経路はigwのままです。
cp routes-nat.json routes.jsonNAT経由の経路案を反映する
python3 route.py traceprivateとpublic両側の転送先を確認する送信元は10.0.1.10:49152から10.0.0.5:62000、さらに203.0.113.5:62000へ変わります。public NATではNATのprivate IPへの変換と、internet gatewayによるElastic IPへの対応を分けて追えます。
5変換前のSYNと応答
tcpdumpで内側の二つのIPv4パケットを読みます。-nnは名前変換を省き、-SはTCPの番号を絶対値で表示します。
tcpdump -nn -tt -S -vv数値のアドレス・時刻・TCP番号と詳細を表示する -r inside.pcap内側の固定パケットを読む 2>/dev/nullSYNは49152→443、seq=1000です。応答のSYN+ACKは443→49152、ack=1001です。SYNはデータ長0でもシーケンス空間を1消費します。
6NATのprivate IPへ変換した段階
NAT側のprivate IPに対応するパケットを読みます。
tcpdump -nn -tt -S -vv -r nat-private.pcapNATのprivate IPに対応する往復を読む 2>/dev/null送信元は10.0.0.5:62000、戻りの宛先も10.0.0.5:62000です。TCPのseqとackは対応を保っています。IPやポートが変わるため、IPv4ヘッダーとTCPのチェックサムもその値に合わせて計算されます。
7インターネット側のアドレスを確認
外側のパケットを読み、変換の対応表を並べます。
tcpdump -nn -tt -S -vv -r outside.pcap外側IPv4に対応する往復を読む 2>/dev/null
cat mapping.json内側・NAT・外側の対応表を読む外側では203.0.113.5:62000として見えます。宛先198.51.100.20:443は同じです。内側と外側のIP・ポートを対応させることで、一つの通信の変換前後を照合できます。
8戻りの変換と対応のない宛先
固定の送信先から62000へ戻る応答と、対応のない62001へ届く場合を確認します。
python3 route.py reply --port 62000既存の変換に対応する応答を確認する
python3 route.py reply --port 62001変換表にない宛先ポートを確認する62000は10.0.1.10:49152へ戻ります。62001は対応がなく破棄されます。外向きの通信に対する戻りと、外部から任意に始める通信は同じ扱いではありません。
まとめ
privateとpublicの両方の経路を確認し、NATからinternet gatewayまでつなげます。
IP・ポートの変換前後をパケットと対応表で照合します。
応答は既存の変換に従って内側へ戻り、対応のない着信は通りません。
操作履歴と解説を振り返れます。
シェル / Python 3
cat topology.txt
cat routes.jsonVPC 10.0.0.0/16
private subnet 10.0.1.0/24: client 10.0.1.10 (public IPなし)
→ public NAT gateway nat-1 (private IP 10.0.0.5)
public subnet 10.0.0.0/24: nat-1 → internet gateway
→ NAT用Elastic IP 203.0.113.5 → server 198.51.100.20:443
パケットは対応する変換点の固定例。TTLは64に固定し、AWS内部のhop数は表さない。
SG・ACL・DNSの条件はこの経路計算では許可済みとする。
{
"private": [
{
"cidr": "10.0.0.0/16",
"target": "local"
},
{
"cidr": "0.0.0.0/0",
"target": "igw"
}
],
"public": [
{
"cidr": "10.0.0.0/16",
"target": "local"
},
{
"cidr": "0.0.0.0/0",
"target": "igw"
}
]
}python3 route.py lookup --dst 10.0.2.20
python3 route.py lookup --dst 198.51.100.2010.0.2.20 route={'cidr': '10.0.0.0/16', 'target': 'local'}
198.51.100.20 route={'cidr': '0.0.0.0/0', 'target': 'igw'}python3 route.py traceprivate route={'cidr': '0.0.0.0/0', 'target': 'igw'}
blocked: source 10.0.1.10 has no public IPv4 mappingcp routes-nat.json routes.json
python3 route.py traceprivate route={'cidr': '0.0.0.0/0', 'target': 'nat-1'}
public route={'cidr': '0.0.0.0/0', 'target': 'igw'}
client: 10.0.1.10:49152 -> 198.51.100.20:443
NAT: 10.0.0.5:62000 -> 198.51.100.20:443
IGW: 203.0.113.5:62000 -> 198.51.100.20:443tcpdump -nn -tt -S -vv -r inside.pcap 2>/dev/null0.000000 IP (tos 0x0, ttl 64, id 1234, offset 0, flags [DF], proto TCP (6), length 40)
10.0.1.10.49152 > 198.51.100.20.443: Flags [S], cksum 0xb9fc (correct), seq 1000, win 64240, length 0
0.010000 IP (tos 0x0, ttl 64, id 1234, offset 0, flags [DF], proto TCP (6), length 40)
198.51.100.20.443 > 10.0.1.10.49152: Flags [S.], cksum 0x96c3 (correct), seq 9000, ack 1001, win 64240, length 0tcpdump -nn -tt -S -vv -r nat-private.pcap 2>/dev/null0.000000 IP (tos 0x0, ttl 64, id 1234, offset 0, flags [DF], proto TCP (6), length 40)
10.0.0.5.62000 > 198.51.100.20.443: Flags [S], cksum 0x88d1 (correct), seq 1000, win 64240, length 0
0.010000 IP (tos 0x0, ttl 64, id 1234, offset 0, flags [DF], proto TCP (6), length 40)
198.51.100.20.443 > 10.0.0.5.62000: Flags [S.], cksum 0x6598 (correct), seq 9000, ack 1001, win 64240, length 0tcpdump -nn -tt -S -vv -r outside.pcap 2>/dev/null
cat mapping.json0.000000 IP (tos 0x0, ttl 64, id 1234, offset 0, flags [DF], proto TCP (6), length 40)
203.0.113.5.62000 > 198.51.100.20.443: Flags [S], cksum 0x56d0 (correct), seq 1000, win 64240, length 0
0.010000 IP (tos 0x0, ttl 64, id 1234, offset 0, flags [DF], proto TCP (6), length 40)
198.51.100.20.443 > 203.0.113.5.62000: Flags [S.], cksum 0x3397 (correct), seq 9000, ack 1001, win 64240, length 0
{
"inside_ip": "10.0.1.10",
"inside_port": 49152,
"nat_private": "10.0.0.5",
"nat_public": "203.0.113.5",
"nat_port": 62000,
"remote_ip": "198.51.100.20",
"remote_port": 443
}python3 route.py reply --port 62000
python3 route.py reply --port 62001198.51.100.20:443 -> 203.0.113.5:62000 -> 10.0.0.5:62000 -> 10.0.1.10:49152 dropped: no established mapping for destination port 62001
演習完了