Skip to main content


DNS em Go​


DNS, que significa Domain Name System, é um sistema responsável por associar nomes de domínio a diferentes informações usadas na comunicação pela rede. Uma de suas funções mais conhecidas é permitir que aplicações encontrem endereços IP a partir de nomes como example.com, evitando que seja necessário conhecer diretamente o endereço numérico de cada servidor.


Quando uma aplicação precisa se conectar a um servidor identificado por um domínio, normalmente é necessário descobrir qual endereço IP corresponde àquele nome. Esse processo é conhecido como resolução de nomes e pode envolver consultas a servidores DNS, arquivos locais de configuração e mecanismos de resolução disponibilizados pelo sistema operacional. O resultado permite que a aplicação continue com a operação de rede usando um endereço adequado.


O DNS também armazena outras informações além de endereços IP. Podemos consultar registros relacionados a servidores de e-mail, aliases de domínio e informações textuais usadas por diferentes serviços. Em Go, o pacote net fornece funções para realizar essas consultas, permitindo desenvolver ferramentas de diagnóstico e aplicações que dependem da resolução de nomes sem precisar implementar diretamente o protocolo DNS.



Resolvendo nomes para IP​


A resolução de nomes é o processo responsável por obter um ou mais endereços IP associados a determinado nome. Quando informamos um domínio como example.com, o sistema de resolução procura as informações necessárias para identificar os endereços correspondentes. Esses endereços podem usar IPv4 ou IPv6, dependendo dos registros disponíveis e da configuração do ambiente.


Um mesmo domínio pode possuir vários endereços IP. Isso acontece porque diferentes servidores podem responder pelo mesmo serviço ou porque a infraestrutura pode distribuir as conexões entre diferentes destinos. Por isso, uma consulta de resolução não retorna obrigatoriamente apenas um endereço, e a quantidade ou a ordem dos resultados pode variar entre diferentes consultas.


Em Go, podemos resolver nomes usando funções do pacote net, como net.LookupHost e net.LookupIP. Essas funções consultam o mecanismo de resolução configurado para a aplicação, que pode considerar informações locais e servidores DNS. Para entender esse comportamento, vamos começar com uma consulta simples que retorna os endereços associados a um domínio.



Resolvendo um domínio com net.LookupHost​


A função net.LookupHost recebe uma string que representa o nome que será resolvido e retorna um slice contendo os endereços encontrados. Os resultados são representados como strings, permitindo apresentá-los diretamente ou convertê-los posteriormente para outros tipos. A função também retorna um erro quando a resolução não pode ser concluída.


Podemos criar um programa que recebe um domínio definido no código e apresenta todos os endereços encontrados. Nesse primeiro exemplo, vamos consultar example.com, que é um domínio reservado para documentação e exemplos técnicos.

package main

import (
"fmt"
"net"
"os"
)

func main() {
// Resolve o domínio usando o resolver padrão.
enderecos, err := net.LookupHost("example.com")

if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro na resolução:",
err,
)

return
}

// Percorre todos os endereços encontrados.
for _, endereco := range enderecos {
fmt.Println(endereco)
}
}

A função net.LookupHost consulta os endereços associados ao domínio e retorna um slice de strings. O laço for percorre os resultados e apresenta cada endereço separadamente, permitindo visualizar os valores encontrados. Dependendo da configuração do domínio e do ambiente, podemos receber endereços IPv4, IPv6 ou ambos.


A saída poderá conter endereços semelhantes a estes:

104.18.26.120
104.18.27.120
2606:4700::6812:1a78
2606:4700::6812:1b78

Esses valores representam apenas um exemplo de resultado, pois os endereços associados a um domínio podem mudar. Também não devemos assumir que a ordem apresentada será sempre a mesma, porque o mecanismo de resolução pode organizar os endereços conforme suas próprias regras. Quando precisamos trabalhar com um endereço específico, normalmente devemos considerar o protocolo de rede e as características da operação que será executada.


A função net.LookupHost usa internamente um contexto sem prazo definido pela aplicação. Isso significa que não conseguimos estabelecer diretamente um timeout para essa chamada usando seus argumentos. Quando precisamos controlar a duração da resolução ou cancelar uma consulta, podemos trabalhar com o tipo net.Resolver, que possui métodos capazes de receber um context.Context.



Resolvendo endereços com net.LookupIP​


A função net.LookupIP também permite resolver nomes, mas retorna os endereços como valores do tipo net.IP, em vez de strings. Esse tipo representa um endereço IP e fornece métodos para examinar sua estrutura ou convertê-lo para uma representação textual. Isso é conveniente quando precisamos trabalhar diretamente com endereços de rede dentro do programa.


Podemos consultar um domínio e percorrer os valores retornados, usando o método String para apresentar cada endereço. O programa continuará recebendo uma lista de resultados, porque um domínio pode possuir mais de um endereço disponível.

package main

import (
"fmt"
"net"
"os"
)

func main() {
// Resolve o domínio e retorna valores net.IP.
ips, err := net.LookupIP("example.com")

if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro ao resolver domínio:",
err,
)

return
}

// Apresenta os endereços encontrados.
for _, ip := range ips {
fmt.Println(ip.String())
}
}

A função net.LookupIP retorna um slice do tipo []net.IP, permitindo trabalhar com os endereços encontrados sem depender exclusivamente de sua representação textual. O método String converte cada endereço para uma string adequada à apresentação, respeitando o formato correspondente a IPv4 ou IPv6. Esse comportamento facilita o uso dos resultados em mensagens de diagnóstico e ferramentas CLI.


O tipo net.IP também possui métodos como To4 e To16, usados para obter representações específicas de endereços IP. Entretanto, esses métodos precisam ser interpretados corretamente, porque a representação interna de um endereço não deve ser confundida com o tipo de registro DNS que originou o resultado. Quando precisamos consultar exclusivamente IPv4 ou IPv6, podemos usar métodos mais específicos de net.Resolver.



Resolvendo nomes com context​


O tipo net.Resolver representa um mecanismo responsável por executar consultas de resolução de nomes. Diferentemente das funções globais mais simples, seus métodos recebem um contexto que permite associar cancelamentos e limites de tempo às operações. Isso é útil em aplicações de rede que precisam evitar consultas demoradas ou interromper tarefas que deixaram de ser necessárias.


Podemos usar net.DefaultResolver, que representa o resolver padrão disponibilizado pelo pacote net, e chamar seu método LookupIPAddr. Esse método recebe um contexto e um nome, retornando uma lista de valores do tipo net.IPAddr, que representam endereços IP e podem incluir informações adicionais relacionadas ao endereço.

package main

import (
"context"
"fmt"
"net"
"os"
"time"
)

func main() {
// Cria um contexto com limite de três segundos.
ctx, cancel := context.WithTimeout(
context.Background(),
3*time.Second,
)

defer cancel()

// Resolve o domínio usando o contexto configurado.
enderecos, err := net.DefaultResolver.LookupIPAddr(
ctx,
"example.com",
)

if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro na resolução:",
err,
)

return
}

// Apresenta os endereços encontrados.
for _, endereco := range enderecos {
fmt.Println(endereco.IP)
}
}

A função context.WithTimeout cria um contexto derivado com limite de três segundos e retorna uma função responsável pelo cancelamento. Registramos essa função com defer cancel() para liberar os recursos associados ao contexto quando a execução terminar. O mesmo contexto é passado para LookupIPAddr, permitindo que a consulta reconheça quando o prazo estabelecido foi ultrapassado.


O método LookupIPAddr retorna um slice de estruturas net.IPAddr, cujo campo IP contém o endereço resolvido. Essa estrutura também possui o campo Zone, que pode representar informações de zona associadas a determinados endereços IPv6. Para consultas comuns de domínios públicos, normalmente trabalhamos apenas com o endereço presente em IP.


Quando o contexto é cancelado, a operação deve reconhecer essa condição e retornar conforme o mecanismo de resolução em uso permitir. Entretanto, o comportamento exato pode depender do resolver selecionado pelo Go e das interfaces disponibilizadas pelo sistema operacional. Por isso, não devemos assumir que todos os mecanismos de resolução oferecem exatamente o mesmo comportamento de cancelamento.



Consultando registros A, AAAA, MX, TXT e CNAME​


O DNS organiza suas informações em registros, também conhecidos como Resource Records. Cada registro possui um tipo que identifica qual informação está sendo representada, permitindo que diferentes consultas obtenham dados relacionados a determinado domínio. Isso significa que consultar um endereço IP e consultar os servidores de e-mail de um domínio são operações diferentes, mesmo quando trabalham com o mesmo nome.


