July 10, 2026
Sentinel-RS: construindo um scanner de rede assíncrono em Rust, do zero ao fingerprinting JA3S
Por que construir mais um scanner de rede?

By Luiz Felipe Grochevski
14 min read
Por que construir mais um scanner de rede?
Existem ferramentas de scanning de rede consolidadas há décadas — Nmap é o exemplo óbvio. Então por que escrever uma do zero, em Rust, rodando inclusive no Android via Termux?
A resposta curta: porque construir a ferramenta te obriga a entender profundamente o que ela faz por baixo dos panos. Nmap é uma caixa preta poderosa; o Sentinel-RS é o processo de abrir essa caixa preta e remontar as peças que mais importam para um perfil de Security Engineering — descoberta de hosts, varredura de portas TCP/UDP, fingerprinting de serviços, análise TLS — usando um ecossistema (Tokio, Rust assíncrono) que também é cada vez mais relevante em ferramentas de segurança de produção.
O Sentinel-RS nasceu como parte de um ecossistema maior de projetos de auditoria de rede (incluindo o TrapRS, um honeypot TCP também em Rust, e uma API/dashboard próprios), mas cresceu até virar o motor de scanning central, hoje integrado à Netwatch-API via REST.
Vale um adendo sobre posicionamento: o objetivo desse ecossistema nunca foi construir "mais um Nmap" ou competir em velocidade com ferramentas consolidadas — Nmap e Masscan resolvem isso há décadas, com equipes inteiras por trás. O objetivo declarado desde o início foi diferente: usar cada projeto como evidência prática de um perfil específico, algo entre backend engineering e AppSec — não pentester, mas alguém que entende a pilha de rede fundo o suficiente para construir as ferramentas que um pentester usaria. É por isso que o Sentinel-RS gera Nmap XML como um dos formatos de saída, em vez de ignorar o ecossistema existente: a ideia é interoperar com as ferramentas do mercado, não substituí-las.
Este artigo cobre a arquitetura do projeto, as decisões técnicas mais interessantes, e — a parte que mais vale a pena contar — como um bug sutil de negociação TLS quase passou despercebido até ser caçado até a causa raiz.
Arquitetura em uma imagem
CIDR/IP
↓
Host Discovery (ping paralelo)
↓
Queue de Scanning (mpsc)
↓
Workers Concorrentes (Tokio)
├── TCP Connect Scan
├── SYN Scan (raw sockets)
├── UDP Scan
├── Service Fingerprinting (64+ assinaturas)
└── TLS Fingerprinting (versão, cipher, certificado, JA3S)
↓
Structured Logging (tracing)
↓
Exportação de Relatórios (JSON, CSV, YAML, XML, MD, Nmap XML)CIDR/IP
↓
Host Discovery (ping paralelo)
↓
Queue de Scanning (mpsc)
↓
Workers Concorrentes (Tokio)
├── TCP Connect Scan
├── SYN Scan (raw sockets)
├── UDP Scan
├── Service Fingerprinting (64+ assinaturas)
└── TLS Fingerprinting (versão, cipher, certificado, JA3S)
↓
Structured Logging (tracing)
↓
Exportação de Relatórios (JSON, CSV, YAML, XML, MD, Nmap XML)O fluxo é deliberadamente simples: uma fila de trabalho (mpsc::channel) alimenta um pool fixo de workers assíncronos, cada um puxando alvos (IP + porta) e decidindo, em runtime, qual estratégia de fingerprinting aplicar. Essa separação entre produção de trabalho e consumo por workers é o que permite escanear uma sub-rede inteira com centenas de conexões simultâneas sem travar a aplicação — o Tokio cuida da concorrência, o Semaphore limita a explosão de tarefas.
Rust e Tokio não foram escolhas incidentais. Um scanner de rede é, por natureza, um problema I/O-bound — a maior parte do tempo é gasta esperando resposta de sockets, não processando dados — o que faz do modelo assíncrono do Tokio um encaixe natural para lidar com centenas de conexões simultâneas sem o overhead de threads do sistema operacional. Rust, por sua vez, garante que essa concorrência não venha ao custo de data races ou uso de memória inseguro — algo particularmente relevante numa ferramenta que manipula bytes crus de protocolos de rede, como o handshake TLS feito à mão para o JA3S.
Vale registrar uma decisão que foi revertida no meio do caminho: em determinado momento, o plano era fazer do SYN scan o modo padrão do scanner, não uma opção extra — é mais furtivo e, em teoria, mais rápido, já que não completa o three-way handshake. Mas o SYN scan depende de raw sockets, que exigem root (ou CAP_NET_RAW) — e como o uso real e recorrente do Sentinel-RS é no celular via Termux, sem root, isso teria tornado o modo padrão inutilizável no ambiente onde o projeto mais roda. A decisão final foi inverter a prioridade: TCP connect scan como padrão universal, SYN scan como opção avançada para quando o ambiente permite. Esse tipo de reversão de decisão, motivada por onde a ferramenta realmente é usada e não pelo que parece "tecnicamente superior" no papel, é tão parte do processo de engenharia quanto a escolha original.
As decisões técnicas que mais importam
1. Fingerprinting de serviço vai muito além de "porta 80 = HTTP"
A abordagem ingênua de scanner é mapear porta → serviço por uma tabela fixa. O Sentinel-RS faz duas coisas melhores:
- Banner grabbing ativo: conecta, manda um probe apropriado ao protocolo esperado (
HEAD / HTTP/1.1para web,PING\r\npara Redis, handshake SSH passivo), e lê a resposta real do serviço. - Base de assinaturas estruturada: mais de 64 assinaturas cobrindo desde bancos de dados clássicos (MySQL, PostgreSQL, MongoDB) até categorias que fazem sentido num contexto de segurança ofensiva/defensiva real — IoT/embedded (GoAhead, Boa, RomPager, Hikvision, Dahua), que são exatamente os serviços expostos por dispositivos vulneráveis do tipo explorado por botnets como Mirai.
O resultado de cada porta vem estruturado (produto + versão separados da string de exibição), o que permite ao consumidor da API (a Netwatch-API) montar keywords de CVE precisas — por exemplo, "OpenSSH 6.6.1p1" em vez de só um texto solto.
2. UDP é mais difícil de fazer bem do que TCP
Escanear UDP corretamente exige lidar com ausência de handshake: uma porta "aberta" pode simplesmente não responder nada, e uma porta "fechada" gera um ICMP Port Unreachable que nem sempre chega ao socket do jeito esperado. O Sentinel-RS resolve isso com probes específicos por protocolo — consultas DNS reais, SNMP GetRequest com community public, NetBIOS Name Query, SSDP M-SEARCH (o mesmo usado por dispositivos UPnP para se anunciar na rede), mDNS e TFTP — e um fallback genérico de banner grabbing para portas sem probe dedicado.
// SNMP GetRequest v1, community "public", OID sysDescr
161 => vec![
0x30, 0x26, 0x02, 0x01, 0x00, 0x04, 0x06, b'p', b'u', b'b', b'l', b'i', b'c',
0xa0, 0x19, 0x02, 0x01, 0x01, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00,
0x30, 0x0e, 0x30, 0x0c, 0x06, 0x08,
0x2b, 0x06, 0x01, 0x02, 0x01, 0x01, 0x01, 0x00,
0x05, 0x00,
],// SNMP GetRequest v1, community "public", OID sysDescr
161 => vec![
0x30, 0x26, 0x02, 0x01, 0x00, 0x04, 0x06, b'p', b'u', b'b', b'l', b'i', b'c',
0xa0, 0x19, 0x02, 0x01, 0x01, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00,
0x30, 0x0e, 0x30, 0x0c, 0x06, 0x08,
0x2b, 0x06, 0x01, 0x02, 0x01, 0x01, 0x01, 0x00,
0x05, 0x00,
],Cada resposta é parseada de forma específica: SSDP extrai o header Server: (revelando produto e versão do stack UPnP), DNS/NTP confirmam o serviço pela própria estrutura da resposta.
3. JA3S: fingerprinting de servidor TLS via handshake cru
Essa foi a parte mais interessante de implementar. JA3 é uma técnica de fingerprinting que originalmente identifica clientes TLS a partir do ClientHello que eles enviam — muito usada por defensores para distinguir tráfego de browsers reais de bots e ferramentas automatizadas. A variante JA3S inverte a lógica: em vez de identificar quem se conecta, identifica o comportamento de negociação do servidor — a combinação específica de versão TLS, cipher suite escolhida e ordem das extensões que ele retorna no ServerHello.
Para calcular isso, não dá para usar uma biblioteca TLS de alto nível como o rustls normalmente — ela abstrai justamente os detalhes de baixo nível que o JA3S precisa capturar. A solução foi montar o handshake manualmente:
- Construir um ClientHello genérico byte a byte (versão legada, cipher suites, extensões como SNI,
supported_groups, ALPN,key_share); - Enviar via socket TCP cru;
- Ler o ServerHello de volta e parsear campo a campo (versão, cipher, lista de extensões, preservando a ordem exata em que o servidor as retornou);
- Concatenar esses valores numa string (
versão,cipher,extensões) e calcular o hash MD5 — esse hash é o "JA3S" propriamente dito.
fn montar_hash_ja3s(versao: u16, cipher: u16, extensoes: &[u16]) -> String {
let ext_str = extensoes.iter().map(|e| e.to_string()).collect::<Vec<_>>().join("-");
let ja3s_string = format!("{},{},{}", versao, cipher, ext_str);
format!("{:x}", md5::compute(ja3s_string.as_bytes()))
}fn montar_hash_ja3s(versao: u16, cipher: u16, extensoes: &[u16]) -> String {
let ext_str = extensoes.iter().map(|e| e.to_string()).collect::<Vec<_>>().join("-");
let ja3s_string = format!("{},{},{}", versao, cipher, ext_str);
format!("{:x}", md5::compute(ja3s_string.as_bytes()))
}O valor prático disso: servidores com a mesma stack TLS (mesma versão de biblioteca, mesma configuração) tendem a produzir o mesmo hash JA3S — o que ajuda a agrupar/identificar infraestrutura por trás de IPs diferentes, uma técnica real usada em threat intelligence para rastrear infraestrutura maliciosa que reaparece sob IPs distintos.
Em uma imagem, o fluxo completo fica assim:
ClientHello (montado byte a byte)
│
▼
Socket TCP cru
│
▼
ServerHello (parseado campo a campo)
│
├── versão TLS
├── cipher suite
└── extensões (na ordem retornada)
│
▼
"versão,cipher,extensões"
│
▼
hash MD5
│
▼
JA3SClientHello (montado byte a byte)
│
▼
Socket TCP cru
│
▼
ServerHello (parseado campo a campo)
│
├── versão TLS
├── cipher suite
└── extensões (na ordem retornada)
│
▼
"versão,cipher,extensões"
│
▼
hash MD5
│
▼
JA3SO bug que quase passou despercebido
Depois de implementar o JA3S, testei contra o DNS público do Google (8.8.8.8:443) e o resultado veio assim:
[+] Alvo 8.8.8.8 | Porta 443/TCP ABERTA | Status/Serviço: HTTPS (Falha no Handshake TLS) | TLS/1.3 | cipher=TLS13_AES_256_GCM_SHA384 | JA3S=eb1d94daa7e0344597e756a1fb6e7054[+] Alvo 8.8.8.8 | Porta 443/TCP ABERTA | Status/Serviço: HTTPS (Falha no Handshake TLS) | TLS/1.3 | cipher=TLS13_AES_256_GCM_SHA384 | JA3S=eb1d94daa7e0344597e756a1fb6e7054Repara na contradição: a mesma linha diz "Falha no Handshake TLS" e, logo em seguida, mostra versão TLS, cipher suite e um hash JA3S calculados com sucesso. Dois módulos diferentes do scanner — o fingerprinting de serviço básico (fingerprint.rs) e o fingerprinting TLS avançado (tls.rs, que também calcula o JA3S) — faziam handshakes TLS separados e independentes contra o mesmo servidor, e um falhava enquanto o outro tinha sucesso.
Minha primeira hipótese foi timeout: talvez um módulo tivesse um timeout mais curto que o outro contra um alvo com RTT mais alto. Ajustei os timeouts para serem idênticos — o bug persistiu, exatamente igual. Hipótese errada, próxima.
A pista real veio de adicionar logging explícito no branch de erro (em vez de simplesmente descartar a falha via if let):
DEBUG Handshake TLS (fingerprint.rs) falhou com erro. erro=received fatal alert: HandshakeFailureDEBUG Handshake TLS (fingerprint.rs) falhou com erro. erro=received fatal alert: HandshakeFailureHandshakeFailure é o alerta que um servidor TLS manda quando não consegue negociar parâmetros em comum com o que o cliente ofereceu. Isso apontou para uma diferença estrutural no ClientHello — não em timing, mas em conteúdo. Investigando os dois verificadores de certificado customizados (necessários porque o scanner desabilita deliberadamente a validação de certificado, já que o objetivo é descobrir informação sobre qualquer alvo, válido ou não), encontrei a causa: o método supported_verify_schemes() de cada verificador retornava uma lista diferente de algoritmos de assinatura.
A descoberta que explicou tudo:
supported_verify_schemes()não serve só para validar certificados recebidos — orustlstambém usa essa mesma lista para montar a extensãosignature_algorithmsque o cliente oferece no ClientHello.
Um verificador com uma lista de apenas 3 algoritmos limita o próprio handshake a oferecer só esses 3 ao servidor. O servidor do Google exige algo fora dessa lista curta; o outro módulo, com uma lista de 11 algoritmos, negociava sem problemas contra o mesmo alvo.
A correção foi simples depois de identificada — alinhar as duas listas — mas o processo de chegar lá é o tipo de trabalho que realmente ensina como TLS funciona por baixo dos panos, muito além do que qualquer tutorial de "como usar rustls" cobre.
Outro bug de plataforma: getnameinfo levou duas rodadas para ficar portável
O bug de TLS não foi o único caso em que a plataforma-alvo revelou algo que não aparece em ambiente Linux x86 padrão. A funcionalidade de DNS reverso do Sentinel-RS (--reverse-dns) usa a chamada de sistema getnameinfo via libc, e essa foi, na prática, resolvida em duas rodadas separadas, com três dias de intervalo entre elas — cada correção resolvendo a plataforma que estava quebrada no momento e, sem perceber, quebrando implicitamente a portabilidade para outra.
Na primeira rodada, o código passava host.len() as libc::socklen_t — um cast explícito para u32 — como tamanho do buffer de hostname. Isso compilava bem em ambientes onde a libc espera socklen_t (u32) nesse parâmetro, mas quebrava a build no Termux (Android/ARM) com um erro de tipos incompatíveis (E0308: mismatched types), porque a Bionic (a libc do Android) declara esse parâmetro como usize. A correção, então, removeu o cast, passando o usize diretamente — o que resolveu a compilação para o Android.
Três dias depois, veio a segunda rodada: essa mesma mudança quebrou a compilação em outro ambiente (provavelmente Linux x86, onde a assinatura volta a exigir u32/socklen_t). A correção final não foi escolher um dos dois tipos fixos, mas usar .try_into().unwrap() — uma conversão que o Rust resolve corretamente para qualquer que seja o tipo esperado na plataforma de compilação atual, em vez de fixar usize ou u32 de antemão.
A lição real aqui não é sobre qual tipo está "certo" — é que qualquer cast explícito e fixo (as u32, as usize) é, por definição, uma aposta em uma única plataforma. A solução robusta para código que precisa compilar tanto em Linux x86 quanto em Android/ARM é evitar decidir o tipo de antemão e deixar o Rust fazer essa conversão de forma genérica via TryInto, checando em tempo de compilação (e de execução, com o unwrap) que a conversão é válida onde quer que o código rode.
É um lembrete de que "compila em Linux" e "compila em Android/ARM" são garantias bem diferentes quando o código toca diretamente em bindings de libc — e outro motivo pelo qual manter o projeto rodando em Termux, apesar do trabalho extra, valeu a pena: cada uma dessas diferenças de plataforma ensina algo que não aparece desenvolvendo só no ambiente mais confortável.
O que eu faria diferente se começasse hoje
Nenhum projeto sai perfeito da primeira vez, e olhar para trás com essa clareza é parte do processo. Se eu recomeçasse o Sentinel-RS agora, a mudança que mais me parece valer a pena é ter escrito testes de integração de TLS desde o início — não só os testes unitários que o projeto já tem (parsing de ClientHello/ServerHello, cálculo de hash, etc.), mas testes que efetivamente abrem uma conexão TLS real contra alvos conhecidos e verificam o resultado ponta a ponta.
O motivo é direto: o bug do HandshakeFailure contra 8.8.8.8 existia silenciosamente há algum tempo, e só apareceu porque eu por acaso testei manualmente contra aquele IP específico depois de implementar o JA3S. Um teste de integração que validasse "o módulo de fingerprint básico e o módulo de TLS avançado devem concordar sobre o mesmo alvo" teria pego essa divergência entre os dois ServerCertVerifier muito antes — provavelmente no mesmo commit que introduziu o segundo verificador, em vez de meses depois.
Rodando em Termux: as restrições que moldaram o design
Uma parte relevante do projeto foi garantir que ele rodasse de forma útil no Android via Termux — ambiente sem privilégios de root, sem ICMP irrestrito, sem portas privilegiadas. Isso gerou decisões concretas:
- Flag
--no-ping: o host discovery padrão depende de ICMP, que o Termux frequentemente bloqueia por permissão. Sem essa flag, o scanner simplesmente reportava "0 hosts encontrados" mesmo contra alvos ativos — não porque o scan de portas estivesse quebrado, mas porque a etapa anterior de descoberta nunca confirmava que o host existia. A flag permite pular essa etapa e tratar os IPs do alvo como ativos diretamente. - SYN Scan como funcionalidade opcional: raw sockets exigem
CAP_NET_RAW, indisponível por padrão no Termux — por isso o TCP connect scan continua sendo o caminho padrão, com SYN scan disponível quando o ambiente permite.
Essas restrições, sinceramente, foram formativas: entender exatamente por que uma ferramenta de rede pode falhar num ambiente restrito ensina mais sobre a pilha de rede do que rodar tudo com privilégios totais o tempo inteiro.
Um episódio relacionado, embora não seja sobre o Termux em si: ao tentar rodar a Netwatch-API (a camada que consome o Sentinel-RS) dentro de Docker, o container teve dificuldade em localizar e chamar o binário do Sentinel-RS corretamente por causa de como a rede do container estava configurada. Nesse processo de debugging, surgiu um problema adjacente: o ambiente resolvia certos endereços preferencialmente via IPv6 — e o Sentinel-RS nunca foi desenhado para resolver domínios externos (como google.com); seu escopo sempre foi IPs diretos e blocos CIDR locais. A correção prática foi restringir e normalizar a entrada para IPv4/CIDR na camada que valida o input antes de chegar ao Rust, em vez de tentar fazer o scanner "adivinhar" o que fazer com qualquer string que pareça um domínio ou um endereço IPv6.
Colocando lado a lado com o Nmap
Depois de fechar o roadmap, valeu a pena rodar testes práticos comparando o Sentinel-RS com o Nmap contra os mesmos alvos — tanto em rede local quanto contra hosts reais na internet.
Velocidade em LAN. Uma varredura completa de uma sub-rede /24 (254 hosts, 4 portas cada — 1016 combinações) levou cerca de 3,3 segundos no Sentinel-RS contra quase 38 segundos no Nmap para o mesmo escopo. Os dois concordaram exatamente sobre qual porta estava de fato aberta na rede inteira (a porta 80 do gateway) — a diferença de velocidade não veio ao custo de precisão.
A diferença de filosofia que só apareceu com dado real. O Nmap reportou três hosts com portas explicitamente closed (o host recusou a conexão com RST — ele existe, só não tem nada rodando ali), enquanto o Sentinel-RS simplesmente não imprimiu nada para esses IPs, porque hoje ele só reporta portas confirmadamente abertas. Não dá para saber, olhando a saída do Sentinel-RS, se um host não apareceu porque está desligado ou porque está ligado com todas as portas fechadas — o Nmap faz essa distinção, o Sentinel-RS ainda não. É uma limitação real de expressividade, registrada aqui como possível item de roadmap futuro.
WAN e o ajuste de timeout. Contra 8.8.8.8:443 e 1.1.1.1:443 com o timeout padrão original (100ms), o Sentinel-RS simplesmente não retornava nada — nem erro, nem resultado, porque o handshake completo (TCP + TLS + JA3S) raramente termina em menos de 100ms contra um alvo fora da rede local. Ajustando o timeout padrão para 300ms, os dois alvos passaram a responder de forma consistente e completa (certificado, cipher, JA3S) sem precisar de nenhum ajuste manual — validado depois em duas redes diferentes (doméstica e corporativa), com resultado idêntico. É um ajuste pequeno, mas que só um teste real contra a internet — não contra localhost ou LAN — conseguiria revelar.
Outro bug real: quando "sem resposta" virou "aberta" por engano
O teste comparativo também expôs um problema mais sério no módulo UDP. Rodando o Sentinel-RS contra a mesma sub-rede /24 nas portas 1900 (SSDP) e 5353 (mDNS), o resultado foi 505 portas "abertas" de 508 possíveis — praticamente cada combinação de host e porta testada. O Nmap, no mesmo teste, só confirmou 4 hosts com alguma resposta real.
A causa: como UDP não tem handshake, um timeout sem resposta é ambíguo — pode significar porta aberta (sem serviço querendo responder ao probe específico) ou porta filtrada por firewall. O código tratava qualquer status diferente de "Fechada"/"Falha"/"Erro" como confirmação de porta aberta, incluindo o caso "Aberta | Filtrada (Sem resposta ao Probe)" — o próprio texto já admitia a incerteza, mas a linha impressa dizia ABERTA mesmo assim. O Nmap, para comparação, trata esse mesmo cenário como o estado open|filtered, sem contar como confirmação.
A correção foi simples uma vez identificada: só marcar uma porta UDP como aberta quando o probe obtém uma resposta identificável (um serviço reconhecido de verdade, como o SSDP do gateway ou um responder mDNS) — timeout sem resposta vira só um log de debug, não uma linha de resultado. Validado depois em duas redes: a de casa confirmou 2 portas reais (SSDP no gateway, mDNS num host) de ~508 testadas; a rede do trabalho confirmou 0 de ~2044 testadas — um resultado plausível para uma rede corporativa com menos dispositivos IoT/UPnP expostos, e sem o ruído de milhares de falsos positivos que o código anterior teria produzido.
Um limite honesto que os dois compartilham. Durante o mesmo teste, o gateway revelou uma porta 179 aberta — BGP, o protocolo de roteamento entre redes. Nenhuma das duas ferramentas conseguiu identificar o serviço: o Nmap reportou tcpwrapped (recebeu o handshake TCP, mas nenhum banner útil), e o Sentinel-RS reportou Desconhecido. Não é uma limitação de uma ferramenta em relação à outra — é uma característica do próprio protocolo. Diferente de SSH ou FTP, que enviam um banner assim que a conexão TCP fecha, o BGP fica em silêncio esperando o cliente mandar uma mensagem OPEN primeiro. Fingerprinting por banner passivo simplesmente não tem o que ler nesse cenário, em nenhuma das duas ferramentas — e reconhecer esse limite é parte de usar a técnica corretamente, não um bug a ser corrigido.
Fechando o ciclo: do scan à vulnerabilidade conhecida
Uma parte que só fez sentido testar depois que o roadmap principal fechou foi o pipeline completo do ecossistema: o Sentinel-RS identifica um serviço com produto e versão, e o CVE Lookup — outro projeto do mesmo conjunto, que consulta a National Vulnerability Database via API — verifica se existem vulnerabilidades conhecidas associadas.
O teste usou o banner real capturado pelo Sentinel-RS num gateway doméstico (SSDP/UPnP POSIX UPnP/1.0 UPnP Stack/2016.6.10.0) como termo de busca. O resultado trouxe CVEs reais e recentes envolvendo justamente esse tipo de componente — vulnerabilidades de severidade alta em stacks UPnP de fabricantes como D-Link e Wavlink. Não é uma correspondência perfeita de CPE contra aquele produto exato (a busca por palavra-chave da NVD é textual e ampla, como o próprio projeto documenta), mas prova o conceito de ponta a ponta: descoberta de serviço → identificação de produto → cruzamento com vulnerabilidades reais, sem qualquer dado sintético no meio do caminho.
Sentinel-RS em números
- 64+ assinaturas de serviço na base de fingerprinting
- 6 formatos de exportação (JSON, CSV, YAML, XML, Markdown, Nmap XML)
- 3 modos de scan: TCP Connect, SYN (raw sockets), UDP com probes dedicados
- TLS fingerprinting completo: versão, cipher suite, certificado e JA3S
- 61 testes automatizados cobrindo desde parsing de CLI até handshake TLS manual
- Compila e roda tanto em Linux x86 quanto em Android/ARM (Termux)
O que ficou de fora (por enquanto)
O roadmap atual do projeto está com os três itens planejados fechados — expansão da base de assinaturas, UDP fingerprinting estruturado, e JA3S. Próximos passos naturais incluem JA4 (a evolução mais recente do JA3, com hash mais robusto) e uma base de assinaturas ainda maior orientada a dados reais de scan.
Mais do que construir um scanner, o Sentinel-RS acabou sendo um laboratório para entender a pilha de rede em profundidade. Os problemas mais interessantes não estavam em algoritmos complexos, mas em detalhes aparentemente pequenos — uma lista diferente de algoritmos de assinatura no TLS, ou um tipo diferente esperado pela libc em plataformas distintas. São justamente esses detalhes, fáceis de ignorar até quebrarem alguma coisa, que transformam uma ferramenta funcional em um projeto que ensina engenharia de verdade.
O código completo está no GitHub: github.com/LuizGrochevski/Sentinel-RS
O Sentinel-RS não vive sozinho — ele é o motor de scanning de um pipeline maior, que inclui a Netwatch-API (camada de persistência e autenticação em FastAPI) e o CVE Lookup, mostrado brevemente aqui. Essa parte do ecossistema tem histórias próprias que valem um artigo à parte.
Se você trabalha com AppSec, segurança ofensiva ou só curte Rust, fico à disposição para trocar ideia — LinkedIn.