Aegis (Proxy Reverso e Rate Limiter)
Proxy reverso em Rust (tokio, axum, hyper) que escrevi para aplicar rate limit por IP com token bucket, responder 429 aos clientes abusivos e encaminhar o resto ao backend.
O problema
Um backend HTTP exposto fica à mercê de clientes abusivos: rajadas de requisições por IP, conexões lentas que seguram sockets (slowloris) e corpos gigantes. Faltava uma camada de proteção rodando na própria origem, sem depender de serviço externo.
A abordagem
Escrevi o Aegis em Rust, sobre tokio, axum e hyper: proxy reverso transparente com rate limit por IP em token bucket (DashMap particionado, lock por shard no caminho quente), limites por rota, agrupamento de IPv6 por prefixo, timeouts em todas as etapas, proxying de WebSocket, TLS opcional e recarga de configuração por SIGHUP sem derrubar conexões. Health check e métricas Prometheus vivem num plano administrativo separado, que nunca é limitado.
Arquitetura
- 01Cliente → server.rs: porta :8080
- 02server.rs → limiter.rs: requisição com ConnectInfo
- 03limiter.rs → proxy.rs: dentro do orçamento
- 04proxy.rs → Backend: HTTP/1.1 com streaming
- 05state.rs → limiter.rs: limites e regras atuais
- 06state.rs → proxy.rs: URL do backend e timeouts
- 07Prometheus → Plano admin :9000: GET /metrics
- 08Plano admin :9000 → state.rs: lê contadores
Decisões
- 01
Buckets em DashMap particionado, com refill preguiçoso e teto
Por quê Coloquei os buckets num DashMap particionado e os reabasteço no acesso, então IPs distintos raramente disputam o mesmo lock. Um sweeper de buckets ociosos e o teto AEGIS_MAX_BUCKETS limitam a memória sob floods de IPs forjados.
- 02
Recarga por SIGHUP via ArcSwap
Por quê Troco o estado recarregável inteiro num ArcSwap. Assim um SIGHUP recarrega backend, limites, regras por rota, teto de corpo e timeouts sem derrubar conexões.
Custo Endereços de escuta e material TLS ficam fixos durante a vida do processo, porque os sockets já estão abertos.
- 03
TLS opcional, atrás de uma feature do Cargo
Por quê Deixei o TLS atrás de uma feature para manter o build padrão pequeno e totalmente estático, sem dependências de C ou de criptografia.
Custo Terminação TLS e backends https:// só existem com --features tls, e o plano admin continua em texto puro.
- 04
Salto para o backend sempre em HTTP/1.1
Por quê O cliente pode falar HTTP/2 via TLS, mas o cliente de upstream em pool fala HTTP/1.1. Fixei a versão para desacoplar os dois saltos, o que corrigiu um erro 502 UserUnsupportedVersion.
Custo O backend nunca recebe HTTP/2, mesmo quando o cliente usa HTTP/2.
- 05
panic = "abort" no perfil de release
Por quê Sem tabelas de unwinding, o binário fica menor. No mesmo perfil uso LTO fat, codegen-units = 1 e strip.
Custo Um handler que entra em pânico derruba o processo inteiro em vez de ser isolado por requisição.
Ao vivo
- Dispare requisições contra GET /api/orders.
Números com fonte
Cada número aponta para o arquivo do repositório de onde saiu.
- Funções de teste
- 15 (13 unitárias, 2 de integração) / 15 (13 unit, 2 integration)
- fonte: src/limiter.rs, src/proxy.rs, tests/rate_limit.rs
- Jobs de CI
- 3 (check, cargo-audit, imagem Docker) / 3 (check, cargo-audit, Docker image)
- fonte: .github/workflows/ci.yml
Stack
- Rust
- Tokio
- Axum
- Hyper
- Prometheus
- Docker