Skip to content

Commit 3315fa6

Browse files
committed
TUN-10630: Fix precheck protocol override
As it stands, cloudflared prechecks are not taking the `protocol` flag into consideration and is instead falling back to the default protocol, which is QUIC. Prechecks should report the protocol cloudflared will use, not the default protocol.
1 parent ad11e67 commit 3315fa6

4 files changed

Lines changed: 200 additions & 7 deletions

File tree

cmd/cloudflared/tunnel/cmd.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -537,9 +537,10 @@ func runPrechecks(c *cli.Context, log *zerolog.Logger, region string) {
537537
}
538538

539539
cfg := prechecks.Config{
540-
Region: region,
541-
IPVersion: ipVersion,
542-
EdgeAddrs: c.StringSlice(cfdflags.Edge),
540+
Region: region,
541+
IPVersion: ipVersion,
542+
EdgeAddrs: c.StringSlice(cfdflags.Edge),
543+
ProtocolOverride: c.String(cfdflags.Protocol),
543544
}
544545

545546
dialers := prechecks.RunDialers{

prechecks/checker.go

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ func Run(ctx context.Context, caCert string, cfg Config, log *zerolog.Logger, ru
156156
return Report{
157157
RunID: runID,
158158
Results: append(dnsResults, results.Collect()...),
159-
SuggestedProtocol: suggestProtocol(results.QUIC, results.HTTP2),
159+
SuggestedProtocol: suggestProtocol(results.QUIC, results.HTTP2, cfg.ProtocolOverride),
160160
}
161161
}
162162

@@ -303,10 +303,52 @@ func severity(s Status) int {
303303
}
304304
}
305305

