A cross-platform VPN leak auditor that proves whether your tunnel actually does what your provider claims. Runs five independent checks (DNS, IPv6, routing, WebRTC, killswitch) and emits a structured, machine-readable report with plain-English error codes mapped to concrete fixes.
Most consumer VPNs claim to block DNS leaks, tunnel IPv6, and ship a working killswitch. Many of them silently don't. leakcheck proves it one way or the other in about thirty seconds, no root required.
═══ leakcheck report ═══
baseline (assumed VPN exit):
IPv4: 185.213.155.74
[PASS ] DNS leak [DNS-PASS] DNS looks clean.
[PASS ] IPv6 leak [V6-PASS] No IPv6 leak.
[FAIL ] Routing leak [RT-002] Some destinations are routed AROUND the VPN.
why it matters: Most traffic goes through the tunnel, but a 'split-tunnel'
rule is carving out specific addresses so they bypass it.
what to do: Open your VPN client and check for a split-tunnel feature; disable
it. If you didn't configure it yourself, malware may have set it.
· exceptions: 1.2.3.4/32 → Wi-Fi
[PASS ] WebRTC leak [WRTC-PASS] WebRTC isn't leaking.
[PASS ] Killswitch / disconnect leak [KS-PASS] Killswitch held during the disconnect.
✗ 1 leak(s) detected, 0 errored, 4/5 pass, 0 skipped
Every finding carries a short error code, a one-sentence headline a non-expert can act on, and the specific fix.
| Audience | Best for | How |
|---|---|---|
| End users (Windows) | Non-technical users. Double-click installer, Start Menu shortcut, Desktop icon, clean uninstall. | Download leakcheck-setup-X.Y.Z.exe from the Releases page and run it. |
| CLI / CI | Servers, scripting, automated security gates. | pip install leakcheck |
| Desktop GUI (cross-platform) | macOS / Linux desktops wanting the visual app. | pip install leakcheck[gui] → leakcheck-gui |
All five checks work out of the box. No browser bundles, no playwright install, no apt install of system packages.
CLI:
leakcheck # run all five checks against the active connection
leakcheck --json # structured output for CI / automation
leakcheck --check dns,routing # subset
leakcheck --skip disconnect # skip the interactive killswitch test
leakcheck --vpn-iface "MyCustomVPN" # tell leakcheck what your tunnel iface is called
leakcheck --expected-resolver 10.8.0.1 # flag a DNS leak if the resolver differs
leakcheck --non-interactive # CI-friendly; no promptsExit codes: 0 clean, 1 at least one leak, 2 at least one inconclusive (errored) check with no confirmed leaks.
GUI:
leakcheck-guiOne-click audit, live per-check progress, JSON export, copy-output button. Settings panel pins expected IPs/resolver/interface name when you want to be strict.
Every finding maps to a code. Codes are stable and indexable, suited for runbooks, alerting, and triage.
| Code | Meaning |
|---|---|
DNS-001 |
Resolver is your consumer ISP (Comcast / Cox / AT&T / Verizon / CenturyLink / Charter / Spectrum / ...). Your ISP can see every website you visit. |
DNS-002 |
Resolver is unrecognized and not co-located with the VPN. Whoever runs it sees your queries. |
DNS-003 |
All three resolver probes failed; check is inconclusive. |
DNS-PASS |
Resolver is a known privacy provider (Cloudflare / Google / Quad9 / OpenDNS / AdGuard / ControlD / NextDNS), or lives inside the tunnel. |
| Code | Meaning |
|---|---|
RT-001 |
Default route bypasses the VPN entirely. All traffic leaks. |
RT-002 |
Default route OK, but split-tunnel rules carve specific destinations around the VPN. |
RT-003 |
No default route exists; the host has no internet path. |
RT-PASS |
Default route correctly tunneled, no leaking exceptions. |
| Code | Meaning |
|---|---|
V6-001 |
Real IPv6 address visible despite VPN. (Many VPNs are IPv4-only by default; v6 traffic escapes.) |
V6-PASS |
Host has no v6 connectivity, or v6 is properly tunneled. |
| Code | Meaning |
|---|---|
WRTC-001 |
STUN reflexive IPv4 ≠ VPN exit. A web page using RTCPeerConnection can read your real public IPv4. |
WRTC-002 |
Real IPv6 exposed via WebRTC (host-global v6 address visible). |
WRTC-PASS |
Reflexive IPs match the VPN exit; nothing useful for a browser to leak. |
| Code | Meaning |
|---|---|
KS-001 |
When the VPN dropped, traffic kept flowing through the underlying ISP. Killswitch failed. |
KS-INCONCLUSIVE |
Test ran but no actual disconnect was observed. Re-run and disconnect mid-window. |
KS-PASS |
During the observed drop, traffic was blocked. Killswitch held. |
Full lookup table with "why it matters" and "what to do" lines lives in src/leakcheck/codes.py.
Issues three independent DNS probes that ask third parties to report which resolver is making queries on the host's behalf:
o-o.myaddr.l.google.comTXT (Google)whoami.cloudflareTXT via1.0.0.1(Cloudflare)whoami.akamai.netA (Akamai)
Majority-votes the answer. Classifies the egress resolver against (a) a user-supplied --expected-resolver, (b) an allowlist of known privacy providers (Cloudflare / Google / Quad9 / OpenDNS / AdGuard / ControlD / NextDNS), (c) a deny-list of consumer-ISP IP prefixes, or (d) co-location with the VPN exit on the same /16. Anything outside those classes is flagged.
Reads the OS routing table directly:
- Linux —
ip -4 route show - macOS —
netstat -nr -f inet - Windows —
Get-NetRoute(PowerShell, gives clean interface aliases) with aroute print -4fallback
A correctly tunneled host has its 0.0.0.0/0 default route pointing at the VPN's interface. leakcheck decides whether that interface IS the tunnel through three signals:
- Explicit override (
--vpn-iface NAME) — the user named the tunnel. - Name match — substring lookup against a list covering Mullvad, NordVPN, ProtonVPN, ExpressVPN, Surfshark, CyberGhost, TunnelBear, Windscribe, IVPN, PIA, AtlasVPN, Hide.me, Tailscale, plus generic
vpn/tunnel/wireguard/openvpnsubstrings. - Structural signature — an interface with a
/32host route to an internet-routable IP via a different interface (the WireGuard / OpenVPN / IPSec bootstrap underlay pattern). Catches any custom tunnel by shape, not name.
Once the tunnel is identified, the routing table is scanned for split-tunnel exceptions: routes to internet-routable destinations (RFC1918, link-local, multicast, loopback, broadcast, and CGNAT explicitly excluded) that go via a non-tunnel interface. The VPN's own underlay route (a /32 to the peer endpoint co-located with the exit IP) is also excluded automatically.
Forces AF_INET6-only HTTP requests to three echo endpoints (api64.ipify.org, ipv6.icanhazip.com, v6.ident.me). If any response arrives and the VPN was declared IPv4-only (default assumption), the returned IPv6 is the host's real address, escaping the tunnel. A host with no IPv6 connectivity at all passes trivially.
Pure-Python STUN (RFC 5389) implementation — no headless browser, no Chromium download. Sends real binding requests over UDP to multiple public STUN servers (Google, Cloudflare, Nextcloud) over IPv4 and IPv6 separately, parses the XOR-MAPPED-ADDRESS attribute from each response. That's exactly the reflexive address a browser's RTCPeerConnection would expose. Then enumerates the host's own global IPv6 addresses — the other major WebRTC leak vector that browsers happily expose to any page.
Any reflexive address that doesn't match the VPN exit, or any host-side global IPv6 when the VPN is supposed to be tunneling everything, is flagged.
Polls a public-IP echo service every 500 ms for a configurable window (default 30 s). The operator disconnects the VPN mid-window. Three outcomes:
- PASS — some samples come back silent (VPN was down) and zero samples report a non-VPN IP. The killswitch held the connection closed.
- FAIL — at least one sample during the window returned a non-VPN IP. Traffic leaked through the gap.
- INCONCLUSIVE — every sample reported the VPN IP. Either the killswitch is perfect and the user actually never disconnected; can't distinguish from this side. Re-run.
--non-interactive records the window without a prompt, suitable for CI.
┌──────────┐
│ baseline │ ← collect public IPv4/IPv6 via 3 redundant echo services
└────┬─────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ five independent checks, each emits a CheckResult │
│ {name, title, status, code, summary, details, time} │
│ │
│ ┌────┐ ┌──────┐ ┌─────────┐ ┌────────┐ ┌───────────┐│
│ │DNS │ │ IPv6 │ │ Routing │ │ WebRTC │ │ Killswitch││
│ └────┘ └──────┘ └─────────┘ └────────┘ └───────────┘│
└────────────────────────┬─────────────────────────────┘
│
┌─────────────┴──────────────┐
▼ ▼
codes.py lookup JSON / terminal renderer
(code → headline, (color glyphs, error code,
meaning, fix) headline, what-to-do, details)
Every check is a pure function. Network-touching code always probes three independent sources and majority-votes. Each check fails gracefully when prerequisites are missing — no exceptions reach the top level.
# Python 3.9+ on PATH
pip install -e ".[gui,build]"
python build_exe.py # → dist\leakcheck-gui.exe (~38 MB, single file)
# Compile the install wizard (requires Inno Setup 6+ from https://jrsoftware.org/isdl.php)
iscc installer\leakcheck.iss # → installer\output\leakcheck-setup-0.1.0.exeThe installer:
- Installs to
%ProgramFiles%\leakcheck\(requests admin) - Adds a Start Menu shortcut (always) and Desktop shortcut (opt-in)
- Optional: prepend the install dir to system
PATHsoleakcheck-guiruns from any terminal (opt-in) - Registers a clean uninstaller in Apps & Features
- Shows the MIT LICENSE page during the wizard
pytest -q24 tests, no network dependencies for the deterministic checks. STUN tests use a synthetic packet round-trip; one live-network test asserts the WebRTC check completes (PASS or ERROR — never SKIP).
- Not a VPN. It audits a VPN you've already set up.
- Not a packet capture tool. No root required for the detection logic.
- Not magic. A malicious VPN provider forwarding your DNS through its own infrastructure can produce a clean report — the resolver IP looks fine because, technically, it is fine. Tool detects routing & leak shape, not trust.
MIT — see LICENSE.
Killian Miller — killianmiller6@gmail.com — github.com/KillianM00