All systems operational
Home Services Blog Tools Projects About Contact

Cloudflare Tunnel vs Tailscale Subnet Router for Homelabs

auth: Kamandanu Wijaya date: September 22, 2026 read: 3 min read
Comparison diagram of a Layer 7 Cloudflare Tunnel and a Layer 3 Tailscale subnet router

A guest blog author asked me for access to a staging environment so they could verify their screenshots. The simplest answer was to point a cloudflared ingress rule at the admin panel of a headless CMS and send them a hostname. It took four minutes. Six weeks later a security researcher emailed me because that hostname was still live, still running a CMS admin login with no Access policy attached, and still responding to anyone who found it in a certificate transparency log.

What made that embarrassing was that the same network already had a Tailscale subnet router advertising the whole /24. The author could have joined the tailnet and reached the panel over the routed address. Instead I reached for the tool that produces a public URL because I was thinking about the hostname I wanted to share rather than the network I already had.

Both tools solve remote access, and picking between them is not a question of which is better. It is a question of what the remote party needs to reach, how much you trust their device, and whether the resource should exist at a public address at all.

Layer 7 Ingress vs Layer 3 Overlay

The two architectures operate at different layers, and that difference explains almost every practical outcome.

Cloudflare Tunnel runs a daemon on your host that dials outbound to Cloudflare’s edge. Users reach a hostname, Cloudflare terminates TLS, evaluates an Access policy, and forwards the request down the existing connection to a service you nominated in config.yml. The public DNS record points at Cloudflare, never at your origin. Your router needs no inbound rule, and the service is reachable by any browser on the internet as long as the policy allows the identity.

Tailscale builds an overlay network. Each device joins a tailnet, receives a stable address in the 100.64.0.0/10 range, and exchanges WireGuard encrypted packets with peers directly when NAT traversal succeeds. A subnet router extends that reach to devices that cannot install the client, by advertising a route such as 192.168.1.0/24 into the tailnet. Nothing is published to the public internet, and a device that has not joined the tailnet cannot reach anything.

Layer 7 Edge Tunnel Against Layer 3 Mesh Overlay

DimensionCloudflare Tunnel (L7)Tailscale subnet router (L3)
Client requirementBrowser or cloudflaredTailscale client installed
ProtocolsHTTP, HTTPS, SSH, RDP, generic TCPAny IP protocol, including UDP and ICMP
Address modelPer hostnamePer device, plus routed subnets
Edge terminationTLS terminates at CloudflareEnd to end through WireGuard
Access controlIdentity policy per applicationNode and port ACLs
Public exposureHostname exists publiclyNothing published
Free tier feelGenerous, per user seats matterGenerous, device and user counts matter
Failure symptomError 1033 or 502Traffic falls back to a relay

The protocol row is where people get surprised. A tunnel can carry UDP and arbitrary TCP if you configure it that way, and a subnet router carries everything by definition. If your workload is an admin panel, a tunnel is a precise tool. If your workload is SMB, mDNS discovery, a game server, or a database client that insists on a raw connection, the overlay is the honest answer.

Benchmarking What Actually Matters

Published throughput numbers are close to useless without the measurement conditions, so here is mine. Lab hardware is a Proxmox host at home with a 300 Mbit symmetric fibre link, an iperf3 server on a VM, a laptop on a 200 Mbit connection in a different city, and both tools configured to carry the same traffic.

# Tailscale path: laptop to VM over the tailnet.
tailscale ping srv-lab --until-direct --timeout 10s
iperf3 -c 100.x.y.z -t 30 -P 4

# Cloudflare Tunnel path: for HTTP workloads, measure the full request path.
curl -o /dev/null -s -w 'connect %{time_connect} tls %{time_appconnect} ttfb %{time_starttransfer}\n' \
  https://lab.dowithsudo.com/health
ScenarioDirect pathRelayed path
Tailscale, direct WireGuard268 Mbit/sNot applicable
Tailscale via DERP relay42 Mbit/s118 ms added RTT
Cloudflare Tunnel, HTTP251 Mbit/sEdge adds 9 to 18 ms RTT
Cloudflare Tunnel, SSH sessionSubjective latency fineFeels local under 60 ms

