July 5, 2026
Construindo um Honeypot em Rust: do zero ao alerta no Telegram
Construindo um honeypot TCP assíncrono em Rust: do Termux aos alertas no Telegram

By Luiz Felipe Grochevski
6 min read
📌 Este artigo faz parte da série "Laboratório de Segurança em Rust", onde documento o desenvolvimento de ferramentas para estudo de redes e segurança ofensiva/defensiva.
O contexto: um laboratório pessoal de segurança
O TrapRS nasceu como a peça defensiva de um laboratório pessoal de segurança em Rust. Enquanto o Sentinel-RS observa serviços expostos na rede e a Netwatch-API centraliza análises e cruza resultados com vulnerabilidades conhecidas (CVEs), o TrapRS inverte a perspectiva: em vez de olhar para servidores, ele observa quem tenta acessá-los.
Os projetos contam uma história em sequência:
- Sentinel-RS → aprendi a descobrir e identificar serviços na rede
- CVE Lookup → aprendi a consultar bases de vulnerabilidades
- Netwatch-API → aprendi a integrar informações e criar uma API
- Netwatch Dashboard → aprendi a visualizar e acompanhar os dados
- TrapRS → aprendi a observar o comportamento de quem tenta invadir
Todos os projetos deste laboratório foram desenvolvidos e validados em ambiente controlado, utilizando o Termux no Android como ambiente principal de desenvolvimento. O TrapRS ainda não foi exposto à Internet pública; por isso, as implementações e exemplos apresentados neste artigo refletem testes funcionais realizados em laboratório, abrangendo os protocolos, o sistema de alertas e o dashboard. Quando isso acontecer, pretendo escrever uma segunda parte com telemetria real.
Por que um honeypot?
Quando você estuda segurança ofensiva, é fácil focar só em ferramentas de ataque. Mas uma coisa que muda sua perspectiva é olhar do outro lado: o que acontece quando você deixa uma porta aberta e espera?
Um honeypot é um sistema propositalmente exposto que imita um serviço real com o objetivo de atrair e registrar tentativas de acesso. Ele não tem função produtiva — qualquer conexão que receber é, por definição, suspeita.
O valor está nos dados coletados:
- Quais IPs estão varrendo a rede?
- Quais credenciais são tentadas no SSH?
- Quais paths HTTP os scanners automatizados buscam primeiro?
Por que Rust?
Três razões:
Performance: Rust compila para binários nativos sem runtime overhead. Um honeypot precisa aguentar dezenas de conexões simultâneas sem travar — exatamente o que o Tokio foi feito para resolver.
Segurança de memória: O compilador do Rust elimina classes inteiras de bugs (use-after-free, buffer overflow) em tempo de compilação. Faz sentido usar uma linguagem segura para construir uma ferramenta de segurança.
Termux: Eu desenvolvo no celular. O Rust compila perfeitamente para ARM no Android, e o binário final roda sem depender de nenhuma biblioteca externa do sistema.
Arquitetura
O TrapRS roda três listeners TCP em paralelo via Tokio, todos alimentando um canal assíncrono (mpsc) para um logger centralizado:
Atacante/Scanner
│
├── TCP :2222 → Honeypot SSH
├── TCP :8080 → Honeypot HTTP
└── TCP :8443 → Honeypot HTTPS (TLS autoassinado)
│
▼
Logger assíncrono (mpsc channel)
│
├── logs/events.jsonl (log estruturado)
├── logs/stats.json (estatísticas persistidas)
├── ThresholdAlert
│ ├── Webhook → Netwatch-API
│ └── Telegram Bot API
└── broadcast (WebSocket)
│
▼
Dashboard web em tempo realAtacante/Scanner
│
├── TCP :2222 → Honeypot SSH
├── TCP :8080 → Honeypot HTTP
└── TCP :8443 → Honeypot HTTPS (TLS autoassinado)
│
▼
Logger assíncrono (mpsc channel)
│
├── logs/events.jsonl (log estruturado)
├── logs/stats.json (estatísticas persistidas)
├── ThresholdAlert
│ ├── Webhook → Netwatch-API
│ └── Telegram Bot API
└── broadcast (WebSocket)
│
▼
Dashboard web em tempo realCada conexão gera um evento serializado em JSON e enviado pelo canal. O logger recebe todos os eventos, grava no arquivo, faz broadcast para o dashboard e verifica o threshold de alertas.
O Honeypot SSH
O maior desafio foi encontrar o equilíbrio certo: simular o suficiente para parecer legítimo a scanners, sem implementar o protocolo SSH completo.
O protocolo SSH começa com uma troca de banners em texto claro — o servidor anuncia sua versão, o cliente responde com a dele. Isso já é valioso:
// Envia banner SSH falso
let banner_line = format!("{}\r\n", banner);
writer.write_all(banner_line.as_bytes()).await?;
// Lê banner do cliente
let mut client_banner = String::new();
buf_reader.read_line(&mut client_banner).await?;// Envia banner SSH falso
let banner_line = format!("{}\r\n", banner);
writer.write_all(banner_line.as_bytes()).await?;
// Lê banner do cliente
let mut client_banner = String::new();
buf_reader.read_line(&mut client_banner).await?;Depois do banner, o cliente envia o pacote de negociação de algoritmos criptográficos. Scanners mais simples tentam enviar credenciais cedo nesse fluxo, antes do handshake completo. O TrapRS captura o que consegue desse payload bruto.
Limitação honesta: o TrapRS não implementa o protocolo SSH real. Clientes SSH modernos (OpenSSH 8+) encriptam as credenciais antes de enviá-las — o que o honeypot captura nesses casos é o payload do handshake, não usuário e senha em texto claro. Para capturar credenciais de clientes sofisticados seria necessário implementar o protocolo completo, o que está fora do escopo atual.
O que você vê no log de um cliente simples (exemplo ilustrativo):
{
"protocol": "SSH",
"src_ip": "127.0.0.1",
"event": {
"kind": "auth_attempt",
"username": "root",
"password": "123456",
"method": "password"
},
"client_banner": "SSH-2.0-OpenSSH_7.4"
}{
"protocol": "SSH",
"src_ip": "127.0.0.1",
"event": {
"kind": "auth_attempt",
"username": "root",
"password": "123456",
"method": "password"
},
"client_banner": "SSH-2.0-OpenSSH_7.4"
}O Honeypot HTTP
HTTP é texto puro — mais simples de implementar e mais rico em informação. O TrapRS lê a request line, os headers e o body (limitado a 4KB para evitar exaustão de memória), e responde com uma página Apache falsa:
let response = format!(
"HTTP/1.1 200 OK\r\n\
Server: Apache/2.4.7 (Ubuntu)\r\n\
Content-Type: text/html\r\n\
Content-Length: {}\r\n\r\n{}",
body_html.len(), body_html
);let response = format!(
"HTTP/1.1 200 OK\r\n\
Server: Apache/2.4.7 (Ubuntu)\r\n\
Content-Type: text/html\r\n\
Content-Length: {}\r\n\r\n{}",
body_html.len(), body_html
);Para validar o comportamento do servidor, realizei requisições para caminhos comumente procurados por scanners automatizados, como:
/admin → tentativa de acesso a painel
/wp-login.php → scanner WordPress
/.env → busca por variáveis de ambiente expostas
/phpMyAdmin → scanner de banco de dados
/config.php → busca por arquivos de configuração/admin → tentativa de acesso a painel
/wp-login.php → scanner WordPress
/.env → busca por variáveis de ambiente expostas
/phpMyAdmin → scanner de banco de dados
/config.php → busca por arquivos de configuraçãoEsse padrão é consistente com o que a comunidade de segurança documenta sobre comportamento de bots automatizados — mas vale deixar claro: esses dados vieram de testes controlados em laboratório, não de exposição real à Internet.
O Honeypot HTTPS: TLS autoassinado em runtime
Para fingir ser um servidor HTTPS, o TrapRS precisa de um certificado TLS. Distribuir um certificado estático no repositório seria um problema de segurança — e inflexível.
A solução foi usar a crate rcgen para gerar um certificado autoassinado em runtime:
let cert = generate_simple_self_signed(vec!["localhost".to_string()])?;
let cert_der = CertificateDer::from(cert.cert.der().to_vec());
let key_der = PrivateKeyDer::Pkcs8(cert.key_pair.serialize_der().into());let cert = generate_simple_self_signed(vec!["localhost".to_string()])?;
let cert_der = CertificateDer::from(cert.cert.der().to_vec());
let key_der = PrivateKeyDer::Pkcs8(cert.key_pair.serialize_der().into());O desafio real: aqui encontrei um bug que me custou algumas horas. O reqwest (que uso para enviar webhooks) e o tokio-rustls (que uso para o TLS) puxavam versões diferentes do provider de criptografia do rustls — um usava ring, outro usava aws-lc-rs. O resultado era um panic em runtime:
Could not automatically determine the process-level CryptoProvider.
Call CryptoProvider::install_default() before this point.Could not automatically determine the process-level CryptoProvider.
Call CryptoProvider::install_default() before this point.A solução foi forçar explicitamente o provider ring na inicialização do processo e garantir que ambas as crates usassem o mesmo backend. Esse tipo de conflito de dependência transitiva é comum em projetos Rust que misturam crates de criptografia — algo que só aprendi errando.
Alertas por threshold
Como saber quando algo merece atenção imediata? A resposta é threshold: se um IP dispara N eventos em X segundos, é provável que seja um scanner agressivo ou brute-force.
O TrapRS implementa isso com uma janela deslizante:
pub fn record(&mut self, ip: &str) -> bool {
let now = Instant::now();
let entry = self.counters.entry(ip.to_string()).or_default();
// Remove eventos fora da janela de tempo
entry.retain(|t| now.duration_since(*t) < Duration::from_secs(self.window_secs));
entry.push(now);
entry.len() >= self.threshold
}pub fn record(&mut self, ip: &str) -> bool {
let now = Instant::now();
let entry = self.counters.entry(ip.to_string()).or_default();
// Remove eventos fora da janela de tempo
entry.retain(|t| now.duration_since(*t) < Duration::from_secs(self.window_secs));
entry.push(now);
entry.len() >= self.threshold
}Quando o threshold é atingido, três coisas acontecem em paralelo: alerta no terminal, POST para o webhook da Netwatch-API, e mensagem no Telegram.
Alertas no Telegram
A parte mais prática do projeto. Quando um IP dispara o threshold, uma mensagem chega no celular:
🪤 TrapRS ALERTA
🔴 IP: 1.2.3.4
📊 Eventos: 10 em 60s
🌐 Protocolo: HTTP🪤 TrapRS ALERTA
🔴 IP: 1.2.3.4
📊 Eventos: 10 em 60s
🌐 Protocolo: HTTPA API do Telegram é REST simples:
client.post(&url)
.json(&serde_json::json!({
"chat_id": chat_id,
"text": text,
"parse_mode": "Markdown"
}))
.send().await?;client.post(&url)
.json(&serde_json::json!({
"chat_id": chat_id,
"text": text,
"parse_mode": "Markdown"
}))
.send().await?;Para configurar: crie um bot no @BotFather, pegue o token e o chat ID via /getUpdates, e passe como argumentos:
./traprs \
--telegram-token SEU_TOKEN \
--telegram-chat-id SEU_CHAT_ID \
--alert-threshold 10 \
--alert-window 60./traprs \
--telegram-token SEU_TOKEN \
--telegram-chat-id SEU_CHAT_ID \
--alert-threshold 10 \
--alert-window 60O suporte a múltiplos webhooks também foi implementado — você pode notificar quantos endpoints quiser passando --webhook-url várias vezes. Cada alerta é enviado em tarefas Tokio separadas, em paralelo.
Dashboard em tempo real
O TrapRS tem uma interface web que mostra os eventos ao vivo via WebSocket — sem framework JavaScript, só HTML/CSS/JS puro.
O servidor WebSocket usa um canal broadcast do Tokio: cada evento que passa pelo logger é reenviado para todos os clientes conectados:
// No logger, após processar cada evento:
let _ = broadcast_tx.send(json);// No logger, após processar cada evento:
let _ = broadcast_tx.send(json);O cliente JavaScript recebe o JSON e atualiza o feed instantaneamente, mantendo contadores por protocolo, top IPs e top paths.
O que aprendi
Sobre honeypots: a maior parte do tráfego malicioso automatizado segue padrões previsíveis. Durante o desenvolvimento, ficou claro como um honeypot pode registrar padrões típicos utilizados por scanners automatizados. Mesmo em ambiente controlado, reproduzir esses comportamentos foi suficiente para validar a arquitetura de captura e reforçar como regras simples conseguem identificar boa parte dessas tentativas.
Sobre Rust assíncrono: o modelo de concorrência do Tokio com canais mpsc e broadcast é elegante para esse tipo de problema — múltiplos produtores (os listeners), um consumidor (o logger), e um broadcast para os clientes WebSocket. O compilador garante que não há condições de corrida.
Sobre desenvolvimento mobile: construir no celular força simplicidade. Sem IDE, sem mouse. Só terminal e cargo build. Algumas das melhores decisões de arquitetura vieram de restrições do ambiente.
Conclusão
Segurança não é apenas bloquear ataques. É compreender sistemas, protocolos e o comportamento de quem interage com eles. O TrapRS nasceu exatamente dessa curiosidade: transformar conexões em observações e observações em aprendizado.
O Sentinel-RS mostrou como descobrir serviços. A Netwatch-API ajudou a organizá-los. O TrapRS mudou a perspectiva: em vez de observar sistemas, passei a observar quem tenta interagir com eles.
Mais do que um projeto isolado, ele é a peça que faltava num ecossistema construído progressivamente — cada projeto respondendo a uma pergunta que o anterior deixou em aberto.
Ainda há muito espaço para evoluir o TrapRS, mas esse projeto cumpriu seu principal objetivo: aprofundar meu entendimento sobre protocolos de rede, programação assíncrona em Rust e os fundamentos por trás de um honeypot.
Código
O TrapRS é open source: github.com/LuizGrochevski/traprs
O ecossistema completo:
- Sentinel-RS — scanner de rede assíncrono em Rust
- Netwatch-API — REST API que orquestra scans e consulta CVEs
- CVE Lookup — CLI para consulta de vulnerabilidades na NVD
- Netwatch Dashboard — painel web para visualização de scans
Luiz Felipe Grochevski — Desenvolvedor Backend em transição para Security Engineering LinkedIn · GitHub