Twee paden kunnen een IP-interface voor een Android-app zetten, en beide eindigen op hetzelfde soort bestandsdescriptor met heel verschillende eigenaren. Het ene loopt via VpnService en de bijbehorende builder, het andere via een root-shell en /dev/net/tun. Elke leesactie op die descriptor is een uitgaand pakket en elke schrijfactie is een inkomend. Wat de paden scheidt, is wie het beleid voert, wie een OS-upgrade overleeft en hoeveel van de routetabel je mag aanraken.

Neem het platformpad, tenzij je het specifieke ding kunt benoemen dat het weigert te doen. Die weigering is meestal TAP, opname buiten je eigen app-set, of een routeringsregel die het framework nooit blootlegt.

Op het platformpad verklaar je bedoeling in plaats van commando's. Je levert minstens één adres, de gewenste routes, DNS-servers en een optionele app-lijst, waarna Androids systeemservice de geprivilegieerde configuratie onder zijn eigen identiteit uitvoert. Je leert de interfacenaam nooit vooraf en je geeft nooit de ioctl uit die hem aanmaakt, en dat is precies het punt.

Twee manieren om een IP-interface te krijgen

Het rootpad is het oudere recept. Je opent /dev/net/tun, roept TUNSETIFF aan met IFF_TUN, brengt de interface omhoog met ip link, en voegt adressen en routes met de hand toe. Elke stap moet je zelf goed doen, ook de stappen die het framework stilzwijgend zou hebben afgehandeld. Eén ontbrekende route levert dezelfde gebruikersmelding op als een dode server.

Foutopsporing verschilt net zoveel als de configuratie. Op het rootpad bestaat de interface ofwel, ofwel je ioctl gaf een fout terug die je direct kunt lezen. Op het platformpad bestaat de interface pas nadat establish() slaagt, dus een fout komt als uitzondering of als null-descriptor binnen in plaats van als apparaatknooppunt waarin je kunt peuteren.

Wat establish() werkelijk teruggeeft

Builder.establish() heeft drie uitkomsten en die vragen drie verschillende reacties. Een descriptor betekent dat de interface is geconfigureerd en van jou om te gebruiken, null betekent dat het verzoek niet doorging, en een gegooide uitzondering betekent dat de systeemservice het afwees. null en succes in één boolean samenvatten is de meest voorkomende manier waarop een app een werkende tunnel meldt die nooit een pakket draagt. De Builder-referentie noemt de adresvereiste, en die is het lezen waard voordat je de server de schuld geeft.

De richtingen worden door de descriptor bepaald, niet door je protocol. Leesacties geven je uitgaande pakketten die al voor de VPN zijn geselecteerd, en schrijfacties injecteren inkomende pakketten terug in de interface. Je eigen transport versleutelt, authenticeert en kadert die pakketten, omdat het platform bij de pakketgrens stopt.

Binder is één sprong, JNI is een andere as

Een builder-verzoek gaat via Binder het systeem in, en ergens daaronder roept het platform native code aan om het apparaat te configureren. Dat zijn twee verschillende mechanismen gestapeld in één aanroep. Binder is procescommunicatie tussen je app en een systeemservice, terwijl JNI de brug van Java naar native code binnen een proces is. Een Android 9 AOSP-boom lezen maakt de stapel letterlijk zichtbaar, en het is goed om te onthouden dat interne klassennamen en bestandspaden tussen releases verschuiven.

Wat niet verschuift, is het contract waarop je mag vertrouwen: de builder, de descriptor die hij teruggeeft en de levenscyclus van de service eromheen. Geen enkele app zou een interne servicetransactie moeten aanroepen of een apparaatknooppunt opnieuw moeten maken om een interface te krijgen. Als je code afhangt van een AOSP-klassenaam, breekt die op een release die je niet hebt getest.

TUN draagt IP, TAP draagt Ethernet

TUN is een IP-interface. TAP is een Ethernet-interface, en dat verschil bepaalt of bridging überhaupt mogelijk is. Een TUN-apparaat heeft geen MAC-adres en geen Ethernet-header om door te sturen, dus een TUN-client kan niet meedoen aan een layer-2-bridge, hoe de routes ook zijn geschreven.

VpnService geeft je de IP-variant, en dat is wat een VPN op app-niveau nodig heeft en kan ondersteunen zonder een ruwe Ethernet-stack. Als je ontwerp bridging vereist, brengt het platformpad je daar niet, en het rootpad alleen als de kernel en de apparaatrechten meewerken.

Waar het rootpad nog wint

Drie taken duwen mensen van het platformpad af. Layer-2-bridging vereist TAP. Verkeer van apps buiten je eigen VPN-bereik opvangen vereist zichtbaarheid die het framework niet geeft. Beleidsroutering die de builder nooit blootlegt, zoals een aparte tabel voor één bestemming, vereist regels die je zelf installeert.

De kosten komen later: een ontgrendelde bootloader of een geroot image, SELinux-beleid dat onder je verschuift, en een apparaat dat zich na een OTA niet meer gedraagt. Een geroot recept kan ook niet via normale app-distributie worden geleverd, dus de meeste teams houden het als een intern diagnostisch pad in plaats van een product.

Kiezen tussen beide

Begin bij de interface die je nodig hebt, niet bij de code die je hebt. Als het antwoord een TUN-apparaat met app-gebonden routes is, is het platformpad korter en overleeft het upgrades. Als het antwoord Ethernet-frames, het opvangen van andere apps of routeringsregels omvat die de builder weigert, zit je op root-terrein en moet je dat als operationele kosten begroten.

Een controle die de meeste discussies beslecht: bouw eerst het platformpad, registreer de uitkomst van establish() apart van de status van de externe sessie, en bevestig één pakket in elke richting. Pas dan bepaal je of de ontbrekende mogelijkheid het verlaten van het ondersteunde pad rechtvaardigt.

Gerelateerde artikelen