Ваша апликација позива 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 или већ затворен значи да сучеље никада није створено, чак и ако се изузетак није појавио. Упоредите име сучеља из buildera с оним које систем пријављује за активни 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 апликације.

Повезани чланци