Кто-то направляет публичный хост HAProxy на локальное приложение и спрашивает, почему браузер не может пройти дальше. Прокси доходит до приложения через свой VPN, перенаправление возвращает заголовок Location, и затем браузер пытается открыть адрес, который существует только внутри туннеля. Снаружи страница просто зависает. Решение касается того, кто открывает второе соединение, и именно это отличает перенаправление от бэкенда.

Два пути, которые в конфигурации выглядят похоже

Перенаправление завершает обмен ответом. Клиент читает Location и начинает новый запрос оттуда, где он находится, то есть из сети, которую вы никогда не контролировали. Прокси больше не участвует. Всё дальнейшее зависит от собственных DNS, маршрута и доверия клиента, поэтому результат зависит от того, где пользователь открыл браузер:

GET /app        client -> proxy
302 Location    proxy  -> client   (http://10.40.0.20:8080/)
GET /app        client -> 10.40.0.20:8080   client network, no route

Бэкенд прокси оставляет второй этап на стороне сервера. Прокси открывает его по уже имеющемуся пути, а клиент продолжает работать с одним публичным именем. Если туннель падает, клиент получает ошибку вместо URL, за которым не может пройти. Это различие ещё и делает сбой диагностируемым, потому что ошибка появляется там, где лежат логи:

GET /app        client -> proxy
GET /app        proxy  -> 10.40.0.20:8080   over the VPN
response        proxy  -> client

Документация HAProxy по перенаправлениям очерчивает ровно эту границу: ответить клиенту перенаправлением — это иное действие, чем переслать запрос в бэкенд. Обе директивы стоят в файле конфигурации в нескольких строках друг от друга и ведут себя совершенно по-разному. Одна завершает запрос, а другая начинает второй, которым владеет прокси.

Что нужно браузеру, чего ему не даёт VPN сервера

Собственному запросу браузера нужно четыре вещи, которых у него может не быть: ответ DNS для приватного имени, маршрут к этому адресу, разрешение достучаться до слушающего сервиса и доверие к сертификату, который этот сервис предъявит. Хост прокси обладает всеми четырьмя благодаря своему туннелю. Ни одна из них не переходит к клиенту, который никогда не был в этом туннеле, потому что туннель — свойство хоста, а не URL.

Этот разрыв объясняет симптом «почти работает». Первый запрос проходит, адресная строка меняется, а затем соединение зависает или обрывается, хотя прокси всё это время здоров. В конфигурации прокси ничего не сломано, и правка её не поможет клиенту, у которого нет пути к 10.40.0.20. Либо клиент получает маршрут, либо прокси должен установить соединение за него.

Когда до приложения может дотянуться только прокси

Уберите действие перенаправления и отправьте эти запросы в бэкенд, указывающий на приватный адрес. Фронтенд сохраняет публичное имя, а бэкенд решает, куда на самом деле уходит запрос. В пути клиента ничего не меняется, потому что прокси перестаёт описывать пункт назначения и сам становится им. Для приложения, которое намеренно слушает внутренний порт 8080, блок сервера короткий:

backend private_app
    mode http
    option httpchk GET /health
    server app1 10.40.0.20:8080 check

Читайте этот блок как утверждение о таблице маршрутизации прокси: адрес 10.40.0.20 должен быть достижим с самого хоста прокси, что здесь означает через интерфейс VPN, а не через маршрут по умолчанию. Ключевое слово check лишь сообщает, что делает этот маршрут, а путь проверки здоровья должен существовать в приложении, иначе каждая проба помечает сервер как недоступный. Успешная проверка ничего не доказывает о клиенте и не заменяет политику, которая решает, кому можно пользоваться бэкендом.

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

Прежде чем объявлять победу, подтвердите обратный путь в обоих направлениях. Теперь приложение видит соединение от прокси, поэтому его логи и любые метаданные об адресе клиента описывают прокси, если не организовать иначе. Решите, как обрабатываются идентичность и пересылаемые адреса, и проверьте это решение, а не предполагайте его.

Собственные перенаправления приложения всё ещё ведут внутрь

Пересылка первого запроса — лишь половина интеграции. Приложение может выдавать абсолютные перенаправления, жёстко заданные URL ресурсов или cookie, привязанные к имени, которое оно знает внутри, и каждое из этого отправляет браузер обратно на адрес, до которого тот не дотянется. Заголовок Location, указывающий на внутреннее имя, выглядит точно как исходная проблема, поэтому его ошибочно диагностируют как сбой прокси.

Задайте документированный внешний URL приложения и его параметры прокси, затем приведите заголовок Host в соответствие с виртуальным хостом и ожиданиями сертификата. Cookie, привязанный к внутреннему имени, не вернётся через публичное имя, и следующий за этим цикл входа читается как ошибка аутентификации. Сначала измените внешний URL, повторите тест из чистого профиля браузера и ожидайте, что наполовину перенесённые настройки дадут вход, который проходит, и сессию, которая сразу умирает.

Доверяйте пересылаемым метаданным только от назначенного прокси. Заголовок, который может выставить любой клиент, никогда не должен становиться значением авторизации, иначе прокси превращается в способ для вызывающего присвоить чужую идентичность. Это правило не зависит от вопроса о перенаправлении и переживает любой последующий редизайн.

Решите, какой дизайн у вас на самом деле

Если у всех пользователей уже есть авторизованный доступ через VPN, перенаправление на приватный URL может быть осознанным решением. Подтвердите это требование явно, а не предполагайте его, потому что два дизайна обслуживают совершенно разные группы. Один из них вообще не работает из гостиничной сети, а другой открывает приложение любому, кто может загрузить публичный URL. Это различие должно быть в проектной записке.

Запустите проверку с машины в сети пользователей и проследите каждый переход, который делает браузер, а не только первый ответ. Следите за адресной строкой и за каждым последующим запросом, потому что одного абсолютного URL в теге ресурса достаточно, чтобы сломать поток, который прошёл первый этап. Вход должен работать через публичное имя, а ресурсы, которые должны быть запрещены, должны остаться запрещёнными.

Публикация обратного прокси — отдельное решение о доступе, а не прозрачное расширение собственного VPN прокси на любого, кто может загрузить его публичный URL. На диаграмме эти вещи выглядят одинаково, а различаются они тем, кому разрешён вход. Решение этого вопроса после развёртывания — это и есть способ случайно превратить внутреннее приложение в публичное.

Запишите, какой вариант вы построили и почему. Следующий человек, который возьмётся за конфигурацию, будет предполагать другой. Одного перенаправления, добавленного для удобства, достаточно, чтобы воссоздать исходный сбой, и симптом будет выглядеть точно как тот, который вы только что исправили.

Похожие статьи