Nemůžete se Androidu zeptat, zda je VPN připojena. VpnService neexponuje žádný getter připojení a ikonu klíče ve stavovém řádku kreslí systém, ne něco, co by vaše aplikace mohla přečíst. Co udělat můžete, je položit síťovému zásobníku užší otázku: kterou síť používá provoz této aplikace a nese tato síť VPN transport. Tato příručka prochází kontroly v pořadí, které se vyhýbá falešně pozitivním výsledkům, od příkazu shell na zařízení až po callback, který necháte běžet v aplikaci.

Zkontrolujte zařízení ze shellu, než sáhnete na kód aplikace

Nejprve získejte základní linii. adb shell dumpsys connectivity vypíše každou síť, kterou systém zná, a každý blok uvádí své transporty, takže VPN síť se v tomto seznamu objeví s VPN. Přečtěte si blok výchozí sítě dřív než cokoli jiného, potom hledejte další sítě, které nesou stejný transport.

Dva výstupy odpovídají na různé otázky. Existence VPN sítě není totéž jako to, zda ji vaše aplikace používá, a na zařízení s pracovním profilem nebo druhým uživatelem může být obojí pravda současně, zatímco váš proces mluví s internetem přímo. Řádek aktivní sítě je ten, který rozhoduje, co dělají vaše sockety.

Zeptejte se na síť, kterou aplikace používá

V kódu je test schopností čtyři řádky. Vyžádejte ACCESS_NETWORK_STATE v manifestu a potom porovnejte schopnosti aktivní sítě s VPN transportem:

ConnectivityManager cm = (ConnectivityManager)
    context.getSystemService(Context.CONNECTIVITY_SERVICE);
Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean vpnInUse = caps != null
    && caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);

Odpověď je záměrně úzká: je výchozí síť aplikace právě teď VPN. Neříká nic o tom, která VPN to je, zda patří vám a zda tunel dosáhne svého vzdáleného konce. Berte true jako „provoz odchází nějakým tunelem“ a vlastní evidenci držte odděleně. Boolean, který potichu znamená tři věci najednou, je důvod, proč obrazovka nastavení nakonec hlásí VPN jako vypnutou, zatímco uživatel skrz ni prohlíží.

Když naopak potřebujete vědět, zda existuje jakákoli VPN síť, iterujte cm.getAllNetworks() a otestujte každou sadu schopností. Obě odpovědi se rozcházejí na zařízení s VPN pro jednotlivé aplikace, která pokrývá jiné aplikace, ale ne tu vaši, a tento rozpor je zdrojem většiny hlášení o chybách, která tvrdí, že aplikace nevidí VPN, jež je zjevně zapnutá.

Sledujte změny místo dotazování

Dotazování na časovači je pro tuto otázku špatný tvar, protože změny stavu přicházejí jako události. Zaregistrujte požadavek, který odpovídá VPN transportu, a nechte framework, aby vám řekl, kdy se odpovídající síť objeví, zmizí nebo změní:

NetworkRequest request = new NetworkRequest.Builder()
    .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
    .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
    .build();
cm.registerNetworkCallback(request, callback);

VPN síť nikdy nenese NET_CAPABILITY_NOT_VPN, takže explicitní odebrání dokumentuje záměr. Bez něj požadavek stále odpovídá, ale další člověk, který kód čte, si to musí domyslet.

Registrace patří rozsahu, který pozorování potřebuje, obvykle obrazovce nebo službě v popředí. Odregistrujte v onStop nebo onDestroy a nenechávejte callback připojený k aktivitě, která už neexistuje. Aplikace bez viditelné komponenty je špatný hostitel pro trvalého pozorovatele a callbacks, které zapomenete, jsou ty, které spadnou po změně konfigurace.

Držte tři otázky oddělené

Většina zmatení v této oblasti pochází z toho, že se tři různé kontroly berou jako jedna. Používají různé API a každá může být pravdivá, zatímco ostatní jsou nepravdivé.

QuestionHow you answer itWhat a yes means
Did the user grant VPN consent to my app?VpnService.prepare() returns nullConsent exists; the tunnel may still be down
Is the device sending default traffic through a VPN?TRANSPORT_VPN on the active networkSomething is tunneling, not necessarily your app
Did this request arrive through the VPN?Source address seen by the serverProof for that connection, nothing wider

Třetí řádek je ten, na který se lidé snaží odpovědět na klientovi, a tam to odpovědět nelze. Dejte VPN klientům vlastní fond adres a čtěte zdrojovou adresu na serveru; to je jediný důkaz, který přežije, když uživatel uprostřed relace přepne tunel.

Pořadí ověření, které vylučuje falešně pozitivní výsledky

Starší ukázkový kód, který je stále v oběhu, volá getNetworkInfo(ConnectivityManager.TYPE_VPN). Toto volání je zastaralé a v několika vydáních vracelo null i při připojené VPN, a proto tolik zkopírovaných úryvků vypadá rozbitě na zařízení, které funguje. Kontroly schopností se pasti vyhýbají a chovají se stejně od Androidu 5.0. Pokud udržujete kódovou základnu, která ho stále volá, volání nahraďte, místo abyste ho obalovali záložní cestou, protože záložní cesta byla tím, co špatnou odpověď původně vytvořilo.

Jedna zkratka si zaslouží varování. Porovnání veřejné adresy, kterou hlásí externí služba, s výstupní adresou VPN sice něco dokazuje o cestě, ale přidává do každé kontroly požadavek na třetí stranu, závisí na tom, že ta služba zůstane dostupná, a může být kešováno nebo blokováno. Držte ho jako ruční diagnostiku, nikdy jako kontrolu stavu uvnitř aplikace.

Provádějte kontroly v tomto pořadí a zastavte se u první, která nesouhlasí s hlášením uživatele: seznam transportů ve shellu, schopnosti aktivní sítě, události callback při přepínání tunelu, potom zdrojová adresa na straně serveru. Pokud všechny čtyři souhlasí a aplikace se stále chová špatně, chyba je ve vašem vlastním stavovém automatu, ne v tom, jak Android vidí síť.

Související články