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.

Registrarse Leer la documentación

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.

request.cf como base
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.cfGET /v0/ip/{ip}
País, región, ciudad, coordenadasSíSí
ASN y organización del ASSíSí, más ISP, prefijo y RIR
Radio de precisión y confianzaNoaccuracy_radius_km, confidence
Flag de hosting/datacenterNois_hosting
Exit de Tor, redes VPN conocidas, relay de privacidadNois_tor, is_vpn, is_relay
Operador móvil, internet satelitalNois_mobile, is_satellite
Cualquier IP, no solo la del visitanteNoSí, 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:

Terminal
$ 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
// 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:

route()
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_hosting o is_tor en 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 cuando confidence sea high o medium.

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 o consumer de Queue
// 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.cf o 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: devuelve US / Mountain View con un radio de 50 km y confianza medium, más is_hosting: true e is_anycast: true, así que tu regla de registro debería mandarlo al desafío.

Relacionado

Registrarse Leer la documentación