Sentinel (Firewall de Bots na Edge)
Firewall de bots que escrevi em Cloudflare Workers: cada requisição recebe uma pontuação de 0 a 100 com sinais do plano gratuito, e o rate limit por IP encolhe conforme a suspeita aumenta.
O problema
Ataques de bots e scrapers consumiam banda e requisições em excesso, inflando o custo da infraestrutura sem uma ferramenta de mitigação viável no free tier.
A abordagem
Desenvolvi o Sentinel, um firewall de bots executado na edge com Cloudflare Workers e Durable Objects. Cada requisição recebe uma pontuação por assinaturas de TLS, versão de HTTP, headers do navegador e rede de origem, passa por um rate limit adaptativo por cliente e sai com um veredito na própria edge: libera, desafia ou bloqueia. O registro acontece em ctx.waitUntil, fora do caminho da resposta.
Arquitetura
- 01Cliente HTTP → Worker (index.js): requisição
- 02Worker (index.js) → score.js: headers e request.cf
- 03Worker (index.js) → RateLimiter DO: score e parâmetros de config
- 04score.js → Veredito: score vs limiares
- 05RateLimiter DO → Veredito: limited, count, limit
- 06Worker (index.js) → Stats DO: ctx.waitUntil, fora do caminho da resposta
- 07Worker (index.js) → logger.js: logDecision
Decisões
- 01
Só sinais do plano gratuito, sem Bot Management pago
Por quê Não usei request.cf.botManagement. Tudo vem de headers, metadados de TLS/HTTP e da rede de origem (ASN e threatScore). A ideia era que as heurísticas fossem o produto, não um recurso pago da Cloudflare.
- 02
Rate limit que se adapta à pontuação
Por quê O orçamento por IP é baseLimit x (1 - min(0,85, score/100)), com piso de 3 requisições. Assim o abuso fica caro sem punir usuários reais: um navegador limpo recebe a janela inteira e o cliente mais suspeito cai para 15% dela.
- 03
Rate limiter que falha aberto
Por quê Uma falha no Durable Object ou no storage não pode negar acesso a um usuário real. Por isso o erro é registrado com IP truncado e a requisição segue como não limitada.
Custo Enquanto a falha durar, nenhum limite é aplicado. A resposta interna sai marcada com degraded: true.
- 04
Estatísticas fora do caminho quente
Por quê O registro no Stats DO roda em ctx.waitUntil, com catch próprio, para nunca adicionar latência nem quebrar o veredito devolvido ao cliente. Pontuação e veredito são o fluxo principal; stats e logs ficam secundários e isolados.
- 05
IP truncado antes de logar ou armazenar
Por quê Escrevi coarsenIp para remover o último octeto IPv4 ou o último grupo IPv6, de modo que o IP completo nunca seja logado nem guardado no Stats DO. O objetivo é a conformidade com a GDPR.
Ao vivo
- +12UA de navegador sem Accept-Language
- +22diz ser Chrome 128 mas não manda Sec-CH-UA (UA forjado)
- +18Chrome moderno sem metadados Sec-Fetch-*
- +15navegador moderno falando HTTP/1.1 (esperado h2/h3)
Números com fonte
Cada número aponta para o arquivo do repositório de onde saiu.
- Redução máxima do orçamento
- 0,85 (até 15% do limite) / 0.85 (down to 15% of the limit)
- fonte: wrangler.toml
- Tamanho máximo do User-Agent guardado
- 80 caracteres / 80 characters
- fonte: src/config.js, src/index.js
- Testes unitários (Vitest)
- 90 passando / 90 passing
- fonte: tests/*.test.js, README.md (confirmado com vitest run / confirmed with vitest run)
- Cobertura de linhas de score.js, ratelimiter.js, config.js e logger.js
- 100%
- fonte: vitest.config.js (vitest run --coverage)
Stack
- Cloudflare Workers
- Durable Objects
- JavaScript
- Vitest