Scanners de portas são ferramentas essenciais para administradores de rede e profissionais de segurança. Eles permitem descobrir quais serviços estão rodando em uma máquina ao verificar quais portas TCP estão abertas e aceitando conexões. Neste projeto, vamos construir um scanner de portas TCP em Rust que usa threads para varredura concorrente, tornando a operação muito mais rápida do que verificar cada porta sequencialmente.
Este projeto é uma excelente oportunidade para aprender sobre programação concorrente em Rust com std::thread, comunicação entre threads com channels, operações de rede com std::net e o modelo de segurança que Rust oferece para programação paralela.
O Que Vamos Construir
Nosso portscan terá os seguintes recursos:
- Varredura de range de portas configurável
- Varredura concorrente com número de threads ajustável
- Timeout de conexão configurável
- Detecção de serviços conhecidos por porta
- Exibição progressiva dos resultados
- Resumo com estatísticas da varredura
Estrutura do Projeto
portscan/
├── Cargo.toml
└── src/
├── main.rs
├── cli.rs
├── scanner.rs
└── servicos.rs
Configurando o Projeto
cargo new portscan
cd portscan
Configure o Cargo.toml:
[package]
name = "portscan"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4", features = ["derive"] }
colored = "2"
Note que não precisamos de crates externas para rede — o std::net da biblioteca padrão fornece tudo que precisamos para conexões TCP.
Passo 1: Interface de Linha de Comando
// src/cli.rs
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "portscan")]
#[command(about = "Scanner de portas TCP com varredura concorrente")]
pub struct Cli {
/// Endereço do host a escanear (IP ou hostname)
pub host: String,
/// Porta inicial do range
#[arg(short = 'i', long, default_value_t = 1)]
pub porta_inicio: u16,
/// Porta final do range
#[arg(short = 'f', long, default_value_t = 1024)]
pub porta_fim: u16,
/// Número de threads para varredura concorrente
#[arg(short = 't', long, default_value_t = 100)]
pub threads: usize,
/// Timeout de conexão em milissegundos
#[arg(short = 'o', long, default_value_t = 500)]
pub timeout_ms: u64,
/// Exibir apenas portas abertas (omitir fechadas)
#[arg(short = 'a', long, default_value_t = true)]
pub apenas_abertas: bool,
/// Exibir progresso da varredura
#[arg(short, long)]
pub progresso: bool,
}
Passo 2: Mapa de Serviços Conhecidos
// src/servicos.rs
use std::collections::HashMap;
/// Retorna um mapa de portas para nomes de serviços conhecidos
pub fn mapa_servicos() -> HashMap<u16, &'static str> {
let mut mapa = HashMap::new();
mapa.insert(20, "FTP (dados)");
mapa.insert(21, "FTP (controle)");
mapa.insert(22, "SSH");
mapa.insert(23, "Telnet");
mapa.insert(25, "SMTP");
mapa.insert(53, "DNS");
mapa.insert(80, "HTTP");
mapa.insert(110, "POP3");
mapa.insert(143, "IMAP");
mapa.insert(443, "HTTPS");
mapa.insert(465, "SMTPS");
mapa.insert(587, "SMTP (submission)");
mapa.insert(993, "IMAPS");
mapa.insert(995, "POP3S");
mapa.insert(1433, "MS SQL Server");
mapa.insert(1521, "Oracle DB");
mapa.insert(3306, "MySQL");
mapa.insert(3389, "RDP");
mapa.insert(5432, "PostgreSQL");
mapa.insert(5672, "RabbitMQ");
mapa.insert(6379, "Redis");
mapa.insert(8080, "HTTP alternativo");
mapa.insert(8443, "HTTPS alternativo");
mapa.insert(9200, "Elasticsearch");
mapa.insert(27017, "MongoDB");
mapa
}
Passo 3: Motor de Varredura Concorrente
// src/scanner.rs
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
/// Resultado da varredura de uma porta
#[derive(Debug, Clone)]
pub struct ResultadoPorta {
pub porta: u16,
pub aberta: bool,
}
/// Resultado completo da varredura
pub struct ResultadoVarredura {
pub host: String,
pub portas_abertas: Vec<u16>,
pub portas_fechadas: usize,
pub tempo_total: Duration,
pub total_escaneadas: usize,
}
/// Resolve o hostname para um endereço IP
pub fn resolver_host(host: &str) -> Result<String, String> {
let endereco = format!("{}:0", host);
match endereco.to_socket_addrs() {
Ok(mut addrs) => {
if let Some(addr) = addrs.next() {
Ok(addr.ip().to_string())
} else {
Err(format!("Não foi possível resolver '{}'", host))
}
}
Err(e) => Err(format!("Erro ao resolver '{}': {}", host, e)),
}
}
/// Verifica se uma porta está aberta tentando uma conexão TCP
fn verificar_porta(endereco: &SocketAddr, timeout: Duration) -> bool {
TcpStream::connect_timeout(endereco, timeout).is_ok()
}
/// Executa a varredura de portas usando múltiplas threads
pub fn escanear(
host: &str,
ip: &str,
porta_inicio: u16,
porta_fim: u16,
num_threads: usize,
timeout_ms: u64,
mostrar_progresso: bool,
) -> ResultadoVarredura {
let inicio = Instant::now();
let timeout = Duration::from_millis(timeout_ms);
let portas: Vec<u16> = (porta_inicio..=porta_fim).collect();
let total = portas.len();
// Canal para receber resultados das threads
let (tx, rx) = mpsc::channel::<ResultadoPorta>();
// Dividir as portas em chunks para as threads
let chunk_size = (total + num_threads - 1) / num_threads;
let chunks: Vec<Vec<u16>> = portas
.chunks(chunk_size)
.map(|c| c.to_vec())
.collect();
let mut handles = Vec::new();
for chunk in chunks {
let tx = tx.clone();
let ip = ip.to_string();
let handle = thread::spawn(move || {
for porta in chunk {
let endereco: SocketAddr = format!("{}:{}", ip, porta)
.parse()
.expect("Endereço inválido");
let aberta = verificar_porta(&endereco, timeout);
let _ = tx.send(ResultadoPorta { porta, aberta });
}
});
handles.push(handle);
}
// Fechar o transmissor original para que o receptor saiba quando parar
drop(tx);
// Coletar resultados
let mut portas_abertas = Vec::new();
let mut portas_fechadas: usize = 0;
let mut processadas: usize = 0;
for resultado in rx {
processadas += 1;
if resultado.aberta {
portas_abertas.push(resultado.porta);
} else {
portas_fechadas += 1;
}
if mostrar_progresso && processadas % 50 == 0 {
let percentual = (processadas as f64 / total as f64) * 100.0;
eprint!(
"\rProgresso: {:.1}% ({}/{} portas)",
percentual, processadas, total
);
}
}
if mostrar_progresso {
eprintln!("\rProgresso: 100.0% ({}/{} portas) ", total, total);
}
// Aguardar todas as threads terminarem
for handle in handles {
let _ = handle.join();
}
portas_abertas.sort();
ResultadoVarredura {
host: host.to_string(),
portas_abertas,
portas_fechadas,
tempo_total: inicio.elapsed(),
total_escaneadas: total,
}
}
O scanner divide o range de portas em chunks iguais e atribui cada chunk a uma thread. Cada thread tenta conectar em suas portas e envia o resultado pelo canal (mpsc::channel). A thread principal coleta os resultados conforme chegam. Essa abordagem é simples e eficaz — com 100 threads, uma varredura de 1024 portas leva apenas alguns segundos.
Passo 4: Integrando no main.rs
// src/main.rs
mod cli;
mod scanner;
mod servicos;
use clap::Parser;
use cli::Cli;
use colored::*;
use std::collections::HashMap;
fn main() {
let cli = Cli::parse();
// Validar range de portas
if cli.porta_inicio > cli.porta_fim {
eprintln!(
"{} A porta inicial ({}) deve ser menor ou igual à final ({}).",
"ERRO:".red().bold(),
cli.porta_inicio,
cli.porta_fim
);
std::process::exit(1);
}
// Resolver hostname
let ip = match scanner::resolver_host(&cli.host) {
Ok(ip) => ip,
Err(e) => {
eprintln!("{} {}", "ERRO:".red().bold(), e);
std::process::exit(1);
}
};
let total_portas = (cli.porta_fim - cli.porta_inicio + 1) as usize;
println!("{}", "Scanner de Portas TCP".bold().cyan());
println!(" Host: {} ({})", cli.host, ip);
println!(" Range: {}-{} ({} portas)", cli.porta_inicio, cli.porta_fim, total_portas);
println!(" Threads: {}", cli.threads);
println!(" Timeout: {} ms", cli.timeout_ms);
println!();
// Executar a varredura
let resultado = scanner::escanear(
&cli.host,
&ip,
cli.porta_inicio,
cli.porta_fim,
cli.threads,
cli.timeout_ms,
cli.progresso,
);
// Carregar mapa de serviços
let servicos: HashMap<u16, &str> = servicos::mapa_servicos();
// Exibir resultados
if resultado.portas_abertas.is_empty() {
println!(
"{} Nenhuma porta aberta encontrada no range {}-{}.",
"INFO:".yellow().bold(),
cli.porta_inicio,
cli.porta_fim
);
} else {
println!(
"{}\n",
"Portas abertas encontradas:".bold().green()
);
println!(
" {:<8} {:<8} {}",
"PORTA".bold(),
"ESTADO".bold(),
"SERVIÇO".bold()
);
println!(" {}", "-".repeat(45));
for porta in &resultado.portas_abertas {
let servico = servicos
.get(porta)
.unwrap_or(&"Desconhecido");
println!(
" {:<8} {:<8} {}",
porta.to_string().cyan(),
"aberta".green(),
servico
);
}
}
// Resumo
println!("\n{}", "Resumo da Varredura".bold().cyan());
println!(
" Portas abertas: {}",
resultado.portas_abertas.len().to_string().green().bold()
);
println!(
" Portas fechadas: {}",
resultado.portas_fechadas
);
println!(
" Total escaneadas: {}",
resultado.total_escaneadas
);
println!(
" Tempo total: {:.2} segundos",
resultado.tempo_total.as_secs_f64()
);
println!(
" Velocidade: {:.0} portas/segundo",
resultado.total_escaneadas as f64 / resultado.tempo_total.as_secs_f64()
);
}
Como Executar
cargo build --release
Exemplos de uso:
# Escanear portas comuns (1-1024) de um host
./target/release/portscan localhost
# Saída exemplo:
# Scanner de Portas TCP
# Host: localhost (127.0.0.1)
# Range: 1-1024 (1024 portas)
# Threads: 100
# Timeout: 500 ms
#
# Portas abertas encontradas:
#
# PORTA ESTADO SERVIÇO
# ---------------------------------------------
# 22 aberta SSH
# 80 aberta HTTP
# 443 aberta HTTPS
# 5432 aberta PostgreSQL
#
# Resumo da Varredura
# Portas abertas: 4
# Portas fechadas: 1020
# Total escaneadas: 1024
# Tempo total: 2.34 segundos
# Velocidade: 437 portas/segundo
# Escanear range específico com mais threads
./target/release/portscan 192.168.1.1 -i 80 -f 9000 -t 200
# Escanear com timeout menor (mais rápido, mas pode perder portas lentas)
./target/release/portscan meuservidor.com -o 200
# Varredura completa (0-65535) com progresso
./target/release/portscan localhost -i 1 -f 65535 -t 500 --progresso
# Escanear portas de banco de dados
./target/release/portscan dbserver -i 3306 -f 5432 -t 50
Desafios para Expandir
Banner grabbing: Após detectar uma porta aberta, envie e receba dados para identificar o serviço exato e sua versão (ex: “OpenSSH 9.0” ou “Apache 2.4”).
Varredura UDP: Além de TCP, implemente varredura de portas UDP enviando datagramas e analisando respostas ICMP “port unreachable” para detectar portas fechadas.
Exportação de resultados: Adicione flags para exportar em JSON, CSV ou formato XML compatível com o Nmap, facilitando a integração com outras ferramentas de segurança.
Detecção de firewall: Analise os tempos de resposta e padrões de erro para diferenciar entre portas realmente fechadas e portas filtradas por firewall.
Varredura assíncrona com tokio: Substitua threads por tarefas assíncronas usando
tokiopara melhorar a escalabilidade e reduzir o consumo de memória em varreduras de grande escala.
Veja Também
- Módulo net — programação de rede em Rust
- Threads em Rust — criando e gerenciando threads
- Channels — comunicação entre threads com canais
- Executando Threads — receita prática de concorrência
- Rust para DevOps — ferramentas de infraestrutura em Rust