Hairpin NAT: почему конечная точка VPN не работает изнутри
Локальный клиент, использующий публичный адрес, покидает LAN и возвращается внутрь. Когда роутер не делает hairpin, снаружи работает, а изнутри нет.
VPN-сервер, который отвечает из мобильной сети, но не работает с дивана, обычно не проблема сертификата. Клиент внутри LAN просит роутер преобразовать пакет, получателем которого является собственный публичный адрес роутера, то есть пакет должен выйти наружу и сразу вернуться внутрь. Этот манёвр называется hairpinning, и многие роутеры либо вообще не реализуют его, либо делают это только при включённом флажке. Именно успешный тест снаружи и сбивает с толку, потому что доказывает, что слушатель и правило перенаправления в порядке.
Пакет выходит наружу и возвращается внутрь
Проследите пакет. Клиент разрешает публичное имя, подключается к публичному адресу, и NAT назначения на роутере переписывает адрес на внутренний сервер. Ответ сервера адресован LAN-адресу клиента, поэтому он возвращается через LAN-интерфейс роутера, ни разу не пройдя через то правило NAT, которое вернуло бы источник.
Теперь клиент получает пакет с адреса, куда ничего не отправлял, и отбрасывает его. Эта асимметрия и есть вся поломка.
RFC 4787 описывает поведение, ожидаемое от NAT в этом случае, и если коротко, на практике оно необязательно даже там, где задано теоретически. Одни роутеры называют эту настройку NAT loopback, другие — hairpin NAT или NAT reflection, а некоторые вообще ничего не показывают. Роутер без неё настроен не неправильно; он делает обычную вещь, и исправление относится к другому месту. Не делайте из неудачного локального теста вывод, что публичное правило перенаправления неверно.
Проверка 1: проверьте два пути по отдельности
Снаружи LAN подключитесь с тем же профилем, который использовал бы реальный пользователь, и убедитесь, что сессия устанавливается. Изнутри подключитесь к публичному адресу с тем же профилем и запишите точную ошибку и время.
Точный локальный тест сохраняет имя хоста и заменяет только адрес: curl -v --resolve vpn.example.com:443:203.0.113.10 https://vpn.example.com/ отправляет запрос на публичный адрес, по-прежнему предъявляя правильное имя сервера. Это отделяет проблему разрешения имён от проблемы преобразования, которые легко перепутать, когда одно и то же имя работает из кафе. Отметьте, падает ли клиент на TCP-соединении или позже на TLS, потому что первое указывает на преобразование, а второе на идентичность.
Проверка 2: прочитайте, что видит сервер
Самое полезное наблюдение — это взгляд сервера. Запустите захват на VPN-хосте с фильтром по LAN-адресу клиента и по LAN-адресу роутера, затем повторите локальную попытку. Если ничего не приходит, пакет не пережил преобразование.
Если соединение приходит с LAN-адреса роутера, а не клиента, hairpinning происходит и источник переписан. Это работающая конфигурация, а не поломка.
Проверка 3: исправьте, не трогая внешний путь
Есть три поддерживаемых варианта в порядке возрастания радиуса действия. Самый узкий — разделённый DNS: опубликуйте внутреннюю запись, чтобы то же имя сервера разрешалось во внутренний адрес для клиентов в LAN, что полностью обходит публичный путь. Следующий — включить документированную опцию loopback или reflection на роутере, если она есть, и применить её только к этому сервису. Самый широкий — правило source-NAT на роутере, которое переписывает трафик из LAN к серверу, чтобы ответ вернулся через роутер; на Linux-роутере это правило MASQUERADE в POSTROUTING, совпадающее с адресом и портом сервера. Все три оставляют внешний путь нетронутым, и именно это свойство нужно сохранить.
Не расширяйте не связанное с задачей перенаправление портов, чтобы локальный путь стал похож на внешний, и не переносите сервис на другой порт, чтобы обойти проблему. Оба изменения меняют то, до чего может добраться внешний мир, чтобы исправить то, что видит только внутренний.
У разделённого DNS есть одна ловушка, которую стоит назвать. Замена имени хоста произвольным IP меняет то, что проверяет клиент, поэтому внутренняя запись всё равно должна совпадать с именем в сертификате.
Проверка 4: после этого проверьте оба пути
Повторите внешнюю попытку снаружи LAN, повторите внутреннюю с тем же профилем и убедитесь, что журнал сервера показывает два отдельных успешных сеанса, а не один успех и одну тишину. Затем проверьте, что другие клиенты по-прежнему достигают слушателя и что маршруты к приватным ресурсам не изменились.
Держите запись короткой: какой адрес использовал каждый клиент, что видел сервер и какое из трёх исправлений применено. Требования hairpinning из RFC 4787 здесь служат ориентиром того, что ожидается от NAT, и объясняют, почему роутер без этой функции ведёт себя нормально. Локальный сбой, который внешний мир не может воспроизвести, — это вопрос преобразования, пока не доказано обратное, поэтому проверяйте его именно так.