One VPN per browser. Give a single browser its own WireGuard exit, with its own cookie jar, and DNS and WebRTC inside the tunnel. The rest of your Mac, and your other VPNs, stay untouched. No root, no kernel extensions.
brew install portdeveloper/tap/perbrowser
perbrowser add sweden ~/Downloads/se-sto-wg-001.conf --browser brave
# → launch "perbrowser - Sweden (Brave)" from SpotlightWorks with any WireGuard .conf that has a PrivateKey and an Endpoint:
Mullvad, Proton, IVPN, self-hosted.
This is not a Mullvad tool.
macOS has no good app-level split tunneling:
- VPN apps tunnel your whole machine. Their split tunneling is absent or exclude-only on macOS, and two VPN network extensions (say, Mullvad plus Tailscale) fight over routes and DNS.
- Proxy extensions leak WebRTC, share your normal browser's cookies, and need a proxy endpoint you still have to build yourself.
- VMs / separate user accounts work, but are heavyweight and awkward.
perbrowser takes the boring path: a userspace WireGuard client (wireproxy) exposes the tunnel as a SOCKS5 proxy on loopback. The tunnel is an ordinary user process making outbound UDP, so your routing table and network interfaces stay untouched. A dedicated browser profile points at it from the first launch, through command-line flags that a website can't undo.
normal apps ─────────────────► default route (Tailscale etc. untouched)
sweden ── wireproxy (launchd) ── SOCKS5 127.0.0.1:1080 ── WireGuard/UDP ─► 🇸🇪
us ── wireproxy (launchd) ── SOCKS5 127.0.0.1:1081 ── WireGuard/UDP ─► 🇺🇸
"perbrowser - Sweden (Brave)".app
→ brave --user-data-dir=…/sweden/profile
--proxy-server=socks5://127.0.0.1:1080
--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE 127.0.0.1"
--force-webrtc-ip-handling-policy=disable_non_proxied_udp
Each instance is fully independent: its own launchd-supervised tunnel
process (restarts on failure and at login), its own port, its own browser
profile, and its own Spotlight-launchable app in ~/Applications.
These three flags do the leak prevention:
| Flag | What it prevents |
|---|---|
--proxy-server=socks5://… |
traffic bypassing the tunnel |
--host-resolver-rules="MAP * ~NOTFOUND , …" |
DNS resolving locally, so names resolve through the tunnel |
--force-webrtc-ip-handling-policy=disable_non_proxied_udp |
WebRTC voice/video exposing your real IP |
brew install portdeveloper/tap/perbrowserOr without Homebrew, because it's a single POSIX shell script:
git clone https://github.com/portdeveloper/perbrowser && cd perbrowser
./perbrowser add …The only binary dependency, wireproxy, is downloaded on first add from its
pinned release and verified against a hardcoded SHA256 before it ever runs.
Requirements: macOS (Apple Silicon or Intel), a Chromium-family browser
(Brave, Chrome, Edge, Chromium), and a WireGuard .conf from your provider
(e.g. Mullvad: account page → WireGuard configuration → download).
perbrowser add <name> <wg.conf> [--browser brave|chrome|edge|chromium]
perbrowser list
perbrowser update-config <name> <new.conf>
perbrowser doctor [name]
perbrowser rm <name> [--keep-profile]
perbrowser version
add builds the whole stack (tunnel daemon, launchd agent, launcher
app), then proves it works by fetching your exit IP through the proxy. If
real traffic doesn't flow, it fails loudly.
list shows every instance with a live tunnel check:
NAME BROWSER PORT AGENT TUNNEL
sweden brave 1080 running 185.65.135.1 SE
us chrome 1081 running 170.62.100.18 US
update-config is for key rotation (e.g. you deleted a device on your
provider's dashboard): swap in the new .conf, restart, re-verify. The
whole swap takes about 15 seconds.
doctor checks each instance end to end: agent loaded, process
running, port listening, file permissions, real traffic flowing, DNS
through the tunnel, launcher flags intact. Safe fixes are applied
automatically; destructive ones are printed for you to run. It exists
because a rotated or deleted WireGuard key still logs handshakes, so
every check ends in real traffic through the proxy.
- Your
.conf(which contains a private key) is storedchmod 600in a700directory.addandupdate-configoffer to delete the source file after copying it; a non-interactive run prints a reminder instead. - wireproxy is version-pinned and SHA256-verified; it is never auto-updated
silently. A bumped pin only takes effect on a fresh install, so delete
~/.local/bin/wireproxyto force a re-download. - The SOCKS5 listeners bind loopback only; nothing on your LAN can use your tunnels.
- The whole tool is one auditable POSIX shell script, short enough to read before you run it.
- It runs entirely as an unprivileged user process and never edits your system network config, so Tailscale and corp VPNs keep working.
- Voice/video calls in the VPN browser won't connect. WebRTC media is UDP, which this path does not carry. The WebRTC flag fails closed by design; it never falls back to your real IP. Everything over TCP works fine, including streaming video.
- If your network blocks WireGuard UDP itself, the tunnel won't come
up (
addwill tell you). You'd need your provider's obfuscation transport, which is out of scope here. - Providers cap WireGuard keys (Mullvad: 5 devices). Each downloaded conf is one device; revoke unused ones on their dashboard.
Firefox? Out of scope for now. Firefox configures proxies through prefs, which needs a different and less tamper-proof mechanism. See CONTRIBUTING.
Linux? Planned, with the same design and systemd user units instead of launchd. Also in CONTRIBUTING.
Can I route other apps through a tunnel? Unofficially, yes: each
instance is an ordinary SOCKS5 proxy on 127.0.0.1:<port>, and anything
that speaks SOCKS5 can use it: curl --proxy socks5h://127.0.0.1:1080 …,
Telegram's proxy setting, and so on. But perbrowser's leak guarantees
(DNS through the tunnel, WebRTC failing closed, isolated state) come from
the browser flags. An app you point at the port yourself may still
resolve DNS outside the tunnel. The guarantees are scoped to the browser,
which is where the flags apply.
Is my normal browser affected? No. The VPN instance uses its own
--user-data-dir; your regular browser, its cookies, and its traffic are
untouched.
How do I verify there are no leaks? In the VPN browser, visit browserleaks.com/ip. IP, DNS and WebRTC should all show the VPN exit, never your ISP.
Why not my VPN app's split tunneling? On macOS it usually doesn't exist, or it can only exclude apps from the tunnel. Either way it can't give the browser an isolated cookie jar.
perbrowser manages instances of wireproxy, which does the actual WireGuard-in-userspace work. This tool wires it to launchd and Chromium correctly, N times.