Чотири припущення про Android VpnService одного разу коштували мені більшої частини тижня, і жодне з них не стосувалося мережевого протоколу. Застосунок казав «підключено», значок ключа сидів у рядку стану, а трафік усе одно йшов звичайним маршрутом. Я продовжував вважати, що тунель мій, доки я його не зупиню, що додані мною маршрути — це ті маршрути, які я отримую, і що виклик stopSelf забере інтерфейс із собою. Сервіс поводився точно так, як описано в документації. Зламаною була моя уявна модель.

Міф 1: тунель стоїть, доки ви його не зупините

Android тримає один VPN-інтерфейс одночасно. Підніміть другий, і платформа не поставить його в чергу, а деактивує ваш і викличе onRevoke() у вашому сервісі. Якщо ваш код розбирає все лише за натисканням кнопки, перехоплення лишає по собі дескриптор і потік читання, і наступний запуск успадковує обидва.

Довідка VpnService прямо каже, що prepare() повертає null, коли застосунок уже має згоду, і Intent, коли користувач ще має її підтвердити. Ця згода надходить лише тоді, коли ви передаєте намір через startActivityForResult(). Пропустіть передачу, і establish() поверне null, що читається як тиха невдача, а не як проблема з дозволами.

Always-on VPN додає власника, яким ви не керуєте. Система може повернути тунель після завантаження або після падіння, і вона випереджає застосунок VPN, який користувач запустив вручну. Будуйте onRevoke() так, щоб він закривав ті самі ресурси, що й звичайна зупинка, і ставтеся до нього як до основного шляху зупинки, а не як до крайового випадку. Користувачі змінюють застосунки VPN; ваш код має це пережити.

Міф 2: addRoute захоплює рівно те, що ви перелічили

Маршрути вирішують, які пакети доходять до вашого дескриптора, а помилка в охопленні створює той дратівливий випадок, коли частина трафіку йде через тунель, а частина ні. addRoute("0.0.0.0", 0) забирає весь IPv4, тобто тепер кожен пакет приходить до вас і його треба передати далі або відкинути. /32 для одного хоста майже нічого не забирає, а все інше поводиться так, ніби ваш VPN ніколи не встановлювався.

Сімейства рахуються окремо. Маршрут лише для IPv4 залишає IPv6 у базовій мережі, тож хост із двома стеками все ще може розв'язувати імена й підключатися поза тунелем, виглядаючи всередині цілком підключеним.

DNS іде тією самою логікою. Сервери, додані через addDnsServer(), дістаються застосункам, які використовують резолвер тунелю, тоді як застосунок із власною зашифрованою кінцевою точкою DNS або з Private DNS, заданим на ім'я хоста, повністю їх ігнорує. Коли імена не розв'язуються для одного застосунку й працюють для іншого, перевірте шлях резолвера, перш ніж торкатися хоч одного маршруту.

Міф 3: правила за застосунками — це перемикач, який можна повернути в будь-який бік

addDisallowedApplication() і addAllowedApplication() — це два режими, а не два фільтри. Конструктор приймає одну модель на тунель, тому застосунок, якому потрібно все, крім трьох пакетів, використовує заборонений список і залишає дозволений порожнім. Вони взаємовиключні, і люди дізнаються про це під час виконання після того, як екран налаштувань уже запропонував обидва перемикачі.

Видимість пакетів теж кусає. На Android 11 і новіше можна назвати лише ті пакети, які ви бачите, тож список винятків, зібраний із запиту, який вашому застосунку виконувати не можна, тихо загубить записи. Порівняйте список із тим, що менеджер пакетів справді повертає, перш ніж йому довіряти.

Робочі профілі знову ділять аудиторію. Тунель, запущений усередині профілю, покриває цей профіль і його застосунки, а особиста сторона зберігає власну зв'язність. Це рішення про охоплення, ухвалене один раз, і його місце в інтерфейсі, а не в заявці до підтримки.

Міф 4: stopSelf завершує тунель

Не завершує. Інтерфейс живе в дескрипторі, який повертає establish(), а об'єкт сервісу може померти, поки цей дескриптор лишається відкритим. Спершу закрийте ParcelFileDescriptor, відпустіть посилання, приєднайте або перервіть потоки читання й запису, а потім викличте stopForeground(true) і stopSelf() у цьому порядку.

Забутий дескриптор — звична причина того, що сервіс ніби переживає власний запит на зупинку, і це пояснює застарілий маршрут, який живе довше за застосунок, що його додав.

Зворотна помилка так само часта: закрити дескриптор, коли потік читання саме читає, а потім лишити цей потік розбиратися з винятком, для якого ніхто не написав обробки. Тримайте послідовність зупинки в одному місці й дозвольте і дії користувача, і onRevoke() викликати її.

Що перевірити, коли тунель здається мертвим

Захистіть довгоживучі сокети через protect() до того, як тунель підніметься. Без цього власний транспорт тунелю спрямовується назад у тунель, і з'єднання застрягає так, що виглядає точно як збій сервера. Перевіряйте на адресі, яка існує лише за віддаленою мережею, а не на публічному сайті, що відповідає з кешу або з базового шляху.

Записуйте результат establish(), маршрути, виключені пакети й причину зупинки. Ці чотири рядки відокремлюють проблему згоди від проблеми охоплення й від проблеми життєвого циклу без перехоплення пакетів.

Схожі статті