Skip to content
mxin
Go back

Running Tailscale and Cloudflare WARP Together on macOS

Running Tailscale and Cloudflare WARP simultaneously has historically been a recipe for networking headaches. The two services compete for routing control, causing authentication failures, broken peer connections, and the dreaded “no route to host” errors.

After troubleshooting with AI, I finally achieved stable coexistence. The solution involves configuring WARP’s split tunnel to exclude Tailscale’s traffic entirely.

Table of contents

Open Table of contents

The problem: competing VPN tunnels

Both Tailscale and Cloudflare WARP operate as system-wide VPNs that want to intercept and route your network traffic. When both are active:

The result is that Tailscale shows “connected” but nothing actually works. Connections time out, authentication loops fail, and you are left wondering which service to disable.

The solution: WARP split tunnel exclusions

The fix involves “punching holes” in WARP’s tunnel so it stops intercepting Tailscale traffic. You need to exclude four categories of addresses in your Cloudflare Zero Trust configuration.

Step 1: Exclude Tailscale service ranges

These IP ranges allow Tailscale to communicate with its control plane and logging infrastructure. Add them to your Split Tunnel (Exclude) list:

Control plane:

Logging:

These ranges are documented in Tailscale’s firewall ports reference.

Step 2: Exclude tailnet traffic

These ranges cover the virtual IP addresses assigned to devices on your tailnet:

IPv4 (Carrier-Grade NAT range):

IPv6 (Unique Local Address range):

Step 3: Exclude control domains

Add these as Domain exclusions to ensure OAuth authentication and API calls work correctly:

Step 4: Configure local domain fallback (mode-dependent)

This step only applies if your WARP deployment uses DNS filtering. Check your WARP service mode to determine if this applies to you.

If you run WARP with DNS filtering enabled:

In Cloudflare Zero Trust:

  1. Navigate to Settings → WARP Client → Local Domain Fallback
  2. Add ts.net pointing to 100.100.100.100

This ensures queries like my-server.ts.net get sent to Tailscale’s internal DNS resolver instead of WARP trying to resolve them on the public internet.

If you run WARP in “Secure Web Gateway without DNS Filtering” mode:

This step does not apply to you. In this mode, WARP does not intercept DNS queries at all—your local DNS resolver (such as AdGuard Home) handles all DNS resolution directly. Configure your local resolver to forward .ts.net queries to 100.100.100.100 instead.

Complete configuration summary

Here is the full list of exclusions you need to add:

IP exclusions (Split Tunnel → Exclude)

AddressTypeDescription
100.64.0.0/10IPv4Tailscale IPv4 CGNAT range
fd7a:115c:a1e0::/48IPv6Tailscale IPv6 ULA range
192.200.0.0/24IPv4Tailscale control plane
2606:B740:49::/48IPv6Tailscale control plane
199.165.136.0/24IPv4Tailscale logging
2606:B740:1::/48IPv6Tailscale logging

Domain exclusions (Split Tunnel → Exclude)

DomainDescription
controlplane.tailscale.comCoordination server
login.tailscale.comAuthentication
log.tailscale.comTelemetry
api.tailscale.comAPI access

Local domain fallback (DNS filtering modes only)

DomainDNS Server
ts.net100.100.100.100

Bonus: Automating DERP relay exclusions

Tailscale uses DERP (Designated Encrypted Relay for Packets) servers as fallback when direct peer-to-peer connections fail. Excluding these IPs ensures reliable relay connectivity.

The DERP server IPs change occasionally as Tailscale adds new regions. Here is a Python script that fetches the latest DERP map and updates your Cloudflare split tunnel automatically:

import requests

# Configuration - replace with your values
CF_API_TOKEN = "<YOUR_CLOUDFLARE_API_TOKEN>"
CF_ACCOUNT_ID = "<YOUR_ACCOUNT_ID>"
CF_POLICY_ID = "<YOUR_POLICY_ID>"

