NATによるアドレスとポートの変換
内外のパケットと対応表から、返信先と宛先変換後の処理を確認します。
説明
NATはパケットのアドレスを変換し、NAPTではTCPやUDPのポートも対応付けます。同じ接続でも内側と外側で見えるIP・ポートが違うため、観測点を意識して記録を読む必要があります。
一つのTCP接続を内外のPCAPで照合し、バイト列と対応表を調べます。二端末の識別、状態を失った場合、公開側からの宛先変換を順に確認し、最後に転送フィルターとの関係を調べます。
1内側で見える接続
inside.pcapは端末10.0.0.10がTCP443へ接続を始めた、NAT装置の内側の記録です。
tcpdump -n -tt -S -vvv時刻と絶対シーケンス番号 -r inside.pcapNAT内側の記録端末49152から198.51.100.20の443へSYNを送り、応答は端末49152へ戻っています。-Sで相対値ではなく実際のシーケンス番号を表示し、別の観測点でも同じ通信を対応させます。
2外側で見える同じ接続
outside.pcapは同じ往復をNAT装置の外側で観測した記録です。
tcpdump -n -tt -S -vvv同じSYNと応答を追う -r outside.pcapNAT外側の記録送信元が203.0.113.5:62000へ変わり、相手はこのIPとポートへ応答しています。SYNのsequence1000と応答のack1001は一致し、内側の49152と外側の62000が一つの接続に対応します。
3バイト列から変換箇所を取り出す
各binは最初のSYNのEthernetフレームです。IPv4の送信元とTCPの送信元ポート・checksumを取り出します。
python3 -c内外の欄を抽出 'from pathlib import Path; import socket,struct; frames=[Path(n).read_bytes() for n in ("inside.bin","outside.bin")]; print([(socket.inet_ntoa(f[26:30]),struct.unpack("!H",f[34:36])[0],f[24:26].hex(),f[50:52].hex()) for f in frames])'アドレス・ポート・チェックサム各行は送信元IP、送信元ポート、IPv4 checksum、TCP checksumです。アドレスとポートの変更に加え、転送によるTTLの減少もIPv4 checksumへ影響します。TCPのchecksumはIPの疑似ヘッダーを含むため、IPだけを変える場合も更新が必要です。
4二端末の同じポートを対応付ける
二台の端末が同じ送信元ポート49152で同じ相手へ接続した対応表です。
cat mapping.csv内側・外側・相手の組外側では62000と62001へ分け、同じ203.0.113.5を共有します。アドレスだけでなくポートも対応付けるのがNAPTです。相手の返信に含まれるポートから、元の端末へ戻すための情報を保存しています。
5返信先から内側の端末を検索する
198.51.100.20:443から外側62001へ来た返信を、保存した対応へ照合します。
python3 -c返信に一致する対応を検索 'import csv; rows=list(csv.DictReader(open("mapping.csv"))); print([r["INSIDE"] for r in rows if r["PROTO"]=="tcp" and r["OUTSIDE"]=="203.0.113.5:62001" and r["REMOTE"]=="198.51.100.20:443"])'プロトコルと内外の組10.0.0.11:49152へ戻す対応が見つかります。この演習は相手を含めて照合する表です。実装のマッピング方式やフィルタリング方式に違いがあるため、すべてのNATが同じ検索キーを使うとは限りません。
6対応を保持しない装置へ返信が来た場合
別のNAT装置へ経路が切り替わり、その装置には対応が共有されていない条件を調べます。
python3 -c状態のない装置で照合 'state={}; key=("tcp","203.0.113.5:62001","198.51.100.20:443"); print(state.get(key,"mapping not found"))'対応表が空の場合この状態表では返信先の内側端末を決められません。冗長化ではIPアドレスや経路の切替だけでなく、変換状態の扱いも必要です。実際に破棄するか等の動作は製品と設定によります。
7公開側の宛先を内部サーバーへ変える
次は外部から公開IPの8443へ入る別の接続です。変換前後のSYNを読みます。
tcpdump -n -tt -S -r dnat-before.pcap宛先変換の前; tcpdump -n -tt -S -r dnat-after.pcap宛先変換の後宛先203.0.113.5:8443が10.0.0.20:443へ変わりました。送信元192.0.2.80:55000とsequence7000はそのままです。宛先変換では、内部サーバーの返信にも逆変換できる経路と状態が必要になります。
8変換の設定と転送の許可
この装置ではDNAT後に転送フィルターを評価する設定です。
cat policy.csv変換後に評価する規則変換後の宛先10.0.0.20:443にdropが一致するため、サーバーへは転送されません。前のPCAPは変換前後の観測点の試料で、サーバーが受信した証拠ではありません。宛先を書き換える設定と通信を許可する規則は、別々に確認します。
まとめ
アドレス・ポート・シーケンス番号を使って、観測点の違う接続を対応付けます。
逆変換に必要な状態と経路、変換後に適用する通信規則を確認します。
操作履歴と解説を振り返れます。
シェル / PCAPと通信の記録
tcpdump -n -tt -S -vvv -r inside.pcapreading from file inside.pcap, link-type EN10MB (Ethernet)
0.000000 IP (tos 0x0, ttl 64, id 1234, offset 0, flags [DF], proto TCP (6), length 40)
10.0.0.10.49152 > 198.51.100.20.443: Flags [S], cksum 0xbafc (correct), seq 1000, win 64240, length 0
0.010000 IP (tos 0x0, ttl 62, id 1234, offset 0, flags [DF], proto TCP (6), length 40)
198.51.100.20.443 > 10.0.0.10.49152: Flags [S.], cksum 0x97c3 (correct), seq 9000, ack 1001, win 64240, length 0tcpdump -n -tt -S -vvv -r outside.pcapreading from file outside.pcap, link-type EN10MB (Ethernet)
0.000000 IP (tos 0x0, ttl 63, 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 63, 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 0python3 -c 'from pathlib import Path; import socket,struct; frames=[Path(n).read_bytes() for n in ("inside.bin","outside.bin")]; print([(socket.inet_ntoa(f[26:30]),struct.unpack("!H",f[34:36])[0],f[24:26].hex(),f[50:52].hex()) for f in frames])'[('10.0.0.10', 49152, '01ad', 'bafc'), ('203.0.113.5', 62000, 'd0b0', '56d0')]cat mapping.csvPROTO,INSIDE,OUTSIDE,REMOTE tcp,10.0.0.10:49152,203.0.113.5:62000,198.51.100.20:443 tcp,10.0.0.11:49152,203.0.113.5:62001,198.51.100.20:443
python3 -c 'import csv; rows=list(csv.DictReader(open("mapping.csv"))); print([r["INSIDE"] for r in rows if r["PROTO"]=="tcp" and r["OUTSIDE"]=="203.0.113.5:62001" and r["REMOTE"]=="198.51.100.20:443"])'['10.0.0.11:49152']
python3 -c 'state={}; key=("tcp","203.0.113.5:62001","198.51.100.20:443"); print(state.get(key,"mapping not found"))'mapping not found
tcpdump -n -tt -S -r dnat-before.pcap; tcpdump -n -tt -S -r dnat-after.pcapreading from file dnat-before.pcap, link-type EN10MB (Ethernet) reading from file dnat-after.pcap, link-type EN10MB (Ethernet) 0.000000 IP 192.0.2.80.55000 > 203.0.113.5.8443: Flags [S], seq 7000, win 64240, length 0 0.000000 IP 192.0.2.80.55000 > 10.0.0.20.443: Flags [S], seq 7000, win 64240, length 0
cat policy.csvSTAGE,DESTINATION,PORT,ACTION after_DNAT,10.0.0.20,443,drop
演習完了