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

Open
#1,736 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.