Split tunneling in Android VpnService: wat je echt kunt routeren
VpnService stuurt alleen pakketten door die de builder in zijn descriptor zet. Controleer prefixen, proxyauthenticatie, DNS en app-lijsten voordat je split tunneling de schuld geeft.
Split tunneling in een VpnService-app breekt op een paar concrete plekken, en de reparatie verschilt per plek. De client toont maar één mislukte bestemming, dus de eerste taak is uitzoeken welke grens je raakt: de routes die de builder kreeg, de proxy die je niet kunt authenticeren, de resolver die je alleen hebt aangekondigd, of de app-lijst die het platform per gebruiker toepast. Daarom houd ik een controle in vier stappen aan. Die loopt in de volgorde hieronder en stopt bij de eerste fout.
Bevestig wat de builder aan het platform gaf
VpnService.Builder is een declaratielijst. establish() retourneert een ParcelFileDescriptor voor een virtuele interface, en het platform stuurt alleen verkeer dat overeenkomt met de prefixen van addAddress en addRoute die descriptor in; addDnsServer vertelt clients welke resolver ze moeten vragen. Een bestemming buiten alle prefixen die je toevoegde, bereikt je app nooit, wat de interface ook belooft. Dat enkele feit verklaart de meeste meldingen van kapotte split tunneling nog voordat er code verandert.
Log de adres-, route- en DNS-lijsten die je builder samenstelde, en vergelijk ze met de bestemmingen die mislukken. Een ontbrekend /32 voor een beheerhost komt veel vaker voor dan een kapotte tunnel. Klopt de lijst, ga dan verder; klopt hij niet, dan is de rest van de checklist verspilde moeite.
excludeRoute bestaat pas vanaf API 33
Uitsluiting op bestemming is het verzoek dat het vaakst een supportthread wordt. builder.excludeRoute(IpPrefix) kwam in API 33; daarvoor is de enige ondersteunde benadering routes toevoegen voor alles behalve het uitgesloten prefix, wat betekent dat je de complementaire prefixen zelf moet berekenen. De twee aanroepen zijn geen spiegelbeelden: de ene trekt af van de routeset van de interface, de andere bouwt de set vanaf nul, en een ouder apparaat gooit of negeert stil een aanroep die het niet kent. Vertak op Build.VERSION.SDK_INT en test beide paden op echte hardware.
Let bij het berekenen van complementen op de prefixuitlijning. Een /24-uitsluiting binnen een /16-route vraagt de dekkende prefixen die de rest echt beschrijven, niet één misvormde /8. Log de definitieve lijst en controleer dat de uitgesloten host nog steeds via een pad buiten de tunnel wordt opgelost.
Proxyreferenties zijn niet uit te drukken
setHttpProxy(ProxyInfo) richt de interface op een HTTP- of SOCKS-proxy, en een PAC-URL kan per bestemming tussen beide kiezen. Noch ProxyInfo noch het PAC-formaat draagt een gebruikersnaam en wachtwoord, dus een proxy met authenticatie valt buiten de platform-API. Apps die er een nodig hebben, bouwen meestal een userspace-stack, tun2socks of een vergelijkbare engine in, en beantwoorden de 407-uitdaging zelf.
Die keuze heeft gevolgen. Een userspace-stack ziet alleen verkeer dat je ernaartoe routeert, dus hij moet binnen dezelfde descriptorgrens leven, en hij erft dezelfde protect()-eis voor zijn eigen uplink-socket. Sla protect() over en de tunnel vangt zijn eigen transport, wat verschijnt als een handshake die na het eerste pakket blijft hangen. Test tegen een proxy die referenties eist, want een open proxy slaagt op het pad van het platform en bewijst niets.
App-lijsten en DNS zijn ook declaraties
Builder heeft zowel addAllowedApplication als addDisallowedApplication, en ze sluiten elkaar uit: beide aanroepen gooit een uitzondering. Hoe dan ook wordt de lijst per gebruiker toegepast bij het instellen van de interface, dus een app in het werkprofiel valt onder de tunnel van het werkprofiel en niet onder die van jou. Wat geen enkele API geeft, is controle over de beslissing van een andere app: je kunt een pakket niet van buitenaf je tunnel in duwen, en je kunt een al actieve VPN niet sluiten of herconfigureren.
DNS volgt hetzelfde patroon. addDnsServer kondigt een resolveradres aan; het laat je app geen queries beantwoorden. Wil je namen zelf oplossen, draai dan een resolver en routeer het adres ervan de tunnel in. Apps die DoT of DoH naar een vaste hostnaam gebruiken, kunnen het aangekondigde adres omzeilen, dus behandel naamresolutie als best effort tenzij je ook het querypad onderschept.
Manifest, always-on en de test in vier stappen
Geschiktheid wordt in het manifest gedeclareerd: BIND_VPN_SERVICE, het intent-filter android.net.VpnService en een voorgrondservice als de tunnel op de achtergrond moet overleven. Always-on VPN vereist bovendien een schone onRevoke, omdat het systeem die aanroept wanneer de gebruiker instellingen wijzigt of een andere app de controle overneemt.
Voer de controles in volgorde uit en stop bij de eerste fout. Ten eerste: dump de adres-, route- en DNS-lijsten die de builder kreeg. Ten tweede: bevestig dat establish() een descriptor teruggaf en dat de transportsocket beschermd is. Ten derde: vraag één bestemming binnen een ingesloten prefix en één binnen een uitgesloten prefix. Ten vierde: controleer welke resolver echt antwoordde door een hostnaam te vragen die alleen jouw resolver kan oplossen.
Een voltooide handshake bewijst geen van de latere stappen. Elke controle isoleert één laag, dus een succes bij stap twee zegt niets over stap vier.
De VpnService.Builder-referentie somt het volledige builder-oppervlak op. Alles wat je daarbuiten nodig hebt, is een andere API, een andere app of je eigen userspace-stack, en dat is een ontwerpbeslissing, geen split-tunnelbug.