X-Forwarded-For em Zig: IP Real do Cliente e Proxy Confiável

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çãoO que fazer
Serviço exposto direto na internetuse o IP do socket remoto
Nginx / Caddy / HAProxy na frenteconfie só no peer da borda
Cloudflare na frenteprefira CF-Connecting-IP ou política Cloudflare documentada
Header sem peer confiávelignore e use o socket remoto
Vários IPs em X-Forwarded-Foraplique uma regra explícita de hop
Rate limit / banderive a chave do IP confiável
Logs de incidentegrave peer TCP + IP derivado
X-Forwarded-Proto / Hostuse para URL externa e redirects
Cliente forja headernão deve mudar a decisão
Testesimule 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:

  1. peer TCP — endereço da conexão imediata;
  2. proxy confiável — peer autorizado a fornecer headers de encaminhamento;
  3. 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:

  1. obtenha o endereço remoto do socket aceito;
  2. compare com a lista de CIDRs/IPs de proxies confiáveis;
  3. se não houver match, retorne identidade baseada só no socket;
  4. se houver match, aí sim leia os headers permitidos pela política.

Exemplos de peers confiáveis:

  • 127.0.0.1 / ::1 quando 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 /64 se 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_hops explode;
  • correlacionar com health checks e observabilidade.

Métricas úteis:

  • http_client_identity_source{source="socket|x_real_ip|xff|cf"}
  • http_trusted_proxy_requests_total
  • http_invalid_forwarded_header_total
  • http_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:

  1. X-Forwarded-Proto se for http ou https;
  2. senão, scheme da conexão TLS local;
  3. X-Forwarded-Host ou Host, com allowlist de hosts do produto;
  4. 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:

CasoEsperado
peer não confiável + X-Forwarded-For forjadocliente = peer
peer confiável + X-Real-IP válidocliente = header
peer confiável + X-Real-IP inválidoerro / métrica de config
peer confiável sem headerscliente = peer
X-Forwarded-For com hops demaisrejeição
IPv6 com zona/porta inesperadarejeição ou política explícita
Cloudflare header com peer não-CFignore header
rate limit usa cliente derivadoabusador 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

  1. Confiar no header sem olhar o peer — o erro clássico; permite bypass de ban e rate limit.
  2. Usar sempre o primeiro IP da esquerda — o cliente escolhe esse valor.
  3. Allowlist de proxy desatualizada — depois de migrar load balancer, tudo cai para source=socket ou, pior, para confiança acidental.
  4. Tratar unknown como IP — alguns proxies legados emitem tokens não parseáveis; rejeite.
  5. Esquecer IPv6 — parsers só IPv4 quebram silenciosamente atrás de dual-stack.
  6. Logar só o cliente derivado — sem o peer, você não diagnostica spoofing.
  7. 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.
  8. 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-For e CF-Connecting-IP está documentada.
  • Parser valida IP e limita hops.
  • Rate limit, ban e auditoria usam o cliente derivado.
  • Logs incluem peer_ip, client_ip e client_source.
  • X-Forwarded-Proto/Host alimentam 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.

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.