Android VpnService підключається, але пакети не доходять до мережі
Тунель піднято, а сторінки досі не відкриваються: перевірте режим читання, захист сокетів, підтримку TCP і список маршрутів, перш ніж звинувачувати VPN-сервер.
Тунель піднімається, з'являється значок ключа, і кожен запит у браузері досі завершується тайм-аутом. У застосунку 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 означає, що віддалена сторона закрилася, поки ваш код ще читав. Помилка неправильної адреси вказує на розбір заголовка або помилку зсуву, а не на мережу.
Порядок роботи: блокуючий режим увімкнено, один пакет за читання, заголовок розібрано, сокет захищено, навантаження надіслано, чисте закриття під час передачі та список маршрутів, що покриває призначення.