Skip to content

Fall back to HTTP/2 when the edge rejects the QUIC handshake - #1744

Open
Shubham-Padkonde wants to merge 1 commit into
cloudflare:masterfrom
Shubham-Padkonde:fix-fallback-on-crypto-error
Open

Shubham-Padkonde wants to merge 1 commit into
cloudflare:masterfrom
Shubham-Padkonde:fix-fallback-on-crypto-error

Conversation

@Shubham-Padkonde

@Shubham-Padkonde Shubham-Padkonde commented Sep 20, 2026

Copy link
Copy Markdown

Fixes #1736

Problem

With --protocol auto, a QUIC handshake that the peer rejects (rather than blackholes) never triggers protocol fallback. The reporter's tunnel retried QUIC indefinitely with exponential backoff and never registered a connection:

ERR Failed to dial a quic connection error="failed to dial to edge with quic: CRYPTO_ERROR 0x178 (remote): tls: no application protocol"
INF Retrying connection in up to 2s
ERR Failed to dial a quic connection error="... tls: no application protocol"
INF Retrying connection in up to 4s
...

selectNextProtocol switches protocol when protocolBackoff.ReachedMaxRetries() or when isQuicBroken(cause) and a fallback exists. isQuicBroken recognises only two shapes today — *quic.IdleTimeoutError (UDP blackholed) and a *quic.TransportError whose text contains operation not permitted. A remote CRYPTO_ERROR matches neither, so the else arm runs and reassigns the current protocol (still QUIC), and the loop retries QUIC forever.

0x178 is CRYPTO_ERROR for TLS alert 120, no_application_protocol: something on the path terminated the TLS handshake because it doesn't offer the ALPN cloudflared needs. No amount of QUIC retrying recovers from that, and HTTP/2 would work — which is exactly what auto exists to do.

Fix

Treat a CRYPTO_ERROR raised by the peer as a broken QUIC transport, next to the existing cases:

if transportErr, ok := errors.AsType[*quic.TransportError](cause); ok &&
    transportErr.Remote && transportErr.ErrorCode.IsCryptoError() {
    return true
}

The check is deliberately narrow: only Remote errors (the peer rejected us, so our own local crypto failures are unaffected) and only the CRYPTO_ERROR range (0x1000x1ff). Users who pin --protocol quic still have no fallback and keep retrying, since selectNextProtocol returns early when the selector offers none.

The second half of the issue — the UDP Connectivity precheck reporting status=pass while these dials fail — is a separate code path and is not addressed here.

Test

TestFallbackOnRemoteCryptoError drives selectNextProtocol with an EdgeQuicDialError wrapping a remote TransportError of code 0x178, exactly as the dial path reports it, and asserts the connection moves to HTTP/2 on the first failure.

Before:

--- FAIL: TestFallbackOnRemoteCryptoError
    tunnel_test.go:129: expected: 0   (HTTP2)
                        actual  : 1   (QUIC)

After:

$ go test ./supervisor/...
ok   github.com/cloudflare/cloudflared/supervisor

go vet and gofmt are clean. TestHTTP2ConfigurationSet in the connection package fails identically on an unmodified tree in my environment, so it is unrelated.


Disclosure: this change was written by Claude Code (Claude Opus 5) working as my agent, at my direction. The test, lint and reproduction output quoted above comes from real runs in my local environment; I am accountable for what is submitted here and will follow up on review feedback.

🤖 Generated with Claude Code

`--protocol auto` only leaves QUIC when `isQuicBroken` recognises the
failure: an idle timeout, or a transport error caused by "operation not
permitted". A handshake the peer rejects with a CRYPTO_ERROR -- for
example TLS alert 120, no_application_protocol, from a middlebox that
intercepts QUIC without offering the tunnel's ALPN -- matches neither,
so `selectNextProtocol` keeps choosing the current protocol and the
connection retries QUIC forever while the tunnel stays down.

Treat a CRYPTO_ERROR raised by the peer as a broken QUIC transport, so
the connection switches to the fallback protocol on the first failure
like it does for the other unrecoverable cases.

Fixes cloudflare#1736

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant