O valor chegou a VpnService.Builder.addDnsServer como string e o builder recusou-o ali antes de algo ser estabelecido. A recusa é local: nenhum resolver foi contactado, não existe túnel e o que falhou é uma verificação de formato, não uma falha de DNS.

Isso importa porque uma aplicação VPN típica tem três entradas DNS semelhantes: um valor escrito no perfil, um valor enviado pelo servidor e Private DNS ao nível do sistema. Apenas as duas primeiras chegam a esta chamada.

Um objeto de preparação verifica o argumento na hora

VpnService.Builder não é o túnel. É um objeto de preparação que reúne endereços, rotas e servidores, e só establish() transforma essas listas num ParcelFileDescriptor vivo e numa interface. A entrada DNS é verificada ao chamar addDnsServer.

Duas consequências: o rasto é curto e propositadamente pouco informativo, e um catch descuidado deixa um túnel sem resolver.

O builder guarda qualquer lista que lhe dê; acrescentar um servidor que passa a verificação prova apenas que a string foi analisada como literal de endereço na construção.

O que o argumento pode ser

O parâmetro é um endereço, não um endpoint. A referência do Builder aceita um literal IPv4 ou IPv6, e a análise é apenas numérica: android.net.InetAddresses.parseNumericAddress é a face pública desse analisador desde a API 29.

O mapeamento prático é este:

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

A versão afeta como valida, não o que o builder aceita. InetAddresses está disponível desde a API 29, por isso no Android 10 a análise é uma chamada; addDnsServer(InetAddress) é a forma original e String a adição mais recente. Abaixo da API 29, InetAddress.getByName resolve um nome de anfitrião com uma consulta de rede bloqueante.

Loopback é recusado aqui, um wildcard não

Os clientes VPN de ambiente de trabalho apontam muitas vezes o seu stub resolver para 127.0.0.1:53; copiar isso para um perfil Android introduz um valor que descreve a máquina local, e aqui não há máquina local. O builder recusa apenas este caso: um endereço null ou loopback lança IllegalArgumentException.

Um wildcard é o caso oposto e é aqui que a maioria das leituras erra. 0.0.0.0 e :: não são loopback, por isso o builder aceita-os; nada é lançado na construção. A interface sobe então com uma entrada que não nomeia destino. Detetar isso é trabalho do seu validador: trate isAnyLocalAddress como entrada inválida.

Analise uma vez no limite da configuração

A correção barata é validar onde a string entra no seu modelo:

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

Um resultado null torna-se um erro ao nível do campo com o texto culpado, e é a única forma sobre a qual o suporte pode agir. No caminho de sucesso o objeto entra diretamente em builder.addDnsServer(address), e capture IllegalArgumentException especificamente, não Exception.

Não substitua por um resolver público quando o valor pretendido é recusado; um perfil que troca em silêncio para um DNS público liga-se, mas os nomes internos deixam de resolver.

Confirme a acessibilidade quando a interface existir

A aceitação pelo builder é o primeiro ponto de controlo, não o último. Com a interface ativa, LinkProperties.getDnsServers() relata o que a rede recebeu realmente, e adb shell dumpsys connectivity imprime as mesmas propriedades de fora.

Private DNS é um controlo separado e não passa por este argumento; o nome de anfitrião de um fornecedor DoT pertence à definição do sistema, não a um campo IP.

Antes de alterar novamente o analisador do perfil, registe a string em bruto e a sua origem, confirme que é um literal e não um nome de anfitrião, que não é loopback nem wildcard, e entregue o objeto analisado.

Artigos relacionados