Split tunneling w aplikacji VpnService psuje się w kilku konkretnych miejscach, a naprawa jest inna dla każdego. Klient pokazuje tylko jedno nieudane miejsce docelowe, więc pierwszym zadaniem jest ustalenie, na którą granicę trafiasz: trasy otrzymane przez builder, proxy, którego nie możesz uwierzytelnić, resolver, który tylko ogłosiłeś, albo lista aplikacji stosowana przez platformę na użytkownika. Właśnie dlatego trzymam kontrolę w czterech krokach. Biegnie w kolejności poniżej i zatrzymuje się na pierwszym błędzie.

Potwierdź, co builder przekazał platformie

VpnService.Builder to lista deklaracji. establish() zwraca ParcelFileDescriptor dla interfejsu wirtualnego, a platforma wysyła do tego deskryptora tylko ruch zgodny z prefiksami z addAddress i addRoute; addDnsServer mówi klientom, którego resolvera pytać. Miejsce docelowe poza wszystkimi dodanymi prefiksami nigdy nie dotrze do aplikacji, cokolwiek obiecuje interfejs. Ten jeden fakt wyjaśnia większość zgłoszeń zepsutego split tunneling przed zmianą kodu.

Zapisz w logu listy adresów, tras i DNS zbudowane przez builder, a następnie porównaj je z miejscami docelowymi, które zawodzą. Brakujący /32 dla hosta zarządzającego jest znacznie częstszy niż zepsuty tunel. Jeśli lista jest poprawna, idź dalej; jeśli nie, reszta listy kontrolnej to strata czasu.

excludeRoute istnieje dopiero od API 33

Wykluczanie po miejscu docelowym to żądanie, które najczęściej kończy się wątkiem wsparcia. builder.excludeRoute(IpPrefix) pojawiło się w API 33; wcześniej jedynym obsługiwanym przybliżeniem jest dodanie tras do wszystkiego poza wykluczonym prefiksem, co oznacza samodzielne obliczenie prefiksów dopełniających. Oba wywołania nie są lustrzanym odbiciem: jedno odejmuje od zbioru tras interfejsu, drugie buduje zbiór od zera, a starsze urządzenie rzuci wyjątek albo po cichu zignoruje nieznane wywołanie. Rozgałęziaj się na Build.VERSION.SDK_INT i testuj obie ścieżki na prawdziwym sprzęcie.

Obliczając dopełnienia, pilnuj wyrównania prefiksów. Wykluczenie /24 wewnątrz trasy /16 wymaga prefiksów pokrywających, które naprawdę opisują resztę, a nie jednego błędnego /8. Zapisz ostateczną listę i sprawdź, że wykluczony host nadal rozwiązuje się ścieżką poza tunelem.

Danych logowania proxy nie da się wyrazić

setHttpProxy(ProxyInfo) kieruje interfejs na proxy HTTP lub SOCKS, a URL PAC może wybierać między nimi dla każdego miejsca docelowego. Ani ProxyInfo, ani format PAC nie przenoszą nazwy użytkownika i hasła, więc proxy wymagające uwierzytelnienia zostaje poza API platformy. Aplikacje, które go potrzebują, zwykle osadzają stos przestrzeni użytkownika, tun2socks lub podobny silnik i same odpowiadają na wyzwanie 407.

Ten wybór ma konsekwencje. Stos przestrzeni użytkownika widzi tylko ruch, który do niego kierujesz, więc musi żyć w tej samej granicy deskryptora i dziedziczy to samo wymaganie protect() dla własnego gniazda uplink. Pomiń protect(), a tunel przechwyci własny transport, co objawia się uściskiem dłoni, który zastyga po pierwszym pakiecie. Testuj z proxy wymagającym danych logowania, bo otwarte proxy przechodzi ścieżką platformy i niczego nie dowodzi.

Listy na aplikację i DNS też są deklaracjami

Builder ma zarówno addAllowedApplication, jak i addDisallowedApplication, a wykluczają się wzajemnie: wywołanie obu rzuca wyjątek. Tak czy inaczej lista jest stosowana na użytkownika przy konfiguracji interfejsu, więc aplikacją profilu służbowego rządzi tunel profilu służbowego, a nie twój. Czego żadne API nie daje, to kontrola nad decyzją innej aplikacji: nie możesz z zewnątrz wepchnąć pakietu do swojego tunelu ani zamknąć bądź przekonfigurować już aktywnego VPN.

DNS idzie tym samym wzorem. addDnsServer ogłasza adres resolvera; nie pozwala aplikacji odpowiadać na zapytania. Jeśli chcesz samodzielnie rozwiązywać nazwy, uruchom resolver i skieruj jego adres do tunelu. Aplikacje używające DoT lub DoH do stałej nazwy hosta mogą ominąć ogłoszony adres, więc traktuj rozwiązywanie nazw jako najlepszą próbę, chyba że przechwytujesz także ścieżkę zapytania.

Manifest, always-on i test w czterech krokach

Kwalifikacja jest deklarowana w manifeście: BIND_VPN_SERVICE, filtr intencji android.net.VpnService i usługa pierwszoplanowa, jeśli tunel ma przetrwać w tle. Always-on VPN wymaga dodatkowo czystego onRevoke, bo system wywołuje go, gdy użytkownik zmienia ustawienia albo inna aplikacja przejmuje kontrolę.

Wykonaj kontrole po kolei i zatrzymaj się na pierwszym błędzie. Najpierw wypisz listy adresów, tras i DNS przekazane builderowi. Następnie potwierdź, że establish() zwróciło deskryptor i że gniazdo transportowe jest chronione. Trzeci krok: zażądaj jednego miejsca docelowego w prefiksie dołączonym i jednego w prefiksie wykluczonym. Czwarty: sprawdź, który resolver naprawdę odpowiedział, pytając o nazwę hosta, którą rozwiąże tylko twój resolver.

Ukończony uścisk dłoni nie dowodzi żadnego z późniejszych kroków. Każda kontrola izoluje jedną warstwę, więc powodzenie w drugim kroku nic nie mówi o czwartym.

Dokumentacja VpnService.Builder wylicza całą powierzchnię buildera. Wszystko, czego potrzeba ponad to, to inne API, inna aplikacja albo własny stos przestrzeni użytkownika, a to decyzja projektowa, nie błąd split tunneling.

Powiązane artykuły