一覧NW-01-1 · 通信の基本モデル

プロトコルの階層とカプセル化

同じ要求のバイト列を、中継前後と受信側で照合します。

説明

Webの要求は、アプリケーションの文字列をTCP、IP、Ethernetで包み、リンク上の信号として運びます。それぞれの層が扱う宛先や長さ、完了の意味は異なります。

一つのHTTP要求を二つの観測点で読み、MACとIPの宛先、TTL、ヘッダー長を照合します。ダンプからHTTPの文字列を取り出し、TCPの受信位置と媒体の情報まで、同じ要求を使って確認します。

1要求を送る二つの端末とルーター

192.0.2.10のクライアントから198.51.100.20のサーバーへ、/statusを取得するHTTP要求を送ります。TCPは接続済みで、NATはない構成です。

cat topology.txt端末・ルーター・MACの対応

端末間にはルーターが一台あります。IPは相手の端末を宛先にしますが、最初のEthernetフレームの宛先は同じLAN上のルーターです。PCAPは各リンクで見える同じ要求を記録した教材試料です。

2クライアント側で見えるフレーム

lan.pcapをtcpdumpで読みます。-nは名前解決を行わず数値で表示、-eはEthernetヘッダー、-SはTCPシーケンス番号を絶対値で表示します。

tcpdump -n -tt -e -vvv -SMAC・IP・TCPを詳しく表示 -r lan.pcapクライアント側の記録

Ethernet宛先はルーターの02:00:00:00:00:01、IP宛先はサーバーの198.51.100.20です。TCPは49152番から80番へ44バイトを運び、seq 1001:1045は1001から1044までの位置を示します。P.はPSHとACKのフラグです。

3ルーター通過後のフレーム

同じ要求をサーバー側LANで記録したrouted.pcapを読みます。

tcpdump -n -tt -e -vvv -S同じ形式で表示 -r routed.pcapルーター通過後の記録

MACはルーターの送信側からサーバーへ変わり、TTLは64から63になっています。IPの送信元・宛先とTCPのポート・シーケンス番号・内容は同じです。この構成では、ルーターが次のリンク用にEthernetヘッダーを付け直します。

4フレームのバイト列

lan.binはPCAPの管理ヘッダーを取り除いた一つのEthernetフレームです。左端はフレーム先頭からの16進オフセットです。

xxd -g 11バイトずつ16進表示 -c 16 lan.bin16バイトごとに配置

先頭6バイトは宛先MAC、次の6バイトは送信元MAC、12〜13バイト目の08 00はIPv4を表します。14バイト目の45からIPv4が始まり、その中にTCP、さらにHTTPの文字列が入っています。この試料にはプリアンブルとFCSは含まれません。

5ヘッダー長から境界を求める

IPv4のIHLとTCPのData Offsetを読み、HTTPが始まる位置を計算します。固定の20バイトと決めつけず、ヘッダー内の値から求めます。

python3 -cバイト列から長さを計算 'from pathlib import Path; f=Path("lan.bin").read_bytes(); ip=14; ihl=(f[ip]&15)*4; tcp=ip+ihl; thl=(f[tcp+12]>>4)*4; app=tcp+thl; print("Ethernet=14 IPv4=",ihl,"TCP=",thl,"HTTP offset=",app,"bytes=",len(f)-app)'ヘッダー内の長さと開始位置

IPv4もTCPも20バイトで、HTTPは先頭から54バイト目、長さは44バイトです。IPの全長84バイトにEthernetの14バイトを加えると、記録された98バイトになります。オプションがあれば境界は変わります。

6中継で変わったバイトを抽出

二つのフレームを同じ位置で比較し、違うバイトだけを表示します。

python3 -c二つのフレームを比較 'from pathlib import Path; a=Path("lan.bin").read_bytes(); b=Path("routed.bin").read_bytes(); print(*[f"0x{i:04x}: {x:02x} -> {y:02x}" for i,(x,y) in enumerate(zip(a,b)) if x!=y],sep="\n")'位置と変更前後のバイト

変わるのはMACの一部、TTL、IPv4ヘッダーチェックサムの一部です。MAC全体の値は前のtcpdump出力と照合できます。TTLの更新に伴いIPv4チェックサムも更新されますが、TCPの対象範囲にはTTLを含まないため、TCPチェックサムは変わりません。

7受信側でHTTPの要求を取り出す

サーバー側のフレームからHTTP部分を取り出します。表示上の改行では見えにくいCRLFもreprで確認します。

python3 -cHTTP部分を取り出す 'from pathlib import Path; f=Path("routed.bin").read_bytes(); body=f[54:]; print(repr(body)); print(body.decode("ascii"),end="")'区切りのバイトと文字列

要求行、Hostヘッダー、空行の順です。CRLFは0d 0aの2バイトで、最後の空行がヘッダーの終わりを示します。アプリケーションはTCPのバイト列から要求を解釈します。一般にはHTTP要求一つがTCPセグメント一つに収まるとは限りません。

844バイトを受信した後のACK番号

TCPシーケンス番号1001に44バイトを加え、連続して受信済みなら次に期待する番号を求めます。

python3 -cTCPの番号を計算 'from pathlib import Path; import struct; f=Path("routed.bin").read_bytes(); seq=struct.unpack_from("!I",f,38)[0]; length=len(f)-54; print(seq,length,(seq+length)%(2**32))'受信範囲の次の位置

