Detekce VPN na Androidu: proč tun0 a ping zavádějí
Název rozhraní je konvence; ping dokazuje, že jeden host byl dostupný. Zeptejte se NetworkCapabilities, co síť deklaruje, a pak reagujte na zpětná volání.
Kontrola, kterou většina aplikací pro Android používá k detekci VPN, je název rozhraní, a nikdy to nebyla kontrola VPN. Hlásí, že existuje nějaké rozhraní s řetězcem, který vývojář očekával, což na jednom zařízení znamená OpenVPN a na jiném něco zcela nesouvisejícího. Ping je stejný druh důkazu: dokazuje, že jedna destinace odpověděla, ne že paket přenesl tunel. Co platforma skutečně vystavuje, je sada sítí a transportů, které každá z nich deklaruje, a to je jiná otázka s jinou odpovědí.
Mýtus: když existuje tun0, VPN běží
Aplikace VPN pro Android jsou implementace VpnService, které systému předávají deskriptor souboru tun. Název rozhraní pochází od toho, kdo ho vytvořil: OpenVPN obvykle vytváří tun0, PPTP a L2TP vytvářejí ppp0, WireGuard pojmenovává rozhraní podle tunelu a tunel IPsec nemusí mít vůbec žádný obvyklý název. Čtení /proc/net/dev nebo výčet objektů NetworkInterface tedy hledá konvenci, ne fakt.
Oba směry chyby jsou živé. Zařízení může mít tun0 pro místní tunel, který s uživatelovou VPN nemá nic společného, a zcela zdravý tunel WireGuard může být neviditelný pro kontrolu, která zná jen řetězec tun0. Falešně pozitivní a falešně negativní výsledky v jedné funkci ji činí nepoužitelnou jako diagnostiku, protože tiketu podpory, který říká „VPN nebyla zjištěna“, nelze věřit v žádném směru.
Přístup se také zúžil. Novější verze Androidu omezují, co běžná aplikace může číst z /proc, takže kontrola založená na názvu, která fungovala na Androidu 8, může na pozdějším zařízení vrátit prázdný seznam bez jakékoli změny kódu. Když kontrola tiše selže v otevřeném směru, aplikace usoudí, že na hardwaru, kde je tunel v pořádku, žádná VPN není.
Mýtus: úspěšný ping znamená, že tunel přenáší provoz
Ping na interní host uspěje vždy, když existuje trasa k tomuto hostu. Při rozděleném tunelování může být host dostupný přes Wi-Fi, zatímco tunel běží a dělá něco úplně jiného. Při VPN pro jednotlivé aplikace může být aplikace, která ping provádí, z tunelu vyloučena, takže ICMP echo vychází mimo něj a vrací se, což dokazuje opak toho, co kontrola předpokládá.
ICMP a TCP se také filtrují odděleně. Tunel, který povoluje echo požadavky, ale zahazuje TCP port, který služba potřebuje, projde testem ping a v aplikaci selže, a to je pravidlo firewallu na druhé straně, nikoli vlastnost zařízení. Jediné poctivé čtení úspěšného pingu je, že tato destinace v tomto okamžiku odpověděla tímto protokolem.
Co platforma skutečně hlásí
Od API 21 vrací ConnectivityManager.getAllNetworks() každou síť, kterou systém zná, a getNetworkCapabilities(network) hlásí, co daná síť deklaruje. Síť VPN nese TRANSPORT_VPN. Procházení všech sítí odpovídá na jednu otázku: je na tomto zařízení právě teď aktivní nějaká VPN?
Druhá, užší otázka vyžaduje jiné volání. getActiveNetwork() vrací síť, kterou by provoz aplikace použil právě teď, a kontrola stejné schopnosti na ní odpovídá: šel by můj provoz přes VPN? Obě se mohou rozcházet a toto rozcházení je informativní. VPN, která běží, ale není aktivní, ukazuje na směrování pro jednotlivé aplikace nebo na konfiguraci rozděleného tunelu, nikoli na chybějící tunel.
Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean viaVpn = caps != null
&& caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
Chraňte obě volání. Schopnosti mohou být null pro síť, která zmizela mezi výčtem a dotazem, a aktivní síť může být null během předání. NET_CAPABILITY_NOT_VPN je užitečná v opačném směru — k potvrzení, že síť rozhodně není tunelem, místo aby jen postrádala příznak transportu VPN.
NetworkInfo byl stejný mýtus s API
ConnectivityManager.TYPE_VPN s NetworkInfo je druhá generace této kontroly a je zastaralá z dobrého důvodu. V moderních verzích může být objekt vrácený pro VPN null, zatímco tunel je připojen, takže kód, který null považuje za „žádná VPN“, tiše hlásí falešně negativní výsledek na zařízeních, kde je odpověď nejdůležitější.
Ani vysílání, které ji obvykle doprovází, není lepší. CONNECTIVITY_CHANGE má zdokumentovanou historii vynechaných přechodů VPN, včetně sestavení, kde se spouštělo jen při připojení nebo odpojení VPN, a ne při každé změně sítě. Posluchač postavený na něm vytváří stavovou proměnnou, která se odchyluje od pohledu platformy až do dalšího plného opětovného připojení.
Reagujte na zpětná volání místo dotazování
Zaregistrujte NetworkRequest, který vyžaduje TRANSPORT_VPN a odstraňuje NET_CAPABILITY_NOT_VPN, a poté zavolejte registerNetworkCallback s tímto požadavkem. onAvailable a onLost vám pak řeknou, kdy se deklarace změní, což je událost, která vás skutečně zajímá. Zrušte registraci zpětného volání, když komponenta skončí, jinak si aplikace drží odkaz na mrtvou obrazovku a zpětné volání se stále spouští.
NetworkRequest req = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();
cm.registerNetworkCallback(req, callback);
To vyžaduje ACCESS_NETWORK_STATE, což je běžné oprávnění: žádný root, žádný dialog se souhlasem uživatele, žádné oprávnění VPN. Dotazování na časovači je alternativa, ke které se lidé uchylují, a spotřebovává baterii, a přitom stále vynechává přechod kratší než interval, což je přesně ten přechod, který uživatel hlásí.
Co vám odpověď stále nemůže říct
Žádné z těchto API nehlásí, zda je tunel zdravý, na kterém serveru končí a zda je provoz určité aplikace uvnitř něj. Síť VPN může existovat bez použitelných tras a tunel pro jednotlivé aplikace může vyloučit aplikaci, která se ptá.
Proto zaznamenejte, co kontrola viděla, ne jen boolean. Zaznamenejte identifikátor sítě, deklarované transporty a zda byla tato síť aktivní. Zpráva, která říká „VPN nebyla zjištěna“, je nepoužitelná, kdežto zpráva, že síť deklarovala TRANSPORT_VPN, ale getActiveNetwork() vrátil Wi-Fi, ukazuje přímo na směrování nebo na seznam vyloučení, a to je oprava, na které může někdo pracovat.