cloudflare / cloudflare/cloudflared
🐛 --protocol auto does not fall back to HTTP/2 when the QUIC handshake is rejected with CRYPTO_ERROR 0x178 (tls: no application protocol), and the UDP precheck reports it as healthy
- Dominant language
- Go
- Stars
- 15.6k
- Forks
- 1.4k
- PR merge metrics
- No merged PRs in 30d
Description
## Describe the bug
On a network where the QUIC handshake to the tunnel edge is **rejected during the TLS handshake** (rather than blackholed), `--protocol auto` never falls back to HTTP/2. It retries QUIC indefinitely with exponential backoff, registers no connections, and the tunnel stays down.
Two distinct observations, in the same run and the same second:
1. **No fallback.** Every dial fails with `CRYPTO_ERROR 0x178 (remote): tls: no application protocol` — TLS alert 120, `no_application_protocol` — and `auto` keeps retrying QUIC instead of switching transports.
2. **The startup precheck reports the opposite of reality.** At the very moment those dials are failing, `UDP Connectivity` is reported `status=pass` with `details="QUIC connection successful"` for both regions, and the run concludes `suggested_protocol=quic`.
This looks like the second causes the first: the precheck's UDP component appears to test reachability rather than a real tunnel handshake, so the transport-selection logic is told QUIC is usable while it demonstrably is not.
## Log
`--protocol auto`, cloudflared 2026.7.2. `run_id` elided, timestamps verbatim:
```
15:51:33Z INF Version 2026.7.2 (Checksum 809407c3d6e78787e2b14863ea6a1d4f31712186b5fd056815d093e29e8bff26)
15:51:33Z INF GOOS: darwin, GOVersion: go1.26.5, GoArch: amd64
15:51:34Z ERR Failed to dial a quic connection error="failed to dial to edge with quic: CRYPTO_ERROR 0x178 (remote): tls: no application protocol" connIndex=0 ip=198.41.200.33
15:51:34Z INF Retrying connection in up to 2s connIndex=0 ip=198.41.200.33
15:51:35Z ERR Failed to dial a quic connection error="... tls: no application protocol" connIndex=0 ip=198.41.200.193
15:51:35Z INF Retrying connection in up to 4s connIndex=0 ip=198.41.200.193
15:51:36Z ERR Failed to dial a quic connection error="... tls: no application protocol" connIndex=0 ip=198.41.200.23
15:51:36Z INF Retrying connection in up to 8s connIndex=0 ip=198.41.200.23
15:51:40Z ERR Failed to dial a quic connection error="... tls: no application protocol" connIndex=0 ip=198.41.192.67
15:51:40Z INF Retrying connection in up to 16s connIndex=0 ip=198.41.192.67
15:51:40Z INF precheck component="DNS Resolution" details="DNS Resolved successfully" status=pass target=region1.v2.argotunnel.com
15:51:40Z INF precheck component="UDP Connectivity" details="QUIC connection successful" status=pass target=region1.v2.argotunnel.com
15:51:40Z INF precheck component="UDP Connectivity" details="QUIC connection successful" status=pass target=region2.v2.argotunnel.com
15:51:40Z INF precheck component="TCP Connectivity" details="HTTP/2 connection successful" status=pass target=region1.v2.argotunnel.com
15:51:40Z INF precheck complete hard_fail=false suggested_protocol=quic
```
The backoff kept growing (observed up to `1m4s`). No `Registered tunnel connection` line ever appeared, over more than fifteen minutes, until the protocol was pinned to `http2` by hand — after which all four connections registered within two seconds.
## Expected behavior
After repeated QUIC dial failures, `--protocol auto` should fall back to HTTP/2, as documented in [Run parameters](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/configure-tunnels/cloudflared-parameters/run-parameters/). A TLS-level rejection of the QUIC handshake seems not to be classified as "QUIC is unusable" the way a timeout is.
Secondarily: `UDP Connectivity` should not report `pass` / `QUIC connection successful` on a path where the tunnel's own QUIC handshake is being rejected. As it stands the precheck is actively misleading — it was the reason we believed QUIC was viable here.
## To Reproduce
1. Be on a network that rejects the QUIC handshake to the edge rather than dropping the packets. Ours is a mobile carrier (see below) where UDP to `198.41.192.0/24` / `198.41.200.0/24` on 7844 is reachable but the handshake is refused.
2. `cloudflared tunnel --protocol auto run --token-file ...`
3. Observe repeated `CRYPTO_ERROR 0x178`, growing backoff, no fallback, no registered connections.
`--protocol http2` on the same host and network works perfectly and has done for months.
## How this differs from #749
[#749](https://github.com/cloudflare/cloudflared/issues/749) (closed, 2022) is the same expectation but a different trigger: there UDP was *blocked* and the symptom was `i/o timeout`. Here UDP reaches the edge and the failure happens inside the TLS handshake, which is presumably why it is classified differently — and why the precheck passes.
## Environment and versions
- OS: macOS 15.7.7
- Architecture: x86_64
- Version: **2026.7.2**, reproduced on that build.
- Also on **2026.8.3**: `--protocol quic` produces the identical `CRYPTO_ERROR 0x178`. `--protocol auto` was **not** retested on 2026.8.3 — this is a production tunnel and each attempt takes the service down, so I did not want to run it again blind. Worth noting that on 2026.8.3 the precheck did, in a later run, correctly report `UDP Connectivity ... QUIC connection failed` with `suggested_protocol=http2`, so the precheck behaviour may already differ between the two builds.
## Additional context
The network-side cause is almost certainly upstream of cloudflared and I am not asking you to fix it — QUIC from this uplink reaches Google, Facebook, Akamai and Vultr over HTTP/3 fine, but times out to Cloudflare (`104.18.0.0/16`, `1.1.1.0/24`) and Fastly (`151.101.0.0/16`). Path MTU is 1500 to all of them.
The report is only about the two client-side behaviours: the missing fallback, and a precheck that reports a path as healthy while that same path is failing in the same second.
Contributor guide
Research direction
Start at the --protocol auto entry point and trace the startup UDP precheck alongside QUIC dial error classification and transport selection. Reproduce the CRYPTO_ERROR 0x178 case, then verify that repeated QUIC handshake failures select HTTP/2 and that the precheck no longer reports QUIC as successful when the tunnel handshake is rejected.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- cli, networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100