mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-06-22 20:00:16 +00:00
4177dd6936
Avoid using fmt.Println and instead switch to logging pre-checks with the provided logger.
418 lines
17 KiB
Go
418 lines
17 KiB
Go
package prechecks
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/rs/zerolog"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/cloudflare/cloudflared/connection"
|
|
)
|
|
|
|
// fixedRunID is used in all fixtures so golden strings are deterministic.
|
|
var fixedRunID = uuid.MustParse("00000000-0000-0000-0000-000000000001")
|
|
|
|
// Fixtures
|
|
|
|
// allPassReport is the all-checks-pass scenario: QUIC is the suggested protocol.
|
|
func allPassReport() Report {
|
|
return Report{
|
|
RunID: fixedRunID,
|
|
SuggestedProtocol: new(connection.QUIC),
|
|
Results: []CheckResult{
|
|
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
|
|
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
|
|
{Type: ProbeTypeQUIC, Component: "UDP Connectivity", Target: "Port 7844 (QUIC)", ProbeStatus: Pass, Details: detailsQUICHandshakeSuccessful},
|
|
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: detailsHTTP2HandshakeSuccessful},
|
|
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: detailsApiReachable},
|
|
},
|
|
}
|
|
}
|
|
|
|
// quicBlockedReport is the degraded scenario: QUIC is blocked, HTTP/2 is the fallback.
|
|
// Only one transport failed so this is a warning, not a hard fail.
|
|
func quicBlockedReport() Report {
|
|
return Report{
|
|
RunID: fixedRunID,
|
|
SuggestedProtocol: new(connection.HTTP2),
|
|
Results: []CheckResult{
|
|
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
|
|
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
|
|
{
|
|
Type: ProbeTypeQUIC,
|
|
Component: "UDP Connectivity",
|
|
Target: "Port 7844 (QUIC)",
|
|
ProbeStatus: Fail,
|
|
Details: detailsQUICHandshakeFailed,
|
|
Action: actionQUICBlocked,
|
|
},
|
|
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: detailsHTTP2HandshakeSuccessful},
|
|
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: detailsApiReachable},
|
|
},
|
|
}
|
|
}
|
|
|
|
// apiFailReport is the degraded scenario: all connectivity passes but the Cloudflare
|
|
// API is unreachable. The tunnel can still run; only automatic updates are unavailable.
|
|
func apiFailReport() Report {
|
|
return Report{
|
|
RunID: fixedRunID,
|
|
SuggestedProtocol: new(connection.QUIC),
|
|
Results: []CheckResult{
|
|
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
|
|
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
|
|
{Type: ProbeTypeQUIC, Component: "UDP Connectivity", Target: "Port 7844 (QUIC)", ProbeStatus: Pass, Details: detailsQUICHandshakeSuccessful},
|
|
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: detailsHTTP2HandshakeSuccessful},
|
|
{
|
|
Type: ProbeTypeManagementAPI,
|
|
Component: "Cloudflare API",
|
|
Target: "api.cloudflare.com:443",
|
|
ProbeStatus: Fail,
|
|
Details: "Connection refused",
|
|
Action: "cloudflared will still run, but automatic software updates are unavailable. Ensure port 443 TCP to api.cloudflare.com is open if you want auto-updates.",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// bothTransportsBlockedReport is the hard-fail scenario: both QUIC and HTTP/2 are blocked.
|
|
func bothTransportsBlockedReport() Report {
|
|
return Report{
|
|
RunID: fixedRunID,
|
|
SuggestedProtocol: nil,
|
|
Results: []CheckResult{
|
|
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
|
|
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
|
|
{
|
|
Type: ProbeTypeQUIC,
|
|
Component: "UDP Connectivity",
|
|
Target: "Port 7844 (QUIC)",
|
|
ProbeStatus: Fail,
|
|
Details: detailsQUICHandshakeFailed,
|
|
Action: "Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.",
|
|
},
|
|
{
|
|
Type: ProbeTypeHTTP2,
|
|
Component: "TCP Connectivity",
|
|
Target: "Port 7844 (HTTP/2)",
|
|
ProbeStatus: Fail,
|
|
Details: detailsHTTP2BlockedOrUnreachable,
|
|
},
|
|
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: detailsApiReachable},
|
|
},
|
|
}
|
|
}
|
|
|
|
// dnsFailReport is the hard-fail scenario: DNS is unresolvable, transports are skipped.
|
|
func dnsFailReport() Report {
|
|
return Report{
|
|
RunID: fixedRunID,
|
|
SuggestedProtocol: nil,
|
|
Results: []CheckResult{
|
|
{
|
|
Type: ProbeTypeDNS,
|
|
Component: "DNS Resolution",
|
|
Target: "region1.v2.argotunnel.com",
|
|
ProbeStatus: Fail,
|
|
Details: "No addresses returned",
|
|
Action: "Ensure your DNS resolver can resolve 'region1.v2.argotunnel.com'. Run: dig A region1.v2.argotunnel.com @1.1.1.1. If that fails, contact your network administrator.",
|
|
},
|
|
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Fail, Details: "No addresses returned"},
|
|
{Type: ProbeTypeQUIC, Component: "UDP Connectivity", Target: "Port 7844 (QUIC)", ProbeStatus: Skip, Details: "DNS prerequisite failed"},
|
|
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Skip, Details: "DNS prerequisite failed"},
|
|
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Fail, Details: "Connection refused"},
|
|
},
|
|
}
|
|
}
|
|
|
|
// String() / table renderer tests
|
|
|
|
func TestString_AllPass(t *testing.T) {
|
|
t.Parallel()
|
|
want := []string{
|
|
"COMPONENT TARGET STATUS DETAILS",
|
|
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
|
|
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
|
|
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful",
|
|
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful",
|
|
"Cloudflare API api.cloudflare.com:443 PASS API is reachable",
|
|
"",
|
|
"SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol.",
|
|
}
|
|
assert.Equal(t, want, allPassReport().String())
|
|
}
|
|
|
|
func TestString_QuicBlocked(t *testing.T) {
|
|
t.Parallel()
|
|
want := []string{
|
|
"COMPONENT TARGET STATUS DETAILS",
|
|
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
|
|
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
|
|
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed",
|
|
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful",
|
|
"Cloudflare API api.cloudflare.com:443 PASS API is reachable",
|
|
"WARNING: Allow outbound QUIC traffic on port 7844 or use HTTP2.",
|
|
"",
|
|
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'http2'.",
|
|
}
|
|
assert.Equal(t, want, quicBlockedReport().String())
|
|
}
|
|
|
|
func TestString_APIFail(t *testing.T) {
|
|
t.Parallel()
|
|
want := []string{
|
|
"COMPONENT TARGET STATUS DETAILS",
|
|
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
|
|
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
|
|
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful",
|
|
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful",
|
|
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused",
|
|
"WARNING: cloudflared will still run, but automatic software updates are unavailable. Ensure port 443 TCP to api.cloudflare.com is open if you want auto-updates.",
|
|
"",
|
|
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'quic'.",
|
|
}
|
|
assert.Equal(t, want, apiFailReport().String())
|
|
}
|
|
|
|
func TestString_BothTransportsBlocked(t *testing.T) {
|
|
t.Parallel()
|
|
want := []string{
|
|
"COMPONENT TARGET STATUS DETAILS",
|
|
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
|
|
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
|
|
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed",
|
|
"TCP Connectivity Port 7844 (HTTP/2) FAIL HTTP/2 connection is blocked or unreachable",
|
|
"Cloudflare API api.cloudflare.com:443 PASS API is reachable",
|
|
"ERROR: Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.",
|
|
"",
|
|
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.",
|
|
}
|
|
assert.Equal(t, want, bothTransportsBlockedReport().String())
|
|
}
|
|
|
|
func TestString_DNSFail(t *testing.T) {
|
|
t.Parallel()
|
|
want := []string{
|
|
"COMPONENT TARGET STATUS DETAILS",
|
|
"DNS Resolution region1.v2.argotunnel.com FAIL No addresses returned",
|
|
"DNS Resolution region2.v2.argotunnel.com FAIL No addresses returned",
|
|
"UDP Connectivity Port 7844 (QUIC) SKIP DNS prerequisite failed",
|
|
"TCP Connectivity Port 7844 (HTTP/2) SKIP DNS prerequisite failed",
|
|
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused",
|
|
"ERROR: Ensure your DNS resolver can resolve 'region1.v2.argotunnel.com'. Run: dig A region1.v2.argotunnel.com @1.1.1.1. If that fails, contact your network administrator.",
|
|
"",
|
|
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.",
|
|
}
|
|
assert.Equal(t, want, dnsFailReport().String())
|
|
}
|
|
|
|
func TestString_EmptyResults(t *testing.T) {
|
|
t.Parallel()
|
|
r := Report{RunID: fixedRunID, SuggestedProtocol: new(connection.QUIC)}
|
|
out := r.String()
|
|
// Must not panic and must still emit a valid skeleton.
|
|
require.Len(t, out, 3)
|
|
assert.Contains(t, out[0], "COMPONENT")
|
|
assert.Contains(t, out[2], "SUMMARY:")
|
|
}
|
|
|
|
// LogEvent() / structured log renderer tests
|
|
|
|
// logEntry is a helper struct to unmarshal a single JSON log line emitted by LogEvent.
|
|
type logEntry struct {
|
|
Level string `json:"level"`
|
|
RunID string `json:"run_id"`
|
|
Component string `json:"component"`
|
|
Target string `json:"target"`
|
|
Status string `json:"status"`
|
|
Details string `json:"details"`
|
|
Message string `json:"message"`
|
|
HardFail *bool `json:"hard_fail"`
|
|
SuggestedProtocol *string `json:"suggested_protocol"`
|
|
}
|
|
|
|
// captureLogLines runs LogEvent against a buffer-backed zerolog logger and
|
|
// returns the parsed JSON entries, one per emitted line.
|
|
func captureLogLines(t *testing.T, r Report) []logEntry {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
logger := zerolog.New(&buf)
|
|
r.LogEvent(&logger)
|
|
|
|
var entries []logEntry
|
|
for _, line := range strings.Split(strings.TrimRight(buf.String(), "\n"), "\n") {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
var e logEntry
|
|
require.NoError(t, json.Unmarshal([]byte(line), &e), "failed to parse log line: %s", line)
|
|
entries = append(entries, e)
|
|
}
|
|
return entries
|
|
}
|
|
|
|
func TestLogEvent_AllPass(t *testing.T) {
|
|
t.Parallel()
|
|
r := allPassReport()
|
|
entries := captureLogLines(t, r)
|
|
|
|
// One line per result plus one summary line.
|
|
require.Len(t, entries, len(r.Results)+1)
|
|
|
|
// Each result line carries the right fields.
|
|
expected := []struct {
|
|
component string
|
|
target string
|
|
status string
|
|
details string
|
|
}{
|
|
{"DNS Resolution", "region1.v2.argotunnel.com", "pass", dnsResolvedSuccessfully},
|
|
{"DNS Resolution", "region2.v2.argotunnel.com", "pass", dnsResolvedSuccessfully},
|
|
{"UDP Connectivity", "Port 7844 (QUIC)", "pass", detailsQUICHandshakeSuccessful},
|
|
{"TCP Connectivity", "Port 7844 (HTTP/2)", "pass", detailsHTTP2HandshakeSuccessful},
|
|
{"Cloudflare API", "api.cloudflare.com:443", "pass", detailsApiReachable},
|
|
}
|
|
for i, exp := range expected {
|
|
e := entries[i]
|
|
assert.Equal(t, "info", e.Level, "entry %d: level", i)
|
|
assert.Equal(t, fixedRunID.String(), e.RunID, "entry %d: run_id", i)
|
|
assert.Equal(t, exp.component, e.Component, "entry %d: component", i)
|
|
assert.Equal(t, exp.target, e.Target, "entry %d: target", i)
|
|
assert.Equal(t, exp.status, e.Status, "entry %d: status", i)
|
|
assert.Equal(t, exp.details, e.Details, "entry %d: details", i)
|
|
assert.Equal(t, logMsgPrecheck, e.Message, "entry %d: message", i)
|
|
}
|
|
|
|
// Summary line.
|
|
summary := entries[len(entries)-1]
|
|
assert.Equal(t, logMsgPrecheckComplete, summary.Message)
|
|
assert.Equal(t, fixedRunID.String(), summary.RunID)
|
|
require.NotNil(t, summary.HardFail)
|
|
assert.False(t, *summary.HardFail)
|
|
require.NotNil(t, summary.SuggestedProtocol)
|
|
assert.Equal(t, "quic", *summary.SuggestedProtocol)
|
|
}
|
|
|
|
func TestLogEvent_QuicBlocked(t *testing.T) {
|
|
t.Parallel()
|
|
entries := captureLogLines(t, quicBlockedReport())
|
|
|
|
// QUIC row (index 2) must carry status=fail and the right details.
|
|
quic := entries[2]
|
|
assert.Equal(t, "fail", quic.Status)
|
|
assert.Equal(t, "UDP Connectivity", quic.Component)
|
|
assert.Equal(t, "Port 7844 (QUIC)", quic.Target)
|
|
assert.Equal(t, "QUIC connection failed", quic.Details)
|
|
assert.Equal(t, fixedRunID.String(), quic.RunID)
|
|
|
|
// Summary: not a hard fail (HTTP/2 still works), protocol falls back to http2.
|
|
summary := entries[len(entries)-1]
|
|
require.NotNil(t, summary.HardFail)
|
|
assert.False(t, *summary.HardFail)
|
|
require.NotNil(t, summary.SuggestedProtocol)
|
|
assert.Equal(t, "http2", *summary.SuggestedProtocol)
|
|
assert.Equal(t, fixedRunID.String(), summary.RunID)
|
|
}
|
|
|
|
func TestLogEvent_APIFail(t *testing.T) {
|
|
t.Parallel()
|
|
entries := captureLogLines(t, apiFailReport())
|
|
|
|
// API row (index 4) carries status=fail and the expected details.
|
|
api := entries[4]
|
|
assert.Equal(t, "fail", api.Status)
|
|
assert.Equal(t, "Cloudflare API", api.Component)
|
|
assert.Equal(t, "api.cloudflare.com:443", api.Target)
|
|
assert.Equal(t, "Connection refused", api.Details)
|
|
assert.Equal(t, fixedRunID.String(), api.RunID)
|
|
|
|
// All transport rows pass.
|
|
assert.Equal(t, "pass", entries[2].Status)
|
|
assert.Equal(t, "pass", entries[3].Status)
|
|
|
|
// Summary: not a hard fail, QUIC is still the suggested protocol.
|
|
summary := entries[len(entries)-1]
|
|
require.NotNil(t, summary.HardFail)
|
|
assert.False(t, *summary.HardFail)
|
|
require.NotNil(t, summary.SuggestedProtocol)
|
|
assert.Equal(t, "quic", *summary.SuggestedProtocol)
|
|
}
|
|
|
|
func TestLogEvent_BothTransportsBlocked(t *testing.T) {
|
|
t.Parallel()
|
|
entries := captureLogLines(t, bothTransportsBlockedReport())
|
|
|
|
// Both transport rows carry status=fail.
|
|
assert.Equal(t, "fail", entries[2].Status)
|
|
assert.Equal(t, "QUIC connection failed", entries[2].Details)
|
|
assert.Equal(t, "fail", entries[3].Status)
|
|
assert.Equal(t, "HTTP/2 connection is blocked or unreachable", entries[3].Details)
|
|
|
|
summary := entries[len(entries)-1]
|
|
require.NotNil(t, summary.HardFail)
|
|
assert.True(t, *summary.HardFail)
|
|
assert.Nil(t, summary.SuggestedProtocol)
|
|
}
|
|
|
|
func TestLogEvent_DNSFail(t *testing.T) {
|
|
t.Parallel()
|
|
entries := captureLogLines(t, dnsFailReport())
|
|
|
|
// Both DNS rows carry status=fail and the expected details.
|
|
assert.Equal(t, "fail", entries[0].Status)
|
|
assert.Equal(t, "No addresses returned", entries[0].Details)
|
|
assert.Equal(t, "fail", entries[1].Status)
|
|
assert.Equal(t, "No addresses returned", entries[1].Details)
|
|
|
|
// Transport rows are skipped.
|
|
assert.Equal(t, "skip", entries[2].Status)
|
|
assert.Equal(t, "DNS prerequisite failed", entries[2].Details)
|
|
assert.Equal(t, "skip", entries[3].Status)
|
|
assert.Equal(t, "DNS prerequisite failed", entries[3].Details)
|
|
|
|
summary := entries[len(entries)-1]
|
|
require.NotNil(t, summary.HardFail)
|
|
assert.True(t, *summary.HardFail)
|
|
assert.Nil(t, summary.SuggestedProtocol)
|
|
}
|
|
|
|
func TestLogEvent_EmptyReport(t *testing.T) {
|
|
t.Parallel()
|
|
r := Report{RunID: fixedRunID, SuggestedProtocol: new(connection.HTTP2)}
|
|
entries := captureLogLines(t, r)
|
|
|
|
// No result lines, only the summary.
|
|
require.Len(t, entries, 1)
|
|
assert.Equal(t, logMsgPrecheckComplete, entries[0].Message)
|
|
assert.Equal(t, fixedRunID.String(), entries[0].RunID)
|
|
require.NotNil(t, entries[0].HardFail)
|
|
assert.False(t, *entries[0].HardFail)
|
|
require.NotNil(t, entries[0].SuggestedProtocol)
|
|
assert.Equal(t, "http2", *entries[0].SuggestedProtocol)
|
|
}
|
|
|
|
// hasHardFail / hasWarn helper tests
|
|
|
|
func TestHasHardFail(t *testing.T) {
|
|
t.Parallel()
|
|
assert.False(t, allPassReport().hasHardFail())
|
|
assert.False(t, quicBlockedReport().hasHardFail())
|
|
assert.False(t, apiFailReport().hasHardFail())
|
|
assert.True(t, bothTransportsBlockedReport().hasHardFail())
|
|
assert.True(t, dnsFailReport().hasHardFail())
|
|
}
|
|
|
|
func TestHasWarn(t *testing.T) {
|
|
t.Parallel()
|
|
assert.False(t, allPassReport().hasWarn())
|
|
assert.True(t, quicBlockedReport().hasWarn())
|
|
assert.True(t, apiFailReport().hasWarn())
|
|
assert.False(t, bothTransportsBlockedReport().hasWarn())
|
|
assert.False(t, dnsFailReport().hasWarn())
|
|
}
|