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.