Защитата на VpnService се проваля, когато друг VPN вече е активен
Android разрешава един VPN интерфейс на потребител. Изпълнете проверките prepare, establish, protect и собственост по ред, след което изберете отнемане или изключване за приложение.
Приложението ви извиква VpnService.prepare, получава null, изгражда тунел и дескрипторът изглежда валиден, но нищо не достига до сървъра. Или prepare връща Intent, потребителят го одобрява, а establish все още се проваля. И двата резултата обикновено означават едно и също: друго приложение вече притежава VPN интерфейса за този потребител и вашите извиквания на protect маркират сокети, които тунелът ви не може да използва. Прегледайте проверките по-долу по ред и спрете на първата, която се провали, защото по-късна стъпка не може да поправи по-ранна.
Проверете мястото за VPN, преди да пипате protect
Android дава на всеки потребител точно един активен VPN интерфейс. Това е правило на платформата, а не разрешение, което приложението ви може да поиска, затова два клиента, които искат да бъдат тунелът, не могат да съществуват едновременно.
Винаги включен VPN, зададен за друго приложение, е най-трудният случай, защото системата поддържа това приложение свързано и отхвърля превземане. Филтър за трафик като AdGuard, корпоративен клиент или потребителски VPN може да държи мястото, докато потребителят е забравил, че е инсталиран. Помолете потребителя да отвори Настройки и да прочете записа под VPN; името там е собственикът, с когото се състезавате. Докато този запис не бъде прекъснат или настройката за винаги включен не бъде изчистена, вашият VpnService не може да стане интерфейсът.
Стъпка 1: прочетете какво връща prepare
VpnService.prepare отговаря на един въпрос: може ли това приложение да стане VPN? Връща null, когато съгласие вече съществува, и Intent, когато потребителят все още трябва да го одобри, което трябва да стартирате с startActivityForResult. Приемайте връщането на null като разрешение да продължите, а не като доказателство, че establish ще успее.
Intent intent = VpnService.prepare(context);
if (intent != null) {
startActivityForResult(intent, REQUEST_VPN);
}Ако пътят с Intent се върне и потребителят е отказал, prepare ще продължи да връща Intent при следващия опит. Записвайте кода на резултата вместо да опитвате отново в цикъл, защото отказаното съгласие е решение на потребителя, а не временна грешка.
Стъпка 2: потвърдете, че интерфейсът принадлежи на приложението ви
След establish полученият ParcelFileDescriptor е вашето доказателство за собственост и си струва да го проверите преди каквато и да е работа със сокети. Дескриптор, който е null или вече е затворен, означава, че интерфейсът никога не е бил създаден, дори да не е възникнало изключение. Сравнете името на интерфейса от builder с това, което системата отчита за активния VPN, като използвате dumpsys на тестово устройство. Ако се различават, тунелът, който сте изградили, не е тунелът, който пренася трафик.
adb shell dumpsys connectivityСъщото несъответствие обяснява свързана икона с мъртъв трафик. Системата показва тунела на другото приложение, докато вашият код държи дескриптор за интерфейс, който не притежава.
Стъпка 3: извикайте protect преди connect
Щом интерфейсът съществува, сокетите, които приложението ви отваря, се насочват в тунела по подразбиране. Външният транспорт не може да следва това правило, иначе инкапсулираният пакет би се върнал в интерфейса, затова VpnService protect маркира сокет да заобиколи VPN и да използва основната мрежа. Извикването връща boolean и игнорирането му е начинът да получите сокет, който изглежда здрав и не носи нищо.
Извикайте protect преди connect или bind, никога след това. Защитата трябва да е на място, преди сокетът да бъде свързан или обвързан, защото свързан сокет вече е избрал маршрута си и по-късен protect не го премества надеждно. Ако protect върне false, спрете и проверете отново собствеността: той се проваля, когато интерфейсът не принадлежи на приложението ви, което е точно състоянието, което друг VPN оставя. Връщането на false е сигнал да се върнете към Стъпка 1, а не да опитате същия сокет отново.
DatagramSocket socket = new DatagramSocket();
if (!vpnService.protect(socket)) {
return;
}
socket.connect(serverAddress, serverPort);Стъпка 4: обвържете процес, когато сокетът е неудобен
Някои транспорти не са един сокет: библиотека може да отвори няколко или може да искате всяка връзка в помощен процес да използва същия изход. ConnectivityManager.bindProcessToNetwork обвързва процеса към избрана мрежа, а по-старият setProcessDefaultNetwork правеше същото, преди да бъде отхвърлен. Заявете основната връзка с NET_CAPABILITY_NOT_VPN, за да избере обвързването мрежата под вашия тунел, а не самия тунел.
NetworkRequest request = new NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();Обвързването на процес не е втори VPN. То избира изход за този процес, което е полезно за вашия транспорт и безполезно за пускане на два тунела наведнъж.
Поправете устройството на потребителя, а не пътя на кода
Няма API, който позволява на две приложения да споделят мястото за VPN, затова поправката е в състоянието на устройството. Предложете на потребителя два конкретни пътя: прекъснете или изчистете винаги включен за другото приложение, за да може вашият prepare да поеме, или отворете настройките за отделни приложения на това приложение и изключете вашето приложение от неговия тунел. Изключването поддържа филтъра работещ за всичко останало и позволява на приложението ви да използва обикновената връзка, което често е резултатът, който потребителят всъщност иска.
Запишете и двата пътя в документацията си за поддръжка, защото потребителите рядко знаят кое приложение притежава VPN. Помолете ги да изпратят името на активния VPN от Настройки и резултатите от prepare, establish и protect от вашите журнали и можете да кажете в рамките на минута коя стъпка се е провалила. След това проверете с реален трафик: търсене на име, TCP връзка и една заявка през тунела, а не само свързана икона. Валиден дескриптор и зелен индикатор доказват собственост в най-добрия случай, но никога, че транспортът е преживял правилата за маршрутизация.
Ако и четирите проверки минат и трафикът все още умира, собствеността вече не е вашият проблем. Продължете към маршрутизация и достъпност на сървъра и тествайте същите идентификационни данни от устройство без инсталирано VPN приложение.