306-
// suggestProtocol recommends QUIC when all QUIC region probes passed, HTTP/2
307-
// when all HTTP/2 probes passed, and nil when neither transport works.
306+
// parseProtocolOverride converts the raw --protocol flag string into a
307+
// *connection.Protocol. It returns nil when the string is empty, "auto", or
308+
// unrecognised, so the probe heuristic is used in those cases. "h2mux" is
309+
// treated as HTTP/2 because both map to the same transport.
310+
func parseProtocolOverride(flag string) *connection.Protocol {
311+
switch flag {
312+
case connection.QUIC.String():
313+
p := connection.QUIC
314+
return &p
315+
case connection.HTTP2.String(), "h2mux":
316+
p := connection.HTTP2
317+
return &p
318+
default:
319+
// "auto", empty, or unknown — no override; let the heuristic decide.
320+
return nil
321+
}
322+
}
323+
324+
// suggestProtocol determines the protocol to report in the pre-check summary.
325+
//
326+
// When the caller has explicitly overridden the protocol via --protocol, that
327+
// choice is honoured when its transport probes produced evidence and did not
328+
// fail.
329+
//
330+
// When there is no override (auto-selection), precedence is QUIC, HTTP/2,
331+
// and nil. A protocol is only suggested if all probes pass.
332+
//
308333
// Any region failing means the transport is treated as failed (worst wins).
309-
func suggestProtocol(quicResults, http2Results []CheckResult) *connection.Protocol {
334+
func suggestProtocol(quicResults, http2Results []CheckResult, overrideFlag string) *connection.Protocol {
335+
if override := parseProtocolOverride(overrideFlag); override != nil {
336+
switch *override {
337+
case connection.QUIC:
338+
// Only report QUIC as the suggested protocol if its probes did not
339+
// all fail — if they did, fall through to the heuristic so the
340+
// summary can report a usable fallback or nil.
341+
if len(quicResults) > 0 && worstStatus(quicResults) != Fail {
342+
return new(connection.QUIC)
343+
}
344+
case connection.HTTP2:
345+
// Same logic for an explicit HTTP/2 override.
346+
if len(http2Results) > 0 && worstStatus(http2Results) != Fail {
347+
return new(connection.HTTP2)
348+
}
349+
}
350+
}
351+
310352
if len(quicResults) > 0 && worstStatus(quicResults) == Pass {
311353
quic := connection.QUIC
312354
return &quic

prechecks/checker_test.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,3 +587,144 @@ func TestRun_EdgeAddrs_UnresolvableAddr(t *testing.T) {
587587
assert.Nil(t, report.SuggestedProtocol)
588588
assert.True(t, report.hasHardFail())
589589
}
590+
591+
// ---------------------------------------------------------------------------
592+
// Protocol override tests
593+
// ---------------------------------------------------------------------------
594+
595+
// TestRun_ProtocolOverride_HTTP2_BothPass verifies that when --protocol http2
596+
// is set and both transports are reachable, the summary reports HTTP/2 (not
597+
// QUIC, which would otherwise win the heuristic).
598+
func TestRun_ProtocolOverride_HTTP2_BothPass(t *testing.T) {
599+
t.Parallel()
600+
ctrl := gomock.NewController(t)
601+
602+
dns := mocks.NewMockDNSResolver(ctrl)
603+
tcp := mocks.NewMockTCPDialer(ctrl)
604+
quicD := mocks.NewMockQUICDialer(ctrl)
605+
mgmt := mocks.NewMockManagementDialer(ctrl)
606+
fakeQUICConn := newFakeQUICConn(ctrl)
607+
608+
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
609+
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
610+
Return(nopConn{}, nil).AnyTimes()
611+
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
612+
Return(fakeQUICConn, nil).AnyTimes()
613+
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
614+
Return(nopConn{}, nil)
615+
616+
cfg := Config{
617+
Timeout: 2 * time.Second,
618+
IPVersion: allregions.Auto,
619+
ProtocolOverride: "http2",
620+
}
621+
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
622+
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
623+
624+
// Both transports pass, but the override must win — HTTP/2 is reported.
625+
require.NotNil(t, report.SuggestedProtocol)
626+
assert.Equal(t, connection.HTTP2, *report.SuggestedProtocol,
627+
"override http2 should be reported even though QUIC probes also passed")
628+
assert.False(t, report.hasHardFail())
629+
}
630+
631+
// TestRun_ProtocolOverride_QUIC_BothPass verifies that when --protocol quic is
632+
// set and both transports are reachable, the summary reports QUIC (same as the
633+
// heuristic would choose, but driven by the override).
634+
func TestRun_ProtocolOverride_QUIC_BothPass(t *testing.T) {
635+
t.Parallel()
636+
ctrl := gomock.NewController(t)
637+
638+
dns := mocks.NewMockDNSResolver(ctrl)
639+
tcp := mocks.NewMockTCPDialer(ctrl)
640+
quicD := mocks.NewMockQUICDialer(ctrl)
641+
mgmt := mocks.NewMockManagementDialer(ctrl)
642+
fakeQUICConn := newFakeQUICConn(ctrl)
643+
644+
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
645+
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
646+
Return(nopConn{}, nil).AnyTimes()
647+
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
648+
Return(fakeQUICConn, nil).AnyTimes()
649+
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
650+
Return(nopConn{}, nil)
651+
652+
cfg := Config{
653+
Timeout: 2 * time.Second,
654+
IPVersion: allregions.Auto,
655+
ProtocolOverride: "quic",
656+
}
657+
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
658+
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
659+
660+
require.NotNil(t, report.SuggestedProtocol)
661+
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
662+
assert.False(t, report.hasHardFail())
663+
}
664+
665+
// TestRun_ProtocolOverride_HTTP2_QUICBlocked verifies that when --protocol http2
666+
// is set and QUIC is blocked, we still report HTTP/2 (not a fallback to the
667+
// heuristic, since the overridden transport is healthy).
668+
func TestRun_ProtocolOverride_HTTP2_QUICBlocked(t *testing.T) {
669+
t.Parallel()
670+
ctrl := gomock.NewController(t)
671+
672+
dns := mocks.NewMockDNSResolver(ctrl)
673+
tcp := mocks.NewMockTCPDialer(ctrl)
674+
quicD := mocks.NewMockQUICDialer(ctrl)
675+
mgmt := mocks.NewMockManagementDialer(ctrl)
676+
677+
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
678+
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
679+
Return(nopConn{}, nil).AnyTimes()
680+
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
681+
Return(nil, errors.New("blocked")).AnyTimes()
682+
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
683+
Return(nopConn{}, nil)
684+
685+
cfg := Config{
686+
Timeout: 2 * time.Second,
687+
IPVersion: allregions.Auto,
688+
ProtocolOverride: "http2",
689+
}
690+
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
691+
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
692+
693+
require.NotNil(t, report.SuggestedProtocol)
694+
assert.Equal(t, connection.HTTP2, *report.SuggestedProtocol)
695+
assert.False(t, report.hasHardFail())
696+
}
697+
698+
// TestRun_ProtocolOverride_HTTP2_BothBlocked verifies that when --protocol http2
699+
// is set but the HTTP/2 transport itself also fails (hard fail), the override
700+
// falls through to the heuristic which returns nil — there is no usable protocol.
701+
func TestRun_ProtocolOverride_HTTP2_BothBlocked(t *testing.T) {
702+
t.Parallel()
703+
ctrl := gomock.NewController(t)
704+
705+
dns := mocks.NewMockDNSResolver(ctrl)
706+
tcp := mocks.NewMockTCPDialer(ctrl)
707+
quicD := mocks.NewMockQUICDialer(ctrl)
708+
mgmt := mocks.NewMockManagementDialer(ctrl)
709+
710+
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
711+
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
712+
Return(nil, errors.New("blocked")).AnyTimes()
713+
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
714+
Return(nil, errors.New("blocked")).AnyTimes()
715+
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
716+
Return(nopConn{}, nil)
717+
718+
cfg := Config{
719+
Timeout: 2 * time.Second,
720+
IPVersion: allregions.Auto,
721+
ProtocolOverride: "http2",
722+
}
723+
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
724+
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
725+
726+
// The overridden transport (HTTP/2) is blocked, so the override cannot be
727+
// honoured and the hard-fail path reports no suggested protocol.
728+
assert.Nil(t, report.SuggestedProtocol)
729+
assert.True(t, report.hasHardFail())
730+
}

prechecks/types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,13 @@ type Config struct {
126126
// no SRV records to validate — and transport probes target each addr
127127
// individually, labeled with the original addr string.
128128
EdgeAddrs []string
129+
130+
// ProtocolOverride is the raw --protocol flag value (e.g. "quic",
131+
// "http2", "h2mux"). When non-empty and not "auto", the pre-checks still
132+
// probe both transports for diagnostic completeness, but the reported
133+
// SuggestedProtocol honours the override so that the summary message
134+
// reflects what cloudflared will actually use — not what the probe
135+
// heuristic would recommend on its own. Parsing happens inside the
136+
// prechecks package.
137+
ProtocolOverride string
129138
}

0 commit comments

Comments
 (0)