Разделено тунелиране в Android VpnService: какво наистина можете да маршрутизирате
VpnService предава само пакетите, които builder е изпратил към своя дескриптор. Проверете префиксите, удостоверяването на прокси, DNS и списъците с приложения, преди да обвинявате разделеното тунелиране.
Разделеното тунелиране в приложение с 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 или подобен engine, и сами отговарят на предизвикателството 407.
Този избор има последствия. Потребителският стек вижда само трафика, който насочите към него, затова трябва да живее в същите граници на дескриптора и наследява същото изискване за protect() за собствения си uplink сокет. Пропуснете 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, друго приложение или вашият собствен потребителски стек и това е решение за дизайна, а не дефект в разделеното тунелиране.