Split-Tunneling in einer VpnService-App bricht an einigen wenigen konkreten Stellen, und die Reparatur ist für jede anders. Der Client zeigt nur ein Ziel, das fehlschlägt, also besteht die erste Aufgabe darin, herauszufinden, welche Grenze Sie treffen: die Routen, die der Builder erhalten hat, den Proxy, den Sie nicht authentifizieren können, den Resolver, den Sie nur bekannt gegeben haben, oder die App-Liste, die die Plattform pro Nutzer anwendet. Genau deshalb halte ich eine Prüfung in vier Schritten bereit. Sie läuft in der Reihenfolge unten und stoppt beim ersten Fehler.

Bestätigen Sie, was der Builder der Plattform übergeben hat

VpnService.Builder ist eine Deklarationsliste. establish() liefert einen ParcelFileDescriptor für eine virtuelle Schnittstelle, und die Plattform schickt nur Verkehr, der zu den Präfixen aus addAddress und addRoute passt, in diesen Deskriptor; addDnsServer sagt den Clients, welchen Resolver sie fragen sollen. Ein Ziel außerhalb aller von Ihnen hinzugefügten Präfixe erreicht Ihre App nie, egal was die Oberfläche verspricht. Diese eine Tatsache erklärt die meisten Meldungen über kaputtes Split-Tunneling, noch bevor Code geändert wird.

Protokollieren Sie die Adress-, Routen- und DNS-Listen, die Ihr Builder zusammengestellt hat, und vergleichen Sie sie mit den fehlschlagenden Zielen. Ein fehlendes /32 für einen Verwaltungshost ist weit häufiger als ein kaputter Tunnel. Stimmt die Liste, machen Sie weiter; stimmt sie nicht, ist der Rest der Checkliste verschwendete Mühe.

excludeRoute gibt es erst ab API 33

Zielbasiertes Ausschließen ist die Anfrage, die am häufigsten in einem Support-Thread endet. builder.excludeRoute(IpPrefix) kam mit API 33; davor ist die einzige unterstützte Annäherung, Routen für alles außer dem ausgeschlossenen Präfix hinzuzufügen, was bedeutet, die komplementären Präfixe selbst zu berechnen. Die beiden Aufrufe sind keine Spiegelbilder: der eine subtrahiert vom Routensatz der Schnittstelle, der andere baut den Satz von Grund auf, und ein älteres Gerät wirft einen Aufruf, den es nicht kennt, oder ignoriert ihn still. Verzweigen Sie nach Build.VERSION.SDK_INT und testen Sie beide Wege auf echter Hardware.

Achten Sie beim Berechnen der Komplemente auf die Präfixausrichtung. Ein /24-Ausschluss innerhalb einer /16-Route braucht die abdeckenden Präfixe, die den Rest tatsächlich beschreiben, nicht ein einzelnes fehlerhaftes /8. Protokollieren Sie die endgültige Liste und prüfen Sie, dass der ausgeschlossene Host weiterhin über einen Weg außerhalb des Tunnels aufgelöst wird.

Proxy-Anmeldedaten lassen sich nicht ausdrücken

setHttpProxy(ProxyInfo) richtet die Schnittstelle auf einen HTTP- oder SOCKS-Proxy, und eine PAC-URL kann pro Ziel zwischen beiden wählen. Weder ProxyInfo noch das PAC-Format transportieren Benutzername und Kennwort, also liegt ein authentifizierter Proxy außerhalb der Plattform-API. Apps, die einen brauchen, betten meist einen Userspace-Stack, tun2socks oder eine ähnliche Engine ein und beantworten die 407-Aufforderung selbst.

Diese Wahl hat Folgen. Ein Userspace-Stack sieht nur Verkehr, den Sie zu ihm routen, also muss er innerhalb derselben Deskriptorgrenze leben, und er erbt dieselbe protect()-Anforderung für seinen eigenen Uplink-Socket. Lassen Sie protect() weg, fängt der Tunnel seinen eigenen Transport ein, was sich als Handshake zeigt, der nach dem ersten Paket stockt. Testen Sie gegen einen Proxy, der Anmeldedaten verlangt, denn ein offener Proxy besteht auf dem Plattformweg und beweist nichts.

App-Listen und DNS sind ebenfalls Deklarationen

Builder hat sowohl addAllowedApplication als auch addDisallowedApplication, und sie schließen sich gegenseitig aus: beide aufzurufen wirft eine Ausnahme. So oder so wird die Liste beim Einrichten der Schnittstelle pro Nutzer angewendet, eine App im Arbeitsprofil unterliegt also dem Tunnel des Arbeitsprofils und nicht Ihrem. Was keine API bietet, ist Kontrolle über die Entscheidung einer anderen App: Sie können ein Paket nicht von außen in Ihren Tunnel zwingen und ein bereits aktives VPN nicht schließen oder umkonfigurieren.

DNS folgt demselben Muster. addDnsServer kündigt eine Resolver-Adresse an; es erlaubt Ihrer App nicht, Anfragen zu beantworten. Wollen Sie Namen selbst auflösen, betreiben Sie einen Resolver und routen dessen Adresse in den Tunnel. Apps, die DoT oder DoH zu einem festen Hostnamen nutzen, können die angekündigte Adresse umgehen, behandeln Sie Namensauflösung also als Best Effort, solange Sie nicht auch den Abfrageweg abfangen.

Manifest, Always-on und der Vier-Schritte-Test

Die Berechtigung wird im Manifest deklariert: BIND_VPN_SERVICE, der Intent-Filter android.net.VpnService und ein Vordergrunddienst, wenn der Tunnel im Hintergrund überleben soll. Always-on VPN verlangt zusätzlich ein sauberes onRevoke, weil das System es aufruft, wenn der Nutzer Einstellungen ändert oder eine andere App die Kontrolle übernimmt.

Führen Sie die Prüfungen der Reihe nach aus und stoppen Sie beim ersten Fehler. Erstens: geben Sie die Adress-, Routen- und DNS-Listen aus, die dem Builder übergeben wurden. Zweitens: bestätigen Sie, dass establish() einen Deskriptor zurückgegeben hat und der Transportsocket geschützt ist. Drittens: fordern Sie ein Ziel innerhalb eines eingeschlossenen Präfixes an und eines innerhalb eines ausgeschlossenen. Viertens: prüfen Sie, welcher Resolver tatsächlich geantwortet hat, indem Sie einen Hostnamen abfragen, den nur Ihr Resolver auflösen kann.

Ein abgeschlossener Handshake beweist keinen der späteren Schritte. Jede Prüfung isoliert eine Schicht, ein Erfolg bei Schritt zwei sagt also nichts über Schritt vier.

Die VpnService.Builder-Referenz listet die vollständige Builder-Oberfläche. Alles darüber hinaus ist eine andere API, eine andere App oder Ihr eigener Userspace-Stack, und das ist eine Designentscheidung und kein Split-Tunnel-Fehler.

Ähnliche Artikel