次に期待するのは1045です。この範囲を確認するACK番号は1045になります。ただしTCPの確認応答は、HTTP処理やデータ保存まで終えた保証ではありません。ネットワーク、トランスポート、アプリケーションで完了の意味が異なります。

9物理媒体はフレーム外の情報から確認

二つのリンクの媒体・速度・距離を読みます。

cat links.csv媒体と伝送条件の記録

LAN-Aは銅線20m、LAN-Bは光ファイバー2000mという構成です。電気・光信号の伝送は物理層の役割ですが、媒体や距離がこのEthernetフレーム内に保存されているわけではありません。OSIの7層は役割を整理するモデルで、各層の独立ヘッダーが必ず七つあるという意味でもありません。

まとめ

ルーターを越えると次のリンク用のMACとTTL等が変わり、この例では端末間のIP・TCP・HTTPは保たれます。

ヘッダー内の長さから各部分の境界を求め、バイト列と表示を照合します。

TCP受信、アプリケーション処理、物理媒体は別の情報として確認します。

操作履歴と解説を振り返れます。

シェル / PCAPと通信の記録

1
cat topology.txt
client 192.0.2.10 -- LAN-A -- router -- LAN-B -- server 198.51.100.20
client MAC 02:00:00:00:00:0a
router LAN-A MAC 02:00:00:00:00:01 / IP 192.0.2.1
router LAN-B MAC 02:00:00:00:00:02 / IP 198.51.100.1
server MAC 02:00:00:00:00:14
NAT: none; TCP connection: established
2
tcpdump -n -tt -e -vvv -S -r lan.pcap
reading from file lan.pcap, link-type EN10MB (Ethernet)
0.000000 02:00:00:00:00:0a > 02:00:00:00:00:01, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 4660, offset 0, flags [DF], proto TCP (6), length 84)
    192.0.2.10.49152 > 198.51.100.20.80: Flags [P.], cksum 0x1ea7 (correct), seq 1001:1045, ack 9001, win 64240, length 44: HTTP, length: 44
	GET /status HTTP/1.1
	Host: example.test
	
3
tcpdump -n -tt -e -vvv -S -r routed.pcap
reading from file routed.pcap, link-type EN10MB (Ethernet)
0.000100 02:00:00:00:00:02 > 02:00:00:00:00:14, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 63, id 4660, offset 0, flags [DF], proto TCP (6), length 84)
    192.0.2.10.49152 > 198.51.100.20.80: Flags [P.], cksum 0x1ea7 (correct), seq 1001:1045, ack 9001, win 64240, length 44: HTTP, length: 44
	GET /status HTTP/1.1
	Host: example.test
	
4
xxd -g 1 -c 16 lan.bin
00000000: 02 00 00 00 00 01 02 00 00 00 00 0a 08 00 45 00  ..............E.
00000010: 00 54 12 34 40 00 40 06 3c 1e c0 00 02 0a c6 33  .T.4@.@.<......3
00000020: 64 14 c0 00 00 50 00 00 03 e9 00 00 23 29 50 18  d....P......#)P.
00000030: fa f0 1e a7 00 00 47 45 54 20 2f 73 74 61 74 75  ......GET /statu
00000040: 73 20 48 54 54 50 2f 31 2e 31 0d 0a 48 6f 73 74  s HTTP/1.1..Host
00000050: 3a 20 65 78 61 6d 70 6c 65 2e 74 65 73 74 0d 0a  : example.test..
00000060: 0d 0a                                            ..
5
python3 -c 'from pathlib import Path; f=Path("lan.bin").read_bytes(); ip=14; ihl=(f[ip]&15)*4; tcp=ip+ihl; thl=(f[tcp+12]>>4)*4; app=tcp+thl; print("Ethernet=14 IPv4=",ihl,"TCP=",thl,"HTTP offset=",app,"bytes=",len(f)-app)'
Ethernet=14 IPv4= 20 TCP= 20 HTTP offset= 54 bytes= 44
6
python3 -c 'from pathlib import Path; a=Path("lan.bin").read_bytes(); b=Path("routed.bin").read_bytes(); print(*[f"0x{i:04x}: {x:02x} -> {y:02x}" for i,(x,y) in enumerate(zip(a,b)) if x!=y],sep="\n")'
0x0005: 01 -> 14
0x000b: 0a -> 02
0x0016: 40 -> 3f
0x0018: 3c -> 3d
7
python3 -c 'from pathlib import Path; f=Path("routed.bin").read_bytes(); body=f[54:]; print(repr(body)); print(body.decode("ascii"),end="")'
b'GET /status HTTP/1.1\r\nHost: example.test\r\n\r\n'
GET /status HTTP/1.1
Host: example.test
8
python3 -c 'from pathlib import Path; import struct; f=Path("routed.bin").read_bytes(); seq=struct.unpack_from("!I",f,38)[0]; length=len(f)-54; print(seq,length,(seq+length)%(2**32))'
1001 44 1045
9
cat links.csv
LINK,MEDIUM,RATE_MBIT_S,DISTANCE_M,PROPAGATION_M_S
LAN-A,copper,1000,20,200000000
LAN-B,fiber,1000,2000,200000000

演習完了