Sprawdzenie, którego większość aplikacji na Androida używa do wykrywania VPN, to nazwa interfejsu i nigdy nie było sprawdzeniem VPN. Zgłasza, że istnieje jakiś interfejs z ciągiem, którego oczekiwał deweloper, co na jednym urządzeniu oznacza OpenVPN, a na innym coś zupełnie niezwiązanego. Ping jest tym samym rodzajem dowodu: dowodzi, że jedno miejsce docelowe odpowiedziało, a nie że pakiet przeniósł tunel. To, co platforma faktycznie udostępnia, to zbiór sieci i transporty, które każda z nich deklaruje, i to jest inne pytanie z inną odpowiedzią.

Mit: jeśli istnieje tun0, VPN jest aktywny

Aplikacje VPN na Androida to implementacje VpnService, które przekazują systemowi deskryptor pliku tun. Nazwa interfejsu pochodzi od tego, kto go utworzył: OpenVPN zwykle tworzy tun0, PPTP i L2TP tworzą ppp0, WireGuard nazywa interfejs od tunelu, a tunel IPsec może w ogóle nie mieć konwencjonalnej nazwy. Dlatego czytanie /proc/net/dev lub wyliczanie obiektów NetworkInterface szuka konwencji, a nie faktu.

Oba kierunki błędu są żywe. Urządzenie może mieć tun0 dla lokalnego tunelu, który nie ma nic wspólnego z VPN użytkownika, a całkowicie zdrowy tunel WireGuard może być niewidoczny dla sprawdzenia, które zna tylko ciąg tun0. Fałszywe pozytywy i fałszywe negatywy w tej samej funkcji czynią ją bezużyteczną jako diagnostykę, ponieważ zgłoszeniu do pomocy technicznej mówiącemu „nie wykryto VPN” nie można ufać w żadnym kierunku.

Dostęp również się zwęził. Nowsze wersje Androida ograniczają, co zwykła aplikacja może czytać z /proc, więc sprawdzenie oparte na nazwie, które działało na Androidzie 8, może zwrócić pustą listę na późniejszym urządzeniu bez żadnej zmiany kodu. Gdy sprawdzenie po cichu zawodzi w stronę otwartą, aplikacja dochodzi do wniosku, że nie ma VPN na sprzęcie, na którym tunel jest w porządku.

Mit: udany ping oznacza, że tunel przenosi ruch

Ping do hosta wewnętrznego udaje się zawsze, gdy istnieje trasa do tego hosta. Przy tunelu dzielonym host może być osiągalny przez Wi-Fi, podczas gdy tunel jest aktywny i robi coś zupełnie innego. Przy VPN dla poszczególnych aplikacji aplikacja wykonująca ping może być wykluczona z tunelu, więc echo ICMP wychodzi poza niego i wraca, co dowodzi czegoś przeciwnego do założenia sprawdzenia.

ICMP i TCP są również filtrowane osobno. Tunel, który zezwala na żądania echa, ale odrzuca port TCP potrzebny usłudze, przejdzie test ping i zawiedzie w aplikacji, a to jest reguła zapory po drugiej stronie, a nie właściwość urządzenia. Jedynym uczciwym odczytem udanego ping jest to, że to miejsce docelowe odpowiedziało na ten protokół w tej chwili.

Co platforma faktycznie raportuje

Od API 21 ConnectivityManager.getAllNetworks() zwraca każdą sieć, którą system zna, a getNetworkCapabilities(network) raportuje, co ta sieć deklaruje. Sieć VPN nosi TRANSPORT_VPN. Przejście przez wszystkie sieci odpowiada na jedno pytanie: czy na tym urządzeniu jest teraz aktywny jakikolwiek VPN?

Drugie, węższe pytanie wymaga innego wywołania. getActiveNetwork() zwraca sieć, której ruch aplikacji użyłby teraz, a sprawdzenie tej samej zdolności na niej odpowiada: czy mój ruch przejdzie przez VPN? Oba mogą się nie zgadzać, a ta niezgodność jest pouczająca. VPN aktywny, ale nie bieżący, wskazuje na routing dla poszczególnych aplikacji lub konfigurację tunelu dzielonego, a nie na brakujący tunel.

Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean viaVpn = caps != null
    && caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);

Chroń oba wywołania. Zdolności mogą być null dla sieci, która zniknęła między wyliczeniem a zapytaniem, a aktywna sieć może być null podczas przekazania. NET_CAPABILITY_NOT_VPN jest przydatne w drugą stronę, aby potwierdzić, że sieć zdecydowanie nie jest tunelem, a nie tylko że brakuje jej flagi transportu VPN.

NetworkInfo był tym samym mitem z API

ConnectivityManager.TYPE_VPN z NetworkInfo to druga generacja tego sprawdzenia i jest przestarzałe z pewnego powodu. W nowoczesnych wersjach obiekt zwracany dla VPN może być null, gdy tunel jest połączony, więc kod traktujący null jako „brak VPN” po cichu zgłasza fałszywy negatyw na urządzeniach, na których odpowiedź ma największe znaczenie.

Towarzysząca temu transmisja też nie jest lepsza. CONNECTIVITY_CHANGE ma udokumentowaną historię pomijania przejść VPN, w tym kompilacje, w których uruchamiała się tylko przy połączeniu lub rozłączeniu VPN, a nie przy każdej zmianie sieci. Odbiornik zbudowany na niej tworzy zmienną stanu, która odchodzi od widzenia platformy aż do następnego pełnego ponownego połączenia.

Reaguj na wywołania zwrotne zamiast odpytywać

Zarejestruj NetworkRequest, który wymaga TRANSPORT_VPN i usuwa NET_CAPABILITY_NOT_VPN, a następnie wywołaj registerNetworkCallback z tym żądaniem. Wtedy onAvailable i onLost powiedzą ci, kiedy deklaracja się zmienia, i to jest zdarzenie, które naprawdę cię interesuje. Wyrejestruj wywołanie zwrotne, gdy komponent się zatrzyma, inaczej aplikacja trzyma odwołanie do martwego ekranu, a wywołanie zwrotne nadal się uruchamia.

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

Wymaga to ACCESS_NETWORK_STATE, czyli zwykłego uprawnienia: bez roota, bez okna zgody użytkownika, bez uprawnienia VPN. Odpytywanie z timerem to alternatywa, do której ludzie wracają, i zużywa baterię, a wciąż pomija przejście krótsze niż interwał, czyli dokładnie to przejście, które zgłasza użytkownik.

Czego odpowiedź wciąż nie może ci powiedzieć

Żadne z tych API nie raportuje, czy tunel jest zdrowy, na którym serwerze się kończy ani czy ruch konkretnej aplikacji jest w nim. Sieć VPN może istnieć bez użytecznych tras, a tunel dla poszczególnych aplikacji może wykluczyć aplikację, która zadaje pytanie.

Dlatego zapisuj, co sprawdzenie widziało, nie tylko wartość logiczną. Zapisuj identyfikator sieci, zadeklarowane transporty i to, czy ta sieć była aktywna. Raport mówiący „nie wykryto VPN” jest bezużyteczny, natomiast raport mówiący, że sieć zadeklarowała TRANSPORT_VPN, ale getActiveNetwork() zwróciło Wi-Fi, wskazuje bezpośrednio na routing lub listę wykluczeń, i to jest poprawka, nad którą ktoś może pracować.

Powiązane artykuły