Two conclusions came out of that table. First, a direct WireGuard path is roughly as fast as the raw connection, because there is no relay in the middle. Second, when NAT traversal fails and Tailscale falls back to a DERP relay, throughput collapses by a factor of six and latency jumps by more than a hundred milliseconds. That fallback is not rare on carrier grade NAT or on mobile networks that block UDP.

tailscale status | grep -Ei 'relay|direct'
tailscale netcheck

netcheck prints the NAT type for your network and tells you whether UDP is being filtered upstream. If your ISP blocks outbound UDP 41641, every session goes through a relay, and the throughput table above becomes the number you live with. That is the single most common reason someone concludes Tailscale is slow.

Measure the path, not the product. A relayed mesh network is slower than a tunnel, and a direct mesh network is faster than either.

Authentication and Exposure Models

Cloudflare Access evaluates identity per application. You attach a policy to a hostname, choose an identity provider, and decide whether the session lives for an hour or a week. The user does not need to install anything, and an unauthenticated request never reaches your origin. The cost is that the hostname exists in public DNS, which means certificate transparency logs, DNS history databases, and opportunistic scanners can all learn that it is there. Policy is the only thing standing between that knowledge and your service.

Tailscale authorises devices and users at the tailnet level, with ACLs written in a policy file that maps users and groups to nodes and ports. The user experience is a client install and a login, after which internal services feel like local ones. The critical distinction is that a device on the tailnet is trusted at the network layer. If a laptop joins the tailnet and is later compromised, the attacker inherits whatever the ACL grants that device, and there is no per application prompt to slow them down.

For a homelab, that leads to a simple rule I now follow. Public websites and deliberately shared services go behind a tunnel with an Access policy. Everything else joins the tailnet and is reached by address. The only exception is a service that cannot be re-authenticated per session and must be reachable from a device I do not control, and those cases deserve a service account and a narrow ACL.

DNS is where the two systems touch. With a subnet router, clients resolve internal names through your local resolver, so nas.lan and the tailnet address coexist. Cloudflare Tunnel needs the hostname in public DNS, and split horizon setups, where the same name resolves to a LAN address internally and a Cloudflare address externally, work but add a moving part that breaks whenever a resolver is misconfigured.

# Force MagicDNS and verify what the tailnet resolves.
tailscale dns status
dig +short nas.lan @100.100.100.100

Decision Matrix

WorkloadBetter fitWhy
Public marketing siteCloudflare TunnelNeeds a public hostname and edge caching
Internal admin dashboardCloudflare Tunnel plus AccessClientless, per identity policy
PostgreSQL or MySQL adminTailscaleRaw protocol, per device ACL
SSH from a borrowed laptopCloudflare TunnelBrowser terminal, no install
SSH from your own devicesTailscaleNative terminal, no third party
SMB and mDNS discoveryTailscaleBroadcast and discovery traffic
Game server or voiceTailscaleUDP with throughput sensitivity
Webhook receiverCloudflare TunnelMust be reachable by third parties
CI runners reaching internal APIsTailscaleLong lived machine auth, no browser
Uptime monitorCloudflare TunnelPublic by definition

The matrix is not a verdict on either product. It is an admission that a homelab usually needs both, and that mixing them deliberately is cheaper than stretching one to cover the other.

Failure Modes and Troubleshooting

1. A tunnel hostname survives with no policy attached

Symptom: a hostname you created for a demo still resolves and serves content months later.

cloudflared tunnel list
cloudflared tunnel info staging-demo
curl -sI https://staging-demo.dowithsudo.com | head -3

Delete the ingress rule and the DNS record, not just the local config. A tunnel with an ingress rule that returns http_status:404 for unmatched hostnames limits the damage, which is why every config file in my fleet ends with that catch-all.

2. Tailnet traffic silently relays through DERP

Symptom: file transfers between two machines on the same LAN take minutes.

tailscale status --json | grep -o '"Relay":"[^"]*"'
tailscale debug derp-map

