Por que o teu túnel Android VpnService deixa de encamiñar o tráfico
Catro crenzas sobre VpnService que rompen en produción: un túnel á vez, alcance das rutas, listas por aplicación e orde de apagado, cunha comprobación para cada unha.
Catro suposicións sobre Android VpnService custáronme unha vez boa parte dunha semana, e ningunha delas trataba do protocolo de rede. A aplicación dicía conectado, a icona da chave estaba na barra de estado, e o tráfico seguía saíndo pola ruta normal. Eu seguía supoñendo que o túnel era meu ata que o paraba, que as rutas que engadía eran as rutas que obtía, e que chamar a stopSelf levaría a interface con el. O servizo comportábase exactamente como está documentado. A parte rota era o meu modelo mental.
Mito 1: o túnel mantense ata que o paras
Android mantén unha interface VPN á vez. Establece unha segunda e a plataforma non a pon en cola: desactiva a túa e chama a onRevoke() no teu servizo. Se o teu código só desmonta ao premer un botón, unha toma de control deixa atrás un descritor e un fío lector, e o seguinte arranque herda ambos.
A referencia de VpnService di explicitamente que prepare() devolve null cando a aplicación xa ten consentimento e un Intent cando o usuario aínda ten que aprobalo. Ese consentimento só chega se entregas a intención mediante startActivityForResult(). Salta a entrega e establish() devolve null, o que se le como un fallo silencioso e non como un problema de permisos.
Always-on VPN engade un dono que non controlas. O sistema pode devolver o túnel tras un arranque ou tras unha caída, e antépóñense a unha aplicación VPN que o usuario iniciou á man. Constrúe onRevoke() para que peche os mesmos recursos que o teu apagado normal, e trátao como unha vía de parada principal e non como un caso raro. Os usuarios cambian de aplicación VPN; o teu código ten que sobrevivir a iso.
Mito 2: addRoute captura exactamente o que enumeraches
As rutas deciden que paquetes chegan ao teu descritor, e un erro de alcance produce o caso exasperante no que parte do tráfico vai polo túnel e parte non. addRoute("0.0.0.0", 0) toma todo o IPv4, o que significa que cada paquete chega agora a ti e hai que reenvialo ou descartalo. Un /32 para un só host non toma case nada, e todo o demais compórtase coma se o teu VPN nunca se instalase.
As familias cóntanse por separado. Unha ruta só IPv4 deixa IPv6 na rede subxacente, así que un host de dobre pila aínda pode resolver e conectar fóra do túnel mentres parece perfectamente conectado dentro.
O DNS segue a mesma lóxica. Os servidores engadidos con addDnsServer() van ás aplicacións que usan o resolutor do túnel, mentres que unha aplicación co seu propio punto final DNS cifrado ou con Private DNS fixado a un nome de host ignóraos por completo. Cando os nomes fallan para unha aplicación e funcionan para outra, revisa a ruta do resolutor antes de tocar unha soa ruta.
Mito 3: as regras por aplicación son un interruptor que podes xirar en calquera sentido
addDisallowedApplication() e addAllowedApplication() son dous modos, non dous filtros. O construtor acepta un modelo por túnel, así que unha aplicación que quere todo agás tres paquetes usa a lista de prohibidos e deixa baleira a de permitidos. Os dous son mutuamente excluíntes, e a xente descóbreo en tempo de execución despois de que a pantalla de axustes xa ofrecese ambos conmutadores.
A visibilidade de paquetes tamén morde. En Android 11 e posteriores só podes nomear paquetes que poidas ver, así que unha lista de exclusións construída a partir dunha consulta que a túa aplicación non pode executar perderá entradas en silencio. Compara a lista co que o xestor de paquetes devolve realmente antes de confiar nela.
Os perfís de traballo volven dividir a audiencia. Un túnel iniciado dentro do perfil cobre ese perfil e as súas aplicacións, mentres que o lado persoal conserva a súa propia conectividade. É unha decisión de alcance tomada unha vez, e pertence á interface, non a un ticket de soporte.
Mito 4: stopSelf termina o túnel
Non o termina. A interface vive no descritor que devolve establish(), e o obxecto do servizo pode morrer mentres ese descritor segue aberto. Pecha primeiro o ParcelFileDescriptor, solta a referencia, une ou interrompe os fíos de lectura e escritura, e despois chama stopForeground(true) e stopSelf() nesa orde.
Un descritor esquecido é a razón habitual pola que un servizo parece sobrevivir á súa propia petición de parada, e explica unha ruta obsoleta que vive máis que a aplicación que a engadiu.
O erro inverso é igual de común: pechar o descritor mentres un fío lector está a medio ler, e despois deixar que ese fío xestione unha excepción para a que ninguén escribiu nada. Mantén a secuencia de apagado nun só lugar e fai que tanto a acción do usuario como onRevoke() chamen a ela.
Que comprobar cando o túnel parece morto
Protexe os sockets de longa vida con protect() antes de que o túnel suba. Sen iso, o transporte do propio túnel encamíñase de volta ao túnel e a conexión atasca dun xeito que parece exactamente unha falla do servidor. Proba contra un enderezo que só exista detrás da rede remota, non contra un sitio público que responda da caché ou da ruta subxacente.
Rexistra o resultado de establish(), as rutas, os paquetes excluídos e o motivo do apagado. Esas catro liñas separan un problema de consentimento dun problema de alcance e dun problema de ciclo de vida sen capturar paquetes.