tcpdumpで理解するTCPのコネクション確立

本エントリでは、tcpdumpコマンドを使用してコネクションの確立、終了を確認することにより、TCPのコネクションの理解を深めます。

## TCPのコネクションを理解する目的

TCPのコネクションを理解すると主に以下のメリットがあります。

* NW関連(主にTCPコネクションまわり)の問題切り分け・トラブルシューティング力が向上する。
* NW関連(主にTCPコネクションまわり)のテストケース観点を習得できる。
* NW関連の共通言語が習得できる。

## TCPのコネクション

TCPでデータを転送する場合、データの転送を行う前にコネクションを確立する必要があります。
TCPのコネクションを確立する際は、以下の3way handshake(3ウェイ・ハンドシェイク)で処理が行われます。

1. TCPクライアント(TCP A)は、TCPサーバー(TCP B)にコネクション確立要求を送信する。(syn)
2. コネクション確立要求を受けたTCPサーバー(TCP B)は、TCPクライアント(TCP A)に確認応答とコネクション確立要求を返します。(syn+ack)
3. コネクション確立要求を受けたTCPクライアント(TCP A)は、TCPサーバー(TCP B)に確認応答を返します。(ack)

![established.png](https://qiita-image-store.s3.amazonaws.com/0/22861/d4a2adbd-3d1e-bb00-3ed6-134f18674ce7.png)


3way handshake(3ウェイ・ハンドシェイク)の詳細については以下のURLを参照ください。

* https://tools.ietf.org/html/rfc793#section-3.4
* https://ja.wikipedia.org/wiki/Transmission_Control_Protocol#コネクション確立

上記のパケットの流れをtcpdumpコマンドで実際に確認して行きます。

## tcpdumpで理解するTCPのコネクション確立

### 0. 前準備 

#### 使用するネットワークコマンド

本エントリでは、以下のLinuxコマンドを使用します。

* tcpdump
 * ネットワーク上のパケットを出力するために使用します。
* nc,telnet
 * tcpサーバー/クライアントとして使用します。
* netstat
 * コネクションの状態を確認するために使用します。

上記コマンドがインストールされていない場合、以下のコマンドでインストールします。

```
$ sudo yum -y install tcpdump nc telnet net-tools 
```

### 1. TCPサーバの起動

ncコマンドのlオプションを使用し任意のport番号でTCPサーバを起動します。ここでは12345番を使います。

```
$ nc -lk 12345
```

別ターミナルを開き、netstatコマンドで12345 portがLISTENされている事を確認します。

```
$ netstat -anp | grep 12345
tcp  0  0  0.0.0.0:12345  0.0.0.0:*  LISTEN  <PID>/nc             
```

* 1列目はprotcolを示します。tcpである事を確認してください。
* 4列目はlocalアドレスとportを示します。portが12345である事を確認してください
* 6列目はソケットの状態を示します。LISTENである事を確認してください。

これで、TCPコネクションを受け付ける準備が完了しました。

### 2. TCPクライアントの起動、コネクション確立。

telnetまたはncコマンドでTCPクライアントを起動し、ローカルで起動している1.のTCPサーバに接続します。

```
$ telnet 127.0.0.1 12345
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.

```

別ターミナルを開き、netstatコマンドでncとtelnetのコネクションが確立されている(ESTABLISHになっている)事を確認します。

```
$ netstat -anp | grep 12345
tcp  0  0 0.0.0.0:12345     0.0.0.0:*        LISTEN      <PID>/nc
tcp  0  0 127.0.0.1:<port>  127.0.0.1:12345  ESTABLISHED <PID>/telnet
tcp  0  0 127.0.0.1:12345   127.0.0.1:<port> ESTABLISHED <PID>/nc 
```

2行目がTCPクライアントであるtelnetのプロセスのソケットです。
* 5列目はForeignアドレスを示します。ncのLISTENしている12345 portである事を確認してください。
* 6列目はソケットの状態を示します。ESTABLISHである事を確認してください。

telnet実行中のターミナルに戻り、Ctrl+[とquitコマンドで接続をCloseします。

```
$ telnet 127.0.0.1 12345
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
^]
telnet> quit
Connection closed.
$ 
```

これで、コネクションの確立ができることのテストが完了しました。

### 3. tcpdumpでコネクション確立の3wayhandshake(3ウェイ・ハンドシェイク)を目視する

1.で起動したTCPサーバが起動している事を確認します。

```
$ netstat -anp | grep 12345
tcp  0  0  0.0.0.0:12345  0.0.0.0:*  LISTEN  <PID>/nc             
```

tcpdumpコマンドでlocal interfaceのport12345を指定して実行します。

```
$ sudo tcpdump -i lo -nnn port 12345
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes
```

telnetまたはncコマンドでTCPクライアントを起動し、ローカルで起動している1.のTCPサーバに接続します。

```
$ telnet 127.0.0.1 12345
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.

```

tcpdumpの出力を確認します。

```
08:52:48.685189 IP 127.0.0.1.<port> > 127.0.0.1.12345:  Flags [S], seq ...
08:52:48.685221 IP 127.0.0.1.12345  > 127.0.0.1.<port>: Flags [S.], seq ...
08:52:48.685237 IP 127.0.0.1.<port> > 127.0.0.1.12345:  Flags [.], ack ...
```

上記のtcpdumpの出力フォーマットは以下です。

```
timestamp src > dst: flags  
```
* timestamp: タイムスタンプ
* src: 送信元IP.ポート
* dst: 送信先IP.ポート
* flags: フラグ S (SYN), ‘.’ (ACK)

上記のパケットキャプチャはTCPクラアント・サーバーのそれぞれ以下の振る舞いを示します。

1. TCPクライアント(telnet)がTCPサーバー(nc)にSYNパケットを送信する。(SYN-SENT)
2. TCPサーバー(nc)がTCPクライアント(telnet)にSYN+ACKパケットが送信する。(SYN-RECEIVED)
3. TCPクライアント(telnet)がTCPサーバー(nc)にACKパケットが送信する。(ESTABLISH)

以下はシーケンスの概要図です。

![tcp3wayhandshack.png](https://qiita-image-store.s3.amazonaws.com/0/22861/4f5abd9f-55b1-4e14-544b-c75bf4cbe9fe.png)

別ターミナルを開き、netstatコマンドでncとtelnetのコネクションが確立されている(ESTABLISHになっている)事を確認します。

```
$ netstat -anp | grep 12345
tcp  0  0 0.0.0.0:12345     0.0.0.0:*        LISTEN      <PID>/nc
tcp  0  0 127.0.0.1:<port>  127.0.0.1:12345  ESTABLISHED <PID>/telnet
tcp  0  0 127.0.0.1:12345   127.0.0.1:<port> ESTABLISHED <PID>/nc 
```

### 4. tcpdumpでコネクションの終了を目視する

tcpdumpコマンドは終了しないまま、telnet実行中のターミナルに戻り、Ctrl+[とquitコマンドで接続をCloseします。

```
$ telnet 127.0.0.1 12345
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
^]
telnet> quit
Connection closed.
$ 
```

tcpdumpの出力を確認します。

```
09:11:41.290540 IP 127.0.0.1.<port> > 127.0.0.1.12345:  Flags [F.], seq 1,
09:11:41.290800 IP 127.0.0.1.12345  > 127.0.0.1.<port>: Flags [F.], seq 1,
09:11:41.290814 IP 127.0.0.1.<port> > 127.0.0.1.12345:  Flags [.], ack 2,
```

上記のtcpdumpの出力フォーマットは以下です。

```
timestamp src > dst: flags  
```
* timestamp: タイムスタンプ
* src: 送信元IP.ポート
* dst: 送信先IP.ポート
* flags: フラグ F (FIN), ‘.’ (ACK)

TCPクライアント(telnet)、TCPサーバー(nc)双方で、FIN/ACKを送信し、コネクションを終了します。

### シーケンスソース

```
note left of TCP A: CLOSED
note right of TCP B: LISTEN
TCP A->TCP B: syn
note left of TCP A: SYN-SENT
note right of TCP B: SYN-RECEIVED
TCP B->TCP A: syn + ack
note left of TCP A: ESTABLISHED
TCP A->TCP B: ack
note right of TCP B: ESTABLISHED
note over TCP A ,TCP B: ESTABLISHED
```

## おまけ: straceコマンドでTCPのコネクション確立を確認する

straceコマンドに本エントリで使用したnc/telnetコマンドを渡して、コネクション確立要求を送信し、システムコールを確認します。

nc側では、socket, bind, listen, acceptシステムコールが確認できます。

```
$ strace -Ttt -e trace=network nc -l 12345 
15:47:30.162392 socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3 <0.000017>
15:47:30.162494 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 <0.000010>
15:47:30.162529 setsockopt(3, SOL_SOCKET, SO_REUSEPORT, [1], 4) = 0 <0.000008>
15:47:30.162556 bind(3, {sa_family=AF_INET, sin_port=htons(12345), sin_addr=inet_addr("0.0.0.0")}, 16) = 0 <0.000011>
15:47:30.162592 listen(3, 1)            = 0 <0.000011>
15:47:30.162621 accept(3, {sa_family=AF_INET, sin_port=htons(48162), sin_addr=inet_addr("127.0.0.1")}, [16]) = 4 <3.104111>
15:47:41.423396 shutdown(4, SHUT_RD)    = 0 <0.000009>
15:47:41.423670 +++ exited with 0 +++
$ 
──────────────────────────
```

telnet側では、socket, connectシステムコールが確認できます。

```
$ strace -Ttt -e trace=network telnet 127.0.0.1 12345
Trying 127.0.0.1...
15:47:33.266522 socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3 <0.000018>
15:47:33.266617 setsockopt(3, SOL_IP, IP_TOS, [16], 4) = 0 <0.000010>
15:47:33.266650 connect(3, {sa_family=AF_INET, sin_port=htons(12345), sin_addr=inet_addr("127.0.0.1")}, 16) = 0 <0.000116>
Connected to 127.0.0.1.
Escape character is '^]'.
15:47:33.266976 setsockopt(3, SOL_SOCKET, SO_OOBINLINE, [1], 4) = 0 <0.000008>

telnet> quit
15:47:41.423280 shutdown(3, SHUT_RDWR)  = 0 <0.000171>
Connection closed.
15:47:41.424360 +++ exited with 0 +++
$ 
```