Zašto vaš Android VpnService tunel prestaje usmjeravati promet
Četiri uvjerenja o VpnServiceu koja pucaju u produkciji: jedan tunel istovremeno, obim ruta, liste po aplikacijama i redoslijed gašenja, s provjerom za svako.
Četiri pretpostavke o Android VpnServiceu jednom su mi uzele veći dio sedmice, a nijedna nije bila o mrežnom protokolu. Aplikacija je govorila da je povezana, ikona ključa je stajala u statusnoj traci, a promet je i dalje izlazio redovnom rutom. Stalno sam pretpostavljao da je tunel moj dok ga ne zaustavim, da su rute koje sam dodao upravo rute koje dobijam i da će poziv stopSelf srušiti interfejs sa sobom. Servis se ponašao tačno kako je dokumentovano. Pokvareni dio bio je moj mentalni model.
Mit 1: tunel ostaje gore dok ga ne zaustavite
Android drži jedan VPN interfejs istovremeno. Uspostavite drugi i platforma ga ne stavlja u red, nego deaktivira vaš i poziva onRevoke() na vašem servisu. Ako vaš kod ruši sve samo na pritisak dugmeta, preuzimanje ostavlja deskriptor i čitalačku nit, a sljedeće pokretanje nasljeđuje oboje.
Referenca za VpnService izričito kaže da prepare() vraća null kada aplikacija već ima saglasnost, a Intent kada je korisnik tek treba da odobri. Ta saglasnost stiže samo ako namjeru prenesete kroz startActivityForResult(). Preskočite predaju i establish() vraća null, što se čita kao tihi neuspjeh, a ne kao problem s dozvolama.
Always-on VPN dodaje vlasnika kojim ne upravljate. Sistem može vratiti tunel nakon podizanja ili nakon pada, a presreće VPN aplikaciju koju je korisnik pokrenuo ručno. Gradite onRevoke() tako da zatvara iste resurse kao vaše uobičajeno gašenje i tretirajte ga kao glavni put zaustavljanja, a ne kao rubni slučaj. Korisnici mijenjaju VPN aplikacije; vaš kod to mora preživjeti.
Mit 2: addRoute hvata tačno ono što ste naveli
Rute odlučuju koji paketi stižu do vašeg deskriptora, a greška u obimu stvara ludu situaciju u kojoj dio prometa ide kroz tunel, a dio ne. addRoute("0.0.0.0", 0) uzima sav IPv4, što znači da svaki paket sada stiže kod vas i mora biti proslijeđen ili odbačen. /32 za jedan host ne uzima gotovo ništa, a sve ostalo se ponaša kao da VPN nikad nije instaliran.
Familije se broje odvojeno. Ruta samo za IPv4 ostavlja IPv6 na osnovnoj mreži, pa dual-stack host može i dalje razriješiti i povezati se izvan tunela dok unutar njega izgleda savršeno povezan.
DNS slijedi istu logiku. Serveri dodani s addDnsServer() idu aplikacijama koje koriste razrješivač tunela, dok aplikacija s vlastitim šifriranim DNS endpointom ili Private DNS postavljenim na ime hosta njih sasvim ignoriše. Kada imena padaju za jednu aplikaciju i rade za drugu, provjerite put razrješivača prije nego dirnete i jednu rutu.
Mit 3: pravila po aplikacijama su prekidač koji možete prebaciti na bilo koju stranu
addDisallowedApplication() i addAllowedApplication() su dva režima, a ne dva filtera. Graditelj prihvata jedan model po tunelu, pa aplikacija koja želi sve osim tri paketa koristi zabranjenu listu i ostavlja dozvoljenu listu praznom. Dvije se međusobno isključuju, a ljudi to otkriju u radu nakon što je ekran postavki već ponudio oba prekidača.
Vidljivost paketa takođe grize. Na Androidu 11 i novijem možete imenovati samo pakete koje vidite, pa će lista izuzetaka građena iz upita koji vaša aplikacija ne smije izvršiti tiho propustiti stavke. Uporedite listu s onim što upravitelj paketa zaista vraća prije nego joj povjerujete.
Radni profili ponovo dijele publiku. Tunel pokrenut unutar profila pokriva taj profil i njegove aplikacije, dok lična strana zadržava vlastitu povezanost. To je odluka o obimu donesena jednom i mjesto joj je u interfejsu, a ne u tiketu podrške.
Mit 4: stopSelf završava tunel
Ne završava. Interfejs živi u deskriptoru koji vraća establish(), a servisni objekt može umrijeti dok taj deskriptor ostaje otvoren. Prvo zatvorite ParcelFileDescriptor, otpustite referencu, pridružite ili prekinite čitalačku i pisačku nit, zatim pozovite stopForeground(true) i stopSelf() tim redom.
Zaboravljeni deskriptor uobičajen je razlog zašto se čini da servis preživi vlastiti zahtjev za zaustavljanjem, i objašnjava zastarjelu rutu koja živi duže od aplikacije koja ju je dodala.
Suprotna greška je jednako česta: zatvaranje deskriptora dok je čitalačka nit nasred čitanja, a zatim ostavljanje te niti da se nosi s izuzetkom za koji niko nije napisao obradu. Držite redoslijed gašenja na jednom mjestu i neka i korisnička radnja i onRevoke() pozivaju u njega.
Šta provjeriti kada tunel izgleda mrtav
Zaštitite dugotrajne sokete s protect() prije nego se tunel podigne. Bez toga se vlastiti transport tunela usmjerava natrag u tunel i veza se zaglavi na način koji izgleda tačno kao kvar servera. Testirajte prema adresi koja postoji samo iza udaljene mreže, a ne prema javnom sajtu koji odgovara iz keša ili s osnovnog puta.
Zabilježite rezultat establish(), rute, izuzete pakete i razlog gašenja. Ta četiri reda odvajaju problem saglasnosti od problema obima i od problema životnog ciklusa bez hvatanja paketa.