Två vägar kan sätta ett IP-gränssnitt framför en Android-app, och båda slutar i samma sorts fildeskriptor med mycket olika ägare. Den ena går genom VpnService och dess builder, den andra genom ett root-skal och /dev/net/tun. Varje läsning på den deskriptorn är ett utgående paket och varje skrivning är ett inkommande. Det som skiljer vägarna är vem som håller policyn, vad som överlever en OS-uppgradering och hur mycket av routingtabellen du får röra.

Ta plattformsvägen om du inte kan namnge det specifika den vägrar göra. Den vägran är oftast TAP, fångst utanför din egen app-uppsättning, eller en routingregel som ramverket aldrig exponerar.

På plattformsvägen deklarerar du avsikt i stället för kommandon. Du anger minst en adress, de rutter du vill ha, DNS-servrar och en valfri app-lista, och sedan utför Androids systemtjänst den privilegierade konfigurationen under sin egen identitet. Du lär dig aldrig gränssnittsnamnet i förväg och du utfärdar aldrig ioctl-anropet som skapar det, och det är poängen.

Två sätt att få ett IP-gränssnitt

Root-vägen är det äldre receptet. Du öppnar /dev/net/tun, anropar TUNSETIFF med IFF_TUN, tar upp gränssnittet med ip link och lägger till adresser och rutter för hand. Varje steg är ditt att få rätt, även de som ramverket hade skött tyst. En saknad rutt ger samma användarrapport som en död server.

Felsökningen skiljer sig lika mycket som uppsättningen. På root-vägen finns gränssnittet antingen, eller så returnerade din ioctl ett fel som du kan läsa direkt. På plattformsvägen finns gränssnittet först efter att establish() lyckats, så ett fel kommer som undantag eller null-deskriptor i stället för en enhetsnod du kan peta i.

Vad establish() faktiskt returnerar

Builder.establish() har tre utfall och de kräver tre olika svar. En deskriptor betyder att gränssnittet är konfigurerat och ditt att använda, null betyder att begäran inte gick igenom, och ett kastat undantag betyder att systemtjänsten avvisade den. Att slå ihop null och framgång till en enda boolean är det vanligaste sättet en app rapporterar en fungerande tunnel som aldrig bär ett paket. Builder-referensen anger adresskravet, och det är värt att läsa innan du skyller på servern.

Riktningarna bestäms av deskriptorn, inte av ditt protokoll. Läsningar ger dig utgående paket som redan valts ut för VPN:et, och skrivningar injicerar inkommande paket tillbaka i gränssnittet. Din egen transport krypterar, autentiserar och ramar in dessa paket, eftersom plattformen stannar vid paketgränsen.

Binder är ett hopp, JNI är en annan axel

En builder-begäran går in i systemet via Binder, och någonstans under det anropar plattformen inbyggd kod för att konfigurera enheten. Det är två olika mekanismer staplade inuti ett enda anrop. Binder är processkommunikation mellan din app och en systemtjänst, medan JNI är bron från Java till inbyggd kod inne i en process. Att läsa ett Android 9 AOSP-träd får stacken att se bokstavlig ut, och det är värt att komma ihåg att interna klassnamn och filsökvägar flyttar sig mellan utgåvor.

Det som inte flyttar sig är kontraktet du kan lita på: buildern, deskriptorn den returnerar och tjänstens livscykel runt den. Ingen app bör anropa en intern tjänstetransaktion eller återskapa en enhetsnod för att få ett gränssnitt. Om din kod beror på ett AOSP-klassnamn går den sönder på en utgåva du inte testat.

TUN bär IP, TAP bär Ethernet

TUN är ett IP-gränssnitt. TAP är ett Ethernet-gränssnitt, och den skillnaden avgör om bryggning ens är möjlig. En TUN-enhet har ingen MAC-adress och ingen Ethernet-rubrik att vidarebefordra, så en TUN-klient kan inte ansluta till en brygga på lager 2, hur rutterna än skrivs.

VpnService ger dig IP-varianten, som är vad ett VPN på appnivå behöver och vad det kan stödja utan en rå Ethernet-stack. Om din design kräver bryggning tar plattformsvägen dig inte dit, och root-vägen bara om kärnan och enhetens behörigheter samarbetar.

Där root-vägen fortfarande vinner

Tre jobb knuffar folk bort från plattformsvägen. Bryggning på lager 2 kräver TAP. Fångst av trafik från appar utanför ditt eget VPN-område kräver synlighet som ramverket inte ger. Policyrouting som buildern aldrig exponerar, som en separat tabell för en destination, kräver regler du installerar själv.

Kostnaderna kommer senare: en upplåst bootloader eller en rootad avbildning, SELinux-policy som flyttar sig under dig, och en enhet som slutar bete sig korrekt efter en OTA. Ett rootat recept kan inte heller levereras via normal app-distribution, så de flesta team behåller det som en intern diagnostisk väg i stället för en produkt.

Valet mellan dem

Börja med gränssnittet du behöver, inte koden du har. Om svaret är en TUN-enhet med appbegränsade rutter är plattformsvägen kortare och överlever uppgraderingar. Om svaret innefattar Ethernet-ramar, fångst av andra appar eller routingregler som buildern avvisar, är du på root-territorium och bör budgetera för det som en driftkostnad.

En kontroll som avgör de flesta diskussioner: bygg plattformsvägen först, registrera establish()-resultatet separat från fjärrsessionens tillstånd, och bekräfta ett paket i varje riktning. Först då avgör du om den saknade förmågan motiverar att lämna den stödda vägen.

Relaterade artiklar