TunnelGuard
Choose the websites that skip your VPN. Everything else stays on it.
A small Mac app that lives in the menu bar. Add a website, press Start, and traffic to that site uses your normal connection while the rest of your Mac stays on the VPN. It works with the routing macOS already has, so there is no kernel extension or network extension to install.
Free and open source under the GPL, published under BlackSwan Development, my own label. Needs macOS 13 Ventura or later and an administrator password. The download is not notarised yet, so the first launch needs right-click, Open.
TunnelGuard
v2.7.0
3 active
Exclusion Rules
4 domains configured · gateway 192.168.1.1
-
intranet.example.com
office
192.0.2.14
-
bank.example
banking
198.51.100.7
198.51.100.8 -
shop.example.net
local marketplace
203.0.113.25
-
cdn.example.org
CDN
203.0.113.80
Activity log
- sudo route -n add 192.0.2.14 192.168.1.1
- sudo route -n add 198.51.100.7 192.168.1.1
- sudo route -n add 198.51.100.8 192.168.1.1
- sudo route -n add 203.0.113.25 192.168.1.1
What you are looking at
- A rule is a website. TunnelGuard looks up every address behind it, and you can add your own.
- Press Start and traffic to those addresses goes through your own router instead of the VPN.
- Pause a rule without deleting it.
- Change networks and it finds the new router and applies the rules again.
Try it: switch a rule off, or press Stop. The log shows the command the app runs.
Illustrative. Example domains and documentation-only addresses; the layout follows the real app.
// in-plain-words
In plain words
When a VPN is on, all of your traffic goes through it. Some services work fine without it, and some break because of it.
Without it
Everything goes through the VPN, including the services that break because of it. The only way out is to switch the VPN off.
With TunnelGuard
The websites you list use your normal connection. Everything else stays on the VPN, and the VPN stays on.
What I keep off the tunnel
Banks, local marketplaces and a few CDNs: the services that have to stay on a normal connection.
It asks before it acts
Changing how your Mac routes traffic needs your administrator password, or a one-time grant that covers the route command and nothing else.
It can start with your Mac
The routes it adds last until a restart. Set it to open at login and it applies your rules again on its own.
// getting-started
Getting started
It needs macOS 13 Ventura or later, and an administrator account.
Download
Get
TunnelGuard.dmgfrom the latest release on GitHub.Drag it into Applications
Open the file and drag the TunnelGuard icon onto the Applications folder beside it.
Open it the first time with right-click
The build is not notarised yet, so macOS warns about an unidentified developer. Right-click the app, choose Open, then Open again. If macOS blocks it outright, use Open Anyway in System Settings, under Privacy and Security.
Grant admin access, if you like
In Settings, Admin Access, Grant Access lets it run the route command without asking for your password each time. It is optional and limited to that command.
Add a website and press Start
Add Domain (⌘N), type the address, then Start Whitelisting (⌘⇧R). Pasted links are cleaned down to the domain.
Rules can be exported and imported as JSON, for a backup or a second Mac. TunnelGuard changes your Mac's routing table: never use it to get around security controls you are required to follow.
// why-i-built-it
Why I built it
macOS gives you no way to exclude a specific domain from a VPN tunnel. TunnelGuard does it with a native menu bar app, bypass routes, and optionally the hosts file.
Context
This started as my own problem. Working from a VPN-heavy environment, some services have to stay off the tunnel: banks, local marketplaces, a few CDNs. Apple removed the kernel extension APIs that VPN clients used for split tunneling in Big Sur, and most clients still do not offer it.
The routing approach came from a post on dev.to by vavilov2212, which is credited in the repository.
You can do this by hand with the route command. But the rules vanish on every network change, DNS still resolves through the VPN so the bypass silently fails, and nothing tells you whether it is currently working.
Constraints
- Admin rights, honestly
- It changes system routing state and needs administrator rights, so it has to be clear about what it is about to do.
- A shared hosts file
- It cannot fight with other tools that write to the hosts file.
- A Mac that moves
- It has to survive the machine moving between networks.
// how-it-works
How it works
One click applies every rule, one click stops. Underneath, it is the same three commands you would type by hand, done carefully.
Every address, not just one
Domains are resolved with dig and nslookup, including domains with several addresses. Manual addresses can sit beside the resolved ones.
The right way out
The local default gateway is detected and validated, with a manual override for when detection returns something that is not an address, such as link#28.
DNS off the tunnel too
Resolved addresses can go into the hosts file so lookups skip the VPN's DNS, in a block format compatible with Local and WordPress Studio, so the tools do not overwrite each other. The file is backed up before every change.
It survives moving
It reads VPN DNS state through scutil, keeps route state by parsing netstat, and re-detects the gateway and reapplies when the network changes.
Pause, not delete
A rule can be switched off and on again without losing its domain, notes or manual addresses.
Narrow permissions
Every input to a shell command is validated, Hardened Runtime is on, and the admin grant is limited to specific commands and paths.
# 1. Find every address behind the domain dig +short example.com A # 2. Send those addresses through your own router, not the VPN sudo route -n add <address> <local-gateway> # 3. Pause or remove the rule sudo route -n delete <address>
// releases
Version 2 was about correctness
Four public releases in March 2026, each with a DMG attached.
Users reported the menu bar freezing. It turned out to be shell commands running on the main thread, and they all moved off it. Start-up diagnostics moved to a background thread. The menu bar icon is now driven by a Combine publisher, so its state can never drift from reality. A duplicate window on dock click and a delete dialog that switched tabs were both fixed.
| Version | Released | What changed |
|---|---|---|
| 1.9.0 | 6 March 2026 | Passwordless route commands through a limited admin grant, gateway validation, inline editing, one copy running at a time. |
| 1.9.5 | 7 March 2026 | The menu bar icon and the sidebar count follow the state of the rules. |
| 2.0 | 7 March 2026 | Correctness: shell commands off the main thread, background start-up checks, a Combine-driven menu bar icon, window and dialog fixes. |
| 2.7.0 | 26 March 2026 | Everything since 2.0: validation on every shell input, Hardened Runtime, a narrower admin grant, network change detection, JSON import and export, keyboard shortcuts, and a hosts file that stays current while rules run. |
Released with a documentation site and a written walkthrough. A small audience by design: it solves one specific problem for people who have it.
// hindsight
Hindsight
Two things I would do from the first release.
// products/
Other things I keep running
Plugins, apps and internal systems. Each one is shipped, documented, and still looked after.
- Ultimate InvoiceInvoices, slips and labels for WooCommerce
- Ultimate Integration for TelegramEvery site event, in the chat you already read
- Easy ReceiptBank transfer receipts, against the right order
- Advanced Profile SolutionLogin, registration, profiles and the account area
- Q&A CommentsA moderated Q&A your experts actually answer
- ToolkitEvery fix we kept writing twice, written once
- html2elementorA finished design, rebuilt as a real Elementor site
- UptimeA WordPress fleet, watched from one channel
- Client HubA client's whole account, inside Telegram
- TunnelGuardDomain-level VPN split tunneling on macOS