IPフラグメンテーションと経路MTU
断片のバイト位置と、分割できない場合の通知を解析します。
説明
大きなIPv4パケットは、次のリンクのMTUに合わせて分割される場合があります。各断片は独立したIPv4ヘッダーを持ち、受信側がオフセットを使って元のデータグラムへ戻します。DFが立っていれば、途中のルーターは分割できません。
全長2028バイトの試料をMTU600に合わせて分割し、4断片を順不同で組み直します。一片が欠ける場合とDFによる配送失敗を比較し、ICMPが伝える次ホップMTUから送信サイズを計算します。
1分割前の大きなデータグラム
2000バイトのデータをUDPで運ぶ試料です。UDPヘッダー8、IPv4ヘッダー20を加え、IP全長2028バイトになっています。
tcpdump -n -tt -vvv全長と分割フラグを確認 -r whole.pcap分割前の記録DFは設定されておらず、分割できる条件です。IPv4のIdentificationは0x4321です。次のリンクのMTUは600バイトであり、2028バイトのままではそのリンクへ送れません。
2断片のデータ長を8バイト単位へそろえる
MTU600からヘッダー20を引くと580ですが、最後以外の断片のデータ長は8バイト単位にそろえます。
python3 -c断片の位置を計算 'mtu=600; header=20; chunk=((mtu-header)//8)*8; total=2008; print(chunk,[(offset,min(chunk,total-offset),offset//8) for offset in range(0,total,chunk)])'MTUから8バイト単位の断片長各断片のデータ長は576・576・576・280です。開始位置は0・576・1152・1728バイト、ヘッダーへ保存するオフセットは0・72・144・216となります。分割する対象にはUDPヘッダーも含まれます。
3分割されたパケットを読む
fragments.pcapの4断片を表示します。
tcpdump -n -tt -vvv断片のフラグと位置 -r fragments.pcap4断片の記録同じ識別子を持つ4断片です。最初の三つはMFが立ち、最後は下ります。全長は596・596・596・300バイトで、すべてMTU600以下です。UDPヘッダーは最初の断片にだけ存在するため、後の断片でポート番号を直接読むことはできません。
4最初と最後のヘッダーを照合
最初と最後をバイト列から復号し、MFとオフセットを確認します。
python3 inspect_ipv4.py fragment-1.bin先頭断片; python3 inspect_ipv4.py fragment-4.bin最後の断片最初はMF=1・開始0、最後はMF=0・開始1728です。最後のデータ長280を加えると元のIPペイロード全体2008バイトと分かります。再構成では送信元、宛先、プロトコル、識別子を対応させ、別のデータグラムを混ぜません。
5到着順を変えて再構成
3・1・4・2の順に並べた断片を再構成します。reassemble.pyはオフセットへ配置し、重複や範囲外を拒否し、全位置がそろったかを確認する教材用処理です。
python3 reassemble.py位置を使って再構成 fragment-3.bin fragment-1.bin fragment-4.bin fragment-2.bin順不同の入力2008バイトすべてが埋まり、元のUDPデータグラムと一致します。到着順ではなくオフセットで並べるため、順不同でも復元できます。ここでは互いに重ならない正常な断片を対象にしています。
6一つの断片が欠けた場合
2番目を除き、残る3断片だけで確認します。
python3 reassemble.py欠けた範囲を検査 fragment-3.bin fragment-1.bin fragment-4.bin2番目なしの入力576バイトが欠け、範囲は576〜1151です。最後の断片が到着していても、途中の穴が残れば完全なUDPデータグラムになりません。IP再構成には期限があり、必要な断片がそろわなければ破棄されます。UDP自身に欠けた断片だけを再送する機能はありません。
7DFが設定されていた場合の応答
同じ全長2028のパケットにDFを設定した試料に対し、MTU600のルーターが返すICMPを読みます。
tcpdump -n -tt -vvvICMPの種別と引用を読む -r need-fragment.pcap分割できない場合の記録ICMP Destination Unreachableのcode 4で、必要な分割がDFによりできず、次ホップMTU600と通知しています。元のIPヘッダーと先頭8バイトが引用されるため、どの送信に対する通知かを対応付けられます。
8通知されたMTUから次の送信サイズを求める
ICMPヘッダーのMTU欄を読み、IPv4とUDPのヘッダーを引きます。
python3 -cMTU欄を復号 'from pathlib import Path; p=Path("icmp.bin").read_bytes(); start=(p[0]&15)*4; mtu=int.from_bytes(p[start+6:start+8],"big"); print(p[start],p[start+1],mtu,mtu-20-8)'通知とヘッダー分を差し引いた量type3・code4・MTU600で、オプションなしIPv4とUDPならデータ572バイトまでがこのMTU内です。送信するアプリケーションのメッセージ境界を保つ方法は別に設計します。ICMPが届かない場合は単に大きな送信が失敗し続けることもあり、損失とサイズの関係を調べます。
まとめ
断片のオフセットは8バイト単位で、MFと各長さから元の範囲を求めます。
再構成は到着順ではなく位置で行い、途中の欠損があるまま上位へ渡しません。
DFで分割できない場合は通知されたMTU等を使い、送信側でサイズを調整します。
操作履歴と解説を振り返れます。
シェル / PCAPと通信の記録
tcpdump -n -tt -vvv -r whole.pcapreading from file whole.pcap, link-type EN10MB (Ethernet)
0.000000 IP (tos 0x0, ttl 64, id 17185, offset 0, flags [none], proto UDP (17), length 2028)
192.0.2.10.40000 > 198.51.100.20.9000: [udp sum ok] UDP, length 2000python3 -c 'mtu=600; header=20; chunk=((mtu-header)//8)*8; total=2008; print(chunk,[(offset,min(chunk,total-offset),offset//8) for offset in range(0,total,chunk)])'576 [(0, 576, 0), (576, 576, 72), (1152, 576, 144), (1728, 280, 216)]
tcpdump -n -tt -vvv -r fragments.pcapreading from file fragments.pcap, link-type EN10MB (Ethernet)
0.000000 IP (tos 0x0, ttl 63, id 17185, offset 0, flags [+], proto UDP (17), length 596)
192.0.2.10.40000 > 198.51.100.20.9000: UDP, length 2000
0.000100 IP (tos 0x0, ttl 63, id 17185, offset 576, flags [+], proto UDP (17), length 596)
192.0.2.10 > 198.51.100.20: ip-proto-17
0.000200 IP (tos 0x0, ttl 63, id 17185, offset 1152, flags [+], proto UDP (17), length 596)
192.0.2.10 > 198.51.100.20: ip-proto-17
0.000300 IP (tos 0x0, ttl 63, id 17185, offset 1728, flags [none], proto UDP (17), length 300)
192.0.2.10 > 198.51.100.20: ip-proto-17python3 inspect_ipv4.py fragment-1.bin; python3 inspect_ipv4.py fragment-4.binversion= 4 IHL_bytes= 20 total= 596 DSCP= 0 ECN= 0 id= 0x4321 DF= 0 MF= 1 offset_units= 0 offset_bytes= 0 TTL= 63 protocol= 17 checksum= 0x2a26 sum= 0xffff source= 192.0.2.10 destination= 198.51.100.20 payload_bytes= 576 first8= 9c 40 23 28 07 d8 f8 3a version= 4 IHL_bytes= 20 total= 300 DSCP= 0 ECN= 0 id= 0x4321 DF= 0 MF= 0 offset_units= 216 offset_bytes= 1728 TTL= 63 protocol= 17 checksum= 0x4a76 sum= 0xffff source= 192.0.2.10 destination= 198.51.100.20 payload_bytes= 280 first8= d6 d7 d8 d9 da db dc dd
python3 reassemble.py fragment-3.bin fragment-1.bin fragment-4.bin fragment-2.binexpected= 2008 covered= 2008 missing= 0 matches_original= True ports,length,checksum= 9c 40 23 28 07 d8 f8 3a
python3 reassemble.py fragment-3.bin fragment-1.bin fragment-4.binexpected= 2008 covered= 1432 missing= 576 missing_range= 576 1151
tcpdump -n -tt -vvv -r need-fragment.pcapreading from file need-fragment.pcap, link-type EN10MB (Ethernet)
0.000000 IP (tos 0x0, ttl 64, id 17185, offset 0, flags [none], proto ICMP (1), length 56)
192.0.2.1 > 192.0.2.10: ICMP 198.51.100.20 unreachable - need to frag (mtu 600), length 36
IP (tos 0x0, ttl 64, id 17185, offset 0, flags [DF], proto UDP (17), length 2028)
192.0.2.10.40000 > 198.51.100.20.9000: UDP, length 2000python3 -c 'from pathlib import Path; p=Path("icmp.bin").read_bytes(); start=(p[0]&15)*4; mtu=int.from_bytes(p[start+6:start+8],"big"); print(p[start],p[start+1],mtu,mtu-20-8)'3 4 600 572
演習完了