Wartość dotarła do VpnService.Builder.addDnsServer jako ciąg znaków i builder odrzucił ją tam, zanim cokolwiek powstało. To odrzucenie jest lokalne: żaden resolver nie został wywołany, nie ma tunelu, a zawiodła kontrola formatu, nie DNS.

To ma znaczenie, bo typowa aplikacja VPN ma trzy podobne wejścia DNS: wartość w profilu, wartość wysyłaną przez serwer i Private DNS na poziomie systemu. Tylko dwa pierwsze docierają do tego wywołania.

Obiekt przygotowania sprawdza argument od razu

VpnService.Builder nie jest tunelem. To obiekt przygotowania, który zbiera adresy, trasy i serwery, i tylko establish() zamienia te listy w żywy ParcelFileDescriptor i interfejs. Wpis DNS jest sprawdzany przy wywołaniu addDnsServer.

Dwa skutki: ślad stosu jest krótki i celowo nieinformacyjny, a nieostrożny catch zostawia tunel bez resolvera.

Builder zachowuje każdą listę; dodanie serwera, który przejdzie kontrolę, dowodzi tylko, że ciąg został przeanalizowany jako literal adresu w czasie budowy.

Czym może być argument

Parametr jest adresem, nie endpointem. Dokumentacja Builder przyjmuje literal IPv4 lub IPv6, a analiza jest wyłącznie numeryczna: android.net.InetAddresses.parseNumericAddress to publiczna twarz tego parsera od API 29.

Praktyczne odwzorowanie wygląda tak:

InputResultReason
10.8.0.1acceptedIPv4 literal
2606:4700:4700::1111acceptedIPv6 literal
dns.example.comrejectedhostname; numeric parsing does not resolve it
10.8.0.1:53rejectedport belongs to your own config model
https://dns.example.com/dns-queryrejectedDoH URL, a different configuration path
127.0.0.1rejectedloopback is not a remote resolver
0.0.0.0 or ::accepted by the builderparses fine, but names no destination; only your own validator rejects it

Wersja wpływa na sposób walidacji, nie na to, co przyjmuje builder. InetAddresses jest dostępny od API 29, więc na Androidzie 10 analiza to jedno wywołanie; addDnsServer(InetAddress) to forma pierwotna, String nowszy dodatek. Poniżej API 29 InetAddress.getByName rozwiąże nazwę hosta blokującym wyszukiwaniem sieciowym.

Loopback jest tu odrzucany, wildcard nie

Klienci VPN na komputerach często kierują swój stub resolver na 127.0.0.1:53; skopiowanie tego do profilu Android wprowadza wartość opisującą maszynę lokalną, a tu maszyny lokalnej nie ma. Builder odrzuca tylko ten przypadek: adres null lub loopback rzuca IllegalArgumentException.

Wildcard to przypadek odwrotny i tu większość odczytań tego błędu się myli. 0.0.0.0 i :: nie są loopback, więc builder je przyjmuje; przy budowie nic nie jest rzucane. Interfejs wstaje wtedy z wpisem, który nie wskazuje celu. Wyłapanie tego należy do walidatora: traktuj isAnyLocalAddress jako nieprawidłowe wejście.

Analizuj raz na granicy konfiguracji

Tania poprawka to walidacja tam, gdzie ciąg wchodzi do modelu:

import android.net.InetAddresses
import java.net.InetAddress

private fun dnsLiteralOrNull(raw: String?): InetAddress? {
    val text = raw?.trim().orEmpty()
    if (text.isEmpty()) return null
    val address = try {
        InetAddresses.parseNumericAddress(text)
    } catch (e: IllegalArgumentException) {
        return null
    }
    return address.takeUnless { it.isAnyLocalAddress || it.isLoopbackAddress }
}

Wynik null staje się błędem na poziomie pola z winnym tekstem i to jedyna forma, na którą wsparcie może zareagować. Na ścieżce sukcesu obiekt trafia prosto do builder.addDnsServer(address), a IllegalArgumentException przechwytuj konkretnie, nie Exception.

Nie zastępuj publicznym resolverem, gdy zamierzona wartość jest odrzucana; profil, który po cichu wstawia publiczny DNS, nadal się łączy, ale nazwy wewnętrzne przestają się rozwiązywać.

Potwierdź osiągalność, gdy interfejs istnieje

Akceptacja przez builder to pierwszy punkt kontrolny, nie ostatni. Gdy interfejs wstanie, LinkProperties.getDnsServers() pokazuje, co sieć naprawdę otrzymała, a adb shell dumpsys connectivity wypisuje te same właściwości z zewnątrz.

Private DNS to osobna kontrola i nie przechodzi przez ten argument; nazwa hosta dostawcy DoT należy do ustawień systemowych, nie do pola IP.

Zanim ponownie zmienisz parser profilu, zapisz surowy ciąg i jego źródło, potwierdź, że to literal, a nie nazwa hosta, że nie jest to loopback ani wildcard, i przekaż przeanalizowany obiekt.

Powiązane artykuły