crabbyproxy: Domain-Based VPN Split Tunneling for macOS

I built crabbyproxy after spending an evening trying to get YouTube and Reddit to bypass my WireGuard VPN on macOS, and discovering that none of the standard approaches work.

The problem

WireGuard’s macOS app uses Apple’s Network Extension framework, which intercepts packets before the routing table. This means AllowedIPs exclusions generate hundreds of CIDR fragments (YouTube alone uses dozens of dynamic CDN subnets), and route add commands are simply ignored. The app also runs in Apple’s sandbox, no PostUp/PostDown scripts, no scutil --nc support, no scripting API.

When you’re running a VPN for security testing or bug bounty, certain sites block datacenter/VPN IPs with CAPTCHAs, bot checks, and degraded service. YouTube videos refuse to play, Netflix blocks playback, and Reddit throws up walls. For these sites, anonymization isn’t the priority, and keeping personal traffic off VPN IPs associated with security testing is actually preferable.

What I tried (and why it failed)

Route-based split tunneling: Added routes via route add to send YouTube/Reddit traffic through the home gateway. The WireGuard Network Extension intercepts at the packet level before macOS even consults the routing table. Dead end.

AllowedIPs modification: Calculated the complement CIDR ranges to exclude Google’s ASN from WireGuard’s AllowedIPs. YouTube uses so many IP ranges that the exclusion list hit 255+ CIDR entries, causing the tunnel to fail entirely. Fewer ranges leaked non-YouTube Google services outside the VPN.

Keychain config automation: Wrote AppleScript to read/modify the WireGuard config in the macOS Keychain and UI-script the app to restart the tunnel. It worked mechanically, but the underlying AllowedIPs approach was still broken.

The solution: IP_BOUND_IF

The breakthrough was discovering macOS’s IP_BOUND_IF socket option. While the Network Extension captures traffic based on routing, it respects socket-level interface binding. curl --interface en0 bypasses the VPN, and so does a SOCKS proxy that sets IP_BOUND_IF on its outgoing connections.

crabbyproxy is a ~2MB Rust SOCKS5 proxy that:

  1. Listens on 127.0.0.1:1080
  2. Resolves domains via DNS-over-HTTPS (Cloudflare, Google, Quad9 with fallback), bypassing the VPN’s DNS for geo-correct CDN IPs
  3. Binds outgoing connections to the physical interface via IP_BOUND_IF
  4. Relays traffic asynchronously via tokio

A browser PAC file routes target domains (YouTube, Reddit, Netflix, Hulu, etc.) to the proxy. Everything else goes DIRECT through the VPN. No WireGuard configuration changes required.

crabbyproxy traffic flow

Install

brew install digital-shokunin/crabbyproxy/crabbyproxy
brew services start crabbyproxy

Or from source:

git clone https://github.com/digital-shokunin/crabbyproxy.git
cd crabbyproxy && ./install.sh

Then point your browser’s automatic proxy configuration at the PAC file. Full documentation on GitHub.


See also