def get_latest_exclusions():
    # Fetch current DERP relay IPs from Tailscale
    derp_data = requests.get("https://controlplane.tailscale.com/derpmap/default").json()
    derp_ips = set()
    for region in derp_data["Regions"].values():
        for node in region.get("Nodes", []):
            if node.get("IPv4"):
                derp_ips.add(node["IPv4"] + "/32")
            if node.get("IPv6"):
                derp_ips.add(node["IPv6"] + "/128")

    # Define static Tailscale infrastructure
    static_rules = [
        {"address": "100.64.0.0/10", "description": "Tailscale IPv4 CGNAT"},
        {"address": "fd7a:115c:a1e0::/48", "description": "Tailscale IPv6 ULA"},
        {"address": "192.200.0.0/24", "description": "Tailscale Control Plane V4"},
        {"address": "2606:B740:49::/48", "description": "Tailscale Control Plane V6"},
        {"address": "199.165.136.0/24", "description": "Tailscale Logs V4"},
        {"address": "2606:B740:1::/48", "description": "Tailscale Logs V6"},
        {"address": "controlplane.tailscale.com", "description": "Tailscale Domain"},
        {"address": "login.tailscale.com", "description": "Tailscale Domain"},
        {"address": "ts.net", "description": "Tailscale MagicDNS"}
    ]

    # Add DERP relay IPs
    for ip in derp_ips:
        static_rules.append({"address": ip, "description": "Tailscale DERP Relay"})

    return static_rules

def update_cloudflare(new_entries):
    url = f"https://api.cloudflare.com/client/v4/accounts/{CF_ACCOUNT_ID}/devices/policy/{CF_POLICY_ID}/exclude"
    headers = {
        "Authorization": f"Bearer {CF_API_TOKEN}",
        "Content-Type": "application/json"
    }

    # PUT replaces the entire list - include your other manual rules too
    response = requests.put(url, headers=headers, json=new_entries)
    print("Update Status:", response.status_code, response.text)

if __name__ == "__main__":
    rules = get_latest_exclusions()
    update_cloudflare(rules)

You can run this script periodically (for example, weekly via cron) to keep DERP exclusions current. Note that the PUT request replaces the entire exclusion list, so include any non-Tailscale exclusions in your configuration.

Why this works

The key insight is understanding what traffic each service needs:

  1. Authentication works — By excluding 2606:B740:49::/48 and the control domains, login requests reach Tailscale’s coordination servers directly without hitting WARP’s internal routing table

  2. Relay connections succeed — By excluding DERP IPs, Tailscale can always reach a relay server via your standard ISP connection when peer-to-peer is blocked

  3. DNS resolves correctly — Local domain fallback prevents WARP from trying to resolve .ts.net addresses on the public internet, allowing Tailscale’s MagicDNS to handle them

  4. Tailnet traffic flows — By excluding the 100.64.0.0/10 and fd7a:115c:a1e0::/48 ranges, packets between your devices bypass WARP entirely

AdGuard Home integration

If you run AdGuard Home as your local DNS server alongside Tailscale, DNS configuration depends on your WARP mode.

For “Secure Web Gateway without DNS Filtering” mode (my setup):

In this mode, WARP does not intercept DNS queries at all. AdGuard Home handles all DNS resolution directly, which simplifies things considerably. Configure AdGuard Home to forward .ts.net queries to Tailscale’s MagicDNS:

  1. In AdGuard Home, go to Settings → DNS Settings → Upstream DNS servers
  2. Add a DNS rewrite or upstream rule: [/ts.net/]100.100.100.100

This ensures AdGuard Home forwards .ts.net queries to Tailscale while handling everything else normally.

For WARP modes with DNS filtering:

You need to configure Local Domain Fallback in Cloudflare Zero Trust (as described in Step 4). This tells WARP to pass .ts.net queries to your local resolver, which then forwards them to Tailscale.

The DNS flow becomes: Query → WARP (passes through) → AdGuard Home → Tailscale MagicDNS.

Verification steps

After applying the configuration:

  1. Restart both services — Toggle WARP off and on, then do the same for Tailscale
  2. Check Tailscale status — Run tailscale status and verify all peers show as reachable
  3. Test MagicDNS — Try ping my-server.ts.net and confirm it resolves to a 100.x.x.x address
  4. Test peer connectivity — SSH or ping between devices on your tailnet
  5. Verify WARP still works — Confirm that non-Tailscale traffic still routes through WARP

If peers show as “offline” or DNS fails, double-check that all exclusions are properly saved in Cloudflare Zero Trust.

Conclusion

Running Tailscale and Cloudflare WARP together is finally achievable with the right split tunnel configuration. The key is excluding all Tailscale infrastructure from WARP’s tunnel: control plane IPs, tailnet address ranges, control domains, and DERP relay servers.

Once configured, both services coexist peacefully. WARP handles your general internet traffic with its security features, while Tailscale maintains your private mesh network with full MagicDNS and peer-to-peer connectivity.

No more choosing between them.


Tested on macOS Sequoia 15.7.3 with Tailscale 1.92.3 and Cloudflare WARP 2025.10.186.0.


Share this post on:

Next Post
Migrating from AstroPaper 4.2.0 to 5.5.1