mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-08-07 07:14:57 +00:00
2206516c3b
Use curves when running pre-checks. Although it is not something critical, pre-checks should closely match the current cloudflared behavior when trying to establish connections to the edge. Adding curves here matches the current behavor.
386 lines
13 KiB
Go
386 lines
13 KiB
Go
package prechecks
|
||
|
||
import (
|
||
"context"
|
||
"crypto/tls"
|
||
"fmt"
|
||
"math"
|
||
"net"
|
||
"net/netip"
|
||
"time"
|
||
|
||
"github.com/quic-go/quic-go"
|
||
"github.com/rs/zerolog"
|
||
|
||
"github.com/cloudflare/cloudflared/connection/dialopts"
|
||
|
||
"github.com/cloudflare/cloudflared/connection"
|
||
cfdcrypto "github.com/cloudflare/cloudflared/crypto"
|
||
edgedial "github.com/cloudflare/cloudflared/edgediscovery"
|
||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||
"github.com/cloudflare/cloudflared/features"
|
||
cfdquic "github.com/cloudflare/cloudflared/quic"
|
||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||
)
|
||
|
||
const (
|
||
perProbeDialTimeout = 5 * time.Second
|
||
|
||
// Action messages for each probe outcome.
|
||
actionDNSFail = "Ensure your DNS resolver can resolve '%s'. Run: dig A %s @1.1.1.1. If that fails, contact your network administrator."
|
||
actionQUICBlocked = "Allow outbound QUIC traffic on port 7844 or use HTTP2."
|
||
actionHTTP2Blocked = "Allow outbound TCP on port 7844."
|
||
actionAPIUnreachable = "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."
|
||
|
||
// Component names for CheckResult.
|
||
componentDNSResolution = "DNS Resolution"
|
||
componentUDPConnectivity = "UDP Connectivity"
|
||
componentTCPConnectivity = "TCP Connectivity"
|
||
componentCloudflareAPI = "Cloudflare API"
|
||
|
||
// Target identifiers for CheckResult.
|
||
targetPortQUIC = "Port 7844 (QUIC)"
|
||
targetPortHTTP2 = "Port 7844 (HTTP/2)"
|
||
targetAPI = "api.cloudflare.com:443"
|
||
noDNSTarget = "No DNS target (Using edge flag)"
|
||
|
||
// Details messages for CheckResult.
|
||
dnsNoAddressesReturned = "No addresses returned"
|
||
dnsResolvedSuccessfully = "DNS Resolved successfully"
|
||
detailsQUICHandshakeFailed = "QUIC connection failed"
|
||
detailsQUICHandshakeSuccessful = "QUIC connection successful"
|
||
detailsHTTP2BlockedOrUnreachable = "HTTP/2 connection is blocked or unreachable"
|
||
detailsHTTP2HandshakeSuccessful = "HTTP/2 connection successful"
|
||
detailsAPIConnectionFailed = "API Connection failed"
|
||
detailsApiReachable = "API is reachable"
|
||
detailsDNSPrerequisiteFailed = "DNS prerequisite failed"
|
||
detailsTLSConfigFailed = "TLS configuration failed"
|
||
|
||
// Region hostname templates.
|
||
region1Global = "region1.v2.argotunnel.com"
|
||
region2Global = "region2.v2.argotunnel.com"
|
||
region1US = "us-region1.v2.argotunnel.com"
|
||
region2US = "us-region2.v2.argotunnel.com"
|
||
region1Fed = "fed-region1.v2.argotunnel.com"
|
||
region2Fed = "fed-region2.v2.argotunnel.com"
|
||
)
|
||
|
||
// EdgeDNSResolver implements DNSResolver for the standard DNS-based edge
|
||
// discovery path.
|
||
type EdgeDNSResolver struct {
|
||
Log *zerolog.Logger
|
||
}
|
||
|
||
func (r *EdgeDNSResolver) Resolve(region string) ([][]*allregions.EdgeAddr, error) {
|
||
return allregions.EdgeDiscovery(r.Log, allregions.RegionalServiceName(region))
|
||
}
|
||
|
||
type EdgeTCPDialer struct{}
|
||
|
||
func (d *EdgeTCPDialer) DialEdge(
|
||
ctx context.Context,
|
||
timeout time.Duration,
|
||
tlsConfig *tls.Config,
|
||
addr *net.TCPAddr,
|
||
localIP net.IP,
|
||
) (net.Conn, error) {
|
||
return edgedial.DialEdge(ctx, timeout, tlsConfig, addr, localIP)
|
||
}
|
||
|
||
type EdgeQUICDialer struct{}
|
||
|
||
func (d *EdgeQUICDialer) DialQuic(
|
||
ctx context.Context,
|
||
quicConfig *quic.Config,
|
||
tlsConfig *tls.Config,
|
||
addr netip.AddrPort,
|
||
localAddr net.IP,
|
||
connIndex uint8,
|
||
logger *zerolog.Logger,
|
||
opts dialopts.DialOpts,
|
||
) (cfdquic.QUICConnection, error) {
|
||
return connection.DialQuic(ctx, quicConfig, tlsConfig, addr, localAddr, connIndex, logger, opts)
|
||
}
|
||
|
||
type NetManagementDialer struct {
|
||
Dialer net.Dialer
|
||
}
|
||
|
||
func (d *NetManagementDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||
return d.Dialer.DialContext(ctx, network, addr)
|
||
}
|
||
|
||
// probeTLSConfig builds a *tls.Config for a pre-check probe using the same
|
||
// certificate pool and curve preferences as the production tunnel. The SNI and
|
||
// NextProtos are taken from p.ProbeTLSSettings() so that the probe SNI is used
|
||
// instead of the production SNI, which avoids noisy logs in origintunneld.
|
||
// Curve preferences are set via cfdcrypto.TLSConfigWithCurvePreferences so that
|
||
// prechecks advertise the same key-exchange algorithms (including post-quantum
|
||
// curves) as the real QUIC/H2 connections.
|
||
func probeTLSConfig(caCert string, p connection.Protocol, pqMode features.PostQuantumMode) (*tls.Config, error) {
|
||
settings := p.ProbeTLSSettings()
|
||
if settings == nil {
|
||
return nil, fmt.Errorf("no probe TLS settings for protocol %s", p)
|
||
}
|
||
cfg, err := tlsconfig.CreateTunnelConfig(caCert, settings.ServerName)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(settings.NextProtos) > 0 {
|
||
cfg.NextProtos = settings.NextProtos
|
||
}
|
||
cfg, err = cfdcrypto.TLSConfigWithCurvePreferences(cfg, pqMode)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("apply curve preferences: %w", err)
|
||
}
|
||
return cfg, nil
|
||
}
|
||
|
||
// probeDNS resolves edge addresses for the given region via the supplied
|
||
// DNSResolver and returns one ResolvedTarget per discovered region. If
|
||
// resolution fails entirely, every ResolvedTarget will carry a Fail DNSResult
|
||
// and nil Addrs.
|
||
func probeDNS(
|
||
resolver DNSResolver,
|
||
region string,
|
||
) []ResolvedTarget {
|
||
region1Target, region2Target := regionTargets(region)
|
||
targets := []string{region1Target, region2Target}
|
||
|
||
addrGroups, err := resolver.Resolve(region)
|
||
if err != nil || len(addrGroups) == 0 {
|
||
detail := dnsNoAddressesReturned
|
||
if err != nil {
|
||
detail = err.Error()
|
||
}
|
||
return []ResolvedTarget{
|
||
{DNSResult: newDNSCheckResult(region1Target, Fail, detail, fmt.Sprintf(actionDNSFail, region1Target, region1Target))},
|
||
{DNSResult: newDNSCheckResult(region2Target, Fail, detail, fmt.Sprintf(actionDNSFail, region2Target, region2Target))},
|
||
}
|
||
}
|
||
|
||
resolved := make([]ResolvedTarget, 0, len(addrGroups))
|
||
for i, target := range targets {
|
||
if i >= len(addrGroups) {
|
||
break
|
||
}
|
||
group := addrGroups[i]
|
||
if len(group) == 0 {
|
||
resolved = append(resolved, ResolvedTarget{
|
||
DNSResult: newDNSCheckResult(target, Fail, dnsNoAddressesReturned, fmt.Sprintf(actionDNSFail, target, target)),
|
||
})
|
||
} else {
|
||
resolved = append(resolved, ResolvedTarget{
|
||
Addrs: group,
|
||
DNSResult: newDNSCheckResult(target, Pass, dnsResolvedSuccessfully, ""),
|
||
})
|
||
}
|
||
}
|
||
|
||
return resolved
|
||
}
|
||
|
||
// probeQUIC performs a QUIC handshake to a single edge address and returns a
|
||
// CheckResult. The connection is closed immediately after the handshake – no
|
||
// streams are opened and no RPC frames are sent – to avoid triggering the OTD
|
||
// registration timeout (TUN-6732). The probe SNI (probe.cftunnel.com) is used
|
||
// instead of the production quic.cftunnel.com to prevent OTD log noise.
|
||
//
|
||
// A per-probe deadline (perProbeDialTimeout) is applied on top of the parent
|
||
// context so that a single blocked handshake cannot consume the entire suite
|
||
// budget.
|
||
func probeQUIC(
|
||
ctx context.Context,
|
||
tlsConfig *tls.Config,
|
||
dialer QUICDialer,
|
||
addr *allregions.EdgeAddr,
|
||
logger *zerolog.Logger,
|
||
) CheckResult {
|
||
dialCtx, cancel := context.WithTimeout(ctx, perProbeDialTimeout)
|
||
defer cancel()
|
||
|
||
// We call dialer.DialQuic with isProbe = true, which bypasses connIndex check.
|
||
// Therefore, whatever we add to connIndex will not be relevant.
|
||
edgeAddrPort := addr.UDP.AddrPort()
|
||
conn, err := dialer.DialQuic(
|
||
dialCtx,
|
||
&quic.Config{},
|
||
tlsConfig,
|
||
edgeAddrPort,
|
||
nil,
|
||
math.MaxUint8,
|
||
logger,
|
||
dialopts.DialOpts{SkipPortReuse: true},
|
||
)
|
||
if err != nil {
|
||
return CheckResult{
|
||
Type: ProbeTypeQUIC,
|
||
Component: componentUDPConnectivity,
|
||
Target: targetPortQUIC,
|
||
ProbeStatus: Fail,
|
||
Details: detailsQUICHandshakeFailed,
|
||
Action: actionQUICBlocked,
|
||
}
|
||
}
|
||
|
||
if err := conn.CloseWithError(0, "precheck complete"); err != nil {
|
||
logger.Debug().Err(err).Msg("Failed to close QUIC connection after successful handshake")
|
||
}
|
||
|
||
return CheckResult{
|
||
Type: ProbeTypeQUIC,
|
||
Component: componentUDPConnectivity,
|
||
Target: targetPortQUIC,
|
||
ProbeStatus: Pass,
|
||
Details: detailsQUICHandshakeSuccessful,
|
||
}
|
||
}
|
||
|
||
// probeHTTP2 performs a TCP + TLS handshake to a single edge address and
|
||
// returns a CheckResult. The connection is closed immediately after the
|
||
// handshake – no HTTP/2 frames are sent – to keep the probe minimal. The probe
|
||
// SNI (probe.cftunnel.com) is used instead of the production h2.cftunnel.com
|
||
// to prevent OTD log noise.
|
||
//
|
||
// The dial timeout is capped at perProbeDialTimeout so that a single blocked
|
||
// dial cannot exhaust the entire suite budget.
|
||
func probeHTTP2(ctx context.Context, tlsConfig *tls.Config, dialer TCPDialer, addr *allregions.EdgeAddr) CheckResult {
|
||
conn, err := dialer.DialEdge(ctx, perProbeDialTimeout, tlsConfig, addr.TCP, nil)
|
||
if err != nil {
|
||
return CheckResult{
|
||
Type: ProbeTypeHTTP2,
|
||
Component: componentTCPConnectivity,
|
||
Target: targetPortHTTP2,
|
||
ProbeStatus: Fail,
|
||
Details: detailsHTTP2BlockedOrUnreachable,
|
||
Action: actionHTTP2Blocked,
|
||
}
|
||
}
|
||
_ = conn.Close()
|
||
|
||
return CheckResult{
|
||
Type: ProbeTypeHTTP2,
|
||
Component: componentTCPConnectivity,
|
||
Target: targetPortHTTP2,
|
||
ProbeStatus: Pass,
|
||
Details: detailsHTTP2HandshakeSuccessful,
|
||
}
|
||
}
|
||
|
||
// probeManagementAPI tests TCP connectivity to api.cloudflare.com:443. A
|
||
// successful TCP connection (no TLS handshake required) confirms the port is
|
||
// reachable. This probe is always a soft failure: the tunnel can run without
|
||
// it, but automatic software updates will be unavailable.
|
||
func probeManagementAPI(ctx context.Context, dialer ManagementDialer) CheckResult {
|
||
dialCtx, cancel := context.WithTimeout(ctx, perProbeDialTimeout)
|
||
defer cancel()
|
||
|
||
conn, err := dialer.DialContext(dialCtx, "tcp", targetAPI)
|
||
if err != nil {
|
||
return CheckResult{
|
||
Type: ProbeTypeManagementAPI,
|
||
Component: componentCloudflareAPI,
|
||
Target: targetAPI,
|
||
ProbeStatus: Fail,
|
||
Details: detailsAPIConnectionFailed,
|
||
Action: actionAPIUnreachable,
|
||
}
|
||
}
|
||
_ = conn.Close()
|
||
|
||
return CheckResult{
|
||
Type: ProbeTypeManagementAPI,
|
||
Component: componentCloudflareAPI,
|
||
Target: targetAPI,
|
||
ProbeStatus: Pass,
|
||
Details: detailsApiReachable,
|
||
}
|
||
}
|
||
|
||
func skipResult(probeType ProbeType, component, target string, details string) CheckResult {
|
||
return CheckResult{
|
||
Type: probeType,
|
||
Component: component,
|
||
Target: target,
|
||
ProbeStatus: Skip,
|
||
Details: details,
|
||
}
|
||
}
|
||
|
||
// newDNSCheckResult creates a DNS CheckResult with the given fields.
|
||
// Type and Component are always ProbeTypeDNS and componentDNSResolution.
|
||
func newDNSCheckResult(target string, status Status, details, action string) CheckResult {
|
||
return CheckResult{
|
||
Type: ProbeTypeDNS,
|
||
Component: componentDNSResolution,
|
||
Target: target,
|
||
ProbeStatus: status,
|
||
Details: details,
|
||
Action: action,
|
||
}
|
||
}
|
||
|
||
// regionTargets returns the human-readable hostnames for region1 and region2
|
||
// based on the optional region flag value.
|
||
func regionTargets(region string) (string, string) {
|
||
switch region {
|
||
case "us":
|
||
return region1US, region2US
|
||
case "fed":
|
||
return region1Fed, region2Fed
|
||
default:
|
||
return region1Global, region2Global
|
||
}
|
||
}
|
||
|
||
// addrsByFamily extracts one V4 and one V6 address from a resolved CNAME group
|
||
// using allregions.NewRegion so that the IP-version preference logic matches
|
||
// production exactly. When cfg.IPVersion restricts to a single family the
|
||
// excluded family's pointer is nil.
|
||
func addrsByFamily(group []*allregions.EdgeAddr, ipVersion allregions.ConfigIPVersion) (v4, v6 *allregions.EdgeAddr) {
|
||
if ipVersion != allregions.IPv6Only {
|
||
v4 = allregions.NewRegion(group, allregions.IPv4Only).GetAnyAddress()
|
||
}
|
||
if ipVersion != allregions.IPv4Only {
|
||
v6 = allregions.NewRegion(group, allregions.IPv6Only).GetAnyAddress()
|
||
}
|
||
return
|
||
}
|
||
|
||
// runDNSProbe runs probeDNS with retry and returns []ResolvedTarget.
|
||
func runDNSProbe(ctx context.Context, resolver DNSResolver, region string) []ResolvedTarget {
|
||
var targets []ResolvedTarget
|
||
withRetry(ctx, maxRetries, func() bool {
|
||
targets = probeDNS(resolver, region)
|
||
for _, t := range targets {
|
||
if t.DNSResult.ProbeStatus == Fail {
|
||
return false
|
||
}
|
||
}
|
||
return len(targets) > 0
|
||
})
|
||
return targets
|
||
}
|
||
|
||
// resolveStaticEdge resolves each --edge addr individually, returning one
|
||
// ResolvedTarget per addr. Unresolvable addrs produce a Fail ResolvedTarget
|
||
// with nil Addrs so the report shows which addresses could not be reached.
|
||
func resolveStaticEdge(addrs []string, log *zerolog.Logger) []ResolvedTarget {
|
||
targets := make([]ResolvedTarget, 0, len(addrs))
|
||
for _, addr := range addrs {
|
||
resolved := allregions.ResolveAddrs([]string{addr}, log)
|
||
if len(resolved) > 0 {
|
||
targets = append(targets, ResolvedTarget{
|
||
Addrs: resolved,
|
||
DNSResult: newDNSCheckResult(addr, Pass, dnsResolvedSuccessfully, ""),
|
||
})
|
||
} else {
|
||
targets = append(targets, ResolvedTarget{
|
||
DNSResult: newDNSCheckResult(addr, Fail, dnsNoAddressesReturned, fmt.Sprintf(actionDNSFail, addr, addr)),
|
||
})
|
||
}
|
||
}
|
||
return targets
|
||
}
|