A tiny, menu-bar-only Proton VPN client for macOS that does one thing: put you on the fastest server that isn't overloaded, and move you to another one when it fills up or stops answering.
Unofficial. LeanVPN is an independent project. It is not made, endorsed or supported by Proton AG. "Proton" and "Proton VPN" are trademarks of Proton AG. You need your own Proton account. Use it at your own risk (see Disclaimer).
Status: alpha. It works daily on the author's Mac, but it has been tested on exactly one machine. See Status and testing before relying on it.
Website: https://zephyrsai.github.io/LeanVPN/
The official app is a 233 MB, full-featured client. My real problem was simpler: servers got congested and the connection kept dropping, so I was constantly switching servers by hand. LeanVPN picks a good server by itself, keeps checking it, and switches before you notice. It has no window, no settings, no country list. Just a shield in the menu bar.
Measured on an M-series MacBook Pro, macOS 26 (memory = Activity Monitor's "Memory" column):
| LeanVPN | Proton VPN 6.5.1 | |
|---|---|---|
| On disk | 6.1 MB (app 124 KB + helper 6 MB) | 233 MB |
| Menu-bar app memory | 11–15 MB | 168 MB |
| Background helper | Only while connected; exits 30 s after you disconnect | Always running |
| Idle CPU | ~0% (one cheap counter check every 10 s) | Noticeable over a day |
| Picking a server | ~0.3 s (all candidates probed in parallel) | n/a |
- An Apple silicon Mac running macOS 13 Ventura or later
- A Proton account. Free works (free servers only); paid plans can use every non-Secure-Core server.
- Apple's Command Line Tools:
xcode-select --install - Go:
brew install go
No Xcode, no Apple developer account, no App Store.
git clone https://github.com/ZephyrSai/LeanVPN.git
cd LeanVPN
make installmake install builds everything (about a minute the first time), then asks for your admin
password once. That's needed to install the background helper that creates the VPN
interface. The installer:
- copies
LeanVPN.appto/Applicationsand starts it (it also starts at login); - installs the helper to
/Library/PrivilegedHelperTools/com.leanvpn.helperand registers it with launchd (/Library/LaunchDaemons/com.leanvpn.helper.plist). launchd only starts it when the app asks, and it quits again when the VPN is off.
Quit the official Proton VPN app first. Two VPNs at once fight over routes and DNS.
LeanVPN never sees your password. It uses Proton's "enter a code on the web" sign-in, the same flow the Proton VPN Apple TV app uses.
- Click the shield in the menu bar, then Sign In…
- Your browser opens
account.protonvpn.com/appletvand the 8-character code is already on your clipboard. Sign in to Proton there if asked, paste the code and approve. - That's it. LeanVPN connects on its own a few seconds later.
If that page doesn't load (some networks block protonvpn.com), open the same address on your
phone over mobile data and type the code shown in the LeanVPN menu. The code is valid for about
10 minutes.
| Icon | Meaning |
|---|---|
| Shield with a check | Connected. Hover for server, city, load and latency. |
| Half-filled shield | Connecting or switching servers |
Shield with ! |
Waiting for network, or the last attempt failed (it retries by itself) |
| Plain shield | Disconnected |
| Shield with a slash | Signed out |
The menu only ever shows what makes sense right now: Connect, Disconnect, Switch Server (jump to the next-best server), Retry Now, Sign Out, and Quit (which also turns the VPN off).
If you were connected when you shut down, LeanVPN reconnects after a restart.
- Shortlist. Out of Proton's server list (filtered to your plan, skipping Secure Core, Tor, restricted servers and servers in maintenance): the 10 best by Proton's own score (distance + load from where you are), plus the 6 least-loaded of the 60 nearest.
- Measure. Real latency to every candidate at once: a TCP connect to each server, sent over your normal connection, never the tunnel. About 0.3 s in total.
- Score. Lowest
latency_ms + 300 × (load/100)²wins. Load hardly matters when it's low, but it dominates as a server nears saturation. A 40 ms server at 90% load loses to a 90 ms server at 30% load. - Verify. A WireGuard handshake isn't proof a server works (overloaded servers often complete one), so LeanVPN also needs an answer from Proton's in-tunnel DNS resolver before it calls itself connected. If there's no answer, it tries the next server, up to 4, rotating through Proton's alternative WireGuard ports in case your network blocks the default one.
Every 10 seconds the helper reads two counters from WireGuard. That costs almost nothing.
- Stalled (sending but receiving nothing for 20 s), or no handshake for 200 s: it asks the in-tunnel resolver. No answer twice means it switches to the next-best server, and avoids the dead one for 30 minutes.
- Every 15 minutes: it refreshes server loads. If yours is 85% full or more and one at least 20 points lighter exists, it moves. A server entering maintenance is left immediately.
- Network changes (Wi-Fi ↔ Ethernet, sleep/wake, DHCP renewals): it re-pins the server route, rebinds the socket and re-applies DNS.
- Certificates and sign-in tokens are renewed automatically. If Proton's API can't be reached, a still-valid certificate and the cached server list are enough to connect.
- If switching fails, the tunnel is taken down rather than left routing your traffic nowhere, and LeanVPN keeps retrying (immediately when your network comes back).
- If the helper ever crashes, launchd restarts it; it restores your DNS and reconnects.
- No password ever passes through LeanVPN. Session tokens and the WireGuard key are stored in
/Library/Application Support/LeanVPN/, readable by root only. - The helper only accepts commands from your user account (and root).
- LeanVPN only talks to
vpn-api.proton.meand the VPN servers. There's no telemetry, crash reporting, analytics or update check. - While connected, all IPv4 traffic goes through the tunnel and DNS goes to Proton's resolver (10.2.0.1). IPv6 is blocked: the tunnel only carries IPv4, so IPv6 would otherwise leak around it.
- There is no kill switch. If the tunnel goes down, your traffic goes out normally until LeanVPN reconnects (usually within seconds). Use the official app if you need a hard kill switch.
- It identifies itself as Proton's Apple TV app. Proton's code-entry page only approves codes for that app, so LeanVPN uses that app's identifier to sign in.
- Restrictive networks. Some networks silently drop the TLS handshake to Proton's API. LeanVPN sends the first handshake packet in two pieces, which gets it through.
Live logs:
/usr/bin/log stream --predicate 'subsystem == "com.leanvpn"' --level infoPast hour (connects, switches, failures):
/usr/bin/log show --last 1h --predicate 'subsystem == "com.leanvpn"'| Problem | Fix |
|---|---|
| Menu says "Helper not installed" | Run make install again |
| Sign-in page is blank | Your network blocks protonvpn.com. Open the page on your phone over mobile data. |
| "Sign-in failed" | Try Sign In… again. If it keeps failing, check the logs. |
| No internet after a crash or force-quit | Reconnect, or run sudo /Library/PrivilegedHelperTools/com.leanvpn.helper --cleanup to restore your DNS |
| Keeps switching servers | Look in the logs for leaving … lines, which give the reason each time |
| Conflicts with another VPN | Disconnect the other VPN (the official Proton app, corporate VPNs) first |
make uninstallThis disconnects, restores your DNS, and removes the app, the helper, the launchd entry and the
saved sign-in. Run it from the cloned folder (or run sudo Support/uninstall.sh).
Alpha. What has actually been verified, on one Apple silicon Mac with a Proton Free account:
- Sign-in with the code flow, connecting, server ranking by location, failover when a server doesn't carry traffic, and reconnecting automatically after the helper restarts.
- IPv4 routing through the tunnel, the server route going via the real gateway, DNS through Proton, IPv6 blocked, and the exit IP being Proton's. About 40 Mbit/s through a free server.
- Footprint and idle CPU as in the table above.
Not yet tested enough: long sessions (days), sleep/wake cycles, switching between Wi-Fi,
Ethernet and phone tethering mid-session, captive portals, certificate and token renewal over
long sessions, paid plans, macOS 13–15, and other people's Macs and networks. Bug reports are
very welcome, especially with the output of the /usr/bin/log show command above.
Kill switch, country or server picker, Secure Core, NetShield, split tunneling, port forwarding, profiles, notifications, settings, auto-update, Intel builds. Keeping it small is the point.
| Path | What |
|---|---|
Sources/App/main.swift |
The menu-bar app (AppKit, about 190 lines) |
Sources/Helper/Engine.swift |
Session, server choice, health checks and failover |
Sources/Helper/Tunnel.swift |
utun interface, routes, DNS, network watcher |
Sources/Helper/Servers.swift |
Server list parsing, shortlist and latency probe |
Sources/Helper/API.swift |
The handful of Proton API calls |
Sources/Helper/bridge.h |
Small C helpers: utun socket, TCP and DNS probes |
wg/ |
wireguard-go (vendored) built as a static library, plus the HTTPS client and its tests |
Support/ |
Installer, uninstaller, launchd plist, Info.plist |
docs/ |
The website (GitHub Pages) |
Build without installing: make (outputs go to build/). Run the tests with make test.
LeanVPN is provided as is, without warranty of any kind. It is not affiliated with Proton AG. It uses Proton's API the way Proton's own apps do, including the Apple TV app's identifier for sign-in. Proton could change or block that at any time, and using an unofficial client may be against Proton's terms of service. You are responsible for how you use it.
LeanVPN is free software under the GNU General Public License v3.0.
- WireGuard engine: wireguard-go (MIT), with a bridge
trimmed from wireguard-apple's
api-apple.go(MIT). The vendored modules keep their own licenses underwg/vendor/. - API behaviour follows Proton's open-source apps (ProtonVPN/ios-mac-app, GPLv3).