addDnsServer Bad Address: co Android naprawdę weryfikuje
Błąd Bad address z addDnsServer to lokalna kontrola formatu, a nie awaria DNS. Przeanalizuj literal, usuń loopback i wildcard, a potem sprawdź osiągalność.
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:
| Input | Result | Reason |
|---|---|---|
10.8.0.1 | accepted | IPv4 literal |
2606:4700:4700::1111 | accepted | IPv6 literal |
dns.example.com | rejected | hostname; numeric parsing does not resolve it |
10.8.0.1:53 | rejected | port belongs to your own config model |
https://dns.example.com/dns-query | rejected | DoH URL, a different configuration path |
127.0.0.1 | rejected | loopback is not a remote resolver |
0.0.0.0 or :: | accepted by the builder | parses 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.