Der Wert erreichte VpnService.Builder.addDnsServer als String, und der Builder lehnte ihn dort ab, bevor irgendetwas aufgebaut war. Diese Ablehnung ist lokal: kein Resolver wurde kontaktiert, es gibt keinen Tunnel, und gescheitert ist eine Formatprüfung, kein DNS-Ausfall.

Das zählt, weil eine typische VPN-App drei ähnliche DNS-Eingaben hat: einen im Profil eingetippten oder importierten Wert, einen vom Server während der Konfiguration gesendeten Wert und Private DNS auf Systemebene. Nur die ersten beiden erreichen diesen Aufruf.

Ein Vorbereitungsobjekt prüft das Argument sofort

VpnService.Builder ist nicht der Tunnel. Es ist ein Vorbereitungsobjekt, das Adressen, Routen und Server sammelt, und nur establish() macht aus diesen Listen ein lebendes ParcelFileDescriptor und ein Interface. Der DNS-Eintrag wird beim Aufruf von addDnsServer geprüft, nicht beim Aufbau.

Daraus folgen zwei Dinge. Der Stacktrace ist kurz und absichtlich nichtssagend: der Wert kam als einfacher String. Und ein sorgloser catch um den ganzen Aufbau hinterlässt einen Tunnel mit Adressen und Routen, aber ohne Resolver.

Der Builder behält jede Liste, die er bekommt. Einen Server hinzuzufügen, der die Prüfung besteht, beweist nur, dass der String zur Bauzeit als Adressliteral geparst wurde.

Was das Argument sein darf

Der Parameter ist eine Adresse, kein Endpoint. Die Builder-Referenz nimmt ein IPv4- oder IPv6-Literal, und das Parsen ist rein numerisch: android.net.InetAddresses.parseNumericAddress ist ab API 29 das öffentliche Gesicht dieses Parsers. Ein Hostname ist hier keine langsame Adresse, sondern ein nicht parsbaren String.

Das praktische Mapping sieht so aus:

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

Die Version beeinflusst, wie Sie validieren, nicht was der Builder akzeptiert. InetAddresses ist ab API 29 verfügbar, also ist das Parsen ab Android 10 ein Aufruf. addDnsServer(InetAddress) ist die ursprüngliche Form, String die neuere Ergänzung. Unter API 29 löst InetAddress.getByName einen Hostnamen mit einem blockierenden Netzwerklookup auf und macht aus einem Tippfehler einen langsamen Start.

Loopback wird hier abgelehnt, ein Wildcard nicht

Desktop-VPN-Clients zeigen ihren eigenen Stub-Resolver oft auf 127.0.0.1:53. In ein Android-Profil kopiert, importiert das einen Wert, der den lokalen Rechner beschreibt, und hier gibt es keinen lokalen Rechner. Der Builder lehnt nur diesen Fall ab: eine Null- oder Loopback-Adresse wirft IllegalArgumentException.

Ein Wildcard ist der umgekehrte Fall, und hier gehen die meisten Lesarten dieses Fehlers schief. 0.0.0.0 und :: sind kein Loopback, also akzeptiert der Builder sie; zur Bauzeit wird nichts geworfen. Das Interface kommt dann mit einem Eintrag hoch, der kein Ziel nennt. Das zu fangen ist Aufgabe Ihres Validators: behandeln Sie isAnyLocalAddress als ungültige Eingabe.

Einmal an der Konfigurationsgrenze parsen

Die billige Lösung ist, dort zu validieren, wo der String in Ihr Modell eintritt:

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 }
}

Ein Null-Ergebnis wird zu einem Feldfehler mit dem schuldigen Text, und nur diese Form kann der Support bearbeiten. Auf dem Erfolgspfad geht das Objekt direkt in builder.addDnsServer(address). Fangen Sie IllegalArgumentException gezielt, nicht Exception: ein breites Catch schluckt auch Nullzeiger-Fehler.

Ersetzen Sie den gewünschten Wert nicht durch einen öffentlichen Resolver, wenn er abgelehnt wird. Ein Profil, das still einen öffentlichen DNS einsetzt, verbindet weiter, aber interne Namen lösen nicht mehr auf.

Erreichbarkeit bestätigen, sobald das Interface steht

Die Annahme durch den Builder ist der erste Kontrollpunkt, nicht der letzte. Wenn das Interface steht, meldet LinkProperties.getDnsServers(), was das Netzwerk tatsächlich erhalten hat. adb shell dumpsys connectivity gibt dieselben Eigenschaften von außen aus.

Private DNS ist eine eigene Steuerung und läuft nicht durch dieses Argument. Ein DoT-Hostname gehört zur Systemeinstellung, nicht in ein IP-Feld.

Bevor Sie den Profilparser erneut ändern, arbeiten Sie in dieser Reihenfolge: rohen String und Quelle protokollieren, bestätigen dass es ein Literal und kein Hostname ist, dass es weder Loopback noch Wildcard ist, und das geparste Objekt weitergeben.

Ähnliche Artikel