IPv4ヘッダーの構造
ヘッダーの各欄をバイト列から読み、転送時の変更と上位プロトコルの位置を確認します。
説明
IPv4ヘッダーには宛先と送信元、パケットの長さ、上位プロトコル、TTL、分割に関する情報などがあります。各欄のビット幅と長さの単位を知ると、解析ツールの表示を元のバイトで確認できます。
UDPを運ぶ一つのパケットを転送前後で比較します。アドレス、DSCP/ECN、TTLとチェックサムを調べ、更新を忘れた例と、オプションでヘッダーが長くなった例を解析します。
1同じIPv4パケットの転送前後
header.pcapには小さなUDPデータグラムを運ぶIPv4パケットを、ルーターの前後で記録しています。NATはなく、TTLは2から始まります。
tcpdump -n -tt -vvvIPとUDPを詳しく表示 -r header.pcap転送前後の記録送信元192.0.2.10、宛先198.51.100.20は同じで、TTLだけが2から1へ減っています。Protocol 17はUDPです。40000と9000はUDPヘッダーのポート番号であり、IPv4ヘッダー自体にはポート番号はありません。
2IPv4ヘッダーを16進数で表示
header-before.binはEthernet部分を除き、IPv4の先頭から始まるバイト列です。
xxd -g 1 -c 161バイトずつ表示 header-before.binIPv4から始まる試料先頭45はVersion=4とIHL=5を表します。IHLの単位は4バイトなのでヘッダー長は20です。次のbaはDSCPとECN、00 28はIP全長40バイト、be efはIdentificationです。
3各フィールドの値を取り出す
inspect_ipv4.pyはヘッダーの位置とビット幅に従って値を取り出す教材用デコーダーです。
python3 inspect_ipv4.py欄を復号する header-before.bin転送前のヘッダーDSCP=46、ECN=2、DF=1、MF=0、断片オフセット=0です。DSCPは処理クラスの指定、ECN=2はECT(0)です。値を付けただけで帯域や応答時間が保証されるわけではありません。sum=0xffffはチェックサムを含むヘッダーの16ビット和が整合したことを示します。
4アドレスの4バイトを十進数へ変換
送信元と宛先の各4バイトを取り出し、バイトの十進値とアドレス表記を並べます。
python3 -cアドレス欄を変換 'from pathlib import Path; import ipaddress; p=Path("header-before.bin").read_bytes(); print(list(p[12:16]),ipaddress.ip_address(p[12:16])); print(list(p[16:20]),ipaddress.ip_address(p[16:20]))'オフセット12と16から4バイトずつc0 00 02 0aは192・0・2・10、c6 33 64 14は198・51・100・20に対応します。これらは配送先を示すネットワーク層のアドレスで、同じリンクの次の受信先を表すMACとは別です。
5TTL変更に伴うチェックサム更新
転送後のヘッダーを同じデコーダーで調べます。
python3 inspect_ipv4.py同じ欄を復号 header-after.binTTLを減らしたヘッダーTTLが1になり、チェックサムの値も変わっています。全長、識別子、アドレス、UDP部分は同じです。次のルーターでTTLが0になるなら転送せず、通常はICMP Time Exceededで送信元へ通知します。TTLは送信元からの経過秒を測る時計ではありません。
6TTLだけを変えて更新を忘れた場合
元のヘッダーをコピーし、TTLだけ1へ変えてチェックサムをそのまま残します。
python3 -c 'from pathlib import Path; p=bytearray(Path("header-before.bin").read_bytes()); p[8]=1; Path("bad-header.bin").write_bytes(p)'TTLだけを変更したコピー; python3 inspect_ipv4.py bad-header.binヘッダー和の不一致ヘッダー和が0xffffではなくなります。IPv4チェックサムはヘッダーの誤りを検出するための値で、変更する際は対応して更新する必要があります。この検査だけではUDPのデータ部分の破損は検出できません。
7オプションがあるとUDPの開始位置が変わる
options.binには4バイトのオプション領域を加えています。先頭2個がNOP、その後がEOLと埋め草です。
python3 inspect_ipv4.pyIHLに従って解析 options.binオプション付きの試料IHLは24バイト、全長は44バイトです。UDPの先頭8バイトは元のパケットと同じですが、開始位置は20から24へ移っています。IPv4の直後を読む処理では、常に20バイトと決めずIHLから計算します。
8IHLからUDPヘッダーを読む
元の試料とオプション付き試料の両方で、IHLからUDPの開始位置を求めます。
python3 -c上位プロトコルの位置を計算 'from pathlib import Path; import struct; packets=[Path(n).read_bytes() for n in ("header-before.bin","options.bin")]; print([((p[0]&15)*4,struct.unpack_from("!HHHH",p,(p[0]&15)*4)) for p in packets])'IHLを使ったUDPの復号開始位置は20と24ですが、送信元40000、宛先9000、UDP長20、チェックサムは一致します。IP全長からIPヘッダー長を引くと20バイトで、UDPヘッダー8とデータ12に分けられます。
まとめ
VersionとIHL、全長、フラグ等をそれぞれの幅と単位で読みます。
TTLを変更するとIPv4ヘッダーチェックサムも更新します。
上位プロトコルの開始位置はIHLから求め、ポート番号は上位ヘッダーから読みます。
操作履歴と解説を振り返れます。
シェル / PCAPと通信の記録
tcpdump -n -tt -vvv -r header.pcapreading from file header.pcap, link-type EN10MB (Ethernet)
0.000000 IP (tos 0xba,ECT(0), ttl 2, id 48879, offset 0, flags [DF], proto UDP (17), length 40)
192.0.2.10.40000 > 198.51.100.20.9000: [udp sum ok] UDP, length 12
0.000100 IP (tos 0xba,ECT(0), ttl 1, id 48879, offset 0, flags [DF], proto UDP (17), length 40)
192.0.2.10.40000 > 198.51.100.20.9000: [udp sum ok] UDP, length 12xxd -g 1 -c 16 header-before.bin00000000: 45 ba 00 28 be ef 40 00 02 11 cc c9 c0 00 02 0a E..(..@......... 00000010: c6 33 64 14 9c 40 23 28 00 14 c2 1b 68 65 6c 6c .3d..@#(....hell 00000020: 6f 20 77 6f 72 6c 64 21 o world!
python3 inspect_ipv4.py header-before.binversion= 4 IHL_bytes= 20 total= 40 DSCP= 46 ECN= 2 id= 0xbeef DF= 1 MF= 0 offset_units= 0 offset_bytes= 0 TTL= 2 protocol= 17 checksum= 0xccc9 sum= 0xffff source= 192.0.2.10 destination= 198.51.100.20 payload_bytes= 20 first8= 9c 40 23 28 00 14 c2 1b
python3 -c 'from pathlib import Path; import ipaddress; p=Path("header-before.bin").read_bytes(); print(list(p[12:16]),ipaddress.ip_address(p[12:16])); print(list(p[16:20]),ipaddress.ip_address(p[16:20]))'[192, 0, 2, 10] 192.0.2.10 [198, 51, 100, 20] 198.51.100.20
python3 inspect_ipv4.py header-after.binversion= 4 IHL_bytes= 20 total= 40 DSCP= 46 ECN= 2 id= 0xbeef DF= 1 MF= 0 offset_units= 0 offset_bytes= 0 TTL= 1 protocol= 17 checksum= 0xcdc9 sum= 0xffff source= 192.0.2.10 destination= 198.51.100.20 payload_bytes= 20 first8= 9c 40 23 28 00 14 c2 1b
python3 -c 'from pathlib import Path; p=bytearray(Path("header-before.bin").read_bytes()); p[8]=1; Path("bad-header.bin").write_bytes(p)'; python3 inspect_ipv4.py bad-header.binversion= 4 IHL_bytes= 20 total= 40 DSCP= 46 ECN= 2 id= 0xbeef DF= 1 MF= 0 offset_units= 0 offset_bytes= 0 TTL= 1 protocol= 17 checksum= 0xccc9 sum= 0xfeff source= 192.0.2.10 destination= 198.51.100.20 payload_bytes= 20 first8= 9c 40 23 28 00 14 c2 1b
python3 inspect_ipv4.py options.binversion= 4 IHL_bytes= 24 total= 44 DSCP= 46 ECN= 2 id= 0xbeef DF= 1 MF= 0 offset_units= 0 offset_bytes= 0 TTL= 2 protocol= 17 checksum= 0xcac4 sum= 0xffff source= 192.0.2.10 destination= 198.51.100.20 payload_bytes= 20 first8= 9c 40 23 28 00 14 c2 1b
python3 -c 'from pathlib import Path; import struct; packets=[Path(n).read_bytes() for n in ("header-before.bin","options.bin")]; print([((p[0]&15)*4,struct.unpack_from("!HHHH",p,(p[0]&15)*4)) for p in packets])'[(20, (40000, 9000, 20, 49691)), (24, (40000, 9000, 20, 49691))]
演習完了