Entre os registros mais comuns estão A, AAAA, MX, TXT e CNAME. Cada um possui uma responsabilidade específica, e o pacote net fornece métodos para obter essas informações sem precisar construir manualmente as mensagens do protocolo DNS. Entretanto, precisamos considerar que essas funções apresentam resultados já interpretados pelo resolver, não necessariamente todos os campos presentes na resposta DNS original.

RegistroResponsabilidade
AAssocia um nome a um endereço IPv4
AAAAAssocia um nome a um endereço IPv6
MXIdentifica servidores responsáveis pelo recebimento de e-mails
TXTArmazena informações textuais associadas a um nome
CNAMEAssocia um nome alternativo a outro nome canônico

Os registros A e AAAA são usados principalmente para localizar endereços de rede, enquanto MX é relacionado ao encaminhamento de mensagens de e-mail. Registros TXT podem armazenar informações usadas por diferentes serviços, incluindo políticas e verificações de domínio. Já CNAME permite criar um alias, fazendo com que determinado nome esteja associado a outro nome dentro do sistema DNS.



Consultando registros A​


O registro A associa um nome a um endereço IPv4. Esse tipo de endereço possui 32 bits e normalmente é representado por quatro números decimais separados por pontos, como 192.0.2.10. Quando uma aplicação precisa estabelecer uma conexão usando IPv4, pode consultar os endereços associados ao domínio antes de iniciar a comunicação.


O método Resolver.LookupIP permite informar qual família de endereços desejamos consultar. Quando passamos "ip4" como argumento, solicitamos endereços IPv4, evitando misturar os resultados com endereços IPv6. Esse método está disponível a partir do Go 1.15 e recebe um contexto, o tipo de rede e o nome que será resolvido.

package main

import (
"context"
"fmt"
"net"
"os"
"time"
)

func main() {
// Define um limite para a consulta.
ctx, cancel := context.WithTimeout(
context.Background(),
3*time.Second,
)

defer cancel()

// Consulta somente endereços IPv4.
ips, err := net.DefaultResolver.LookupIP(
ctx,
"ip4",
"example.com",
)

if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro na consulta A:",
err,
)

return
}

// Apresenta os endereços IPv4 encontrados.
for _, ip := range ips {
fmt.Println("A:", ip.String())
}
}

O método LookupIP recebe "ip4" para indicar que desejamos endereços IPv4. O retorno contém um slice de net.IP, permitindo percorrer os resultados e apresentar cada endereço encontrado. O contexto limita a duração da operação, enquanto o tratamento de erro permite identificar quando a resolução não foi concluída normalmente.


Precisamos considerar que LookupIP é uma função de resolução de endereços, não uma interface para examinar diretamente pacotes DNS. Dependendo do resolver utilizado, o resultado pode considerar informações locais, como arquivos de hosts, e mecanismos do sistema operacional. Quando precisamos verificar exatamente quais registros A foram devolvidos por um servidor DNS específico, incluindo informações como TTL, normalmente precisamos trabalhar com uma biblioteca que permita interpretar respostas DNS diretamente.



Consultando registros AAAA​


O registro AAAA associa um nome a um endereço IPv6, que possui 128 bits. Diferentemente do IPv4, endereços IPv6 são representados por grupos de valores hexadecimais separados por dois-pontos, permitindo representar um espaço muito maior de endereçamento. Um mesmo domínio pode possuir registros A e AAAA, possibilitando que clientes IPv4 e IPv6 localizem endereços compatíveis com suas redes.


Para consultar somente endereços IPv6, podemos usar novamente Resolver.LookupIP, mas informando "ip6" como tipo de rede. Essa configuração restringe o resultado aos endereços IPv6 disponibilizados pelo mecanismo de resolução.

package main

import (
"context"
"fmt"
"net"
"os"
"time"
)

func main() {
ctx, cancel := context.WithTimeout(
context.Background(),
3*time.Second,
)

defer cancel()

// Consulta somente endereços IPv6.
ips, err := net.DefaultResolver.LookupIP(
ctx,
"ip6",
"example.com",
)

if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro na consulta AAAA:",
err,
)

return
}

// Apresenta os endereços IPv6 encontrados.
for _, ip := range ips {
fmt.Println("AAAA:", ip.String())
}
}

A diferença principal em relação à consulta anterior está no argumento "ip6", responsável por restringir a família de endereços retornados. O método String apresenta os endereços IPv6 em sua representação textual, aplicando as regras de formatação usadas pelo tipo net.IP. Como IPv6 permite representações abreviadas, não devemos assumir que o resultado será apresentado sempre com todos os grupos completos.


A ausência de registros AAAA não significa que o domínio não exista ou que o serviço esteja indisponível. Um domínio pode possuir somente endereços IPv4, assim como pode disponibilizar endereços IPv6 sem possuir registros A. Quando uma aplicação precisa trabalhar com ambas as famílias, podemos consultar "ip" ou usar LookupIPAddr para obter os endereços disponíveis sem restringir a família.



Consultando registros MX​


O registro MX, que significa Mail Exchange, identifica servidores responsáveis pelo recebimento de e-mails destinados a determinado domínio. Um domínio pode possuir vários registros MX, permitindo definir diferentes servidores e prioridades para o recebimento das mensagens. Esses registros não representam diretamente endereços IP, mas nomes de servidores que normalmente precisam ser resolvidos posteriormente.


Cada registro MX possui um nome de servidor e um valor de preferência. Valores menores representam maior preferência durante a escolha do servidor de destino, embora o comportamento completo da entrega dependa das regras do protocolo de e-mail. Em Go, podemos consultar esses registros usando Resolver.LookupMX, que recebe um contexto e o domínio solicitado.

package main

import (
"context"
"fmt"
"net"
"os"
"time"
)

func main() {
ctx, cancel := context.WithTimeout(
context.Background(),
3*time.Second,
)

defer cancel()

// Consulta os registros MX do domínio.
registros, err := net.DefaultResolver.LookupMX(
ctx,
"example.com",
)

if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro na consulta MX:",
err,
)

return
}

// Apresenta os servidores e suas preferências.
for _, registro := range registros {
fmt.Printf(
"MX: %s | Preferência: %d\n",
registro.Host,
registro.Pref,
)
}
}

O método LookupMX retorna um slice de ponteiros para estruturas net.MX. Cada estrutura possui o campo Host, que representa o nome do servidor de e-mail, e o campo Pref, que contém o valor numérico de preferência. O pacote retorna os registros organizados por preferência, permitindo visualizar os servidores conforme os valores associados a cada um.


Precisamos considerar que consultar MX não verifica se os servidores encontrados estão acessíveis ou aceitando mensagens SMTP. O DNS apenas informa quais nomes estão associados ao recebimento de e-mails daquele domínio. Para verificar uma conexão SMTP, precisaríamos resolver o endereço do servidor e executar operações específicas do protocolo de e-mail.


Também não devemos assumir que todos os domínios possuem registros MX. Alguns domínios não recebem e-mails, enquanto outros podem publicar registros específicos para indicar que não aceitam mensagens. Um exemplo é o Null MX, definido pela RFC 7505, que usa preferência zero e o nome raiz . para indicar que o domínio não aceita e-mails.



Consultando registros TXT​


Registros TXT permitem associar informações textuais a um nome DNS. Originalmente, esse tipo de registro foi definido para transportar strings, mas atualmente aparece em diferentes mecanismos de verificação e configuração de serviços. É comum encontrar registros TXT relacionados a políticas de e-mail, comprovação de propriedade de domínio e configurações exigidas por aplicações externas.


Um domínio pode possuir vários registros TXT, e cada registro também pode conter diferentes partes de texto. Em Go, podemos consultar essas informações usando Resolver.LookupTXT, que recebe o contexto e o nome desejado. O resultado é um slice de strings, permitindo apresentar separadamente os valores encontrados.

package main

import (
"context"
"fmt"
"net"
"os"
"time"
)

func main() {
ctx, cancel := context.WithTimeout(
context.Background(),
3*time.Second,
)

defer cancel()

// Consulta os registros TXT do domínio.
registros, err := net.DefaultResolver.LookupTXT(
ctx,
"example.com",
)

if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro na consulta TXT:",
err,
)

return
}

// Apresenta cada registro encontrado.
for _, registro := range registros {
fmt.Println("TXT:", registro)
}
}

O método LookupTXT retorna uma string para cada registro TXT encontrado. Quando um único registro DNS contém várias partes de texto, o pacote concatena essas partes em uma única string antes de retornar o resultado. Isso significa que a representação fornecida pelo método não preserva necessariamente a divisão original das strings existentes dentro do registro DNS.


Registros TXT podem conter informações usadas por mecanismos como SPF e verificações de domínio, mas o pacote net não interpreta automaticamente o significado desses valores. Ele apenas retorna o conteúdo textual encontrado, deixando para a aplicação a responsabilidade de identificar quais registros são relevantes. Se precisarmos interpretar políticas SPF, por exemplo, teremos que seguir as regras específicas desse formato.


