Para resolver um hostname em Zig, use a API de resolução de std.net disponível na versão instalada — normalmente getAddressList —, percorra todos os endereços IPv4 e IPv6 retornados e tente a conexão com um prazo total limitado. Não fixe o primeiro IP, não mantenha um cache eterno e não trate DNS como uma simples conversão de texto: a consulta pode bloquear, retornar vários candidatos, mudar ao longo do tempo ou falhar independentemente do servidor de destino.
Em uma CLI pequena, a resolução direta do sistema costuma bastar. Em um backend de alta concorrência, vale separar resolução, seleção de endereço, conexão, cache e observabilidade. Essa divisão evita um problema comum: culpar TCP ou HTTP quando o atraso real aconteceu antes, no DNS.
Resposta rápida
| Necessidade | Recomendação |
|---|---|
| Converter IP literal em endereço | use std.net.Address.parseIp ou função equivalente da versão |
Resolver api.exemplo.com | use std.net.getAddressList e percorra os candidatos |
| Conectar por hostname | prefira helper da stdlib quando suficiente; controle manualmente quando precisar de política |
| IPv4 e IPv6 | aceite ambos e implemente fallback com prazo |
| Timeout | imponha prazo à operação completa, não apenas ao socket conectado |
| Cache | comece sem cache próprio; adicione somente após medir |
| Erro negativo | não guarde para sempre; use expiração curta e backoff |
| Segurança | valide cada IP resolvido antes de conectar |
| Métricas | separe tempo de DNS, tempo de conexão e tempo da aplicação |
| Produção | limite concorrência, tentativas e memória por consulta |
Se você ainda está trabalhando com endereços e sockets, comece pela referência de std.net.Address e pelo guia de sockets TCP e UDP em Zig. Para chamadas web, combine este conteúdo com o tutorial de HTTP client em Zig.
DNS não é parse de endereço IP
Estas duas operações parecem semelhantes, mas têm contratos diferentes.
Fazer parse de um IP literal é local:
const std = @import("std");
pub fn main() !void {
const ipv4 = try std.net.Address.parseIp("192.0.2.10", 443);
const ipv6 = try std.net.Address.parseIp("2001:db8::10", 443);
std.debug.print("IPv4: {}\nIPv6: {}\n", .{ ipv4, ipv6 });
}
O programa valida a sintaxe, monta o endereço e não precisa consultar servidor DNS. Já resolver um nome como api.exemplo.com pode envolver:
- configuração local em
/etc/hosts; - cache do sistema ou de um resolver local;
- consulta a servidores recursivos;
- registros A para IPv4;
- registros AAAA para IPv6;
- aliases CNAME;
- mais de um endereço por nome;
- timeout, erro temporário ou resposta sem registros úteis.
Por isso, uma função que recebe “host” deve declarar se aceita apenas IP, apenas hostname ou ambos. Também deve diferenciar erro de sintaxe, erro de resolução e erro de conexão.
Resolvendo com getAddressList
O padrão da biblioteca padrão nas versões recentes de Zig é pedir uma lista de endereços ao resolver do sistema. A assinatura exata pode mudar antes do Zig 1.0; confira o código de std.net da versão usada pelo projeto.
O formato conceitual é:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var lista = try std.net.getAddressList(
allocator,
"ziglang.org",
443,
);
defer lista.deinit();
for (lista.addrs) |endereco| {
std.debug.print("{}\n", .{endereco});
}
}
Há três detalhes importantes no exemplo:
- a resolução pode alocar, portanto recebe um allocator;
- o resultado pode conter vários endereços;
- a lista possui recursos e precisa de
deinit.
Não copie apenas lista.addrs[0] sem verificar se a lista está vazia e sem pensar no fallback. DNS distribui tráfego e representa múltiplas rotas possíveis. A ordem retornada também pode refletir preferências da plataforma.
Conectando sem assumir um único IP
Para um cliente TCP, a política básica é tentar os candidatos até obter sucesso:
const std = @import("std");
fn conectarPrimeiroDisponivel(
allocator: std.mem.Allocator,
host: []const u8,
porta: u16,
) !std.net.Stream {
var lista = try std.net.getAddressList(allocator, host, porta);
defer lista.deinit();
var ultimo_erro: ?anyerror = null;
for (lista.addrs) |endereco| {
const stream = std.net.tcpConnectToAddress(endereco) catch |err| {
ultimo_erro = err;
continue;
};
return stream;
}
return ultimo_erro orelse error.NenhumEnderecoResolvido;
}
Esse exemplo ensina o fluxo, mas ainda não é uma implementação completa de produção. Cada tentativa pode consumir tempo demais, e fazer todas em série pode multiplicar a latência. Além disso, usar anyerror na API pública perde precisão. Em código real, crie um error set coerente com sua camada e aplique uma política de prazo.
Quando você só precisa abrir uma conexão simples por hostname, procure também o helper tcpConnectToHost da sua versão de std.net. Ele reduz código, mas oferece menos espaço para controlar ordem, telemetria, filtros de segurança e estratégia de fallback.
IPv4, IPv6 e o problema do fallback lento
Um hostname pode retornar registros A e AAAA:
api.exemplo.com. 60 IN A 198.51.100.20
api.exemplo.com. 60 IN AAAA 2001:db8::20
Ter um endereço IPv6 configurado não garante conectividade IPv6 saudável. Uma rede pode anunciar rota, mas descartar pacotes; um firewall pode bloquear a saída; ou o destino pode estar indisponível apenas em uma família.
O comportamento ruim é:
resolver -> escolher IPv6 -> esperar timeout longo -> tentar IPv4
O usuário percebe uma pausa grande antes de cada conexão. Clientes maduros usam uma estratégia inspirada em Happy Eyeballs: iniciam a alternativa após um pequeno atraso controlado, ou mantêm um histórico temporário da família que funcionou. Implementar a RFC inteira pode ser desnecessário para um serviço interno, mas três regras já ajudam:
- não desabilite IPv6 apenas por conveniência;
- não espere indefinidamente antes do fallback;
- use um orçamento total compartilhado entre resolução e conexão.
Por exemplo, se a operação tem 2 segundos, não conceda 2 segundos para DNS e mais 2 para cada um de quatro IPs. O deadline deve acompanhar toda a operação.
DNS pode bloquear uma thread
A resolução tradicional oferecida pelo sistema operacional frequentemente passa por funções bloqueantes. Isso é aceitável em uma CLI que faz uma consulta. Em um servidor com milhares de requisições concorrentes, resolver dentro do loop principal pode pausar trabalho não relacionado.
Opções comuns:
- resolver antes de iniciar o serviço para dependências estáveis;
- usar um pool limitado de workers para consultas bloqueantes;
- reutilizar conexões para reduzir resoluções repetidas;
- usar um resolver assíncrono quando o ambiente justificar a dependência;
- proteger o sistema com bulkhead, limitando consultas simultâneas.
Nunca crie uma thread sem limite para cada hostname. Uma falha no DNS poderia transformar um problema externo em esgotamento de memória, file descriptors e scheduler.
Para serviços resilientes, combine esse limite com timeout, retry e circuit breaker em Zig. O retry deve considerar a classe do erro: repetir imediatamente um nome inexistente não ajuda; repetir com backoff um erro temporário pode ajudar.
Quando criar cache DNS na aplicação
A resposta inicial é: não crie até provar a necessidade. O sistema operacional, systemd-resolved, nscd, um sidecar ou o DNS da infraestrutura pode já manter cache. Reutilizar conexões HTTP também costuma reduzir mais custo do que guardar IPs manualmente.
Um cache dentro do processo passa a fazer sentido quando:
- a aplicação resolve os mesmos nomes em alta frequência;
- o resolver local é uma fonte medida de latência;
- você precisa controlar stale-while-revalidate;
- existe uma política explícita para serviços descobertos por DNS;
- o ambiente fornece TTL e semântica que sua biblioteca consegue preservar.
Um cache correto precisa definir:
| Aspecto | Pergunta obrigatória |
|---|---|
| chave | hostname e porta? família? contexto de rede? |
| valor | todos os endereços ou apenas o último que funcionou? |
| expiração | qual TTL usar quando a API não o expõe? |
| concorrência | como evitar dez refreshes para a mesma chave? |
| limite | quantos hosts e bytes podem permanecer em memória? |
| falhas | existe cache negativo? por quanto tempo? |
| atualização | o valor antigo pode ser usado enquanto atualiza? |
| invalidação | como reagir a mudança de rede, VPN ou DNS local? |
Inventar um TTL de uma hora para todo domínio é perigoso. O serviço pode mudar de IP durante deploy ou failover, e o processo continuará tentando um destino antigo. Se a API usada não expõe TTL, prefira uma janela curta e conservadora ou deixe o cache para a infraestrutura.
Cache negativo e tempestade de retries
Quando uma resolução falha, centenas de requisições podem repetir a mesma consulta simultaneamente. Isso é um cache stampede aplicado a DNS.
Uma camada defensiva pode:
- coalescer consultas idênticas em andamento;
- guardar falhas temporariamente por poucos segundos;
- aplicar backoff exponencial com jitter;
- limitar a fila de espera;
- permitir que o chamador cancele ao atingir o deadline.
Separe pelo menos estas classes:
- nome inexistente ou sem registro útil;
- falha temporária do resolver;
- timeout;
- operação cancelada;
- endereço resolvido, mas conexão recusada;
- endereço resolvido, mas conexão expirou.
“Não conectou” é um diagnóstico insuficiente. Cada classe pede uma reação diferente.
Segurança: DNS, SSRF e rebinding
Se o hostname vem de input do usuário — webhook, importador de URL, proxy, crawler ou gerador de preview — a resolução faz parte da fronteira de segurança.
Validar apenas o texto não basta. Um domínio aparentemente público pode resolver para:
127.0.0.1ou::1;- redes privadas como
10.0.0.0/8e192.168.0.0/16; - link-local, incluindo endpoints de metadata da nuvem;
- multicast ou faixas reservadas;
- um IP público na primeira consulta e privado na seguinte.
A política segura é:
- aceitar apenas esquemas e portas necessários;
- resolver o hostname;
- validar todos os endereços retornados;
- bloquear faixas não permitidas;
- conectar ao endereço validado, sem resolver novamente de forma invisível;
- repetir a validação após redirecionamentos;
- limitar corpo, tempo, número de redirects e bytes transferidos.
O passo 5 reduz a janela para DNS rebinding, em que respostas diferentes aparecem entre validação e conexão. Dependendo da API HTTP, controlar exatamente o endereço conectado exige uma camada própria; documente essa limitação no threat model em vez de afirmar segurança absoluta.
Timeouts e cancelamento
DNS não deve ter um timeout isolado que ignora o restante da chamada. Modele um deadline:
prazo total: 2.000 ms
DNS: consumiu 180 ms
tentativa IPv6: consumiu 250 ms
tentativa IPv4: tem no máximo 1.570 ms restantes
Esse orçamento impede que cada etapa reinicie o relógio. O mesmo princípio vale para HTTP: resolução, conexão TCP, handshake TLS, envio, primeiro byte e leitura do corpo pertencem à mesma operação percebida pelo usuário.
Se a API de resolução subjacente não oferece cancelamento, execute-a em workers limitados e permita que o chamador deixe de esperar. O worker ainda pode terminar depois, então o pool também precisa de capacidade máxima; cancelamento lógico não torna uma syscall bloqueante magicamente cancelável.
Observabilidade que realmente ajuda
Não registre apenas request_failed. Separe métricas:
dns_lookup_duration_seconds
dns_lookup_total{result="ok|timeout|not_found|temporary"}
tcp_connect_duration_seconds{family="ipv4|ipv6"}
tcp_connect_total{result="ok|refused|timeout"}
Labels devem ter cardinalidade limitada. Não coloque o hostname completo como label se o conjunto pode crescer sem controle; isso explode séries no Prometheus. Para poucos upstreams conhecidos, use um nome lógico como upstream="pagamentos".
Nos logs estruturados, campos úteis incluem:
- nome lógico do upstream;
- duração da resolução;
- quantidade de candidatos;
- família escolhida;
- número da tentativa;
- classe do erro;
- tempo restante no deadline;
- se houve hit, miss ou refresh de cache.
Evite logar credenciais presentes em URLs. Para integrar essas medições, veja o guia de logs estruturados e Prometheus em Zig.
Testes que evitam incidentes
Teste a política sem depender sempre da internet. Separe a interface do resolver para injetar respostas previsíveis:
const Resolver = struct {
context: *anyopaque,
resolveFn: *const fn (
context: *anyopaque,
allocator: std.mem.Allocator,
host: []const u8,
port: u16,
) anyerror![]std.net.Address,
};
Com um fake, cubra:
- apenas IPv4;
- apenas IPv6;
- IPv6 falha e IPv4 funciona;
- lista vazia;
- nome inexistente;
- timeout temporário;
- quatro IPs falhando antes de um sucesso;
- deadline vencendo entre tentativas;
- cache expirado;
- consultas concorrentes coalescidas;
- endereço privado bloqueado por política SSRF.
Depois, tenha poucos testes de integração contra um resolver controlado ou nomes reservados para teste. Não faça a suíte unitária depender de google.com: rede externa deixa o teste lento e não determinístico.
Checklist de produção
Antes de publicar um cliente ou serviço que depende de DNS, confirme:
- hostname e porta têm limites de tamanho e formato;
- IP literal e hostname seguem caminhos explícitos;
- todos os endereços retornados são considerados;
- IPv4 e IPv6 têm fallback com deadline;
- resolução bloqueante não pausa o loop principal;
- concorrência de consultas é limitada;
- retry usa backoff, jitter e orçamento total;
- cache próprio, se existir, tem expiração e limite;
- falhas negativas não ficam guardadas para sempre;
- IPs privados e reservados são filtrados quando necessário;
- redirects passam pela mesma validação de segurança;
- métricas distinguem DNS, TCP, TLS e aplicação;
- logs não expõem tokens nem criam cardinalidade ilimitada;
- testes cobrem IPv4, IPv6, fallback, timeout e lista vazia;
- nomes de funções foram conferidos na versão real do Zig.
Conclusão
Resolver DNS em Zig começa com poucas linhas, mas um cliente confiável precisa tratar a lista de endereços como uma lista de alternativas — não como um único IP definitivo. Use o resolver do sistema, percorra IPv4 e IPv6, limite a operação inteira por deadline e meça DNS separadamente da conexão.
Mantenha a primeira versão simples: sem cache próprio e sem abstração excessiva. Quando métricas mostrarem gargalo, adicione workers limitados, coalescing, expiração e fallback mais inteligente. E, se o hostname vier do usuário, trate cada endereço resolvido como input não confiável antes de abrir o socket.
Perguntas frequentes
Como resolver um hostname para IP em Zig?
Use a API de resolução da sua versão de std.net, normalmente getAddressList com allocator, hostname e porta. Percorra a lista retornada e libere seus recursos com deinit.
Devo escolher sempre o primeiro endereço retornado pelo DNS?
Não. O primeiro pode estar indisponível ou pertencer a uma família de rede sem conectividade funcional. Tente candidatos com prazo limitado e registre qual opção funcionou.
É uma boa ideia implementar cache DNS dentro da aplicação Zig?
Somente após medir uma necessidade. Um cache correto precisa de TTL, limite, sincronização, atualização e política para erros. O cache do sistema ou a reutilização de conexões pode resolver o problema com menos risco.
Qual é a diferença entre resolver um hostname e fazer parse de um IP?
Parse de IP é uma operação local e determinística. Resolução de hostname consulta a infraestrutura configurada, pode bloquear, retornar vários endereços e mudar com o tempo.
Como evitar SSRF ao resolver hostnames recebidos do usuário?
Valide todos os IPs depois da resolução e conecte apenas ao endereço validado. Bloqueie loopback, link-local, redes privadas e reservadas conforme o produto, e repita a política em redirects.
Zig usa DNS assíncrono automaticamente?
Não presuma. Verifique a implementação da versão e da plataforma. Quando a resolução puder bloquear, isole-a em workers limitados ou adote um resolver assíncrono adequado ao ambiente.