VpnService protect() чи виключення застосунку: що насправді виправляє маршрутизацію
Захистіть сокет, що несе тунель, або виключіть весь застосунок: три умови, які визначають, який обсяг потрібен вашому Android VPN.
Є два способи тримати власні пакети тунелю поза тунелем: захистити транспортний сокет або вивести весь застосунок із VPN. Вони розв'язують різні проблеми, і вибір ширшого лише тому, що його легше під'єднати, — це саме те, через що застосунки непомітно втрачають доступ до локальної мережі та розділений DNS. Почніть із того, що має залишитися поза тунелем, а потім виберіть найменший обсяг, який це забезпечує.
Що protect() прибирає з тунелю
VpnService.protect() — це виклик для окремого сокета. Ви передаєте йому сокет або дескриптор, що несе тунель, і Android позначає цей сокет так, щоб його трафік використовував базову мережу замість інтерфейсу VPN. Усе інше, що надсилає застосунок, далі йде звичайним маршрутом VPN.
Саме цей вузький обсяг і є сенсом. Захищений сокет не змінює того, як ваш клієнт HTTP розв'язує імена, який інтерфейс використовують ваші аналітичні виклики, або що станеться з другим з'єднанням, відкритим пізніше.
Три умови, що визначають обсяг
Перша умова — чи має вийти назовні лише транспорт тунелю. Якщо інший трафік застосунку має й далі йти через VPN, захист сокета і є правильним обсягом. Друга умова — чи контролюєте ви створення сокетів. Бібліотека, яка відкриває власний сокет десь усередині, не залишає вам місця для виклику protect(), якщо вона не надає гачок або не приймає вже створений сокет. Третя умова — чи потрібно застосунку напряму дістатися локальної мережі або порталу входу, що є іншою вимогою, ніж тримати тунель живим.
Виключення застосунку за допомогою 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);Тут кусають три деталі. По-перше, логічне значення — це єдиний сигнал, який ви отримуєте, тому журналюйте збій із достатнім контекстом, щоб відрізнити відкликану згоду від відхиленого виклику. По-друге, protect() покриває сокет, а не номер дескриптора: якщо закрити сокет і пізніше використати те саме ціле число для іншого сокета, новий сокет не захищений. По-третє, коли транспорт відкриває нативна бібліотека, захистіть дескриптор до того, як нативний код під'єднається, інакше маршрут може бути вже вибраний. Довідка VpnService — це місце, де підтверджують, які перевантаження насправді надає ваш цільовий рівень API.
Перевіряйте заблокований шлях, а не лише успішний
Перевіряйте з піднятим тунелем, бо це єдиний стан, у якому два маршрути можуть відрізнятися. Надішліть один запит через захищений транспорт і один із коду, який ніколи не викликав protect(), потім порівняйте, що бачить далекий кінець: захищене з'єднання має прийти з базовою мережею як джерелом, тоді як незахищене показує власну адресу тунелю й рухає його лічильники байтів. На пристрої ss або netstat показує той самий поділ ще до того, як щось залишить радіо.
Відкликання перевіряйте окремо, бо зміна згоди руйнує інтерфейс, не торкаючись вашого коду сокета. Відкличте згоду на VPN або зупиніть службу, перебудуйте транспорт і підтвердьте, що protect() і досі повертає true і що транспорт знову підключається, коли згоду надано знову.
Якщо захищений запит усе одно йде через тунель, виклик захисту, ймовірно, відбувається після того, як сокет уже маршрутизовано. Перенесіть його раніше й перевірте знову.
Тримайте рішення записаним поруч із налаштуванням транспорту: які сокети захищені, який трафік застосунку виключено і що застосунок робить, коли захист не вдається. Саме цей рядок не дає пізнішому рефакторингу розширити виключення на весь застосунок через те, що один тест падав. Він також дає наступній людині спосіб перевірити намір, не роблячи реверс-інжиніринг маніфесту.