Android VpnService łączy się, ale żadne pakiety nie docierają do sieci
Tunel działa, a strony nadal nie wstają: sprawdź tryb odczytu, ochronę gniazd, obsługę TCP i listę tras, zanim obwinisz serwer VPN.
Tunel wstaje, ikona klucza się pojawia, a każde żądanie w przeglądarce nadal kończy się przekroczeniem czasu. W aplikacji VpnService taki układ zwykle oznacza, że pętla odczytu żyje, a wyjściowa połowa jest zepsuta, a nie że serwer odrzucił uścisk dłoni. Android już utworzył interfejs, przeniósł do niego ruch aplikacji i przekazał procesowi deskryptor pliku. Wszystko po tym deskryptorze to twój kod, a jednego brakującego wywołania wystarczy, by połknąć cały strumień.
Co daje system, a czego nie daje
establish() zwraca ParcelFileDescriptor dla interfejsu tun. System zapisuje w nim wychodzące pakiety IP i oczekuje, że proces je odczyta, zdecyduje, dokąd idą, i wyśle je przez zwykłe gniazdo. Pod spodem nie ma przekazywania w jądrze, więc gdy nikt nie czyta, pakiety są odrzucane, a liczniki interfejsu nigdy się nie ruszają.
Zacznij od liczenia, nie od zgadywania. Zwiększaj licznik przy każdym udanym odczycie i rejestruj cel oraz protokół pierwszych pakietów. Jeśli licznik stoi na zerze, gdy ruch jest generowany, błąd leży powyżej kodu przekazywania, co zawęża poszukiwania do konfiguracji buildera i trybu odczytu. Jeśli licznik się rusza, a przeglądarka nadal zawodzi, błąd jest w ścieżce wysyłania albo powrotu.
Odczyt blokujący decyduje, czy pętla się porusza
setBlocking(true) przełącza deskryptor w tryb blokujący i istnieje od API 21. W starszych wydaniach obejściem jest mały pomocnik JNI, który usuwa O_NONBLOCK przez fcntl na surowym deskryptorze. Tryb ma znaczenie, bo decyduje, co wolno zrobić odczytowi bez oczekującego pakietu.
W trybie nieblokującym taki odczyt wraca od razu z wartością ujemną, a pętla, która ignoruje wynik, kręci rdzeń, nie przenosząc żadnego ruchu. W trybie blokującym odczyt czeka, czego chce wątek przekazywania, ale wtedy wątku nie można już przerwać. Zamknięcie deskryptora to wspierany sposób, by odczyt zawiódł i pętla mogła wyjść. Zabicie procesu albo przerwanie wątku może zostawić następny start z deskryptorem, który nie pasuje już do interfejsu.
Czytaj jeden pakiet na wywołanie do bufora o rozmiarze MTU interfejsu, a zero lub wynik ujemny traktuj jako błąd, nie jako pusty pakiet.
protect() przed pierwszym wysłaniem, nie po nim
Każde gniazdo, które niesie pakiet kapsułkowany, musi najpierw zostać ochronione. Niechronione gniazdo podlega tym samym regułom routingu, które właśnie ustawiłeś, więc jego pakiety wracają do tunelu, a twoja własna pętla odczytu podnosi je ponownie. Tunel przekazuje wtedy własne wyjście bez końca, a z zewnątrz wygląda to jak połączenie, które się otwiera i nigdy nie odpowiada.
Kolejność to protect, potem connect lub bind, potem send. Wywołanie protect po connect może pozostawić wcześniejszą decyzję o trasie, więc gniazdo nadal korzysta ze złej ścieżki, mimo że wywołanie zwróciło true. Sprawdź zwracany boolean i głośno zawiedź, gdy jest false, zamiast wchodzić w wysyłkę, która nie może zadziałać.
Błąd uprawnień z sendto na gnieździe, które nigdy nie dotknęło protect(), jest typowym podpisem tego błędu.
Przykład UDP nie przenosi HTTPS
Przykład przekazywania, który kopiuje większość poradników, obsługuje tylko UDP. TCP wymaga uścisku dłoni, numerów sekwencji, potwierdzeń i retransmisji, a pętla kopiująca ładunek między deskryptorem tun a gniazdem nie daje żadnego z nich. Dlatego zapytanie DNS po UDP przechodzi, gdy każde żądanie HTTPS staje. Albo uruchom stos TCP w przestrzeni użytkownika, albo oddaj połączenia lokalnemu proxy, które kończy TCP na urządzeniu i przekazuje strumień.
Przetestuj podział, zanim cokolwiek przepiszesz: wyślij jedno zapytanie UDP i otwórz jedno połączenie TCP do tego samego hosta. Jeśli ścieżka UDP działa, a ścieżka TCP wisi na pierwszym SYN, problem jest w kodzie przekazywania, a nie w serwerze. To jedno porównanie oszczędza dużo czasu spędzonego na czytaniu logów serwera, które nie zawierają nic użytecznego.
Lista tras i filtry per aplikacja ograniczają to, co widzisz
addAddress, addRoute i addDns definiują, co system wysyła do tunelu. Cel spoza listy tras nigdy nie dociera do twojego deskryptora, więc żadna ilość logiki przekazywania tego nie naprawi. Rejestruj wywołania buildera i porównuj je z adresami, do których aplikacja naprawdę próbuje dotrzeć.
Wybór per aplikacja ma własny próg: addAllowedApplication i addDisallowedApplication wymagają API 21, a w starszych wydaniach tych metod po prostu nie ma. Projekt filtrujący po pakiecie nie może tam w ogóle działać.
Ponowne ustanowienie zostawia nieaktualne deskryptory
Ponowne wywołanie establish() zwraca nowy deskryptor i zamyka poprzedni interfejs. Stary deskryptor jest teraz nieważny, więc wątek wciąż na nim zablokowany dostaje błąd, a gniazda utworzone dla starej ścieżki mogą wymagać wymiany. onRevoke to wywołanie zwrotne dla tunelu, który użytkownik lub system odebrał. Trzymanie cyklu życia u jednego właściciela, który zatrzymuje czytelnika przed ponownym ustanowieniem, zapobiega większości stanów połączony-ale-martwy.
Czytaj każdy błąd według kroku, do którego należy. Reset połączenia z recvfrom oznacza, że zdalna strona zamknęła, podczas gdy twój kod jeszcze czytał. Błąd złego adresu wskazuje na parsowanie nagłówka lub błąd przesunięcia, nie na sieć.
Kolejność pracy: tryb blokujący włączony, jeden pakiet na odczyt, nagłówek sparsowany, gniazdo chronione, ładunek wysłany, czyste zamknięcie przy przekazaniu i lista tras obejmująca cel.