laravel クライアントのIPアドレスを取得するヘルパ関数を書いてみる

## 概要

`$request->ip()`でIPを取得すると、ダイレクトにクライアントからサーバーにアクセスされている場合は問題ないが、リバプロやロードバランサーなどがあると「サーバー到達直線のリソースのIP」が取れてしまう。クライアントのIPを取得したい場合の処理を考えてみた。

## 注意

本内容は完璧ではない可能性があります。何なりとご指摘いただければと思っております。

## 処理

下記のような処理を考えてみた。今回は独自ヘルパ関数として登録している。

```helpers.php
if (!function_exists('getClientIp')) {
    function getClientIp(): array
    {
        $forwardedFor = request()->headers->get('X-Forwarded-For');
        $realIp = request()->headers->get('X-Real-IP');
        $clientIp = null;
        $isClientIpUnreliable = false;

        if ($forwardedFor) {
            $ipAddresses = explode(',', $forwardedFor);
            $clientIp = trim($ipAddresses[0]);
        } elseif ($realIp) {
            $clientIp = $realIp;
        }

        if (!$clientIp) {
            $clientIp = request()->ip();
            $isClientIpUnreliable = true;
        }

        return ['clientIp' => $clientIp, 'isClientIpUnreliable' => $isClientIpUnreliable];
    }
}
```

## 簡単説明

### ヘッダのX-Forwarded-Forキーの値を使ったIPの取得

下記の処理について簡単に説明する　。

```php
request()->headers->get('X-Forwarded-For');
```

上記の処理はリクエストヘッダのX-Forwarded-Forキーに紐づく値が返される。
X-Forwarded-Forキーに紐づく値というのはもちろん「IPアドレス」である。
ただし、X-Forwarded-Forに紐づく値は複数の「IPアドレスを表す文字列」が入っている可能性がある。
下記で詳しく説明する。

- 「クライアント（1.1.1.1） → laravelが動作するサーバー」の構成の場合
    - X-Forwarded-Forに紐づく値は`1.1.1.1`となる。
- 「クライアント（1.1.1.1） → リバプロ（2.2.2.2） → ロードバランサー(3.3.3.3) → laravelが動作するサーバー」の構成の場合
    - X-Forwarded-Forに紐づく値は`1.1.1.1,2.2.2.2,3.3.3.3`となる。

X-Forwarded-Forはリクエストが通ってきた道のりで関わったリソースのIPを持っており、後のIPになればなるほど（今回は`3.3.3.3`）サーバー側に近いリソースのIPとなる。それぞれのIPは`,`（コロン）で区切られる。区切られているだけで配列とかではなく「複数のIPの情報を持った文字列」である。

なので下記のような一見めんどくさいような処理をしている。

```php
$ipAddresses = explode(',', $forwardedFor); // X-Forwarded-Forに紐づく値を , 区切りで配列化
$clientIp = trim($ipAddresses[0]); // 配列の先頭のインデックスの値を得て、先頭・末尾のホワイトスペースを削除して返す。
```

### ヘッダのX-Real-IPキーの値を使ったIPの取得

下記の処理について簡単に説明する。

```php
request()->headers->get('X-Real-IP');
```

上記の処理はリクエストヘッダのX-Real-IPキーに紐づく値が返される。
X-Real-IPキーに紐づく値というのはこちらももちろんIPアドレスである。
よっぽど特殊なことをしていなければ一つのIPアドレスが入っている。
主にNginxでリバプロを作った時にクライアントのIPをリバプロがこのヘッダに入れてくれるらしい。

## まとめ

今回の処理は
1. 最も確からしいクライアントIPの取得を試みる（X-Forwarded-For）
1. 1.がだめなら精度はちょっと下がるけど別の方法でクライアントIPの取得を試みる（X-Real-IP）
1. ここまででクライアントIPが取れなければしょうがないのでサーバーに一番近いリソースのリクエストを送っているIPの取得を試みる（`$request->ip()`）※リクエスト送信ツールで特にヘッダに値入れずにリクエスト投げるとこれになる。

という感じである。

## 余談

どうやらちょっと細工をすれば`$request->ip()`でもある程度正確なクライアントIPアドレスを取れるらしい。
TrustProxiesミドルウェアの`$proxies`プロパティに開発者側が意図して設置したリバプロやロードバランサーのIPを指定する方法だ。（とはいっても結局、X-Forwarded-Forのヘッダをみて、`$proxies`プロパティのIP取り除いてクライアントIPと思われる文字列を`$request->ip()`で返しているっぽい。参考程度に当該ミドルウェアの内容を後述する。（wip））

ただし、ちょっと注意が必要だ。
- リクエストは必ずしも「開発者側が意図して設置したリバプロやロードバランサー」だけを通ってきているわけではない。
- 「開発者側が意図して設置したリバプロやロードバランサー」のIPを直接コードに記載するので管理が面倒。

前者の内容は「クライアントがプロキシ環境からリクエストを送ってきても対応できない」ということである。もちろんこの世のすべてのクライアント側プロキシを`$proxies`プロパティに登録すれば話は別なのかもしれないが、そんなことは現実的じゃない。単純に「この設定をしたからといって必ずクライアントのIPが取れるわけじゃない」ということだ。改ざんされていないことを前提とするが、まだX-Forwarded-Forに紐づいた文字列の一番最初のIPを取得したほうが本当のクライアントのIPを取得できる可能性が高いと考える。
後者の内容はそのままで、そもそもIPをコード上に残すことがなんか気になる。（個人的所感）.envに書いてconfigからIP呼び出せばまだ運用しやすいかもしれないが、そこまでしても得られるリターンが少ない気がする。

## 参考文献

https://www.php.net/manual/ja/function.explode.php

https://www.php.net/manual/ja/function.trim.php

https://readouble.com/laravel/9.x/ja/requests.html#configuring-trusted-proxies

https://r-tech14.com/ip_restrictions/