Scanner de Portas de Rede

Construa um scanner de portas TCP em Rust com varredura concorrente usando threads, timeout configurável e detecção de serviços.

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

  1. 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”).

  2. Varredura UDP: Além de TCP, implemente varredura de portas UDP enviando datagramas e analisando respostas ICMP “port unreachable” para detectar portas fechadas.

  3. 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.

  4. 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.

  5. Varredura assíncrona com tokio: Substitua threads por tarefas assíncronas usando tokio para melhorar a escalabilidade e reduzir o consumo de memória em varreduras de grande escala.

Veja Também