Também precisamos considerar que algumas informações TXT são publicadas em nomes específicos, e não diretamente no domínio principal. Registros relacionados a DKIM, por exemplo, normalmente aparecem em nomes que incluem um seletor e o segmento _domainkey. Por isso, consultar TXT apenas no domínio principal não garante que encontraremos todas as configurações textuais associadas aos serviços daquele domínio.



Consultando registros CNAME​


O registro CNAME, que significa Canonical Name, permite associar um nome alternativo a outro nome. Esse mecanismo é usado quando queremos que um nome funcione como alias de outro, evitando manter diretamente determinados endereços em todos os nomes envolvidos. Durante a resolução, o sistema pode seguir essa associação até encontrar o nome responsável pelas informações finais.


Em Go, o método Resolver.LookupCNAME retorna o nome canônico associado ao host consultado. Entretanto, precisamos entender que esse método não funciona exatamente como uma consulta que apresenta somente registros CNAME existentes. Ele pode seguir uma cadeia de aliases e retornar o nome canônico final, mesmo quando o nome informado não possui um registro CNAME diretamente associado.

package main

import (
"context"
"fmt"
"net"
"os"
"time"
)

func main() {
ctx, cancel := context.WithTimeout(
context.Background(),
3*time.Second,
)

defer cancel()

// Consulta o nome canônico associado ao domínio.
cname, err := net.DefaultResolver.LookupCNAME(
ctx,
"www.example.com",
)

if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro na consulta CNAME:",
err,
)

return
}

// Apresenta o nome canônico encontrado.
fmt.Println("Nome canônico:", cname)
}

O método LookupCNAME recebe um nome e retorna uma string que representa seu nome canônico final. Caso exista uma cadeia de registros CNAME, o resultado poderá representar o último nome encontrado depois de seguir essas associações. Se não existir um registro CNAME, o método ainda poderá retornar o próprio nome consultado, desde que ele seja resolvido adequadamente para registros de endereço.


Por isso, não devemos interpretar qualquer retorno bem-sucedido de LookupCNAME como prova de que existe um registro CNAME publicado no DNS. O método foi desenvolvido para obter o nome canônico, e não para apresentar necessariamente os registros intermediários de uma resposta DNS. Quando precisamos inspecionar os registros CNAME reais, incluindo seus nomes de origem e destino, ferramentas como dig ou bibliotecas que interpretam mensagens DNS diretamente são mais adequadas.


Também precisamos considerar que nomes DNS completos podem ser apresentados com um ponto final, representando a raiz da hierarquia DNS. Esse formato é conhecido como nome de domínio absoluto e pode aparecer nos resultados de algumas operações. Não devemos remover esse ponto indiscriminadamente quando precisamos preservar a representação completa do nome ou trabalhar com regras específicas de resolução.



Diferença entre usar o resolver do sistema e um resolver específico​


Quando uma aplicação precisa resolver um domínio, normalmente ela depende das configurações de resolução disponíveis no ambiente em que está sendo executada. Essas configurações podem incluir servidores DNS, arquivos locais de hosts, domínios de pesquisa e mecanismos próprios do sistema operacional. Isso significa que uma consulta de nome nem sempre corresponde diretamente ao envio de uma mensagem DNS para um servidor público.


Em Go, o pacote net pode trabalhar com mecanismos de resolução implementados pela própria linguagem ou disponibilizados pelo sistema operacional. A escolha depende da plataforma, das configurações disponíveis e de determinadas opções de execução. Para aplicações comuns, normalmente podemos usar o resolver padrão, mas ferramentas de diagnóstico podem precisar consultar um servidor DNS específico para comparar resultados.


Essa diferença é relevante quando estamos investigando problemas de resolução. Um domínio pode funcionar em determinada máquina porque existe uma entrada local de hosts, enquanto outro computador depende exclusivamente de servidores DNS. Também podemos encontrar resultados diferentes quando dois servidores DNS possuem caches, configurações ou políticas de resolução distintas.



Usando o resolver padrão do sistema​


Quando usamos funções como net.LookupHost ou net.DefaultResolver.LookupIPAddr, o programa trabalha com o mecanismo de resolução padrão configurado para aquela execução. No Linux, isso pode envolver informações presentes em arquivos como /etc/hosts, /etc/resolv.conf e outras configurações de resolução. Em outros sistemas operacionais, os mecanismos usados podem ser diferentes.


O arquivo /etc/hosts, por exemplo, permite associar nomes a endereços IP diretamente na máquina. Isso significa que um nome pode ser resolvido localmente sem necessariamente provocar uma consulta DNS pela rede. Já /etc/resolv.conf normalmente contém informações relacionadas aos servidores DNS e aos domínios de pesquisa, embora alguns ambientes usem serviços locais responsáveis por administrar essas configurações.


Podemos consultar um nome local usando o mesmo método empregado para resolver domínios públicos. Se o sistema estiver configurado para reconhecer esse nome, o programa poderá receber um endereço sem precisar conhecer a origem exata da informação.

package main

import (
"fmt"
"net"
"os"
)

func main() {
// Consulta o nome usando a resolução padrão.
enderecos, err := net.LookupHost("localhost")

if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}

// Apresenta os endereços encontrados.
for _, endereco := range enderecos {
fmt.Println(endereco)
}
}

O nome localhost normalmente é associado à interface de loopback por mecanismos locais de resolução. Dependendo do sistema, o resultado pode incluir 127.0.0.1, ::1 ou ambos. Isso demonstra que a resolução de nomes não depende exclusivamente de registros DNS publicados na internet, porque o sistema operacional também pode fornecer informações usadas pelas aplicações.


O comportamento exato depende da configuração da máquina. No Linux, a resolução pode ser influenciada pelo arquivo /etc/nsswitch.conf, enquanto Windows e macOS possuem mecanismos próprios para administrar a resolução de nomes. Por isso, uma aplicação que depende do resolver padrão deve considerar que resultados e regras de pesquisa podem variar entre sistemas operacionais.



Resolver implementado em Go e resolver nativo​


O pacote net possui dois caminhos principais para realizar determinadas operações de resolução. Um deles é o resolver implementado em Go, que pode ler configurações do sistema e enviar consultas DNS diretamente. O outro utiliza interfaces nativas do sistema operacional, tradicionalmente identificadas na documentação do Go como o caminho cgo, mesmo quando a implementação não depende diretamente da ferramenta cgo.


A escolha entre esses mecanismos pode depender do sistema operacional, das opções usadas durante a compilação e das configurações disponíveis durante a execução. Em determinadas situações, o resolver implementado em Go não reproduz todos os recursos oferecidos pelo mecanismo nativo, por isso o pacote pode escolher outro caminho para manter compatibilidade com as regras de resolução do ambiente.


Podemos influenciar essa escolha usando a variável de ambiente GODEBUG, que possui a opção netdns. Em sistemas Linux e macOS, usando um shell compatível com a sintaxe apresentada, podemos solicitar preferência pelo resolver implementado em Go.

GODEBUG=netdns=go go run main.go

Também podemos solicitar preferência pelo mecanismo nativo:

GODEBUG=netdns=cgo go run main.go

Essas opções não garantem que todos os mecanismos estarão disponíveis em qualquer sistema ou configuração de compilação. Também não alteram automaticamente os servidores DNS configurados no sistema, porque sua responsabilidade é influenciar o caminho de resolução usado pelo pacote net. Quando precisamos identificar qual mecanismo está sendo selecionado, podemos habilitar informações de diagnóstico.

GODEBUG=netdns=go+1 go run main.go

Nesse exemplo, solicitamos preferência pelo resolver implementado em Go e habilitamos mensagens relacionadas às decisões de resolução. O conteúdo dessas mensagens depende da versão do Go e do ambiente em que o programa está sendo executado. Elas são úteis para diagnóstico, mas não devem ser tratadas como uma interface estável para processamento automático.



Criando um resolver específico​


Em ferramentas de diagnóstico, podemos precisar consultar um servidor DNS diferente daquele configurado no sistema. Isso permite comparar respostas, investigar problemas de cache ou verificar se determinada informação já está disponível em um servidor específico. Para esse comportamento, podemos criar um net.Resolver personalizado e definir como ele estabelece conexões com o servidor DNS.


O tipo net.Resolver possui o campo Dial, que permite informar uma função responsável por criar conexões usadas pelo resolver implementado em Go. Essa função recebe um contexto, o tipo de rede e o endereço que seria consultado normalmente, mas podemos direcionar a conexão para um servidor escolhido pela aplicação. O campo PreferGo permite solicitar preferência pelo resolver implementado em Go.


