How the Tunnel Really Works
Every conceptual question asked and answered during the guydev.ca Cloudflare Tunnel deployment — covering stateful connections, CGNAT, TCP vs UDP, TLS sessions, HTTP/2 multiplexing, and why port forwarding can never work on a Telus residential line.
Stateful pipe vs stateless HTTP — is the connection persistent?
Mostly right, and the intuition is sharp — but the precise mechanism is richer. There are two layers to separate cleanly.
What you got exactly right
cloudflared initiates the connection outward. That is the entire reason it works on Telus CGNAT — the gate has no problem with outbound traffic, only inbound. Once cloudflared has punched out through the gate and reached Cloudflare's edge, it holds that connection open permanently. Cloudflare never needs to initiate anything inward — it just uses the connection that is already sitting there waiting.
The two-layer picture
The transport layer — the raw TCP connection that cloudflared opens to Cloudflare's edge — stays open continuously. cloudflared sends keepalive signals so the connection never goes idle long enough to be closed. This is the tunnel itself. It is not stateless. It is a permanent pipe.
The application layer — individual visitor requests (someone loading guydev.ca) — travel through that pipe as separate, independent HTTP transactions. These are stateless in the HTTP sense. Each visitor request goes in one end of the pipe, gets forwarded to IIS, IIS sends the response back through the pipe, and that transaction is done. But the pipe itself stays open the whole time, ready for the next request.
QUIC vs HTTP/2 — why did the tunnel drop every 4–5 minutes?
The log tells the story precisely. All four connections registered successfully using QUIC (UDP). Exactly 4 minutes 35 seconds later, all four dropped simultaneously.
Root cause
Telus CGNAT is expiring your UDP NAT state after ~4–5 minutes of idle time. QUIC runs over UDP. The CGNAT device tracks UDP flows in a state table with a short idle timeout. When no traffic crosses that flow within the timeout window, the NAT entry is silently dropped. cloudflared's keepalive packets never reach Cloudflare's edge because the NAT device has already discarded the mapping. All four connections die together because they all share the same NAT device.
The fix
Add one line to config.yml to force HTTP/2 over TCP. TCP NAT state survives much longer on residential CGNAT devices (typically 30+ minutes), and TCP keepalives are honoured correctly.
After restarting, watch the startup logs — all four connections must show protocol=http2, not protocol=quic. Wait 6+ minutes idle and confirm no ERR lines appear.
UDP and TCP confusion — what actually changes between QUIC and HTTP/2?
The confusion is completely understandable because "the tunnel" and "the protocol carrying the tunnel" are two different things layered on top of each other. There are three distinct layers stacked on top of each other, and it is easy to conflate them.
The three layers
Layer 1 — Visitor HTTP request: stateless, identical in both modes. This is what the visitor's browser sends.
Layer 2 — cloudflared tunnel: the persistent pipe concept, identical in both modes. cloudflared's job does not change.
Layer 3 — Transport protocol: this is the only layer that changed. In the before state, the tunnel was wrapped in QUIC (which runs over UDP). In the after state, the tunnel is wrapped in HTTP/2 (which runs over TCP). CGNAT only ever sees this outer layer.
Why CGNAT behaves differently
TCP has a built-in handshake — SYN, SYN-ACK, ACK. When the CGNAT device sees a TCP connection open, it writes "ESTABLISHED" next to the logbook entry and sets a 30+ minute idle timer. TCP's own keepalive mechanism resets that timer. The entry never expires.
UDP has no handshake at all — it is fire-and-forget. CGNAT cannot tell whether a UDP flow is long-lived or a one-off. It applies a short, conservative timer of 4–5 minutes. When cloudflared goes quiet because no visitors are loading the site, the entry expires, the checkpoint forgets the flow, and all four connections collapse at the same moment.
Pack / unpack — how does cloudflared handle the request and response?
(2) Cloudflared will pack the HTTP response from laptop's IIS and then send it to Cloudflare through the pipe?
(3) Once Cloudflare receives the data, then unpack it and send to the visitor's browser in HTTP response format?
(4) It must be Cloudflared that initiates the connection to Cloudflare through TCP. If it is reversed, then Telus's CGNAT will reject it, am I right?
All four answers are yes. A few precise details worth adding:
On ①: cloudflared does not fully "unpack" in the sense of decrypting the visitor's data. It strips the tunnel's own encapsulation layer (the HTTP/2 framing that wraps the request for transport through the pipe), then makes a fresh, independent HTTPS connection to localhost:443. To IIS, it looks like any ordinary local client — IIS has no idea a visitor in another country is on the other end.
On ④: in Phase 2, Cloudflare does send data toward cloudflared — but it is not initiating a new connection. It is sending data on the TCP connection that cloudflared already opened in Phase 1. What CGNAT blocks is an entirely new inbound SYN packet arriving from Cloudflare's servers that has no matching outbound record in the state table. That packet would be silently dropped.
CGNAT logbook — how does return traffic get through?
Yes, exactly right. The moment cloudflared completes the TCP handshake outbound, both NAT devices write a state entry:
Every packet arriving from Cloudflare's IP on port 443 matching that entry is passed inward automatically. Neither NAT device reads the content of the packets — they do not know or care that what is flowing through is a visitor's HTTP request being proxied from Cloudflare. All they see at the transport layer is: "a packet arrived from a known destination on a known port matching an established TCP flow — let it through."
One small precision: the traffic from Cloudflare is not technically a "response" in the HTTP sense — it can carry new visitor requests arriving minutes or hours after the original TCP handshake. But from the NAT devices' perspective at the TCP layer, it is always return traffic on an established connection, which is indistinguishable from a genuine response. That distinction — HTTP stateless requests riding inside a stateful TCP connection — is the core layering of this entire setup.
192.168.x.x to 100.x.x.x, and Telus CGNAT translating from 100.x.x.x to 142.x.x.x. cloudflared's outbound TCP crosses both, both write entries, and from that point both allow the return path.
Can port forwarding on the Telus router avoid all this work?
No — and the reason is fundamental to what CGNAT actually is. Port forwarding on your home router does nothing against CGNAT.
Your home router has a port forwarding feature. The problem is that your router's WAN interface is not on the public internet. It has a private address in the 100.64.x.x range — that is the RFC 6598 CGNAT address space — assigned by Telus's equipment sitting upstream. You are forwarding a port on a private address that no outside machine can reach in the first place.
Port forwarding only moves traffic between your router and your laptop — it does nothing to the wall upstream of your router. Telus's CGNAT equipment is shared infrastructure serving thousands of customers simultaneously. There is no support ticket, no settings panel, and no phone call that changes this. It is a fundamental property of residential internet on Telus.
| Cloudflare Tunnel (current) | Telus Business Static IP | |
|---|---|---|
| Monthly cost | Free | ~$80–150 CAD/month more |
| Setup | Done — already working | Port forward + firewall rules |
| Security | No inbound ports open | Port 443 exposed to internet |
| DDoS protection | Cloudflare absorbs it | Your laptop absorbs it |
| TLS certificate | Automatic via Cloudflare | You manage it yourself |
The WAN IP as the smoking gun for CGNAT
Exactly right. That 100.x.x.x address is the smoking gun that tells you immediately you are behind CGNAT. The 100.64.0.0/10 address block — covering 100.64.x.x through 100.127.x.x — was reserved by the IETF specifically for carrier-grade NAT. It is called the "shared address space." When you log into your Telus home router and look at the WAN IP, if it starts with 100., you are definitively behind CGNAT.
You can verify this by comparing your router's WAN IP with your true public IP:
Compare the result to your router's WAN IP. On a Telus residential connection they will always be different — confirming CGNAT.
There are actually two layers of NAT between your laptop and the public internet:
cloudflared's outbound TCP connection crosses both NAT boundaries going out, both NAT devices record the flow in their state tables, and from that point forward the established connection is bidirectional. The two-NAT problem simply does not exist for outbound-initiated persistent connections.
All traffic from Cloudflare treated as return traffic — the logbook entry
Yes, exactly right. And there is one important precision to add to your model: the traffic from Cloudflare is not technically a "response" in the HTTP sense — it can carry new visitor requests arriving minutes or hours after the original TCP handshake. But from the NAT devices' perspective at the TCP layer, it is always return traffic on an established connection, which is indistinguishable from a genuine response.
Cloudflare is exploiting the fact that TCP's return path is always allowed, and cloudflared's tunnel rides that return path permanently. The NAT devices only ever see the outer TCP layer, and that layer says: "established — allow it."
The content is completely opaque to them. They do not know or care that what is flowing through is a visitor's HTTP request being proxied from Japan, Canada, or anywhere else. All they see is: "a packet arrived from a known destination on a known port matching an established TCP flow — pass it through."
Correcting the bidirectional flow diagram
Three adjustments needed. The sequence of entities is correct. The labels on each leg need updating:
Adjustment ① and ③ — both end legs are HTTPS, not HTTP
The visitor's browser always connects to Cloudflare over HTTPS — that is the padlock the visitor sees, issued by Cloudflare's certificate. On the other side, in this setup IIS forces an HTTP→HTTPS redirect, so cloudflared connects to localhost:443 with noTLSVerify: true to accept the self-signed certificate.
Adjustment ② — the middle leg is not just "TCP connection"
TCP is only the transport layer underneath. What rides on top of TCP is HTTP/2 — the protocol cloudflared uses to multiplex multiple visitor requests over the 4 persistent connections. On top of that, the tunnel has its own TLS encryption. So more precisely: HTTP/2 frames, encrypted with TLS, transported over TCP.
Important consequence
Because there are three separate TLS sessions, Cloudflare is not a dumb pipe — it fully decrypts the visitor's request at its edge in order to read the Host header, apply security rules, and decide which tunnel to route it through. It then re-encrypts the request for the tunnel leg. This means you are trusting Cloudflare to see your plaintext traffic. The payoff is DDoS protection and a valid public certificate.
The three TLS sessions and what HTTP/2 is
Session 1 — Visitor to Cloudflare
The visitor's browser opens a standard HTTPS connection to Cloudflare's servers. Cloudflare presents its own TLS certificate — the padlock the visitor sees. Cloudflare fully decrypts this traffic at its edge. This is where Cloudflare reads the Host header to decide which tunnel to route the request through.
Session 2 — Cloudflare to cloudflared
The tunnel pipe itself is encrypted with a separate TLS session using a Cloudflare-issued origin certificate. This protects the data travelling between Cloudflare's edge and your laptop. cloudflared initiated this session outbound in Phase 1 and keeps it alive permanently.
Session 3 — cloudflared to IIS
cloudflared makes a fresh HTTPS connection to localhost:443 on your laptop. IIS presents a self-signed certificate — which is why noTLSVerify: true is in your config.yml, telling cloudflared to accept it without checking who signed it. This session is entirely local, never leaves the machine.
HTTP vs HTTP/2
HTTP/1.1 works like a single-lane road with a rule that only one car can travel at a time. You send a request, wait for the full response, then send the next request. Browsers worked around this by opening six or more parallel TCP connections — but each had its own overhead.
HTTP/2 works like a motorway. One single TCP connection stays open, and inside it you can send hundreds of requests simultaneously as independent numbered streams. The server sends back responses in whichever order it finishes — responses do not need to arrive in the order requests were sent. This is called multiplexing.
For cloudflared specifically, HTTP/2 means all four tunnel connections and all visitor requests ride inside one persistent TCP pipe. That single TCP pipe is what CGNAT sees, writes one state entry for, and keeps alive.
Correction — HTTP/1.1 would not actually cause CGNAT problems
You are completely right on both points. That statement was wrong and needs correcting.
What HTTP/2 actually improves over HTTP/1.1 — the real difference
The genuine difference is about concurrency inside each connection, not about CGNAT survival.
With HTTP/1.1, each of the 4 connections can carry only one visitor request at a time. If 10 visitors arrive simultaneously, visitors 5 through 10 queue and wait. Maximum concurrency: 4 at a time.
With HTTP/2, each connection carries unlimited streams simultaneously. All 10 visitors are served in parallel without queuing. Maximum concurrency: effectively unlimited.
Why cloudflared uses QUIC in the first place then?
QUIC takes HTTP/2's stream multiplexing but builds it over UDP. The theoretical advantage: in HTTP/2 over TCP, one packet loss stalls all streams on that connection because TCP guarantees ordered delivery. In QUIC over UDP, one packet loss only stalls the affected stream — the others continue. In practice on a stable connection this advantage is negligible, which is why switching to HTTP/2 solved the CGNAT problem with no noticeable performance cost.
Why 4 connections — and can it be adjusted for busier traffic?
Why exactly 4
The number 4 is hardcoded by Cloudflare's design, not configurable by the user. The reasoning behind it is resilience, not throughput. Each of the 4 connections goes to a different Cloudflare edge server. If one edge server has a problem — hardware fault, network blip, software crash — the other 3 connections keep the tunnel alive without any interruption visible to visitors. Cloudflare's edge automatically reroutes incoming traffic to whichever connections are healthy.
Can you adjust it for busier traffic?
No — and more connections would not help throughput anyway. The bottleneck in this setup is never the tunnel connections. It is one of two other things: either your laptop's CPU and RAM serving IIS responses, or your home internet upload bandwidth delivering those responses back through the tunnel.
Adding a 5th or 6th connection to Cloudflare's edge does nothing to either of those constraints. HTTP/2 already lets you pack unlimited visitor streams into each of the 4 lanes. Adding lanes when the lanes are not the bottleneck is pointless.
| Real bottleneck | Correct fix |
|---|---|
| IIS slow to respond | Add caching, optimise the app, increase IIS thread pool |
| Upload bandwidth saturated | Upgrade Telus plan for more upload capacity |
| Single machine is the limit | Run cloudflared on multiple machines — each creates its own 4-connection tunnel, Cloudflare load-balances (paid feature) |
| Route different traffic differently | Use cloudflared ingress rules to route different paths to different local services |