Dwa objawy pojawiają się w tej samej pętli VpnService i oba są zrzucane na to samo: czytnik, który nigdy nie wraca, i zapis, który kończy się błędem ENOBUFS. To nie są warianty jednego błędu. Zablokowany odczyt to spór o kontrolę przepływu z pe-erem; nieudany zapis to pakiet, którego interfejs nie przyjmie. Jeśli potraktujesz je jako jeden problem, spędzisz dzień na strojeniu buforów, które nigdy nie były zaangażowane. Podział poniżej pokazuje, jak decyduję, którą połowę pętli oprzyrządować najpierw.

Jedna pętla, dwa niezależne punkty awarii

Twoja usługa czyta pakiet IP z deskryptora pliku TUN, sprawdza go i zapisuje odpowiedź do tego samego deskryptora. Wszystko, co się psuje na tej drodze, ujawnia się przy jednym z tych dwóch wywołań, dlatego oba są zgłaszane jako zepsuty tunel. Wywołania mają różne kontrakty. Odczyt czeka, aż jądro przekaże ci pakiet, a zapis przekazuje jądru pakiet, który może ono odrzucić.

Odczyty to ta łatwiejsza połowa do przemyślenia. Odczyt, który nic nie zwraca, albo czeka na ruch, który nigdy nie nadchodzi, albo czeka, bo pe-er przestał wysyłać.

Zablokowany odczyt to spór o kontrolę przepływu

Odczyt z urządzenia TUN blokuje się, dopóki pakiet nie będzie dostępny. To zachowanie zaprojektowane, i dlatego wątek zaparkowany w read wygląda normalnie na nieaktywnym łączu. Staje się zawieszeniem, gdy twój własny stos TCP nadal przyjmuje dane z lokalnej aplikacji, ale nigdy nie ogłasza okna wystarczająco dużego, by zdalny pe-er mógł wysłać więcej. Pe-er wypełnia ogłoszone okno, czeka na ACK i aktualizację okna, i zatrzymuje się. Wtedy twój czytnik nie ma nic do zwrócenia, a aplikacja widzi połączenie otwarte i ciche.

Częste przyczyny to stos, który pisze do gniazda bez śledzenia ogłoszonego okna, oraz taki, który zapomina wysłać aktualizację okna po opróżnieniu bufora. Z zewnątrz wyglądają identycznie: bajty wychodzą, nic nie wraca, żaden błąd nie jest zgłaszany. Zablokowanie wątku czytającego na parsowaniu, rozwiązywaniu DNS lub I/O dysku daje ten sam kształt objawu z niepowiązanego powodu, więc potwierdź, że wątek naprawdę jest w read, zanim obwinisz kontrolę przepływu.

Sprawdź kolejkę wysyłania pe-era i okno, które ostatnio widział. Jeśli pe-er trzyma dane, bo twoje okno wynosi zero, poprawka jest w twojej ścieżce ACK, a nie w odczycie TUN.

ENOBUFS to problem rozmiaru, nie brak bufora

Zapis, który zwraca ENOBUFS na interfejsie TUN, zwykle odrzuca długość pakietu. Interfejs ma MTU, większy pakiet nie może trafić do kolejki, a ponawianie tego samego pakietu zawodzi za każdym razem tak samo. Ten determinizm jest wskazówką. Prawdziwy brak bufora ustępuje, gdy ciśnienie spada, podczas gdy odrzucenie rozmiaru nie dba o to, jak długo czekasz.

TCP ma mechanizm dokładnie do tego i to jest krok, który ludzie pomijają: ograniczenie MSS. Gdy syntetyzujesz segmenty w przestrzeni użytkownika, wybierasz rozmiar ładunku, a wzięcie go z MTU lokalnego interfejsu zamiast z MTU tunelu emituje segmenty, których tunel nie uniesie. Ogranicz do mniejszego z dwóch. Ograniczenie obejmuje tylko TCP, więc zbyt duży datagram UDP musi zostać pofragmentowany lub odrzucony przez twój kod.

Rejestruj długość każdego pakietu, który zapisujesz podczas debugowania. Długość, która zawodzi, jest odpowiedzią i nie zmieni się między próbami.

Trzy pomiary, które rozdzielają przypadki

Nie potrzebujesz debuggera do żadnej z tych awarii. Trzy obserwacje wystarczą, by skierować się do właściwej sekcji, i zajmują mniej czasu niż przebudowa.

What you seeWhere to look
Reader parked, peer send queue empty, last advertised window zeroWindow updates in your ACK path
Write fails on one length and succeeds on a smaller oneMSS clamp and tunnel MTU
Both directions look fine, app still sees no dataProtected socket and routing loop

Jeśli pasuje trzeci wiersz, sama pętla jest w porządku, a problem jest tam, gdzie tunel nie może go zobaczyć: niechronione gniazdo transportowe, które niesie twój własny ruch VPN z powrotem do tunelu, co zwykle objawia się jako połączenie, które się udaje, a potem nic nie dostarcza.

Napraw instalację, od której zależą obie ścieżki

Żaden objaw nie jest wart ścigania, dopóki interfejs nie jest poprawnie zbudowany. Wywołania builder, które się liczą, to adres, trasa, MTU i zgoda, w tej kolejności:

Builder()
  .addAddress(10.8.0.2, 32)
  .addRoute(0.0.0.0, 0)
  .setMtu(1400)
  .establish()

Ustaw MTU na rzeczywistą wartość tunelu, zamiast zostawiać tę, której użył przykład. Wszystko, co później zapiszesz, jest ograniczone tą liczbą, a rozbieżność między ustawianym MTU a MSS, do którego ograniczasz, to najczęstszy sposób ponownego wprowadzenia błędu zapisu po poprawce. Następnie chroń gniazdo, które niesie ruch do samego serwera VPN. Bez protect te pakiety są kierowane z powrotem do twojego tunelu i połączenie nigdy się nie kończy, co wygląda jak zablokowany odczyt, ale jest pętlą routingu.

Zgody nie można obejść. Usługa uruchomiona bez tego, by prepare zwróciło null, nie dostaje interfejsu, a establish zwraca null zamiast rzucać wyjątek, więc sprawdź deskryptor przed uruchomieniem pętli. Dokumentacja VpnService to miejsce, by potwierdzić kontrakt builder, zamiast kopiować przykład.

Weryfikuj jeden kierunek naraz

Wyślij echo UDP przez tunel i obserwuj długości pakietów, zanim dotkniesz TCP. Następnie otwórz uścisk dłoni i potwierdź, że segmenty pozostają poniżej obliczonego MSS. Licz pakiety, a nie połączenia, aby zastój pojawił się jako luka w liczniku, a nie zawieszone gniazdo. Jeśli odczyty zatrzymują się po serii zapisów, podejrzanym jest okno; jeśli zapisy zawodzą natychmiast, to długość.

Potem uruchom ponownie urządzenie i powtórz raz, ponieważ VpnService, który działa tylko do pierwszej zmiany sieci, to problem routingu, a nie kontroli przepływu.

Powiązane artykuły