kawasin73のブログ

技術記事とかいろんなことをかくブログです

FIN -> FIN/ACK -> ACK という TCP の幻想

掴んで離さぬコネクション。どうも、かわしんです。しがみつかずに適切なタイミングで離しましょう。

この1週間で RFC を読みながら TCP/IP プロトコルスタックを自作した 1 のですが、その時にコネクションの終了処理でハマったので後学のために書き残しておきます。

一言でまとめると FIN -> FIN/ACK -> ACK は間違っていて、正しくは FIN/ACK -> FIN/ACK -> ACK であったという話です。

ちなみに、僕が自作した TCP/IP プロトコルスタックはこれです。

github.com

現象

それは TCP のリスナーと close 処理が出来上がってコネクション管理のテストをしていた時のことでした。

自作 TCP スタックでポートを Listen して Accept したらすぐにサーバ側からコネクションを切断するというテストコードを書いて実行し、Linux 内の telnet から接続して即座に接続が切断されるかどうかをテストしていました。

しかし、接続には成功するのですが なぜか接続は切断されません 。サーバからは確かに FIN セグメントが送られているのに telnet からは ACKFIN/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 次ソースをあたらないと嘘の情報に惑わされてしまうといういい教訓になりました。