Błędne założenie jest takie, że aplikacja VpnService przełącza systemowy przełącznik i od tego momentu telefon jest chroniony. W rzeczywistości dzieje się coś węższego: aplikacja prosi o zgodę, buduje interfejs, czyta i zapisuje pakiety przez deskryptor pliku, a po zatrzymaniu zwraca interfejs. Jednocześnie może żyć tylko jeden taki tunel, a system może go odebrać bez pytania. Większość mylącego zachowania, które ludzie przypisują protokołowi, bierze się właśnie z tego modelu własności.

Traktuj każde przejście cyklu życia jako część projektu, a nie jako przypadek brzegowy.

Mit: aplikacja włącza VPN w telefonie

Przepływ zaczyna się od VpnService.prepare(), które zwraca Intent, który użytkownik musi zaakceptować, i trwa z establish(), które potem nadal może zawieść z powodów, których aplikacja nie kontroluje. Ustanowienie drugiego tunelu dezaktywuje pierwszy interfejs i zastępuje powiadomienie systemowe. Nigdzie w tej sekwencji aplikacja nie posiada przełącznika dla całego urządzenia; posiada deskryptor, który jej przyznano, oraz pakiety, które na niego trafiają. Nawet trwałe powiadomienie należy do systemu, a nie do projektu aplikacji.

Ponieważ interfejs jest przyznawany, a nie zagarniany, inny klient może zająć to miejsce w dowolnym momencie. Aplikacja dowiaduje się o tym przez wywołanie zwrotne, a nie przez wartość zwracaną.

Wymuszone zatrzymanie aplikacji widać w panelu powiadomień z tego samego powodu: system raportuje, co przyznał, a nie to, co aplikacja postanowiła pokazać.

Mit: zgodę daje się raz

Gdy system przekazuje tunel innemu klientowi, wysyła transakcję odwołania na binder, który klasa bazowa zwraca z onBind(). Aplikacja, która zastępuje ten binder własnym, musi sama przekazać transakcję odwołania, inaczej VpnService.onRevoke() nigdy się nie uruchomi, a usługa nadal pisze do deskryptora, którego już nie ma. Zgodę może też wycofać użytkownik, a system może zatrzymać usługę podczas restartu lub ponownego połączenia w trybie zawsze włączonym.

Radzenie sobie z tym to konkretna praca, a nie linia logu: przestań czytać deskryptor, zamknij go, wyczyść flagę połączenia i powiedz użytkownikowi, w jakim stanie się znajduje. Następnie zdecyduj, czy aplikacja ma odtworzyć połączenie, zapytać ponownie czy czekać, bo w konfiguracji zawsze włączonej system może sam przywrócić usługę.

Aplikacja, która traktuje odwołanie jako wyjątek, a nie jako stan, będzie zgłaszać połączenie, podczas gdy każdy zapisany przez nią pakiet jest odrzucany, a to najgorszy możliwy tryb awarii dla narzędzia prywatności.

Mit: interfejs filtruje ruch za ciebie

To, co przychodzi, to surowy IP. Przechwytywanie lub filtrowanie oznacza analizę nagłówków IP, TCP, UDP i IPv6, śledzenie przepływów i zapisywanie odpowiedzi z powrotem do deskryptora, co czyni aplikację przechwytującą małym stosem sieciowym, a nie udogodnieniem systemowym. protect() istnieje po to, by własne gniazdo transportowe aplikacji omijało jej tunel, zamiast przechwytywać samą siebie, i zwraca wartość logiczną, która zasługuje na sprawdzenie w każdym gnieździe utworzonym po podniesieniu tunelu.

Listy dozwolonych i zabronionych aplikacji w kreatorze decydują, czyj ruch trafia do interfejsu. Nie dają dostępu do ładunku poza pakietami IP, które interfejs niesie, i żadne API nie ujawnia tunelu innego klienta ani wymienia profile VPN systemu. Możliwości sieci mogą powiedzieć, że sieć jest transportem VPN, ale nie czyj to tunel.

Dlatego pytania o stan innych aplikacji dostają sprzeczne odpowiedzi: informacja po prostu nie jest ujawniana twojemu procesowi.

Czego naprawdę wymaga cykl życia

Śledź stany, które kontrolujesz: przygotowany, ustanowiony, odwołany, zatrzymany. Ponowne połączenie powinno być rozbiórką, po której następuje ustanowienie, z jednym właścicielem deskryptora i gniazda transportowego, bo dwa komponenty ścigające się o ustanowienie będą walczyć o jedno wolne miejsce. Uruchomienia w tle są ograniczone w nowoczesnym Androidzie, więc rozgłoszenie przy starcie nie jest gwarancją; użyj usługi pierwszoplanowej z jej powiadomieniem tam, gdzie poziom API na to pozwala.

Rejestruj przejście, a nie tylko stan końcowy, i utrzymuj maszynę stanów na tyle małą, by przetestować każdą ścieżkę. Stan, do którego nie możesz dojść w teście, to stan, który spotkasz w terenie bez logu.

Przetestuj cykl życia na urządzeniu

Emulatory potrafią fałszować łączność, ale nie przekonująco ścieżkę zgody i odwołania, więc uruchamiaj je na prawdziwym urządzeniu z zainstalowanym drugim klientem VPN, który ukradnie miejsce.

adb shell dumpsys connectivity | grep -i -A3 vpn
adb shell dumpsys activity services | grep -i -A2 vpnservice
adb logcat -s VpnService:V AndroidRuntime:E
adb shell am force-stop com.example.vpnapp

Uruchom tunel, potwierdź powiadomienie i interfejs, następnie podłącz drugiego klienta i obserwuj, czy wywołanie zwrotne odwołania działa i czy stan się czyści. Wymuś zatrzymanie aplikacji i sprawdź, czy coś nadal zgłasza żywy deskryptor.

Powtórz sekwencję po restarcie w trybie zawsze włączonym, bo ta ścieżka różni się od połączenia zainicjowanego przez użytkownika. Nie szukasz zielonej ikony, tylko dowodu, że aplikacja przestała używać deskryptora, którego już nie posiadała.

Powiązane artykuły