Disclosure: I run BLST VPN Navi, a Japanese guide that answers worries about being watched, hacked, or cut off online, using official sources and daily price data. Some links on the site are affiliate links. This post is about how the test works.

My site had a "VPN leak test". While auditing it, I found that it gave every visitor 100 points, whether or not they used a VPN.

What was broken

  • The result function started with let score = 100 and never subtracted anything.
  • All three checks had empty failure branches.
  • When a request to an outside service threw, the code still stored status: 'success'.
  • The "DNS leak" check only compared the public IP returned by two services. It never looked at DNS.

So the test always said "safe". A security tool that gives false reassurance is worse than no tool: people keep using a leaking connection because they were told it's fine.

Rules for the rebuild

  1. No score. A score needs weights (how much worse is a WebRTC leak than an IPv6 leak?). I can't justify those weights, so any score would be made up. The test now says only: leaking, not leaking, or can't tell.
  2. If the browser can't measure it, say so. Never mark it as passed.
  3. If an outside service is unreachable, say "can't tell". Never "safe".
  4. Let people re-test right after fixing, and show what changed.

The baseline IP comes from my own server

First, get the IP the site sees. Everything is compared against it.

I used to call api.ipify.org directly from the browser. It turned out to be on standard ad-blocker lists. Readers who care about privacy are the ones running ad blockers — so the people I most wanted to reach were the ones whose test broke.

Now the baseline comes from a REST endpoint on my own domain, which blockers don't touch:

register_rest_route('blst/v1', '/whoami', [ 'methods' => 'GET', 'callback' => 'blst_whoami', 'permission_callback' => '__return_true', ]); function blst_whoami() { $ip = blst_whoami_ip(); $r = ['ip' => $ip] + blst_whoami_geo($ip); // country, city, network owner nocache_headers(); // per-visitor value: never cache return rest_ensure_response($r); } 

Country, city, and network owner are looked up server-side via an outside service (ipapi.co) and cached for one day.

Check 1: WebRTC

To find a direct route between browsers, WebRTC asks a STUN server for your public address. With some VPNs, that request goes around the tunnel and exposes your real IP.

var pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }); pc.createDataChannel(''); pc.createOffer().then(function (o) { return pc.setLocalDescription(o); }); pc.onicecandidate = function (e) { if (!e || !e.candidate || !e.candidate.candidate) return finish(); var m = e.candidate.candidate.match(/([0-9]{1,3}\.){3}[0-9]{1,3}|(?:[a-f0-9]{0,4}:){2,}[a-f0-9]{0,4}/i); if (!m) return; var ip = m[0]; // Private, link-local and CGNAT addresses are not leaks if (/^(10\.|127\.|169\.254\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.)/.test(ip)) return; if (/^(fe80:|fc|fd|::1$)/i.test(ip)) return; if (found.indexOf(ip) < 0) found.push(ip); }; 

A leak means: a public address that differs from the one the server sees. Home LAN addresses don't count, and modern browsers usually hide them behind random .local names anyway.

Check 2: IPv6

If a VPN only tunnels IPv4, IPv6 traffic can flow outside it. I ask a dual-stack service (api64.ipify.org) which address I'm using:

fetch('https://api64.ipify.org?format=json', { cache: 'no-store' }) .then(function (r) { return r.json(); }) .then(function (d) { var ip = (d && d.ip) || ''; if (ip.indexOf(':') < 0) return { state: 'ok' }; // IPv4 only if (ip === serverIp) return { state: 'ok' }; // IPv6, same route return { state: 'ng', ips: [ip] }; // IPv6 outside the tunnel }) .catch(function () { return { state: 'na' }; // unreachable: "can't tell", never "safe" }); 

That catch is the biggest difference from the old version, which reported success here.

If WebRTC and IPv6 find the same address (common when the device has a public IPv6), the result says it's one cause, not two.

Check 3: location vs. device settings

A VPN changes your IP, not your device. If you connect from abroad but your time zone is Asia/Tokyo and your language is ja, a site can infer you're in Japan using a foreign server. The test says plainly that a VPN can't hide this.

DNS leaks: "can't measure"

You can only see which DNS server resolved a name from the side that receives the query. JavaScript in a browser can't see it, and my site doesn't run that infrastructure. So the DNS row says the test can't be done in the browser and links to a dedicated site — instead of measuring something else and calling it "DNS: OK".

Results

State Shown as
Leaking red, with the address and the route
Not leaking green
Can't tell gray, with the reason

Even when nothing is found, it does not say "safe":

Within what this test can check, no leak was found. This does not mean you are safe.

It also shows the network owner. If it's your ISP rather than a VPN company, you're not on a VPN at all, and your IP is visible to every site you visit.

Re-test after fixing

The previous IP is kept in sessionStorage. On re-test, the page shows whether it changed — or warns that it didn't and the setting may not have applied. Results stay in the browser.

Takeaways

  • A security tool must not hand out unfounded scores or "you're safe".
  • If you can't justify the weights, don't make a score.
  • Say "can't measure" and "can't tell" out loud.
  • Serve the baseline IP from your own domain so ad blockers don't break it.

The rebuilt test (Japanese UI): https://www.blstweb.jp/network/vpn/leak-check/