Android VPN: deskryptor VpnService a urządzenie root tun
Aplikacje Android nie mogą same utworzyć tun0: co zwraca establish(), gdzie kończy się Binder i zaczyna JNI, i kiedy wygrywa urządzenie utworzone przez root.
Dwie ścieżki mogą postawić interfejs IP przed aplikacją Android, i obie kończą się tym samym rodzajem deskryptora pliku z bardzo różnymi właścicielami. Jedna prowadzi przez VpnService i jego builder, druga przez powłokę root i /dev/net/tun. Każde czytanie z tego deskryptora to pakiet wychodzący, a każdy zapis to pakiet przychodzący. Ścieżki różni to, kto trzyma politykę, co przetrwa aktualizację systemu i ile tablicy routingu wolno ci dotknąć.
Wybierz ścieżkę platformy, chyba że potrafisz nazwać konkretną rzecz, której ona odmawia. Ta odmowa to zwykle TAP, przechwytywanie poza własnym zestawem aplikacji albo reguła routingu, której framework nigdy nie ujawnia.
Na ścieżce platformy deklarujesz zamiar zamiast poleceń. Podajesz co najmniej jeden adres, żądane trasy, serwery DNS i opcjonalną listę aplikacji, a następnie usługa systemowa Androida wykonuje uprzywilejowaną konfigurację pod własną tożsamością. Nigdy nie poznajesz nazwy interfejsu z góry i nigdy nie wydajesz ioctl, który go tworzy, i o to właśnie chodzi.
Dwa sposoby na uzyskanie interfejsu IP
Ścieżka root to starszy przepis. Otwierasz /dev/net/tun, wywołujesz TUNSETIFF z IFF_TUN, podnosisz interfejs przez ip link i dodajesz adresy oraz trasy ręcznie. Każdy krok musisz wykonać dobrze sam, także te, które framework załatwiłby po cichu. Jeden brakujący wpis trasy daje użytkownikowi ten sam komunikat co martwy serwer.
Debugowanie różni się tak samo jak konfiguracja. Na ścieżce root interfejs albo istnieje, albo twoje ioctl zwróciło błąd, który możesz odczytać bezpośrednio. Na ścieżce platformy interfejs istnieje dopiero po udanym establish(), więc niepowodzenie przychodzi jako wyjątek lub deskryptor null, a nie jako węzeł urządzenia, w którym można dłubać.
Co establish() naprawdę zwraca
Builder.establish() ma trzy wyniki i wymagają one trzech różnych reakcji. Deskryptor oznacza, że interfejs jest skonfigurowany i twój do użycia, null oznacza, że żądanie nie przeszło, a rzucony wyjątek — że usługa systemowa je odrzuciła. Zlewanie null i sukcesu w jeden boolean to najczęstszy sposób, w jaki aplikacja raportuje działający tunel, który nigdy nie przenosi pakietu. Dokumentacja Builder podaje wymóg adresu i warto ją przeczytać, zanim obwinisz serwer.
Kierunki ustala deskryptor, a nie twój protokół. Odczyty dają pakiety wychodzące już wybrane dla VPN, a zapisy wstrzykują pakiety przychodzące z powrotem do interfejsu. Twój własny transport szyfruje, uwierzytelnia i ramkuje te pakiety, bo platforma zatrzymuje się na granicy pakietu.
Binder to jeden krok, JNI to inna oś
Żądanie buildera przechodzi do systemu przez Binder, a gdzieś pod tym platforma wywołuje kod natywny, aby skonfigurować urządzenie. To dwa różne mechanizmy ułożone w jednym wywołaniu. Binder to komunikacja międzyprocesowa między twoją aplikacją a usługą systemową, natomiast JNI to most z Javy do kodu natywnego wewnątrz procesu. Czytanie drzewa AOSP Androida 9 sprawia, że stos wygląda dosłownie, i warto pamiętać, że wewnętrzne nazwy klas i ścieżki plików zmieniają się między wydaniami.
To, co się nie zmienia, to kontrakt, na którym możesz polegać: builder, zwracany przez niego deskryptor i cykl życia usługi wokół niego. Żadna aplikacja nie powinna wywoływać wewnętrznej transakcji usługi ani odtwarzać węzła urządzenia, aby zdobyć interfejs. Jeśli twój kod zależy od nazwy klasy z AOSP, zepsuje się na wydaniu, którego nie testowałeś.
TUN niesie IP, TAP niesie Ethernet
TUN to interfejs IP. TAP to interfejs Ethernet, i ta różnica decyduje, czy mostkowanie jest w ogóle możliwe. Urządzenie TUN nie ma adresu MAC ani nagłówka Ethernet do przekazania, więc klient TUN nie może dołączyć do mostu warstwy 2, jakkolwiek zapisano by trasy.
VpnService daje ci wariant IP, którego potrzebuje VPN na poziomie aplikacji i który może obsłużyć bez surowego stosu Ethernet. Jeśli twój projekt wymaga mostkowania, ścieżka platformy tam cię nie doprowadzi, a ścieżka root tylko wtedy, gdy jądro i uprawnienia urządzenia będą współpracować.
Gdzie ścieżka root wciąż wygrywa
Trzy zadania wypychają ludzi ze ścieżki platformy. Mostkowanie warstwy 2 wymaga TAP. Przechwytywanie ruchu aplikacji spoza twojego zakresu VPN wymaga widoczności, której framework nie daje. Routing polityk, którego builder nigdy nie ujawnia, na przykład osobna tablica dla jednego celu, wymaga reguł, które instalujesz sam.
Koszty przychodzą później: odblokowany bootloader lub obraz z rootem, polityka SELinux, która przesuwa się pod tobą, i urządzenie, które po OTA przestaje zachowywać się poprawnie. Przepisu z rootem nie można też dostarczyć przez zwykłą dystrybucję aplikacji, więc większość zespołów trzyma go jako wewnętrzną ścieżkę diagnostyczną, a nie produkt.
Wybór między nimi
Zacznij od interfejsu, którego potrzebujesz, a nie od kodu, który masz. Jeśli odpowiedzią jest urządzenie TUN z trasami ograniczonymi do aplikacji, ścieżka platformy jest krótsza i przetrwa aktualizacje. Jeśli odpowiedź obejmuje ramki Ethernet, przechwytywanie innych aplikacji lub reguły routingu, które builder odrzuca, jesteś na terytorium root i powinieneś to uwzględnić w budżecie jako koszt operacyjny.
Kontrola, która rozstrzyga większość sporów: najpierw zbuduj ścieżkę platformy, zapisz wynik establish() osobno od stanu sesji zdalnej i potwierdź jeden pakiet w każdą stronę. Dopiero potem zdecyduj, czy brakująca możliwość uzasadnia opuszczenie wspieranej ścieżki.