Deine App meldet getrennt, die Menüleiste meldet verbunden, und der Traffic fließt sichtbar. Diese Lücke ist keine Race Condition, die du mit einem Retry behebst: App und Tunnel sind getrennte Prozesse, und das Objekt, das dein Code liest, ist eine zwischengespeicherte Ansicht einer Konfiguration, die das System speichert. Apples Personal-VPN-Oberfläche ist eine Control-API, kein Datenpfad, und das meiste ihres seltsamen Verhaltens folgt aus genau dieser Tatsache. Hier steht, was bei jedem Aufruf tatsächlich passiert.

Das Manager-Objekt ist eine zwischengespeicherte Ansicht, nicht der Tunnel

NETunnelProviderManager wird aus einem Preferences-Store gefüllt, den das System besitzt, seine Eigenschaften bedeuten also nichts, bis loadFromPreferences abgeschlossen ist. Liest du connection.status vorher, bekommst du einen Standardwert, weshalb der erste Status-Refresh nach einem Neustart getrennt meldet, obwohl der Tunnel steht. Lade zuerst und lies dann Status und Protokollkonfiguration im selben Completion-Handler. Beobachte spätere Änderungen über NEVPNStatusDidChange statt zu pollen, und beachte, dass die Notification auch für Verbindungen feuert, die von On Demand gestartet wurden, wo es gar keinen App-Aufruf gibt, der etwas zurückgeben könnte. Dasselbe gilt für Protokolleinstellungen: Sie werden aus der geladenen Konfiguration gelesen, nicht aus dem, was die App übergeben hat.

Der Tunnel selbst lebt in einem Network-Extension-Provider-Prozess, die App kann also ihren eigenen Zustand verlieren, ohne die Verbindung zu verlieren. Alles, was die UI zeigt, muss aus dem Manager neu aufgebaut werden, nicht aus der letzten Sitzung erinnert.

Startreihenfolge: speichern, laden, dann starten

Eine providerbasierte Verbindung braucht drei Schritte in dieser Reihenfolge. Schreib die Provider-Konfiguration und ruf saveToPreferences auf, lade den Manager, damit die gespeicherte Konfiguration die ist, die du hältst, und starte dann über NETunnelProviderSession.startVPNTunnel(). Lässt du das Speichern weg, hat der Start nichts zu starten; lädst du vor dem Speichern, spiegelt der Manager noch die alte Konfiguration.

Das Boolean, das startVPNTunnel zurückgibt, sagt nur, dass die Anfrage angenommen wurde. Fehler auf Provider-Ebene kommen später als Statuswechsel zu getrennt, den du über die Notification NEVPNStatusDidChange beobachtest, und der Notification-Handler muss connection.status nach loadFromPreferences erneut lesen, weil der zwischengespeicherte Wert noch der alte sein kann. Fehler auf Anfrage-Ebene kommen sofort aus der werfenden Form des Aufrufs zurück oder über den Fehler-Out-Parameter von startVPNTunnelAndReturnError:. Code, der den Rückgabewert als Ergebnis behandelt, meldet eine Verbindung, die nie zustande kam. Der andere stille Fehler ist ein Provider-Bundle-Identifier, der nicht zur installierten Extension passt: Die Anfrage wird abgelehnt, und nichts an der Meldung deutet auf den Identifier hin. Ist der Identifier falsch, scheitert die Anfrage meist sofort, bevor irgendeine Logzeile des Providers erscheint.

Preferences nach einem Start neu zu laden ist das, was Statusbildschirme korrekt macht. Der Manager schiebt dir keine neue Konfiguration, wenn das System sie ändert.

Vertrauensmaterial kommt per Profil, nie aus App-Code

Eine App hat keinen unterstützten Weg, ein Root-Zertifikat zum iOS-Trust-Store hinzuzufügen, eine Personal-VPN-Konfiguration kann also nur Material referenzieren, dem das Gerät bereits vertraut: eine Kette, die durch ein installiertes Profil, durch Geräteverwaltung oder durch eine CA entstanden ist, der das System aus anderen Gründen vertraut. Eine App, die beim ersten Start ihre eigene CA erzeugt und erwartet, dass das System sie anerkennt, scheitert an der Validierung im Provider, wo die UI der App den Grund nicht sehen kann.

Nimm diese Grenze als die Designvorgabe, die sie ist, und liefere den Trust Anchor mit dem Enrollment-Profil aus. Halte die VPN-Authentifizierung getrennt von der Vertrauensentscheidung, weil sie an verschiedenen Stellen scheitern und von verschiedenen Leuten debuggt werden.

Zähler kommen vom Interface, nicht von der VPN-API

Der Manager exponiert Status und Konfiguration und nichts, was Bytes pro Tunnel zählt. Brauchst du Nutzungszahlen, ruf getifaddrs() auf und lies if_data für das passende Interface: en0 für WLAN, die pdp_ip-Interfaces für Mobilfunk und ein utun-Gerät für den Tunnel selbst. Die Zähler von utun beschreiben den inneren Traffic und enthalten Protokoll-Overhead.

Diese Zahlen sind Schätzungen, keine Abrechnung pro App, und sie werden zurückgesetzt, wenn das Interface neu erstellt wird. Kennzeichne sie in der UI so, sonst nutzt sie jemand, um einem Kunden etwas in Rechnung zu stellen. Brauchst du Zahlen pro App, brauchst du einen ganz anderen Mechanismus, kein besseres Auslesen dieser Felder.

Aufräumen gehört in stopVPNTunnel, nicht in die Termination

applicationWillTerminate gibt dir ein paar Sekunden, bevor das System den Prozess beenden darf, und das ist kein verlässlicher Ort, um eine Verbindung abzubauen. Schick den Stopp an stopVPNTunnel() oder behandle ihn im Stop-Callback des Providers, und behalte den Manager- oder Konfigurations-Identifier, mit dem du gestartet hast, damit der Stopp diese Verbindung trifft statt jeder Konfiguration auf dem Gerät.

Speichere diesen Identifier zusammen mit dem Profil, zu dem er gehört, da ein Manager-Objekt nicht festhält, welche Konfiguration der Nutzer ausgewählt hat. Und denk daran, dass eine von On Demand hochgefahrene Verbindung gar keinen App-Aufruf dahinter hat, ein Statusbildschirm also trotzdem Preferences laden muss, bevor er etwas zeichnet.

Ähnliche Artikel