Proč váš tunel Android VpnService přestane směrovat provoz
Čtyři přesvědčení o VpnService, která v produkci praskají: jeden tunel najednou, rozsah tras, seznamy podle aplikací a pořadí vypnutí, s kontrolou pro každé.
Čtyři předpoklady o Android VpnService mě jednou stály většinu týdne a ani jeden z nich nebyl o síťovém protokolu. Aplikace hlásila připojeno, ikona klíče seděla ve stavovém řádku a provoz přesto odcházel běžnou trasou. Pořád jsem předpokládal, že tunel je můj, dokud ho nezastavím, že trasy, které přidám, jsou trasy, které dostanu, a že volání stopSelf vezme rozhraní s sebou. Služba se chovala přesně tak, jak je zdokumentováno. Rozbitá byla moje mentální představa.
Mýtus 1: tunel vydrží, dokud ho nezastavíte
Android drží jedno rozhraní VPN najednou. Zřiďte druhé a platforma ho nezařadí do fronty: deaktivuje to vaše a zavolá onRevoke() na vaší službě. Pokud váš kód bourá všechno jen při stisku tlačítka, převzetí nechá za sebou deskriptor a čtecí vlákno a další start zdědí obojí.
Referenční dokumentace VpnService výslovně říká, že prepare() vrací null, když aplikace už má souhlas, a Intent, když ho uživatel ještě musí schválit. Tento souhlas dorazí jen tehdy, když záměr doručíte přes startActivityForResult(). Přeskočte předání a establish() vrátí null, což se čte jako tiché selhání, nikoli jako problém s oprávněními.
Always-on VPN přidává vlastníka, kterého neovládáte. Systém může tunel vrátit po startu nebo po pádu a předběhne aplikaci VPN, kterou uživatel spustil ručně. Stavějte onRevoke() tak, aby zavíral stejné prostředky jako vaše běžné vypnutí, a berte ho jako hlavní cestu zastavení, ne jako okrajový případ. Uživatelé střídají aplikace VPN; váš kód to musí přežít.
Mýtus 2: addRoute zachytí přesně to, co jste vyjmenovali
Trasy rozhodují, které pakety dorazí k vašemu deskriptoru, a chyba v rozsahu vytvoří šílenou situaci, kdy část provozu jde tunelem a část ne. addRoute("0.0.0.0", 0) zabere celý IPv4, což znamená, že každý paket teď dorazí k vám a musí být předán dál nebo zahozen. /32 pro jediného hosta nezabere téměř nic a všechno ostatní se chová, jako by váš VPN nikdy nebyl nainstalován.
Rodiny se počítají zvlášť. Trasa jen pro IPv4 nechává IPv6 na podkladové síti, takže host se dvěma zásobníky může stále resolvovat a připojovat se mimo tunel, i když uvnitř vypadá dokonale připojený.
DNS jde stejnou logikou. Servery přidané přes addDnsServer() jdou aplikacím, které používají resolver tunelu, zatímco aplikace s vlastním šifrovaným DNS koncovým bodem nebo s Private DNS nastaveným na jméno hosta je úplně ignoruje. Když jména selhávají u jedné aplikace a fungují u jiné, zkontrolujte cestu resolveru, než se dotknete jediné trasy.
Mýtus 3: pravidla podle aplikací jsou přepínač, který lze obrátit na kteroukoli stranu
addDisallowedApplication() a addAllowedApplication() jsou dva režimy, ne dva filtry. Tvůrce přijímá jeden model na tunel, takže aplikace, která chce všechno kromě tří balíčků, použije zakázaný seznam a povolený nechá prázdný. Ty dva se vylučují a lidé to zjistí za běhu poté, co obrazovka nastavení už nabídla oba přepínače.
Viditelnost balíčků taky kouše. Na Androidu 11 a novějším můžete pojmenovat jen balíčky, které vidíte, takže seznam výjimek postavený z dotazu, který vaše aplikace nesmí spustit, tiše vynechá položky. Porovnejte seznam s tím, co správce balíčků skutečně vrací, než mu uvěříte.
Pracovní profily znovu dělí publikum. Tunel spuštěný uvnitř profilu pokrývá ten profil a jeho aplikace, zatímco osobní strana si drží vlastní konektivitu. To je rozhodnutí o rozsahu učiněné jednou a patří do rozhraní, ne do tiketu podpory.
Mýtus 4: stopSelf ukončí tunel
Neukončí. Rozhraní žije v deskriptoru, který vrací establish(), a objekt služby může zemřít, zatímco deskriptor zůstává otevřený. Nejprve zavřete ParcelFileDescriptor, uvolněte referenci, připojte nebo přerušte čtecí a zapisovací vlákna a pak zavolejte stopForeground(true) a stopSelf() v tomto pořadí.
Zapomenutý deskriptor je obvyklý důvod, proč se zdá, že služba přežije svůj vlastní požadavek na zastavení, a vysvětluje zastaralou trasu, která žije déle než aplikace, jež ji přidala.
Opačná chyba je stejně častá: zavřít deskriptor, zatímco čtecí vlákno právě čte, a pak nechat to vlákno řešit výjimku, pro kterou nikdo nic nenapsal. Držte pořadí vypnutí na jednom místě a nechte do něj volat jak akci uživatele, tak onRevoke().
Co zkontrolovat, když tunel vypadá mrtvý
Chraňte dlouho žijící sockety pomocí protect(), než tunel vstane. Bez toho se vlastní transport tunelu směruje zpět do tunelu a spojení se zasekne způsobem, který vypadá přesně jako porucha serveru. Testujte proti adrese, která existuje jen za vzdálenou sítí, ne proti veřejnému webu, který odpovídá z mezipaměti nebo z podkladové cesty.
Zaznamenejte výsledek establish(), trasy, vyloučené balíčky a důvod vypnutí. Ty čtyři řádky oddělí problém se souhlasem od problému s rozsahem a od problému s životním cyklem bez zachytávání paketů.