import { BlockList, isIP, isIPv4 } from "node:net"; // Loopback is the Caddy in front of us, the rest Cloudflare's edge, from // https://www.cloudflare.com/ips (2026-08-22). Refresh when that list changes. const TRUSTED_PROXY_CIDRS = [ "127.0.0.0/8", "::1/128", "173.245.48.0/20", "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "141.101.64.0/18", "108.162.192.0/18", "190.93.240.0/20", "188.114.96.0/20", "197.234.240.0/22", "198.41.128.0/17", "162.158.0.0/15", "104.16.0.0/13", "104.24.0.0/14", "172.64.0.0/13", "131.0.72.0/22", "2400:cb00::/32", "2606:4700::/32", "2803:f800::/32", "2405:b500::/32", "2405:8100::/32", "2a06:98c0::/29", "2c0f:f248::/32", ]; const trustedProxies = new BlockList(); for (const cidr of TRUSTED_PROXY_CIDRS) { const [net, prefix] = cidr.split("/"); if (!net || !prefix) continue; trustedProxies.addSubnet(net, Number(prefix), isIPv4(net) ? "ipv4" : "ipv6"); } function isTrustedProxy(ip: string): boolean { return trustedProxies.check(ip, isIPv4(ip) ? "ipv4" : "ipv6"); } // ::ffff:127.0.0.1 and 127.0.0.1 are one visitor, not two buckets. function normalizeIp(raw: string | null | undefined): string | null { const value = raw?.trim(); if (!value) return null; const unmapped = value.startsWith("::ffff:") ? value.slice(7) : value; return isIP(unmapped) ? unmapped : null; } export interface PeerAddressSource { requestIP(request: Request): { address: string } | null; } // The address to rate-limit on, or null when the request came through no socket // we can read — headers alone are the client's to write, so they never decide. export function getClientIp( request: Request, server: PeerAddressSource | null | undefined, ): string | null { const peer = normalizeIp(server?.requestIP(request)?.address); if (!peer) return null; // Each hop appends the address it saw, so walking in from the peer is // trustworthy exactly as far as the hops are ours. const forwarded = request.headers.get("x-forwarded-for")?.split(",") ?? []; const hops = [...forwarded, peer]; for (let i = hops.length - 1; i >= 0; i--) { const hop = normalizeIp(hops[i]); if (hop && !isTrustedProxy(hop)) return hop; } // Reached only when the whole chain is Cloudflare's or ours, so this header // is Cloudflare's, which overwrites whatever the client sent. return normalizeIp(request.headers.get("cf-connecting-ip")) ?? peer; }