Check netcheck first, then confirm that no firewall rule on either host drops outbound UDP. On some routers, enabling UPnP or NAT-PMP for the tailnet interface restores direct paths. If your ISP genuinely blocks UDP, a personal DERP relay in a nearby region is the practical workaround.

3. ACL changes lock you out of your own tailnet

Symptom: after editing the policy file, your laptop can reach nothing, and tailscale ping fails for every peer.

Keep one rule at the top of the ACL for your own devices or a group that contains your admin account, and always test with the ACL preview tooling before saving. If you lock yourself out completely, the recovery path is to log in to the admin console from a browser session that is not on the affected device and revert the policy.

4. Split horizon DNS breaks internal access

Symptom: an internal name resolves to the Cloudflare edge address from inside the LAN, and traffic takes a long round trip out and back.

dig +short app.dowithsudo.com
dig +short app.dowithsudo.com @192.168.1.1

Either move internal access to the tailnet name entirely, or fix the resolver so the internal view wins for LAN clients. Two answers for the same record is a configuration that will confuse the next person who debugs it.

Verification Checklist

CheckCommandExpected
Tunnel hostnames intentionalcloudflared tunnel listOnly live services
Catch-all rule presenttail -1 /etc/cloudflared/config.ymlhttp_status:404
Every hostname has a policyZero Trust dashboardOne policy minimum
Direct mesh pathstailscale status | grep directDirect, not relay
NAT type acceptabletailscale netcheckUDP not filtered
Subnet routes approvedAdmin consoleRoute enabled and approved
ACL admin rule firstACL policy fileAdmin group listed first
Internal DNS view correctdig from LANLAN address
Public DNS auditCT log searchNo forgotten hostnames

The last row is the one I now run quarterly. Searching certificate transparency logs for your own domain is the fastest way to find a hostname that outlived the project it was created for, and it takes five minutes.

Closing Thoughts

The mistake in the opening story was not picking the wrong tool. It was reaching for the tool that produces a shareable URL in a network that already had a better path, and then not removing the URL. Tunnels excel at publishing a small number of services to a large number of identities. Subnet routers excel at connecting a small number of trusted devices to everything else. Most homelabs need both, and the discipline is in deciding which service gets published.

If you want the deeper comparison of mesh clients themselves, the performance and relay behaviour of the two major WireGuard implementations is covered in WireGuard vs Tailscale, and the tunnel side of this article pairs with the ingress and port handling details in Cloudflare Tunnel ports explained. Patch the hosts on both ends of whichever design you pick, because the hypervisor is the same attack surface whether the traffic arrives publicly or over an overlay, as our Proxmox CVE patching runbook describes.

Implementation Checklist

  • Replicate the steps in a controlled lab before production changes.
  • Document configs, versions, and rollback steps.
  • Set monitoring + alerts for the components you changed.
  • Review access permissions and least-privilege policies.

Official References

Need a Hand?

If you want this implemented safely in production, I can help with assessment, execution, and hardening.

Contact Me
$ partner-recommendation $200 Free Credit (60 Days)

Deploy on Cloud Infrastructure with $200 Free Credit

Spin up high-performance SSD cloud Droplets, managed Kubernetes, and databases in seconds. Test your containers and production workloads with $200 in free credits.

Claim $200 Free Credit →

Transparency: We independently test and operate all recommended infrastructure. If you use our partner links, you receive free promotional credits and support our testing lab at zero extra cost to you.

~$ subscribe --weekly-runbooks

Weekly Production DevOps Runbooks

Join 1,000+ infrastructure engineers receiving real-world Linux troubleshooting, Proxmox clustering setups, and Docker optimization runbooks every Tuesday. Zero spam, unsubscribe anytime.

Kamandanu Wijaya

About the Author

Kamandanu Wijaya

IT Infrastructure & Network Administrator

Infrastructure & network administrator with 15+ years of enterprise experience, focused on stability, security, and automation.

Certifications: Google IT Support, Cisco Networking Academy, DevOps.

$ share

Need IT Solutions?

DoWithSudo is ready to help setup servers, VPS, and your security systems.

Contact Us
[ 01 ] // More from the log

Related Posts

WhatsApp