Телефон перезагружается, always-on включён, «блокировать подключения без VPN» включено, а туннель так и не поднимается. Ничего не настроено неправильно: блокировка делает своё дело, и попытка самого клиента достучаться до своей конечной точки тоже ей подчинена. Имя конечной точки должно быть разрешено до того, как туннель существует, клиентом, которому разрешено говорить с сетью, пока всем остальным приложениям нет. Понимание того, какой компонент это делает и в каком порядке, превращает эту загадку в вопрос конфигурации.

Что на самом деле запрашивает конструктор VPN

Приложение Android VPN не передаёт платформе адрес сервера. Оно строит локальный интерфейс: адреса и маршруты для туннеля, резолверы, которые приложения должны использовать внутри него, а затем establish() возвращает файловый дескриптор, который приложение читает и пишет как необработанные IP-пакеты. Удалённая конечная точка в этом конструкторе вообще не появляется.

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

Одно поле конструктора всё же принимает адреса: DNS-серверы. Они должны быть числовыми адресами резолверов, обычно теми, которые оператор хочет опрашивать через туннель. Это поле описывает, куда приложения отправляют запросы после поднятия туннеля. Оно ничего не говорит о том, как клиент находит конечную точку, и именно смешение этих двух настроек порождает путаницу.

Два вопроса DNS, а не один

Первый вопрос: куда приложения отправляют запросы, когда туннель уже поднят? Ответ — адрес резолвера внутри туннеля, достижимый через интерфейс tun и по построению числовой.

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

StageWho resolvesPathFailure looks like
Endpoint lookupThe VPN appUnderlying network, protected socketNever connects; no tunnel interface traffic
Peer authenticationClient and serverTunnel transportConnects then drops; certificate or key error
Private name lookupApps via the tunnelResolver inside the tunTunnel up, internal sites do not resolve

Три этапа, три разных симптома. Одно сообщение об ошибке «не удаётся подключиться» сливает их воедино, и поэтому люди начинают править поле DNS-сервера, чтобы исправить проблему, которая возникает ещё до того, как это поле вообще будет прочитано.

Почему bootstrap может зайти в тупик

Always-on с включённой блокировкой означает, что трафик, не находящийся внутри VPN, отбрасывается, включая трафик приложения, которому принадлежит VPN. Выход задан явно: приложение вызывает protect() на сокете, что привязывает его к нижележащей сети, и тот обходит туннель и получает разрешение на выход. Каждый VPN-клиент обязан делать это для своего транспорта, иначе он никогда не достигнет сервера.

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

Время вскрывает ту же зависимость в более мягких формах. При загрузке до первой разблокировки у устройства может ещё не быть пригодной сети; на captive portal сеть есть, но блокирует всё, кроме портала; при переходе с Wi-Fi на мобильную сеть нижележащая сеть защищённого сокета меняется, и клиент должен это заметить и разрешить имя заново. Каждое из этого — событие bootstrap, и каждое стоит проверять намеренно, а не обнаруживать в 3 часа ночи.

Почему фиксация ответа не является решением

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

У записей DNS есть время жизни именно для того, чтобы ответы могли меняться; кэширование определяется этим сроком, а не навсегда. Фиксация ответа выбрасывает механизм, который делает имя лучше адреса, сохраняя при этом эксплуатационные издержки использования имени.

Фиксация обычно идёт рука об руку со второй дурной привычкой: ослаблением проверки подлинности, потому что соединение теперь падает иначе. Проверка подлинности сервера не входит в проблему bootstrap, и её ослабление меняет неудобство DNS на риск перехвата.

Решения, переживающие перезагрузку

Долговечное решение неприглядно. Используйте клиент, который корректно реализует защищённые сокеты и разрешает свою конечную точку в нижележащей сети, и дайте ему имя конечной точки, разрешаемое публично или через резолвер, доступный до туннеля. Оставьте блокировку включённой; полное её отключение ради работы DNS убирает свойство, ради которого существует развёртывание.

Там, где конечная точка должна оставаться приватной, обеспечьте явный путь bootstrap, которым может воспользоваться платформа — резолвер, доступный без туннеля, или управляющее соединение, поддерживаемое оператором, — и держите это исключение узким и принадлежащим политике, а не пользовательскому переключателю. Документация Android VpnService — это справка о том, что платформа гарантирует в отношении защиты и жизненного цикла, а справка по VpnService.Builder подтверждает, что поле DNS принимает числовые адреса.

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

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

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