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

Что даёт система, а что нет

establish() возвращает ParcelFileDescriptor для интерфейса tun. Система записывает в него исходящие IP-пакеты и ожидает, что ваш процесс прочитает их, решит, куда они направляются, и отправит через обычный сокет. Ниже нет пересылки в ядре, поэтому когда никто не читает, пакеты отбрасываются, а счётчики интерфейса никогда не двигаются.

Начните со счёта вместо догадок. Увеличивайте счётчик при каждом успешном чтении и записывайте назначение и протокол первых пакетов. Если счётчик остаётся нулевым, пока генерируется трафик, ошибка находится выше кода пересылки, что сужает поиск до конфигурации builder и режима чтения. Если счётчик движется, а браузер всё равно не работает, ошибка в пути отправки или на обратном пути.

Блокирующее чтение решает, движется ли цикл

setBlocking(true) переводит дескриптор в блокирующий режим и существует с API 21. В старых выпусках обходной путь — небольшой помощник JNI, который очищает O_NONBLOCK через fcntl на сыром дескрипторе. Режим важен, потому что определяет, что разрешено делать чтению без ожидающего пакета.

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

Читайте по одному пакету за вызов в буфер размером с MTU интерфейса, и считайте ноль или отрицательный возврат ошибкой, а не пустым пакетом.

protect() до первой отправки, а не после

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

Порядок: protect, затем connect или bind, затем send. Вызов protect после connect может оставить прежнее решение о маршруте, поэтому сокет продолжает использовать неправильный путь, хотя вызов вернул true. Проверьте возвращаемый boolean и громко завершайтесь с ошибкой, когда он false, вместо того чтобы входить в отправку, которая не может работать.

Ошибка прав от sendto на сокете, который никогда не касался protect(), — обычная подпись этой ошибки.

Пример с UDP не несёт HTTPS

Пример пересылки, который копируют большинство руководств, обрабатывает только UDP. TCP нужен рукопожатие, порядковые номера, подтверждения и повторная передача, а цикл, копирующий полезную нагрузку между дескриптором tun и сокетом, не даёт ничего из этого. Вот почему запрос DNS через UDP проходит, а каждый запрос HTTPS застревает. Либо запускайте стек TCP в пользовательском пространстве, либо отдайте соединения локальному прокси, который завершает TCP на устройстве и пересылает поток.

Проверьте разделение, прежде чем что-то переписывать: отправьте один запрос UDP и откройте одно соединение TCP к тому же хосту. Если путь UDP работает, а путь TCP висит на первом SYN, проблема в коде пересылки, а не в сервере. Это единственное сравнение экономит много времени, потраченного на чтение серверных журналов, в которых нет ничего полезного.

Список маршрутов и фильтры по приложениям ограничивают то, что вы видите

addAddress, addRoute и addDns определяют, что система отправляет в туннель. Назначение вне списка маршрутов никогда не достигает вашего дескриптора, поэтому никакое количество логики пересылки это не исправит. Записывайте вызовы builder и сравнивайте их с адресами, к которым приложение действительно пытается попасть.

Выбор по приложениям имеет свой порог: addAllowedApplication и addDisallowedApplication требуют API 21, а в старых выпусках этих методов просто нет. Дизайн, фильтрующий по пакету, там вообще не может работать.

Повторное создание оставляет устаревшие дескрипторы

Повторный вызов establish() возвращает новый дескриптор и закрывает предыдущий интерфейс. Старый дескриптор теперь недействителен, поэтому поток, всё ещё заблокированный на нём, получает ошибку, а сокеты, созданные для старого пути, возможно, придётся заменить. onRevoke — это обратный вызов для туннеля, который забрал пользователь или система. Хранение жизненного цикла у одного владельца, который останавливает читателя перед повторным созданием, предотвращает большинство состояний «подключено, но мертво».

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

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

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