Ні, застосунок VpnService не може ввімкнути VPN телефона
Застосунку належить один тунель і його життєвий цикл, а не перемикач VPN телефона. Згоду можна відкликати, одночасно працює лише один VPN, а пакети розбирати вам.
Хибне припущення полягає в тому, що застосунок VpnService перемикає системний тумблер і відтоді телефон захищений. Насправді відбувається вужче: застосунок просить згоду, будує інтерфейс, читає й пише пакети через файловий дескриптор і повертає інтерфейс, коли зупиняється. Одночасно може існувати лише один такий тунель, і система може забрати його, не питаючи. Більшість заплутаної поведінки, яку люди приписують протоколу, походить саме з цієї моделі володіння.
Розглядайте кожен перехід життєвого циклу як частину проєкту, а не як крайній випадок.
Міф: застосунок вмикає VPN телефона
Потік починається з VpnService.prepare(), який повертає Intent, обов’язковий до прийняття користувачем, і продовжується establish(), який після цього все ще може не спрацювати з причин, які застосунок не контролює. Створення другого тунелю деактивує перший інтерфейс і замінює системне сповіщення. Ніде в цій послідовності застосунок не володіє перемикачем для всього пристрою; він володіє наданим йому дескриптором і пакетами, що на нього потрапляють. Навіть постійне сповіщення належить системі, а не дизайну застосунку.
Оскільки інтерфейс надається, а не захоплюється, інший клієнт може зайняти місце будь-коли. Застосунок дізнається про це через зворотний виклик, а не через значення, що повертається.
Примусова зупинка застосунку видна в панелі сповіщень з тієї ж причини: система повідомляє, що вона надала, а не те, що застосунок вирішив показати.
Міф: згода дається один раз
Коли система передає тунель іншому клієнту, вона надсилає транзакцію відкликання по binder, який базовий клас повертає з onBind(). Застосунок, що замінює цей binder своїм, має сам переслати транзакцію відкликання, інакше VpnService.onRevoke() ніколи не спрацює, а служба продовжить писати в дескриптор, якого більше немає. Згоду може відкликати й користувач, а система може зупинити службу під час перезапуску або перепідключення в режимі постійної роботи.
Давати раду цьому — конкретна робота, а не рядок журналу: припиніть читати дескриптор, закрийте його, скиньте прапорець підключення й скажіть користувачеві, в якому він стані. Потім вирішіть, чи має застосунок відновити з’єднання, запитати знову або зачекати, адже в конфігурації постійної роботи система може повернути службу сама.
Застосунок, який вважає відкликання винятком, а не станом, повідомлятиме про підключення, поки кожен записаний ним пакет відкидається, а це найгірший можливий режим відмови для інструмента приватності.
Міф: інтерфейс фільтрує трафік за вас
Надходить сирий IP. Перехоплення або фільтрування означає розбір заголовків IP, TCP, UDP і IPv6, відстеження потоків і запис відповідей назад у дескриптор, що робить застосунок перехоплення невеликим мережевим стеком, а не системною можливістю. protect() існує, щоб власний транспортний сокет застосунку обходив свій тунель, а не перехоплював самого себе, і повертає логічне значення, яке варто перевіряти на кожному сокеті, створеному після підняття тунелю.
Списки дозволених і заборонених застосунків у конструкторі визначають, чий трафік потрапляє в інтерфейс. Вони не дають доступу до корисного навантаження за межами IP-пакетів, які несе інтерфейс, і жоден API не розкриває тунель іншого клієнта або перелічує VPN-профілі системи. Мережеві можливості можуть сказати, що мережа — це транспорт VPN, але не чий це тунель.
Тому питання про стан інших застосунків отримують суперечливі відповіді: інформація просто не розкривається вашому процесу.
Чого насправді вимагає життєвий цикл
Відстежуйте стани, якими керуєте: підготовлено, встановлено, відкликано, зупинено. Перепідключення має бути розбиранням, після якого йде встановлення, з єдиним власником дескриптора й транспортного сокета, бо два компоненти, що змагаються за встановлення, битимуться за єдине вільне місце. Фонові запуски обмежені в сучасному Android, тож широкомовна розсилка під час завантаження не є гарантією; використовуйте службу переднього плану з її сповіщенням там, де рівень API це дозволяє.
Записуйте перехід, а не лише кінцевий стан, і тримайте машину станів достатньо малою, щоб перевірити кожен шлях. Стан, до якого не можна дійти в тесті, — це стан, з яким ви зустрінетеся в полі без журналу.
Перевірте життєвий цикл на пристрої
Емулятори вміють підробляти підключення, але не шлях згоди й відкликання переконливо, тому запускайте це на реальному пристрої з установленим другим VPN-клієнтом, який украде місце.
adb shell dumpsys connectivity | grep -i -A3 vpn
adb shell dumpsys activity services | grep -i -A2 vpnservice
adb logcat -s VpnService:V AndroidRuntime:E
adb shell am force-stop com.example.vpnapp
Запустіть тунель, підтвердьте сповіщення та інтерфейс, потім підключіть інший клієнт і дивіться, чи виконується зворотний виклик відкликання і чи очищається стан. Примусово зупиніть застосунок і перевірте, чи щось іще повідомляє про живий дескриптор.
Повторіть послідовність після перезапуску в режимі постійної роботи, бо цей шлях відрізняється від з’єднання, розпочатого користувачем. Ви шукаєте не зелений значок, а доказ того, що застосунок перестав використовувати дескриптор, яким більше не володів.