VPC Flow LogsのTCPフラグを真面目に調査してみた

## はじめに
社会人 20 年、AWS をお触りするようになり 10 年が経ちました。
これまで AWS の **VPC Flow Logs** のお世話になってきました。

しかし、通信の行きと戻りを意識したり、順次追加されてきた
カスタム形式のログフィールドについて、じっくりと観察したことがありませんでした。

ということで、今回はいくつかのシナリオでトラフィックを流してみて
特に **TCPフラグ**がどのような値になっているか見てみました。

**【参考】**
・ [VPC フローログを使用した IP トラフィックのログ記録][00]

## 利用環境
・ Private subnet に EC2 インスタンスを2台用意し、VPC Flow Logs は CloudWatch Logs に出力する構成としています。
・ vm01 から vm02 に対して、PING や SSH （SFTP） で通信できるようにセキュリティグループを設定しています。
・ VPC Flow Logs は VPC に対して作成しています。

**【構成図】**
<img width="1000" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211371/e34d0556-169d-8a08-b967-0649215ddfbc.png">

**【EC2情報】**
| # | 名前 | OS | Instance Type |instance-id | eni | IPアドレス |
|---|-----|----|---------------|----------|-------------|-----|
| 01 | vm01 | Amazon Linux 2 | t2.micro |i-0360afced64ba1444 | eni-0fb7dcfbbbb2f6573 | 10.0.129.221 |
| 02 | vm02 | Amazon Linux 2 | t2.micro |i-026cb4d7681325c92 | eni-0652da0ac0ca00dee | 10.0.130.11 |

**【セキュリティグループ設定】**
| 方向 | タイプ | プロトコル | ポート範囲 | ソース |
|-----|-------|---------|---------|-------|
| インバウンドルール | すべての ICMP - IPv4 | ICMP | すべて | 10.0.128.0/20 |
| インバウンドルール | SSH | TCP | 22 | 10.0.128.0/20 |
| アウトバウンドルール | すべてのトラフィック | すべて | すべて | 0.0.0.0/0 |

**【VPC FLow Logsの設定】**
| 項目 | 内容 |
|-----|------|
| フィルタ | すべて | |
| 最大集約間隔 | 1 分間 | |
| 送信先 | CloudWatch Logs に送信 |
| ログレコード形式 | カスタム形式 |
| ログ形式 | すべて選択（詳細は「形式のプレビュー」参照） |

```text:形式のプレビュー
${account-id} ${action} ${az-id} ${bytes} ${dstaddr} ${dstport} ${end} ${flow-direction} ${instance-id} ${interface-id} ${log-status} ${packets} ${pkt-dst-aws-service} ${pkt-dstaddr} ${pkt-src-aws-service} ${pkt-srcaddr} ${protocol} ${region} ${srcaddr} ${srcport} ${start} ${sublocation-id} ${sublocation-type} ${subnet-id} ${tcp-flags} ${traffic-path} ${type} ${version} ${vpc-id}
```

**【参考】**
・ [VPCフローログの取得方法（CloudWatch Logsに保存）][01]

## 実施内容
・vm01 から下記 3 つの通信を実施し、結果のログを CloudWatch Logs Insight でクエリしてみます。
1. PING 疎通
2. curl コマンドで Web アクセス
3. SFTP でファイル転送

### 1. PING 疎通
- まずはシンプルに vm01 から vm02 に対して、PING (毎秒 10,000 Bytes ) を実施しました。

```text:ping コマンド
[root@ip-10-0-129-221 bin]# ping -s 10000 10.0.130.11 
```

**【シーケンス図】**
<img width="700" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211371/0f3e36f0-81f2-201b-3d30-f49157e6fcae.png">

- 次に CloudWatch Logs Insight で確認します。
- [CloudWatch] > [ログ] > [ログのインサイト]を開きます。
- 画面上部で VPC Flow Logs を出力している **ロググループ** を選択します。(今回は `vpcflowlogs`)
- 下記のクエリ文で **[クエリの実行]** をおこないます。

**【参考】**
・ [CloudWatch Logs Insights のクエリ構文][02]

```text:PING 実行結果確認用のクエリ文
fields @timestamp
| parse '* * * * * * * * * * * * * * * * * * * * * * * * * * * * *' as `account-id`, action, `az-id`, bytes, dstaddr, dstport, end, `flow-direction`, `instance-id`, `interface-id`, logstatus, packets, `pkt-dst-aws-service`, pktdstaddr, `pkt-src-aws-service`, pktsrcaddr, protocol, region, srcaddr, srcport, start, sublocationid, sublocationtype, `subnet-id`, tcpflags, `traffic-path`, type, version, `vpc-id`
| display @timestamp, start, end, action, logstatus, srcaddr, srcport, dstaddr, dstport, `interface-id`, `flow-direction`, packets, bytes, protocol, tcpflags
| filter `instance-id` in ['i-0360afced64ba1444','i-026cb4d7681325c92']
| filter dstaddr in ['10.0.129.221','10.0.130.11']
| filter srcaddr in ['10.0.129.221','10.0.130.11']
| filter protocol=1
| sort @timestamp desc
```

