macOS: Tailscale's transport cannot escape the tunnel; allowed LAN ranges do not cover it

Author: andredezzyCreated Sep 4, 2026Updated Sep 20, 2026
LabelsmacOS

What I ran into

With the Mullvad app connected on macOS, Tailscale cannot carry traffic. Mullvad owns the default route, so Tailscale's own transport (control plane, logs, DERP and STUN) is captured by the tunnel and never reaches the relays. Tailnet peers appear online in the control plane while tx climbs and rx stays at zero, with no handshake.

This overlaps with #6086 (the CGNAT range 100.64.0.0/10 not being covered by allowed LAN ranges) and #7443. I am filing separately because neither thread covers the transport path, and this issue carries a reproduction plus a reference implementation. Close it as a duplicate if you would rather consolidate — the link below is the part worth keeping.

Environment

  • macOS 26.6.2 (Apple silicon)
  • Mullvad 2026.4, WireGuard, relay br-sao-wg-303
  • Tailscale 1.102.3 (macsys, the standalone variant)

Reproduction

  1. Connect Mullvad with its default settings.
  2. Bring up Tailscale and try to reach any tailnet peer.
  3. Traffic leaves but nothing comes back:
tx 6396 rx 0
LastHandshake: (never)

Allowing local network access does not help, because the relays are public addresses rather than LAN ones.

Why local-network access is not enough

100.64.0.0/10 is where tailnet peers live, but reaching them first requires Tailscale's transport to get out: the coordination server, the log endpoint, and the DERP and STUN relays. Those are public addresses, so no LAN allowance covers them. Adding the CGNAT range as discussed in #6086 would still leave the transport captured.

What worked here was narrower than split tunnelling an entire application. Only the DERP relays in use, plus the control and log endpoints, need to egress the physical interface. The default route stays with Mullvad, and container traffic keeps using it.

Reference implementation

I packaged that as a root LaunchDaemon so it survives boot, reconnects and sleep/wake:

https://github.com/andredezzy/tailnet-keeper

It journals every route it displaces before touching it and restores the previous state exactly on failure or removal, including REJECT and BLACKHOLE policy. Its firewall rules live in a dedicated PF child anchor under com.apple/; it never flushes the global ruleset and never modifies com.apple.internet-sharing. Mullvad remains the default egress for the host and for containers throughout.

The ask

Is there a supported way to exempt a small set of destinations from the tunnel, so that a peer VPN's transport can coexist? Today this needs routing and PF work outside the app, which is not something most users should have to assemble. Happy to supply more detail from this setup if it helps.