Ochrona VpnService zawodzi, gdy inny VPN jest już aktywny
Android pozwala na jedno interfejs VPN na użytkownika. Wykonaj kontrole prepare, establish, protect i własności po kolei, a następnie wybierz cofnięcie lub wykluczenie dla aplikacji.
Twoja aplikacja wywołuje VpnService.prepare, dostaje null, buduje tunel, a deskryptor wygląda poprawnie, ale nic nie dociera do serwera. Albo prepare zwraca Intent, użytkownik go zatwierdza, a establish nadal zawodzi. Oba wyniki zwykle oznaczają to samo: inna aplikacja już posiada interfejs VPN tego użytkownika, a Twoje wywołania protect oznaczają gniazda, których tunel nie może użyć. Przejdź przez poniższe kontrole po kolei i zatrzymaj się na pierwszej, która zawodzi, bo późniejszy krok nie naprawi wcześniejszego.
Sprawdź miejsce VPN, zanim dotkniesz protect
Android daje każdemu użytkownikowi dokładnie jeden aktywny interfejs VPN. To reguła platformy, a nie uprawnienie, o które aplikacja może prosić, więc dwaj klienci, którzy obaj chcą być tunelem, nie mogą współistnieć.
Zawsze włączony VPN ustawiony dla innej aplikacji to najtrudniejszy przypadek, bo system utrzymuje tę aplikację połączoną i odrzuca przejęcie. Filtr ruchu taki jak AdGuard, klient korporacyjny lub VPN konsumencki może trzymać miejsce, gdy użytkownik zapomniał, że jest zainstalowany. Poproś użytkownika, aby otworzył Ustawienia i przeczytał wpis wymieniony pod VPN; nazwa tam jest właścicielem, z którym konkurujesz. Dopóki ten wpis nie zostanie rozłączony albo ustawienie zawsze włączone nie zostanie wyczyszczone, Twój VpnService nie może stać się interfejsem.
Krok 1: przeczytaj, co zwraca prepare
VpnService.prepare odpowiada na jedno pytanie: czy ta aplikacja może stać się VPN? Zwraca null, gdy zgoda już istnieje, i Intent, gdy użytkownik musi ją jeszcze zatwierdzić, co musisz uruchomić przez startActivityForResult. Traktuj zwrot null jako zgodę na kontynuację, nie jako dowód, że establish się powiedzie.
Intent intent = VpnService.prepare(context);
if (intent != null) {
startActivityForResult(intent, REQUEST_VPN);
}Jeśli ścieżka Intent wraca i użytkownik odmówił, prepare będzie nadal zwracać Intent przy następnej próbie. Zarejestruj kod wyniku zamiast ponawiać w pętli, bo odrzucona zgoda to decyzja użytkownika, a nie przejściowy błąd.
Krok 2: potwierdź, że interfejs należy do Twojej aplikacji
Po establish otrzymany ParcelFileDescriptor jest dowodem własności i warto go sprawdzić przed jakąkolwiek pracą z gniazdami. Deskryptor, który jest null lub już zamknięty, oznacza, że interfejs nigdy nie został utworzony, nawet jeśli nie pojawił się żaden wyjątek. Porównaj nazwę interfejsu z buildera z tą, którą system podaje dla aktywnego VPN, używając dumpsys na urządzeniu testowym. Jeśli się różnią, tunel, który zbudowałeś, nie jest tunelem przenoszącym ruch.
adb shell dumpsys connectivityTa sama niezgodność wyjaśnia połączoną ikonę z martwym ruchem. System pokazuje tunel drugiej aplikacji, a Twój kod trzyma deskryptor interfejsu, którego nie posiada.
Krok 3: wywołaj protect przed connect
Gdy interfejs istnieje, gniazda otwierane przez aplikację są domyślnie kierowane do tunelu. Zewnętrzny transport nie może przestrzegać tej reguły, inaczej enkapsulowany pakiet wróciłby do interfejsu, dlatego VpnService protect oznacza gniazdo, aby ominąć VPN i użyć sieci pod spodem. Wywołanie zwraca boolean, a jego ignorowanie to droga do gniazda, które wygląda zdrowo i nic nie przenosi.
Wywołaj protect przed connect lub bind, nigdy po. Ochrona musi być na miejscu, zanim gniazdo zostanie połączone lub powiązane, bo połączone gniazdo już wybrało trasę, a późniejszy protect nie przenosi go niezawodnie. Jeśli protect zwróci false, zatrzymaj się i ponownie sprawdź własność: zawodzi, gdy interfejs nie należy do Twojej aplikacji, a to dokładnie ten stan, który zostawia inny VPN. Zwrot false to sygnał, by wrócić do Kroku 1, a nie ponowić to samo gniazdo.
DatagramSocket socket = new DatagramSocket();
if (!vpnService.protect(socket)) {
return;
}
socket.connect(serverAddress, serverPort);Krok 4: powiąż proces, gdy gniazdo jest niewygodne
Niektóre transporty nie są jednym gniazdem: biblioteka może otworzyć kilka albo chcesz, aby każde połączenie w procesie pomocniczym używało tego samego wyjścia. ConnectivityManager.bindProcessToNetwork wiąże proces z wybraną siecią, a starszy setProcessDefaultNetwork robił to samo, zanim uznano go za przestarzały. Zażądaj połączenia pod spodem z NET_CAPABILITY_NOT_VPN, aby powiązanie wybrało sieć pod Twoim tunelem, a nie sam tunel.
NetworkRequest request = new NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();Powiązanie procesu nie jest drugim VPN. Wybiera wyjście dla tego procesu, co jest przydatne dla transportu i bezużyteczne do uruchamiania dwóch tuneli naraz.
Napraw urządzenie użytkownika, nie ścieżkę kodu
Nie ma API, które pozwala dwóm aplikacjom dzielić miejsce VPN, więc poprawka należy do stanu urządzenia. Zaproponuj użytkownikowi dwie konkretne drogi: rozłącz lub wyczyść zawsze włączone dla drugiej aplikacji, aby Twoje prepare mogło przejąć, albo otwórz ustawienia dla poszczególnych aplikacji tej aplikacji i wyklucz swoją aplikację z jej tunelu. Wykluczenie utrzymuje filtr działający dla wszystkiego innego i pozwala Twojej aplikacji używać zwykłego połączenia, co często jest wynikiem, którego użytkownik naprawdę chce.
Opisz obie drogi w dokumentacji wsparcia, bo użytkownicy rzadko wiedzą, która aplikacja posiada VPN. Poproś ich o przysłanie nazwy aktywnego VPN z Ustawień i wyników prepare, establish i protect z Twoich logów, a w ciągu minuty możesz powiedzieć, który krok zawiódł. Następnie zweryfikuj prawdziwym ruchem: zapytanie o nazwę, połączenie TCP i jedno żądanie przez tunel, a nie tylko połączoną ikonę. Poprawny deskryptor i zielony wskaźnik dowodzą co najwyżej własności, nigdy tego, że transport przetrwał reguły trasowania.
Jeśli wszystkie cztery kontrole przejdą, a ruch nadal ginie, własność nie jest już Twoim problemem. Przejdź do trasowania i osiągalności serwera, i przetestuj te same dane uwierzytelniające z urządzenia bez zainstalowanej aplikacji VPN.