Por que seu app VPN iOS mostra desconectado enquanto o túnel está ativo
Seu app iOS controla o túnel mas não o possui, e o estado do gerenciador está em cache. Carregue as preferências primeiro e depois leia o status e os contadores da interface.
Seu app diz desconectado, a barra de menus diz conectado, e o tráfego claramente flui. Essa lacuna não é uma condição de corrida que você corrige com uma nova tentativa: o app e o túnel são processos separados, e o objeto que seu código lê é uma visão em cache de uma configuração que o sistema armazena. A superfície Personal VPN da Apple é uma API de controle, não um caminho de dados, e a maior parte do comportamento estranho dela decorre desse único fato. Eis o que realmente acontece em cada chamada.
O objeto gerenciador é uma visão em cache, não o túnel
NETunnelProviderManager é preenchido a partir de um armazenamento de preferências que o sistema possui, então suas propriedades não significam nada até loadFromPreferences terminar. Leia connection.status antes disso e você obtém um valor padrão, e é por isso que a primeira atualização de status após reiniciar informa desconectado enquanto o túnel está ativo. Carregue primeiro e depois leia o status e a configuração do protocolo no mesmo manipulador de conclusão. Observe mudanças posteriores via NEVPNStatusDidChange em vez de sondar, e note que a notificação também dispara para conexões iniciadas pelo On Demand, onde não existe nenhuma chamada do app que possa retornar algo. O mesmo vale para as configurações do protocolo: elas são lidas da configuração carregada, não do que o app passou.
O túnel em si vive em um processo provedor do Network Extension, então o app pode perder seu próprio estado sem perder a conexão. Tudo o que a interface mostra precisa ser reconstruído a partir do gerenciador, não lembrado da sessão anterior.
Ordem de início: salve, carregue e então inicie
Uma conexão baseada em provedor exige três passos em ordem. Escreva a configuração do provedor e chame saveToPreferences, carregue o gerenciador para que a configuração salva seja a que você tem em mãos, e então inicie via NETunnelProviderSession.startVPNTunnel(). Se pular o salvamento, o início não tem nada para iniciar; se carregar antes de salvar, o gerenciador ainda reflete a configuração antiga.
O booleano retornado por startVPNTunnel diz apenas que a solicitação foi aceita. Falhas no nível do provedor chegam depois como mudança de estado para desconectado, que você observa pela notificação NEVPNStatusDidChange, e o manipulador da notificação precisa reler connection.status após loadFromPreferences porque o valor em cache pode ainda ser o antigo. Falhas no nível da solicitação voltam imediatamente da forma que lança exceções da chamada, ou pelo parâmetro de saída de erro de startVPNTunnelAndReturnError:. Código que trata o valor de retorno como o resultado relata uma conexão que nunca foi estabelecida. A outra falha silenciosa é um identificador de pacote do provedor que não corresponde à extensão instalada: a solicitação é rejeitada, e nada na mensagem aponta para o identificador. Se o identificador estiver errado, a solicitação geralmente falha na hora, antes de aparecer qualquer linha de log do provedor.
Recarregar as preferências após um início é o que torna as telas de status corretas. O gerenciador não empurra uma configuração nova para você quando o sistema a altera.
O material de confiança chega por perfil, nunca do código do app
Um app não tem forma suportada de adicionar um certificado raiz ao armazenamento de confiança do iOS, então uma configuração de Personal VPN só pode referenciar material em que o dispositivo já confia: uma cadeia estabelecida por um perfil instalado, pela gestão do dispositivo ou por uma CA em que o sistema confia por outros motivos. Um app que cria sua própria CA no primeiro início e espera que o sistema a aceite falhará na validação dentro do provedor, onde a interface do app não consegue ver o motivo.
Trate essa fronteira como a restrição de projeto que ela é e entregue a âncora de confiança junto com o perfil de inscrição. Mantenha a autenticação VPN separada da decisão de confiança, porque falham em lugares diferentes e são depuradas por pessoas diferentes.
Os contadores vêm da interface, não da API de VPN
O gerenciador expõe status e configuração, e nada que conte bytes por túnel. Se você precisa de números de uso, chame getifaddrs() e leia if_data da interface relevante: en0 para Wi-Fi, as interfaces pdp_ip para celular e um dispositivo utun para o próprio túnel. Os contadores de utun descrevem o tráfego interno e incluem a sobrecarga do protocolo.
Esses números são estimativas, não contabilidade por app, e são redefinidos quando a interface é recriada. Rotule-os assim na interface, ou alguém vai usá-los para faturar um cliente. Se você precisa de números por app, precisa de um mecanismo completamente diferente, não de uma leitura melhor desses campos.
A limpeza fica em stopVPNTunnel, não na terminação
applicationWillTerminate dá a você alguns segundos antes de o sistema poder matar o processo, o que não é um lugar confiável para desmontar uma conexão. Envie a parada para stopVPNTunnel() ou trate-a no callback de parada do provedor, e guarde o identificador do gerenciador ou da configuração com o qual você iniciou, para que a parada atinja aquela conexão em vez de todas as configurações do dispositivo.
Armazene esse identificador ao lado do perfil a que pertence, já que um objeto gerenciador não registra qual configuração o usuário selecionou. E lembre-se de que uma conexão levantada pelo On Demand não tem nenhuma chamada do app por trás, então uma tela de status ainda precisa carregar as preferências antes de desenhar qualquer coisa.