Die irrige Annahme lautet, eine VpnService-App lege einen Systemschalter um, und von da an sei das Telefon geschützt. Was tatsächlich geschieht, ist enger: Die App fragt nach Zustimmung, baut eine Schnittstelle auf, liest und schreibt Pakete über einen Dateideskriptor und gibt die Schnittstelle zurück, wenn sie endet. Es kann immer nur ein solcher Tunnel leben, und das System kann ihn ohne Nachfrage entziehen. Der größte Teil des verwirrenden Verhaltens, das Leute dem Protokoll anlasten, stammt aus diesem Besitzmodell.

Behandeln Sie jeden Übergang im Lebenszyklus als Teil des Entwurfs und nicht als Sonderfall.

Mythos: die App schaltet das Telefon-VPN ein

Der Ablauf beginnt mit VpnService.prepare(), das einen Intent zurückgibt, den der Nutzer akzeptieren muss, und setzt sich mit establish() fort, das danach noch aus Gründen scheitern kann, die die App nicht kontrolliert. Ein zweiter Tunnel deaktiviert die erste Schnittstelle und ersetzt die Systembenachrichtigung. Nirgends in dieser Folge besitzt die App einen geräteweiten Schalter; sie besitzt den Deskriptor, der ihr gewährt wurde, und die Pakete, die darauf landen. Selbst die dauerhafte Benachrichtigung gehört dem System und nicht dem Entwurf der App.

Weil die Schnittstelle gewährt und nicht ergriffen wird, kann ein anderer Client den Platz jederzeit übernehmen. Die App erfährt davon über einen Callback und nicht über einen Rückgabewert.

Dass die App zwangsweise beendet wurde, ist aus demselben Grund im Benachrichtigungsbereich sichtbar: Das System meldet, was es gewährt hat, nicht was die App anzuzeigen beschloss.

Mythos: die Zustimmung wird einmal erteilt

Wenn das System den Tunnel an einen anderen Client übergibt, sendet es eine Widerrufstransaktion auf dem Binder, den die Basisklasse aus onBind() zurückgibt. Eine App, die diesen Binder durch ihren eigenen ersetzt, muss die Widerrufstransaktion selbst weiterleiten, sonst wird VpnService.onRevoke() nie ausgelöst und der Dienst schreibt weiter in einen Deskriptor, den es nicht mehr gibt. Die Genehmigung kann auch der Nutzer zurückziehen, und das System kann den Dienst während eines Neustarts oder einer Immer-an-Wiederverbindung beenden.

Der Umgang damit ist konkrete Arbeit und keine Logzeile: Hören Sie auf, den Deskriptor zu lesen, schließen Sie ihn, löschen Sie das Verbunden-Flag und sagen Sie dem Nutzer, in welchem Zustand er ist. Dann entscheiden Sie, ob die App neu aufbauen, erneut fragen oder warten soll, denn bei einer Immer-an-Konfiguration kann das System den Dienst von selbst zurückbringen.

Eine App, die den Widerruf als Ausnahme statt als Zustand behandelt, meldet verbunden, während jedes von ihr geschriebene Paket verworfen wird, und das ist der schlechteste mögliche Fehlermodus für ein Datenschutswerkzeug.

Mythos: die Schnittstelle filtert den Verkehr für Sie

Was ankommt, ist rohes IP. Mitschneiden oder Filtern bedeutet, IP-, TCP-, UDP- und IPv6-Header zu parsen, Flüsse zu verfolgen und Antworten zurück in den Deskriptor zu schreiben, was eine Mitschneide-App zu einem kleinen Netzwerk-Stack und nicht zu einer Systemeinrichtung macht. protect() existiert, damit der eigene Transport-Socket der App ihren Tunnel umgeht, statt sich selbst mitzuschneiden, und es gibt einen booleschen Wert zurück, der bei jedem Socket eine Prüfung verdient, das nach dem Hochfahren des Tunnels erstellt wird.

Die Listen erlaubter und verbotener Anwendungen im Builder entscheiden, wessen Verkehr in die Schnittstelle gelangt. Sie gewähren keinen Zugriff auf Nutzdaten jenseits der IP-Pakete, die die Schnittstelle trägt, und keine API legt den Tunnel eines anderen Clients offen oder listet die VPN-Profile des Systems auf. Netzwerkfähigkeiten können sagen, dass ein Netzwerk VPN-Transport ist, aber nicht, wessen Tunnel es ist.

Deshalb bekommen Statusfragen zu anderen Apps widersprüchliche Antworten: Die Information wird Ihrem Prozess schlicht nicht offengelegt.

Was der Lebenszyklus tatsächlich verlangt

Verfolgen Sie die Zustände, die Sie steuern: vorbereitet, aufgebaut, widerrufen, beendet. Wiederverbindung sollte Abbau gefolgt von Aufbau sein, mit einem einzigen Eigentümer für den Descriptor und den Transport-Socket, denn zwei Komponenten, die um den Aufbau wetteifern, streiten um den einen freien Platz. Hintergrundstarts sind auf modernem Android eingeschränkt, ein Boot-Broadcast ist also keine Garantie; nutzen Sie einen Vordergrunddienst mit seiner Benachrichtigung, wo die API-Ebene es erlaubt.

Protokollieren Sie den Übergang und nicht nur den Endzustand, und halten Sie die Zustandsmaschine klein genug, um jeden Pfad zu testen. Ein Zustand, den Sie im Test nicht erreichen, ist ein Zustand, dem Sie im Feld ohne Log begegnen.

Testen Sie den Lebenszyklus auf einem Gerät

Emulatoren können Konnektivität vortäuschen, aber nicht den Zustimmungs- und Widerrufspfad überzeugend, also führen Sie diese auf einem echten Gerät mit einem zweiten installierten VPN-Client aus, der den Platz stiehlt.

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

Starten Sie den Tunnel, bestätigen Sie Benachrichtigung und Schnittstelle, verbinden Sie dann den anderen Client und beobachten Sie, ob der Widerrufs-Callback läuft und der Zustand sich leert. Beenden Sie die App zwangsweise und prüfen Sie, ob noch etwas einen lebenden Deskriptor meldet.

Wiederholen Sie die Sequenz nach einem Immer-an-Neustart, denn dieser Pfad unterscheidet sich von einer vom Nutzer ausgelösten Verbindung. Sie suchen kein grünes Symbol, sondern den Beleg, dass die App aufgehört hat, einen Deskriptor zu nutzen, den sie nicht mehr besaß.

Ähnliche Artikel