Vamos criar um resolver que encaminha suas consultas DNS para o endereço 1.1.1.1, usando a porta padrão 53. Esse endereço representa um servidor DNS público, mas o exemplo também poderia trabalhar com o IP de um servidor interno, desde que ele esteja acessível e aceite consultas DNS.

package main

import (
"context"
"fmt"
"net"
"os"
"time"
)

func main() {
// Define o servidor DNS que será consultado.
servidorDNS := "1.1.1.1:53"

// Cria um dialer para estabelecer as conexões.
dialer := &net.Dialer{
Timeout: 3 * time.Second,
}

// Configura um resolver com servidor DNS específico.
resolver := &net.Resolver{
PreferGo: true,

Dial: func(
ctx context.Context,
network string,
address string,
) (net.Conn, error) {
// Direciona a conexão ao servidor escolhido.
return dialer.DialContext(
ctx,
network,
servidorDNS,
)
},
}

// Define um limite para a resolução.
ctx, cancel := context.WithTimeout(
context.Background(),
5*time.Second,
)

defer cancel()

// Resolve o domínio usando o resolver configurado.
ips, err := resolver.LookupIP(
ctx,
"ip4",
"example.com",
)

if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro na consulta DNS:",
err,
)

return
}

// Apresenta os endereços encontrados.
for _, ip := range ips {
fmt.Println(ip)
}
}

A estrutura net.Resolver é configurada com PreferGo: true, indicando preferência pelo mecanismo implementado em Go. O campo Dial recebe uma função que será usada para estabelecer as conexões DNS, enquanto net.Dialer realiza a conexão propriamente dita. Nesse exemplo, ignoramos o endereço recebido pelo callback porque queremos direcionar as consultas de rede para o servidor definido em servidorDNS.


O argumento network precisa ser preservado porque o resolver pode solicitar conexões UDP ou TCP, dependendo da operação e das condições da comunicação. O DNS tradicional normalmente usa UDP para muitas consultas, mas TCP também faz parte do protocolo e pode ser necessário quando uma resposta é grande ou quando determinadas condições exigem outro transporte. Forçar todas as consultas para UDP poderia impedir que o resolver trabalhasse corretamente em algumas situações.


O contexto com limite de cinco segundos controla a duração geral da resolução, enquanto o dialer possui um limite de três segundos para estabelecer uma conexão individual. Esses valores possuem responsabilidades diferentes, porque uma consulta pode envolver mais de uma tentativa de comunicação. O pacote net continua sendo responsável por construir, enviar e interpretar as mensagens DNS utilizadas durante a resolução.


O campo Dial altera o destino das conexões DNS feitas pelo resolver implementado em Go, mas não transforma todos os mecanismos de resolução de nomes em consultas DNS puras. Informações locais e algumas regras do ambiente ainda podem influenciar determinados resultados, dependendo do nome consultado e do comportamento do resolver. Para inspecionar exclusivamente as respostas recebidas de um servidor DNS, precisamos trabalhar diretamente com mensagens do protocolo ou usar ferramentas próprias para esse diagnóstico.


Também precisamos considerar que o exemplo usa DNS tradicional na porta 53, sem criptografia própria para o transporte das consultas. Ele não implementa DNS over HTTPS ou DNS over TLS, que possuem regras de comunicação diferentes. Se precisarmos de consultas DNS protegidas, teremos que trabalhar com mecanismos específicos para esses protocolos.



Usando servidores DNS IPv4 e IPv6​


O servidor DNS escolhido não precisa obrigatoriamente possuir um endereço IPv4. Também podemos direcionar consultas para servidores acessíveis por IPv6, desde que o ambiente tenha conectividade adequada. Nesse caso, precisamos montar corretamente o endereço e a porta, porque endereços IPv6 contêm dois-pontos e não podem ser concatenados manualmente da mesma maneira usada em um endereço IPv4.


O pacote net possui a função net.JoinHostPort, que combina um host e uma porta respeitando as regras de representação dos endereços. Quando o host é um endereço IPv6, a função acrescenta os colchetes necessários para diferenciar o endereço da porta.

package main

import (
"fmt"
"net"
)

func main() {
// Monta um endereço DNS IPv4.
ipv4 := net.JoinHostPort(
"1.1.1.1",
"53",
)

// Monta um endereço DNS IPv6.
ipv6 := net.JoinHostPort(
"2606:4700:4700::1111",
"53",
)

fmt.Println(ipv4)
fmt.Println(ipv6)
}

A saída será:

1.1.1.1:53
[2606:4700:4700::1111]:53

A função net.JoinHostPort permite montar endereços de maneira independente da família IP usada pelo servidor. Isso é conveniente quando uma ferramenta recebe o endereço DNS por flag ou arquivo de configuração, porque não precisamos criar regras diferentes para concatenar IPv4 e IPv6. Entretanto, a montagem correta do endereço não garante que exista conectividade com o servidor informado.


Quando permitimos que o usuário escolha um servidor DNS, também precisamos validar sua entrada. Podemos usar net.ParseIP para verificar se o valor representa um endereço IP válido ou net.SplitHostPort quando o parâmetro inclui uma porta. Essa validação evita ambiguidades e permite apresentar mensagens de erro antes de tentar estabelecer a conexão.



Diferenças práticas entre os resolvers​


Usar o resolver padrão é conveniente quando queremos que a aplicação respeite o ambiente em que está sendo executada. Ele pode considerar configurações locais, regras de pesquisa e mecanismos fornecidos pelo sistema operacional. Isso normalmente é adequado para clientes HTTP, servidores e ferramentas que precisam resolver nomes da mesma maneira que outras aplicações da máquina.


Já um resolver personalizado é útil quando precisamos direcionar consultas DNS para determinado servidor, especialmente em ferramentas de diagnóstico. Entretanto, personalizar o destino das conexões não significa ignorar automaticamente todas as outras regras de resolução. Também precisamos considerar que diferentes servidores podem retornar respostas distintas por causa de caches, políticas de resolução ou configurações específicas.

CaracterísticaResolver padrãoResolver personalizado
Configuração DNSSegue a resolução configurada para o ambientePode direcionar conexões para um servidor escolhido
Arquivos locaisPode considerar informações locaisAlgumas regras locais ainda podem influenciar resultados
Controle de timeoutDisponível por métodos com contextoDisponível por contexto e configuração do dialer
Servidor DNS específicoNão é escolhido diretamente pelas funções globaisPode ser definido pelo campo Dial
Uso comumResolução normal de aplicaçõesDiagnóstico e consultas direcionadas

Também precisamos considerar que um nome com ou sem ponto final pode ser tratado de maneira diferente pelo mecanismo de resolução. Nomes relativos podem passar por regras de pesquisa de domínios configuradas no sistema, enquanto nomes absolutos terminados em ponto indicam que a consulta deve partir daquele nome completo. Esse comportamento é relevante quando queremos comparar respostas DNS sem incluir automaticamente sufixos configurados no ambiente.



Criando consultas DNS simples​


Depois de conhecer os métodos de resolução, podemos organizar essas operações em funções reutilizáveis. Isso permite criar ferramentas capazes de consultar diferentes tipos de registros sem repetir todo o código responsável por configurar contextos, selecionar resolvers e tratar os resultados. Uma ferramenta DNS simples pode receber o nome desejado e o tipo de registro, executando a operação correspondente.


Como os métodos do pacote net possuem diferentes tipos de retorno, precisamos definir como cada resultado será apresentado. Consultas A e AAAA retornam endereços IP, MX retorna estruturas com nomes e preferências, TXT retorna strings e CNAME retorna um nome canônico. Podemos tratar essas diferenças em uma função responsável por selecionar a consulta e formatar os valores encontrados.


Também podemos centralizar o tratamento de timeout usando um contexto compartilhado pela operação. Dessa maneira, cada consulta possui um limite de duração conhecido e pode ser interrompida quando esse prazo terminar. Para começar, vamos criar uma função simples que consulta registros A e retorna os endereços encontrados.



Criando uma função de consulta​


Uma função de consulta pode receber um contexto, um resolver e o nome que será consultado. Essa organização permite usar o mesmo código com o resolver padrão ou com um resolver personalizado, sem alterar sua implementação. Também facilita a reutilização da função em ferramentas CLI ou em outras partes de uma aplicação.


Vamos criar uma função chamada consultarA, responsável por obter endereços IPv4 e retorná-los como strings. Como a operação pode falhar, a função também retornará um valor do tipo error, permitindo que o código responsável pela chamada decida como apresentar ou tratar o problema.

package main

import (
"context"
"fmt"
"net"
"os"
"time"
)

