Hvorfor din Android VpnService-tunnel holder op med at route trafik
Fire antagelser om VpnService, der brækker i produktion: én tunnel ad gangen, ruteomfang, lister pr. app og nedlukningsrækkefølge, med et tjek til hver.
Fire antagelser om Android VpnService kostede mig engang det meste af en uge, og ingen af dem handlede om netværksprotokollen. Appen sagde forbundet, nøgleikonet sad i statuslinjen, og trafikken gik stadig ud over den normale rute. Jeg blev ved med at antage, at tunnelen var min, indtil jeg stoppede den, at de ruter, jeg tilføjede, var de ruter, jeg fik, og at et kald til stopSelf ville tage interfacet ned med sig. Tjenesten opførte sig præcis som dokumenteret. Min mentale model var den brudte del.
Myte 1: tunnelen bliver oppe, indtil du stopper den
Android holder ét VPN-interface ad gangen. Opret et nummer to, og platformen sætter den ikke i kø: den deaktiverer din og kalder onRevoke() på din tjeneste. Hvis din kode kun river ned ved et knaptryk, efterlader en overtagelse en deskriptor og en læsetråd, og næste start arver begge.
VpnService-referencen siger udtrykkeligt, at prepare() returnerer null, når appen allerede har samtykke, og en Intent, når brugeren stadig skal godkende. Det samtykke lander kun, hvis du leverer intenten gennem startActivityForResult(). Spring overdragelsen over, og establish() returnerer null, hvilket læses som en tavs fejl snarere end et tilladelsesproblem.
Always-on VPN tilføjer en ejer, du ikke styrer. Systemet kan bringe tunnelen tilbage efter opstart eller efter et nedbrud, og det fortrænger en VPN-app, som brugeren startede i hånden. Byg onRevoke(), så den lukker de samme ressourcer som din normale nedlukning, og behandl den som en primær stopvej i stedet for et hjørnetilfælde. Brugere skifter VPN-apps; din kode skal overleve det.
Myte 2: addRoute fanger præcis det, du listede
Ruterne afgør, hvilke pakker der når din deskriptor, og en fejl i omfanget giver det vanvittige tilfælde, hvor noget af trafikken går gennem tunnelen, og noget ikke. addRoute("0.0.0.0", 0) tager hele IPv4, hvilket betyder, at hver pakke nu ankommer til dig og skal videresendes eller droppes. En /32 til en enkelt vært tager næsten intet, og alt andet opfører sig, som om din VPN aldrig blev installeret.
Familierne tælles hver for sig. En rute kun for IPv4 lader IPv6 blive på det underliggende netværk, så en dobbeltstak-vært stadig kan slå op og forbinde uden for tunnelen, mens den ser perfekt forbundet ud indeni.
DNS følger samme logik. Servere tilføjet med addDnsServer() går til apps, der bruger tunnelens resolver, mens en app med sit eget krypterede DNS-endepunkt eller Private DNS sat til et værtsnavn ignorerer dem helt. Når navne fejler for én app og virker for en anden, så tjek resolverstien, før du rører en enkelt rute.
Myte 3: regler per app er en kontakt, du kan vippe begge veje
addDisallowedApplication() og addAllowedApplication() er to tilstande, ikke to filtre. Byggeren accepterer én model per tunnel, så en app, der vil have alt undtagen tre pakker, bruger den forbudte liste og lader den tilladte liste være tom. De to er gensidigt udelukkende, og folk finder det ud under kørsel, efter at indstillingsskærmen allerede har tilbudt begge knapper.
Pakkesynlighed bider også. På Android 11 og nyere kan du kun navngive pakker, du kan se, så en udelukkelsesliste bygget på en forespørgsel, din app ikke må køre, vil stille og roligt mangle poster. Sammenlign listen med det, pakkehåndteringen faktisk returnerer, før du stoler på den.
Arbejdsprofiler deler publikummet igen. En tunnel startet inde i profilen dækker den profil og dens apps, mens den personlige side beholder sin egen forbindelse. Det er en omfangsbeslutning, der træffes én gang, og den hører i brugerfladen, ikke i en supportbillet.
Myte 4: stopSelf afslutter tunnelen
Det gør den ikke. Interfacet lever i den deskriptor, som establish() returnerer, og tjenesteobjektet kan dø, mens deskriptoren forbliver åben. Luk ParcelFileDescriptor først, slip referencen, join eller afbryd læser- og skrivertrådene, og kald derefter stopForeground(true) og stopSelf() i den rækkefølge.
En glemt deskriptor er den sædvanlige grund til, at en tjeneste ser ud til at overleve sin egen stopanmodning, og den forklarer en forældet rute, der lever længere end den app, som tilføjede den.
Den omvendte fejl er lige så almindelig: at lukke deskriptoren, mens en læsetråd er midt i en læsning, og derefter lade den tråd håndtere en undtagelse, som ingen har skrevet kode til. Hold nedlukningssekvensen på ét sted, og lad både brugerhandlingen og onRevoke() kalde ind i den.
Hvad du skal tjekke, når tunnelen ser død ud
Beskyt langlivede sockets med protect(), før tunnelen kommer op. Uden det bliver tunnelens egen transport routet tilbage i tunnelen, og forbindelsen går i stå på en måde, der ser præcis ud som en serverfejl. Test mod en adresse, der kun findes bag det fjerne netværk, ikke et offentligt websted, der svarer fra cache eller fra den underliggende sti.
Log resultatet af establish(), ruterne, de udelukkede pakker og nedlukningsårsagen. De fire linjer adskiller et samtykkeproblem fra et omfangsproblem fra et livscyklusproblem uden en pakkefangst.