TCPのTIME-WAITを温かく見守る

# tl;dr

TCP における `TIME-WAIT` は悪い子ではないですが誤解されがちです。みんな仲良くしてあげましょう。
なお、これを書いている私自身も誤解している可能性があるので、それに気づいた方はご指摘いただければ思います。
また、kernel option の話には踏み込みません。

# `TIME-WAIT` を含む状態遷移

`ss` (`netstat`) で `TIME_WAIT` のコネクションが多数表示された、トラブルでは?というようなことを気にされる現場も多いと思います。この `TIME_WAIT` というのは TCP の状態の一つですが、いろいろと誤解されがちなかわいそうな状態でもあります。

まずは前提として、TCP の状態遷移図を [RFC 793](https://tools.ietf.org/html/rfc793) から引用します。

```
                              +---------+ ---------\      active OPEN
                              |  CLOSED |            \    -----------
                              +---------+<---------\   \   create TCB
                                |     ^              \   \  snd SYN
                   passive OPEN |     |   CLOSE        \   \
                   ------------ |     | ----------       \   \
                    create TCB  |     | delete TCB         \   \
                                V     |                      \   \
                              +---------+            CLOSE    |    \
                              |  LISTEN |          ---------- |     |
                              +---------+          delete TCB |     |
                   rcv SYN      |     |     SEND              |     |
                  -----------   |     |    -------            |     V
 +---------+      snd SYN,ACK  /       \   snd SYN          +---------+
 |         |<-----------------           ------------------>|         |
 |   SYN   |                    rcv SYN                     |   SYN   |
 |   RCVD  |<-----------------------------------------------|   SENT  |
 |         |                    snd ACK                     |         |
 |         |------------------           -------------------|         |
 +---------+   rcv ACK of SYN  \       /  rcv SYN,ACK       +---------+
   |           --------------   |     |   -----------
   |                  x         |     |     snd ACK
   |                            V     V
   |  CLOSE                   +---------+
   | -------                  |  ESTAB  |
   | snd FIN                  +---------+
   |                   CLOSE    |     |    rcv FIN
   V                  -------   |     |    -------
 +---------+          snd FIN  /       \   snd ACK          +---------+
 |  FIN    |<-----------------           ------------------>|  CLOSE  |
 | WAIT-1  |------------------                              |   WAIT  |
 +---------+          rcv FIN  \                            +---------+
   | rcv ACK of FIN   -------   |                            CLOSE  |
   | --------------   snd ACK   |                           ------- |
   V        x                   V                           snd FIN V
 +---------+                  +---------+                   +---------+
 |FINWAIT-2|                  | CLOSING |                   | LAST-ACK|
 +---------+                  +---------+                   +---------+
   |                rcv ACK of FIN |                 rcv ACK of FIN |
   |  rcv FIN       -------------- |    Timeout=2MSL -------------- |
   |  -------              x       V    ------------        x       V
    \ snd ACK                 +---------+delete TCB         +---------+
     ------------------------>|TIME WAIT|------------------>| CLOSED  |
                              +---------+                   +---------+

                      TCP Connection State Diagram
                               Figure 6.
```

`TIME-WAIT` という状態が生じる主な遷移は、`ESTABLISHED` -> `FIN-WAIT-1` -> `FIN-WAIT-2` -> `TIME-WAIT` -> `CLOSE` という下半分左側から右下隅に至る遷移です。要するにこれは、

1. 接続中の状態において(`ESTABLISHED`)、
2. 通信をしている一方のノードが先に「このコネクションにもう送るデータ無いわ」と主張し(`FIN-WAIT-1`への遷移)、
3. 他方のノードが当該ノードにその主張を受け取ったことを返答(`ACK`)し (`FIN-WAIT-2`への遷移)、
4. 他方のノードも「自分にも送るデータ無くなったから、おれもコネクション切断して良いと思うわ」と同意し (`TIME-WAIT`への遷移)
5. 一定時間待ってコネクションが切断される (`CLOSE`への遷移)

という、「最初に切断を要求する」側のノードの状態遷移を示しており、Active-Close と呼ばれます。
`TIME-WAIT` を含む状態遷移は「両ノードがほぼ同時に切断要求をしたとき」にも起こりえますが、これはまぁ RFC 793 を読んでください。

# `TIME-WAIT` の存在目的

`TIME-WAIT` が誤解されがちなのは、その出現率の高さと量に起因すると思います。

まずは `TIME-WAIT` の目的を整理しましょう。ぼくの理解している限り、 `TIME-WAIT` の目的は 2 つあります。

1. wondering duplicate 問題への対策
2. TCP 切断に至るフローの最後の ACK が届かないケースへの対策

## Wandering Duplicate 問題への対策

前提として、TCP のコネクションは (srcAddr, srcPort, dstAddr, dstPort) の4 つ組で識別されます。そして、そのコネクションの中でやり取りされるデータグラムは、Sequence Number (SN)で識別されます。

ここで思考実験として、2つのノード間で確立されたコネクション A があり、以下の状況になったことを考えます。

1. コネクション A で送信された SN 100 のセグメントだけが、地球を一周する経路をたどってしまった
2. 地球一周の間に、コネクションAが切断され、同じノード間で同じ 4 つ組の値を持つコネクション A' が確立した
3. コネクション A' 確立中に、地球一周を終えたコネクション A のセグメント(SN 100)が宛先ノードに到着した

SN100 はコネクション A のデータを保持しており、本来 コネクション A' とは無関係です。しかし、TCP においてアンラッキーが発生した場合、これは正常なパケットとして受け入れられてしまう場合があります。

`TIME-WAIT` の目的の一つは、これを抑止することです。

問題の一因は、コネクション A のデータグラムが旅行をしている間に、コネクション A の切断が完了してしまうことにあります。であれば、コネクション A の切断を「旅行が終わると考えられる妥当な時間」が経過するまで留保してしまえば良い。 `TIME-WAIT` は、コネクションが切断されたとみなす前にこの妥当な時間だけ待機することで、上記のような問題を起こさないようにしています。

### その時間って

セグメントがネットワークの中で滞留する最大時間は RFC の中で `Maximum Segment Lifetime` (`MSL`) として定められており、 [RFC 793](https://tools.ietf.org/html/rfc793) では以下のように述べられています。

> For this specification the MSL is taken to be 2 minutes.  This
>  is an engineering choice, and may be changed if experience indicates
>  it is desirable to do so.

Linux では…この値なんですかね…ちょっと自信がない…。

https://github.com/torvalds/linux/blob/v5.1/include/net/tcp.h#L121-L122

```c
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
                                  * state, about 60 seconds */
```

## TCP 切断に至るフローの最後の ACK が届かないケースへの対策

TCP は信頼性がある通信プロトコルであるので、「正しく切断されたのか」それとも「正しくないフローで切断されたのか」も判断したい。このため、TCP においては、何かを送信してきてきた相手に、必ず「届いたよ」という通知 (ACK) を返すようになっています。

そこで問題となるのが、「最後に送信した ACK が相手に届いたのかわからない問題」です。

Active-Close においては先に自身(A と呼びます)→通信相手(B と呼びます)に「データがないから切断して良い」という通知を送り、B も送信すべきデータがなくなると、それを A に伝えます。まずこの遷移を、A と B それぞれで整理してみましょう。

### A の立場

1. A は既に `FIN-WAIT-2` に遷移しています。
2. ここで B から「もう送るデータがない」という連絡が送られてくるので、それに対する ACK を返し、`TIME-WAIT` に遷移します。
3. 一定時間待って、`CLOSED` に遷移します。

### B の立場

1. B は既に A から「送るデータがない」という連絡を受けているので、`CLOSE-WAIT` 状態に遷移しています。
2. B は送るデータがなくなったので、「送るデータがなくなった」ことを A に伝え、`LAST-ACK` に遷移します。
3. A から ACK が返却されると、 `CLOSED` に遷移します。


### 問題

「A の立場」の 2、「B の立場」の 3. で述べる ACK が消失してしまった場合を考えます。しかし、A はそれに気づく術はありません。 `TIME-WAIT` を経て `CLOSED` に遷移し、コネクションは正常に切断されたものと見なすでしょう。

A は「コネクションを正しく切断した」と思い込んでいるので、B に対し同じ4つ組 (srdAddr, srcPort, dstAddr, dstPort) でコネクションを新たに確立することができます。しかし、B はその 4 つ組のコネクションを `LAST-ACK` で待ち続けているところに `SYN` が送られてくることになるので、不正な `SYN` としてコネクションを異常終了させてしまうでしょう。

一方で B は待っても待っても ACK が返ってこないので、A に対して 再送を行うことになります。ここでもし、`TIME-WAIT` がなければ、再送セグメントを受け取った A は、CLOSED 済みのコネクションにデータが送信されてきたので、B にエラー応答を返却し、B の方のコネクションは異常終了してしまうことになります。

これらの問題を回避するためにも、 `TIME-WAIT` は存在しています。

# まとめ

`TIME-WAIT` 状態での待ち時間がそれなりにあるため、この `TIME-WAIT` は `ESTABLISHED` とともに見る機会の多い状態であり、`WAIT` という響きもあって「なんで待ち状態のまま滞留しているんだ、バグか?」という考えにもなりやすいです。しかし、上記のように、`TIME-WAIT` は様々なトラブルを避けるために敢えて存在している状態です。基本的に悪さはしないはずなので、しばらく温かい目で、正常にコネクションが終了するまで見守ってあげて良いのかなと思います。

# 参考文献

1. [2.7 Please explain the TIME_WAIT state.](http://apodeline.free.fr/FAQ/Socket/socket-2.html#ss2.7)
2. [Coping with the TCP TIME-WAIT state on busy Linux servers](https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux)
3. [RFC 793](https://tools.ietf.org/html/rfc793)
4. [RFC 1337](https://tools.ietf.org/html/rfc1337)