To veier kan sette et IP-grensesnitt foran en Android-app, og begge ender i samme slags fildeskriptor med svært ulike eiere. Den ene går gjennom VpnService og byggeren, den andre gjennom et root-skal og /dev/net/tun. Hver lesing på den deskriptoren er en utgående pakke, og hver skriving er en inngående. Det som skiller veiene er hvem som holder policyen, hvem som overlever en OS-oppgradering, og hvor mye av rutetabellen du får røre.

Ta plattformveien med mindre du kan navngi det konkrete den nekter å gjøre. Den nektelsen er vanligvis TAP, fanging utenfor ditt eget app-sett, eller en rutingsregel som rammeverket aldri eksponerer.

På plattformveien erklærer du hensikt i stedet for kommandoer. Du oppgir minst én adresse, rutene du vil ha, DNS-servere og en valgfri app-liste, og deretter utfører Androids systemtjeneste den privilegerte oppsettet under sin egen identitet. Du lærer aldri grensesnittnavnet på forhånd, og du sender aldri ioctl-en som lager det, og det er poenget.

To måter å få et IP-grensesnitt

Root-veien er den eldre oppskriften. Du åpner /dev/net/tun, kaller TUNSETIFF med IFF_TUN, tar grensesnittet opp med ip link, og legger til adresser og ruter for hånd. Hvert steg er ditt å få riktig, også de rammeverket ville ha håndtert i stillhet. Én manglende rute gir samme brukerrapport som en død server.

Feilsøking skiller seg like mye som oppsett. På root-veien finnes grensesnittet enten, eller så returnerte ioctl-en din en feil du kan lese direkte. På plattformveien finnes grensesnittet først etter at establish() lykkes, så en feil kommer som et unntak eller en null-deskriptor i stedet for en enhetsnode du kan pirke i.

Hva establish() faktisk returnerer

Builder.establish() har tre utfall, og de krever tre ulike svar. En deskriptor betyr at grensesnittet er konfigurert og ditt å bruke, null betyr at forespørselen ikke gikk gjennom, og et kastet unntak betyr at systemtjenesten avviste den. Å slå null og suksess sammen til én boolean er den vanligste måten en app rapporterer om en fungerende tunnel som aldri bærer en pakke. Builder-referansen angir adressekravet, og det er verdt å lese før du klandrer serveren.

Retningene bestemmes av deskriptoren, ikke av protokollen din. Lesinger gir deg utgående pakker som allerede er valgt ut for VPN-et, og skrivinger injiserer inngående pakker tilbake i grensesnittet. Din egen transport krypterer, autentiserer og rammer inn disse pakkene, fordi plattformen stopper ved pakkegrensen.

Binder er ett hopp, JNI er en annen akse

En byggerforespørsel går inn i systemet gjennom Binder, og et sted under der kaller plattformen inn native kode for å konfigurere enheten. Det er to ulike mekanismer stablet inne i ett kall. Binder er prosesskommunikasjon mellom appen din og en systemtjeneste, mens JNI er broen fra Java til native kode inne i en prosess. Å lese et Android 9 AOSP-tre får stakken til å se bokstavelig ut, og det er verdt å huske at interne klassenavn og filstier flytter seg mellom utgivelser.

Det som ikke flytter seg, er kontrakten du kan stole på: byggeren, deskriptoren den returnerer, og tjenestens livssyklus rundt den. Ingen app bør kalle en intern tjenestetransaksjon eller gjenskape en enhetsnode for å få et grensesnitt. Hvis koden din avhenger av et AOSP-klassenavn, vil den knekke på en utgivelse du ikke har testet.

TUN bærer IP, TAP bærer Ethernet

TUN er et IP-grensesnitt. TAP er et Ethernet-grensesnitt, og den forskjellen avgjør om broing i det hele tatt er mulig. En TUN-enhet har ingen MAC-adresse og ingen Ethernet-header å videresende, så en TUN-klient kan ikke bli med i en lag-2-bro, uansett hvordan rutene er skrevet.

VpnService gir deg IP-varianten, som er det et VPN på app-nivå trenger og kan støtte uten en rå Ethernet-stakk. Hvis designet ditt krever broing, fører ikke plattformveien deg dit, og root-veien gjør det bare hvis kjernen og enhetens tillatelser samarbeider.

Hvor root-veien fortsatt vinner

Tre jobber skyver folk bort fra plattformveien. Lag-2-broing trenger TAP. Fanging av trafikk fra apper utenfor ditt eget VPN-område trenger synlighet rammeverket ikke gir. Policy-ruting som byggeren aldri eksponerer, som en egen tabell for én destinasjon, trenger regler du installerer selv.

Kostnadene kommer senere: en opplåst bootloader eller et rootet image, SELinux-policy som flytter seg under deg, og en enhet som slutter å oppføre seg riktig etter en OTA. En rootet oppskrift kan heller ikke sendes ut gjennom normal app-distribusjon, så de fleste team beholder den som en intern diagnostisk vei i stedet for et produkt.

Valget mellom dem

Begynn med grensesnittet du trenger, ikke koden du har. Hvis svaret er en TUN-enhet med app-begrensede ruter, er plattformveien kortere og overlever oppgraderinger. Hvis svaret involverer Ethernet-rammer, fanging av andre apper eller rutingsregler byggeren avviser, er du på root-territorium og bør budsjettere for det som en driftskostnad.

En sjekk som avgjør de fleste diskusjoner: bygg plattformveien først, registrer establish()-resultatet atskilt fra tilstanden til fjernøkten, og bekreft én pakke i hver retning. Først da avgjør du om den manglende evnen rettferdiggjør å forlate den støttede veien.

Relaterte artikler