Гормональный holywar Админа и Разраба PHP или REMOTE_ADDR vs HTTP_X_FORWARDED_FOR

в 6:47, , рубрики: fpm, nginx, php, proxy, tcp, настройка сервера, Программирование, хостинг, метки: , , , , , ,

Давеча был свидетелем одного интересного спора о том как же действительно нужно определять IP адрес конечного пользователя из скриптов PHP.
Собственно, каждое слово сабжа отображает действительную ситуацию. Это был религиозный спор обострённый весенней замечательной погодой, в котором, я считаю, не оказалось правых и не правых, но который побудил меня к мини-исследованию и к моему счастью поставил точку в понимании этого конфессионального но по факту очень простого вопроса.
Для тех, кто как и я сомневался был уверен, что во всём разобрался, но боялся спросить лень было разбираться в мелочах — под кат.

Предыстория

Занимаясь разработкой VOD сервиса для Samsung SmartTV платформы нам непременно нужно знать страну пользователя, чтобы вдруг нечаянно не показать счастливому пользователю фильм там, где запрещает правообладатель… А ведь за нарушение данного условия договора идут не детские штрафы в тысячах долларов (при чем за каждый факт такой оплошности).

На сервере имеем следующее: php-fpm+nginx

Как определить страну? Ну естественно через IP пользователя и GEO IP базу maxmind
«Пффф....» — подумалось нам всем мне — да проще простого. И дабы не писать свой велосипед, нагуглил на stackoverflow, даже вник в каждую строчку, прикрутил и оставил как там и росло код:

    public function getUserHostAddress(){
        if (!empty($_SERVER['HTTP_X_REAL_IP']))   //check ip from share internet
        {
            $ip=$_SERVER['HTTP_X_REAL_IP'];
        }
        elseif (!empty($_SERVER['HTTP_CLIENT_IP']))   //check ip from share internet
        {
            $ip=$_SERVER['HTTP_CLIENT_IP'];
        }
        elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))   //to check ip is pass from proxy
        {
            $ip=$_SERVER['HTTP_X_FORWARDED_FOR'];
        }
        else
        {
            $ip=$_SERVER['REMOTE_ADDR'];
        }
        return $ip;
    }

И всё работало! Почти год… пока не случилось кое-что неожиданное. Естественно неожиданное для этого кода…

Как запутать php или цепочка прокси(всё ещё часть предыстории)

