掴んで離さぬコネクション。どうも、かわしんです。しがみつかずに適切なタイミングで離しましょう。
この1週間で RFC を読みながら TCP/IP プロトコルスタックを自作した 1 のですが、その時にコネクションの終了処理でハマったので後学のために書き残しておきます。
一言でまとめると FIN -> FIN/ACK -> ACK
は間違っていて、正しくは FIN/ACK -> FIN/ACK -> ACK
であったという話です。
ちなみに、僕が自作した TCP/IP プロトコルスタックはこれです。
現象
それは TCP のリスナーと close 処理が出来上がってコネクション管理のテストをしていた時のことでした。
自作 TCP スタックでポートを Listen
して Accept
したらすぐにサーバ側からコネクションを切断するというテストコードを書いて実行し、Linux 内の telnet から接続して即座に接続が切断されるかどうかをテストしていました。
しかし、接続には成功するのですが なぜか接続は切断されません 。サーバからは確かに FIN
セグメントが送られているのに telnet からは ACK
や FIN/ACK
が送信されていないのです。 netstat で確認しても telnet のコネクションは ESTABLISHED 状態のままです。
逆に telnet から切断する時のパケットを観察すると切断時には FIN
セグメントではなく FIN/ACK
セグメントを送っているようでした。そのため、自作プロトコルスタックの CLOSE 処理で FIN
ではなく FIN/ACK
セグメントを送るようにすると無事 telnet から FIN/ACK
が返ってきてコネクションの切断ができるようになりました 。
RFC を読み直す
TCP の基本的な仕様は RFC 793 にまとめられていますが、その中の 3.9 Event Processing の CLOSE Call では、終了命令がユーザから来た時に FIN
セグメントを送信するように規定しています。
ESTABLISHED STATE Queue this until all preceding SENDs have been segmentized, then form a FIN segment and send it. In any case, enter FIN-WAIT-1 state.
僕はこの規定を読んで実装していたのですが、うまく動きません。
そこで 3.5 Closing a Connection も読むとその中の実例ではコネクションを切断する側は、FIN
ではなく FIN/ACK
を送っています。
TCP A TCP B 1. ESTABLISHED ESTABLISHED 2. (Close) FIN-WAIT-1 --> <SEQ=100><ACK=300><CTL=FIN,ACK> --> CLOSE-WAIT 3. FIN-WAIT-2 <-- <SEQ=300><ACK=101><CTL=ACK> <-- CLOSE-WAIT 4. (Close) TIME-WAIT <-- <SEQ=300><ACK=101><CTL=FIN,ACK> <-- LAST-ACK 5. TIME-WAIT --> <SEQ=101><ACK=301><CTL=ACK> --> CLOSED 6. (2 MSL) CLOSED Normal Close Sequence Figure 13.
どうやら FIN/ACK
を送ることが想定されているようです。
次に受信側の仕様を見てみます。3.9 Event Processing の SEGMENT ARRIVES をみると Step 5 の fifth check the ACK field
では以下のように記述されています。
fifth check the ACK field, if the ACK bit is off drop the segment and return
FIN フラグのチェックは、eighth, check the FIN bit,
と Step 8 であるため、そこに到達する前に ACK の付いてないセグメントは無視されてしまう ようです。
そのため、コネクションの終了処理では、FIN
ではなく FIN/ACK
を送らなくてはなりません。 それなら FIN segment
を送ると書かずに FIN/ACK
を送るというように明確に書いておいてくれ
まとめ
FIN
-> FIN/ACK
-> ACK
のフローは、正しくは FIN/ACK
-> FIN/ACK
-> ACK
でした。
ESTABLISHED 状態になった後は基本的に全てのセグメントに ACK
をつける必要があります。FIN
単体のセグメントは不正なセグメントとみなされて無視されてしまいます。
StackOverflow でも同じような質問がされていました。( linux - FIN omitted, FIN-ACK sent - Stack Overflow )その回答では以下のように説明されています。
1. client: FIN (will not send more) 2. server: ACK (received the FIN) .. server: sends more data..., client ACKs these data 3. server: FIN (will not send more) 4. client: ACK (received the FIN)
Note that the packet you see in step#1 might have an ACK inside too. But this ACK just acknowledges data send before by the server.
FIN
には ACK
をつけることができると言っていますが 大嘘 です。むしろ ACK は必須で、つけないと無視されてしまいます。 RFC をよく読めばわかることです。(僕もよく読んでないので人のことはあまり言えませんが)
世の中には雰囲気で回答して雰囲気で理解して approve していることが多いのだなと感じます。ちゃんと 1 次ソースをあたらないと嘘の情報に惑わされてしまうといういい教訓になりました。