How to Block VPN Users in PHP

Detect and block VPN, proxy and Tor users in plain PHP or Laravel, with caching and a safe fallback.

1. Get an API key

Create a free account and copy your API key from the dashboard. The Free plan includes 10,000 lookups a month. Keep the key on your server in an environment variable called VPNBLOCKER_KEY, never in code that runs in the browser.

2. Get the visitor’s real IP

Normally that’s $_SERVER['REMOTE_ADDR']. If your site is behind Cloudflare, use the address Cloudflare passes on instead:

const BEHIND_CLOUDFLARE = false; // set to true only if all traffic comes through Cloudflare

function visitor_ip(): string
{
    if (BEHIND_CLOUDFLARE && !empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
        return $_SERVER['HTTP_CF_CONNECTING_IP'];
    }
    return $_SERVER['REMOTE_ADDR'];
}

Only trust a forwarded header if every request really reaches your server through that proxy. Otherwise anyone can send a fake one and pretend to be any IP address.

3. Look up the IP

One HTTPS request with a short timeout. If the lookup fails, the function returns null so you can let the user through rather than locking everyone out.

function vpnblocker_lookup(string $ip): ?array
{
    $ch = curl_init('https://api.vpnblocker.net/v2/json/' . rawurlencode($ip));
    curl_setopt_array($ch, [
        CURLOPT_HTTPHEADER => ['X-API-KEY: ' . getenv('VPNBLOCKER_KEY')],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 3,
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    return ($body !== false && $status === 200) ? json_decode($body, true) : null;
}

4. Cache the answer

Caching saves lookups and keeps pages fast. An hour is a good length, because Tor exits change every hour. This uses APCu when it’s installed:

function vpnblocker_cached(string $ip): ?array
{
    $key = 'vpnblocker:' . $ip;
    if (function_exists('apcu_fetch')) {
        $hit = apcu_fetch($key, $found);
        if ($found) {
            return $hit;
        }
    }
    $r = vpnblocker_lookup($ip);
    if ($r !== null && function_exists('apcu_store')) {
        apcu_store($key, $r, 3600);
    }
    return $r;
}

5. Decide: block, challenge or allow

This turns a lookup into one of three actions. It’s the same logic as the example in the docs: change the settings at the top to fit your site.

// Tune these to fit your site.
const BLOCK_CATEGORIES = ['vpn', 'proxy', 'tor']; // add 'hosting' to block data centers too
const BLOCK_AT = 90;       // block these categories at or above this confidence
const CHALLENGE_AT = 70;   // challenge anything flagged at or above this confidence
const CLEAN_TRUST_AT = 80; // clean answers below this are little-known addresses

function decide(array $r, bool $sensitive = false): string
{
    $flagged = $r['host-ip'] === true || count($r['categories']) > 0;
    $blockable = (bool) array_intersect($r['categories'], BLOCK_CATEGORIES);

    if ($blockable && $r['confidence'] >= BLOCK_AT) return 'block';
    if ($flagged && $r['confidence'] >= CHALLENGE_AT) return 'challenge';
    if ($sensitive && !$flagged && $r['confidence'] < CLEAN_TRUST_AT) return 'challenge';
    return 'allow';
}

6. Use it where it matters

Check on the pages abuse goes for, such as sign-up, login and checkout, rather than every page view. Pass true for $sensitive on payments.

$r = vpnblocker_cached(visitor_ip());
$action = $r === null ? 'allow' : decide($r); // allow if the lookup failed

if ($action === 'block') {
    http_response_code(403);
    exit('Please turn off your VPN, proxy or Tor and try again.');
}
if ($action === 'challenge') {
    // show a captcha, or ask for email or phone verification
}

Laravel

In Laravel, a middleware does the same job. $request->ip() respects your trusted proxy settings. Add the key to config/services.php as 'vpnblocker' => ['key' => env('VPNBLOCKER_KEY')].

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

class BlockVpn
{
    public function handle(Request $request, Closure $next)
    {
        $ip = $request->ip();
        $r = Cache::remember("vpnblocker:$ip", 3600, function () use ($ip) {
            try {
                $res = Http::withHeaders(['X-API-KEY' => config('services.vpnblocker.key')])
                    ->timeout(3)
                    ->get('https://api.vpnblocker.net/v2/json/' . rawurlencode($ip));
                return $res->ok() ? $res->json() : null;
            } catch (\Throwable) {
                return null; // network error or timeout: let the user through
            }
        });

        if ($r !== null && decide($r) === 'block') {
            abort(403, 'Please turn off your VPN, proxy or Tor and try again.');
        }
        return $next($request);
    }
}

Put decide() from step 5 in a helper file, then add the middleware to the routes you want to protect, for example Route::post('/register', ...)->middleware(BlockVpn::class);.

Block VPNs in your PHP app today

Create a free account and make your first lookup in minutes. 10,000 lookups a month at no charge.