VpnService protect() a wykluczenie aplikacji: co naprawia routing
Chroń gniazdo, które niesie tunel, albo wyklucz całą aplikację: trzy warunki, które decydują, jakiego zakresu potrzebuje Twój Android VPN.
Masz dwa sposoby, by trzymać własne pakiety tunelu poza tunelem: chronić gniazdo transportowe albo wyjąć całą aplikację z VPN. Rozwiązują różne problemy, a wybór szerszego dlatego, że łatwiej go podłączyć, jest dokładnie tym, przez co aplikacje tracą dostęp do sieci lokalnej i dzielonego DNS, nie zauważając tego. Zacznij od tego, co musi zostać poza tunelem, a potem wybierz najmniejszy zakres, który to osiąga.
Co protect() usuwa z tunelu
VpnService.protect() to wywołanie na pojedyncze gniazdo. Przekazujesz mu gniazdo lub deskryptor, który niesie tunel, a Android oznacza to gniazdo tak, by jego ruch korzystał z sieci pod spodem zamiast interfejsu VPN. Wszystko inne, co wysyła aplikacja, nadal podąża zwykłą trasą VPN.
Dokładnie ten wąski zakres jest sensem. Chronione gniazdo nie zmienia tego, jak Twój klient HTTP rozwiązuje nazwy, którego interfejsu używają wywołania analityki ani co dzieje się z drugim połączeniem otwartym później.
Trzy warunki, które decydują o zakresie
Pierwszy warunek to, czy tylko transport tunelu musi wydostać się na zewnątrz. Jeśli pozostały ruch aplikacji nadal ma iść przez VPN, ochrona gniazda jest właściwym zakresem. Drugi warunek to, czy kontrolujesz tworzenie gniazda. Biblioteka, która otwiera własne gniazdo głęboko w środku, nie zostawia Ci miejsca na wywołanie protect(), chyba że udostępnia hook albo przyjmuje gotowe gniazdo. Trzeci warunek to, czy aplikacja musi bezpośrednio dotrzeć do sieci lokalnej albo do captive portal, co jest innym wymaganiem niż utrzymanie tunelu przy życiu.
Wykluczenie aplikacji za pomocą addDisallowedApplication() to tępe narzędzie. Usuwa z VPN każde połączenie, które nawiązuje aplikacja, także te, które chciałeś sprawdzić, a na niektórych urządzeniach zmienia też zachowanie lokalnego wykrywania.
Trzecia opcja to przypisanie transportu do wybranej sieci za pomocą bindProcessToNetwork() lub uchwytu sieci na poziomie gniazda. Jest to przydatne, gdy urządzenie ma jednocześnie Wi-Fi i sieć komórkową, a tunel ma iść określonym łączem wyjściowym.
Wywołaj to, gdy transport powstaje
Wywołaj protect() w tym samym bloku, który tworzy transport, przed connect() lub send(). Jeśli wywołanie zwróci false, zamknij gniazdo i pozwól konfiguracji zawieść; kontynuowanie zostawia transport, którego pakiety mogą wrócić do Twojego własnego interfejsu.
Kształt jest dość mały, by mieścić się w jednym miejscu:
DatagramSocket transport = new DatagramSocket();
if (!protect(transport)) {
transport.close();
throw new IOException("transport not protected");
}
transport.connect(serverAddress);Trzy szczegóły gryzą tutaj. Po pierwsze,boolean to jedyny sygnał, jaki otrzymujesz, więc zapisz porażkę z dość kontekstem, by odróżnić cofniętą zgodę od odrzuconego wywołania. Po drugie,protect() obejmuje gniazdo, a nie numer deskryptora: jeśli zamkniesz gniazdo i później użyjesz tej samej liczby całkowitej dla innego gniazda, nowe gniazdo nie jest chronione. Po trzecie, gdy biblioteka natywna otwiera transport, chroń deskryptor, zanim kod natywny się połączy, inaczej trasa może być już wybrana.Dokumentacja VpnService to miejsce, w którym potwierdzasz, jakie przeciążenia naprawdę udostępnia Twój docelowy poziom API.
Testuj zablokowaną ścieżkę, nie tylko szczęśliwą
Weryfikuj przy podniesionym tunelu, bo to jedyny stan, w którym obie ścieżki mogą się różnić. Wyślij jedno żądanie przez chroniony transport, a drugie ze ścieżki kodu, która nigdy nie wywołała protect(), i porównaj, co widzi drugi koniec: chronione połączenie powinno dotrzeć z siecią pod spodem jako źródłem, a niechronione pokazuje własny adres tunelu i przesuwa jego liczniki bajtów. Na urządzeniu ss lub netstat pokazuje ten sam podział, zanim cokolwiek opuści radio.
Testuj cofnięcie osobno, bo zmiana zgody burzy interfejs bez dotykania Twojego kodu gniazda. Cofnij zgodę VPN albo zatrzymaj usługę, zbuduj transport od nowa i potwierdź, że protect() nadal zwraca true i że transport łączy się ponownie, gdy zgoda zostanie ponownie udzielona.
Jeśli chronione żądanie mimo to idzie przez tunel, wywołanie ochrony prawdopodobnie odbywa się już po routingu gniazda. Przenieś je wcześniej i przetestuj ponownie.
Trzymaj decyzję zapisaną obok konfiguracji transportu: które gniazda są chronione, który ruch aplikacji jest wykluczony i co robi aplikacja, gdy ochrona zawodzi. Właśnie ta linia powstrzymuje późniejszy refaktor przed rozszerzeniem wykluczenia na całą aplikację, bo jeden test zawodził. Daje też następnej osobie sposób sprawdzenia intencji bez inżynierii wstecznej manifestu.