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

Что protect() убирает из туннеля

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

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

Три условия, определяющие охват

Первое условие — должен ли выйти только транспорт туннеля. Если остальной трафик приложения должен и дальше идти через VPN, защита сокета — правильный охват. Второе условие — контролируете ли вы создание сокета. Библиотека, которая открывает собственный сокет глубоко внутри, не оставляет вам места вызвать protect(), если только она не предоставляет hook или не принимает готовый сокет. Третье условие — должно ли приложение напрямую достигать локальной сети или captive portal, а это другое требование, чем поддержание туннеля живым.

Исключение приложения с помощью addDisallowedApplication()— это грубый инструмент. Оно убирает из VPN каждое соединение приложения, включая те, что вы хотели проверить, а на некоторых устройствах меняет и поведение локального обнаружения.

Третий вариант — привязать транспорт к выбранной сети с помощью bindProcessToNetwork() или сетевого дескриптора на уровне сокета. Это полезно, когда у устройства одновременно есть Wi-Fi и мобильная сеть, а туннель должен идти по определённому восходящему каналу.

Вызывайте его, пока транспорт создаётся

Вызовите protect() в том же блоке, который создаёт транспорт, до connect() или send(). Если вызов возвращает false, закройте сокет и провалите настройку; продолжение оставляет вас с транспортом, пакеты которого могут снова попасть в ваш собственный интерфейс.

Форма достаточно мала, чтобы держать её в одном месте:

DatagramSocket transport = new DatagramSocket();
if (!protect(transport)) {
    transport.close();
    throw new IOException("transport not protected");
}
transport.connect(serverAddress);

Здесь кусаются три детали. Во-первых,boolean — единственный сигнал, который вы получаете, поэтому записывайте сбой с достаточным контекстом, чтобы отличить отозванное согласие от отклонённого вызова. Во-вторых,protect() охватывает сокет, а не номер дескриптора: если закрыть сокет и позже использовать то же целое число для другого сокета, новый сокет не защищён. В-третьих, когда нативная библиотека открывает транспорт, защитите дескриптор до подключения нативного кода, иначе маршрут может быть уже выбран.Справочник VpnService— это место, где подтверждают, какие перегрузки действительно предоставляет ваш целевой уровень API.

Проверяйте заблокированный путь, а не только счастливый

Проверяйте при поднятом туннеле, потому что это единственное состояние, в котором два пути могут различаться. Отправьте один запрос через защищённый транспорт, а другой — из кода, который никогда не вызывал protect(), затем сравните, что видит дальняя сторона: защищённое соединение должно прийти с нижележащей сетью в качестве источника, а незащищённое показывает собственный адрес туннеля и двигает его счётчики байтов. На устройстве ss или netstat показывают то же разделение ещё до того, как что-либо покинет радио.

Проверяйте отзыв отдельно, потому что смена согласия сносит интерфейс, не затрагивая ваш код сокета. Отзовите согласие VPN или остановите службу, перестройте транспорт и подтвердите, что protect() по-прежнему возвращает true и что транспорт переподключается, как только согласие снова выдано.

Если защищённый запрос всё же идёт через туннель, вызов защиты, вероятно, происходит после того, как сокет уже маршрутизирован. Перенесите его раньше и проверьте снова.

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

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