【再】リバースプロキシの動きをパケットキャプチャで確認

前回の記事では、リバースプロキシをNginx で立て、リバースプロキシでパケットキャプチャで動きを見てみました。
今回はWEB サーバ側から見たパケットをキャプチャしてみます。
## 概要
バックエンドのWEB サーバでパケットキャプチャして、どんなリクエストが来ているか見る
## 作業
前回と同様に、wireshark をバックエンドサーバにインストールします。
次に、WEB サーバをpython で起動して待ち受けている状態にします。
そして、80 番ポートに来るパケットをキャプチャし保存します。
テストしたのは、
* 手元の端末のブラウザからリバースプロキシのIP アドレスにアクセス
* リバースプロキシからバックエンドに直接curl 
です。
一連のコマンドはこんな感じです。
ちなみに、リバプロが10.0.0.4 でバックエンドが10.0.1.4 です。
```
# dnf install wireshark
# python3 -m http.server 80 &
# tshark -i eth0 -f "port 80" -w /var/www/capture.pcap
Running as user "root" and group "root". This could be dangerous.
Capturing on 'eth0'
123 10.0.0.4 - - [14/Feb/2025 12:44:41] "GET / HTTP/1.0" 200 -
10.0.0.4 - - [14/Feb/2025 12:44:42] code 404, message File not found
10.0.0.4 - - [14/Feb/2025 12:44:42] "GET /favicon.ico HTTP/1.0" 404 -
169 10.0.0.4 - - [14/Feb/2025 12:45:04] "GET / HTTP/1.1" 200 -
195 ^C
#
```



:::note
* -w オプションでキャプチャを保存する
* -f "port XX" でポート指定
:::


## キャプチャの確認
保存したキャプチャを確認します。
```
# tshark -r /var/www/capture.pcap -V
```


:::note
* -r オプションで保存したキャプチャを確認する
* -V で詳細なキャプチャを見る
:::