func consultarA(
ctx context.Context,
resolver *net.Resolver,
dominio string,
) ([]string, error) {
// Consulta os endereços IPv4 do domínio.
ips, err := resolver.LookupIP(
ctx,
"ip4",
dominio,
)

if err != nil {
return nil, fmt.Errorf(
"consultar A de %s: %w",
dominio,
err,
)
}

// Converte os endereços para strings.
resultados := make([]string, 0, len(ips))

for _, ip := range ips {
resultados = append(
resultados,
ip.String(),
)
}

return resultados, nil
}

func main() {
// Define um contexto com timeout.
ctx, cancel := context.WithTimeout(
context.Background(),
3*time.Second,
)

defer cancel()

// Executa a consulta usando o resolver padrão.
resultados, err := consultarA(
ctx,
net.DefaultResolver,
"example.com",
)

if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}

// Apresenta os resultados da consulta.
for _, resultado := range resultados {
fmt.Println(resultado)
}
}

A função consultarA recebe três argumentos que representam o contexto da operação, o mecanismo de resolução e o domínio consultado. Ela chama LookupIP com "ip4" e converte os endereços encontrados para strings antes de retorná-los. Isso permite que outras partes do programa trabalhem com os resultados sem precisar conhecer diretamente o tipo net.IP.


Quando a consulta falha, usamos fmt.Errorf com %w para acrescentar informações sobre a operação e preservar o erro original. Esse comportamento facilita o diagnóstico porque o erro passa a indicar qual tipo de consulta foi executado e qual domínio estava sendo resolvido. Também permite que o código responsável pela chamada continue usando errors.Is e errors.As para identificar determinadas condições.


A função main apenas cria o contexto, executa a consulta e apresenta os resultados. Como o resolver é recebido como argumento, podemos substituir net.DefaultResolver por uma instância personalizada sem modificar a função consultarA. Essa separação ajuda a manter a ferramenta organizada conforme adicionamos novos tipos de registros ou diferentes mecanismos de configuração.



Consultando diferentes registros em uma única função​


Quando precisamos consultar vários tipos de registros, podemos criar uma função que recebe o tipo desejado e seleciona a operação correspondente. Uma estrutura switch permite manter as consultas organizadas, enquanto cada alternativa trabalha com o método adequado do pacote net. Isso é conveniente para ferramentas CLI que recebem uma flag responsável por indicar o tipo de registro solicitado.


O exemplo seguinte cria uma função chamada consultarDNS, que recebe um contexto, um resolver, um domínio e o tipo de registro. Para simplificar o retorno, ela apresenta diretamente os resultados e retorna somente um erro quando a consulta não pode ser concluída. Essa abordagem é adequada para uma ferramenta de diagnóstico simples, embora aplicações maiores possam preferir retornar estruturas de dados em vez de escrever diretamente no terminal.

package main

import (
"context"
"errors"
"fmt"
"net"
"strings"
)

func consultarDNS(
ctx context.Context,
resolver *net.Resolver,
dominio string,
tipo string,
) error {
// Normaliza o nome do tipo de registro.
tipo = strings.ToUpper(tipo)

switch tipo {
case "A", "AAAA":
rede := "ip4"

if tipo == "AAAA" {
rede = "ip6"
}

// Consulta os endereços da família solicitada.
ips, err := resolver.LookupIP(ctx, rede, dominio)

if err != nil {
return err
}

for _, ip := range ips {
fmt.Printf("%s: %s\n", tipo, ip)
}

case "MX":
// Consulta os servidores de e-mail.
registros, err := resolver.LookupMX(ctx, dominio)

if err != nil {
return err
}

for _, registro := range registros {
fmt.Printf(
"MX: %s | Preferência: %d\n",
registro.Host,
registro.Pref,
)
}

case "TXT":
// Consulta os registros de texto.
registros, err := resolver.LookupTXT(ctx, dominio)

if err != nil {
return err
}

for _, registro := range registros {
fmt.Println("TXT:", registro)
}

case "CNAME":
// Obtém o nome canônico final.
nome, err := resolver.LookupCNAME(ctx, dominio)

if err != nil {
return err
}

fmt.Println("Nome canônico:", nome)

default:
return errors.New("tipo de registro não suportado")
}

return nil
}

A função usa strings.ToUpper para converter o tipo recebido para letras maiúsculas, permitindo aceitar valores como a, A, mx e MX. Depois, o switch seleciona o método correspondente do resolver. As consultas A e AAAA compartilham o mesmo código, alterando apenas o argumento que representa a família de endereços.


Os registros MX são apresentados com seus nomes e preferências, enquanto TXT é apresentado como texto. Para CNAME, exibimos o nome canônico retornado por LookupCNAME, lembrando que isso não comprova a existência de um registro CNAME direto. Quando o tipo informado não pertence ao conjunto aceito, a função retorna um erro sem realizar uma consulta.


Esse exemplo representa uma função reutilizável, não um programa completo, porque ainda precisamos criar o contexto, selecionar o resolver e chamar consultarDNS. Essas responsabilidades serão reunidas posteriormente na ferramenta CLI. Também precisamos considerar que o pacote net não fornece automaticamente informações como TTL ou todos os campos da resposta DNS original, por isso a função apresenta apenas os dados disponibilizados pelos métodos usados.



Entendendo os limites das consultas de alto nível​


As funções do pacote net foram desenvolvidas principalmente para resolver nomes e obter informações comuns do DNS. Elas facilitam operações como encontrar endereços IP e consultar MX ou TXT, mas não oferecem uma interface genérica para construir qualquer consulta DNS e examinar todos os campos da resposta. Isso significa que uma ferramenta baseada somente nesses métodos terá limitações em relação a aplicações como dig.


Uma resposta DNS pode conter informações como identificador da mensagem, código de resposta, flags, registros de resposta, registros de autoridade, registros adicionais e valores de TTL. Esses dados fazem parte da estrutura do protocolo, mas não são apresentados diretamente pelos métodos como LookupIP ou LookupMX. O pacote também pode seguir aliases e aplicar regras de resolução antes de devolver os resultados para a aplicação.


Quando precisamos construir mensagens DNS manualmente ou interpretar respostas completas, podemos trabalhar com bibliotecas específicas. O módulo oficial golang.org/x/net possui o pacote dns/dnsmessage, que fornece recursos para empacotar e interpretar mensagens do protocolo. Entretanto, ele não faz parte da biblioteca padrão instalada diretamente com Go e precisa ser adicionado como dependência quando usado em um projeto.


Para ferramentas simples de resolução e diagnóstico, os métodos de net.Resolver normalmente são suficientes. Quando a finalidade é inspecionar registros DNS exatamente como foram recebidos de determinado servidor, incluindo informações de autoridade, TTL e códigos de resposta, precisamos considerar uma implementação mais próxima do protocolo. Essa diferença evita apresentar funções de resolução de nomes como se fossem equivalentes a uma ferramenta completa de inspeção DNS.



Tratando erros de resolução​


Consultas DNS podem falhar por diferentes motivos, incluindo nomes inexistentes, ausência do tipo de registro solicitado, servidores que não respondem ou problemas de comunicação pela rede. Também podemos encontrar erros relacionados ao mecanismo de resolução configurado no sistema ou ao cancelamento de uma operação. Para criar ferramentas úteis, precisamos identificar essas situações e apresentar mensagens que ajudem a entender o problema.


Em Go, as funções do pacote net retornam valores do tipo error, permitindo examinar o motivo de uma falha antes de continuar. Um erro de resolução pode ser representado pelo tipo net.DNSError, que possui campos relacionados ao nome consultado, ao servidor e a determinadas condições encontradas durante a operação. Podemos usar errors.As para identificar esse tipo mesmo quando ele está encapsulado por outros erros.


Também precisamos diferenciar erros DNS de problemas encontrados depois da resolução. Um domínio pode ser resolvido corretamente, mas o endereço retornado pode não aceitar conexões TCP ou não possuir um serviço disponível na porta desejada. Nesse caso, a resolução foi concluída, e a falha pertence à operação de conexão, não necessariamente ao DNS.



Identificando erros com net.DNSError​


O tipo net.DNSError representa erros associados à resolução de nomes. Ele possui campos como Name, que identifica o nome consultado, Server, que pode identificar o servidor usado, e indicadores booleanos relacionados a determinadas condições. Entre esses indicadores estão IsTimeout, IsTemporary e IsNotFound.


Podemos criar uma função que recebe um erro e verifica se ele corresponde a net.DNSError. Quando essa condição é atendida, podemos examinar seus campos e apresentar uma mensagem mais específica. Isso é conveniente para ferramentas CLI, porque permite distinguir erros de resolução conhecidos de outros problemas retornados pelas operações.

package main

import (
"errors"
"fmt"
"net"
)

