Tutorial: How to Detect VPNs and Tor Users in Node.js Express
Source: Dev.to
Overview
If you run any kind of public API, SaaS, or forum, you already know the pain: bot traffic.
You ban a user for spamming, and seconds later they’re back with a new account because they toggled their VPN. You block an IP, and they switch to a Tor exit node.
In this tutorial, I’ll show you how to detect non‑residential IPs (VPNs, proxies, and hosting centers) in your Node.js application so you can block them—or at least challenge them with a CAPTCHA—before they touch your database.
The Goal
We want a middleware function in Express that looks like this:
app.use((req, res, next) => {
if (isHighRisk(req.ip)) {
return res.status(403).send("VPNs are not allowed.");
}
next();
});
Here is how to build it.
Method 1: The “Hard” Way (Self‑Hosted Lists)
Step 1: Get the Data
- Tor Exit Nodes – The Tor project publishes a list of exit addresses.
- Cloud Ranges – AWS and Google publish their IP ranges in massive JSON files.
You’ll need to download these files (e.g., tor-exit-nodes.txt, aws-ip-ranges.json) and keep them up to date.
Step 2: The Code
const fs = require('fs');
const ipRangeCheck = require('ip-range-check'); // npm install ip-range-check
// Load the massive lists into memory (careful with RAM!)
const torNodes = fs.readFileSync('tor-exit-nodes.txt', 'utf8')
.split('\n')
.filter(Boolean);
const awsRanges = JSON.parse(
fs.readFileSync('aws-ip-ranges.json', 'utf8')
).prefixes.map(p => p.ip_prefix);
function isHighRisk(userIp) {
// Check if IP is in the Tor list
if (torNodes.includes(userIp)) return true;
// Check if IP is in a Cloud Range (CPU intensive)
if (ipRangeCheck(userIp, awsRanges)) return true;
return false;
}
The Problem with Method 1
- Stale data – VPN providers rotate IPs daily. Without hourly updates you’ll miss many attacks.
- Memory hog – Loading millions of IPs into Node.js memory can crash your server.
- False positives – Distinguishing a legitimate data‑center IP from a malicious VPN can be tricky.
Method 2: The “Easy” Way (Live API Lookup)
Step 1: Get a Free API Key
Grab a free key from the provider’s website (no credit card required).
Step 2: The Middleware
The API returns a trustScore (0‑100) along with flags for Tor and VPN.
const axios = require('axios');
async function checkRiskScore(req, res, next) {
const userIp = req.ip;
try {
const response = await axios.get('https://candycorndb.com/api/public/ip-score', {
params: { ip: userIp }
});
const { score, isTor, isVPN } = response.data;
// BLOCK if it's a confirmed Tor node or very high risk
if (isTor || score > 85) {
return res.status(403).json({ error: 'Anonymizers not allowed.' });
}
// CHALLENGE if it's suspicious (e.g., DigitalOcean droplet)
if (score >= 50) {
// Insert CAPTCHA logic here…
console.log(`Suspicious traffic from ${userIp}`);
}
next();
} catch (err) {
// Fail open: if the API is down, let the user in so you don’t block real people
next();
}
}
// Apply to your sensitive routes
app.post('/api/signup', checkRiskScore, (req, res) => {
res.send("Account created!");
});
Why This Is Better
- Just‑in‑time scanning – If the API hasn’t seen the IP before, it scans open ports and ISP data in < 500 ms, so you never get “unknown.”
- No maintenance – No need to download daily CSV dumps.
- Saves RAM – Your Node server handles logic, not massive IP storage.
Summary
Blocking bad IPs is an arms race. If you’re building a small hobby project, Method 1 is a fun learning exercise. For production apps, offloading risk detection to a dedicated API (Method 2) is usually cheaper than the time you’ll spend unbanning spam accounts.
Feel free to ask questions about IP‑filtering logic! 😅