-V オプションを付けると大量の情報が出てくるかと思います。
目的の通信であるリバプロとの間のパケット以外のものが多く含まれていて邪魔です。
フィルターをかけてみます。
どうやら次のようにすると、host 10.0.0.4 との間のhttp 通信だけを表示させられるようです（copilot の回答）。
```
tshark -r /var/www/capture.pcap -Y "http && ip.addr == 10.0.0.4" -V
```
出力結果は結構長いですが、それでも４個の通信に絞ることができました。
１個だけ取り出してみます。イーサネットフレームからはじまり、Internet Protocol (IP)、Transmission Control Protocol (TCP)、Hypertext Transfer Protocol (HTTP)のセクションがあることがわかります。
```
Frame 14: 481 bytes on wire (3848 bits), 481 bytes captured (3848 bits) on interface 0
    Interface id: 0 (eth0)
        Interface name: eth0
    Encapsulation type: Ethernet (1)
    Arrival Time: Feb 16, 2025 04:53:39.761000894 UTC
    [Time shift for this packet: 0.000000000 seconds]
    Epoch Time: 1739681619.761000894 seconds
    [Time delta from previous captured frame: 0.000046300 seconds]
    [Time delta from previous displayed frame: 0.060670877 seconds]
    [Time since reference or first frame: 0.063412954 seconds]
    Frame Number: 14
    Frame Length: 481 bytes (3848 bits)
    Capture Length: 481 bytes (3848 bits)
    [Frame is marked: False]
    [Frame is ignored: False]
    [Protocols in frame: eth:ethertype:ip:tcp:http]
Ethernet II, Src: AristaNe_bb:c7:5c (74:83:ef:bb:c7:5c), Dst: Microsof_62:e4:3c (60:45:bd:62:e4:3c)
    Destination: Microsof_62:e4:3c (60:45:bd:62:e4:3c)
        Address: Microsof_62:e4:3c (60:45:bd:62:e4:3c)
        .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)
        .... ...0 .... .... .... .... = IG bit: Individual address (unicast)
    Source: AristaNe_bb:c7:5c (74:83:ef:bb:c7:5c)
        Address: AristaNe_bb:c7:5c (74:83:ef:bb:c7:5c)
        .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)
        .... ...0 .... .... .... .... = IG bit: Individual address (unicast)
    Type: IPv4 (0x0800)
Internet Protocol Version 4, Src: 10.0.0.4, Dst: 10.0.1.4
    0100 .... = Version: 4
    .... 0101 = Header Length: 20 bytes (5)
    Differentiated Services Field: 0x00 (DSCP: CS0, ECN: Not-ECT)
        0000 00.. = Differentiated Services Codepoint: Default (0)
        .... ..00 = Explicit Congestion Notification: Not ECN-Capable Transport (0)
    Total Length: 467
    Identification: 0x8d5f (36191)
    Flags: 0x4000, Don't fragment
        0... .... .... .... = Reserved bit: Not set
        .1.. .... .... .... = Don't fragment: Set
        ..0. .... .... .... = More fragments: Not set
        ...0 0000 0000 0000 = Fragment offset: 0
    Time to live: 64
    Protocol: TCP (6)
    Header checksum: 0x96be [validation disabled]
    [Header checksum status: Unverified]
    Source: 10.0.0.4
    Destination: 10.0.1.4
Transmission Control Protocol, Src Port: 35398, Dst Port: 80, Seq: 1, Ack: 1, Len: 415
    Source Port: 35398
    Destination Port: 80
    [Stream index: 1]
    [TCP Segment Len: 415]
    Sequence number: 1    (relative sequence number)
    [Next sequence number: 416    (relative sequence number)]
    Acknowledgment number: 1    (relative ack number)
    1000 .... = Header Length: 32 bytes (8)
    Flags: 0x018 (PSH, ACK)
        000. .... .... = Reserved: Not set
        ...0 .... .... = Nonce: Not set
        .... 0... .... = Congestion Window Reduced (CWR): Not set
        .... .0.. .... = ECN-Echo: Not set
        .... ..0. .... = Urgent: Not set
        .... ...1 .... = Acknowledgment: Set
        .... .... 1... = Push: Set
        .... .... .0.. = Reset: Not set
        .... .... ..0. = Syn: Not set
        .... .... ...0 = Fin: Not set
        [TCP Flags: ·······AP···]
    Window size value: 229
    [Calculated window size: 29312]
    [Window size scaling factor: 128]
    Checksum: 0x558a [unverified]
    [Checksum Status: Unverified]
    Urgent pointer: 0
    Options: (12 bytes), No-Operation (NOP), No-Operation (NOP), Timestamps
        TCP Option - No-Operation (NOP)
            Kind: No-Operation (1)
        TCP Option - No-Operation (NOP)
            Kind: No-Operation (1)
        TCP Option - Timestamps: TSval 1848653027, TSecr 2874087447
            Kind: Time Stamp Option (8)
            Length: 10
            Timestamp value: 1848653027
            Timestamp echo reply: 2874087447
    [SEQ/ACK analysis]
        [iRTT: 0.001505887 seconds]
        [Bytes in flight: 415]
        [Bytes sent since last PSH flag: 415]
    [Timestamps]
        [Time since first frame in this TCP stream: 0.001552187 seconds]
        [Time since previous frame in this TCP stream: 0.000046300 seconds]
    TCP payload (415 bytes)
Hypertext Transfer Protocol
    GET /favicon.ico HTTP/1.0\r\n
        [Expert Info (Chat/Sequence): GET /favicon.ico HTTP/1.0\r\n]
            [GET /favicon.ico HTTP/1.0\r\n]
            [Severity level: Chat]
            [Group: Sequence]
        Request Method: GET
        Request URI: /favicon.ico
        Request Version: HTTP/1.0
    Host: backend\r\n
    Connection: close\r\n
    Pragma: no-cache\r\n
    Cache-Control: no-cache\r\n
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36\r\n
    Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8\r\n
    Referer: http://130.33.25.44:8888/\r\n
    Accept-Encoding: gzip, deflate\r\n
    Accept-Language: ja,en-US;q=0.9,en;q=0.8\r\n
    \r\n
    [Full request URI: http://backend/favicon.ico]
    [HTTP request 1/1]

```
まず、IP のセクションで、Source: 10.0.0.4 でDestination: 10.0.1.4 となっています。
リバースプロキシは手元の端末からの通信を一回受け止めてから、新しくバックエンドサーバへ通信を行っていることが見て取れます。

そしてHTTP のセクションをみると、python で立てたWEB サーバからテストのテキストをGET で取ってるのがわかります。
どこにも私が使ったグローバルIP情報がないです。

結論として、**バックエンドサーバにとっては通信相手が全部リバースプロキシになっている**ことがわかります。

## じゃあどうやって通信相手を知るのか
Copilot に聞いてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617557/fca0703c-3e26-4f7f-88b6-7dec78044847.png)
3 の方法が興味深いですね。ネスぺにも出てたくらいなので定石なんでしょう。
Nginx のデフォルトではヘッダーに元の通信相手を記録する機能はないことが、先ほどのパケットキャプチャから推測されます。

