A súa aplicación chama a VpnService.prepare, recibe null, constrúe un túnel e o descritor parece válido, pero nada chega ao servidor. Ou prepare devolve un Intent, o usuario apruébao e establish segue fallando. Ambos resultados adoitan significar o mesmo: outra aplicación xa posúe a interface VPN deste usuario, e as súas chamadas protect están marcando sockets que o túnel non pode usar. Percorra as comprobacións seguintes en orde e detéñase na primeira que falle, porque un paso posterior non pode reparar un anterior.

Comprobe a praza VPN antes de tocar protect

Android dá a cada usuario exactamente unha interface VPN activa. É unha regra da plataforma, non un permiso que a súa aplicación poida solicitar, así que dous clientes que queiran ser o túnel non poden coexistir.

Unha VPN sempre activa configurada para outra aplicación é o caso máis difícil, porque o sistema mantén esa aplicación conectada e rexeita unha toma de control. Un filtro de tráfico como AdGuard, un cliente corporativo ou unha VPN de consumo poden ocupar a praza mentres o usuario esqueceu que está instalado. Pida ao usuario que abra Axustes e lea a entrada que aparece en VPN; o nome que hai alí é o propietario co que compite. Ata que esa entrada se desconecte ou se borre o axuste de sempre activa, a súa VpnService non pode converterse na interface.

Paso 1: lea que devolve prepare

VpnService.prepare responde a unha soa pregunta: pode esta aplicación converterse na VPN? Devolve null cando xa existe consentimento, e un Intent cando o usuario aínda debe aprobalo, que debe lanzar con startActivityForResult. Trate un retorno null como permiso para continuar, non como proba de que establish terá éxito.

Intent intent = VpnService.prepare(context);
if (intent != null) {
    startActivityForResult(intent, REQUEST_VPN);
}

Se a ruta do Intent volve e o usuario rexeitou, prepare seguirá devolvendo un Intent no seguinte intento. Rexistre o código de resultado en lugar de reintentar en bucle, porque un consentimento rexeitado é unha decisión do usuario, non un erro transitorio.

Paso 2: confirme que a interface pertence á súa aplicación

Tras establish, o ParcelFileDescriptor que recibe é a súa proba de propiedade, e paga a pena comprobalo antes de calquera traballo con sockets. Un descritor null ou xa pechado significa que a interface nunca se creou, aínda que non aparecese ningunha excepción. Compare o nome da interface do builder co que o sistema informa para a VPN activa, usando dumpsys nun dispositivo de proba. Se difiren, o túnel que construíu non é o túnel que transporta tráfico.

adb shell dumpsys connectivity

A mesma discrepancia explica unha icona conectada con tráfico morto. O sistema mostra o túnel da outra aplicación, mentres o seu código ten un descritor dunha interface que non posúe.

Paso 3: chame protect antes de connect

Unha vez existe a interface, os sockets que abre a súa aplicación enrútanse ao túnel por defecto. O transporte exterior non pode seguir esa regra ou o paquete encapsulado volvería á interface, así que VpnService protect marca un socket para saltar a VPN e usar a rede subxacente. A chamada devolve un booleano, e ignoralo é como acabas cun socket que parece san e non transporta nada.

Chame protect antes de connect ou bind, nunca despois. A protección debe estar no seu sitio antes de que o socket se conecte ou se ligue, porque un socket conectado xa escolleu a súa ruta e un protect posterior non a move de forma fiable. Se protect devolve false, detéñase e volva comprobar a propiedade: falla cando a interface non pertence á súa aplicación, que é exactamente o estado que deixa outra VPN. Un retorno false é un sinal para volver ao Paso 1, non para reintentar o mesmo socket.

DatagramSocket socket = new DatagramSocket();
if (!vpnService.protect(socket)) {
    return;
}
socket.connect(serverAddress, serverPort);

Paso 4: ligue un proceso cando un socket é incómodo

Algúns transportes non son un só socket: unha biblioteca pode abrir varios, ou pode que queira que cada conexión dun proceso auxiliar use a mesma saída. ConnectivityManager.bindProcessToNetwork liga o proceso a unha rede escollida, e o antigo setProcessDefaultNetwork facía o mesmo antes de quedar obsoleto. Solicite a conexión subxacente con NET_CAPABILITY_NOT_VPN para que a ligazón seleccione a rede baixo o seu túnel e non o túnel mesmo.

NetworkRequest request = new NetworkRequest.Builder()
    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
    .build();

Ligar un proceso non é unha segunda VPN. Escolle unha saída para ese proceso, o que resulta útil para o seu transporte e inútil para executar dous túneles á vez.

Arranxe o dispositivo do usuario, non a ruta do código

Non hai ningunha API que permita a dúas aplicacións compartir a praza VPN, así que a solución está no estado do dispositivo. Ofreza ao usuario dous camiños concretos: desconecte ou borre o sempre activa da outra aplicación para que o seu prepare poida tomar o relevo, ou abra os axustes por aplicación desa aplicación e exclúa a súa aplicación do seu túnel. A exclusión mantén o filtro funcionando para todo o demais e permite que a súa aplicación use a conexión ordinaria, que a miúdo é o resultado que o usuario realmente quere.

Escriba ambos camiños na súa documentación de soporte, porque os usuarios raramente saben que aplicación posúe a VPN. Pídalles que envíen o nome da VPN activa de Axustes e os resultados de prepare, establish e protect dos seus rexistros, e nun minuto poderá dicir que paso fallou. Logo verifique con tráfico real: unha consulta de nome, unha conexión TCP e unha petición a través do túnel, non só unha icona conectada. Un descritor válido e un indicador verde proban a propiedade no mellor dos casos, nunca que o transporte sobrevivise ás regras de encamiñamento.

Se as catro comprobacións pasan e o tráfico segue morrendo, a propiedade xa non é o seu problema. Pase ao encamiñamento e á accesibilidade do servidor, e probe as mesmas credenciais desde un dispositivo sen ningunha aplicación VPN instalada.

Artigos relacionados