Waarom je iOS VPN-app losgekoppeld toont terwijl de tunnel draait
Je iOS-app bestuurt de tunnel maar bezit hem niet, en de status van de manager is gecachet. Laad eerst de preferences en lees daarna status en interface-tellers.
Je app zegt losgekoppeld, de menubalk zegt verbonden, en het verkeer stroomt zichtbaar. Dat gat is geen race condition die je met een nieuwe poging oplost: de app en de tunnel zijn aparte processen, en het object dat je code leest is een gecachete weergave van een configuratie die het systeem opslaat. Het Personal VPN-oppervlak van Apple is een controle-API, geen datapad, en het meeste van zijn vreemde gedrag volgt uit dat ene feit. Dit gebeurt er echt bij elke aanroep.
Het managerobject is een gecachete weergave, niet de tunnel
NETunnelProviderManager wordt gevuld uit een preferences-store die het systeem bezit, dus de eigenschappen betekenen niets tot loadFromPreferences is voltooid. Lees je connection.status daarvoor, dan krijg je een standaardwaarde, en daarom meldt de eerste statusverversing na een herstart losgekoppeld terwijl de tunnel draait. Laad eerst en lees daarna de status en de protocolconfiguratie uit dezelfde completion-handler. Volg latere wijzigingen via NEVPNStatusDidChange in plaats van te pollen, en merk op dat de melding ook afgaat voor verbindingen die door On Demand zijn gestart, waar er helemaal geen app-aanroep is die iets kan teruggeven. Hetzelfde geldt voor protocolinstellingen: die worden gelezen uit de geladen configuratie, niet uit wat de app heeft doorgegeven.
De tunnel zelf leeft in een Network Extension-providerproces, dus de app kan zijn eigen status verliezen zonder de verbinding te verliezen. Alles wat de interface toont moet opnieuw uit de manager worden opgebouwd, niet uit de vorige sessie worden herinnerd.
Startvolgorde: opslaan, laden, dan starten
Een providergebaseerde verbinding vereist drie stappen in volgorde. Schrijf de providerconfiguratie en roep saveToPreferences aan, laad de manager zodat de opgeslagen configuratie degene is die je vasthoudt, en start dan via NETunnelProviderSession.startVPNTunnel(). Sla je het opslaan over, dan heeft de start niets om te starten; laad je vóór het opslaan, dan weerspiegelt de manager nog de oude configuratie.
De boolean die startVPNTunnel teruggeeft zegt alleen dat het verzoek is geaccepteerd. Fouten op providerniveau komen later als statuswijziging naar losgekoppeld, die je via de melding NEVPNStatusDidChange waarneemt, en de meldingshandler moet connection.status opnieuw lezen na loadFromPreferences omdat de gecachete waarde nog de oude kan zijn. Fouten op verzoekniveau komen meteen terug uit de werpende vorm van de aanroep, of via de fout-uitparameter van startVPNTunnelAndReturnError:. Code die de retourwaarde als de uitkomst behandelt, meldt een verbinding die nooit tot stand kwam. De andere stille fout is een provider-bundle-identifier die niet overeenkomt met de geïnstalleerde extensie: het verzoek wordt afgewezen, en niets in het bericht wijst op de identifier. Is de identifier verkeerd, dan mislukt het verzoek meestal meteen, voordat er ook maar één logregel van de provider verschijnt.
Preferences opnieuw laden na een start is wat statusschermen correct maakt. De manager duwt je geen nieuwe configuratie wanneer het systeem die wijzigt.
Vertrouwensmateriaal komt via een profiel, nooit uit app-code
Een app heeft geen ondersteunde manier om een rootcertificaat aan de iOS-vertrouwensopslag toe te voegen, dus een Personal VPN-configuratie kan alleen materiaal refereren dat het apparaat al vertrouwt: een keten die is opgezet door een geïnstalleerd profiel, door apparaatbeheer of door een CA die het systeem om andere redenen vertrouwt. Een app die bij de eerste start zijn eigen CA maakt en verwacht dat het systeem die erkent, zal de validatie binnen de provider niet halen, waar de interface van de app de reden niet kan zien.
Behandel die grens als de ontwerpbeperking die het is en lever de vertrouwensanker samen met het inschrijfprofiel. Houd VPN-authenticatie gescheiden van de vertrouwensbeslissing, want ze falen op verschillende plekken en worden door verschillende mensen gedebugd.
Tellers komen van de interface, niet van de VPN-API
De manager toont status en configuratie, en niets dat bytes per tunnel telt. Heb je gebruikscijfers nodig, roep dan getifaddrs() aan en lees if_data voor de relevante interface: en0 voor Wi-Fi, de pdp_ip-interfaces voor mobiel en een utun-apparaat voor de tunnel zelf. De tellers van utun beschrijven innerlijk verkeer en bevatten protocoloverhead.
Die cijfers zijn schattingen, geen boekhouding per app, en ze worden gereset wanneer de interface opnieuw wordt gemaakt. Label ze zo in de interface, anders gebruikt iemand ze om een klant te factureren. Heb je cijfers per app nodig, dan heb je een heel ander mechanisme nodig, niet een betere lezing van deze velden.
Opruimen hoort in stopVPNTunnel, niet in beëindiging
applicationWillTerminate geeft je een paar seconden voordat het systeem het proces mag doden, en dat is geen betrouwbare plek om een verbinding af te breken. Stuur de stop naar stopVPNTunnel() of handel die af in de stop-callback van de provider, en bewaar de manager- of configuratie-identifier waarmee je bent gestart, zodat de stop die verbinding raakt in plaats van elke configuratie op het apparaat.
Bewaar die identifier naast het profiel waar hij bij hoort, want een managerobject legt niet vast welke configuratie de gebruiker heeft gekozen. En onthoud dat een verbinding die door On Demand omhoog wordt gebracht helemaal geen app-aanroep erachter heeft, dus een statusscherm moet nog steeds de preferences laden voordat het iets tekent.