Nginx のリバースプロキシに戻って、nginx.conf を確認します。Nginx の構成は[別記事](https://qiita.com/dessin/items/d2854a0025f6ea01f7b7)を参照してください。

conf ファイルの最初の方に次のような記述があります。ログの内容と、出力先を決めているようです。
```
http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;
```

:::note
$remote_addr: クライアントのIP アドレス
ログ出力先は /var/log/nginx/access.log 
:::

### Nginx のログ確認
まず、現状のNginx のアクセスログを見ましょう。とりあえず２行抜き出してみました。

```
X.X.X.X (私のIP) - - [04/Feb/2025:12:24:37 +0000] "GET / HTTP/1.1" 502 4020 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" "-"

X.X.X.X (私のIP) - - [07/Feb/2025:11:49:13 +0000] "GET / HTTP/1.1" 200 63 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" "-"
```
conf ファイルに記載されたログの内容と、上に貼った実際のログ内の値を対応付けます。
XFF は現状で空欄みたいです。
| ログの項目 | 値 |
|:-:|:-:|
|$remote_addr   |x.x.x.x (私のグローバルIP）   |
|$remote_user   | -  |
|$time_local   | [04/Feb/2025:12:24:37 +0000]   |
|$request   |  "GET / HTTP/1.1" |
|$status  | 502  |
|$body_bytes_Sent   |4020   |
|$http_referer   | "-"  |
|$http_user_agent   |"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"   |
|$http_x_forwarded_for   |"-"   |

とりあえず、デフォルトではNginx は自分だけ通信相手を知っていて、バックエンドサーバには教えていないということがわかりましたね。

### Nginx でXFF 追加する設定
nginx.conf に戻り、ヘッダ追加の設定を行います。
server の下のlocation に書きます。
```
    upstream backend {
        server 10.0.1.4;
    }

    server {
        listen       8888 default_server;
        listen       [::]:8888 default_server;
        server_name  _;
        root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / {
            proxy_set_header X-Forwarded-for $remote_addr;　<-----追記
            proxy_pass http://backend;    
        }
```
conf ファイルをリロードして、もう一回やってみます。

### XFF ヘッダ追記検証
再び、バックエンドサーバでパケットキャプチャとWEB サーバを起動します。
ブラウザからリバースプロキシのIP アドレスにアクセスして、WEB サーバのテストページが見れることを確認しました。
リバースプロキシからバックエンドサーバ宛てのパケットのHTTP 部分を抜粋します。
```
Hypertext Transfer Protocol
    GET /favicon.ico HTTP/1.0\r\n
        [Expert Info (Chat/Sequence): GET /favicon.ico HTTP/1.0\r\n]
            [GET /favicon.ico HTTP/1.0\r\n]
            [Severity level: Chat]
            [Group: Sequence]
        Request Method: GET
        Request URI: /favicon.ico
        Request Version: HTTP/1.0
    X-Forwarded-for: x.x.x.x\r\n　　　<-----私のグローバルIP
    Host: backend\r\n
    Connection: close\r\n
    Pragma: no-cache\r\n
    Cache-Control: no-cache\r\n
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML                                                                                     , like Gecko) Chrome/132.0.0.0 Safari/537.36\r\n
    Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8\r\n
    Referer: http://130.33.25.44:8888/\r\n
    Accept-Encoding: gzip, deflate\r\n
    Accept-Language: ja,en-US;q=0.9,en;q=0.8\r\n
    \r\n
    [Full request URI: http://backend/favicon.ico]
    [HTTP request 1/1]
```
WEB サーバに、X-Forwarded-for として通信相手のIP アドレスが見えました！
Nginx のログも見ておきましょう。
```
x.x.x.x - - [16/Feb/2025:04:28:11 +0000] "GET /favicon.ico HTTP/1.1" 404 469 "http://130.33.25.44:8888/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36" "-"
```
Nginx のログにはXFF 出てませんでした。そうか、こっちはNginx が受け取るHTTP リクエストのログが出てるのか。受け取った時点ではXFF はありません。


### ちょっとした疑問

もし、複数のバックエンドサーバに分散している場合、振り分け先がどうなったかのログはどこに出るのかしら。
軽く調べる限りでは、またNginx の追加の設定が必要です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617557/fe5c2fec-f76c-4d9a-86d9-2030b5fdf528.png)


## まとめ
* Nginx をリバースプロキシとして使う場合、デフォルトの設定ではWEB サーバには通信相手がわからない。XFF ヘッダを付ける設定などが追加で必要

* tshark でL7 の情報まで詳しく見るなら、-V オプションを使う


以上です。お疲れさまでした。