DNS Resolver em Zig — Tutorial Passo a Passo
Neste tutorial, vamos construir um resolver DNS customizado em Zig que envia consultas DNS via UDP, parseia as respostas e exibe os registros encontrados. Este é um projeto fascinante para entender como a internet resolve nomes de domínio.
O Que Vamos Construir
Nosso DNS resolver vai:
- Enviar consultas DNS via UDP para servidores DNS públicos
- Parsear respostas DNS incluindo headers, questões e registros
- Suportar tipos de registro A, AAAA, CNAME e MX
- Implementar compressão de nomes DNS (label compression)
- Exibir resultados formatados com TTL e classe
Por Que Este Projeto?
DNS é a espinha dorsal da internet. Entender como funciona — desde a codificação binária das mensagens até a compressão de nomes — é fundamental para qualquer programador de sistemas. Em Zig, podemos manipular bytes diretamente com total controle, ideal para protocolos binários.
Pré-requisitos
- Zig 0.13+ instalado (guia de instalação)
- Conhecimento básico de redes e UDP
- Familiaridade com manipulação de bytes
Passo 1: Estrutura do Projeto
mkdir dns-resolver
cd dns-resolver
zig init
Passo 2: Estruturas do Protocolo DNS
O protocolo DNS define um formato binário específico para mensagens. Cada mensagem tem um header de 12 bytes, seguido de seções de pergunta, resposta, autoridade e adicional.
const std = @import("std");
const net = std.net;
const posix = std.posix;
const io = std.io;
const mem = std.mem;
const fmt = std.fmt;
/// Tipos de registro DNS.
const TipoRegistro = enum(u16) {
A = 1, // IPv4
NS = 2, // Name Server
CNAME = 5, // Alias
MX = 15, // Mail Exchange
AAAA = 28, // IPv6
_,
pub fn nome(self: TipoRegistro) []const u8 {
return switch (self) {
.A => "A",
.NS => "NS",
.CNAME => "CNAME",
.MX => "MX",
.AAAA => "AAAA",
_ => "UNKNOWN",
};
}
};
/// Header DNS (12 bytes).
const DnsHeader = struct {
id: u16,
flags: u16,
num_questoes: u16,
num_respostas: u16,
num_autoridade: u16,
num_adicional: u16,
};
/// Um registro DNS parseado.
const DnsRecord = struct {
nome: [256]u8,
nome_len: usize,
tipo: TipoRegistro,
classe: u16,
ttl: u32,
dados: [512]u8,
dados_len: usize,
pub fn nomeStr(self: *const DnsRecord) []const u8 {
return self.nome[0..self.nome_len];
}
pub fn dadosStr(self: *const DnsRecord) []const u8 {
return self.dados[0..self.dados_len];
}
};
Passo 3: Construção da Query DNS
/// Constrói uma query DNS em formato binário.
fn construirQuery(
buf: []u8,
dominio: []const u8,
tipo: TipoRegistro,
id: u16,
) !usize {
var pos: usize = 0;
// Header
// ID
buf[pos] = @intCast(id >> 8);
buf[pos + 1] = @intCast(id & 0xFF);
pos += 2;
// Flags: recursion desired
buf[pos] = 0x01;
buf[pos + 1] = 0x00;
pos += 2;
// Questions: 1
buf[pos] = 0x00;
buf[pos + 1] = 0x01;
pos += 2;
// Answer, Authority, Additional: 0
@memset(buf[pos .. pos + 6], 0);
pos += 6;
// Question section: encode domain name
// "www.example.com" -> [3]www[7]example[3]com[0]
var it = mem.splitScalar(u8, dominio, '.');
while (it.next()) |label| {
if (label.len == 0 or label.len > 63) return error.DominioInvalido;
buf[pos] = @intCast(label.len);
pos += 1;
@memcpy(buf[pos .. pos + label.len], label);
pos += label.len;
}
buf[pos] = 0; // terminator
pos += 1;
// Type
const tipo_val = @intFromEnum(tipo);
buf[pos] = @intCast(tipo_val >> 8);
buf[pos + 1] = @intCast(tipo_val & 0xFF);
pos += 2;
// Class: IN (Internet) = 1
buf[pos] = 0x00;
buf[pos + 1] = 0x01;
pos += 2;
return pos;
}
Passo 4: Parser de Respostas DNS
/// Lê um u16 big-endian de um buffer.
fn lerU16(buf: []const u8, pos: usize) u16 {
return (@as(u16, buf[pos]) << 8) | buf[pos + 1];
}
/// Lê um u32 big-endian de um buffer.
fn lerU32(buf: []const u8, pos: usize) u32 {
return (@as(u32, buf[pos]) << 24) |
(@as(u32, buf[pos + 1]) << 16) |
(@as(u32, buf[pos + 2]) << 8) |
buf[pos + 3];
}
/// Decodifica um nome DNS com suporte a compressão de ponteiros.
/// DNS usa compressão: se os dois bits mais significativos de um byte são 11,
/// os próximos 14 bits são um offset para o início do nome comprimido.
fn decodificarNome(
buf: []const u8,
pos_inicial: usize,
nome_buf: *[256]u8,
) !struct { len: usize, bytes_consumidos: usize } {
var pos = pos_inicial;
var nome_pos: usize = 0;
var bytes_consumidos: usize = 0;
var seguiu_ponteiro = false;
while (pos < buf.len) {
const len_byte = buf[pos];
if (len_byte == 0) {
// Fim do nome
if (!seguiu_ponteiro) bytes_consumidos = pos - pos_inicial + 1;
break;
}
if (len_byte & 0xC0 == 0xC0) {
// Ponteiro de compressão
if (!seguiu_ponteiro) {
bytes_consumidos = pos - pos_inicial + 2;
seguiu_ponteiro = true;
}
const offset = (@as(u16, len_byte & 0x3F) << 8) | buf[pos + 1];
pos = offset;
continue;
}
// Label normal
if (nome_pos > 0) {
nome_buf[nome_pos] = '.';
nome_pos += 1;
}
const label_len = len_byte;
@memcpy(nome_buf[nome_pos .. nome_pos + label_len], buf[pos + 1 .. pos + 1 + label_len]);
nome_pos += label_len;
pos += 1 + label_len;
}
if (!seguiu_ponteiro) bytes_consumidos = pos - pos_inicial + 1;
return .{ .len = nome_pos, .bytes_consumidos = bytes_consumidos };
}
/// Parseia a seção de respostas DNS.
fn parsearRespostas(
buf: []const u8,
pos_inicial: usize,
num_registros: u16,
registros: *std.ArrayList(DnsRecord),
) !usize {
var pos = pos_inicial;
for (0..num_registros) |_| {
var registro: DnsRecord = undefined;
// Decodificar nome
const nome_result = try decodificarNome(buf, pos, ®istro.nome);
registro.nome_len = nome_result.len;
pos += nome_result.bytes_consumidos;
// Tipo, classe, TTL, tamanho dos dados
registro.tipo = @enumFromInt(lerU16(buf, pos));
pos += 2;
registro.classe = lerU16(buf, pos);
pos += 2;
registro.ttl = lerU32(buf, pos);
pos += 4;
const rdlength = lerU16(buf, pos);
pos += 2;
// Interpretar dados conforme o tipo
registro.dados_len = 0;
switch (registro.tipo) {
.A => {
if (rdlength == 4) {
const ip = fmt.bufPrint(®istro.dados, "{d}.{d}.{d}.{d}", .{
buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3],
}) catch "";
registro.dados_len = ip.len;
}
},
.AAAA => {
if (rdlength == 16) {
var ip_buf: [39]u8 = undefined;
var ip_len: usize = 0;
var j: usize = 0;
while (j < 16) : (j += 2) {
if (j > 0) {
ip_buf[ip_len] = ':';
ip_len += 1;
}
const parte = fmt.bufPrint(ip_buf[ip_len..], "{x:0>2}{x:0>2}", .{
buf[pos + j], buf[pos + j + 1],
}) catch break;
ip_len += parte.len;
}
@memcpy(registro.dados[0..ip_len], ip_buf[0..ip_len]);
registro.dados_len = ip_len;
}
},
.CNAME, .NS => {
var nome_dados: [256]u8 = undefined;
const result = try decodificarNome(buf, pos, &nome_dados);
@memcpy(registro.dados[0..result.len], nome_dados[0..result.len]);
registro.dados_len = result.len;
},
.MX => {
const preferencia = lerU16(buf, pos);
var nome_mx: [256]u8 = undefined;
const result = try decodificarNome(buf, pos + 2, &nome_mx);
const mx_str = fmt.bufPrint(®istro.dados, "{d} {s}", .{
preferencia, nome_mx[0..result.len],
}) catch "";
registro.dados_len = mx_str.len;
},
_ => {
const len = @min(rdlength, @as(u16, @intCast(registro.dados.len)));
@memcpy(registro.dados[0..len], buf[pos .. pos + len]);
registro.dados_len = len;
},
}
pos += rdlength;
try registros.append(registro);
}
return pos;
}
Passo 5: Função Main e Execução
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const stdout = io.getStdOut().writer();
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
var dominio: []const u8 = "ziglang.org";
var tipo = TipoRegistro.A;
var servidor_dns: [4]u8 = .{ 8, 8, 8, 8 }; // Google DNS
if (args.len >= 2) {
dominio = args[1];
}
if (args.len >= 3) {
const tipo_str = args[2];
if (mem.eql(u8, tipo_str, "A")) tipo = .A
else if (mem.eql(u8, tipo_str, "AAAA")) tipo = .AAAA
else if (mem.eql(u8, tipo_str, "CNAME")) tipo = .CNAME
else if (mem.eql(u8, tipo_str, "MX")) tipo = .MX
else if (mem.eql(u8, tipo_str, "NS")) tipo = .NS;
}
try stdout.print(
\\
\\ ==========================================
\\ DNS RESOLVER - Zig
\\ ==========================================
\\ Consultando: {s}
\\ Tipo: {s}
\\ Servidor DNS: {d}.{d}.{d}.{d}
\\ ==========================================
\\
, .{ dominio, tipo.nome(), servidor_dns[0], servidor_dns[1], servidor_dns[2], servidor_dns[3] });
// Construir query
var query_buf: [512]u8 = undefined;
const query_len = try construirQuery(&query_buf, dominio, tipo, 0xABCD);
// Enviar via UDP
const socket = try posix.socket(posix.AF.INET, posix.SOCK.DGRAM, 0);
defer posix.close(socket);
const dest = net.Address.initIp4(servidor_dns, 53);
_ = try posix.sendto(socket, query_buf[0..query_len], 0, &dest.any, dest.getOsSockLen());
// Receber resposta
var resp_buf: [4096]u8 = undefined;
const resp_len = try posix.read(socket, &resp_buf);
const resposta = resp_buf[0..resp_len];
// Parsear header
const header = DnsHeader{
.id = lerU16(resposta, 0),
.flags = lerU16(resposta, 2),
.num_questoes = lerU16(resposta, 4),
.num_respostas = lerU16(resposta, 6),
.num_autoridade = lerU16(resposta, 8),
.num_adicional = lerU16(resposta, 10),
};
const rcode = header.flags & 0x0F;
try stdout.print(" Status: {s}\n", .{switch (rcode) {
0 => "NOERROR",
1 => "FORMERR",
2 => "SERVFAIL",
3 => "NXDOMAIN",
else => "UNKNOWN",
}});
try stdout.print(" Respostas: {d}, Autoridade: {d}, Adicional: {d}\n\n", .{
header.num_respostas, header.num_autoridade, header.num_adicional,
});
// Pular seção de questões
var pos: usize = 12;
for (0..header.num_questoes) |_| {
var dummy: [256]u8 = undefined;
const result = try decodificarNome(resposta, pos, &dummy);
pos += result.bytes_consumidos;
pos += 4; // type + class
}
// Parsear respostas
var registros = std.ArrayList(DnsRecord).init(allocator);
defer registros.deinit();
pos = try parsearRespostas(resposta, pos, header.num_respostas, ®istros);
// Exibir resultados
if (registros.items.len == 0) {
try stdout.print(" Nenhum registro encontrado.\n", .{});
} else {
try stdout.print(" {s:<30} {s:<8} {s:<8} {s}\n", .{ "NOME", "TIPO", "TTL", "DADOS" });
try stdout.print(" {s:-<70}\n", .{""});
for (registros.items) |r| {
try stdout.print(" {s:<30} {s:<8} {d:<8} {s}\n", .{
r.nomeStr(), r.tipo.nome(), r.ttl, r.dadosStr(),
});
}
}
}
Testes
test "construir query DNS" {
var buf: [512]u8 = undefined;
const len = try construirQuery(&buf, "example.com", .A, 0x1234);
try std.testing.expect(len > 12); // pelo menos o header
try std.testing.expectEqual(@as(u8, 0x12), buf[0]); // ID high byte
try std.testing.expectEqual(@as(u8, 0x34), buf[1]); // ID low byte
}
test "decodificar nome simples" {
// [3]www[7]example[3]com[0]
const dados = [_]u8{ 3, 'w', 'w', 'w', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0 };
var nome: [256]u8 = undefined;
const result = try decodificarNome(&dados, 0, &nome);
try std.testing.expectEqualStrings("www.example.com", nome[0..result.len]);
}
test "lerU16 big-endian" {
const dados = [_]u8{ 0x01, 0x00 };
try std.testing.expectEqual(@as(u16, 256), lerU16(&dados, 0));
}
test "lerU32 big-endian" {
const dados = [_]u8{ 0x00, 0x00, 0x01, 0x00 };
try std.testing.expectEqual(@as(u32, 256), lerU32(&dados, 0));
}
Compilando e Executando
# Resolver um domínio (padrão: ziglang.org tipo A)
zig build run
# Resolver domínio específico
zig build run -- example.com
# Consultar tipo específico
zig build run -- google.com MX
zig build run -- ziglang.org AAAA
# Rodar testes
zig build test
Conceitos Aprendidos
- Protocolo DNS com formato binário de mensagens
- Sockets UDP para comunicação sem conexão
- Big-endian e manipulação de bytes
- Compressão de nomes com ponteiros de offset
- Enums com valores para tipos de registro
Próximos Passos
- Explore a stdlib de redes para mais protocolos
- Veja o Port Scanner para mais uso de sockets
- Construa o Protocolo Binário para serialização avançada
- Consulte manipulação de bytes na stdlib