LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Tailscale Funnel vs Cloudflare Tunnels: Two Ways to Expose a Service Through NAT

tailscalecloudflaretunnelsfunnelnat-traversalhomelabzero-trustnetworkingself-hosted

Tailscale Funnel and Cloudflare Tunnels look interchangeable from a distance. Both let a service sitting behind a residential router — or worse, behind carrier-grade NAT where you do not even own a routable address — answer requests from the open internet. Neither one forwards a port. Neither one needs a static IP or a dynamic-DNS client babysitting your changing address. In both cases a small agent on your box dials out to a provider’s network, and the provider hands inbound traffic back down that already-open connection. That shared shape is where the similarity ends. Cloudflare Tunnels is a reverse proxy bolted onto a global CDN that terminates your TLS at the edge and can gate every request behind an identity provider. Tailscale Funnel is a public on-ramp welded to the side of a private WireGuard mesh, and it deliberately does not decrypt your traffic or know who is knocking. Picking between them is not a matter of taste. It is a decision about who holds your plaintext, what your threat model tolerates, and how much throughput you actually need.


The Shared Problem: NAT, CGNAT, and Why Port Forwarding Lost

For two decades the answer to “expose my home service” was port forwarding. You logged into the router, mapped external port 443 to an internal host, registered a dynamic-DNS hostname so your rotating residential IP stayed resolvable, and stood up a reverse proxy with a Let’s Encrypt certificate. It worked until it did not. Bot scanners find a freshly opened 443 within hours and start probing for known CVEs. The DDNS client fails silently at the exact moment you are away and need access. A reverse-proxy misconfiguration leaks an admin panel you assumed was internal. And the whole arrangement publishes, via public DNS, the IP address of the building you sleep in.

Then carrier-grade NAT made the old approach impossible for a growing share of people. When your ISP puts you behind CGNAT, you share a public IPv4 address with hundreds of other subscribers and have no routable address to forward a port to. There is no port-forwarding screen that helps; the listening socket the world would connect to does not exist on equipment you control.

Both Tailscale Funnel and Cloudflare Tunnels sidestep the entire problem by inverting the direction of connection setup. Instead of the world dialing into you — which NAT and CGNAT block — your agent dials out to the provider and keeps that connection alive. Inbound requests ride back down the tunnel the agent already established. No inbound port is open on your firewall, your home IP never appears in public DNS, and CGNAT is irrelevant because nothing ever tries to reach you directly. The provider becomes your front door. The only real question left is what kind of front door you want.


How Cloudflare Tunnels Actually Works

Cloudflare Tunnels (the product formerly called Argo Tunnel) runs a daemon called cloudflared on your host. On startup it opens several outbound connections — QUIC by default, falling back to HTTP/2 — to the nearest Cloudflare data centers. Those connections stay open. When a request arrives for one of your hostnames at Cloudflare’s edge, the edge multiplexes it down an existing cloudflared connection, and the daemon forwards it to the local service you mapped.

Setup is genuinely quick. You authenticate, create a named tunnel, point DNS at it, and describe your ingress rules:

1
2
3
cloudflared tunnel login
cloudflared tunnel create homelab
cloudflared tunnel route dns homelab app.example.com
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# ~/.cloudflared/config.yml
tunnel: homelab
credentials-file: /root/.cloudflared/8f3c....json

ingress:
  - hostname: app.example.com
    service: http://localhost:8080
  - hostname: grafana.example.com
    service: http://localhost:3000
  - service: http_status:404   # catch-all, required last rule
1
cloudflared service install   # run as a systemd unit, reconnects on its own

The DNS route creates a proxied CNAME pointing your hostname at <tunnel-uuid>.cfargotunnel.com. From that moment, app.example.com resolves to Cloudflare’s anycast addresses, and Cloudflare alone knows how to reach the tunnel behind them.

The architecturally decisive fact is where TLS terminates. The browser negotiates TLS with Cloudflare’s edge using a Cloudflare-managed certificate. Cloudflare decrypts the request, sees the plaintext HTTP — headers, paths, cookies, bodies — applies WAF rules, caching, and DDoS scrubbing, then re-encrypts to your origin if you configured an HTTPS local service. This is not a flaw; it is the entire value proposition. Cloudflare can only filter, cache, and authenticate traffic it can read. But it means Cloudflare is, by design, an inline party with technical access to everything flowing through.

