diff --git a/cmd/cloudflared/flags/flags.go b/cmd/cloudflared/flags/flags.go index f31ff60e..89eadd8a 100644 --- a/cmd/cloudflared/flags/flags.go +++ b/cmd/cloudflared/flags/flags.go @@ -126,6 +126,9 @@ const ( // NoPrechecks is the command line flag to skip connectivity pre-checks at startup. NoPrechecks = "no-prechecks" + // Prechecks is the command line flag to run connectivity pre-checks at startup. + Prechecks = "prechecks" + // LogLevel is the command line flag for the cloudflared logging level LogLevel = "loglevel" diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go index 16270980..f37e76fd 100644 --- a/cmd/cloudflared/tunnel/cmd.go +++ b/cmd/cloudflared/tunnel/cmd.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "fmt" + "net" "net/url" "os" "path/filepath" @@ -31,11 +32,13 @@ import ( "github.com/cloudflare/cloudflared/credentials" "github.com/cloudflare/cloudflared/diagnostic" "github.com/cloudflare/cloudflared/edgediscovery" + "github.com/cloudflare/cloudflared/edgediscovery/allregions" "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/management" "github.com/cloudflare/cloudflared/metrics" "github.com/cloudflare/cloudflared/orchestration" + "github.com/cloudflare/cloudflared/prechecks" "github.com/cloudflare/cloudflared/signal" "github.com/cloudflare/cloudflared/supervisor" "github.com/cloudflare/cloudflared/tlsconfig" @@ -372,6 +375,17 @@ func StartServer( info.Log(log) logClientOptions(c, log) + // Run connectivity pre-checks for cloudflared. This runs in a separate + // goroutine, as we want to keep initializing cloudflared while prechecks + // are running. + if c.Bool(cfdflags.Prechecks) && !c.Bool(cfdflags.NoPrechecks) { + resolvedRegion := c.String(cfdflags.Region) + if resolvedRegion == "" && namedTunnel != nil { + resolvedRegion = namedTunnel.Credentials.Endpoint + } + go runPrechecks(c, log, resolvedRegion) + } + // this context drives the server, when it's canceled tunnel and all other components (origins, dns, etc...) should stop ctx, cancel := context.WithCancel(c.Context) defer cancel() @@ -513,6 +527,49 @@ func StartServer( return waitToShutdown(&wg, cancel, errC, graceShutdownC, gracePeriod, log) } +// runPrechecks executes connectivity pre-checks and logs the results. +// Pre-checks are diagnostic only and do not gate tunnel startup. +func runPrechecks(c *cli.Context, log *zerolog.Logger, region string) { + ipVersion := allregions.Auto + if ipVersionStr := c.String(cfdflags.EdgeIpVersion); ipVersionStr != "" { + parsedVersion, err := parseConfigIPVersion(ipVersionStr) + if err == nil { + ipVersion = parsedVersion + } else { + log.Warn().Str("edgeIpVersion", ipVersionStr).Err(err).Msg("Invalid edge-ip-version value, using auto") + } + } + + cfg := prechecks.Config{ + Region: region, + IPVersion: ipVersion, + } + + // Mirror the static/dynamic edge selection from supervisor/supervisor.go: + // when --edge addresses are provided, bypass DNS discovery entirely. + var dnsResolver prechecks.DNSResolver + if edgeAddrs := c.StringSlice(cfdflags.Edge); len(edgeAddrs) > 0 { + dnsResolver = &prechecks.StaticEdgeDNSResolver{Addrs: edgeAddrs, Log: log} + } else { + dnsResolver = &prechecks.EdgeDNSResolver{Log: log} + } + + dialers := prechecks.RunDialers{ + DNSResolver: dnsResolver, + TCPDialer: &prechecks.EdgeTCPDialer{}, + QUICDialer: &prechecks.EdgeQUICDialer{}, + ManagementDialer: &prechecks.NetManagementDialer{Dialer: net.Dialer{}}, + } + + report := prechecks.Run(c.Context, c.String(cfdflags.CACert), cfg, log, dialers) + + // Output the human-readable table to console + fmt.Println(report.String()) + + // Also log structured results for log aggregation + report.LogEvent(log) +} + func waitToShutdown(wg *sync.WaitGroup, cancelServerContext func(), errC <-chan error, @@ -889,6 +946,13 @@ func configureCloudflaredFlags(shouldHide bool) []cli.Flag { Value: false, Hidden: shouldHide, }), + altsrc.NewBoolFlag(&cli.BoolFlag{ + Name: cfdflags.Prechecks, + Usage: "Run connectivity pre-checks at startup.", + EnvVars: []string{"TUNNEL_PRECHECKS"}, + Value: false, + Hidden: shouldHide, + }), altsrc.NewStringFlag(&cli.StringFlag{ Name: cfdflags.Metrics, Value: metrics.GetMetricsDefaultAddress(metrics.Runtime), diff --git a/cmd/cloudflared/tunnel/configuration.go b/cmd/cloudflared/tunnel/configuration.go index 96283906..225da347 100644 --- a/cmd/cloudflared/tunnel/configuration.go +++ b/cmd/cloudflared/tunnel/configuration.go @@ -262,6 +262,7 @@ func prepareTunnelConfig( QUICConnectionLevelFlowControlLimit: c.Uint64(flags.QuicConnLevelFlowControlLimit), QUICStreamLevelFlowControlLimit: c.Uint64(flags.QuicStreamLevelFlowControlLimit), NoPrechecks: c.Bool(flags.NoPrechecks), + Prechecks: c.Bool(flags.Prechecks), OriginDNSService: dnsService, OriginDialerService: originDialerService, } diff --git a/cmd/cloudflared/tunnel/subcommands.go b/cmd/cloudflared/tunnel/subcommands.go index b3a2198d..ff46acd5 100644 --- a/cmd/cloudflared/tunnel/subcommands.go +++ b/cmd/cloudflared/tunnel/subcommands.go @@ -489,7 +489,7 @@ func readyCommand(c *cli.Context) error { if err != nil { return err } - // nolint: gosec + // nolint: gosec // URL is constructed from the user-configured local metrics endpoint. res, err := http.DefaultClient.Do(req) if err != nil { return err @@ -1109,6 +1109,7 @@ func diagCommand(ctx *cli.Context) error { Address: sctx.c.String(flags.Metrics), ContainerID: sctx.c.String(diagContainerIDFlagName), PodID: sctx.c.String(diagPodFlagName), + Region: sctx.c.String(flags.Region), Toggles: diagnostic.Toggles{ NoDiagLogs: sctx.c.Bool(noDiagLogsFlagName), NoDiagMetrics: sctx.c.Bool(noDiagMetricsFlagName), diff --git a/diagnostic/consts.go b/diagnostic/consts.go index 6a7e4449..57db38c8 100644 --- a/diagnostic/consts.go +++ b/diagnostic/consts.go @@ -34,4 +34,5 @@ const ( cliConfigurationBaseName = "cli-configuration.json" configurationBaseName = "configuration.json" taskResultBaseName = "task-result.json" + prechecksBaseName = "prechecks.json" ) diff --git a/diagnostic/diagnostic.go b/diagnostic/diagnostic.go index 92d760f6..5b29b591 100644 --- a/diagnostic/diagnostic.go +++ b/diagnostic/diagnostic.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "net" "net/url" "os" "path/filepath" @@ -16,6 +17,8 @@ import ( "github.com/rs/zerolog" network "github.com/cloudflare/cloudflared/diagnostic/network" + "github.com/cloudflare/cloudflared/edgediscovery/allregions" + "github.com/cloudflare/cloudflared/prechecks" ) const ( @@ -32,6 +35,7 @@ const ( networkInformationJobName = "network information" cliConfigurationJobName = "cli configuration" configurationJobName = "configuration" + prechecksJobName = "connectivity pre-checks" ) // Struct used to hold the results of different routines executing the network collection. @@ -92,6 +96,7 @@ type Options struct { Address string ContainerID string PodID string + Region string Toggles Toggles } @@ -230,7 +235,7 @@ func networkInformationCollectors() (rawNetworkCollector, jsonNetworkCollector c } func rawNetworkInformationWriter(resultMap map[string]networkCollectionResult) (string, error) { - // nolint: gosec + // nolint: gosec // Intentionally creating a temporary diagnostic file in the OS temp directory. networkDumpHandle, err := os.Create(filepath.Join(os.TempDir(), rawNetworkBaseName)) if err != nil { return "", ErrCreatingTemporaryFile @@ -372,6 +377,7 @@ func resolveInstanceBaseURL( func createJobs( client *httpClient, tunnel *TunnelState, + region string, diagContainer string, diagPod string, noDiagSystem bool, @@ -434,11 +440,55 @@ func createJobs( fn: collectFromEndpointAdapter(client.GetTunnelConfiguration, configurationBaseName), bypass: false, }, + { + jobName: prechecksJobName, + fn: collectPrechecks(region), + bypass: noDiagNetwork, + }, } return jobs } +// collectPrechecks runs connectivity pre-checks and writes the results to a JSON file. +func collectPrechecks(region string) collectFunc { + return func(ctx context.Context) (string, error) { + cfg := prechecks.Config{ + Region: region, + IPVersion: allregions.Auto, + Timeout: defaultTimeout, + } + + // Create a no-op logger since we don't want to spam logs during diagnostic collection + log := zerolog.New(io.Discard) + + dialers := prechecks.RunDialers{ + DNSResolver: &prechecks.EdgeDNSResolver{Log: &log}, + TCPDialer: &prechecks.EdgeTCPDialer{}, + QUICDialer: &prechecks.EdgeQUICDialer{}, + ManagementDialer: &prechecks.NetManagementDialer{Dialer: net.Dialer{}}, + } + + emptyCert := "" + report := prechecks.Run(ctx, emptyCert, cfg, &log, dialers) + + // Write the report to a JSON file + // nolint: gosec + dumpHandle, err := os.Create(filepath.Join(os.TempDir(), prechecksBaseName)) + if err != nil { + return "", ErrCreatingTemporaryFile + } + defer func() { _ = dumpHandle.Close() }() + + encoder := newFormattedEncoder(dumpHandle) + if err := encoder.Encode(report); err != nil { + return dumpHandle.Name(), fmt.Errorf("error encoding prechecks report: %w", err) + } + + return dumpHandle.Name(), nil + } +} + func createTaskReport(taskReport map[string]taskResult) (string, error) { // nolint: gosec dumpHandle, err := os.Create(filepath.Join(os.TempDir(), taskResultBaseName)) @@ -527,6 +577,7 @@ func RunDiagnostic( jobs := createJobs( client, tunnel, + options.Region, options.ContainerID, options.PodID, options.Toggles.NoDiagSystem, diff --git a/supervisor/tunnel.go b/supervisor/tunnel.go index 2a2f41c0..3641b19c 100644 --- a/supervisor/tunnel.go +++ b/supervisor/tunnel.go @@ -65,6 +65,9 @@ type TunnelConfig struct { // NoPrechecks disables connectivity pre-checks at startup. NoPrechecks bool + // Prechecks enables connectivity pre-checks at startup. + Prechecks bool + NamedTunnel *connection.TunnelProperties ProtocolSelector connection.ProtocolSelector EdgeTLSConfigs map[connection.Protocol]*tls.Config