articles

Tailscale + Cloudflare WARP on macOS

Why macOS only allows one VPN at a time, and two real ways to run Tailscale and Cloudflare WARP together.

macOS networking. Why one disconnects the other, what’s actually layerable, and how to set it up.

The verdict, up front

Your stated layering doesn’t exist. You asked for Tailscale “inside” and WARP “outside” — Tailscale’s UDP transiting WARP’s tunnel. That’s the configuration that breaks both apps. WARP NATs and MASQUE-encapsulates packets; Tailscale’s WireGuard peer/DERP traffic can’t survive that. Every working coexistence config explicitly punches Tailscale’s bytes out of WARP, in the opposite direction.

What’s deliverable. If your real intent is “all my open-internet traffic exits through Cloudflare and my Tailnet still works,” that’s a solved problem. WARP holds the default route; Tailscale runs alongside; WARP’s split-tunnel exclusions let Tailscale reach its peers and control plane via the physical interface. Browser, curl, apps → Cloudflare. Tailscale → tailnet, directly.

You’re picking between two real setups. There is no third option I’d put in front of you.

OptionWARP scopeTailscalePick when
A · Recommended — Systemwide WARP + Tailscale alongsideEvery byte of internet traffic egresses through CloudflareNative, full TailnetYou want WARP applied to everything, no per-app config. Requires a free Cloudflare Zero Trust account.
B · Alternative — WARP Proxy Mode + native TailscalePer-app SOCKS5/HTTP proxy on 127.0.0.1:40000Native, full TailnetYou only want WARP for specific apps (browser, curl). No NetworkExtension conflict at all because WARP isn’t a system VPN in this mode.

Why only one VPN at a time

It’s a hard framework limit, not a soft UI choice. Both Tailscale (App Store and standalone) and WARP install themselves as NEPacketTunnelProvider system extensions on the Apple NetworkExtension framework. Apple’s developer-tools engineering, on the developer forums, states the rule directly: only one NEPacketTunnelProvider can run on the system at one time because they are considered Enterprise VPNs. A “Personal VPN” (the older NEVPNManager path used by IKEv2) can coexist with one Enterprise tunnel — but that’s not what either of these apps uses.

Even where macOS is more permissive than iOS at the kernel level (you can have multiple utun interfaces in principle), the moment two NE apps both try to claim the system default route or system DNS, they fight. WARP grabs 0.0.0.0/0 as egress and intercepts everything — including Tailscale’s control-plane and DERP relay traffic — even though WARP’s defaults already exclude the 100.64.0.0/10 CGNAT range Tailscale’s peers live in. The fix is to extend WARP’s exclusion list far enough to cover everything Tailscale needs to talk to.

Option A — Systemwide WARP + Tailscale alongside

The configuration: WARP is the default-route VPN; Tailscale runs without an exit node and without route acceptance; WARP’s Split Tunnel exclusions cover Tailscale’s full set of endpoints so they exit directly via Wi-Fi/Ethernet.

1. Cloudflare Zero Trust account (free)

Consumer 1.1.1.1+WARP has a Split Tunnel editor in Preferences → Advanced, but the full exclusion list (especially domain wildcards and IPv6 ranges) is cleaner to manage in the Zero Trust dashboard. Free up to 50 users, no credit card. Sign up at one.dash.cloudflare.com, pick a team name, complete onboarding.

2. Install the apps

3. Bring up Tailscale without owning routes

tailscale up --accept-routes=false

No --exit-node. If you ever turn on a Tailscale exit node, this whole config breaks — Tailscale will then claim 0.0.0.0/0 and fight WARP for the default route. Tailscale’s own KB confirms this is unworkable.

4. Add WARP Split Tunnel exclusions

Zero Trust dashboard → Settings → WARP Client → Device Settings → Profile → Split Tunnels → Manage. Confirm mode is Exclude IPs and domains. Add:

100.64.0.0/10
fd7a:115c:a1e0::/48
2606:B740:49::/48
controlplane.tailscale.com
login.tailscale.com
*.tailscale.com
*.ts.net

The *.tailscale.com wildcard covers DERP relay hostnames (derpN.tailscale.com), which is important — DERP IPs change over time, so excluding them by domain is more durable than by IP. Domain-based exclusions are slightly slower than IP-based per Cloudflare’s docs, but with the wildcard backstop here the safety wins.

5. Fix MagicDNS resolution

Both apps want to set the system resolver. Without intervention, lookups for *.ts.net hit WARP’s resolver, which doesn’t know MagicDNS. The fix is Local Domain Fallback: in the Zero Trust dashboard under WARP → Profile → Local Domains, add:

ts.net

(Plus your custom MagicDNS suffix if you’ve set one.) This tells WARP to hand .ts.net queries to the system resolver, which Tailscale has pointed at 100.100.100.100.

Option B — WARP Proxy Mode + native Tailscale

Different shape: WARP doesn’t run as a system VPN at all. It runs as a local proxy on 127.0.0.1:40000 (SOCKS5 or HTTPS). Tailscale gets full native networking; nothing fights for the default route; you opt apps into Cloudflare egress on a per-app basis.

This is the cleanest match for the “Tailscale native, WARP as a configurable filter” mental model — but it’s per-app, not systemwide.

Setup

  1. Install Tailscale (standalone PKG) and the WARP client.
  2. Tailscale: tailscale up. Nothing special needed.
  3. WARP: open the menu bar app → Preferences → Advanced → Configure Proxy. Toggle on. Default port 40000, SOCKS5 + HTTPS both available.
  4. Switch WARP from VPN mode to Proxy mode (the toggle replaces the system-VPN behavior; WARP stops claiming default route).
  5. Configure your apps:
    • Browser: SOCKS5 proxy 127.0.0.1:40000, or use a per-tab proxy extension.
    • curl: curl --socks5 127.0.0.1:40000 https://example.com.
    • Shell: export ALL_PROXY=socks5://127.0.0.1:40000 in the sessions you want.

Verifying it works

Run these after either setup. Output should look as described.

tailscale status

Peers should show active or direct, not relay only indefinitely.

tailscale ping <peer-name>

Should succeed with low latency. pong via DERP for an extended period means peer connections aren’t establishing — your exclusions are missing something.

curl https://www.cloudflare.com/cdn-cgi/trace

Option A: warp=on and ip= a Cloudflare egress IP. Option B (without forcing the proxy): warp=off; with --socks5 127.0.0.1:40000: warp=on.

traceroute 1.1.1.1

Option A: first hop inside Cloudflare’s network. Option B (no proxy): your normal ISP path.

traceroute <peer-100.x-IP>

Should leave via the Tailscale utun interface, not Cloudflare. If a tailnet peer’s traceroute shows Cloudflare hops, your exclusions for the CGNAT range broke.

scutil --dns | head -40

Resolver chain. ts.net should route via 100.100.100.100 (Tailscale MagicDNS).

dig +short controlplane.tailscale.com

Should resolve. Then traceroute the result and verify it leaves the box on the physical interface, not via WARP.

Gotchas

Sources