A native macOS utility built with Swift and SwiftUI for advanced users who want traffic to specific hostnames to follow the system’s current IPv4 default gateway instead of a VPN’s default route—when that routing table layout makes sense for your network. The app resolves domains, then adds or removes per-host IPv4 routes via a bundled bypass_routes.sh script (wrapped for privilege elevation).
- macOS 13.0 (Ventura) or newer
- Xcode 15 or newer (recommended)
- Administrator authorization when applying or removing routes (see Permissions and security)
- Open
VPNBypass.xcodeprojin Xcode. - Select the VPNBypass scheme and My Mac.
- Product → Run (⌘R).
The project also defines a VPNBypassRouteHelper target (privileged helper executable). It is part of the optional XPC path described below; the default build path does not require a separate run configuration for day-to-day UI development.
On launch, the app ensures:
~/Library/Application Support/com.vpnbypass.VPNBypass/state.json— domains, route bookkeeping, and settings- A copy of
bypass_routes.shin that folder (refreshed when the bundled script’s size changes)
Bundled hostnames from VPNBypass/Resources/default_domains.json are merged on every launch: new preset entries are added if missing; existing user domains and settings are not removed.
- Refresh resolves each hostname to IPv4 using system DNS (
getaddrinfowithAF_INETinResolverService). - Apply VPN Bypass collects IPv4 addresses for domains included in routing (
DomainRecord.isIncludedInRoutes), reads the default IPv4 gateway (script actiongateway:route -n get default), then runsapply, which for each IP runsroute -n delete -host(ignored if missing) androute -n add -host <ip> <gateway>. - Turn Off Bypass runs
removeto delete those host routes using the IPv4 list saved inRouteApplicationState. - The UI shows resolution status, addresses, whether the last apply appears to cover each domain, timestamps, activity logs, and settings (login item, auto-apply on VPN heuristic).
The codebase is organized into four conceptual layers plus shared routing infrastructure.
| Layer | Role |
|---|---|
| Views | SwiftUI screens and components (ManagerView and Views/Manager/*, DomainRowView, LogsPanelView). The app entry uses ManagerView; MainView remains in the project but is not the active root. |
| ViewModels | DomainListViewModel (@MainActor): owns user actions, loading state, logs, coordinates services, and maps persistence + resolution cache into DomainRowDisplay for the list. |
| Services | Side effects and integration: DNS resolution, route orchestration, script/XPC execution, VPN heuristic monitoring, login item registration, disk persistence. |
| Models | Pure types: AppPersistence, DomainRecord, RouteApplicationState, DomainValidation, DomainRowDisplay, LogEntry. |
| Component | Responsibility |
|---|---|
DomainStore |
Loads/saves AppPersistence JSON; DefaultDomainsLoader merges default_domains.json; mutations for domains, route state, and settings. |
ResolverService |
Async IPv4 resolution via getaddrinfo (no shell dig). |
RouteManager |
Single entry for gateway probe and apply/remove; delegates privileged work to PrivilegedRouteExecuting. |
ScriptRunner |
Copies the bundled script to Application Support; runs gateway without elevation; runs apply / remove through AppleScript (osascript + do shell script … with administrator privileges). Implements PrivilegedRouteExecuting. |
PrivilegedHelperClient |
Alternative PrivilegedRouteExecuting: XPC to VPNBypassRouteHelper, JSON request/response (RouteXPCRequestBody / RouteXPCResultBody). No osascript in the app when this path is enabled. |
RoutePrivilegedExecutorFactory |
Selects ScriptRunner vs PrivilegedHelperClient from AppPrivilegedRoutingConfiguration.usePrivilegedHelper. |
VPNMonitor |
NWPathMonitor + interface-name heuristic (utun, tun/tap, ppp, ipsec) for “VPN likely”; drives optional auto-apply when enabled. |
LoginItemManager |
SMAppService.mainApp register/unregister for “run at login” (macOS 13+). |
Shared helpers live under PrivilegedRoutingShared/ (XPC protocol/constants, payload types, RouteScriptProcessRunner).
SwiftUI (ManagerView)
→ DomainListViewModel
→ DomainStore (read/write state.json)
→ ResolverService (DNS → IPv4)
→ RouteManager
→ PrivilegedRouteExecuting
→ ScriptRunner (bash + osascript) OR PrivilegedHelperClient (XPC)
→ bypass_routes.sh → /sbin/route
← @Published / ObservableObject updates → UI
User toggles (per-domain inclusion, run at login, auto-apply on VPN) update persistence through the view model and store.
Host-specific routing
The script installs host routes (-host): only packets destined to those exact IPv4 addresses use the supplied next hop (the gateway string from route -n get default). Other traffic follows the rest of the routing table unchanged.
Relation to “bypassing” a VPN
Many VPN clients install a default route (or otherwise attract traffic) through a tunnel interface (often utun*). A more specific route -n add -host entry can steer matching destinations out of the default path if the chosen gateway is reachable and correct for leaving via the physical LAN. The app does not enumerate Wi‑Fi/Ethernet separately; it uses whatever route -n get default reports. If the active default route is already the VPN, that gateway value may be VPN-related, and bypass may not behave like “always use the home router”—behavior is setup-dependent.
Default gateway
Gateway discovery is /sbin/route -n get default parsed for gateway:. Changing networks or VPN state after apply can make old host routes stale (wrong next hop or interface); re-apply after network changes when needed.
Limitations and edge cases
- IPv4 only — no IPv6 host routes;
bypass_routes.shskips non-IPv4 tokens. - DNS-dependent — routes target resolved addresses. CDNs or short TTLs can change A records; refresh and re-apply if IPs drift.
- VPN detection is a heuristic —
NWPathMonitorinterface names are not a universal “VPN connected” API; false positives/negatives are possible. - Route conflicts — other software or scripts may add overlapping routes;
applydeletes a matching host route before add, but unrelated changes can still surprise you. - DNS leaks vs routing — this tool adjusts routing; it does not replace system resolver policy. Understand your threat model if DNS privacy matters.
- VPN or OS updates may reorder routes or replace behavior; treat bypass as best-effort for advanced users.
| Topic | Behavior in this version |
|---|---|
| Routing (default) | route changes require root. ScriptRunner elevates via AppleScript (osascript), which triggers Apple’s standard administrator prompt per elevated invocation (each apply/remove is a new authorization workflow). |
| Routing (optional) | With AppPrivilegedRoutingConfiguration.usePrivilegedHelper = true, the app uses XPC to VPNBypassRouteHelper, which runs the same script inside the helper. Avoiding repeated prompts requires a properly installed and blessed helper (e.g. SMJobBless + signing). PrivilegedHelperServiceManager in the repo is a stub; full install wiring is not completed here. |
| Sandbox | App Sandbox is disabled in VPNBypass.entitlements so the app can run route, bash, and osascript in a typical local/developer-signed build. App Store distribution would need a different design (privileged helper, hardened constraints, etc.). |
| Hardened Runtime | Enabled in the Xcode project for the relevant targets. |
| Login items | SMAppService (LoginItemManager). The user may need to approve the app under System Settings → General → Login Items when status is requiresApproval. |
- Finish SMJobBless (or equivalent) for
VPNBypassRouteHelper, matching Team ID, embedded helper location, launchd plist, and Mach service namecom.vpnbypass.VPNBypass.route-helper(RouteXPCConstants). - Keep
usePrivilegedHelperas the single switch once install UX exists. - Add automated tests around
DomainValidation, persistence encoding, and privileged executor selection / payload round-trips.
Run when VPN connected uses VPNMonitor: when the active path is satisfied and an interface name matches the documented patterns (utun, tun/tap, ppp, ipsec), the app treats that as VPN likely. On transition from not likely → likely, DomainListViewModel can trigger Apply automatically if the setting is enabled. This is not a vendor API; some VPNs will not match.
VPNBypass.xcodeproj/
VPNBypass/
VPNBypassApp.swift # App entry (@main); hosts DomainListViewModel
Info.plist
VPNBypass.entitlements
Assets.xcassets/
Models/ # Persistence, domain, validation, row display, logs
Services/ # Store, resolver, routes, script/XPC, VPN monitor, login item
PrivilegedRouting/ # Factory, client, configuration, SMJobBless scaffold
ViewModels/ # DomainListViewModel
Views/ # ManagerView (primary UI), Manager/*, rows, logs
Resources/
bypass_routes.sh
default_domains.json
PrivilegedRoutingShared/ # XPC wire types, protocol, process runner
VPNBypassRouteHelper/ # Privileged helper target (XPC listener + command handler)
This project is licensed under the GPL-3.0 (GNU General Public License v3.0).
See the LICENSE file for details.