func tratarErroDNS(err error) {
if err == nil {
return
}

var dnsErr *net.DNSError

// Verifica se o erro representa uma falha DNS.
if errors.As(err, &dnsErr) {
fmt.Println("Nome consultado:", dnsErr.Name)

switch {
case dnsErr.IsNotFound:
fmt.Println("Nome ou registro não encontrado.")

case dnsErr.IsTimeout:
fmt.Println("Tempo limite da consulta excedido.")

case dnsErr.IsTemporary:
fmt.Println("Erro temporário na resolução.")

default:
fmt.Println("Erro DNS:", dnsErr.Err)
}

return
}

// Apresenta erros que não são DNSError.
fmt.Println("Erro:", err)
}

A função errors.As permite verificar se o erro recebido contém um valor do tipo *net.DNSError, preenchendo a variável dnsErr quando encontra uma correspondência. Depois, usamos os campos booleanos para identificar condições conhecidas e apresentar uma mensagem adequada. Quando nenhum desses indicadores está definido, podemos consultar o campo Err, que contém uma descrição textual do problema.


O campo IsNotFound exige cuidado porque pode indicar tanto que o nome consultado não existe quanto que o nome existe, mas não possui registros do tipo solicitado. Isso significa que não podemos interpretar automaticamente esse indicador como prova de uma resposta DNS NXDOMAIN. Para diferenciar essas situações com precisão, pode ser necessário examinar diretamente a resposta DNS ou executar consultas adicionais.


Os campos IsTimeout e IsTemporary também não identificam obrigatoriamente todas as situações possíveis. A própria documentação do pacote informa que determinadas falhas temporárias ou de timeout podem ocorrer sem que os indicadores correspondentes estejam definidos. Por isso, aplicações que precisam classificar erros com maior precisão também devem considerar o contexto da operação e outros tipos de erro retornados.



Tratando timeouts de resolução​


Quando realizamos consultas para servidores DNS externos, precisamos considerar que o servidor pode não responder dentro do intervalo esperado. Isso pode acontecer por problemas de conectividade, filtros de rede, indisponibilidade do serviço ou outras condições relacionadas ao ambiente. Para impedir que a aplicação permaneça aguardando por tempo excessivo, podemos usar um contexto com timeout.


A função context.WithTimeout cria um contexto que será cancelado automaticamente depois de determinada duração. Podemos passar esse contexto para métodos de net.Resolver, permitindo que a consulta reconheça o prazo estabelecido. Quando o contexto é cancelado, também podemos consultar ctx.Err() para identificar se o encerramento aconteceu por cancelamento explícito ou por prazo excedido.

package main

import (
"context"
"errors"
"fmt"
"net"
"time"
)

func main() {
// Define um limite de dois segundos.
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)

defer cancel()

// Executa a consulta DNS.
_, err := net.DefaultResolver.LookupIP(
ctx,
"ip4",
"example.com",
)

if err != nil {
// Verifica se o contexto excedeu seu prazo.
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
fmt.Println("Consulta excedeu o tempo limite.")
return
}

fmt.Println("Erro na consulta:", err)
return
}

fmt.Println("Consulta concluída.")
}

O programa cria um contexto com limite de dois segundos e executa uma consulta IPv4. Se a operação retornar um erro, verificamos ctx.Err() para identificar se o contexto ultrapassou seu prazo. Isso permite distinguir o encerramento provocado pelo timeout de outras falhas, como um nome não encontrado ou um problema retornado pelo mecanismo de resolução.


Entretanto, o timeout do contexto não é necessariamente o único limite aplicado durante uma consulta DNS. O resolver pode possuir regras próprias de tentativas e prazos, dependendo da plataforma e das configurações disponíveis. Por isso, a consulta pode falhar antes do prazo definido pelo contexto, e nem todo erro de timeout significa que o contexto já foi cancelado.


Quando precisamos identificar qualquer erro relacionado a timeout, também podemos examinar a interface net.Error, que possui o método Timeout. Isso permite reconhecer algumas condições relacionadas à rede que não aparecem diretamente como context.DeadlineExceeded. A classificação final deve considerar tanto o erro retornado quanto o estado do contexto.



Diferenciando domínio inexistente e registro ausente​


Uma consulta DNS pode não encontrar resultados por motivos diferentes. O domínio consultado pode realmente não existir, ou o nome pode existir sem possuir o tipo de registro solicitado. Essas situações são diferentes no protocolo DNS, mas os métodos de alto nível do pacote net podem apresentar ambas por meio de um erro cujo campo IsNotFound está definido.


Considere um domínio que possui registros A, mas não possui registros AAAA. Nesse caso, uma consulta IPv4 pode ser concluída normalmente, enquanto uma consulta IPv6 pode retornar um erro indicando que não existem dados correspondentes. Isso não significa que o domínio desapareceu do DNS ou que seus registros IPv4 deixaram de existir.


Para uma ferramenta que precisa distinguir esses comportamentos, podemos executar consultas de diferentes tipos e comparar seus resultados. Entretanto, isso continua sendo uma interpretação baseada nas consultas disponíveis, não uma inspeção direta do código de resposta do protocolo. Para identificar com precisão respostas como NXDOMAIN ou ausência de dados do tipo solicitado, normalmente precisamos trabalhar com mensagens DNS completas.



Cuidados com tentativas repetidas​


Quando uma consulta falha temporariamente, podemos considerar executar outra tentativa depois de determinado intervalo. Entretanto, não devemos repetir todas as falhas indiscriminadamente, porque alguns erros não serão resolvidos apenas com novas consultas. Um nome inexistente, por exemplo, normalmente não passa a existir imediatamente porque executamos a mesma operação várias vezes.


Também precisamos limitar a quantidade de tentativas e respeitar o tempo máximo permitido para a operação. Se criarmos um novo contexto com timeout a cada tentativa, o tempo total poderá ultrapassar bastante o limite inicialmente esperado. Uma abordagem mais controlada é usar um contexto geral para a operação e permitir que as tentativas terminem quando esse prazo for alcançado.


Em ferramentas de diagnóstico, normalmente é conveniente apresentar o número de tentativas, o servidor consultado e o erro encontrado. Isso ajuda a diferenciar falhas intermitentes de problemas persistentes, principalmente quando estamos comparando servidores DNS. Entretanto, algumas informações internas do resolver podem não estar disponíveis pelos métodos de alto nível, exigindo mecanismos adicionais quando precisamos registrar todos os detalhes da comunicação.



Testando DNS em ferramentas CLI​


Ferramentas de linha de comando permitem consultar informações DNS diretamente pelo terminal, sem precisar desenvolver uma aplicação específica para cada operação. Elas são úteis para verificar registros, comparar servidores e investigar problemas de resolução. Podemos usar ferramentas existentes, como dig e nslookup, ou criar uma ferramenta própria em Go usando os recursos apresentados anteriormente.


Uma ferramenta DNS normalmente recebe o domínio que será consultado, o tipo de registro e, opcionalmente, o servidor responsável pela consulta. Também podemos permitir configurar o tempo máximo de espera, evitando que o programa permaneça aguardando indefinidamente. Esses parâmetros podem ser recebidos por flags, usando o pacote flag da biblioteca padrão.


Antes de implementar nossa ferramenta, vamos observar como consultas semelhantes são realizadas com programas já existentes. Isso permite comparar o comportamento da aplicação Go com ferramentas desenvolvidas especificamente para diagnóstico DNS e entender quais informações cada uma apresenta.



Consultando DNS com dig​


A ferramenta dig, que significa Domain Information Groper, permite executar consultas DNS e apresentar diferentes informações presentes nas respostas. Ela é bastante usada em diagnósticos porque permite escolher o tipo de registro e o servidor consultado, além de apresentar detalhes como códigos de resposta, flags e registros retornados. Sua disponibilidade depende do sistema e dos pacotes instalados.


Para consultar registros A de um domínio, podemos executar:

dig example.com A

Para consultar registros AAAA, basta modificar o tipo solicitado:

dig example.com AAAA

Também podemos consultar registros MX, TXT e CNAME usando a mesma estrutura de comando. O tipo informado determina quais registros serão solicitados ao servidor DNS, permitindo verificar diferentes informações associadas ao domínio.

dig example.com MX
dig example.com TXT
dig www.example.com CNAME

Por padrão, dig consulta os servidores DNS configurados para seu ambiente de execução. Entretanto, podemos escolher um servidor específico usando o prefixo @ seguido pelo endereço desejado. Isso é conveniente quando precisamos comparar respostas entre diferentes servidores.

dig @1.1.1.1 example.com A

Também podemos solicitar uma saída mais curta usando a opção +short, que apresenta principalmente os dados encontrados sem incluir todas as informações de diagnóstico da resposta. Essa opção é conveniente para scripts, mas também reduz a quantidade de informações disponíveis para investigar problemas.

