How to Block VPN Users in Python

Detect and block VPN, proxy and Tor users in Python, with ready-made Flask and Django examples.

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. Look up the IP

Put the helpers in their own module, vpnblocker.py. This uses the requests package and Python 3.10 or later, with a 3 second timeout. If the lookup fails it returns None, so you can let the user through.

import os
import requests

def lookup(ip: str) -> dict | None:
    try:
        res = requests.get(
            f"https://api.vpnblocker.net/v2/json/{ip}",
            headers={"X-API-KEY": os.environ["VPNBLOCKER_KEY"]},
            timeout=3,
        )
    except requests.RequestException:
        return None  # network error or timeout
    return res.json() if res.status_code == 200 else None

3. Cache the answer

Caching saves lookups and keeps responses fast. An hour is a good length, because Tor exits change every hour.

import time

_cache: dict[str, tuple[float, dict]] = {}

def cached_lookup(ip: str) -> dict | None:
    hit = _cache.get(ip)
    if hit and hit[0] > time.monotonic():
        return hit[1]
    r = lookup(ip)
    if r is not None:
        _cache[ip] = (time.monotonic() + 3600, r)
    return r

This cache lives in one process and grows forever. With several workers, use your framework’s cache or Redis instead.

4. 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.
BLOCK_CATEGORIES = {"vpn", "proxy", "tor"}  # add "hosting" to block data centers too
BLOCK_AT = 90        # block these categories at or above this confidence
CHALLENGE_AT = 70    # challenge anything flagged at or above this confidence
CLEAN_TRUST_AT = 80  # clean answers below this are little-known addresses

def decide(r: dict, sensitive: bool = False) -> str:
    flagged = r["host-ip"] is True or len(r["categories"]) > 0
    blockable = bool(BLOCK_CATEGORIES.intersection(r["categories"]))

    if blockable and r["confidence"] >= BLOCK_AT:
        return "block"
    if flagged and r["confidence"] >= CHALLENGE_AT:
        return "challenge"
    if sensitive and not flagged and r["confidence"] < CLEAN_TRUST_AT:
        return "challenge"
    return "allow"

5a. Flask

A decorator protects the views you choose. If your app runs behind a load balancer or reverse proxy, ProxyFix makes request.remote_addr the visitor’s real IP.

from functools import wraps

from flask import Flask, abort, g, request
from werkzeug.middleware.proxy_fix import ProxyFix

from vpnblocker import cached_lookup, decide

app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1)  # only if you're behind exactly one proxy

def vpn_check(sensitive: bool = False):
    def wrap(view):
        @wraps(view)
        def inner(*args, **kwargs):
            r = cached_lookup(request.remote_addr)
            action = decide(r, sensitive) if r is not None else "allow"  # allow if the lookup failed
            if action == "block":
                abort(403, "Please turn off your VPN, proxy or Tor and try again.")
            g.vpn_action = action  # show a captcha when this is "challenge"
            return view(*args, **kwargs)
        return inner
    return wrap

@app.post("/signup")
@vpn_check()
def signup():
    ...

5b. Django

A middleware checks the paths you list. Add it to MIDDLEWARE in your settings.

# myapp/middleware.py
from django.http import HttpResponseForbidden

from .vpnblocker import cached_lookup, decide

PROTECTED = {"/signup/": False, "/checkout/": True}  # path -> sensitive

class VpnBlockerMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if request.method == "POST" and request.path in PROTECTED:
            r = cached_lookup(request.META["REMOTE_ADDR"])
            action = decide(r, PROTECTED[request.path]) if r is not None else "allow"
            if action == "block":
                return HttpResponseForbidden("Please turn off your VPN, proxy or Tor and try again.")
            request.vpn_action = action  # show a captcha when this is "challenge"
        return self.get_response(request)

REMOTE_ADDR is the address that connected to your server. Behind a proxy, set it from the proxy’s header in your web server config, or use a package such as django-ipware.

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.

Block VPNs in your Python app today

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