Varför din Android VpnService-tunnel slutar dirigera trafik
Fyra VpnService-antaganden som brister i produktion: en tunnel i taget, ruttomfång, listor per app och ordningen vid avstängning, med en kontroll för varje.
Fyra antaganden om Android VpnService kostade mig en gång större delen av en vecka, och ingen av dem handlade om nätverksprotokollet. Appen sa ansluten, nyckelikonen satt i statusfältet, och trafiken gick fortfarande ut över den vanliga rutten. Jag fortsatte att anta att tunneln var min tills jag stoppade den, att rutterna jag lade till var rutterna jag fick, och att ett anrop till stopSelf skulle ta ner gränssnittet med sig. Tjänsten betedde sig precis som dokumenterat. Min mentala modell var den trasiga delen.
Myt 1: tunneln står kvar tills du stoppar den
Android håller ett VPN-gränssnitt i taget. Sätt upp ett andra och plattformen ställer det inte i kö: den avaktiverar ditt och anropar onRevoke() på din tjänst. Om din kod bara river ner vid ett knapptryck lämnar ett övertagande en deskriptor och en läst tråd efter sig, och nästa start ärver båda.
VpnService-referensen säger uttryckligen att prepare() returnerar null när appen redan har samtycke, och en Intent när användaren fortfarande måste godkänna. Det samtycket kommer bara fram om du levererar intentionen genom startActivityForResult(). Hoppa över överlämningen och establish() returnerar null, vilket läses som ett tyst fel snarare än ett behörighetsproblem.
Always-on VPN lägger till en ägare du inte kontrollerar. Systemet kan hämta tillbaka tunneln efter uppstart eller efter en krasch, och det tränger undan en VPN-app som användaren startade för hand. Bygg onRevoke() så att den stänger samma resurser som din vanliga avstängning, och behandla den som en primär stoppväg i stället för ett hörnfall. Användare byter VPN-app; din kod måste tåla det.
Myt 2: addRoute fångar exakt det du listade
Rutterna avgör vilka paket som når din deskriptor, och ett fel i omfånget ger det galna fallet där en del av trafiken går genom tunneln och en del inte. addRoute("0.0.0.0", 0) tar hela IPv4, vilket betyder att varje paket nu kommer till dig och måste vidarebefordras eller släppas. En /32 för en enda värd tar nästan ingenting, och allt annat beter sig som om din VPN aldrig installerades.
Familjerna räknas separat. En rutt bara för IPv4 lämnar IPv6 på det underliggande nätet, så en dual-stack-värd kan fortfarande slå upp och ansluta utanför tunneln medan den ser perfekt ansluten ut inuti.
DNS följer samma logik. Servrar som lagts till med addDnsServer() går till appar som använder tunnelns resolver, medan en app med egen krypterad DNS-slutpunkt eller Private DNS satt till ett värdnamn ignorerar dem helt. När namn misslyckas för en app och fungerar för en annan, kontrollera resolvervägen innan du rör en enda rutt.
Myt 3: regler per app är en strömbrytare du kan vippa åt båda hållen
addDisallowedApplication() och addAllowedApplication() är två lägen, inte två filter. Byggaren accepterar en modell per tunnel, så en app som vill ha allt utom tre paket använder den förbjudna listan och lämnar den tillåtna listan tom. De två utesluter varandra, och folk upptäcker det vid körning efter att inställningsskärmen redan erbjudit båda reglagen.
Paketsynlighet biter också. Från Android 11 kan du bara namnge paket du ser, så en undantagslista byggd på en fråga som din app inte får köra missar poster i tysthet. Jämför listan med vad pakethanteraren faktiskt returnerar innan du litar på den.
Arbetsprofiler delar publiken igen. En tunnel som startas inne i profilen täcker den profilen och dess appar, medan den personliga sidan behåller sin egen anslutning. Det är ett omfångsbeslut som fattas en gång, och det hör hemma i gränssnittet, inte i en supportbiljett.
Myt 4: stopSelf avslutar tunneln
Det gör den inte. Gränssnittet lever i deskriptorn som establish() returnerar, och tjänsteobjektet kan dö medan deskriptorn förblir öppen. Stäng ParcelFileDescriptor först, släpp referensen, joina eller avbryt läs- och skrivtrådarna, och anropa sedan stopForeground(true) och stopSelf() i den ordningen.
En glömd deskriptor är det vanliga skälet till att en tjänst verkar överleva sin egen stoppbegäran, och den förklarar en inaktuell rutt som lever längre än appen som lade till den.
Det omvända felet är lika vanligt: att stänga deskriptorn medan en läst tråd är mitt i en läsning, och sedan låta den tråden hantera ett undantag som ingen har skrivit hantering för. Håll avstängningssekvensen på ett ställe och låt både användarens åtgärd och onRevoke() anropa in i den.
Vad du bör kontrollera när tunneln ser död ut
Skydda långlivade sockets med protect() innan tunneln kommer upp. Utan det routas tunnelns egen transport tillbaka in i tunneln och anslutningen fastnar på ett sätt som ser exakt ut som ett serverfel. Testa mot en adress som bara finns bakom det avlägsna nätverket, inte mot en offentlig sajt som svarar från cachen eller från den underliggande vägen.
Logga resultatet av establish(), rutterna, de undantagna paketen och orsaken till avstängningen. De fyra raderna skiljer ett samtyckesproblem från ett omfångsproblem och från ett livscykelproblem utan paketfångst.