That access is what powers Cloudflare Access, the Zero Trust auth layer you can put in front of any hostname. Access intercepts the request before your origin sees it, redirects unauthenticated users to an identity provider — Google Workspace, GitHub, a one-time PIN over email — and only forwards the request after issuing a signed JWT. Your service can be a stock app with no login of its own, and Access turns it into something only your chosen identities can reach. The free Zero Trust plan covers 50 seats, which no personal or family deployment will exhaust.


How Tailscale Funnel Actually Works

Tailscale is a mesh VPN built on WireGuard. Every device you enroll joins a private network — a tailnet — and gets a stable 100.x.y.z address plus a MagicDNS name. A coordination server exchanges public keys and helps peers find each other; a fleet of DERP relays carries traffic when two peers cannot establish a direct WireGuard path through their NATs. By default everything in a tailnet is private. Only your own authenticated devices can talk to each other.

tailscale serve is the first step toward exposure: it publishes a local service to the rest of your tailnet over HTTPS, using a certificate for your node’s *.ts.net name. That is still private — only devices logged into your tailnet can reach it. Funnel is the switch that opens that same service to the entire public internet:

1
2
3
4
5
6
7
# expose localhost:8080 to your tailnet over HTTPS
tailscale serve --bg --https=443 http://localhost:8080

# now open it to the public internet
tailscale funnel --bg 443

tailscale funnel status

Funnel is gated by your tailnet’s ACL policy. A node may only use it if granted the funnel node attribute:

1
2
3
4
5
6
7
8
{
  "nodeAttrs": [
    {
      "target": ["autogroup:member"],
      "attr":   ["funnel"]
    }
  ]
}

Here is the part that makes Funnel architecturally the opposite of Cloudflare. A public client connects to a Tailscale ingress node on port 443. That ingress node reads only the TLS ClientHello — specifically the SNI field — to decide which tailnet and node the connection belongs to. It then proxies the raw, still-encrypted TLS bytes over the WireGuard mesh to your node, and your node terminates TLS using its own *.ts.net certificate. Tailscale’s relays move ciphertext. They never hold your plaintext. The encryption is effectively end to end, from the visitor’s browser to a process on your own hardware.

That design buys privacy at the cost of capability. Because Tailscale never decrypts, it cannot offer a WAF, cannot cache, and cannot — for a Funnel connection — insert an identity check. Funnel is deliberately anonymous: anyone who knows the *.ts.net URL reaches your service directly, with no login challenge in front of it. The strong, identity-based access control that makes Tailscale excellent applies to serve (tailnet-only), not to funnel. Funnel is the explicit escape hatch where you give up the private mesh’s identity guarantees in exchange for reaching people who are not on your tailnet.


The Data Plane: Who Sees Your Bytes

Strip away the marketing and the difference is a single question — does the provider decrypt your traffic? The answer shapes everything downstream.

CLOUDFLARE TUNNEL  (TLS terminates at the edge)

  Browser ==TLS==> [ Cloudflare edge ]  --re-encrypt-->  cloudflared --> service
                         |  decrypts here
                         |  WAF / cache / Access / DDoS scrub
                         v
                   sees plaintext HTTP


TAILSCALE FUNNEL  (TLS terminates on your node)

  Browser ==TLS=========================================> your node --> service
                   [ Tailscale ingress ]                    |  decrypts here
                         |  SNI routing only                v
                         |  proxies ciphertext         sees plaintext HTTP
                         v
                   never decrypts

With Cloudflare, plaintext exists at the edge. For a public blog, a status page, or a family photo gallery, that is a routine and reasonable trade — you gain DDoS protection and global caching, and the content was not secret anyway. For an administrative dashboard, a password manager, or anything touching credentials, routing decrypted traffic through a third party is a real consideration that deserves a deliberate decision rather than a default.

With Funnel, your node is the only place plaintext appears. Nobody between the browser and your process can read the request. That is a meaningfully stronger confidentiality posture, and it is the right default when the data is sensitive and the audience is small. The price is that you also forfeit everything the middle could have done for you: no scrubbing of a volumetric flood before it reaches your link, no edge cache, no identity wall you did not build yourself.


The Identity and Auth Models

This is where people most often choose wrong, because the two products put identity in different places.

