一覧NW-04-1 · IPv4のパケット

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

1
tcpdump -n -tt -vvv -r header.pcap
reading 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 12
2
xxd -g 1 -c 16 header-before.bin
00000000: 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!
3
python3 inspect_ipv4.py header-before.bin
version= 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
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]))'
[192, 0, 2, 10] 192.0.2.10
[198, 51, 100, 20] 198.51.100.20
5
python3 inspect_ipv4.py header-after.bin
version= 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
6
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.bin
version= 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
7
python3 inspect_ipv4.py options.bin
version= 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
8
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))]

演習完了