Ниже — две части.
- База — её можно быстро пролистать.
- Боевая имплементация — там то, что мы хотим на практике.
Важно учесть, что настройка Nginx — не только для "наших" задач.
Все наши фреймворки, их middleware и т.д. — тоже рассчитывают на достоверные заголовки. Для корректной работы ихautoban
и т.д.
База. Зачем нужен Nginx. Логика X-Forwarded-For
.
Тестировать будем на starlette.
Создадим простой main.py
, который просто рисует http-мету:
from starlette.applications import Starlette
from starlette.responses import HTMLResponse
from starlette.routing import Route
async def home(request):
return HTMLResponse(f"""<pre>
url: {str(request.url)}
client:
host: {request.client.host}
port: {request.client.port}
headers:
{('<br>' + ' ' * 12).join(k + ': ' + v for k, v in request.headers.items())}
</pre>""")
app = Starlette(debug=True, routes=[Route('/', home),])
Его можно запустить на сервере:
uvicorn main:app --host=0.0.0.0 --port=2100
И, если DNS настроены:
- Его уже можно открыть в браузере по http://myproject.org:2100
- Он уже будет знать:
- IP-пользователя в
request.client.host
вида 'xxx.xxx.xxx.xxx'. - Host-Cервера в
request.headers.host
вида 'myproject.org:2100'.
- IP-пользователя в
- Даже ssl/https можно прикрутить.
И если обращаться к серверу прям так — http://myproject.org:2100 — без прокси, без VPN-ов — как минимум с заголовками — всё будет нормально.
Но:
- Возможно — нам бы хотелось иметь возможность как-то знать о прокси/VPN-ах.
- Возможно — нам также хотелось бы — мапить/перемапливать урлы (чтобы по 'myproject.org/api/' открывалось 'api.org/take/some/api/dude/'); эффективней обслуживать статику (не приложением); контролировать — таймауты, размеры файловых аплоудов, размеры текстовых аплоудов; иметь возможность показать пользователю хоть что-то, пока сервер лежит/перезапускается; клёвое логирование и т.д. и т.п.
А главное что нам хотелось бы — делать всё это через одну привычную технологию, на чём бы мы не писали наше приложение.
Nginx
Сэмулируем проксирование самих себя (может это мы сами для наших нужд):
server {
listen 2200;
location / {
proxy_pass http://myproject.org:2100;
}
}
Открываем браузер по http://myproject.org:2200
.
В request.client.host
лежит уже не IP-Пользователя, а IP-Cервера, который к нам обратился.
Скажем сразу — если доступа к этому конфигу у нас нет — многого мы не сделаем.
Но, можем отловить "чуть более верхнего инициатора запроса" добавив "подконтрольный" нам конфиг:
server {
listen 2100;
location / {
proxy_pass http://myproject.org:2000;
proxy_set_header X-Real-IP $remote_addr;
}
}
Запускаемся (но уже с портом 2000):
uvicorn main:app --host=0.0.0.0 --port=2000
Открываем браузер также по http://myproject.org:2200
.
Теперь наш сервис дополнительно отображает нам некий request.headers['x-real-ip']
с IP отличающимся от request.client.host
.
И если в request.client.host
у нас IP-Сервера, то в request.headers['x-real-ip']
похоже IP-Роутера-Сервера. (Но это не важно).
А вот если бы тот первый конфиг был нам подконтролен (или просто настроен хорошо), то мы могли бы в обоих случаях добавить:
server {
listen 2200;
location / {
proxy_pass http://myproject.org:2100;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # +
proxy_set_header Host $http_host; # +
}
}
server {
listen 2100;
location / {
proxy_pass http://myproject.org:2000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # +
proxy_set_header Host $http_host; # +
}
}
И теперь, в http://myproject.org:2200
мы получим:
- В
request.headers.host
лежит всё как надо — 'myproject.com:2200'. - В
request.client.host
всё также лежит IP-Сервера. - В
request.headers['x-real-ip']
лежит всё тот же IP-Роутера-Сервера. - Но, появился ещё
request.headers['x-forwarded-for']
, и это — цепочка IP-шников! Потерянный IP-Пользователя и IP-Роутера-Сервера.
Т.е., самый достоверный IP-пользователя — это всегда — request.headers['x-forwarded-for'][0]
.
Боевая имплементация
Нам конечно не нужны такие двойные конфиги.
Просто только так можно было понять 'X-Forwarded-For'.
Что и в какой степени достоверно.
И вот перед нами просто "голый" конфиг:
server {
listen 2200;
location / {
proxy_pass http://headhall.com:2000;
}
}
По http://myproject.org:2200
мы получим:
Который показывает:
- В
request.headers.host
лежит 'myproject.org:2000', хотя браузер открыт с ':2200'. - В
request.client.host
— IP-Сервера, а не IP-Пользователя. -
request.headers['x-real-ip']
вообще нет. -
request.headers['x-forwarded-for']
также нет.
Приводим конфиг в боевое состояние:
server {
listen 2200;
location / {
# Чтобы показывал :2200
proxy_set_header Host $http_host;
# Пытаемся получить IP-Пользователя (и получим, если без прокси)
proxy_set_header X-Real-IP $remote_addr;
# Ещё пытаемся получить IP-Пользователя (и получим, если прокси наше или "нормальное")
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://headhall.com:2000;
}
}
(Вместо $http_host
конечно можно — и $host
(без порта), и $server_name
(если хочется) и т.д. Просто $http_host
самый полный и универсальный.).
(К слову, все эти proxy_*
можно указать и на уровне server
).
Закрепим, что мы получим в http://myproject.org:2200
:
- В
request.headers.host
лежит гарантированный 'myproject.org:2200'. - В
request.client.host
— IP-Сервера, а не IP-Пользователя, как можно было ожидать — всегда НЕ надёжный ориентир. -
request.headers['x-real-ip']
— IP-Пользователя, но, всё же — тоже НЕ надёжный ориентир. -
request.headers['x-forwarded-for'][0]
— IP-Пользователя, как — наиболее надёжный ориентир.
А вот теперь можно уже к — таймаутам, лимиту аплоудов и т.д.
В помощь по всему что выше:
Alphabetical index of variables: https://nginx.org/en/docs/varindex.html
Злой, но золотой комментарий на SO: https://stackoverflow.com/a/72586833/4117781
Удачи, авантюрист!
Top comments (2)
К вашим услугам)