dig +short example.com A

Quando precisamos identificar por que determinada consulta falhou, normalmente é melhor examinar a saída completa. Informações como o código de resposta e o servidor consultado ajudam a diferenciar problemas de resolução, ausência de registros e situações relacionadas ao servidor DNS.



Consultando DNS com nslookup​


A ferramenta nslookup também permite consultar informações DNS e está disponível em diferentes sistemas operacionais, incluindo instalações comuns do Windows. Ela pode trabalhar com consultas simples ou receber parâmetros que definem o tipo de registro e o servidor consultado. Sua saída normalmente é menos detalhada do que a de dig, mas é suficiente para várias verificações básicas.


Para consultar os endereços associados a um domínio, podemos executar:

nslookup example.com

Para consultar um tipo específico, podemos informar a opção -type seguida pelo registro desejado. Por exemplo, uma consulta MX pode ser executada da seguinte maneira:

nslookup -type=MX example.com

Também podemos informar um servidor DNS específico como argumento adicional, permitindo comparar a resposta com aquela obtida pelo mecanismo configurado por padrão.

nslookup -type=TXT example.com 1.1.1.1

A sintaxe apresentada é aceita por implementações comuns do nslookup, mas algumas opções e detalhes de saída podem variar entre sistemas. Como a ferramenta consulta DNS diretamente, seus resultados também podem ser diferentes daqueles obtidos por aplicações que consultam arquivos locais de hosts ou usam mecanismos adicionais do sistema operacional.



Criando uma ferramenta DNS CLI em Go​


Podemos criar uma ferramenta chamada dnscli, responsável por consultar registros DNS diretamente pelo terminal. O programa receberá o domínio, o tipo de registro, um servidor DNS opcional e um timeout. Quando nenhum servidor for informado, a ferramenta usará o resolver padrão; quando receber um endereço IP válido, criará um resolver personalizado para direcionar as consultas DNS a esse servidor.


Vamos usar o pacote flag para interpretar as opções e net.Resolver para executar as consultas. A ferramenta aceitará os tipos A, AAAA, MX, TXT e CNAME, apresentando os resultados encontrados e retornando códigos de saída diferentes quando ocorrerem erros de parâmetros ou falhas de resolução. Também vamos manter o encerramento do processo concentrado na função main, permitindo que as demais funções retornem normalmente.


A estrutura do projeto pode permanecer simples, porque usaremos apenas recursos da biblioteca padrão do Go.

dnscli/
├── go.mod
└── main.go

Podemos iniciar o módulo Go dentro do diretório do projeto usando o comando:

go mod init exemplo/dnscli

O comando cria o arquivo go.mod, responsável por identificar o módulo e registrar informações relacionadas ao projeto. O nome exemplo/dnscli é apenas um identificador local para este material e pode ser substituído pelo caminho adequado ao projeto. Como o programa usa somente pacotes da biblioteca padrão, não precisamos adicionar dependências externas.


No arquivo main.go, podemos implementar a ferramenta completa.

package main

import (
"context"
"errors"
"flag"
"fmt"
"net"
"os"
"strings"
"time"
)

func main() {
// Executa a ferramenta e retorna seu código de saída.
os.Exit(executar(os.Args[1:]))
}

func criarResolver(servidor string) (*net.Resolver, error) {
// Usa o resolver padrão quando nenhum servidor é informado.
if servidor == "" {
return net.DefaultResolver, nil
}

// Exige um endereço IP para evitar resolução recursiva.
if net.ParseIP(servidor) == nil {
return nil, errors.New(
"o servidor DNS deve ser um endereço IP válido",
)
}

// Monta o endereço com a porta DNS padrão.
endereco := net.JoinHostPort(servidor, "53")

dialer := &net.Dialer{
Timeout: 3 * time.Second,
}

// Cria um resolver direcionado ao servidor informado.
resolver := &net.Resolver{
PreferGo: true,

Dial: func(
ctx context.Context,
network string,
address string,
) (net.Conn, error) {
return dialer.DialContext(
ctx,
network,
endereco,
)
},
}

return resolver, nil
}

func consultar(
ctx context.Context,
resolver *net.Resolver,
dominio string,
tipo string,
) error {
switch tipo {
case "A", "AAAA":
rede := "ip4"

if tipo == "AAAA" {
rede = "ip6"
}

// Consulta os endereços da família solicitada.
ips, err := resolver.LookupIP(
ctx,
rede,
dominio,
)

if err != nil {
return err
}

for _, ip := range ips {
fmt.Printf(
"%s: %s\n",
tipo,
ip.String(),
)
}

case "MX":
// Consulta os servidores de e-mail.
registros, err := resolver.LookupMX(
ctx,
dominio,
)

if err != nil {
return err
}

for _, registro := range registros {
fmt.Printf(
"MX: %s | Preferência: %d\n",
registro.Host,
registro.Pref,
)
}

case "TXT":
// Consulta os registros de texto.
registros, err := resolver.LookupTXT(
ctx,
dominio,
)

if err != nil {
return err
}

for _, registro := range registros {
fmt.Println("TXT:", registro)
}

case "CNAME":
// Obtém o nome canônico final.
nome, err := resolver.LookupCNAME(
ctx,
dominio,
)

if err != nil {
return err
}

fmt.Println("Nome canônico:", nome)

default:
return errors.New("tipo de registro não suportado")
}

return nil
}

func executar(args []string) int {
// Cria um conjunto de flags para a ferramenta.
flags := flag.NewFlagSet(
"dnscli",
flag.ContinueOnError,
)

dominio := flags.String(
"nome",
"",
"Domínio que será consultado",
)

tipo := flags.String(
"tipo",
"A",
"Tipo de registro: A, AAAA, MX, TXT ou CNAME",
)

servidor := flags.String(
"servidor",
"",
"IP do servidor DNS (vazio usa o padrão)",
)

timeout := flags.Duration(
"timeout",
5*time.Second,
"Tempo máximo da consulta",
)

// Personaliza a mensagem de ajuda.
flags.Usage = func() {
fmt.Fprintln(
flags.Output(),
"Uso: dnscli -nome <domínio> [opções]",
)

fmt.Fprintln(flags.Output())

flags.PrintDefaults()
}

// Interpreta as opções recebidas.
if err := flags.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return 0
}

return 2
}

// Rejeita argumentos posicionais inesperados.
if flags.NArg() != 0 {
fmt.Fprintln(
os.Stderr,
"Erro: argumentos inesperados.",
)

return 2
}

// Verifica se o domínio foi informado.
if strings.TrimSpace(*dominio) == "" {
fmt.Fprintln(
os.Stderr,
"Erro: a flag -nome é obrigatória.",
)

return 2
}

// Normaliza o tipo de registro.
registro := strings.ToUpper(*tipo)

switch registro {
case "A", "AAAA", "MX", "TXT", "CNAME":
// Tipo de registro aceito.

default:
fmt.Fprintln(
os.Stderr,
"Erro: tipo de registro inválido.",
)

return 2
}

// Verifica se o timeout é positivo.
if *timeout <= 0 {
fmt.Fprintln(
os.Stderr,
"Erro: timeout deve ser maior que zero.",
)

return 2
}

// Cria o resolver correspondente à configuração.
resolver, err := criarResolver(*servidor)

if err != nil {
fmt.Fprintln(os.Stderr, "Erro:", err)
return 2
}

// Define um limite para a consulta completa.
ctx, cancel := context.WithTimeout(
context.Background(),
*timeout,
)

defer cancel()

// Registra o início da operação.
inicio := time.Now()

// Executa a consulta solicitada.
err = consultar(
ctx,
resolver,
*dominio,
registro,
)

duracao := time.Since(inicio)

if err != nil {
var dnsErr *net.DNSError

if errors.As(err, &dnsErr) {
switch {
case dnsErr.IsNotFound:
fmt.Fprintln(
os.Stderr,
"Erro: nome ou registro não encontrado.",
)

case dnsErr.IsTimeout:
fmt.Fprintln(
os.Stderr,
"Erro: consulta DNS excedeu o tempo limite.",
)

default:
fmt.Fprintln(
os.Stderr,
"Erro DNS:",
dnsErr,
)
}
} else {
fmt.Fprintln(
os.Stderr,
"Erro na consulta:",
err,
)
}

return 1
}

// Apresenta o tempo gasto na resolução.
fmt.Println("Duração:", duracao)

return 0
}

A função main apenas chama executar e encaminha seu código de retorno para os.Exit. Essa organização permite que a função principal da ferramenta execute seus tratamentos normalmente, incluindo a liberação do contexto registrada com defer. Também facilita separar erros de parâmetros, que retornam o código 2, de falhas durante a consulta, que retornam o código 1.


