mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-06-22 20:00:16 +00:00
52519f67e8
The bump of the QUIC library introduces a cyclic dependency between the connection and quic modules hence it is necessary to break this coupling. Right now, the connection module depends on the quic module for the datagram v2/v3 and to which a QUIC connection (currently an interface) is passed. As it is there is no issue however, under the hood, interface is a wrapper around an UDP connection and a QUIC connection meaning this type must be exposed to the quic module since the QUIC Connection will no longer be a interface but a struct. Given the above, these changes introduce an interface, QUICConnection, with the surface used today in cloudflared and a struct, ConnWithCloser, that implements said interface within the quic module. Closes TUN-10563
377 lines
12 KiB
Go
377 lines
12 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"
|
||
edgedial "github.com/cloudflare/cloudflared/edgediscovery"
|
||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||
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 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.
|
||
func probeTLSConfig(caCert string, p connection.Protocol) (*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
|
||
}
|
||
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
|
||
}
|