addDnsServer Bad Address: o que o Android realmente valida
O erro Bad address do addDnsServer é uma verificação local de formato, não uma falha de DNS. Analise o literal, descarte loopback e wildcards e teste a acessibilidade.
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:
| 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 |
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.