一覧NW-10-1 · TCPの接続と終了

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最初のSYN

c0 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])'ポート・番号・ヘッダー長・MSS

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

1
tcpdump -n -tt -S -r connection.pcap
reading 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
2
xxd -g 1 -c 16 syn.bin
00000000: 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                          .[......
3
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
4
tcpdump -n -tt -S -vvv -r handshake.pcap
reading 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 0
5
python3 -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]
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:])'
b'HELLO WORLD' True
b'HEL' b'LO WORLD'
7
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)

演習完了