Warum Ihr Android-VpnService-Tunnel keinen Datenverkehr mehr weiterleitet
Vier Annahmen über VpnService, die im Produktivbetrieb brechen: ein Tunnel zur Zeit, Routenbereich, App-Listen und Abschaltreihenfolge, mit einer Prüfung für jede.
Vier Annahmen über Android VpnService haben mich einmal fast eine Woche gekostet, und keine davon betraf das Netzwerkprotokoll. Die App meldete verbunden, das Schlüsselsymbol saß in der Statusleiste, und trotzdem ging der Verkehr weiter über die normale Route hinaus. Ich nahm weiterhin an, der Tunnel sei meiner, bis ich ihn stoppe, die Routen, die ich hinzufügte, seien die Routen, die ich bekam, und ein Aufruf von stopSelf nehme die Schnittstelle mit. Der Dienst verhielt sich genau wie dokumentiert. Mein mentales Modell war der kaputte Teil.
Mythos 1: der Tunnel bleibt oben, bis Sie ihn stoppen
Android hält jeweils eine VPN-Schnittstelle. Richten Sie eine zweite ein, und die Plattform stellt sie nicht in die Warteschlange, sondern deaktiviert Ihre und ruft onRevoke() in Ihrem Dienst auf. Wenn Ihr Code nur beim Tastendruck abbaut, hinterlässt eine Übernahme einen Deskriptor und einen Leserthread, und der nächste Start erbt beides.
Die VpnService-Referenz sagt ausdrücklich, dass prepare() null zurückgibt, wenn die App bereits Zustimmung hat, und ein Intent, wenn der Nutzer noch zustimmen muss. Diese Zustimmung kommt nur an, wenn Sie den Intent über startActivityForResult() zustellen. Überspringen Sie die Übergabe, und establish() gibt null zurück, was wie ein stiller Fehlschlag statt wie ein Berechtigungsproblem gelesen wird.
Always-on VPN bringt einen Besitzer ins Spiel, den Sie nicht kontrollieren. Das System kann den Tunnel nach einem Neustart oder nach einem Absturz zurückholen, und es verdrängt eine VPN-App, die der Nutzer von Hand gestartet hat. Bauen Sie onRevoke() so, dass es dieselben Ressourcen schließt wie Ihr normales Herunterfahren, und behandeln Sie es als primären Stoppweg statt als Sonderfall. Nutzer wechseln VPN-Apps; Ihr Code muss das überleben.
Mythos 2: addRoute fängt genau das ab, was Sie aufgelistet haben
Routen entscheiden, welche Pakete Ihren Deskriptor erreichen, und ein Bereichsfehler erzeugt den ärgerlichen Fall, in dem ein Teil des Verkehrs durch den Tunnel geht und ein Teil nicht. addRoute("0.0.0.0", 0) nimmt das gesamte IPv4, was bedeutet, dass jedes Paket nun bei Ihnen ankommt und weitergeleitet oder verworfen werden muss. Ein /32 für einen einzelnen Host nimmt fast nichts, und alles andere verhält sich, als wäre Ihr VPN nie installiert worden.
Familien werden getrennt gezählt. Eine reine IPv4-Route lässt IPv6 im darunterliegenden Netz, sodass ein Dual-Stack-Host weiterhin außerhalb des Tunnels auflösen und verbinden kann, während er innen vollkommen verbunden aussieht.
DNS folgt derselben Logik. Mit addDnsServer() hinzugefügte Server gehen an Apps, die den Resolver des Tunnels nutzen, während eine App mit eigenem verschlüsselten DNS-Endpunkt oder mit auf einen Hostnamen gesetztem Private DNS sie völlig ignoriert. Wenn Namen für eine App fehlschlagen und für eine andere funktionieren, prüfen Sie den Resolver-Pfad, bevor Sie eine einzige Route anfassen.
Mythos 3: App-Regeln sind ein Schalter, den Sie in beide Richtungen umlegen können
addDisallowedApplication() und addAllowedApplication() sind zwei Modi, keine zwei Filter. Der Builder akzeptiert ein Modell pro Tunnel, also nutzt eine App, die alles außer drei Paketen will, die Sperrliste und lässt die Erlaubnisliste leer. Beide schließen sich gegenseitig aus, und die Leute erfahren es zur Laufzeit, nachdem der Einstellungsbildschirm bereits beide Schalter angeboten hat.
Paketsichtbarkeit beißt ebenfalls. Ab Android 11 können Sie nur Pakete benennen, die Sie sehen können, sodass eine Ausschlussliste, die aus einer Abfrage gebaut wird, die Ihre App nicht ausführen darf, stillschweigend Einträge verliert. Vergleichen Sie die Liste mit dem, was der Paketmanager tatsächlich zurückgibt, bevor Sie ihr trauen.
Arbeitsprofile teilen das Publikum erneut. Ein im Profil gestarteter Tunnel deckt dieses Profil und seine Apps ab, während die private Seite ihre eigene Konnektivität behält. Das ist eine einmal getroffene Bereichsentscheidung, und sie gehört in die Oberfläche statt in ein Support-Ticket.
Mythos 4: stopSelf beendet den Tunnel
Tut es nicht. Die Schnittstelle lebt in dem Deskriptor, den establish() zurückgibt, und das Dienstobjekt kann sterben, während dieser Deskriptor offen bleibt. Schließen Sie zuerst den ParcelFileDescriptor, lassen Sie die Referenz fallen, joinen oder unterbrechen Sie die Lese- und Schreibthreads, und rufen Sie dann stopForeground(true) und stopSelf() in dieser Reihenfolge auf.
Ein vergessener Deskriptor ist der übliche Grund, warum ein Dienst seine eigene Stoppanforderung zu überleben scheint, und er erklärt eine veraltete Route, die länger lebt als die App, die sie hinzugefügt hat.
Der umgekehrte Fehler ist genauso verbreitet: den Deskriptor schließen, während ein Leserthread mitten im Lesen ist, und diesen Thread dann eine Ausnahme behandeln lassen, für die niemand etwas geschrieben hat. Halten Sie die Abschaltreihenfolge an einer Stelle, und lassen Sie sowohl die Nutzeraktion als auch onRevoke() hineinrufen.
Was zu prüfen ist, wenn der Tunnel tot aussieht
Schützen Sie langlebige Sockets mit protect(), bevor der Tunnel hochkommt. Ohne das wird der eigene Transport des Tunnels zurück in den Tunnel geroutet, und die Verbindung hängt auf eine Weise, die genau wie ein Serverfehler aussieht. Testen Sie gegen eine Adresse, die nur hinter dem entfernten Netz existiert, nicht gegen eine öffentliche Seite, die aus dem Cache oder über den darunterliegenden Pfad antwortet.
Protokollieren Sie das Ergebnis von establish(), die Routen, die ausgeschlossenen Pakete und den Abschaltgrund. Diese vier Zeilen trennen ein Zustimmungsproblem von einem Bereichsproblem und von einem Lebenszyklusproblem, ohne Paketmitschnitt.