**【補足説明】**
・ カスタム形式のログに合わせて、`parse`句でログを解析してフィールド抽出しています。
・ ログ分析に必要なフィールドだけに絞るため、`display`句で表示するフィールドを指定しています。
・ vm01 と vm02 間の通信に絞るため、`filter`句で インスタンス ID、宛先 IP、送信元 IPを使って絞っています。
・ ICMP 通信に絞るため、`filter`句でプロトコル番号を 1 に絞っています。
・ イベント発生時刻の新しい順に`sort`句で並び替えています。

**【クエリ結果】**
<img width="1000" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211371/6da3d47c-3fb7-912d-600f-412439f943f9.png">

**【考察】** 
・ フローログの最大集約間隔が 1 分間 のため、1 分以内の間隔に集約されています。
・ IP アドレスから #2 がエコー要求、 #1 がエコー応答のはずですが、情報がないため行きと帰りの判断ができませんでした。
・ ICMP 通信は TCP ではないため、当然`tcpflag`は`0`です。

### 2. curl コマンドで Web アクセス
- 事前に nslookup コマンドで`www.google.com`の名前解決を実施し、IP アドレスを確認しました。
- IP アドレスを確認した上で curl コマンドで`www.google.com`に Web アクセスを実施しました。

```text:curl コマンド
[root@ip-10-0-129-221 bin]# nslookup www.google.com
Server:         10.0.0.2
Address:        10.0.0.2#53

Non-authoritative answer:
Name:   www.google.com
Address: 172.217.175.36
Name:   www.google.com
Address: 2404:6800:4004:822::2004

[root@ip-10-0-129-221 bin]# curl www.google.com
```

**【シーケンス図】**
・ まずは、TCP スリーウェイハンドシェークによるコネクション接続開始フェーズです。
<img width="700" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211371/03a9b1b1-21dd-85b4-c5c9-acd876d530f9.png">

・ 次に GET でデータを受信します。
<img width="700" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211371/e121d726-7fd2-c1f3-003c-aea1b58b040b.png">

※ 公式ドキュメントの抜粋ですが、ACK のみの場合は`tcpflag`が`0`になります。
> フローログエントリが ACK パケットのみで構成されている場合、フラグ値は 16 ではなく 0 になります。

・ 最後に TCP コネクションを切断します。（腑に落ちない点がありますが、クエリ結果で後述します）
<img width="700" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211371/7d5e6d8a-792f-aff5-912e-a8163ad00590.png">

- CloudWatch Logs Insight において、下記内容で **[クエリの実行]** をおこないます。
 
```text:Web アクセス結果確認用のクエリ文
fields @timestamp
| parse '* * * * * * * * * * * * * * * * * * * * * * * * * * * * *' as `account-id`, action, `az-id`, bytes, dstaddr, dstport, end, `flow-direction`, `instance-id`, `interface-id`, logstatus, packets, `pkt-dst-aws-service`, pktdstaddr, `pkt-src-aws-service`, pktsrcaddr, protocol, region, srcaddr, srcport, start, sublocationid, sublocationtype, `subnet-id`, tcpflags, `traffic-path`, type, version, `vpc-id`
| display @timestamp, start, end, action, logstatus, srcaddr, srcport, dstaddr, dstport, `interface-id`, `flow-direction`, packets, bytes, protocol, tcpflags
| filter dstaddr in ['10.0.129.221','172.217.175.36']
| filter srcaddr in ['10.0.129.221','172.217.175.36']
| filter protocol=6
| sort @timestamp desc
```

**【補足説明】**
・ カスタム形式のログに合わせて、 `parse`句でログを解析してフィールド抽出しています。
・ ログ分析に必要なフィールドだけに絞るため、 `display`句で表示するフィールドを指定しています。
・ vm01 と `www.google.com` 間の通信に絞るため、`filter`句で 宛先 IP、送信元 IPを使って絞っています。
・ google の IP は、nslookup コマンドで名前解決した`172.217.175.36`としています。
・ TCP 通信に絞るため、`filter`句でプロトコル番号を 6 に絞っています。
・ イベント発生時刻の新しい順に `sort`句で並び替えています。

**【クエリ結果】**
<img width="1000" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211371/fa41adf4-d985-eb3a-8dd8-b0f345c4c55f.png">

**【考察】** 
・ フローログの最大集約間隔が 1分間 のため、1分以内の間隔に集約されています。
・ #2 の宛先ポートが`80`、TCP フラグが ❶SYN(`2`)+❺FIN(`1`)で合計`3`となっているため、行きの通信だと判断できます。
・ #1 の送信元ポートが`80`、TCP フラグが ❷SYN(`2`)+ACK(`16`)+❼FIN(`1`)で合計`19`となっているため、戻りの通信だと判断できます。
・　どうも❺と❼のTCP切断要求時のACK(`16`)は、計算に入らないようです。理由までは解明できませんでした。

