一覧NW-08-2 · ルーティングとNAT

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と通信の記録

1
tcpdump -n -tt -S -vvv -r inside.pcap
reading 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 0
2
tcpdump -n -tt -S -vvv -r outside.pcap
reading 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 0
3
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])'
[('10.0.0.10', 49152, '01ad', 'bafc'), ('203.0.113.5', 62000, 'd0b0', '56d0')]
4
cat mapping.csv
PROTO,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
5
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']
6
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
7
tcpdump -n -tt -S -r dnat-before.pcap; tcpdump -n -tt -S -r dnat-after.pcap
reading 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
8
cat policy.csv
STAGE,DESTINATION,PORT,ACTION
after_DNAT,10.0.0.20,443,drop

演習完了