iplogs.com

How to block VPN, proxy and Tor traffic

Working rules for Cloudflare WAF, Nginx, Caddy, and Stripe Radar — plus the false-positive playbook. Most VPN traffic is legitimate, so blunt blocks cost you customers. Use these patterns to block only where the friction is worth it.

Decision: where to block, where to step up

  • Block: account creation, KYC submission, money-out flows (withdrawals, refunds), admin/staff routes.
  • Step up auth: login, password reset, payment, subscription change, bulk operations.
  • Allow: read traffic, RSS feeds, public docs. Hard-blocking these costs more than the abuse you'd prevent.

Cloudflare WAF expression

Cloudflare's built-in cf.threat_scoreand ip.src.is_in_european_uniondon't catch enough on their own. Layer in an external lookup via Cloudflare Worker (see implementation guide), then enforce with a WAF rule:

# Block VPN/proxy on signup + checkout
(http.request.uri.path matches "^/(signup|checkout|api/payment)" and
 cf.threat_score gt 14 or
 ip.src.is_in_european_union eq false and any(http.request.headers["x-iplogs-verdict"][*] in {"vpn_detected" "vpn_likely"}))

Set the x-iplogs-verdict header from a Worker that calls POST /v1/check on the visitor IP.

Nginx — block by ASN list

For static blocking by ASN, mount the IPLogs CSV at /data/datacenter-asns.csv and convert to an Nginx geo block:

# Generate /etc/nginx/conf.d/vpn-block.conf from the IPLogs ASN list
geo $is_vpn_asn {
    default 0;
    # Add CIDR ranges for AS9009 (M247), AS62240 (Clouvider), AS9009 etc.
    # Pull from https://iplogs.com/data/datacenter-asns.csv
    23.234.0.0/16 1;
    185.65.0.0/16 1;
    # ...
}

server {
    location ~ ^/(signup|checkout) {
        if ($is_vpn_asn) {
            return 403 "VPN traffic blocked on this route";
        }
        proxy_pass http://app;
    }
}

Caddy — header-based block via reverse-proxy

# Caddyfile snippet — call IPLogs from a Caddy plugin or
# pre-set the verdict header from your app middleware
example.com {
    @vpn_block {
        path /signup* /checkout*
        header X-IPLogs-Verdict vpn_detected vpn_likely
    }
    respond @vpn_block 403 "VPN traffic blocked on this route"

    reverse_proxy localhost:3000
}

Stripe Radar — rule recipe

Stripe Radar 2.0 already scores VPN/proxy IPs as elevated risk by default. To enforce harder rules, pass the IPLogs verdict into the Customer.metadata at payment intent creation:

// On payment intent creation
const verdict = (await checkVpn(customerIp)).verdict;
await stripe.paymentIntents.create({
  amount, currency,
  customer: customerId,
  metadata: { iplogs_verdict: verdict },
});

// Stripe Radar rule (UI):
// Block if :metadata:iplogs_verdict: == "vpn_detected"
// Review if :metadata:iplogs_verdict: == "vpn_likely"
// 3DS if :metadata:iplogs_verdict: == "suspicious" and :amount: > 5000

False-positive playbook

Before you block, plan how to handle false positives. Most "false positive" reports come from these populations:

  • CGNAT / mobile carriers — shared IP pools that overlap with commercial VPN exits. Step-up over hard-block.
  • Cloudflare WARP / iCloud Private Relay — most users have these on by default. Treat as a separate "private_relay" tier with much lower friction.
  • Corporate proxies — every enterprise customer routes traffic through a centralised egress that can look like a datacenter exit.
  • Travelling users — business travellers commonly use VPN for work, and would be your highest-LTV customer to wrongly block.

Always include a path to dispute. A "VPN detected — try again on a different network" message that links to a contact form catches lost revenue from false positives.

Building VPN detection in code first? See implementation guide. Wondering why your IP shows as a VPN? Read the troubleshooting walkthrough.