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と通信の記録
tcpdump -n -tt -S -r complete.pcapreading 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
tcpdump -n -tt -S -A -r half-close.pcapreading 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. ...........
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)]cat states.csvEVENT,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
python3 -c 'fin_seq=9004; fin_data_length=0; ack=(fin_seq+fin_data_length+1)%(2**32); print(ack)'9005
tcpdump -n -tt -S -vvv -r reset.pcapreading 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 0python3 -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)]演習完了