Cztery założenia o Android VpnService kosztowały mnie kiedyś większość tygodnia i żadne nie dotyczyło protokołu sieciowego. Aplikacja mówiła połączono, ikona klucza siedziała na pasku stanu, a ruch i tak wychodził zwykłą trasą. Ciągle zakładałem, że tunel jest mój, dopóki go nie zatrzymam, że trasy, które dodaję, są trasami, które dostaję, i że wywołanie stopSelf zabierze ze sobą interfejs. Usługa zachowywała się dokładnie tak, jak opisano. Zepsuty był mój model mentalny.

Mit 1: tunel trzyma się, dopóki go nie zatrzymasz

Android trzyma jeden interfejs VPN naraz. Ustanów drugi, a platforma nie postawi go w kolejce: dezaktywuje twój i wywoła onRevoke() w twojej usłudze. Jeśli twój kod rozbiera wszystko tylko po naciśnięciu przycisku, przejęcie zostawia deskryptor i wątek czytający, a następny start dziedziczy oba.

Dokumentacja VpnService mówi wprost, że prepare() zwraca null, gdy aplikacja ma już zgodę, a Intent, gdy użytkownik musi ją jeszcze zatwierdzić. Ta zgoda trafia tylko wtedy, gdy przekażesz intencję przez startActivityForResult(). Pomiń przekazanie, a establish() zwróci null, co czyta się jako cicha porażka, a nie problem z uprawnieniami.

Always-on VPN dodaje właściciela, którego nie kontrolujesz. System może przywrócić tunel po rozruchu lub po awarii i wyprzedza aplikację VPN uruchomioną ręcznie przez użytkownika. Buduj onRevoke() tak, aby zamykał te same zasoby co twoje normalne wyłączanie, i traktuj go jako główną ścieżkę zatrzymania, a nie przypadek brzegowy. Użytkownicy zmieniają aplikacje VPN; twój kod musi to przetrwać.

Mit 2: addRoute przechwytuje dokładnie to, co wymieniłeś

Trasy decydują, które pakiety docierają do twojego deskryptora, a błąd zasięgu daje ten doprowadzający do szału przypadek, w którym część ruchu idzie przez tunel, a część nie. addRoute("0.0.0.0", 0) bierze cały IPv4, co oznacza, że każdy pakiet trafia teraz do ciebie i musi zostać przekazany dalej albo odrzucony. /32 dla jednego hosta nie bierze prawie nic, a wszystko inne zachowuje się, jakby twój VPN nigdy nie został zainstalowany.

Rodziny liczy się osobno. Trasa tylko IPv4 zostawia IPv6 w sieci bazowej, więc host z dwoma stosami nadal może rozwiązywać nazwy i łączyć się poza tunelem, wyglądając w nim na idealnie połączony.

DNS rządzi się tą samą logiką. Serwery dodane przez addDnsServer() trafiają do aplikacji korzystających z resolvera tunelu, podczas gdy aplikacja z własnym szyfrowanym punktem końcowym DNS albo z Private DNS ustawionym na nazwę hosta ignoruje je całkowicie. Kiedy nazwy zawodzą w jednej aplikacji, a w innej działają, sprawdź ścieżkę resolvera, zanim dotkniesz choćby jednej trasy.

Mit 3: reguły per aplikacja to przełącznik, który można przełączyć w obie strony

addDisallowedApplication() i addAllowedApplication() to dwa tryby, nie dwa filtry. Kreator przyjmuje jeden model na tunel, więc aplikacja, która chce wszystkiego poza trzema pakietami, używa listy zabronionych i zostawia listę dozwolonych pustą. Oba wykluczają się wzajemnie, a ludzie dowiadują się o tym w czasie działania po tym, jak ekran ustawień zaproponował już oba przełączniki.

Widoczność pakietów też gryzie. Od Androida 11 można nazwać tylko pakiety, które widzisz, więc lista wykluczeń zbudowana z zapytania, którego twoja aplikacja nie może wykonać, po cichu pominie wpisy. Porównaj listę z tym, co faktycznie zwraca menedżer pakietów, zanim jej zaufasz.

Profile pracy znów dzielą publiczność. Tunel uruchomiony w profilu obejmuje ten profil i jego aplikacje, a strona prywatna zachowuje własną łączność. To decyzja o zasięgu podjęta raz i jej miejsce jest w interfejsie, a nie w zgłoszeniu do wsparcia.

Mit 4: stopSelf kończy tunel

Nie kończy. Interfejs żyje w deskryptorze zwracanym przez establish(), a obiekt usługi może umrzeć, podczas gdy ten deskryptor pozostaje otwarty. Najpierw zamknij ParcelFileDescriptor, upuść referencję, dołącz do wątków czytającego i piszącego albo je przerwij, a potem wywołaj stopForeground(true) i stopSelf() w tej kolejności.

Zapomniany deskryptor to zwykły powód, dla którego usługa wygląda, jakby przeżyła własne żądanie zatrzymania, i wyjaśnia nieaktualną trasę, która żyje dłużej niż aplikacja, która ją dodała.

Odwrotny błąd jest równie częsty: zamknięcie deskryptora, gdy wątek czytający jest w trakcie czytania, a potem pozostawienie temu wątkowi wyjątku, dla którego nikt nie napisał obsługi. Trzymaj sekwencję wyłączania w jednym miejscu i pozwól, by zarówno działanie użytkownika, jak i onRevoke() wywoływały ją.

Co sprawdzić, gdy tunel wygląda na martwy

Chroń długowieczne gniazda przez protect(), zanim tunel wstanie. Bez tego transport samego tunelu jest trasowany z powrotem do tunelu, a połączenie zastyga w sposób wyglądający dokładnie jak awaria serwera. Testuj na adres, który istnieje tylko za siecią zdalną, a nie na publiczną stronę, która odpowiada z cache lub z trasy bazowej.

Zapisuj wynik establish(), trasy, wykluczone pakiety i powód wyłączenia. Te cztery linie oddzielają problem zgody od problemu zasięgu i problemu cyklu życia bez przechwytywania pakietów.

Powiązane artykuły