Il valore è arrivato a VpnService.Builder.addDnsServer come stringa e il builder lo ha rifiutato lì prima che qualsiasi cosa fosse stabilita. Il rifiuto è locale: nessun resolver è stato contattato, non esiste tunnel e ciò che ha fallito è un controllo di formato, non un guasto DNS.

Questo conta perché un’app VPN tipica ha tre ingressi DNS simili: un valore scritto o importato nel profilo, un valore inviato dal server durante la configurazione e Private DNS a livello di sistema. Solo i primi due raggiungono questa chiamata.

Un oggetto di preparazione controlla subito l’argomento

VpnService.Builder non è il tunnel. È un oggetto di preparazione che raccoglie indirizzi, rotte e server, e solo establish() trasforma quegli elenchi in un ParcelFileDescriptor vivo e in un’interfaccia. La voce DNS viene controllata quando chiami addDnsServer, non quando stabilisci.

Seguono due conseguenze: la traccia è breve e volutamente inutile, e un catch sciatto attorno all’intera costruzione lascia un tunnel con indirizzi e rotte ma senza resolver.

Il builder conserva qualsiasi elenco gli si dia; aggiungere un server che supera il controllo prova solo che la stringa è stata analizzata come letterale di indirizzo in fase di costruzione.

Cosa può essere l’argomento

Il parametro è un indirizzo, non un endpoint. Il riferimento Builder accetta un letterale IPv4 o IPv6, e l’analisi è solo numerica: android.net.InetAddresses.parseNumericAddress è la faccia pubblica di quel parser da API 29.

La mappatura pratica è questa:

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

La versione conta per come validi, non per ciò che il builder accetta. InetAddresses è disponibile da API 29, quindi su Android 10 l’analisi è una sola chiamata; addDnsServer(InetAddress) è la forma originale, String l’aggiunta più recente. Sotto API 29 InetAddress.getByName risolverà un nome host con una ricerca di rete bloccante.

Loopback viene rifiutato qui, un wildcard no

I client VPN desktop spesso puntano il proprio stub resolver su 127.0.0.1:53; copiarlo in un profilo Android importa un valore che descrive la macchina locale, e qui non c’è una macchina locale. Il builder rifiuta solo questo caso: un indirizzo null o loopback lancia IllegalArgumentException.

Un wildcard è il caso opposto e qui sbagliano la maggior parte delle letture. 0.0.0.0 e :: non sono loopback, quindi il builder li accetta; nulla viene lanciato in costruzione. L’interfaccia sale poi con una voce che non nomina alcuna destinazione. Catturarlo spetta al tuo validatore: tratta isAnyLocalAddress come input non valido.

Analizza una volta al confine della configurazione

La correzione economica è validare dove la stringa entra nel tuo modello:

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

Un risultato null diventa un errore a livello di campo con il testo colpevole ed è l’unica forma su cui il supporto può agire. Sul percorso di successo l’oggetto va direttamente in builder.addDnsServer(address), e cattura IllegalArgumentException in modo specifico, non Exception.

Non sostituire con un resolver pubblico quando il valore voluto viene rifiutato; un profilo che passa in silenzio a un DNS pubblico si connette, ma i nomi interni smettono di risolversi.

Conferma la raggiungibilità quando l’interfaccia esiste

L’accettazione da parte del builder è il primo punto di controllo, non l’ultimo. Quando l’interfaccia è attiva, LinkProperties.getDnsServers() riporta ciò che la rete ha davvero ricevuto e adb shell dumpsys connectivity stampa le stesse proprietà dall’esterno.

Private DNS è un controllo separato e non passa da questo argomento; il nome host di un provider DoT appartiene alle impostazioni di sistema, non a un campo IP.

Prima di cambiare di nuovo il parser del profilo, registra la stringa grezza e la sua origine, conferma che è un letterale e non un nome host, che non è loopback né wildcard, e passa l’oggetto analizzato.

Articoli correlati