@peerapat.srs/vpn-bypass
v0.3.2
Published
CLI and localhost UI that split-route IPv4/IPv6 and split-DNS around a full-tunnel corporate VPN (home LAN for the web, intranet via VPN).
Maintainers
Readme
vpn-bypass
CLI and localhost UI that split-route IPv4 (and best-effort IPv6 on macOS/Linux) around a full-tunnel corporate VPN, and apply system split-DNS on macOS: general web uses your home internet; intranet stays on the VPN.
Package: @peerapat.srs/vpn-bypass
Command after install: vpn-bypass
Source: github.com/peerapatsrs/vpn-bypass
This is not official corporate split-tunnel. The only well-tested production case is macOS + Palo Alto GlobalProtect (utun). Windows and Linux inverse split is implemented and fixture-tested; it has not been proven in daily use in this tree. Browser VPNs, per-app MDM, and iOS are out of scope.
CLI and UI are Thai/English (locale in config, default th): vpn-bypass --lang en <command> or vpn-bypass lang en.
Features
- Default inverse mode: public web via the home LAN gateway; RFC1918 (
10/8,172.16/12,192.168/16) and extra VPN CIDRs stay on the tunnel; your home LAN/24is protected so printers/NAS still work. - Split-DNS (macOS): a small local forwarder on
127.0.0.1sends general names to home DNS and corporate suffixes / RFC1918 PTR to VPN DNS. Linux is best-effort (resolvectl). Windows does not change system DNS. - IPv6 (macOS/Linux): when an inet6 LAN gateway exists, adds
::/1and8000::/1via that gateway. - Localhost UI on
127.0.0.1(preferred port 18787) with CSRF token, live HTTP(S) connections (LAN vs VPN), host lookup, Thai/English. - No sudo to open the UI. Admin is asked only on Start / Stop / allow / deny / watch: macOS password dialog, Windows UAC, Linux
pkexecwhen installed. The UI process stays your user. - Save-and-close routes: after
on, you can quit; routes stay untiloff.offdeletes only this tool’s ledger (ownedRoutes/ownedDns), not the VPN client’s defaults. - Reconnect repair: while the UI is open (after the first admin prompt), overwritten
/1routes and split-DNS are repaired. CLIwatchis off by default and must keep that process running. tryis not a third mode: probehost:443via LAN; VPN host routes require explicit confirm ([y/N]/ UI type-again). No automatic failover.- Advanced domains mode: full tunnel stays; only listed sites get LAN
/32host routes. Empty list cannot enable this mode.wwwand apex are stored together. - Zero runtime npm dependencies. Node.js 18+.
Supported VPNs
There is no complete catalog of “every VPN on earth.” Detection is topology first (dual IPv4 default, or a tunnel-looking default plus a recoverable home LAN), with adapter names as a hint only. Name lists can never be complete — GlobalProtect on Windows is often just Ethernet 3.
This tool only acts on full-tunnel IPv4 (a second default route / tunnel NIC), not browser extensions or per-app MDM VPN.
| Kind | Examples | This tool |
|---|---|---|
| Corporate full tunnel | GlobalProtect, AnyConnect, FortiClient, Pulse/Ivanti, IKEv2/SSTP | Inverse split when there is a dual default or tunnel NIC — even if the adapter is Ethernet 3 / ens192 / eth1 |
| Consumer tun | WireGuard, OpenVPN TUN/TAP, WARP, Tailscale, ZeroTier, NordLynx, Mullvad, Proton, Clash/sing-box TUN | Same: works when a tun/wg NIC or a second default is up |
| Official split-tunnel profile | GP portal split, per-app VPN | Not replaced; inverse overlays routing and may be the wrong tool |
| Browser-only / proxy | Chrome VPN extensions, SOCKS, HTTP proxy | No tunnel to split |
Verified in daily use: macOS + Palo Alto GlobalProtect (utun).
Implemented and fixture-tested: Windows IPv4 dual-default / unnamed adapters, Linux ip route dual-default including unnamed eth1/ens192. Live Windows Start/UAC and live Linux apply have not been proven end-to-end here.
Parallels Shared Network (10.211.55.0/24) or VirtualBox NAT (10.0.2.0/24) alone is not treated as a VPN.
Supported OS
A Node CLI cannot run on iOS/iPadOS. Desktop OS this package actually drives:
| OS | Routes | Split-DNS | Admin prompt from UI | Proof |
|---|---|---|---|---|
| macOS (darwin) | IPv4 + IPv6 /1 | yes | password dialog | Verified in daily use with GlobalProtect |
| Windows (win32) | IPv4 | no (left unchanged) | UAC Yes/No (RunAs) | Fixture-tested detect + route add argv. Live UAC Start is implemented, not proven here |
| Linux | IPv4 + IPv6 /1 | resolvectl best-effort | pkexec when installed | Fixture-tested detect/plan. Not live-tested in this tree |
| Android / Termux | same as Linux if ip exists | same | usually none — use root/su | experimental |
| FreeBSD / OpenBSD / NetBSD | same route/ifconfig family as macOS (experimental) | no | CLI sudo (no GUI helper) | experimental |
Anything else (aix, sunos, …) returns EUNSUPPORTED.
Use @peerapat.srs/[email protected] or later. 0.3.1 still matches many Windows VPNs by adapter name and can miss GlobalProtect as Ethernet N, and it can fail to find route/ipconfig when PATH is odd.
How inverse works
- Adds
0.0.0.0/1and128.0.0.0/1via the LAN gateway so they beat the VPN default (0/0) for the public Internet. The VPN default is not deleted. - Re-pins RFC1918 via the VPN iface so intranet is not stolen by those
/1routes, then pins your home LAN subnet via LAN so it wins over192.168.0.0/16. - On macOS, starts a DNS forwarder on
127.0.0.1and points system DNS at it. Public names (e.g.facebook.com) go to home DNS; GP search/corp suffixes and RFC1918 reverse lookups still go to VPN DNS.offstops the forwarder and restores the previous servers — it must not leave127.0.0.1behind. - If GlobalProtect later points owned
/1prefixes back at the tunnel, repair usesroute change(macOS) orip route replace(Linux) on owned destinations only.
The UI live table is not a full visit history: it polls established TCP 80/443 (and 8080/8443) in this process’s memory. Short-lived sockets, QUIC/HTTP3, and DoH in the browser are often missed. Names come from this process’s A-record cache plus reverse DNS, not browser tabs.
Tunnel/firewall visibility of general-web DNS+data can be reduced; a GlobalProtect or EDR agent on the laptop can still see local processes.
Warnings
- Changing routes or system DNS needs administrator rights (CLI:
sudo; UI: macOS password dialog, Windows UAC, Linuxpkexecwhen available). Installing the package is not enough. - Closing the UI tab or the terminal does not restore routes. Run
vpn-bypass off(or Stop in the UI). - Dry-run first:
vpn-bypass on --dry-run. There is no automatic failover if a site is unreachable on home internet. - Windows does not change system DNS. Linux split-DNS is
resolvectlbest-effort — neither equals macOS split-DNS.
Requirements
- Node.js 18 or later
- Admin rights to change the routing table:
sudoon the CLI; UI: macOS password dialog, Windows UAC, Linuxpkexecwhen available - An IPv4 VPN that is actually up (full tunnel, not a browser extension)
- A reachable home LAN gateway
Install
npm i -g @peerapat.srs/vpn-bypass
vpn-bypass statusOr without a global install:
npx @peerapat.srs/vpn-bypass status
npx @peerapat.srs/vpn-bypass uiNeed 0.3.2 or later on Windows. From a clone, npm i then npx vpn-bypass / npm i -g . runs this tree.
Quick start
vpn-bypass status
vpn-bypass on --dry-run
sudo vpn-bypass on # CLI apply (Windows: elevated prompt / Run as administrator)
vpn-bypass ui # no sudo; admin prompt on Start
sudo vpn-bypass offon is save-and-close: you can quit the CLI; routes stay until off. On macOS inverse, the DNS forwarder is spawned detached so split-DNS also survives quitting the CLI; off stops it and restores previous DNS.
Open the UI without sudo (vpn-bypass ui / npm start). Status and preview run as your user. Start / Stop / allow / deny / watch prompt for admin (macOS password, Windows UAC, Linux pkexec), then the unprivileged UI refreshes. Closing the UI stops reconnect repair; routes already applied stay until off.
If a previous sudo vpn-bypass ui left root-owned files:
sudo chown -R "$(whoami)" ~/.config/vpn-bypass
vpn-bypass uiModes
Default — inverse
“General web via home internet, company traffic via VPN.” See How inverse works.
Advanced — domains
Full tunnel stays. Only listed sites get host routes (/32) via the LAN gateway.
- An empty list cannot enable this mode (
EDOMAIN_EMPTY). domain addexpands www and apex (example.com↔www.example.com).- Hosts are resolved again every time you apply.
try is not a third mode
Use after inverse. Probes host:443 via LAN. If that fails, the CLI asks [y/N] (default no) before adding a VPN host route. The UI requires typing the hostname again plus a checkbox. There is no automatic failover.
CLI
vpn-bypass [--lang th|en] [--help] [--version] <command>
status Show status (lightweight; no public-IP wait)
on [--mode inverse|domains] [--dry-run]
Apply routes (default mode: inverse)
off Delete only routes this tool added
domain add|rm|list [host] Manage advanced domain list
try <host> Probe host:443 via LAN after inverse
allow <host> Add a host route via the VPN (after confirm)
deny <host> Remove that VPN host route
watch [on|off] Re-apply missing/overwritten owned routes (off by default; process must stay up)
lookup <host> Show whether a host/IP uses LAN or VPN
ui Open the UI on 127.0.0.1 (no sudo at launch)
lang th|en Save CLI/UI language in configwatch on keeps the process in the foreground. Ctrl+C stops watch; it does not run off. Prefer leaving vpn-bypass ui open (after the first admin password) for reconnect repair while you are at the desk.
UI
vpn-bypass ui
# or from a clone:
npm start- Listens on
127.0.0.1only (preferred port 18787, or a random free port). There is noHOSTenv to bind0.0.0.0. - Per-process token (
X-Vpn-Bypass-Token);Host/Originmust behttp://127.0.0.1:<port>. - Thai/English, live traffic card, lookup, and DNS status (
split/lan/vpn). - No admin dialog at launch. Mutating actions prompt on macOS; after success the UI stays unprivileged and refreshes status.
- Session repair while the UI process is up (after that first prompt): does not set
watch: truein config; also re-applies split-DNS if GlobalProtect overwrites it.
Closing the tab does not call off. Closing the UI process stops automatic reconnect repair.
Configuration
| Platform | Directory |
| --- | --- |
| macOS / Linux | ~/.config/vpn-bypass/ |
| Windows | %APPDATA%\vpn-bypass\ |
Override the directory with VPN_BYPASS_HOME (or XDG_CONFIG_HOME on Unix). Under sudo, the tool prefers the invoking user’s home, not /var/root. Privileged apply chowns the config dir back to that user so the UI can keep running unprivileged.
| File | Role |
| --- | --- |
| config.json | Mode, domains, locale (th/en), watch flag, LAN protect |
| applied.json | Ledger of owned routes and DNS (off deletes/restores only these) |
| apply.lock | Apply lock |
Troubleshooting
| Symptom | Likely cause |
| --- | --- |
| Routes revert to full tunnel soon after apply | GlobalProtect reconnect overwrote /1. Leave vpn-bypass ui open (after the admin password), run watch on, or apply again. Repair never deletes the VPN default. |
| Banner “VPN overwrote the routes” | Owned /1 still exists but now via the tunnel. Repair or Start using again. |
| Web works on home internet but org login / hostnames are wrong | Corp suffix missing from split-DNS (check status DNS line). Intranet should still use VPN DNS. |
| dig facebook.com still hits VPN DNS | Split-DNS not applied, GP overwrote it, or you are on Windows. Re-apply or leave vpn-bypass ui open. off must restore LAN/DHCP DNS, never leave 127.0.0.1. |
| Some sites still use the VPN after inverse | No inet6 LAN gateway, or Windows. Check lookup. |
| Live traffic shows an IP or CDN PTR | Expected without a prior lookup of that host. Lookup the site once to cache the name. |
| Home printer / NAS disappears | VPN pushed 192.168.0.0/16 over the LAN. The tool pins your LAN subnet (typically /24) via the LAN gateway; if the prefix is not /24, check status. |
| ENOTVPN | No full-tunnel VPN: need a second IPv4 default (any adapter name) or a tunnel NIC. Connect the VPN first. Browser VPNs / per-app MDM do not count. Upgrade to 0.3.2+ if an older Windows build missed Ethernet N. |
| EPRIV / “needs sudo” | CLI: re-run on / off / allow / deny with sudo. UI: click Start/Stop again and complete the admin password dialog. |
| EACCES on ~/.config/vpn-bypass | A previous sudo vpn-bypass ui left root-owned files. Once: sudo chown -R "$(whoami)" ~/.config/vpn-bypass then vpn-bypass ui without sudo. |
| EDOMAIN_EMPTY | Add at least one domain before on --mode domains. |
Development
npm test
# or
node --testTests use fixtures and inject exec. They do not add or delete real routes or change live DNS in CI.
Supported adapters: darwin, linux, win32, plus android→Linux and FreeBSD/OpenBSD/NetBSD via the BSD/netstat adapter. Other OS values fail with EUNSUPPORTED.
Changelog
0.3.2
- Detect full-tunnel VPN from dual IPv4 defaults / recoverable home LAN on macOS, Windows, and Linux — adapter name is a hint, not the only path (
Ethernet 3,eth1,ens192). - Windows: PATH / UTF-16
route print/Get-NetRoutefallback, UACRunAsfor apply, hypervisor LAN (Parallels/VirtualBox) not treated as VPN. - Honest OS matrix: macOS + GlobalProtect verified; Windows/Linux fixture-tested.
0.3.1
- Windows PATH/CRLF/named-pipe elevate (GitHub). npm
0.3.1still missed many unnamed adapters.
0.3.0
- Inverse: RFC1918 via VPN, home LAN protect, IPv6
/1on macOS/Linux, system split-DNS on macOS. - Repair overwritten
/1withroute change/replace; UI session repair without turning CLI watch on. - UI runs unprivileged; macOS admin password only on mutate. Config dir is chowned back after elevate.
- Live traffic card and
lookup. FriendlyEACCESif config is still root-owned.
0.1.0
- First public scoped package: inverse
/1via LAN, domains mode, try/confirm, localhost UI.
License
MIT