Cloudflare attaches identity at the edge, in front of the origin. Access evaluates a policy before your service is touched, and it integrates with real identity providers and SSO. You write rules like “anyone in my Google Workspace org” or “these three email addresses, plus require a valid second factor,” and Cloudflare enforces them globally. The service behind it can be authentication-naive.

Tailscale attaches identity to the device, inside the tailnet. Every peer is a cryptographically authenticated machine tied to a user account, and ACLs decide who reaches what. This is a superb model — but it only applies while traffic stays private via serve. The instant you flip on funnel, you have stepped outside the identity perimeter on purpose. Funnel traffic is anonymous public traffic. If you need to know who is connecting through a Funnel, you must implement that yourself in the application or with an authenticating reverse proxy on your own node.

That asymmetry yields a clean rule of thumb. If your audience can all be invited to your tailnet — you, your family, a handful of collaborators willing to install Tailscale — you almost never want Funnel at all; you want serve, and you get device-level zero-trust for free. You reach for Funnel only when the audience genuinely cannot be enrolled: a webhook from a third-party SaaS, a public demo link, a service a stranger must open. Cloudflare Access, by contrast, gates strangers with their own existing identities, no client install required, which is exactly the gap Funnel leaves open.


Limits, Performance, and Cost

The operational envelopes differ sharply, and the differences track the architectures.

Dimension Cloudflare Tunnels Tailscale Funnel
Allowed public ports 80/443 for proxied HTTP(S); arbitrary TCP/UDP via cloudflared access client 443, 8443, 10000 only
Protocols HTTP/HTTPS first-class; TCP/UDP need the WARP/Access client at the other end Anything over TLS on the allowed ports; client needs no special software
TLS terminated by Cloudflare edge Your own node
Path to your box Cloudflare anycast edge, 300+ cities Tailscale DERP/ingress relays, dozens of regions
Throughput posture High; backed by CDN, built for traffic Modest; relayed, explicitly not for high-bandwidth use
Identity gating Built in via Access (50 free seats) None on Funnel; build it yourself
DDoS / WAF / cache Yes, at the edge No
Bandwidth cost (homelab) Free, no practical cap for normal use Free on personal plans
Public hostname Your own domain node.tailnet.ts.net (your domain needs extra proxying)

Two entries deserve elaboration. First, ports: Funnel is hard-limited to 443, 8443, and 10000 for public ingress, and Funnel is fundamentally about TLS — you are exposing an HTTPS endpoint, full stop. Cloudflare proxies HTTP(S) natively and can carry arbitrary TCP or UDP, but the non-HTTP path requires the person connecting to run Cloudflare’s client, which makes it useful for your own remote access to SSH or RDP rather than for exposing a service to the anonymous public.

Second, throughput. Cloudflare’s whole business is moving traffic; a Tunnel rides infrastructure built to absorb floods and serve cache hits worldwide. Tailscale Funnel rides shared DERP relays, and Tailscale is explicit that Funnel is not intended for high-bandwidth workloads. A Funnel is perfect for a low-traffic dashboard, a webhook receiver, or a personal app a few people hit. Put a popular file download or a video stream behind it and you will feel the relay. This is not a bug — it reflects the fact that all Funnel bytes are relayed precisely because Tailscale refuses to decrypt and proxy at the application layer.

On cost, both are free for realistic homelab use. Cloudflare’s free tier carries no bandwidth cap for ordinary traffic, though its self-serve terms (the long-standing Section 2.8) frown on using the free CDN to serve large volumes of non-HTML media like video and big binaries; a homelab dashboard never approaches that line. Tailscale’s personal plans include Funnel at no charge.


Setup Compared, Side by Side

Beyond the daemon commands shown earlier, the day-two operational shapes differ. Cloudflare wants you to think in hostnames and ingress rules; one cloudflared instance fronts many services, each a stanza in config.yml, each its own DNS record under your domain. Adding a service is editing the ingress list and adding a route:

1
2
3
# add a new service to an existing tunnel
cloudflared tunnel route dns homelab vault.example.com
# then append an ingress rule and reload the service

Tailscale wants you to think in nodes and paths. Each node runs its own tailscale serve/funnel configuration, and you expose paths on that node’s ts.net name:

1
2
3
4
# put two apps under one node, different paths
tailscale serve --bg --https=443 --set-path=/app  http://localhost:8080
tailscale serve --bg --https=443 --set-path=/grafana http://localhost:3000
tailscale funnel --bg 443

