Cloudflare Workers: geolocalización de IP y detección de hosting/Tor más allá de request.cf
Usá request.cf como base gratis y sumá confianza, hosting, exit de Tor, redes VPN conocidas y relay de privacidad, con caché y fail-open.
Gratis: 1.000 req/día · HTTPS · uso comercial OK
Qué te da request.cf y qué no
Cada request que llega a un Worker trae el objeto request.cf con el país, la región, la ciudad, las coordenadas, el ASN y la organización del AS del visitante. Para saber “¿de qué país es?”, es gratis e instantáneo, y conviene usarlo.
export default {
async fetch(request) {
const cf = request.cf ?? {};
return Response.json({
country: cf.country, // "US"
city: cf.city, // "Mountain View"
asn: cf.asn, // 15169
asOrganization: cf.asOrganization,
});
},
};
Lo que no te dice: qué tan precisa es esa ciudad, si la IP es de un datacenter, un exit de Tor o una red VPN conocida, ni nada sobre IPs que no sean las del visitante actual (líneas de log, webhooks, registros guardados). Ese es el hueco que cubre esta guía.
request.cf | GET /v0/ip/{ip} | |
|---|---|---|
| País, región, ciudad, coordenadas | Sí | Sí |
| ASN y organización del AS | Sí | Sí, más ISP, prefijo y RIR |
| Radio de precisión y confianza | No | accuracy_radius_km, confidence |
| Flag de hosting/datacenter | No | is_hosting |
| Exit de Tor, redes VPN conocidas, relay de privacidad | No | is_tor, is_vpn, is_relay |
| Operador móvil, internet satelital | No | is_mobile, is_satellite |
| Cualquier IP, no solo la del visitante | No | Sí, más POST /v0/bulk |
Obtené la IP del cliente
Dentro de un Worker, la dirección del visitante viene en el header CF-Connecting-IP. Lo pone Cloudflare en el edge, así que el cliente no lo puede pisar como pasa con un X-Forwarded-For crudo.
Llamá a /v0/ip/{ip} desde el Worker
Guardá la key como secret, nunca en wrangler.toml:
$ npx wrangler secret put NDL_API_KEY
Después consultá la IP con el header Bearer, cacheá el resultado por IP con la Cache API y seguí de largo si la llamada tarda:
// src/index.js
const API = "https://api.networkdatalabs.com/v0/ip/";
const TTL = 6 * 60 * 60; // seconds
async function lookup(ip, env, ctx) {
const cache = caches.default;
const cacheKey = new Request(`https://ndl-cache.internal/ip/${ip}`);
const hit = await cache.match(cacheKey);
if (hit) return hit.json();
const res = await fetch(API + encodeURIComponent(ip), {
headers: { Authorization: `Bearer ${env.NDL_API_KEY}` },
signal: AbortSignal.timeout(500),
});
if (!res.ok) throw new Error(`lookup ${res.status}`);
const data = await res.json();
ctx.waitUntil(cache.put(cacheKey, new Response(JSON.stringify(data), {
headers: { "Cache-Control": `max-age=${TTL}`, "Content-Type": "application/json" },
})));
return data;
}
export default {
async fetch(request, env, ctx) {
const ip = request.headers.get("CF-Connecting-IP");
let intel = null;
try {
intel = await lookup(ip, env, ctx);
} catch {
// fail open: fall through to the origin
}
return route(request, intel);
},
};
La Cache API es por data center y no tiene costo extra. Si querés una caché compartida entre ubicaciones, guardá el JSON en Workers KV con expirationTtl.
Reglas: desafiar tráfico riesgoso y rutear por país
Con los traits a mano, protegé las rutas sensibles y pasale el resto de las señales a tu origen como headers:
const PROTECTED = ["/signup", "/login", "/checkout"];
function route(request, intel) {
const url = new URL(request.url);
const t = intel?.traits ?? {};
if (PROTECTED.includes(url.pathname) && (t.is_tor || t.is_hosting)) {
// send to a page with a challenge (Turnstile) instead of a hard block
return Response.redirect(`${url.origin}/verify?next=${url.pathname}`, 302);
}
const headers = new Headers(request.headers);
headers.set("X-IP-Country", intel?.location?.country_code ?? request.cf?.country ?? "");
headers.set("X-IP-Confidence", intel?.location?.confidence ?? "unknown");
headers.set("X-IP-Flags", Object.entries(t).filter(([, v]) => v).map(([k]) => k).join(","));
return fetch(new Request(request, { headers })); // pass to origin
}
is_hostingois_toren registro/login: mostrá un desafío (Turnstile) en vez de bloquear.is_vpn(redes VPN conocidas, incluidos los principales proveedores comerciales de VPN): dejalo pasar con un flag y pedí un paso extra al pagar.is_relay(relay de privacidad): en general son usuarios comunes; solo no tomes la ubicación como precisa.- Ruteo por país: usá
country_code, y la ciudad solo cuandoconfidenceseahighomedium.
Enriquecé logs por lotes con POST /v0/bulk
Para IPs que no son del visitante actual, como la salida de Logpush o una cola de registros, agrupalas. /v0/bulk acepta hasta 100 IPs, cuenta como un request para el límite por minuto y como uno por IP para la cuota diaria:
// Cron Trigger or Queue consumer: enrich up to 100 IPs per call
async function enrichBatch(ips, env) {
const res = await fetch("https://api.networkdatalabs.com/v0/bulk", {
method: "POST",
headers: {
Authorization: `Bearer ${env.NDL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ ips: ips.slice(0, 100) }),
});
const { results, errors } = await res.json();
return { results, errors }; // results keyed by IP; bad IPs land in errors
}
Presupuesto de latencia y fail-open
- Llamá a la API solo en las rutas que lo necesitan; para los assets estáticos alcanza con
request.cfo nada. - Timeout corto (unos 500 ms) y siempre seguí hacia el origen si hay error.
- Caché por IP: el que vuelve no consume consultas.
- Probá con una IP de EE. UU. como
8.8.8.8: devuelveUS/ Mountain View con un radio de 50 km y confianzamedium, másis_hosting: trueeis_anycast: true, así que tu regla de registro debería mandarlo al desafío.
Relacionado
- API búsqueda de ASN: ASN, ISP y organización, más
GET /v0/asn/{asn} - API detección de VPN
- Detectar registros sospechosos por IP en Python
- API Docs