addDnsServer Bad Address: wat Android werkelijk valideert
De Bad address-fout van addDnsServer is een lokale formaatcontrole, geen DNS-storing. Parse de literal, verwijder loopback en wildcards en test daarna de bereikbaarheid.
De waarde bereikte VpnService.Builder.addDnsServer als string en de builder weigerde hem daar voordat er iets werd opgezet. Die weigering is lokaal: er is geen resolver benaderd, er is geen tunnel en wat faalde is een formaatcontrole, geen DNS-storing.
Dat telt omdat een typische VPN-app drie vergelijkbare DNS-invoer heeft: een waarde in het profiel, een waarde die de server tijdens de configuratie stuurt en Private DNS op systeemniveau. Alleen de eerste twee bereiken deze aanroep.
Een voorbereidingsobject controleert het argument meteen
VpnService.Builder is niet de tunnel. Het is een voorbereidingsobject dat adressen, routes en servers verzamelt, en alleen establish() maakt van die lijsten een levende ParcelFileDescriptor en een interface. De DNS-invoer wordt gecontroleerd bij het aanroepen van addDnsServer.
Twee gevolgen: de stacktrace is kort en opzettelijk nietszeggend, en een slordige catch rond de hele opbouw laat een tunnel achter zonder resolver.
De builder bewaart elke lijst die je geeft; een server toevoegen die de controle doorstaat bewijst alleen dat de string bij de opbouw als adresliteral is geparseerd.
Wat het argument mag zijn
De parameter is een adres, geen endpoint. De Builder-referentie neemt een IPv4- of IPv6-literal, en het parsen is alleen numeriek: android.net.InetAddresses.parseNumericAddress is sinds API 29 het publieke gezicht van die parser.
De praktische mapping ziet er zo uit:
| 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 |
De versie beïnvloedt hoe je valideert, niet wat de builder accepteert. InetAddresses is beschikbaar vanaf API 29, dus op Android 10 is het parsen één aanroep; addDnsServer(InetAddress) is de oorspronkelijke vorm, String de nieuwere toevoeging. Onder API 29 lost InetAddress.getByName een hostnaam op met een blokkerende netwerkzoekopdracht.
Loopback wordt hier geweigerd, een wildcard niet
Desktop-VPN-clients wijzen hun stub resolver vaak naar 127.0.0.1:53; dat in een Android-profiel kopiëren voert een waarde in die de lokale machine beschrijft, en hier is geen lokale machine. De builder weigert alleen dit geval: een null- of loopbackadres gooit IllegalArgumentException.
Een wildcard is het omgekeerde geval en hier gaat de meeste uitleg van deze fout mis. 0.0.0.0 en :: zijn geen loopback, dus de builder accepteert ze; bij de opbouw wordt niets gegooid. De interface komt dan op met een invoer die geen bestemming noemt. Dat vangen is het werk van je validator: behandel isAnyLocalAddress als ongeldige invoer.
Parse één keer op de configuratiegrens
De goedkope oplossing is valideren waar de string je model binnenkomt:
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 }
}
Een null-resultaat wordt een veldfout met de schuldige tekst, en dat is de enige vorm waarop support kan handelen. Op het succespad gaat het object direct in builder.addDnsServer(address), en vang IllegalArgumentException specifiek, niet Exception.
Vervang niet door een publieke resolver wanneer de bedoelde waarde wordt geweigerd; een profiel dat stil een publieke DNS instelt, verbindt nog steeds, maar interne namen worden niet meer opgelost.
Bevestig bereikbaarheid zodra de interface bestaat
De acceptatie door de builder is het eerste controlepunt, niet het laatste. Als de interface staat, meldt LinkProperties.getDnsServers() wat het netwerk echt heeft ontvangen, en adb shell dumpsys connectivity drukt dezelfde eigenschappen van buitenaf af.
Private DNS is een aparte regeling en loopt niet via dit argument; de hostnaam van een DoT-provider hoort bij de systeeminstelling, niet bij een IP-veld.
Voordat je de profielparser opnieuw wijzigt, log de ruwe string en de bron, bevestig dat het een literal is en geen hostnaam, dat het geen loopback en geen wildcard is, en geef het geparseerde object door.