La suposición equivocada es que una aplicación VpnService acciona un interruptor del sistema y que, a partir de ahí, el teléfono queda protegido. Lo que ocurre en realidad es más estrecho: la aplicación pide consentimiento, construye una interfaz, lee y escribe paquetes a través de un descriptor de archivo, y devuelve la interfaz cuando se detiene. Solo puede haber un túnel así vivo a la vez, y el sistema puede quitarlo sin preguntar. La mayor parte del comportamiento confuso que la gente achaca al protocolo proviene de ese modelo de propiedad.

Trate cada transición del ciclo de vida como parte del diseño y no como un caso límite.

Mito: la aplicación enciende la VPN del teléfono

El flujo comienza con VpnService.prepare(), que devuelve un Intent que el usuario debe aceptar, y continúa con establish(), que después aún puede fallar por motivos que la aplicación no controla. Establecer un segundo túnel desactiva la primera interfaz y reemplaza la notificación del sistema. En ningún punto de esa secuencia la aplicación posee un interruptor para todo el dispositivo; posee el descriptor que se le concedió y los paquetes que aterrizan en él. Incluso la notificación persistente pertenece al sistema y no al diseño de la aplicación.

Como la interfaz se concede en lugar de arrebatarse, otro cliente puede ocupar el hueco en cualquier momento. La aplicación se entera mediante una llamada de retorno, no mediante un valor de retorno.

La detención forzada de la aplicación se ve en el panel de notificaciones por el mismo motivo: el sistema informa de lo que concedió, no de lo que la aplicación eligió mostrar.

Mito: el consentimiento se da una sola vez

Cuando el sistema entrega el túnel a otro cliente, envía una transacción de revocación en el binder que la clase base devuelve desde onBind(). Una aplicación que sustituya ese binder por el suyo debe reenviar ella misma la transacción de revocación, o VpnService.onRevoke() nunca se disparará y el servicio seguirá escribiendo en un descriptor que ya no existe. La aprobación también la puede retirar el usuario, y el sistema puede detener el servicio durante un reinicio o una reconexión siempre activa.

Gestionarlo es trabajo concreto y no una línea de registro: deje de leer el descriptor, ciérrelo, limpie el indicador de conectado y diga al usuario en qué estado está. Luego decida si la aplicación debe restablecer, preguntar de nuevo o esperar, porque en una configuración siempre activa el sistema puede devolver el servicio por sí solo.

Una aplicación que trate la revocación como una excepción y no como un estado informará de que está conectada mientras cada paquete que escribe se descarta, que es el peor modo de fallo posible para una herramienta de privacidad.

Mito: la interfaz filtra el tráfico por usted

Lo que llega es IP en bruto. Capturar o filtrar significa analizar cabeceras IP, TCP, UDP e IPv6, seguir flujos y escribir las respuestas de vuelta al descriptor, lo que convierte una aplicación de captura en una pequeña pila de red y no en una instalación del sistema. protect() existe para que el socket de transporte de la propia aplicación evite su túnel en lugar de capturarse a sí misma, y devuelve un booleano que merece una comprobación en cada socket creado después de que el túnel esté en marcha.

Las listas de aplicaciones permitidas y prohibidas del constructor deciden de quién entra el tráfico en la interfaz. No dan acceso a la carga útil más allá de los paquetes IP que la interfaz transporta, y ninguna API expone el túnel de otro cliente ni enumera los perfiles de VPN del sistema. Las capacidades de red pueden decir que una red es transporte de VPN, pero no de quién es el túnel.

Por eso las preguntas sobre el estado de otras aplicaciones reciben respuestas contradictorias: la información simplemente no se expone a su proceso.

Qué exige realmente el ciclo de vida

Siga los estados que controla: preparado, establecido, revocado, detenido. La reconexión debería ser desmontar y luego establecer, con un único propietario del descriptor y del socket de transporte, porque dos componentes que compitan por establecer se pelearán por el único hueco disponible. Los inicios en segundo plano están restringidos en el Android moderno, así que una difusión de arranque no es una garantía; use un servicio en primer plano con su notificación donde el nivel de API lo permita.

Registre la transición y no solo el estado final, y mantenga la máquina de estados lo bastante pequeña para probar cada ruta. Un estado al que no puede llegar en una prueba es un estado que encontrará en campo sin registro.

Pruebe el ciclo de vida en un dispositivo

Los emuladores pueden falsear la conectividad pero no el camino de consentimiento y revocación de forma convincente, así que ejecute estos en un dispositivo real con un segundo cliente VPN instalado que robe el hueco.

adb shell dumpsys connectivity | grep -i -A3 vpn
adb shell dumpsys activity services | grep -i -A2 vpnservice
adb logcat -s VpnService:V AndroidRuntime:E
adb shell am force-stop com.example.vpnapp

Inicie el túnel, confirme la notificación y la interfaz, luego conecte el otro cliente y observe si se ejecuta la llamada de retorno de revocación y si el estado se limpia. Detenga la aplicación a la fuerza y compruebe si algo sigue informando de un descriptor vivo.

Repita la secuencia tras un reinicio siempre activo, porque esa ruta difiere de una conexión iniciada por el usuario. Lo que busca no es un icono verde sino la prueba de que la aplicación dejó de usar un descriptor que ya no poseía.

Artículos relacionados