Para obter o IP real do cliente em um serviço Zig atrás de proxy, nunca leia X-Forwarded-For de forma cega. Aceite headers de encaminhamento somente quando o peer TCP imediato estiver em uma lista de proxies confiáveis. Depois, escolha o hop correto da cadeia — descontando à direita os proxies que você controla — e use esse valor para logs, rate limit, geolocalização e auditoria. Se o peer não for confiável, ignore o header e use o endereço do socket remoto.
Esse detalhe parece operacional, mas decide se o rate limiting funciona, se o Nginx como proxy reverso entrega telemetria correta e se incidentes de abuso apontam para a origem certa. Em APIs com cookies e sessões, o IP também aparece em trilhas de login e em alertas de sessão suspeita.
Como a API de std.http pode mudar antes do Zig 1.0, confira zig version e a documentação do release instalado. Os nomes exatos de request, headers e peer address podem variar; as regras de confiança e parsing permanecem as mesmas.
Resposta rápida
| Situação | O que fazer |
|---|---|
| Serviço exposto direto na internet | use o IP do socket remoto |
| Nginx / Caddy / HAProxy na frente | confie só no peer da borda |
| Cloudflare na frente | prefira CF-Connecting-IP ou política Cloudflare documentada |
| Header sem peer confiável | ignore e use o socket remoto |
Vários IPs em X-Forwarded-For | aplique uma regra explícita de hop |
| Rate limit / ban | derive a chave do IP confiável |
| Logs de incidente | grave peer TCP + IP derivado |
X-Forwarded-Proto / Host | use para URL externa e redirects |
| Cliente forja header | não deve mudar a decisão |
| Teste | simule peer confiável e peer externo |
Se você ainda está montando o serviço, comece pelo guia de servidor HTTP em Zig para produção e configure a borda com o proxy reverso e load balancing.
Por que o socket remoto não basta
Quando o cliente fala direto com o binário Zig, o endereço do peer TCP é o cliente. Depois que um proxy entra no caminho, o peer passa a ser o proxy:
navegador 203.0.113.50
→ Cloudflare / Nginx 10.0.0.2
→ serviço Zig :8080
Do ponto de vista do processo Zig, toda requisição parece vir de 10.0.0.2. Sem uma política de encaminhamento:
- o rate limit agrupa o mundo inteiro no IP do proxy;
- banimentos bloqueiam a borda em vez do abusador;
- logs de autenticação apontam para a infraestrutura;
- regras de geofencing e allowlist quebram;
- alertas de “novo IP de login” disparam o tempo todo.
Os proxies preenchem headers para recuperar o cliente original. O problema é que qualquer cliente também pode enviar esses headers. Confiança sem autenticação do peer vira buraco de segurança.
Headers comuns e o que significam
X-Forwarded-For
Formato típico:
X-Forwarded-For: 203.0.113.50, 198.51.100.7, 10.0.0.2
Por convenção, a lista cresce da esquerda para a direita: o primeiro valor costuma ser o cliente original e os seguintes são proxies intermediários. Na prática, o cliente pode ter prefixado valores falsos antes de a sua borda acrescentar o hop real. Por isso a regra segura é: só confie nos hops adicionados depois que o tráfego entrou na sua infraestrutura.
X-Real-IP
Muitos setups Nginx enviam um único IP:
X-Real-IP: 203.0.113.50
É mais simples de parsear, mas continua sendo um header forjável se o serviço aceitar conexões de qualquer origem. Use-o apenas com peer confiável.
CF-Connecting-IP
Em Cloudflare, este header carrega o cliente visto pela rede da Cloudflare. Continua válido o mesmo princípio: aceite-o somente de IPs Cloudflare (ou do seu proxy que já validou a origem).
X-Forwarded-Proto e X-Forwarded-Host
Não identificam o cliente, mas restauram o esquema e o host externos:
X-Forwarded-Proto: https
X-Forwarded-Host: api.exemplo.com.br
Sem eles, redirects, cookies Secure, links absolutos e checagens de CSRF/Origin podem assumir http://127.0.0.1:8080 em vez do domínio público. Veja também redirects HTTP seguros e CORS em APIs Zig.
Modele a política antes do parser
Separe três conceitos:
- peer TCP — endereço da conexão imediata;
- proxy confiável — peer autorizado a fornecer headers de encaminhamento;
- cliente derivado — resultado da política aplicada aos headers.
const std = @import("std");
const net = std.net;
const ClientIdentity = struct {
peer: net.Address,
client: net.Address,
source: enum { socket, x_real_ip, x_forwarded_for, cf_connecting_ip },
forwarded_chain_len: usize = 0,
};
const TrustError = error{
UntrustedPeer,
InvalidHeader,
EmptyHeader,
TooManyHops,
UnsupportedAddress,
};
Essa separação evita misturar parsing de string com decisão de segurança. O handler de negócio deve receber um ClientIdentity já resolvido, nunca o header cru.
Checklist de confiança do peer
Antes de olhar qualquer header:
- obtenha o endereço remoto do socket aceito;
- compare com a lista de CIDRs/IPs de proxies confiáveis;
- se não houver match, retorne identidade baseada só no socket;
- se houver match, aí sim leia os headers permitidos pela política.
Exemplos de peers confiáveis:
127.0.0.1/::1quando o proxy roda na mesma máquina;- rede privada do cluster (
10.0.0.0/8,192.168.0.0/16, etc.) quando só a malha interna alcança o serviço; - IPs públicos do load balancer ou da Cloudflare, atualizados por processo operacional.
Não hardcode uma allowlist incompleta em produção sem revisão. Prefira configuração explícita por ambiente (TRUSTED_PROXIES=10.0.0.2/32,10.0.0.3/32) e falhe fechado: peer desconhecido ⇒ sem confiança nos headers.
Parsing seguro de X-Forwarded-For
Um parser inicial precisa:
- rejeitar header vazio;
- dividir por vírgula;
- fazer trim de espaços;
- limitar o número de hops;
- validar cada token como IPv4 ou IPv6;
- rejeitar lixo (
unknown, aspas, portas inesperadas, texto livre).
Esboço de abordagem:
fn parseForwardedFor(
allocator: std.mem.Allocator,
header_value: []const u8,
max_hops: usize,
) TrustError![]net.Address {
if (header_value.len == 0) return TrustError.EmptyHeader;
var list: std.ArrayList(net.Address) = .{};
errdefer list.deinit(allocator);
var it = std.mem.splitScalar(u8, header_value, ',');
while (it.next()) |raw_part| {
const part = std.mem.trim(u8, raw_part, " \t");
if (part.len == 0) return TrustError.InvalidHeader;
if (list.items.len >= max_hops) return TrustError.TooManyHops;
const addr = net.Address.parseIp(part, 0) catch return TrustError.InvalidHeader;
try list.append(allocator, addr);
}
if (list.items.len == 0) return TrustError.EmptyHeader;
return try list.toOwnedSlice(allocator);
}
Ajuste parseIp e a representação de porta conforme a stdlib do seu release. O ponto importante é falhar em input ambíguo em vez de “melhor esforço”. Rate limit e banimentos não admitem IP inventado por parser permissivo.
Qual hop escolher
Há duas políticas comuns:
1. Borda única + X-Real-IP
Quando só o seu Nginx fala com o Zig:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
No Zig, com peer confiável, leia X-Real-IP e ignore a cadeia completa. É o desenho mais simples e menos sujeito a spoofing do prefixo.
2. Cadeia com hops confiáveis conhecidos
Se existem N proxies seus, conte hops a partir da direita (próximos do seu serviço) e descarte somente os hops que sua infraestrutura adicionou. O próximo endereço à esquerda dessa zona confiável é o cliente.
Exemplo:
X-Forwarded-For: 198.51.100.9, 203.0.113.50, 10.0.0.2
peer TCP: 10.0.0.2 (confiável)
Se a política diz “um hop interno”, o cliente derivado é 203.0.113.50. O valor 198.51.100.9 pode ter sido prefixado pelo próprio cliente e não deve mandar na decisão.
Nunca use “primeiro da esquerda” sem modelar hops confiáveis. Essa heurística é exatamente o que um atacante explora.
Implementação de resolução
fn resolveClient(
peer: net.Address,
trusted: []const Cidr,
headers: anytype,
max_hops: usize,
allocator: std.mem.Allocator,
) TrustError!ClientIdentity {
if (!isTrustedPeer(peer, trusted)) {
return .{
.peer = peer,
.client = peer,
.source = .socket,
};
}
if (headers.get("cf-connecting-ip")) |cf| {
const addr = net.Address.parseIp(std.mem.trim(u8, cf, " \t"), 0) catch {
return TrustError.InvalidHeader;
};
return .{
.peer = peer,
.client = addr,
.source = .cf_connecting_ip,
.forwarded_chain_len = 1,
};
}
if (headers.get("x-real-ip")) |real| {
const addr = net.Address.parseIp(std.mem.trim(u8, real, " \t"), 0) catch {
return TrustError.InvalidHeader;
};
return .{
.peer = peer,
.client = addr,
.source = .x_real_ip,
.forwarded_chain_len = 1,
};
}
if (headers.get("x-forwarded-for")) |xff| {
const chain = try parseForwardedFor(allocator, xff, max_hops);
defer allocator.free(chain);
// Borda única com $proxy_add_x_forwarded_for: o Nginx acrescenta
// $remote_addr à direita. O cliente visto pela borda é o último hop.
// Valores à esquerda podem ter sido prefixados pelo próprio cliente.
// Se houver mais de um proxy interno, desconte hops confiáveis à direita
// e só então leia o próximo endereço. Prefira X-Real-IP quando puder.
const client = chain[chain.len - 1];
return .{
.peer = peer,
.client = client,
.source = .x_forwarded_for,
.forwarded_chain_len = chain.len,
};
}
return .{
.peer = peer,
.client = peer,
.source = .socket,
};
}
O comentário no trecho de X-Forwarded-For é proposital: a escolha do índice depende da topologia. Em muitos times brasileiros a combinação mais segura é peer confiável + X-Real-IP, deixando X-Forwarded-For só para auditoria.
Integração com rate limit e ban
O token bucket por IP só é justo se a chave for o cliente derivado:
const key = try std.fmt.allocPrint(allocator, "{f}", .{identity.client});
defer allocator.free(key);
try limiter.consume(key, 1);
Regras práticas:
- não misture peer e cliente na mesma chave;
- se a resolução falhar por header inválido vindo de peer confiável, trate como erro de configuração (métrica + log), não como “IP desconhecido liberado”;
- considere IPv6 e prefixos
/64se for limitar redes, não apenas hosts; - em login e recuperação de senha, combine IP derivado com outros sinais (conta, device, cookie) para não punir CGNAT demais.
Logs, métricas e observabilidade
Grave campos distintos:
peer_ip=10.0.0.2 client_ip=203.0.113.50 client_source=x_real_ip xff_hops=2
Isso permite:
- detectar queda súbita de
client_source=socket(proxy deixou de enviar header); - auditar spoofing quando
xff_hopsexplode; - correlacionar com health checks e observabilidade.
Métricas úteis:
http_client_identity_source{source="socket|x_real_ip|xff|cf"}http_trusted_proxy_requests_totalhttp_invalid_forwarded_header_totalhttp_rate_limit_key_ip_family{family="ipv4|ipv6"}
Scheme e host externos
Além do IP, normalize a URL pública:
const ForwardedContext = struct {
scheme: []const u8, // "https" ou "http"
host: []const u8,
};
Política sugerida com peer confiável:
X-Forwarded-Protose forhttpouhttps;- senão, scheme da conexão TLS local;
X-Forwarded-HostouHost, com allowlist de hosts do produto;- rejeite hosts inesperados para bloquear cache poisoning e redirects abertos.
Essa base alimenta Location em redirects, links em e-mails, cookies Secure e checagens de origem. Combine com TLS/HTTPS e mTLS quando o Zig termina TLS diretamente.
Testes que precisam passar
Cubra pelo menos:
| Caso | Esperado |
|---|---|
peer não confiável + X-Forwarded-For forjado | cliente = peer |
peer confiável + X-Real-IP válido | cliente = header |
peer confiável + X-Real-IP inválido | erro / métrica de config |
| peer confiável sem headers | cliente = peer |
X-Forwarded-For com hops demais | rejeição |
| IPv6 com zona/porta inesperada | rejeição ou política explícita |
| Cloudflare header com peer não-CF | ignore header |
| rate limit usa cliente derivado | abusador isolado, proxy não banido |
Teste de integração com curl atrás de um Nginx local:
# peer aparente = nginx; cliente derivado deve ser o IP que o nginx viu
curl -i https://api.local.exemplo/health
E um teste negativo apontando direto no serviço:
curl -i \
-H 'X-Forwarded-For: 198.51.100.9' \
-H 'X-Real-IP: 198.51.100.9' \
http://127.0.0.1:8080/debug/whoami
Se o processo Zig estiver configurado para confiar só no Nginx da rede interna, a resposta de whoami deve mostrar o seu IP de socket, não 198.51.100.9.
Armadilhas frequentes
- Confiar no header sem olhar o peer — o erro clássico; permite bypass de ban e rate limit.
- Usar sempre o primeiro IP da esquerda — o cliente escolhe esse valor.
- Allowlist de proxy desatualizada — depois de migrar load balancer, tudo cai para
source=socketou, pior, para confiança acidental. - Tratar
unknowncomo IP — alguns proxies legados emitem tokens não parseáveis; rejeite. - Esquecer IPv6 — parsers só IPv4 quebram silenciosamente atrás de dual-stack.
- Logar só o cliente derivado — sem o peer, você não diagnostica spoofing.
- Aplicar a mesma política em ambientes locais e produção — em dev, confiar em
127.0.0.1é ok; em produção, documente CIDRs reais. - Misturar CDN e origem sem mTLS/rede privada — se qualquer host alcançar a origem, a allowlist de proxy vaza.
Checklist antes do deploy
- O serviço não é alcançável publicamente exceto via proxy/CDN esperados.
- Existe lista explícita de peers/CIDRs confiáveis por ambiente.
- Headers de encaminhamento são ignorados para peers fora da lista.
- A escolha entre
X-Real-IP,X-Forwarded-ForeCF-Connecting-IPestá documentada. - Parser valida IP e limita hops.
- Rate limit, ban e auditoria usam o cliente derivado.
- Logs incluem
peer_ip,client_ipeclient_source. -
X-Forwarded-Proto/Hostalimentam redirects e cookies com allowlist. - Testes cobrem spoofing direto na origem.
- Métricas alertam header inválido e queda de confiança.
Perguntas frequentes
Posso confiar no X-Forwarded-For enviado por qualquer cliente?
Não. Qualquer cliente pode inventar X-Forwarded-For. Só use esse header quando a conexão TCP imediata vier de um proxy ou load balancer que você controla e confia. Caso contrário, use o endereço do socket remoto.
Qual header preferir: X-Forwarded-For, X-Real-IP ou CF-Connecting-IP?
Depende da borda. Em Nginx típico, X-Real-IP ou o hop confiável de X-Forwarded-For. Em Cloudflare, CF-Connecting-IP costuma ser o mais direto. Escolha uma política explícita e documente a cadeia esperada.
Como X-Forwarded-For afeta rate limiting em Zig?
Se o limitador usar o IP errado, um atacante pode forjar o header e escapar do limite, ou vários usuários legítimos atrás do mesmo NAT/CDN podem ser bloqueados juntos. Derive a chave somente de um hop confiável.
Devo registrar o socket remoto ou o IP encaminhado?
Registre os dois quando houver proxy: o endereço do peer TCP e o cliente derivado da política. Isso facilita auditar spoofing, erros de configuração e incidentes de abuso.
X-Forwarded-For resolve sozinho redirects HTTPS e CSRF?
Não. Ele ajuda a recuperar o cliente e, com X-Forwarded-Proto/Host, o esquema e o host externos. Redirects, cookies Secure e CSRF ainda precisam de política própria.
Conclusão
Em Zig, descobrir o IP do cliente atrás de proxy é um problema de confiança, não de parsing. Comece pelo peer TCP, aceite headers apenas de proxies autorizados, escolha o hop com uma regra documentada e propague essa identidade para logs, rate limit e auditoria. Prefira desenhos simples — borda única com X-Real-IP — antes de cadeias longas de X-Forwarded-For.
Esse cuidado fecha um buraco clássico em serviços que já usam Nginx, CDN e limitadores. Combine este guia com servidor HTTP de produção, proxy reverso e rate limiting para manter abuso, observabilidade e redirects alinhados ao mundo real fora do container.