A função criarResolver recebe o endereço de um servidor DNS opcional. Quando o valor está vazio, retorna net.DefaultResolver, permitindo usar a resolução padrão do ambiente. Quando um endereço é informado, a função verifica se ele representa um IP válido e cria um resolver com Dial personalizado, direcionando as conexões DNS para a porta 53 daquele servidor.


A validação usando net.ParseIP também evita depender de uma resolução de nomes adicional para localizar o próprio servidor DNS. Isso simplifica o comportamento da ferramenta, principalmente quando estamos investigando problemas de DNS. O uso de net.JoinHostPort permite trabalhar tanto com endereços IPv4 quanto com IPv6, desde que o ambiente possua conectividade adequada com o servidor escolhido.


A função consultar recebe o contexto, o resolver, o domínio e o tipo de registro. Ela usa switch para selecionar o método correspondente, apresentando os valores de maneira adequada a cada tipo. Como os métodos possuem retornos diferentes, cada alternativa é responsável por interpretar suas próprias estruturas antes de escrever os resultados no terminal.


Na função executar, usamos flag.NewFlagSet para declarar as opções aceitas. A flag -nome representa o domínio consultado, -tipo define o registro desejado, -servidor permite escolher um DNS específico e -timeout estabelece o limite máximo da operação. Os valores recebidos são validados antes de executar a consulta, evitando realizar operações de rede quando os parâmetros já são conhecidos como inválidos.


Também registramos o instante inicial usando time.Now() e calculamos o tempo decorrido com time.Since. Esse valor permite observar quanto tempo a consulta demorou, incluindo o processamento realizado pelo método de resolução. Entretanto, ele não representa exclusivamente o tempo de transporte de uma mensagem DNS, porque o resolver pode realizar operações adicionais ou considerar informações locais.


Quando a consulta retorna um erro, tentamos identificar net.DNSError usando errors.As. Isso permite apresentar mensagens específicas para algumas situações, como ausência de registros e timeout. Para outros erros, a ferramenta apresenta o valor retornado, preservando informações úteis para o diagnóstico. Como os métodos de alto nível não expõem todos os detalhes do protocolo, a ferramenta também não tenta apresentar códigos DNS que não foram disponibilizados pela API usada.



Executando consultas pela ferramenta​


Depois de criar o arquivo main.go, podemos executar a ferramenta usando go run. Como as opções são interpretadas pelo pacote flag, precisamos informar seus nomes e valores conforme a interface definida no programa. Quando não informamos o tipo de registro, a ferramenta usa A como padrão, enquanto o resolver do sistema é usado quando a flag -servidor permanece vazia.


Para consultar registros A:

go run . -nome example.com -tipo A

Para consultar registros AAAA:

go run . -nome example.com -tipo AAAA

Para consultar registros MX:

go run . -nome example.com -tipo MX

Para consultar registros TXT:

go run . -nome example.com -tipo TXT

Para consultar o nome canônico:

go run . -nome www.example.com -tipo CNAME

Também podemos escolher um servidor DNS específico usando a flag -servidor. Nesse exemplo, a consulta será direcionada ao endereço 1.1.1.1, desde que o mecanismo de resolução realmente precise consultar o DNS pela rede e o servidor esteja acessível.

go run . \
-nome example.com \
-tipo A \
-servidor 1.1.1.1

Para modificar o tempo máximo da consulta, podemos informar uma duração na flag -timeout. O pacote flag interpreta valores como 2s, 500ms e 1m, convertendo automaticamente o texto para time.Duration.

go run . \
-nome example.com \
-tipo MX \
-servidor 1.1.1.1 \
-timeout 3s

Nesse exemplo, a ferramenta tentará consultar registros MX usando o servidor informado e um prazo máximo de três segundos. Caso a resolução termine normalmente, os registros encontrados serão apresentados junto ao tempo gasto na operação. Se o prazo for ultrapassado ou acontecer outro erro, a ferramenta apresentará a mensagem correspondente e terminará com um código diferente de zero.



Compilando a ferramenta DNS​


Depois de testar o programa com go run, podemos compilá-lo para gerar um executável. Isso permite executar a ferramenta diretamente pelo terminal, sem precisar iniciar a compilação do código-fonte em cada uso. O comando go build recebe a opção -o, que permite definir o nome e o caminho do arquivo executável gerado.


Em sistemas Linux e macOS, podemos compilar a ferramenta da seguinte maneira:

go build -o dnscli .

Depois da compilação, podemos executar o programa diretamente pelo caminho do arquivo gerado:

./dnscli -nome example.com -tipo A

No Windows, podemos gerar um executável com a extensão .exe, seguindo as convenções da plataforma.

go build -o dnscli.exe .

Depois, podemos executá-lo pelo PowerShell:

.\dnscli.exe -nome example.com -tipo A

O binário gerado contém o programa compilado e pode ser executado sem depender diretamente do comando go run. Entretanto, isso não significa que todas as consultas DNS serão independentes do ambiente, porque o resolver padrão ainda pode depender das configurações de rede e dos mecanismos de resolução disponíveis no sistema operacional.


Também precisamos considerar que executáveis são gerados para uma combinação específica de sistema operacional e arquitetura. Um binário compilado para Linux não poderá ser executado normalmente no Windows sem uma versão compatível. Go permite configurar compilações para outras plataformas usando variáveis como GOOS e GOARCH, mas o comportamento de determinadas funcionalidades pode depender do sistema de destino.



Comparando resultados entre servidores DNS​


Uma das aplicações práticas de uma ferramenta DNS é comparar respostas obtidas por diferentes servidores. Podemos executar a mesma consulta usando o resolver padrão e depois direcioná-la para outro endereço DNS, verificando se os resultados são equivalentes. Isso ajuda a investigar situações em que determinado domínio funciona em uma rede, mas apresenta falhas em outra.


Podemos começar consultando o resolver padrão:

./dnscli -nome example.com -tipo A

Depois, podemos consultar um servidor específico:

./dnscli \
-nome example.com \
-tipo A \
-servidor 1.1.1.1

Também podemos realizar a mesma consulta usando dig, permitindo comparar os resultados apresentados pela ferramenta Go com uma ferramenta que oferece informações mais detalhadas sobre o protocolo DNS.

dig @1.1.1.1 example.com A

Se os resultados forem diferentes, isso não significa automaticamente que algum servidor está com defeito. As respostas podem variar por causa de caches, balanceamento de carga, políticas de resolução ou diferentes informações disponíveis em cada servidor. Também precisamos considerar que o resolver padrão pode consultar arquivos locais ou aplicar regras que não correspondem diretamente a uma consulta DNS enviada pela rede.


Quando investigamos problemas de propagação de alterações DNS, normalmente precisamos examinar informações adicionais, como TTL e registros retornados pelos servidores responsáveis pela zona. Como nossa ferramenta usa os métodos de alto nível de net.Resolver, ela não apresenta esses campos diretamente. Para esse tipo de análise, ferramentas como dig continuam sendo úteis, especialmente quando precisamos consultar servidores autoritativos e examinar os detalhes completos das respostas.



Cuidados ao interpretar resultados DNS​


Uma consulta DNS concluída com sucesso não significa que o serviço associado ao domínio está funcionando corretamente. Encontrar um registro A ou AAAA apenas indica que o mecanismo de resolução conseguiu obter um endereço correspondente. Para verificar se existe um serviço TCP ou UDP disponível naquele destino, precisamos executar uma operação de conexão ou comunicação adequada ao protocolo esperado.


Também precisamos considerar que registros DNS podem mudar ao longo do tempo. Um servidor pode atualizar seus endereços, remover registros ou modificar configurações, enquanto caches ainda armazenam informações anteriores. Isso significa que consultas realizadas em momentos diferentes ou por resolvers diferentes podem apresentar resultados distintos sem que necessariamente exista uma falha.


Outro cuidado envolve informações recebidas de servidores DNS externos. Consultas DNS tradicionais não oferecem criptografia e autenticação equivalentes às fornecidas por protocolos seguros, permitindo determinados riscos relacionados à observação ou manipulação do tráfego. Quando precisamos de garantias adicionais, podemos trabalhar com mecanismos como DNSSEC para validação de dados DNS e transportes protegidos, como DNS over TLS ou DNS over HTTPS, respeitando as diferenças entre suas responsabilidades.


Por fim, uma ferramenta baseada no pacote net deve deixar claro quais informações está apresentando. Os métodos usados neste capítulo permitem resolver nomes e consultar registros comuns, mas não substituem uma implementação completa do protocolo DNS. Quando precisamos controlar diretamente as mensagens, interpretar códigos de resposta ou consultar campos adicionais, devemos trabalhar com bibliotecas que ofereçam acesso à estrutura do protocolo.