Защо вашият тунел 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(), маршрутите, изключените пакети и причината за спирането. Тези четири реда разделят проблем със съгласие от проблем с обхват от проблем с жизнения цикъл без прихващане на пакети.