The best VPN detection is asking the VPN
ASN lists and per-IP feeds both infer. NordVPN, Mullvad and Surfshark publish their exit nodes at public endpoints their own apps call — which is more authoritative than any heuristic can be.
VPN detection usually rests on inference: this address belongs to a network that hosts VPNs, or a third-party list says it is one. Both fail identically against a provider that rents servers from an ordinary hosting company.
The measurement
We sampled 15 real NordVPN exit nodes from NordVPN's own server list. Twelve scored as VPN. Three came back at 30 — allow. Their hosts, PacketHub and HostRoyale, were not on anyone's list, so those exits looked like ordinary hosting.
NordVPN announces from no ASN of its own. We caught it only when the underlying host happened to be listed, which is luck rather than design.
Ask the provider instead
Several major VPNs publish their complete server lists at public endpoints — the same ones their own apps call to pick a server. No scraping, no subscription, no credentials.
- NordVPN — 8,959 exit IPs, each with country and city.
- Mullvad — 575 relays with country.
- Surfshark — cluster hostnames, resolved to addresses.
- ProtonVPN — requires an authenticated session, so it stays covered by its ASNs.
This is the most authoritative source that can exist for 'is this a NordVPN exit', because NordVPN is the one deciding.
It fixes geolocation too
A GeoIP database resolves a VPN range to wherever the operator is registered. Proton's Amsterdam exit reads as Switzerland, because Proton AG is Swiss. The user is in the Netherlands; the database says Switzerland; both are 'correct' about different things and only one is useful.
For a known exit we now prefer the operator's declared location. Those three NordVPN addresses went from an unhelpful company-registration country to Seattle, Salt Lake City and New York — where the servers actually are.
Say which VPN, or say nothing
Responses now carry a vpn_provider field naming the operator when its own list confirms it, and null when is_vpn was merely inferred. Naming a provider we guessed at would be worse than staying quiet, and null here means 'not confirmed by an operator list' — never 'not a VPN'.
What this does not solve
ExpressVPN, Private Internet Access, CyberGhost and hide.me publish nothing public. They remain inference-only, and pretending otherwise would be the same overclaim we just removed.