Зашто ваш Android VpnService тунел престаје да рутира саобраћај
Четири уверења о VpnService која се ломе у продукцији: један тунел истовремено, обим рута, листе по апликацији и редослед гашења, са провером за свако.
Четири претпоставке о 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(), руте, изузете пакете и разлог гашења. Та четири реда одвајају проблем сагласности од проблема обима и од проблема животног циклуса без хватања пакета.