Port Scanner TCP em Zig — Tutorial Passo a Passo
Neste tutorial, vamos construir um scanner de portas TCP em Zig. O scanner tenta se conectar a portas de um host e reporta quais estão abertas. Este projeto nos ensina sobre programação de rede, timeouts e detecção de serviços.
O Que Vamos Construir
Nosso port scanner vai:
- Escanear um range de portas TCP em um host
- Implementar timeout configurável por conexão
- Identificar serviços conhecidos (HTTP, SSH, FTP, etc.)
- Exibir resultados formatados com estado de cada porta
- Medir o tempo total do scan
Por Que Este Projeto?
Scanners de portas são ferramentas fundamentais para diagnóstico de rede e segurança. Construir um nos ensina como funcionam conexões TCP, handshakes, timeouts e o mapeamento de serviços a portas. O controle de baixo nível de Zig sobre sockets torna isso direto e eficiente.
Pré-requisitos
- Zig 0.13+ instalado (guia de instalação)
- Conhecimento básico de TCP/IP
- Familiaridade com programação de rede em Zig
Passo 1: Estrutura do Projeto
mkdir port-scanner
cd port-scanner
zig init
Passo 2: Mapa de Serviços Conhecidos
Começamos com uma tabela que mapeia portas comuns aos seus serviços. Isso permite que o scanner identifique automaticamente o que provavelmente está rodando em cada porta aberta.
const std = @import("std");
const net = std.net;
const posix = std.posix;
const io = std.io;
const mem = std.mem;
const time = std.time;
const fmt = std.fmt;
/// Resultado do scan de uma porta individual.
const ResultadoPorta = struct {
porta: u16,
aberta: bool,
servico: ?[]const u8,
tempo_ms: u64,
};
/// Retorna o nome do serviço associado a uma porta conhecida.
fn servicoConhecido(porta: u16) ?[]const u8 {
return switch (porta) {
20 => "FTP (dados)",
21 => "FTP (controle)",
22 => "SSH",
23 => "Telnet",
25 => "SMTP",
53 => "DNS",
80 => "HTTP",
110 => "POP3",
143 => "IMAP",
443 => "HTTPS",
993 => "IMAPS",
995 => "POP3S",
3306 => "MySQL",
5432 => "PostgreSQL",
6379 => "Redis",
8080 => "HTTP Alt",
8443 => "HTTPS Alt",
27017 => "MongoDB",
else => null,
};
}
Passo 3: Lógica de Scan
/// Configuração do scanner.
const ConfigScanner = struct {
host: []const u8,
porta_inicio: u16,
porta_fim: u16,
timeout_ms: u64,
};
/// Tenta conectar a uma porta específica com timeout.
/// Retorna true se a porta está aberta (aceita conexão).
fn escanearPorta(host: [4]u8, porta: u16, timeout_ns: u64) bool {
const endereco = net.Address.initIp4(host, porta);
// Criar socket TCP
const socket = posix.socket(posix.AF.INET, posix.SOCK.STREAM | posix.SOCK.NONBLOCK, 0) catch return false;
defer posix.close(socket);
// Tentar conexão (não-bloqueante)
_ = posix.connect(socket, &endereco.any, endereco.getOsSockLen()) catch |err| {
if (err != error.WouldBlock) return false;
};
// Usar poll para esperar a conexão com timeout
var pollfds = [_]posix.pollfd{.{
.fd = socket,
.events = posix.POLL.OUT,
.revents = 0,
}};
const timeout_ms: i32 = @intCast(timeout_ns / time.ns_per_ms);
const resultado = posix.poll(&pollfds, timeout_ms) catch return false;
if (resultado == 0) return false; // timeout
// Verificar se a conexão foi estabelecida
if (pollfds[0].revents & posix.POLL.OUT != 0) {
// Verificar erro do socket
var err_code: i32 = 0;
const err_bytes = mem.asBytes(&err_code);
_ = posix.getsockopt(socket, posix.SOL.SOCKET, posix.SO.ERROR, err_bytes) catch return false;
return err_code == 0;
}
return false;
}
/// Resolve um hostname para endereço IPv4.
fn resolverHost(host: []const u8) ?[4]u8 {
// Tentar parsear como IP direto primeiro
var ip: [4]u8 = undefined;
var it = mem.splitScalar(u8, host, '.');
var i: usize = 0;
while (it.next()) |parte| {
if (i >= 4) return null;
ip[i] = fmt.parseInt(u8, parte, 10) catch return null;
i += 1;
}
if (i == 4) return ip;
return null;
}
Passo 4: Execução do Scan e Relatório
/// Executa o scan completo e retorna os resultados.
fn executarScan(
config: ConfigScanner,
allocator: mem.Allocator,
writer: anytype,
) !std.ArrayList(ResultadoPorta) {
var resultados = std.ArrayList(ResultadoPorta).init(allocator);
const ip = resolverHost(config.host) orelse {
try writer.print("Erro: nao foi possivel resolver '{s}'\n", .{config.host});
return resultados;
};
const total_portas = @as(u32, config.porta_fim) - config.porta_inicio + 1;
try writer.print("\nEscaneando {s} ({d}.{d}.{d}.{d})\n", .{
config.host, ip[0], ip[1], ip[2], ip[3],
});
try writer.print("Portas: {d}-{d} ({d} portas)\n", .{
config.porta_inicio, config.porta_fim, total_portas,
});
try writer.print("Timeout: {d}ms\n\n", .{config.timeout_ms});
const inicio_total = time.milliTimestamp();
var portas_abertas: u32 = 0;
var porta: u32 = config.porta_inicio;
while (porta <= config.porta_fim) : (porta += 1) {
const p: u16 = @intCast(porta);
const inicio = time.milliTimestamp();
const aberta = escanearPorta(ip, p, config.timeout_ms * time.ns_per_ms);
const duracao: u64 = @intCast(time.milliTimestamp() - inicio);
if (aberta) {
const servico = servicoConhecido(p);
try resultados.append(.{
.porta = p,
.aberta = true,
.servico = servico,
.tempo_ms = duracao,
});
portas_abertas += 1;
// Exibir imediatamente quando encontra porta aberta
if (servico) |s| {
try writer.print(" ABERTA {d:>5}/tcp {s} ({d}ms)\n", .{ p, s, duracao });
} else {
try writer.print(" ABERTA {d:>5}/tcp desconhecido ({d}ms)\n", .{ p, duracao });
}
}
// Barra de progresso simples
if (porta % 100 == 0 or porta == config.porta_fim) {
const progresso = (porta - config.porta_inicio) * 100 / total_portas;
try writer.print("\r Progresso: {d}%", .{progresso});
}
}
const duracao_total: u64 = @intCast(time.milliTimestamp() - inicio_total);
try writer.print("\r \r", .{}); // limpar progresso
try writer.print(
\\
\\--- Relatorio ---
\\ Host: {s}
\\ Portas testadas: {d}
\\ Portas abertas: {d}
\\ Tempo total: {d}ms ({d:.1}s)
\\
, .{
config.host,
total_portas,
portas_abertas,
duracao_total,
@as(f64, @floatFromInt(duracao_total)) / 1000.0,
});
return resultados;
}
Passo 5: Função Main com CLI
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);
// Valores padrão
var config = ConfigScanner{
.host = "127.0.0.1",
.porta_inicio = 1,
.porta_fim = 1024,
.timeout_ms = 200,
};
// Parsing de argumentos
if (args.len < 2) {
try stdout.print(
\\
\\ ==========================================
\\ PORT SCANNER - Zig
\\ ==========================================
\\ Uso: portscan <host> [porta_inicio] [porta_fim] [timeout_ms]
\\
\\ Exemplos:
\\ portscan 127.0.0.1
\\ portscan 192.168.1.1 1 1024
\\ portscan 10.0.0.1 80 443 500
\\ ==========================================
\\
\\ Executando scan padrao em localhost (1-1024)...
\\
, .{});
} else {
config.host = args[1];
if (args.len >= 3) {
config.porta_inicio = try fmt.parseInt(u16, args[2], 10);
}
if (args.len >= 4) {
config.porta_fim = try fmt.parseInt(u16, args[3], 10);
}
if (args.len >= 5) {
config.timeout_ms = try fmt.parseInt(u64, args[4], 10);
}
}
// Validação
if (config.porta_inicio > config.porta_fim) {
try stdout.print("Erro: porta_inicio ({d}) > porta_fim ({d})\n", .{
config.porta_inicio, config.porta_fim,
});
return;
}
var resultados = try executarScan(config, allocator, stdout);
defer resultados.deinit();
// Resumo final das portas abertas
if (resultados.items.len > 0) {
try stdout.print("\n Portas abertas encontradas:\n", .{});
try stdout.print(" {s:<8} {s:<12} {s:<20} {s}\n", .{ "ESTADO", "PORTA", "SERVICO", "TEMPO" });
try stdout.print(" {s:-<50}\n", .{""});
for (resultados.items) |r| {
try stdout.print(" {s:<8} {d:<12}/tcp {s:<20} {d}ms\n", .{
"ABERTA", r.porta, r.servico orelse "desconhecido", r.tempo_ms,
});
}
}
}
Testes
test "servico conhecido - HTTP" {
try std.testing.expectEqualStrings("HTTP", servicoConhecido(80).?);
}
test "servico conhecido - SSH" {
try std.testing.expectEqualStrings("SSH", servicoConhecido(22).?);
}
test "servico desconhecido" {
try std.testing.expect(servicoConhecido(12345) == null);
}
test "resolver IP valido" {
const ip = resolverHost("127.0.0.1");
try std.testing.expect(ip != null);
try std.testing.expectEqual(@as(u8, 127), ip.?[0]);
try std.testing.expectEqual(@as(u8, 0), ip.?[1]);
try std.testing.expectEqual(@as(u8, 0), ip.?[2]);
try std.testing.expectEqual(@as(u8, 1), ip.?[3]);
}
test "resolver IP invalido" {
try std.testing.expect(resolverHost("nao.um.ip.valido") == null);
try std.testing.expect(resolverHost("256.1.1.1") == null);
}
Compilando e Executando
# Compilar e executar scan padrão
zig build run
# Escanear host específico
zig build run -- 192.168.1.1
# Escanear range de portas
zig build run -- 127.0.0.1 80 443
# Com timeout customizado (em ms)
zig build run -- 10.0.0.1 1 65535 100
# Rodar testes
zig build test
Conceitos Aprendidos
- Sockets TCP com conexão não-bloqueante
- Poll para implementar timeout de conexão
- Switch expressions para mapeamento de dados
- Formatação de saída tabulada
- Medição de tempo com
std.time
Próximos Passos
- Explore a documentação de redes da stdlib
- Aprenda sobre threads para scan paralelo
- Veja o projeto Proxy HTTP para redes avançadas
- Consulte o Chat TCP para servidor de conexões