Tu app dice desconectado, la barra de menús dice conectado y el tráfico claramente fluye. Esa brecha no es una condición de carrera que arregles con un reintento: la app y el túnel son procesos separados, y el objeto que lee tu código es una vista en caché de una configuración que guarda el sistema. La superficie Personal VPN de Apple es una API de control, no una ruta de datos, y la mayor parte de su comportamiento extraño se deriva de ese único hecho. Esto es lo que ocurre realmente en cada llamada.

El objeto gestor es una vista en caché, no el túnel

NETunnelProviderManager se rellena desde un almacén de preferencias que posee el sistema, así que sus propiedades no significan nada hasta que loadFromPreferences termina. Lee connection.status antes de eso y obtendrás un valor predeterminado, y por eso la primera actualización de estado tras reiniciar informa desconectado mientras el túnel está activo. Carga primero y luego lee el estado y la configuración del protocolo desde el mismo manejador de finalización. Observa los cambios posteriores mediante NEVPNStatusDidChange en lugar de sondear, y ten en cuenta que la notificación también se dispara para conexiones iniciadas por On Demand, donde no existe ninguna llamada de la app que pueda devolver algo. Lo mismo vale para los ajustes del protocolo: se leen de la configuración cargada, no de lo que la app pasó.

El túnel vive en un proceso proveedor de Network Extension, así que la app puede perder su propio estado sin perder la conexión. Todo lo que muestra la interfaz hay que reconstruirlo desde el gestor, no recordarlo de la sesión anterior.

Orden de inicio: guarda, carga y luego inicia

Una conexión basada en proveedor necesita tres pasos en orden. Escribe la configuración del proveedor y llama a saveToPreferences, carga el gestor para que la configuración guardada sea la que tienes en la mano y luego inicia mediante NETunnelProviderSession.startVPNTunnel(). Si te salta el guardado, el inicio no tiene nada que lanzar; si cargas antes de guardar, el gestor sigue reflejando la configuración antigua.

El booleano que devuelve startVPNTunnel solo dice que la solicitud fue aceptada. Los fallos a nivel de proveedor llegan después como un cambio de estado a desconectado, que observas mediante la notificación NEVPNStatusDidChange, y el manejador de la notificación tiene que releer connection.status después de loadFromPreferences porque el valor en caché puede seguir siendo el antiguo. Los fallos a nivel de solicitud vuelven de inmediato desde la forma que lanza excepciones de la llamada, o mediante el parámetro de salida de error de startVPNTunnelAndReturnError:. El código que trata el valor de retorno como el resultado informa de una conexión que nunca se estableció. El otro fallo silencioso es un identificador de paquete del proveedor que no coincide con la extensión instalada: la solicitud se rechaza y nada del mensaje apunta al identificador. Si el identificador es incorrecto, la solicitud suele fallar de inmediato, antes de que aparezca ninguna línea de registro del proveedor.

Recargar las preferencias después de un inicio es lo que hace correctas las pantallas de estado. El gestor no te empuja una configuración nueva cuando el sistema la cambia.

El material de confianza llega por perfil, nunca desde el código de la app

Una app no tiene forma soportada de añadir un certificado raíz al almacén de confianza de iOS, así que una configuración de Personal VPN solo puede referenciar material en el que el dispositivo ya confía: una cadena establecida por un perfil instalado, por la gestión del dispositivo o por una CA en la que el sistema confía por otros motivos. Una app que genera su propia CA en el primer arranque y espera que el sistema la acepte fallará la validación dentro del proveedor, donde la interfaz de la app no puede ver el motivo.

Trata ese límite como la restricción de diseño que es y entrega el ancla de confianza junto con el perfil de inscripción. Mantén la autenticación VPN separada de la decisión de confianza, porque fallan en sitios distintos y las depuran personas distintas.

Los contadores vienen de la interfaz, no de la API de VPN

El gestor expone estado y configuración, y nada que cuente bytes por túnel. Si necesitas cifras de uso, llama a getifaddrs() y lee if_data de la interfaz correspondiente: en0 para Wi-Fi, las interfaces pdp_ip para móvil y un dispositivo utun para el propio túnel. Los contadores de utun describen el tráfico interno e incluyen la sobrecarga del protocolo.

Esas cifras son estimaciones, no contabilidad por app, y se reinician cuando se recrea la interfaz. Etiquétalas así en la interfaz, o alguien las usará para facturar a un cliente. Si necesitas cifras por app, necesitas un mecanismo completamente distinto, no una mejor lectura de estos campos.

La limpieza va en stopVPNTunnel, no en la terminación

applicationWillTerminate te da unos segundos antes de que el sistema pueda matar el proceso, lo que no es un sitio fiable para desmontar una conexión. Envía la parada a stopVPNTunnel() o gestiónala en el callback de parada del proveedor, y conserva el identificador del gestor o de la configuración con el que iniciaste para que la parada apunte a esa conexión y no a todas las configuraciones del dispositivo.

Guarda ese identificador junto al perfil al que pertenece, ya que un objeto gestor no registra qué configuración seleccionó el usuario. Y recuerda que una conexión levantada por On Demand no tiene ninguna llamada de app detrás, así que una pantalla de estado sigue teniendo que cargar las preferencias antes de dibujar nada.

Artículos relacionados