※ 公式ドキュメントの抜粋ですが、集約間隔内だと各パケットの TCPフラグ は合計されます。
> TCP フラグは、集約間隔内に OR 処理することができます。短い接続の場合、フラグがフローログレコードの同じ行に設定されることがあります (例えば、SYN-ACK と FIN の場合は 19、SYN と FIN の場合は 3 など)。例については、「TCP フラグシーケンス」を参照してください。

**【参考】**
・ [TCPフラグ値 早見表][03]
・ [TCP three-way handshaking][04]

### 3. SFTP でファイル転送
- 最後に SFTP 接続を行い、410 Bytes の適用なテキストファイルを転送してみます。
- また、SFTP の切断はデータ転送後、6 分の間をおいてみました。

```text:sftp コマンド
[root@ip-10-0-129-221 tmp]# sftp hibino@10.0.130.11
hibino@10.0.130.11's password:
Connected to 10.0.130.11.
sftp> put test.txt
Uploading test.txt to /home/hibino/test.txt
test.txt                                                                                                       
100%  410   777.3KB/s   00:00

< 約 6 分放置 >

sftp> quit
```

**【シーケンス図】**
・ まずは、TCP スリーウェイハンドシェークによるコネクション接続開始フェーズです。
<img width="700" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211371/67059d47-9c01-ee3f-0d16-e0e81d51e090.png">

・ 次に`test.txt`のテキストデータを転送します。
<img width="700" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211371/fa6faa9b-a6d9-829e-93ca-1e52f8ad0f9c.png">

・ 約 6 分の間をおいて、 SFTP を切断します。
<img width="700" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211371/cd548d9c-b60b-39ab-cc63-4a1279f82ea3.png">

- CloudWatch Logs Insight において、下記内容で **[クエリの実行]** をおこないます。
 
```text:SFTP データ転送結果確認用のクエリ文
fields @timestamp
| parse '* * * * * * * * * * * * * * * * * * * * * * * * * * * * *' as `account-id`, action, `az-id`, bytes, dstaddr, dstport, end, `flow-direction`, `instance-id`, `interface-id`, logstatus, packets, `pkt-dst-aws-service`, pktdstaddr, `pkt-src-aws-service`, pktsrcaddr, protocol, region, srcaddr, srcport, start, sublocationid, sublocationtype, `subnet-id`, tcpflags, `traffic-path`, type, version, `vpc-id`
| display @timestamp, start, end, action, logstatus, srcaddr, srcport, dstaddr, dstport, `interface-id`, `flow-direction`, packets, bytes, protocol, tcpflags
| filter `instance-id` in ['i-0360afced64ba1444','i-026cb4d7681325c92']
| filter dstaddr in ['10.0.129.221','10.0.130.11']
| filter srcaddr in ['10.0.129.221','10.0.130.11']
| filter protocol=6
| sort @timestamp desc
```

**【補足説明】**
・ カスタム形式のログに合わせて、 `parse`句でログを解析してフィールド抽出しています。
・ ログ分析に必要なフィールドだけに絞るため、 `display`句で表示するフィールドを指定しています。
・ vm01 と vm02 間の通信に絞るため、`filter`句で インスタンス ID、宛先 IP、送信元 IPを使って絞っています。
・ TCP 通信に絞るため、`filter`句でプロトコル番号を 6 に絞っています。
・ イベント発生時刻の新しい順に `sort`句で並び替えています。

**【クエリ結果】**
<img width="1000" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211371/4db4e638-0e42-530a-a926-74b90648e156.png">

**【考察】** 
・ データ転送時の #7 と 8 では、ACK のみのため、やはり`tcpflag`は`0`でした。
・ SFTP 切断時の #1 〜 4 では、ENI ごとに ingress と egress で計 4 つありましたが、いずれも`tcpflag`は FYN(`1`)でした。

## まとめ
さて、いかがでしたでしょうか？

通信の方向は、ポート番号と TCP フラグを組み合わせてみることで判断できることが分かりました。
戻り通信の場合、TCP フラグ値は SNY/ACK(`2+16=18`)以上になっていることでも判断材料になります。
しかし、コネクション切断時の TCP フラグ値が FYN(`1`) のみであることは腑に落ちませんでした。

検証が終わった後に、確認した内容が全く同じように[公式ドキュメント][05]に書かれていて落ち込みました。


あとは最大集約間隔 1 分間でフローをまとめられてしまうため、正確な通信を把握したい場合は
やはり VPC Traffic Mirroring を使って、フルパケットキャプチャするしかないですね。

[00]: https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/flow-logs.html#flow-log-records "VPC フローログを使用した IP トラフィックのログ記録"
[01]: https://qiita.com/kerorinfather/items/64c3ca83017e521d9917 "VPCフローログの取得方法（CloudWatch Logsに保存）"
[02]: https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/CWL_QuerySyntax.html "CloudWatch Logs Insights のクエリ構文"
[03]: https://mymanfile.com/?p=4018 "TCPフラグ値 早見表"
[04]: https://www.infraexpert.com/study/tcpip9.html "TCP three-way handshaking"
[05]: https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/flow-logs-records-examples.html#flow-log-example-tcp-flag "TCP フラグシーケンス"