Роздільне тунелювання в застосунку VpnService ламається в кількох конкретних місцях, і виправлення для кожного своє. Клієнт показує лише одну невдалу адресу призначення, тож перше завдання — з'ясувати, яку межу ви зачіпаєте: маршрути, які отримав builder, проксі, який не вдається автентифікувати, resolver, який ви лише оголосили, або список застосунків, який платформа застосовує для кожного користувача. Саме тому я тримаю перевірку з чотирьох кроків. Вона йде в наведеному нижче порядку й зупиняється на першій помилці.

Підтвердьте, що builder передав платформі

VpnService.Builder — це список оголошень. establish() повертає ParcelFileDescriptor для віртуального інтерфейсу, і платформа спрямовує в цей дескриптор лише трафік, що відповідає префіксам із addAddress і addRoute; addDnsServer повідомляє клієнтам, який resolver опитувати. Адреса призначення поза всіма доданими префіксами ніколи не досягає вашого застосунку, хоч би що обіцяв інтерфейс. Цей єдиний факт пояснює більшість повідомлень про зламане роздільне тунелювання ще до зміни коду.

Запишіть у журнал списки адрес, маршрутів і DNS, які зібрав builder, і порівняйте їх з адресами, що не працюють. Відсутній /32 для хоста керування трапляється значно частіше, ніж зламаний тунель. Якщо список правильний, рухайтеся далі; якщо ні, решта чек-листа — марна робота.

excludeRoute існує лише з API 33

Виключення за адресою призначення — це запит, який найчастіше перетворюється на тему підтримки. builder.excludeRoute(IpPrefix) з'явився в API 33; до того єдине підтримуване наближення — додати маршрути для всього, крім виключеного префікса, тобто обчислити додаткові префікси самостійно. Ці два виклики не дзеркальні: один віднімає з набору маршрутів інтерфейсу, інший будує набір з нуля, а старіший пристрій викине виняток або тихо проігнорує невідомий виклик. Розгалужуйтеся за Build.VERSION.SDK_INT і перевіряйте обидва шляхи на реальному обладнанні.

Обчислюючи доповнення, стежте за вирівнюванням префіксів. Виключення /24 усередині маршруту /16 потребує покривних префіксів, які справді описують залишок, а не один спотворений /8. Запишіть остаточний список і переконайтеся, що виключений хост усе ще розв'язується шляхом поза тунелем.

Облікові дані проксі неможливо виразити

setHttpProxy(ProxyInfo) спрямовує інтерфейс на проксі HTTP або SOCKS, а PAC-URL може вибирати між ними для кожної адреси. Ні ProxyInfo, ні формат PAC не передають ім'я користувача й пароль, тому проксі з автентифікацією залишається поза API платформи. Застосунки, яким він потрібен, зазвичай вбудовують стек простору користувача, tun2socks або схожий рушій і самі відповідають на виклик 407.

Цей вибір має наслідки. Стек простору користувача бачить лише той трафік, який ви до нього спрямовуєте, тож він мусить жити в тих самих межах дескриптора й успадковує ту саму вимогу protect() для власного сокета висхідного каналу. Пропустіть protect() — і тунель захопить власний транспорт, що виглядає як рукостискання, яке застрягає після першого пакета. Перевіряйте на проксі, що вимагає облікових даних, бо відкритий проксі проходить шляхом платформи й нічого не доводить.

Списки застосунків і DNS — теж оголошення

Builder має і addAllowedApplication, і addDisallowedApplication, а вони взаємовиключні: виклик обох спричиняє виняток. У будь-якому разі список застосовується для кожного користувача під час налаштування інтерфейсу, тож застосунком робочого профілю керує тунель робочого профілю, а не ваш. Чого не дає жоден API — контролю над рішенням іншого застосунку: ви не можете ззовні проштовхнути пакет у свій тунель і не можете закрити або переналаштувати вже активний VPN.

DNS дотримується тієї самої схеми. addDnsServer оголошує адресу resolver; він не дозволяє застосунку відповідати на запити. Якщо хочете розв'язувати імена самі, запустіть resolver і спрямуйте його адресу в тунель. Застосунки, що використовують DoT або DoH до фіксованого імені хоста, можуть обійти оголошену адресу, тож вважайте розв'язання імен найкращою спробою, якщо ви не перехоплюєте й шлях запиту.

Маніфест, always-on і тест із чотирьох кроків

Придатність оголошується в маніфесті: BIND_VPN_SERVICE, intent-фільтр android.net.VpnService і foreground-служба, якщо тунель має виживати у фоні. Always-on VPN додатково вимагає чистого onRevoke, бо система викликає його, коли користувач змінює налаштування або інший застосунок перебирає контроль.

Виконуйте перевірки по черзі й зупиняйтеся на першій помилці. Перше: виведіть списки адрес, маршрутів і DNS, передані builder. Друге: підтвердьте, що establish() повернув дескриптор і транспортний сокет захищений. Третє: запитайте одну адресу всередині включеного префікса й одну всередині виключеного. Четверте: з'ясуйте, який resolver справді відповів, запитавши ім'я хоста, яке може розв'язати лише ваш resolver.

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

Довідка VpnService.Builder перелічує всю поверхню builder. Усе, що потрібно поза нею, — це інший API, інший застосунок або ваш власний стек простору користувача, і це рішення проєктування, а не помилка роздільного тунелювання.

Схожі статті