The mental models matter for growth. A sprawling multi-service homelab maps cleanly to Cloudflare’s central ingress file and per-service hostnames on a domain you own. A few self-contained boxes each exposing one thing map cleanly to Tailscale’s per-node model — at the cost of *.ts.net URLs unless you front them with your own domain and certificates.


Failure Modes Nobody Mentions

Every abstraction leaks. Here is where each one does.

Cloudflare’s leaks are about trust and dependency. The edge holds your plaintext, so a compromise, a subpoena, or an internal access incident on Cloudflare’s side is in your threat surface for everything you route through it. Your availability is coupled to Cloudflare’s control plane; the rare-but-real edge outage takes your services with it, and there is no failover unless you build one. Your domain’s DNS lives at Cloudflare, which is convenient until it is a single point of management. And the free CDN’s terms can, in principle, be enforced against someone using a Tunnel to serve heavy media — unlikely for a homelab, but not a guarantee.

Tailscale’s leaks are about exposure and capacity. A Funnel is genuinely public and genuinely undefended at the edge: there is no WAF, no rate limiting, and no anonymity for the URL itself. Your *.ts.net certificate is logged in public Certificate Transparency logs, so the hostname is discoverable by anyone watching CT streams — security through an obscure URL is no security at all. Because everything is relayed, a sudden traffic spike does not get scrubbed upstream; it lands on a relay and then on your link. And because identity is your job on a Funnel, forgetting to add application auth means you have published an open service to the world. The most common real-world Funnel incident is exactly that: someone exposes an admin tool “just to test from their phone,” never adds auth, and forgets the URL is public.

A subtler shared failure mode: both make your service reachable from anywhere, which is the point, but also means a vulnerability in the exposed app is now reachable from anywhere. Neither tunnel patches your software. Cloudflare’s WAF buys you some generic protection; Funnel buys you none. Keep the thing behind the tunnel patched regardless.


Which to Use for What

The decision collapses to a few honest questions.

Can everyone who needs access install Tailscale? If yes, you probably do not want Funnel at all — use tailscale serve and enjoy device-level zero-trust with no public exposure whatsoever. This is the best outcome and people forget it is on the table.

Is the audience anonymous strangers, and is the content public anyway? A blog, a status page, a public demo, a webhook receiver from a SaaS that will not join your tailnet. Either tool works; Cloudflare adds free DDoS protection, caching, and a clean hostname on your own domain, which makes it the stronger default for genuinely public, traffic-bearing services.

Is the data sensitive, the audience small, and third-party plaintext access unacceptable? This is Funnel’s sweet spot — or better, serve if the audience can enroll. End-to-end encryption to your own node, no provider in the clear, modest throughput that a private dashboard never strains.

Do you need to gate strangers with their own identities and no client install? Only Cloudflare Access does this cleanly. Funnel cannot, and bolting auth onto a public Funnel by hand is more work and more risk than letting Access do it.

For most homelabs the mature answer is both, by role: Cloudflare Tunnels for the world-facing, traffic-bearing, identity-gated surface, and Tailscale — usually serve, occasionally funnel — for private administrative access and the rare sensitive endpoint you want encrypted end to end. They are not competitors so much as different tools that happen to share a problem statement.


Verdict

Tailscale Funnel and Cloudflare Tunnels answer the same question — how do I reach a service behind NAT without forwarding a port — and then disagree about everything that matters. Cloudflare terminates your TLS at a global edge, which lets it filter, cache, scrub floods, and gate strangers with their own identities through Access, at the cost of being an inline party that holds your plaintext and an availability dependency you do not control. Tailscale Funnel terminates TLS on your own node and merely SNI-routes ciphertext through its relays, which gives you end-to-end encryption and no third-party access at the cost of modest relayed throughput, a fixed handful of ports, a discoverable ts.net hostname, and zero edge defenses or identity gating.

Choose by threat model, not by habit. If the content is public and you want it fast, defended, and gated, reach for Cloudflare. If the content is sensitive, the audience is small, and you refuse to hand a provider your plaintext, reach for Tailscale — and reach for serve instead of funnel whenever the people who need access can simply join your tailnet, because the best NAT-traversal exposure is often no public exposure at all. The operators who get this right tend to run both: Cloudflare facing the world, Tailscale guarding the private mesh, each doing the job its architecture was actually built for.


Sources

Comments