TCPの接続確立とシーケンス番号
一つのTCP接続を開始からデータ受信まで解析します。
説明
TCPは順序付きのバイト列を両方向に運ぶプロトコルです。接続の開始では初期シーケンス番号を確認し、データ転送ではバイト番号と確認応答で受信状況を伝えます。
同じ接続のPCAPを読み、SYNヘッダーのバイト列からポート・番号・MSSを取り出します。続いて5バイトと6バイトのデータ、ACK、アプリケーションへ渡す11バイトの関係を確認します。
1同じ接続の開始からデータまで
端末192.0.2.10:49152がサーバー198.51.100.20:8080へ接続し、HELLO WORLDを送る記録です。
tcpdump -n -tt -S時刻と絶対シーケンス番号 -r connection.pcap接続開始とデータSYN、SYN/ACK、ACKの後に、5バイトと6バイトのデータが続きます。-Sは実際のsequenceを表示する指定です。この通信の送信元と宛先、各ポートを固定して、以後も同じ履歴を調べます。
2SYNのヘッダーをバイトで確認
syn.binは最初のTCPヘッダー24バイトで、IPv4ヘッダーは含みません。
xxd -g 1 -c 16TCPヘッダーのバイト syn.bin最初のSYNc0 00は49152、1f 90は8080、続く00 00 03 e8はsequence1000です。12バイト目の60はData Offset6、13バイト目の02はSYNです。20バイト目から02 04 05 b4というMSSオプションが続きます。
3ヘッダー長とMSSを復号
TCPのData Offsetは4バイト単位、MSSオプションの値は16ビットの整数です。
python3 -c固定欄とオプションを復号 'from pathlib import Path; import struct; t=Path("syn.bin").read_bytes(); print(struct.unpack("!HHII",t[:12])); print((t[12]>>4)*4,hex(t[13]),struct.unpack("!H",t[22:24])[0])'ポート・番号・ヘッダー長・MSS49152、8080、sequence1000、ACK欄0、ヘッダー長24、flags0x2、MSS1460です。SYNのACKフラグは0なので、このACK欄を有効な確認応答として読みません。MSSはこの端末が受信できるTCPデータの最大長を相手へ知らせ、IPやTCPヘッダーの長さを含みません。
4接続開始の番号の対応
接続開始の三つだけを詳しく表示します。
tcpdump -n -tt -S -vvv両方向の初期番号 -r handshake.pcap三つの開始セグメントクライアントのSYN1000に対しACK1001、サーバーのSYN9000に対しACK9001を返します。SYNはデータ0バイトでも番号を一つ消費します。両方向で別の番号空間を持ち、以後クライアントのデータは1001、サーバーのデータは9001から始まります。
5データの範囲と次のACK
data.jsonは同じPCAP生成時に保存したsequence・長さ・ACKの表です。データを持つ行の次の番号を計算します。
python3 -cデータ範囲と累積ACK 'import json; rows=json.load(open("data.json")); print([(r["seq"],r["length"],r["seq"]+r["length"]) for r in rows if r["length"]]); print([r["ack"] for r in rows if r["direction"]=="S>C"])'送信範囲の終端と確認応答最初は1001から5バイトで次が1006、次は1006から6バイトで次が1012です。受信側のACK1006と1012は、その直前まで連続して受信したことを知らせます。ACK値そのものは「最後に受信したバイト」ではなく、次に期待する番号です。
6二つのセグメントを一つのバイト列にする
この試料は順序通りで欠損も重複もありません。データ部分を連結し、保存したストリームと照合します。
python3 -cデータ部分を連結 'import json; from pathlib import Path; rows=json.load(open("data.json")); stream=b"".join(bytes.fromhex(r["data"]) for r in rows if r["direction"]=="C>S"); print(stream,stream==Path("stream.bin").read_bytes()); print(stream[:3],stream[3:])'連結結果と異なる読出し単位HELLO WORLDの11バイトに戻ります。アプリケーションが3バイトと8バイトで読み出すこともあり、送信セグメントの5・6バイトの境界をそのまま受け取る保証はありません。TCPはメッセージの境界ではなく順序付きのバイト列を提供します。
7同じサーバーへの別接続
TCPの接続を送受信のIP・ポートの四つ組で比較します。
python3 -c接続の識別情報を比較 'a=("192.0.2.10",49152,"198.51.100.20",8080); b=("192.0.2.10",49153,"198.51.100.20",8080); print(a==b); print(a[2:]+a[:2])'ポートの違いと逆方向送信元ポートが違うので別接続です。返信では同じ四要素の方向が逆転します。ポート8080だけでまとめると複数接続が混ざるため、通信履歴を読むときは両端の組を確認します。
まとめ
SYNは番号を一つ消費し、データはバイト数だけ番号を進めます。
ACKは次に期待する番号で、TCPのセグメント境界とアプリケーションの読出し境界は一致するとは限りません。
操作履歴と解説を振り返れます。
シェル / PCAPと通信の記録
tcpdump -n -tt -S -r connection.pcapreading from file connection.pcap, link-type EN10MB (Ethernet) 0.000000 IP 192.0.2.10.49152 > 198.51.100.20.8080: Flags [S], seq 1000, win 8192, options [mss 1460], length 0 0.010000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [S.], seq 9000, ack 1001, win 8192, options [mss 1460], length 0 0.020000 IP 192.0.2.10.49152 > 198.51.100.20.8080: Flags [.], ack 9001, win 8192, length 0 0.030000 IP 192.0.2.10.49152 > 198.51.100.20.8080: Flags [P.], seq 1001:1006, ack 9001, win 8192, length 5: HTTP 0.040000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [.], ack 1006, win 8192, length 0 0.050000 IP 192.0.2.10.49152 > 198.51.100.20.8080: Flags [P.], seq 1006:1012, ack 9001, win 8192, length 6: HTTP 0.060000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [.], ack 1012, win 8192, length 0
xxd -g 1 -c 16 syn.bin00000000: c0 00 1f 90 00 00 03 e8 00 00 00 00 60 02 20 00 ............`. . 00000010: a8 5b 00 00 02 04 05 b4 .[......
python3 -c 'from pathlib import Path; import struct; t=Path("syn.bin").read_bytes(); print(struct.unpack("!HHII",t[:12])); print((t[12]>>4)*4,hex(t[13]),struct.unpack("!H",t[22:24])[0])'(49152, 8080, 1000, 0) 24 0x2 1460
tcpdump -n -tt -S -vvv -r handshake.pcapreading from file handshake.pcap, link-type EN10MB (Ethernet)
0.000000 IP (tos 0x0, ttl 64, id 1, offset 0, flags [DF], proto TCP (6), length 44)
192.0.2.10.49152 > 198.51.100.20.8080: Flags [S], cksum 0xa85b (correct), seq 1000, win 8192, options [mss 1460], length 0
0.010000 IP (tos 0x0, ttl 64, id 1, offset 0, flags [DF], proto TCP (6), length 44)
198.51.100.20.8080 > 192.0.2.10.49152: Flags [S.], cksum 0x8522 (correct), seq 9000, ack 1001, win 8192, options [mss 1460], length 0
0.020000 IP (tos 0x0, ttl 64, id 1, offset 0, flags [DF], proto TCP (6), length 40)
192.0.2.10.49152 > 198.51.100.20.8080: Flags [.], cksum 0x9cdf (correct), seq 1001, ack 9001, win 8192, length 0python3 -c 'import json; rows=json.load(open("data.json")); print([(r["seq"],r["length"],r["seq"]+r["length"]) for r in rows if r["length"]]); print([r["ack"] for r in rows if r["direction"]=="S>C"])'[(1001, 5, 1006), (1006, 6, 1012)] [1006, 1012]
python3 -c 'import json; from pathlib import Path; rows=json.load(open("data.json")); stream=b"".join(bytes.fromhex(r["data"]) for r in rows if r["direction"]=="C>S"); print(stream,stream==Path("stream.bin").read_bytes()); print(stream[:3],stream[3:])'b'HELLO WORLD' True b'HEL' b'LO WORLD'
python3 -c 'a=("192.0.2.10",49152,"198.51.100.20",8080); b=("192.0.2.10",49153,"198.51.100.20",8080); print(a==b); print(a[2:]+a[:2])'False
('198.51.100.20', 8080, '192.0.2.10', 49152)演習完了