Всё сломалось! А случилось это когда нам пришлось прикручивать одну из платёжных систем и весь этот код рухнул от того, что в HTTP_X_FORWARDED_FOR пришёл не один адрес, а список адресов через запятую (что строго говоря законно, допустимо, и даже не регламентировано в доке по php)
И никто бы ничего не заметил, если бы HTTP_X_REAL_IP или HTTP_CLIENT_IP(которые тоже не регламентирован докой) содержали искомый IP, но увы они были пусты :(

«Ну ладно» — подумали мы(теперь я был уже не один) перепишем всё и попросим админов запихивать пользовательский IP в переменную REMOTE_ADDR:

    public function getUserHostAddress(){
        $ip=$_SERVER['REMOTE_ADDR'];
        return $ip;
    }

И всё работало! Почти месяц… пока не случилось кое-что неожиданное. Естественно неожиданное для этого кода…

Весенний спор крутых мужиков(это не ирония — они крутые)

Всё сломалось! А случилось это потому, что нам нужно было обновить nginx. И мы обратились к профессионалам в этом деле — к нашим админам.
А те в свою очередь решили обновить и конфиг избавившись от нашего «костыля/не костыля» (пока мы этого не поняли) с пробросом в REMOTE_ADDR.

REMOTE_ADDR оставили без изменения т.е. там теперь светилось что-то типа «127.0.0.1»
в HTTP_X_FORWARDED_FOR прокинули IP пользователя (который между делом с лёгкостью удалось переопределить отправкой из браузера заголовка `x-forwarded-for: 999.999.999.999`)
И тут понеслось — Р=Разраб, А=Админ:

А: у вас всё сломалось, и поскольку мы имеем nginx-прокси то нужный вам адрес лежит в HTTP_X_FORWARDED_FOR а в REMOTE_ADDR будет лежать реальный IP сдресс клиента к php-fpm (т.е. 127.0.0.1)
Р: но мы не можем верить HTTP_X_FORWARDED_FOR, ведь это переменная, которую с лёгкостью можно переопределить через заголовок к серверу, ссылаясь на давольно интересную статью
А: нет, мы сделаем так что в ней будет лежать реальный IP конечного пользователя, а в REMOTE_ADDR реальный адрес клиента к php
Р: тогда мы не проследим последовательность проксей, и всё равно для универсализации на другом сервере (скажем без прокси) эти конфиги могут быть не правдивыми пихайте всё в REMOTE_ADDR который в любом случае будет работать.

… это кратко и без матов…

По итогу то конечно всё завелось… и остановились на прозрачном проксировании, когда php думает, что к нему подключаются напрямую клиенты безо всяких проксей и все переменные(точнее одна на которую мы обращаем внимание) в нужном нам состоянии.
Однако не хватает фэншуя в этом деле и по факту у нас ведь есть прокся а может и не одна.

Кто виноват из них кто прав

Судить не нам, но никто!

Если мы имеем действительно кучу клиентов напрямую к php, или прозрачное проксирование то всё просто — юзай REMOTE_ADDR на здоровье и наслаждайся.

Но как быть с фэншуем и где что должно лежать, если мы используем нормальное проксирование и хотим чтобы об этом знал PHP?

Рецепт… но не панацея:

  • REMOTE_ADDR — содержит IP адрес непосредственно обращающегося к нему nginx, в нашем случае 127.0.0.1
  • HTTP_X_FORWARDED_FOR — содержит цепочку прокси адресов и последним идёт IP непосредственного клиента обратившегося к прокси серверу. И тут рассмотрим два частных случая:
    • Не каскадное проксирование. В HTTP_X_FORWARDED_FOR последним или единственным IP адресом (в зависимости от того что прислал/не прислал пользователь в заголовке x-forwarded-for) будет реальный, искомый, тот самый адрес пользователя.

      Казалось бы ну в чем проблема парсить эту переменную и доставать оттуда последний элемент. Но в нашем случае настройки не были до конца корректными и весь HTTP_X_FORWARDED_FOR заменялся заголовком от браузера x-forwarded-for, а должен был приклеивать к нему реальный IP непосредственного пользователя.

      Для примера проверил на промышленном vps хостинге:
      Гормональный holywar Админа и Разраба PHP или REMOTE ADDR vs HTTP X FORWARDED FOR
      Доверять таким данным тоже страшновато, но если всё правильно сделано в настройках то последним IP будет адресс пользователя, вне зависимости от того что придёт в заголовках.

    • Каскадное проксирование. В этом случае действительно HTTP_X_FORWARDED_FOR — содержит цепочку прокси адресов и последним идёт IP непосредственного клиента обратившегося к прокси серверу. Но это не реальный IP пользователя, а всего лишь IP предыдущей прокси в списке.

      Казалось бы ну в чем проблема парсить эту переменную и доставать оттуда первый элемент. Но как было показано выше на рисунке, это уж точно не корректные данные и пользователь может нас ввести в заблуждение в два счёта, прислав в x-forwarded-for первым элементом какой захочет IP

  • HTTP_X_REAL_IP (или любая другая переменная на которую договорятся Админ и Разраб) — содержит IP обращающегося к php пользователя или первой от сервера недоверенной прокси (что для нас равно адресу клиента)

    Для удобства можно использовать специальный модуль для nginx который нивелирует проблемы определения каскадного и не каскадного проксирования, но он по умолчанию «в стандартных сборках центоса, дебика и федоры nginx идет, почему-то без параметра --with-http_realip_module»(с)Админ, а так же для него должен быть корректно сформированна цепочка в HTTP_X_FORWARDED_FOR и настроены адреса доверенных прокси серверов от которых мы можем брать последний элемент из HTTP_X_FORWARDED_FOR

    Однако опять же HTTP_X_REAL_IP это не реальный в общем случае IP конечного пользователя, а лишь первый IP в списке проксей при каскадном проксировании.
    Хотя если проксирование не каскадное, то там может лежать и адрес конечного пользователя.
    А если проксирование каскадное и корректно настроен модуль http_realip то там должен лежать либо IP конечного пользователя либо корректный IP первой недоверенной прокси если считать от php-сервера, что для нас тоже сгодится

  • HTTP_CLIENT_IP (или любая другая переменная на которую договорятся Админ и Разраб) — содержит при любом типе проксирования первый IP из HTTP_X_FORWARDED_FOR, а при отсутствии проксирования содержимое http заголовка client-ip. Который можно использовать только для справки. И ни в коем случае не для определения реального IP пользователя.

В заключении

Есть несколько вариантов проксирования для php+nginx

  • Прозрачное — характерно неизменное содержание переменных в _SERVER (в том числе и REMOTE_ADDR) как если бы мы работали напрямую с php
  • Не прозрачное не каскадное — характерно то, что Админу и Разрабу нужно договориться, где будет храниться реальные IP адрес пользователя :)
  • Не прозрачное каскадное — характерно то же самое что и для не прозрачного не каскадного + правильно настроенный модуль для nginx А также необходимо помнить о возможности каскадного проксирования и о том, что пользователь злой и может присылать в _SERVER[«HTTP_xxxx»] очень неправдивые данные

PS
Позже мы наведём фэншуй в настройках и избавимся от прозрачного проксирования, а так же напишем универсальную функцию определения IP для обоих случаев проксирования.

PPS
Ради фана кому интересно: если кто-то в комментариях напишет эту функцию и конфиг nginx за нас и мы её будем использовать, то под честное слово, тот получит 100р на телефон.
Но эта функция и конфиг должны быть во истину православными и учитывать всё :) все зацепки есть в статье.
Главное — дзен: не торопитесь — вдруг первые напишут с ошибками и вы их учтёте, торопитесь — вдруг первый правильный ответ будет до Вас.

Всем спасибо. Хорошей весны! Договаривайтесь с коллегами и любите их! :)

Автор: ZmeeeD

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js