From 100da2a3c6daf1c34efb685a988be14844839822 Mon Sep 17 00:00:00 2001 From: Ahmed Laban <79754427+amlaban65@users.noreply.github.com> Date: Mon, 26 Oct 2026 10:33:29 -0400 Subject: [PATCH 1/2] fix(connection): keep HTTP/2 MaxConcurrentStreams within int32 cloudflared configures its HTTP/2 tunnel server with MaxConcurrentStreams = math.MaxUint32. With Go 1.27 and golang.org/x/net v0.61.0, http2.Server is backed by net/http, which converts the limit to an int and replaces anything above math.MaxInt32 with its default of 250. Each HTTP/2 tunnel connection was therefore silently limited to 250 concurrent streams. Use math.MaxInt32 instead. Fixes #1777 Signed-off-by: Ahmed Laban --- connection/connection.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/connection/connection.go b/connection/connection.go index 24ae9b54c47..37267fe017e 100644 --- a/connection/connection.go +++ b/connection/connection.go @@ -24,7 +24,11 @@ const ( lbProbeUserAgentPrefix = "Mozilla/5.0 (compatible; Cloudflare-Traffic-Manager/1.0; +https://www.cloudflare.com/traffic-manager/;" LogFieldConnIndex = "connIndex" MaxGracePeriod = time.Minute * 3 - MaxConcurrentStreams = math.MaxUint32 + // MaxConcurrentStreams is the HTTP/2 stream limit advertised to the edge. + // It must fit in an int32: http2.Server implementations backed by + // net/http (Go 1.27+) silently replace larger values with their default + // of 250 streams. + MaxConcurrentStreams = math.MaxInt32 contentTypeHeader = "content-type" contentLengthHeader = "content-length" From 79be387a6757a13ffec61526c83c0316aec9ae0d Mon Sep 17 00:00:00 2001 From: Ahmed Laban <79754427+amlaban65@users.noreply.github.com> Date: Mon, 26 Oct 2026 10:35:49 -0400 Subject: [PATCH 2/2] test(connection): check the HTTP/2 stream limit sent to the edge Read the SETTINGS frame the HTTP/2 server actually sends and check that SETTINGS_MAX_CONCURRENT_STREAMS equals MaxConcurrentStreams. Checking the configured http2.Server field alone would not catch the fallback to 250 streams. Signed-off-by: Ahmed Laban --- connection/http2_test.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/connection/http2_test.go b/connection/http2_test.go index 9a4b3f9d756..1074a98c213 100644 --- a/connection/http2_test.go +++ b/connection/http2_test.go @@ -18,6 +18,7 @@ import ( "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/net/http2" "github.com/cloudflare/cloudflared/client" "github.com/cloudflare/cloudflared/tracing" @@ -101,6 +102,42 @@ func TestHTTP2ConfigurationSet(t *testing.T) { wg.Wait() } +// TestHTTP2AdvertisedMaxConcurrentStreams checks the SETTINGS frame the +// HTTP/2 server actually sends to the edge, not just the configured value. +// Some http2.Server implementations silently replace limits they cannot +// represent with their default of 250 streams. +func TestHTTP2AdvertisedMaxConcurrentStreams(t *testing.T) { + http2Conn, edgeConn := newTestHTTP2Connection() + + ctx, cancel := context.WithCancel(t.Context()) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + _ = http2Conn.Serve(ctx) + }() + defer func() { + cancel() + wg.Wait() + }() + + go func() { + _, _ = edgeConn.Write([]byte(http2.ClientPreface)) + }() + + require.NoError(t, edgeConn.SetReadDeadline(time.Now().Add(5*time.Second))) + framer := http2.NewFramer(nil, edgeConn) + frame, err := framer.ReadFrame() + require.NoError(t, err) + settings, ok := frame.(*http2.SettingsFrame) + require.True(t, ok, "expected the server to send SETTINGS first, got %T", frame) + require.False(t, settings.IsAck()) + + maxStreams, ok := settings.Value(http2.SettingMaxConcurrentStreams) + require.True(t, ok, "SETTINGS_MAX_CONCURRENT_STREAMS was not advertised") + assert.Equal(t, uint32(MaxConcurrentStreams), maxStreams) +} + func TestServeHTTP(t *testing.T) { tests := []testRequest{ {