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

TCPの接続終了とリセット

片側の送信終了から両端の終了までを追い、中断と比較します。

説明

TCPは双方向の通信なので、一方が送信を終えても反対方向のデータが残ることがあります。正常終了のFINと中断のRSTを、フラグだけでなく番号と両端の状態で確認します。

11バイト送信したクライアントがFINを送り、その後サーバーの3バイトを受け取る履歴を解析します。状態遷移と最終ACKの再送を確認した後、同じ時点からRSTで中断した別の履歴と比較します。

1接続開始から終了までの全体

この接続ではクライアントが11バイトを送り、その送信側を閉じた後にサーバーからOKと改行の3バイトを受け取ります。

tcpdump -n -tt -S一つの接続の全履歴 -r complete.pcap開始・データ・終了

クライアントの次の番号1012でFINを送り、その後もサーバーから9001:9004のデータが届いています。片側の送信終了と、接続全体の終了は同じ時点ではありません。

2FIN後にも届くデータ

終了部分だけを表示し、クライアントFINとサーバーのデータの順を確認します。

tcpdump -n -tt -S -A終了フラグとデータ -r half-close.pcap半閉鎖の記録

最初のFINに対するACKは1013です。その後サーバーはOKと改行を送ります。クライアントが送信方向だけを閉じて受信を続ける半閉鎖なら、この応答を読み取れます。FINを受けた側がただちに自分の送信まで終了する必要はありません。

3FINが消費する番号

FINを持つ行を取り出し、sequenceにデータ長と1を加えます。

python3 -cFINの番号を計算 'import json; rows=json.load(open("half-close.json")); print([(r["direction"],r["seq"],r["seq"]+r["length"]+1) for r in rows if r["flags"]&1])'両方向のFINと次の番号

クライアントFIN1012にはACK1013、サーバーFIN9004にはACK9005です。FINもSYNと同じく番号を一つ消費します。この例ではFINとデータは別セグメントですが、同じセグメントに入る場合もあります。

4両端の状態を対応させる

states.csvはこの通常終了のイベントとTCP状態を整理した表です。

cat states.csv両端の状態遷移

クライアントはFIN_WAIT_1からFIN_WAIT_2へ進み、相手のFINを受けるとTIME_WAITになります。サーバーはCLOSE_WAITから、自分のFIN送信でLAST_ACKへ進み、最終ACKを受けてCLOSEDです。CLOSE_WAITが長く残る場合には、受信FIN後のアプリケーション側の終了処理も調べます。

5最後のACKが失われた場合

サーバーのFIN9004を再受信したときに返すACKを計算します。

python3 -c再受信FINへの確認番号 'fin_seq=9004; fin_data_length=0; ack=(fin_seq+fin_data_length+1)%(2**32); print(ack)'番号空間内で次を求める

再びACK9005を返せます。TIME_WAITは、最後のACKを再送できる状態を保ち、古いセグメントが後の接続へ混入するのを避ける役割があります。標準の待機は2MSLで、固定の「何秒」と一律には読めません。

6RSTで中断する別の結末

reset.pcapは同じ11バイト送信後、通常終了の代わりにサーバーがRSTを送った別シナリオです。

tcpdump -n -tt -S -vvv中断フラグと番号 -r reset.pcapRSTで終わる別シナリオ

flags RとACKが付き、sequence9001です。この時点でクライアントが期待するサーバー側の次番号と一致しています。受理されるRSTは接続を中断し、FINによる両方向の正常終了とは違う扱いになります。アプリケーションのエラー表示はAPIや読出し時点にも依存します。

7FINとRSTのフラグを照合する

二つの記録から、FINビットとRSTビットの有無を取り出します。

python3 -c終了フラグを比較 'import json; fin=json.load(open("half-close.json"))[0]; reset=json.load(open("reset.json"))[0]; print([(hex(r["flags"]),bool(r["flags"]&1),bool(r["flags"]&4)) for r in (fin,reset)])'FIN・RST・ACKの組合せ

FIN/ACKは0x11でFINだけが立ち、RST/ACKは0x14でRSTだけが立ちます。FINはそれまでのデータの後に送信終了を知らせますが、RSTはそのような正常終了を意味しません。どちらもACKが付く場合があるため、ACKだけを見て終了の種類を判断しません。

まとめ

FINで送信方向を閉じても、反対方向のデータを受け取る半閉鎖が可能です。

TIME_WAITと正常終了の確認、RSTによる中断を、フラグ・番号・状態で照合します。

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

シェル / PCAPと通信の記録

1
tcpdump -n -tt -S -r complete.pcap
reading from file complete.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
0.070000 IP 192.0.2.10.49152 > 198.51.100.20.8080: Flags [F.], seq 1012, ack 9001, win 8192, length 0
0.080000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [.], ack 1013, win 8192, length 0
0.090000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [P.], seq 9001:9004, ack 1013, win 8192, length 3: HTTP
0.100000 IP 192.0.2.10.49152 > 198.51.100.20.8080: Flags [.], ack 9004, win 8192, length 0
0.110000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [F.], seq 9004, ack 1013, win 8192, length 0
0.120000 IP 192.0.2.10.49152 > 198.51.100.20.8080: Flags [.], ack 9005, win 8192, length 0
2
tcpdump -n -tt -S -A -r half-close.pcap
reading from file half-close.pcap, link-type EN10MB (Ethernet)
0.000000 IP 192.0.2.10.49152 > 198.51.100.20.8080: Flags [F.], seq 1012, ack 9001, win 8192, length 0
E..(..@.@.N}...
.3d...........#)P. ...........
0.010000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [.], ack 1013, win 8192, length 0
E..(..@.@.N}.3d....
......#)....P. ...........
0.020000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [P.], seq 9001:9004, ack 1013, win 8192, length 3: HTTP
E..+..@.@.Nz.3d....
......#)....P. .C}..OK
...
0.030000 IP 192.0.2.10.49152 > 198.51.100.20.8080: Flags [.], ack 9004, win 8192, length 0
E..(..@.@.N}...
.3d...........#,P. ...........
0.040000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [F.], seq 9004, ack 1013, win 8192, length 0
E..(..@.@.N}.3d....
......#,....P. ...........
0.050000 IP 192.0.2.10.49152 > 198.51.100.20.8080: Flags [.], ack 9005, win 8192, length 0
E..(..@.@.N}...
.3d...........#-P. ...........
3
python3 -c 'import json; rows=json.load(open("half-close.json")); print([(r["direction"],r["seq"],r["seq"]+r["length"]+1) for r in rows if r["flags"]&1])'
[('C>S', 1012, 1013), ('S>C', 9004, 9005)]
4
cat states.csv
EVENT,CLIENT,SERVER
initial,ESTABLISHED,ESTABLISHED
client_FIN_sent,FIN_WAIT_1,ESTABLISHED
server_FIN_received,FIN_WAIT_1,CLOSE_WAIT
client_FIN_ACK_received,FIN_WAIT_2,CLOSE_WAIT
server_FIN_sent,FIN_WAIT_2,LAST_ACK
client_server_FIN_received,TIME_WAIT,LAST_ACK
server_final_ACK_received,TIME_WAIT,CLOSED
2MSL_expired,CLOSED,CLOSED
5
python3 -c 'fin_seq=9004; fin_data_length=0; ack=(fin_seq+fin_data_length+1)%(2**32); print(ack)'
9005
6
tcpdump -n -tt -S -vvv -r reset.pcap
reading from file reset.pcap, link-type EN10MB (Ethernet)
0.000000 IP (tos 0x0, ttl 64, id 1, offset 0, flags [DF], proto TCP (6), length 40)
    198.51.100.20.8080 > 192.0.2.10.49152: Flags [R.], cksum 0x9cd0 (correct), seq 9001, ack 1012, win 8192, length 0
7
python3 -c 'import json; fin=json.load(open("half-close.json"))[0]; reset=json.load(open("reset.json"))[0]; print([(hex(r["flags"]),bool(r["flags"]&1),bool(r["flags"]&4)) for r in (fin,reset)])'
[('0x11', True, False), ('0x14', False, True)]

演習完了