mirror of
https://github.com/cloudflare/cloudflared
synced 2026-06-08 13:33:07 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e84636de9 | |||
| 4177dd6936 | |||
| f6f60e1059 | |||
| 4494eee13d | |||
| 905d983d14 | |||
| 168f09cb4c | |||
| 0c9014870a | |||
| 31de04f858 | |||
| fbfd76089f | |||
| 21ca2e225e | |||
| f674b82e2a | |||
| ae3799a098 | |||
| 4d8df2b2c0 | |||
| a67c583bf1 | |||
| 22a955f7bb | |||
| a453612e7c | |||
| e8f8b2afb7 | |||
| 7585e38948 | |||
| a9b6f703f0 | |||
| da81fb02ec | |||
| 23b15d0eb6 | |||
| 4a2cbd1870 | |||
| 9978cfd0d5 | |||
| a0401df621 | |||
| cf17ba93b2 | |||
| f827e6216b | |||
| df981b4d89 | |||
| ddd76fa05f |
@@ -6,7 +6,7 @@ RUN apt-get update && \
|
||||
apt-get install --no-install-recommends --allow-downgrades -y \
|
||||
build-essential \
|
||||
git \
|
||||
go-boring=1.26.0-1 \
|
||||
go-boring=1.26.3-1 \
|
||||
libffi-dev \
|
||||
procps \
|
||||
python3-dev \
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
runner: linux-x86-8cpu-16gb
|
||||
stage: build
|
||||
golangVersion: "boring-1.26"
|
||||
imageVersion: "3501-fc698419a625@sha256:aff18c895a50e8451982484e629319e893ebd411675e0482d247079801253e7b"
|
||||
imageVersion: "3605-596a300@sha256:19fa512630b4c5681082c68fd98902e2f92092fc216412df44f7dda31cfa57c3"
|
||||
CGO_ENABLED: 1
|
||||
|
||||
.default-packaging-job: &packaging-job-defaults
|
||||
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
# Pre-push hook for cloudflared
|
||||
# Runs linting and tests before allowing pushes
|
||||
|
||||
set -e
|
||||
|
||||
echo "========================================"
|
||||
echo "Running pre-push checks..."
|
||||
echo "========================================"
|
||||
|
||||
# Run formatting check
|
||||
echo ""
|
||||
echo "==> Checking formatting..."
|
||||
make fmt-check
|
||||
|
||||
# Run linter
|
||||
echo ""
|
||||
echo "==> Running linter..."
|
||||
make lint
|
||||
|
||||
# Run tests
|
||||
echo ""
|
||||
echo "==> Running tests..."
|
||||
make test
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "All pre-push checks passed!"
|
||||
echo "========================================"
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
variables:
|
||||
GO_VERSION: "1.26.0"
|
||||
GO_VERSION: "1.26.3"
|
||||
MAC_GO_VERSION: "go@$GO_VERSION"
|
||||
WIN_GO_VERSION: "go$GO_VERSION"
|
||||
GIT_DEPTH: "0"
|
||||
|
||||
@@ -60,6 +60,16 @@ make vet
|
||||
cd component-tests && python -m pytest test_file.py::test_function_name
|
||||
```
|
||||
|
||||
Notes on linting:
|
||||
|
||||
- `.golangci.yaml` is configured with `new-from-rev` and `whole-files: true`.
|
||||
Touching a file triggers linting of the ENTIRE file, not just the changed
|
||||
hunks. Expect to fix pre-existing issues in files you modify, or add
|
||||
targeted `// nolint: <linter>` comments with a short justification.
|
||||
- Prefer `defer func() { _ = resource.Close() }()` over `defer resource.Close()`
|
||||
for `io.Closer` values whose error truly does not matter — this satisfies
|
||||
`errcheck` without hiding real failures elsewhere.
|
||||
|
||||
## Project Knowledge
|
||||
|
||||
### Package Structure
|
||||
@@ -68,6 +78,24 @@ cd component-tests && python -m pytest test_file.py::test_function_name
|
||||
- Package names should be lowercase, single words when possible
|
||||
- Avoid generic names like `util`, `common`, `helper`
|
||||
|
||||
#### Well-known shared packages
|
||||
|
||||
- `crypto/`: Single source of truth for TLS curve preferences and other
|
||||
cryptographic primitives shared by every edge-facing transport. Import as
|
||||
`cfdcrypto "github.com/cloudflare/cloudflared/crypto"` to avoid colliding
|
||||
with the standard library's `crypto` package. Do NOT duplicate TLS curve
|
||||
or cipher selection logic in other packages.
|
||||
- `tlsconfig/`: Builds the base `*tls.Config` used for edge connections
|
||||
(`CreateTunnelConfig`) and loads origin/CA pools. Curve selection is
|
||||
intentionally NOT set here; it is applied per-connection from the
|
||||
`crypto/` package so the same config can be cloned and reused across
|
||||
protocols.
|
||||
- `features/`: Runtime feature flags including `PostQuantumMode`
|
||||
(`PostQuantumPrefer` = default, `PostQuantumStrict` = `--post-quantum`).
|
||||
- `fips/`: Build-tag driven FIPS detection. Only `fips.IsFipsEnabled()` is
|
||||
exposed; never branch on `fipsEnabled` inside a function if the two
|
||||
branches return the same value.
|
||||
|
||||
### Function and Method Guidelines
|
||||
|
||||
```go
|
||||
@@ -171,6 +199,30 @@ type TunnelProperties struct {
|
||||
- Use channels for goroutine communication
|
||||
- Protect shared state with mutexes
|
||||
- Prefer `sync.RWMutex` for read-heavy workloads
|
||||
- `*tls.Config` values stored in shared maps (e.g.
|
||||
`TunnelConfig.EdgeTLSConfigs`) must be `Clone()`d before mutating
|
||||
per-connection fields like `CurvePreferences` or `NextProtos`. Writing
|
||||
through the shared pointer races with concurrent connection attempts.
|
||||
|
||||
### TLS & Post-Quantum key exchange
|
||||
|
||||
- Per-connection TLS configuration for edge connections is built via
|
||||
`cfdcrypto.TLSConfigWithCurvePreferences(tlsConfig, pqMode)`. It clones
|
||||
the provided `*tls.Config` and sets `CurvePreferences` based on `pqMode`,
|
||||
so callers never need to clone or mutate `CurvePreferences` themselves.
|
||||
Do NOT reach for the package-private `getCurvePreferences` helper; the
|
||||
exported `TLSConfigWithCurvePreferences` is the only supported entry
|
||||
point.
|
||||
- Two PQ modes are supported and apply identically to QUIC and HTTP/2:
|
||||
- `PostQuantumPrefer` (default): `[X25519MLKEM768, P256Kyber768Draft00, CurveP256]`
|
||||
- `PostQuantumStrict` (`--post-quantum`): `[X25519MLKEM768, P256Kyber768Draft00]`
|
||||
- FIPS and non-FIPS builds use the same curve list. Do NOT reintroduce a
|
||||
`fipsEnabled` branch in curve-selection code; if the two modes ever
|
||||
diverge, express the divergence inside `crypto/` so call sites remain
|
||||
untouched.
|
||||
- HTTP/2 supports post-quantum handshakes. Never re-add a
|
||||
`PostQuantumStrict`-based rejection to H2 code paths, and never force
|
||||
`--post-quantum` to select QUIC-only in protocol selection.
|
||||
|
||||
### Configuration
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# use a builder image for building cloudflare
|
||||
ARG TARGET_GOOS
|
||||
ARG TARGET_GOARCH
|
||||
FROM golang:1.26.0 AS builder
|
||||
FROM golang:1.26.3 AS builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
TARGET_GOOS=${TARGET_GOOS} \
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# use a builder image for building cloudflare
|
||||
FROM golang:1.26.0 AS builder
|
||||
FROM golang:1.26.3 AS builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
# the CONTAINER_BUILD envvar is used set github.com/cloudflare/cloudflared/metrics.Runtime=virtual
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# use a builder image for building cloudflare
|
||||
FROM golang:1.26.0 AS builder
|
||||
FROM golang:1.26.3 AS builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
# the CONTAINER_BUILD envvar is used set github.com/cloudflare/cloudflared/metrics.Runtime=virtual
|
||||
|
||||
@@ -289,3 +289,9 @@ ci-test: fmt-check lint test
|
||||
.PHONY: ci-fips-test
|
||||
ci-fips-test:
|
||||
@FIPS=true $(MAKE) ci-test
|
||||
|
||||
.PHONY: install-hooks
|
||||
install-hooks:
|
||||
git config core.hooksPath .githooks
|
||||
@echo "Git hooks installed from .githooks/"
|
||||
@echo "Pre-push hook will run: make fmt-check lint test"
|
||||
|
||||
@@ -79,4 +79,11 @@ To locally run the tests run `make test`
|
||||
To format the code and keep a good code quality use `make fmt` and `make lint`
|
||||
|
||||
### Mocks
|
||||
After changes on interfaces you might need to regenerate the mocks, so run `make mock`
|
||||
After changes on interfaces you might need to regenerate the mocks, so run `make mocks`
|
||||
|
||||
### Git Hooks
|
||||
To avoid CI errors, you can install pre-push hooks that run linting and tests before each push:
|
||||
```bash
|
||||
make install-hooks
|
||||
```
|
||||
This will configure git to use the hooks in `.githooks/` that run `make fmt-check lint test` before each push.
|
||||
|
||||
@@ -1,3 +1,41 @@
|
||||
2026.5.2
|
||||
- 2026-05-26 TUN-10391: Avoid using fmt.Println
|
||||
|
||||
2026.5.1
|
||||
- 2026-05-22 fix: Bump go to 1.26.3 and go.opentelemetry.io/otel and go-jose/v4 to fix CVE's
|
||||
- 2026-05-22 TUN-10391: Avoid blocking cloudflared due to logging
|
||||
- 2026-05-22 TUN-10391: Add precheck integration tests
|
||||
- 2026-05-14 TUN-10511: Revise --edge support for pre-checks
|
||||
- 2026-05-13 fix: Update golang.org/x/net to v0.54.0
|
||||
- 2026-05-13 TUN-10525: Add prechecks kill switch
|
||||
|
||||
2026.5.0
|
||||
- 2026-05-08 Bump golang.org/x/net from v0.40.0 to v0.53.0
|
||||
- 2026-05-07 TUN-10507: Bump go and go-boring to 1.26.2
|
||||
- 2026-05-07 TUN-10511: Add Static DNS Resolvers
|
||||
- 2026-05-07 TUN-10390: Call prechecks
|
||||
- 2026-05-07 TUN-10513: Disable /debug/pprof/cmdline endpoint
|
||||
- 2026-05-06 TUN-10390: Fix missing TLS settings
|
||||
- 2026-05-05 chore: Fix warnings
|
||||
- 2026-05-04 TUN-10389: Implement main run method
|
||||
- 2026-04-30 TUN-10388: Adding probe check
|
||||
- 2026-04-30 TUN-10388 Implement dialers for connectivity checks
|
||||
- 2026-04-30 TUN-10389: Improve probe functions
|
||||
- 2026-04-29 SECENG-13496 update pkg docs for gokeyless to support multiple builds
|
||||
- 2026-04-29 chore: Add pre-push hooks
|
||||
- 2026-04-29 TUN-10388: Use pointer for suggested protocol
|
||||
- 2026-04-27 TUN-10387: Add no-prechecks flag
|
||||
- 2026-04-23 TUN-10386: Add Table Renderer
|
||||
- 2026-04-21 AUTH-4699, AUTH-8460, TUN-10179: Vendor gopsutil/v4 for cross-platform process identification
|
||||
- 2026-04-21 AUTH-4699, AUTH-8460, TUN-10179: Fix .lock file deletion race condition
|
||||
- 2026-04-20 TUN-10413: Centralize TLS curve configuration in crypto/ and adopt X25519MLKEM768 for QUIC/H2
|
||||
- 2026-04-15 TUN-10385: Add connectivity checks foundation
|
||||
- 2026-04-14 chore: Fix errors in cmd
|
||||
- 2026-04-14 TUN-10384: Probe TLS Helper
|
||||
- 2026-04-14 TUN-10383: Set edge-ip-version to auto
|
||||
- 2026-04-10 SECENG-13056 update gokeyless install instructions on pkg.cloudflare.com/index.html
|
||||
- 2026-04-02 TUN-9952: Bump go to 1.26
|
||||
|
||||
2026.3.0
|
||||
- 2026-03-05 TUN-10292: Add cloudflared management token command
|
||||
- 2026-03-03 chore: Addressing small fixes and typos
|
||||
|
||||
@@ -72,3 +72,7 @@ func (c ConnectionOptionsSnapshot) ConnectionOptions() *pogs.ConnectionOptions {
|
||||
func (c ConnectionOptionsSnapshot) LogFields(event *zerolog.Event) *zerolog.Event {
|
||||
return event.Strs("features", c.client.Features)
|
||||
}
|
||||
|
||||
func (c *Config) ConnectionFeaturesSnapshot() features.FeatureSnapshot {
|
||||
return c.featureSelector.Snapshot()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
|
||||
@@ -57,3 +60,57 @@ func ConfigureLoggingFlags(shouldHide bool) []cli.Flag {
|
||||
FlagLogOutput,
|
||||
}
|
||||
}
|
||||
|
||||
// LogTable renders lines inside an ASCII table and logs each rendered row.
|
||||
func LogTable(log *zerolog.Logger, lines []string, title ...string) {
|
||||
tableTitle := ""
|
||||
if len(title) > 0 {
|
||||
tableTitle = title[0]
|
||||
}
|
||||
for _, line := range asciiBox(lines, tableTitle, 2) {
|
||||
if line != "" {
|
||||
log.Info().Msg(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// asciiBox wraps lines in a bordered ASCII box with an optional title row.
|
||||
func asciiBox(lines []string, title string, padding int) (box []string) {
|
||||
maxLen := maxLen(lines, title)
|
||||
spacer := strings.Repeat(" ", padding)
|
||||
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
|
||||
box = append(box, border)
|
||||
if title != "" {
|
||||
box = append(box, renderBoxLine(centerLine(title, maxLen), maxLen, spacer))
|
||||
box = append(box, border)
|
||||
}
|
||||
for _, line := range lines {
|
||||
box = append(box, renderBoxLine(line, maxLen, spacer))
|
||||
}
|
||||
box = append(box, border)
|
||||
return
|
||||
}
|
||||
|
||||
// renderBoxLine pads a single line so it fills the box width.
|
||||
func renderBoxLine(line string, maxLen int, spacer string) string {
|
||||
return "|" + spacer + line + strings.Repeat(" ", maxLen-len(line)) + spacer + "|"
|
||||
}
|
||||
|
||||
// centerLine pads line evenly so it is centered within width.
|
||||
func centerLine(line string, width int) string {
|
||||
padding := width - len(line)
|
||||
leftPadding := padding / 2
|
||||
rightPadding := padding - leftPadding
|
||||
return strings.Repeat(" ", leftPadding) + line + strings.Repeat(" ", rightPadding)
|
||||
}
|
||||
|
||||
// maxLen returns the longest visible line length including the title.
|
||||
func maxLen(lines []string, title string) int {
|
||||
max := len(title)
|
||||
for _, line := range lines {
|
||||
if len(line) > max {
|
||||
max = len(line)
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLogTableWithoutTitle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
lines := captureTableLogs(t, []string{"first", "second"})
|
||||
|
||||
assert.Equal(t, []string{
|
||||
"+----------+",
|
||||
"| first |",
|
||||
"| second |",
|
||||
"+----------+",
|
||||
}, lines)
|
||||
}
|
||||
|
||||
func TestLogTableWithTitle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
lines := captureTableLogs(t, []string{"first", "second"}, "TT")
|
||||
|
||||
assert.Equal(t, []string{
|
||||
"+----------+",
|
||||
"| TT |",
|
||||
"+----------+",
|
||||
"| first |",
|
||||
"| second |",
|
||||
"+----------+",
|
||||
}, lines)
|
||||
}
|
||||
|
||||
func captureTableLogs(t *testing.T, lines []string, title ...string) []string {
|
||||
t.Helper()
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
LogTable(&logger, lines, title...)
|
||||
|
||||
// nolint: prealloc
|
||||
var messages []string
|
||||
for _, line := range bytes.Split(bytes.TrimSpace(buf.Bytes()), []byte("\n")) {
|
||||
var entry struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(line, &entry))
|
||||
messages = append(messages, entry.Message)
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
@@ -81,6 +81,9 @@ const (
|
||||
// EdgeBindAddress is the command line flag to bind to IP address for outgoing connections to Cloudflare Edge
|
||||
EdgeBindAddress = "edge-bind-address"
|
||||
|
||||
// CACert Certificate Authority authenticating connections with Cloudflare's edge network.
|
||||
CACert = "cacert"
|
||||
|
||||
// Force is the command line flag to specify if you wish to force an action
|
||||
Force = "force"
|
||||
|
||||
@@ -120,6 +123,9 @@ const (
|
||||
// NoAutoUpdate is the command line flag to disable cloudflared from checking for updates
|
||||
NoAutoUpdate = "no-autoupdate"
|
||||
|
||||
// NoPrechecks is the command line flag to skip connectivity pre-checks at startup.
|
||||
NoPrechecks = "no-prechecks"
|
||||
|
||||
// LogLevel is the command line flag for the cloudflared logging level
|
||||
LogLevel = "loglevel"
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -77,6 +80,7 @@ var (
|
||||
"config",
|
||||
cfdflags.AutoUpdateFreq,
|
||||
cfdflags.NoAutoUpdate,
|
||||
cfdflags.NoPrechecks,
|
||||
cfdflags.Metrics,
|
||||
"pidfile",
|
||||
"url",
|
||||
@@ -413,6 +417,13 @@ func StartServer(
|
||||
}
|
||||
connectorID := tunnelConfig.ClientConfig.ConnectorID
|
||||
|
||||
// Run connectivity pre-checks for cloudflared. This runs in a separate
|
||||
// goroutine, as we want to keep initializing cloudflared while prechecks
|
||||
// are running. Prechecks are controlled via DNS flag for remote kill-switch capability.
|
||||
if !tunnelConfig.ClientConfig.ConnectionFeaturesSnapshot().SkipPrechecks && !c.Bool(cfdflags.NoPrechecks) {
|
||||
go runPrechecks(c, log, tunnelConfig.Region)
|
||||
}
|
||||
|
||||
// Disable ICMP packet routing for quick tunnels
|
||||
if quickTunnelURL != "" {
|
||||
tunnelConfig.ICMPRouterServer = nil
|
||||
@@ -512,6 +523,41 @@ 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,
|
||||
EdgeAddrs: c.StringSlice(cfdflags.Edge),
|
||||
}
|
||||
|
||||
dialers := prechecks.RunDialers{
|
||||
DNSResolver: &prechecks.EdgeDNSResolver{Log: log},
|
||||
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
|
||||
cliutil.LogTable(log, report.String(), "CONNECTIVITY PRE-CHECKS")
|
||||
|
||||
// Also log structured results for log aggregation
|
||||
report.LogEvent(log)
|
||||
}
|
||||
|
||||
func waitToShutdown(wg *sync.WaitGroup,
|
||||
cancelServerContext func(),
|
||||
errC <-chan error,
|
||||
@@ -641,7 +687,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
Hidden: false,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: tlsconfig.CaCertFlag,
|
||||
Name: cfdflags.CACert,
|
||||
Usage: "Certificate Authority authenticating connections with Cloudflare's edge network.",
|
||||
EnvVars: []string{"TUNNEL_CACERT"},
|
||||
Hidden: true,
|
||||
@@ -881,6 +927,13 @@ func configureCloudflaredFlags(shouldHide bool) []cli.Flag {
|
||||
Value: false,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: cfdflags.NoPrechecks,
|
||||
Usage: "Skip connectivity pre-checks at startup.",
|
||||
EnvVars: []string{"TUNNEL_NO_PRECHECKS"},
|
||||
Value: false,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: cfdflags.Metrics,
|
||||
Value: metrics.GetMetricsDefaultAddress(metrics.Runtime),
|
||||
@@ -904,6 +957,7 @@ and virtualized host network stacks from each other`,
|
||||
}
|
||||
|
||||
func configureProxyFlags(shouldHide bool) []cli.Flag {
|
||||
//nolint: prealloc
|
||||
flags := []cli.Flag{
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "url",
|
||||
|
||||
@@ -140,23 +140,13 @@ func prepareTunnelConfig(
|
||||
}
|
||||
tags = append(tags, pogs.Tag{Name: "ID", Value: clientConfig.ConnectorID.String()})
|
||||
|
||||
clientFeatures := featureSelector.Snapshot()
|
||||
pqMode := clientFeatures.PostQuantum
|
||||
if pqMode == features.PostQuantumStrict {
|
||||
// Error if the user tries to force a non-quic transport protocol
|
||||
if transportProtocol != connection.AutoSelectFlag && transportProtocol != connection.QUIC.String() {
|
||||
return nil, nil, fmt.Errorf("post-quantum is only supported with the quic transport")
|
||||
}
|
||||
transportProtocol = connection.QUIC.String()
|
||||
}
|
||||
|
||||
cfg := config.GetConfiguration()
|
||||
ingressRules, err := ingress.ParseIngressFromConfigAndCLI(cfg, c, log)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
protocolSelector, err := connection.NewProtocolSelector(transportProtocol, namedTunnel.Credentials.AccountTag, c.IsSet(TunnelTokenFlag), isPostQuantumEnforced, edgediscovery.ProtocolPercentage, connection.ResolveTTL, log)
|
||||
protocolSelector, err := connection.NewProtocolSelector(transportProtocol, namedTunnel.Credentials.AccountTag, c.IsSet(TunnelTokenFlag), edgediscovery.ProtocolPercentage, connection.ResolveTTL, log)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -168,7 +158,7 @@ func prepareTunnelConfig(
|
||||
if tlsSettings == nil {
|
||||
return nil, nil, fmt.Errorf("%s has unknown TLS settings", p)
|
||||
}
|
||||
edgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c, tlsSettings.ServerName)
|
||||
edgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c.String(flags.CACert), tlsSettings.ServerName)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "unable to create TLS config to connect with edge")
|
||||
}
|
||||
@@ -261,6 +251,7 @@ func prepareTunnelConfig(
|
||||
DisableQUICPathMTUDiscovery: c.Bool(flags.QuicDisablePathMTUDiscovery),
|
||||
QUICConnectionLevelFlowControlLimit: c.Uint64(flags.QuicConnLevelFlowControlLimit),
|
||||
QUICStreamLevelFlowControlLimit: c.Uint64(flags.QuicStreamLevelFlowControlLimit),
|
||||
NoPrechecks: c.Bool(flags.NoPrechecks),
|
||||
OriginDNSService: dnsService,
|
||||
OriginDialerService: originDialerService,
|
||||
}
|
||||
@@ -300,7 +291,7 @@ func gracePeriod(c *cli.Context) (time.Duration, error) {
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
return term.IsTerminal(int(os.Stdout.Fd()))
|
||||
return term.IsTerminal(int(os.Stdout.Fd())) // nolint:gosec
|
||||
}
|
||||
|
||||
// ParseConfigIPVersion returns the IP version from possible expected values from config
|
||||
@@ -341,7 +332,7 @@ func testIPBindable(ip net.IP) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listener.Close()
|
||||
_ = listener.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -503,7 +494,7 @@ func findLocalAddr(dst net.IP, port int) (netip.Addr, error) {
|
||||
if err != nil {
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
defer udpConn.Close()
|
||||
defer func() { _ = udpConn.Close() }()
|
||||
localAddrPort, err := netip.ParseAddrPort(udpConn.LocalAddr().String())
|
||||
if err != nil {
|
||||
return netip.Addr{}, err
|
||||
|
||||
@@ -100,6 +100,7 @@ func login(c *cli.Context) error {
|
||||
c.Bool(cfdflags.AutoCloseInterstitial),
|
||||
isFEDRamp,
|
||||
log,
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Failed to write the certificate.\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", path)
|
||||
@@ -122,7 +123,7 @@ func login(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, resourceData, 0600); err != nil {
|
||||
if err := os.WriteFile(path, resourceData, 0600); err != nil { // nolint: gosec
|
||||
return errors.Wrap(err, fmt.Sprintf("error writing cert to %s", path))
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
)
|
||||
@@ -44,7 +45,7 @@ func RunQuickTunnel(sc *subcommandContext) error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to request quick Tunnel")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
// This will read the entire response into memory so we can print it in case of error
|
||||
rsp_body, err := io.ReadAll(resp.Body)
|
||||
@@ -76,12 +77,10 @@ func RunQuickTunnel(sc *subcommandContext) error {
|
||||
url = "https://" + url
|
||||
}
|
||||
|
||||
for _, line := range AsciiBox([]string{
|
||||
cliutil.LogTable(sc.log, []string{
|
||||
"Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):",
|
||||
url,
|
||||
}, 2) {
|
||||
sc.log.Info().Msg(line)
|
||||
}
|
||||
})
|
||||
|
||||
if !sc.c.IsSet(flags.Protocol) {
|
||||
_ = sc.c.Set(flags.Protocol, "quic")
|
||||
@@ -116,26 +115,3 @@ type QuickTunnel struct {
|
||||
AccountTag string `json:"account_tag"`
|
||||
Secret []byte `json:"secret"`
|
||||
}
|
||||
|
||||
// Print out the given lines in a nice ASCII box.
|
||||
func AsciiBox(lines []string, padding int) (box []string) {
|
||||
maxLen := maxLen(lines)
|
||||
spacer := strings.Repeat(" ", padding)
|
||||
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
|
||||
box = append(box, border)
|
||||
for _, line := range lines {
|
||||
box = append(box, "|"+spacer+line+strings.Repeat(" ", maxLen-len(line))+spacer+"|")
|
||||
}
|
||||
box = append(box, border)
|
||||
return
|
||||
}
|
||||
|
||||
func maxLen(lines []string) int {
|
||||
max := 0
|
||||
for _, line := range lines {
|
||||
if len(line) > max {
|
||||
max = len(line)
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
@@ -421,7 +421,7 @@ func listCommand(c *cli.Context) error {
|
||||
|
||||
func formatAndPrintTunnelList(tunnels []*cfapi.Tunnel, showRecentlyDisconnected bool) {
|
||||
writer := tabWriter()
|
||||
defer writer.Flush()
|
||||
defer func() { _ = writer.Flush() }()
|
||||
|
||||
_, _ = fmt.Fprintln(writer, "You can obtain more detailed information for each tunnel with `cloudflared tunnel info <name/uuid>`")
|
||||
|
||||
@@ -444,13 +444,14 @@ func formatAndPrintTunnelList(tunnels []*cfapi.Tunnel, showRecentlyDisconnected
|
||||
func fmtConnections(connections []cfapi.Connection, showRecentlyDisconnected bool) string {
|
||||
// Count connections per colo
|
||||
numConnsPerColo := make(map[string]uint, len(connections))
|
||||
for _, connection := range connections {
|
||||
if !connection.IsPendingReconnect || showRecentlyDisconnected {
|
||||
numConnsPerColo[connection.ColoName]++
|
||||
for _, cfConnections := range connections {
|
||||
if !cfConnections.IsPendingReconnect || showRecentlyDisconnected {
|
||||
numConnsPerColo[cfConnections.ColoName]++
|
||||
}
|
||||
}
|
||||
|
||||
// Get sorted list of colos
|
||||
// nolint: prealloc
|
||||
sortedColos := []string{}
|
||||
for coloName := range numConnsPerColo {
|
||||
sortedColos = append(sortedColos, coloName)
|
||||
@@ -488,11 +489,12 @@ func readyCommand(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// nolint: gosec // URL is constructed from the user-configured local metrics endpoint.
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
if res.StatusCode != 200 {
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
@@ -613,7 +615,7 @@ func getTunnel(sc *subcommandContext, tunnelID uuid.UUID) (*cfapi.Tunnel, error)
|
||||
|
||||
func formatAndPrintConnectionsList(tunnelInfo Info, showRecentlyDisconnected bool) {
|
||||
writer := tabWriter()
|
||||
defer writer.Flush()
|
||||
defer func() { _ = writer.Flush() }()
|
||||
|
||||
// Print the general tunnel info table
|
||||
_, _ = fmt.Fprintf(writer, "NAME: %s\nID: %s\nCREATED: %s\n\n", tunnelInfo.Name, tunnelInfo.ID, tunnelInfo.CreatedAt)
|
||||
@@ -654,14 +656,14 @@ func formatAndPrintConnectionsList(tunnelInfo Info, showRecentlyDisconnected boo
|
||||
|
||||
func tabWriter() *tabwriter.Writer {
|
||||
const (
|
||||
minWidth = 0
|
||||
tabWidth = 8
|
||||
padding = 1
|
||||
padChar = ' '
|
||||
flags = 0
|
||||
minWidth = 0
|
||||
tabWidth = 8
|
||||
padding = 1
|
||||
padChar = ' '
|
||||
formatFlags = 0
|
||||
)
|
||||
|
||||
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags)
|
||||
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, formatFlags)
|
||||
return writer
|
||||
}
|
||||
|
||||
@@ -712,7 +714,8 @@ func renderOutput(format string, v interface{}) error {
|
||||
}
|
||||
|
||||
func buildRunCommand() *cli.Command {
|
||||
flags := []cli.Flag{
|
||||
//nolint: prealloc
|
||||
cliFlags := []cli.Flag{
|
||||
credentialsFileFlag,
|
||||
credentialsContentsFlag,
|
||||
postQuantumFlag,
|
||||
@@ -725,7 +728,7 @@ func buildRunCommand() *cli.Command {
|
||||
maxActiveFlowsFlag,
|
||||
dnsResolverAddrsFlag,
|
||||
}
|
||||
flags = append(flags, configureProxyFlags(false)...)
|
||||
cliFlags = append(cliFlags, configureProxyFlags(false)...)
|
||||
return &cli.Command{
|
||||
Name: "run",
|
||||
Action: cliutil.ConfiguredAction(runCommand),
|
||||
@@ -740,7 +743,7 @@ func buildRunCommand() *cli.Command {
|
||||
If you experience other problems running the tunnel, "cloudflared tunnel cleanup" may help by removing
|
||||
any old connection records.
|
||||
`,
|
||||
Flags: flags,
|
||||
Flags: cliFlags,
|
||||
CustomHelpTemplate: commandHelpTemplate(),
|
||||
}
|
||||
}
|
||||
@@ -765,6 +768,7 @@ func runCommand(c *cli.Context) error {
|
||||
// Check if tokenStr is blank before checking for tokenFile
|
||||
if tokenStr == "" {
|
||||
if tokenFile := c.String(TunnelTokenFileFlag); tokenFile != "" {
|
||||
// nolint: gosec
|
||||
data, err := os.ReadFile(tokenFile)
|
||||
if err != nil {
|
||||
return cliutil.UsageError("Failed to read token file: %s", err.Error())
|
||||
@@ -1105,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),
|
||||
|
||||
+4
-31
@@ -1,10 +1,9 @@
|
||||
import json
|
||||
import subprocess
|
||||
from time import sleep
|
||||
|
||||
from constants import MANAGEMENT_HOST_NAME
|
||||
from setup import get_config_from_file
|
||||
from util import get_tunnel_connector_id
|
||||
from util import get_tunnel_connector_id, CloudflaredProcess
|
||||
|
||||
SINGLE_CASE_TIMEOUT = 600
|
||||
|
||||
@@ -83,38 +82,12 @@ class CloudflaredCli:
|
||||
|
||||
def __enter__(self):
|
||||
self.basecmd += ["run"]
|
||||
self.process = subprocess.Popen(self.basecmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
self.logger.info(f"Run cmd {self.basecmd}")
|
||||
return self.process
|
||||
self.cfd = CloudflaredProcess(self.basecmd, allow_input=False, capture_output=True)
|
||||
return self.cfd
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||
terminate_gracefully(self.process, self.logger, self.basecmd)
|
||||
self.logger.debug(f"{self.basecmd} logs: {self.process.stderr.read()}")
|
||||
|
||||
|
||||
def terminate_gracefully(process, logger, cmd):
|
||||
process.terminate()
|
||||
process_terminated = wait_for_terminate(process)
|
||||
if not process_terminated:
|
||||
process.kill()
|
||||
logger.warning(f"{cmd}: cloudflared did not terminate within wait period. Killing process. logs: \
|
||||
stdout: {process.stdout.read()}, stderr: {process.stderr.read()}")
|
||||
|
||||
|
||||
def wait_for_terminate(opened_subprocess, attempts=10, poll_interval=1):
|
||||
"""
|
||||
wait_for_terminate polls the opened_subprocess every x seconds for a given number of attempts.
|
||||
It returns true if the subprocess was terminated and false if it didn't.
|
||||
"""
|
||||
for _ in range(attempts):
|
||||
if _is_process_stopped(opened_subprocess):
|
||||
return True
|
||||
sleep(poll_interval)
|
||||
return False
|
||||
|
||||
|
||||
def _is_process_stopped(process):
|
||||
return process.poll() is not None
|
||||
self.cfd.cleanup()
|
||||
|
||||
|
||||
def cert_path():
|
||||
|
||||
@@ -5,6 +5,17 @@ MAX_LOG_LINES = 50
|
||||
|
||||
MANAGEMENT_HOST_NAME = "management.argotunnel.com"
|
||||
|
||||
# How long to wait for the cloudflared process to exit after SIGTERM before
|
||||
# sending SIGKILL.
|
||||
GRACEFUL_SHUTDOWN_TIMEOUT = 10
|
||||
# How long to wait for each pipe reader thread to finish after the process
|
||||
# exits.
|
||||
READER_THREAD_JOIN_TIMEOUT = 5
|
||||
# How long to wait for an expected log message to appear before giving up.
|
||||
LOG_POLL_TIMEOUT = 30
|
||||
# How often to re-check the accumulated log lines while polling.
|
||||
LOG_POLL_INTERVAL = 0.5
|
||||
|
||||
|
||||
def protocols():
|
||||
return ["http2", "quic"]
|
||||
|
||||
@@ -17,25 +17,6 @@ class TestEdgeDiscovery:
|
||||
config["edge-ip-version"] = edge_ip_version
|
||||
return config
|
||||
|
||||
@pytest.mark.parametrize("protocol", protocols())
|
||||
def test_default_only(self, tmp_path, component_tests_config, protocol):
|
||||
"""
|
||||
This test runs a tunnel with the default edge-ip-version (auto), which will use
|
||||
whichever address family the system resolver returns first.
|
||||
"""
|
||||
if self.has_ipv6_only():
|
||||
self.expect_address_connections(
|
||||
tmp_path, component_tests_config, protocol, None, self.expect_ipv6_address)
|
||||
elif self.has_ipv4_only():
|
||||
self.expect_address_connections(
|
||||
tmp_path, component_tests_config, protocol, None, self.expect_ipv4_address)
|
||||
elif self.has_dual_stack(address_family_preference=socket.AddressFamily.AF_INET6):
|
||||
self.expect_address_connections(
|
||||
tmp_path, component_tests_config, protocol, None, self.expect_ipv6_address)
|
||||
else:
|
||||
self.expect_address_connections(
|
||||
tmp_path, component_tests_config, protocol, None, self.expect_ipv4_address)
|
||||
|
||||
@pytest.mark.parametrize("protocol", protocols())
|
||||
def test_ipv4_only(self, tmp_path, component_tests_config, protocol):
|
||||
"""
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from constants import MAX_LOG_LINES
|
||||
from constants import MAX_LOG_LINES, LOG_POLL_INTERVAL, LOG_POLL_TIMEOUT
|
||||
from util import start_cloudflared, wait_tunnel_ready, send_requests
|
||||
|
||||
# Rolling logger rotate log files after 1 MB
|
||||
@@ -12,12 +13,14 @@ expect_message = "Starting Hello"
|
||||
|
||||
|
||||
def assert_log_to_terminal(cloudflared):
|
||||
for _ in range(0, MAX_LOG_LINES):
|
||||
line = cloudflared.stderr.readline()
|
||||
if not line:
|
||||
break
|
||||
if expect_message.encode() in line:
|
||||
return
|
||||
# All logs are drained by a background thread into cloudflared.stdout_lines.
|
||||
# Poll the accumulated lines until the expected message appears.
|
||||
deadline = time.monotonic() + LOG_POLL_TIMEOUT
|
||||
while time.monotonic() < deadline:
|
||||
for line in list(cloudflared.stdout_lines):
|
||||
if expect_message.encode() in line:
|
||||
return
|
||||
time.sleep(LOG_POLL_INTERVAL)
|
||||
raise Exception(f"terminal log doesn't contain {expect_message}")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Integration tests for cloudflared connectivity pre-checks (TUN-10391).
|
||||
|
||||
Scope
|
||||
-----
|
||||
These tests verify the end-to-end behavior of cloudflared pre-checks:
|
||||
- that the human-readable table written to the log output has the correct
|
||||
structure and content,
|
||||
- that structured JSON log lines are emitted with the expected fields, and
|
||||
- that running the `diag` subcommand against a live tunnel instance produces a
|
||||
zip archive that contains prechecks.json.
|
||||
|
||||
They do NOT cover every failure mode of the precheck logic — those are owned
|
||||
by the unit tests in prechecks/checker_test.go which use mock dialers.
|
||||
|
||||
At the integration level the only reliable way to induce specific failure modes
|
||||
without real firewall intervention is:
|
||||
|
||||
- --edge <unreachable>: StaticEdgeDNSResolver resolves the literal IP
|
||||
directly (DNS row = PASS), then both QUIC and HTTP/2 probes time out
|
||||
-> hard fail (both transports blocked).
|
||||
This does NOT exercise the DNS-failure -> transport-skip path.
|
||||
|
||||
DNS failure and Management API failure cannot be triggered via CLI flags alone;
|
||||
they require network-level intervention outside the component-test harness.
|
||||
|
||||
stdout/stderr design
|
||||
--------------------
|
||||
The pre-checks table is emitted via cliutil.LogTable, which wraps the content
|
||||
in an ASCII box and logs each line at Info level through zerolog. zerolog
|
||||
writes to stderr, which the test harness merges into stdout (stderr=STDOUT in
|
||||
Popen). We poll a --logfile for the "precheck complete" sentinel before
|
||||
leaving the `with` block, ensuring the goroutine has finished. We then call
|
||||
cfd.terminate(). After the `with` block exits, the process is dead and all
|
||||
output has been captured by CloudflaredProcess's background reader thread. We
|
||||
read the accumulated lines from cfd.stdout_lines.
|
||||
|
||||
Box format (cliutil.asciiBox with padding=2, title="CONNECTIVITY PRE-CHECKS"):
|
||||
+----...----+
|
||||
| CONNECTIVITY PRE-CHECKS | (centered title)
|
||||
+----...----+
|
||||
| COMPONENT TARGET ... | (content rows)
|
||||
...
|
||||
+----...----+
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
import zipfile as zipfilemod
|
||||
|
||||
from constants import METRICS_PORT
|
||||
from util import LOGGER, start_cloudflared, wait_tunnel_ready
|
||||
|
||||
# ASCII box constants (cliutil.asciiBox, padding=2, title="CONNECTIVITY PRE-CHECKS")
|
||||
BOX_TITLE = "CONNECTIVITY PRE-CHECKS"
|
||||
BOX_BORDER_RE = re.compile(r"^\+(-+)\+$", re.MULTILINE) # matches +----...----+
|
||||
COL_HEADER = "COMPONENT" # first word of the column-header row
|
||||
|
||||
# zerolog console format: "2006-01-02T15:04:05Z LVL <message>"
|
||||
_LOG_PREFIX_RE = re.compile(r"^\S+ \w+ ")
|
||||
|
||||
# Component names (probes.go: componentXxx)
|
||||
COMP_DNS = "DNS Resolution"
|
||||
COMP_QUIC = "UDP Connectivity"
|
||||
COMP_H2 = "TCP Connectivity"
|
||||
COMP_API = "Cloudflare API"
|
||||
|
||||
# Target labels used in the rendered table.
|
||||
#
|
||||
# probeRegion() (checker.go:216) always overwrites the Target field of
|
||||
# whatever CheckResult the inner probe function returns with the regionTarget
|
||||
# hostname, so QUIC and HTTP/2 rows carry the same region hostname as the
|
||||
# corresponding DNS row — not the "Port 7844 (QUIC/HTTP2)" strings that
|
||||
# targetPortQUIC/targetPortHTTP2 define. Those port-label constants are only
|
||||
# used in the empty-addrs SKIP branch and inside action message strings.
|
||||
TARGET_API = "api.cloudflare.com:443"
|
||||
TARGET_REGION1 = "region1.v2.argotunnel.com"
|
||||
TARGET_REGION2 = "region2.v2.argotunnel.com"
|
||||
|
||||
# Details strings (probes.go: detailsXxx)
|
||||
DETAILS_DNS_RESOLVED = "DNS Resolved successfully"
|
||||
DETAILS_QUIC_OK = "QUIC connection successful"
|
||||
DETAILS_HTTP2_OK = "HTTP/2 connection successful"
|
||||
DETAILS_API_OK = "API is reachable"
|
||||
DETAILS_QUIC_FAIL = "QUIC connection failed"
|
||||
DETAILS_HTTP2_FAIL = "HTTP/2 connection is blocked or unreachable"
|
||||
|
||||
# Status labels (result.go: xyzStatus)
|
||||
PASS = "PASS"
|
||||
FAIL = "FAIL"
|
||||
SKIP = "SKIP"
|
||||
|
||||
# Action prefixes (result.go: renderActions)
|
||||
PREFIX_ERROR = "ERROR: "
|
||||
PREFIX_WARNING = "WARNING: "
|
||||
|
||||
# Action messages (probes.go: actionXxx)
|
||||
ACTION_QUIC_BLOCKED = "Allow outbound QUIC traffic on port 7844 or use HTTP2."
|
||||
ACTION_HTTP2_BLOCKED = "Allow outbound TCP on port 7844."
|
||||
|
||||
# Exact summary lines (result.go: summaryLine)
|
||||
SUMMARY_HEALTHY = "SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol."
|
||||
SUMMARY_CRITICAL = "SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel."
|
||||
|
||||
# structured log constants (result.go)
|
||||
|
||||
LOG_MSG_PRECHECK = "precheck"
|
||||
LOG_MSG_PRECHECK_COMPLETE = "precheck complete"
|
||||
STATUS_PASS_LOG = "pass"
|
||||
|
||||
UNREACHABLE_EDGE = "192.0.2.1:7844"
|
||||
|
||||
# cloudflared dial timeout per probe: 5 s, up to 2 retries -> ~15 s total.
|
||||
PRECHECK_POLL_TIMEOUT_SECS = 15
|
||||
PRECHECK_POLL_INTERVAL_SECS = 1
|
||||
|
||||
# ---------- helpers ----------
|
||||
|
||||
def _poll_log_file_for_precheck_complete(log_file: str, timeout: float) -> list[dict]:
|
||||
"""
|
||||
Poll a JSON log file until a 'precheck complete' line appears or timeout
|
||||
expires. Returns all precheck-related log lines found.
|
||||
|
||||
cloudflared's --logfile writes one JSON object per line. Polling keeps
|
||||
the test fast on healthy networks and still tolerates slow CI hosts.
|
||||
|
||||
We re-read from the beginning of the file on every poll because the file
|
||||
is append-only, small, and tracking a byte offset would add complexity with
|
||||
no meaningful performance benefit for a ~15 s total window.
|
||||
"""
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
lines = _read_precheck_log_lines_from_file(log_file)
|
||||
if any(l.get("message") == LOG_MSG_PRECHECK_COMPLETE for l in lines):
|
||||
return lines
|
||||
time.sleep(PRECHECK_POLL_INTERVAL_SECS)
|
||||
return _read_precheck_log_lines_from_file(log_file)
|
||||
|
||||
|
||||
def _read_precheck_log_lines_from_file(log_file: str) -> list[dict]:
|
||||
"""Parse all precheck-related JSON log lines from a --logfile path."""
|
||||
result = []
|
||||
try:
|
||||
with open(log_file, "r") as f:
|
||||
for raw_line in f:
|
||||
raw_line = raw_line.strip()
|
||||
if not raw_line:
|
||||
continue
|
||||
try:
|
||||
obj = json.loads(raw_line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
msg = obj.get("message") or obj.get("msg", "")
|
||||
if msg in (LOG_MSG_PRECHECK, LOG_MSG_PRECHECK_COMPLETE):
|
||||
result.append(obj)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
# stdout table parse
|
||||
class TableRow:
|
||||
"""One data row parsed from the rendered precheck table."""
|
||||
def __init__(self, component: str, target: str, status: str, details: str):
|
||||
self.component = component
|
||||
self.target = target
|
||||
self.status = status
|
||||
self.details = details
|
||||
|
||||
def __repr__(self):
|
||||
return f"TableRow({self.component!r}, {self.target!r}, {self.status!r}, {self.details!r})"
|
||||
|
||||
|
||||
def _strip_log_prefix(line: str) -> str:
|
||||
"""Remove the zerolog console prefix ('2006-01-02T15:04:05Z LVL ') if present."""
|
||||
return _LOG_PREFIX_RE.sub("", line, count=1)
|
||||
|
||||
|
||||
def _unbox_line(line: str) -> str:
|
||||
"""Strip the box border padding from a content line: '| text |' -> 'text'.
|
||||
|
||||
Accepts lines that may still carry a zerolog console prefix; the prefix is
|
||||
removed before the box delimiters are stripped.
|
||||
"""
|
||||
msg = _strip_log_prefix(line)
|
||||
if msg.startswith("|") and msg.endswith("|"):
|
||||
return msg[1:-1].strip()
|
||||
return msg.strip()
|
||||
|
||||
|
||||
def _parse_table(stdout: str) -> list[TableRow]:
|
||||
"""
|
||||
Parse the data rows from a precheck table in stdout.
|
||||
|
||||
The table is now wrapped in an ASCII box by cliutil.LogTable. Each
|
||||
content line has the form '| <content> |', optionally preceded by a
|
||||
zerolog console prefix. We strip both the prefix and the box borders
|
||||
before splitting on two-or-more spaces (text/tabwriter padding=2).
|
||||
|
||||
We skip the column-header row and stop at blank lines, SUMMARY, box
|
||||
border lines, ERROR, or WARNING lines.
|
||||
"""
|
||||
rows = []
|
||||
in_data = False
|
||||
for raw_line in stdout.splitlines():
|
||||
msg = _strip_log_prefix(raw_line)
|
||||
line = _unbox_line(raw_line)
|
||||
if line.startswith("COMPONENT"):
|
||||
in_data = True
|
||||
continue
|
||||
if not in_data:
|
||||
continue
|
||||
if (line == "" or line.startswith("SUMMARY") or BOX_BORDER_RE.match(msg)
|
||||
or line.startswith("ERROR") or line.startswith("WARNING")):
|
||||
in_data = False
|
||||
continue
|
||||
parts = re.split(r" +", line.rstrip())
|
||||
if len(parts) >= 3:
|
||||
rows.append(TableRow(
|
||||
component=parts[0],
|
||||
target=parts[1],
|
||||
status=parts[2],
|
||||
details=parts[3] if len(parts) >= 4 else "",
|
||||
))
|
||||
return rows
|
||||
|
||||
|
||||
def _rows_for(rows: list[TableRow], component: str) -> list[TableRow]:
|
||||
return [r for r in rows if r.component == component]
|
||||
|
||||
|
||||
# log assertions
|
||||
|
||||
def _assert_precheck_summary_log(
|
||||
log_lines: list[dict],
|
||||
*,
|
||||
hard_fail: bool,
|
||||
suggested_protocol: str | None = None,
|
||||
):
|
||||
"""Assert the 'precheck complete' summary log line has the expected fields."""
|
||||
summary_lines = [l for l in log_lines if l.get("message") == LOG_MSG_PRECHECK_COMPLETE]
|
||||
assert len(summary_lines) == 1, \
|
||||
f"Expected exactly one '{LOG_MSG_PRECHECK_COMPLETE}' log line; got {summary_lines}"
|
||||
summary = summary_lines[0]
|
||||
|
||||
assert summary.get("hard_fail") is hard_fail, \
|
||||
f"Expected hard_fail={hard_fail} in summary log: {summary}"
|
||||
|
||||
if suggested_protocol is not None:
|
||||
assert summary.get("suggested_protocol") == suggested_protocol, \
|
||||
(f"Expected suggested_protocol={suggested_protocol!r}; "
|
||||
f"got {summary.get('suggested_protocol')!r}")
|
||||
|
||||
|
||||
# ---------- Tests ----------
|
||||
|
||||
class TestPrechecksHappyPath:
|
||||
"""
|
||||
On a healthy connection all probes pass. We assert:
|
||||
- the full table structure (header, column header, separator)
|
||||
- every row's component, target, status, and details
|
||||
- no ERROR/WARNING action lines
|
||||
- the exact summary line
|
||||
- the structured log summary (hard_fail=false, suggested_protocol=quic)
|
||||
"""
|
||||
|
||||
def test_prechecks_pass_on_healthy_connection(self, tmp_path, component_tests_config):
|
||||
log_file = str(tmp_path / "cloudflared.log")
|
||||
config = component_tests_config({"logfile": log_file})
|
||||
|
||||
with start_cloudflared(
|
||||
tmp_path,
|
||||
config,
|
||||
cfd_pre_args=["tunnel", "--ha-connections", "1"],
|
||||
cfd_args=["run"],
|
||||
new_process=True,
|
||||
capture_output=True,
|
||||
) as cfd:
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
||||
# Poll the log file for the sentinel before signalling the process.
|
||||
log_lines = _poll_log_file_for_precheck_complete(
|
||||
log_file, timeout=PRECHECK_POLL_TIMEOUT_SECS
|
||||
)
|
||||
# Signal shutdown.
|
||||
cfd.terminate()
|
||||
|
||||
# The process is now dead. All output was captured by the background
|
||||
# reader thread into cfd.stdout_lines (stderr is merged into stdout).
|
||||
stdout = b"".join(cfd.stdout_lines).decode(errors="replace")
|
||||
|
||||
LOGGER.debug(f"[happy-path] stdout:\n{stdout}")
|
||||
LOGGER.debug(f"[happy-path] log_lines:\n{log_lines}")
|
||||
|
||||
# Strip zerolog console prefixes so pattern matching works on raw messages.
|
||||
messages = "\n".join(_strip_log_prefix(l) for l in stdout.splitlines())
|
||||
|
||||
# ── table structure ──────────────────────────────────────────────────
|
||||
# zerolog writes to stderr which is merged into stdout by the harness.
|
||||
# The table is wrapped in an ASCII box by cliutil.LogTable.
|
||||
assert BOX_TITLE in messages, \
|
||||
f"Expected box title '{BOX_TITLE}' in output;\ngot:\n{stdout}"
|
||||
assert COL_HEADER in messages, \
|
||||
f"Expected column header row in output;\ngot:\n{stdout}"
|
||||
assert BOX_BORDER_RE.search(messages), \
|
||||
f"Expected box border line (+---+) in output;\ngot:\n{stdout}"
|
||||
|
||||
# ── row content ──────────────────────────────────────────────────────
|
||||
rows = _parse_table(stdout)
|
||||
assert len(rows) == 7, \
|
||||
f"Expected 7 rows (2 DNS + 2 QUIC + 2 HTTP/2 + 1 API); got {len(rows)}: {rows}"
|
||||
|
||||
dns_rows = _rows_for(rows, COMP_DNS)
|
||||
assert len(dns_rows) == 2, f"Expected 2 DNS rows; got {dns_rows}"
|
||||
assert dns_rows[0].target == TARGET_REGION1
|
||||
assert dns_rows[1].target == TARGET_REGION2
|
||||
for r in dns_rows:
|
||||
assert r.status == PASS, f"DNS row not PASS: {r}"
|
||||
assert r.details == DETAILS_DNS_RESOLVED, f"DNS row details wrong: {r}"
|
||||
|
||||
quic_rows = _rows_for(rows, COMP_QUIC)
|
||||
assert len(quic_rows) == 2, f"Expected 2 QUIC rows; got {quic_rows}"
|
||||
assert quic_rows[0].target == TARGET_REGION1, f"QUIC row[0] target wrong: {quic_rows[0]}"
|
||||
assert quic_rows[1].target == TARGET_REGION2, f"QUIC row[1] target wrong: {quic_rows[1]}"
|
||||
for r in quic_rows:
|
||||
assert r.status == PASS, f"QUIC row not PASS: {r}"
|
||||
assert r.details == DETAILS_QUIC_OK, f"QUIC row details wrong: {r}"
|
||||
|
||||
h2_rows = _rows_for(rows, COMP_H2)
|
||||
assert len(h2_rows) == 2, f"Expected 2 HTTP/2 rows; got {h2_rows}"
|
||||
assert h2_rows[0].target == TARGET_REGION1, f"HTTP/2 row[0] target wrong: {h2_rows[0]}"
|
||||
assert h2_rows[1].target == TARGET_REGION2, f"HTTP/2 row[1] target wrong: {h2_rows[1]}"
|
||||
for r in h2_rows:
|
||||
assert r.status == PASS, f"HTTP/2 row not PASS: {r}"
|
||||
assert r.details == DETAILS_HTTP2_OK, f"HTTP/2 row details wrong: {r}"
|
||||
|
||||
api_rows = _rows_for(rows, COMP_API)
|
||||
assert len(api_rows) == 1, f"Expected 1 API row; got {api_rows}"
|
||||
assert api_rows[0].target == TARGET_API, f"API row target wrong: {api_rows[0]}"
|
||||
assert api_rows[0].status == PASS, f"API row not PASS: {api_rows[0]}"
|
||||
assert api_rows[0].details == DETAILS_API_OK, f"API row details wrong: {api_rows[0]}"
|
||||
|
||||
# ── no action lines ──────────────────────────────────────────────────
|
||||
assert PREFIX_ERROR not in messages, f"Unexpected ERROR action:\n{stdout}"
|
||||
assert PREFIX_WARNING not in messages, f"Unexpected WARNING action:\n{stdout}"
|
||||
|
||||
# ── summary line ─────────────────────────────────────────────────────
|
||||
assert SUMMARY_HEALTHY in messages, \
|
||||
f"Expected healthy summary;\ngot:\n{stdout}"
|
||||
|
||||
# ── structured log ───────────────────────────────────────────────────
|
||||
assert len(log_lines) > 0, \
|
||||
"Expected at least one structured precheck log line in log file"
|
||||
for line in log_lines:
|
||||
if line.get("message") == LOG_MSG_PRECHECK:
|
||||
assert line.get("status") == STATUS_PASS_LOG, \
|
||||
f"Expected status=pass in precheck log line: {line}"
|
||||
_assert_precheck_summary_log(log_lines, hard_fail=False, suggested_protocol="quic")
|
||||
|
||||
|
||||
class TestPrechecksHardFail:
|
||||
"""
|
||||
When --edge points at an unreachable IP, StaticEdgeDNSResolver resolves
|
||||
the literal address directly (DNS row = PASS), but both transport probes
|
||||
time out -> hard fail. We assert:
|
||||
- the full table structure
|
||||
- DNS row: PASS (the literal IP was resolved)
|
||||
- QUIC row: FAIL with correct details + ERROR action
|
||||
- HTTP/2 row: FAIL with correct details + ERROR action
|
||||
- API row: PASS (api.cloudflare.com:443 is independently reachable)
|
||||
- the exact critical summary line
|
||||
- the structured log summary (hard_fail=true)
|
||||
|
||||
This test does NOT call wait_tunnel_ready because the tunnel will not
|
||||
connect to the unreachable address.
|
||||
"""
|
||||
|
||||
def test_prechecks_hard_fail_when_edge_unreachable(self, tmp_path, component_tests_config):
|
||||
log_file = str(tmp_path / "cloudflared.log")
|
||||
config = component_tests_config({"logfile": log_file})
|
||||
|
||||
with start_cloudflared(
|
||||
tmp_path,
|
||||
config,
|
||||
cfd_pre_args=[
|
||||
"tunnel",
|
||||
"--ha-connections", "1",
|
||||
"--edge", UNREACHABLE_EDGE,
|
||||
],
|
||||
cfd_args=["run"],
|
||||
new_process=True,
|
||||
capture_output=True,
|
||||
) as cfd:
|
||||
log_lines = _poll_log_file_for_precheck_complete(
|
||||
log_file, timeout=PRECHECK_POLL_TIMEOUT_SECS
|
||||
)
|
||||
cfd.terminate()
|
||||
|
||||
stdout = b"".join(cfd.stdout_lines).decode(errors="replace")
|
||||
|
||||
LOGGER.debug(f"[hard-fail] stdout:\n{stdout}")
|
||||
LOGGER.debug(f"[hard-fail] log_lines:\n{log_lines}")
|
||||
|
||||
# Strip zerolog console prefixes so pattern matching works on raw messages.
|
||||
messages = "\n".join(_strip_log_prefix(l) for l in stdout.splitlines())
|
||||
|
||||
# ── table structure ──────────────────────────────────────────────────
|
||||
# zerolog writes to stderr which is merged into stdout by the harness.
|
||||
# The table is wrapped in an ASCII box by cliutil.LogTable.
|
||||
assert BOX_TITLE in messages, \
|
||||
f"Expected box title '{BOX_TITLE}' in output;\ngot:\n{stdout}"
|
||||
assert COL_HEADER in messages, \
|
||||
f"Expected column header row in output;\ngot:\n{stdout}"
|
||||
assert BOX_BORDER_RE.search(messages), \
|
||||
f"Expected box border line (+---+) in output;\ngot:\n{stdout}"
|
||||
|
||||
# ── row content ──────────────────────────────────────────────────────
|
||||
rows = _parse_table(stdout)
|
||||
assert len(rows) == 4, \
|
||||
f"Expected 4 rows (1 DNS + 1 QUIC + 1 HTTP/2 + 1 API); got {len(rows)}: {rows}"
|
||||
|
||||
dns_rows = _rows_for(rows, COMP_DNS)
|
||||
assert len(dns_rows) == 1, f"Expected 1 DNS row; got {dns_rows}"
|
||||
assert dns_rows[0].target == UNREACHABLE_EDGE
|
||||
assert dns_rows[0].status == PASS, f"DNS row not PASS: {dns_rows[0]}"
|
||||
assert dns_rows[0].details == DETAILS_DNS_RESOLVED, f"DNS row details wrong: {dns_rows[0]}"
|
||||
|
||||
quic_rows = _rows_for(rows, COMP_QUIC)
|
||||
assert len(quic_rows) == 1, f"Expected 1 QUIC row; got {quic_rows}"
|
||||
assert quic_rows[0].target == UNREACHABLE_EDGE, f"QUIC row target wrong: {quic_rows[0]}"
|
||||
assert quic_rows[0].status == FAIL, f"QUIC row not FAIL: {quic_rows[0]}"
|
||||
assert quic_rows[0].details == DETAILS_QUIC_FAIL, f"QUIC row details wrong: {quic_rows[0]}"
|
||||
|
||||
h2_rows = _rows_for(rows, COMP_H2)
|
||||
assert len(h2_rows) == 1, f"Expected 1 HTTP/2 row; got {h2_rows}"
|
||||
assert h2_rows[0].target == UNREACHABLE_EDGE, f"HTTP/2 row target wrong: {h2_rows[0]}"
|
||||
assert h2_rows[0].status == FAIL, f"HTTP/2 row not FAIL: {h2_rows[0]}"
|
||||
assert h2_rows[0].details == DETAILS_HTTP2_FAIL, f"HTTP/2 row details wrong: {h2_rows[0]}"
|
||||
|
||||
api_rows = _rows_for(rows, COMP_API)
|
||||
assert len(api_rows) == 1, f"Expected 1 API row; got {api_rows}"
|
||||
assert api_rows[0].target == TARGET_API, f"API row target wrong: {api_rows[0]}"
|
||||
assert api_rows[0].status == PASS, f"API row not PASS: {api_rows[0]}"
|
||||
assert api_rows[0].details == DETAILS_API_OK, f"API row details wrong: {api_rows[0]}"
|
||||
|
||||
assert f"{PREFIX_ERROR}{ACTION_QUIC_BLOCKED}" in messages, \
|
||||
f"Expected QUIC ERROR action;\ngot:\n{stdout}"
|
||||
assert f"{PREFIX_ERROR}{ACTION_HTTP2_BLOCKED}" in messages, \
|
||||
f"Expected HTTP/2 ERROR action;\ngot:\n{stdout}"
|
||||
|
||||
assert SUMMARY_CRITICAL in messages, \
|
||||
f"Expected critical summary;\ngot:\n{stdout}"
|
||||
|
||||
_assert_precheck_summary_log(log_lines, hard_fail=True, suggested_protocol=None)
|
||||
|
||||
|
||||
class TestPreChecksDiag:
|
||||
"""
|
||||
Verify that `cloudflared tunnel diag` includes prechecks.json in the
|
||||
diagnostic zip archive produced against a live tunnel instance.
|
||||
|
||||
The precheck job in diagnostic.go is gated on noDiagNetwork; we do NOT
|
||||
pass --no-diag-network so prechecks.json must be present. We skip the
|
||||
heavier collectors (logs, metrics, system, runtime) to keep the test fast.
|
||||
|
||||
The diag subcommand writes the zip to its current working directory. We
|
||||
run it with cwd=tmp_path so the archive lands there and is cleaned up
|
||||
automatically by pytest. We resolve config.cloudflared_binary to an
|
||||
absolute path before changing cwd, because the binary path may be relative
|
||||
to the original working directory.
|
||||
"""
|
||||
|
||||
def test_diag_contains_prechecks_json(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config()
|
||||
binary = os.path.abspath(config.cloudflared_binary)
|
||||
|
||||
with start_cloudflared(
|
||||
tmp_path,
|
||||
config,
|
||||
cfd_pre_args=["tunnel", "--ha-connections", "1"],
|
||||
cfd_args=["run"],
|
||||
new_process=True,
|
||||
capture_output=True,
|
||||
) as cfd:
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
||||
|
||||
# Run the diag subcommand as a one-shot process against the
|
||||
# already-running instance. We skip log/metrics/system/runtime
|
||||
# collectors; the network collector (which runs prechecks) is left
|
||||
# enabled.
|
||||
diag_result = subprocess.run(
|
||||
[
|
||||
binary,
|
||||
"tunnel",
|
||||
"diag",
|
||||
"--metrics", f"localhost:{METRICS_PORT}",
|
||||
"--no-diag-logs",
|
||||
"--no-diag-metrics",
|
||||
"--no-diag-system",
|
||||
"--no-diag-runtime",
|
||||
],
|
||||
cwd=str(tmp_path),
|
||||
capture_output=True,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
cfd.terminate()
|
||||
|
||||
diag_stdout = diag_result.stdout.decode(errors="replace")
|
||||
diag_stderr = diag_result.stderr.decode(errors="replace")
|
||||
LOGGER.debug(f"[diag] stdout:\n{diag_stdout}")
|
||||
LOGGER.debug(f"[diag] stderr:\n{diag_stderr}")
|
||||
|
||||
assert diag_result.returncode == 0, (
|
||||
f"cloudflared tunnel diag exited with code {diag_result.returncode}\n"
|
||||
f"stdout:\n{diag_stdout}\nstderr:\n{diag_stderr}"
|
||||
)
|
||||
|
||||
# Locate the zip file written to tmp_path by the diag command.
|
||||
zip_files = list(tmp_path.glob("cloudflared-diag-*.zip"))
|
||||
assert len(zip_files) == 1, \
|
||||
f"Expected exactly one cloudflared-diag-*.zip in {tmp_path}; found {zip_files}"
|
||||
|
||||
zip_path = zip_files[0]
|
||||
with zipfilemod.ZipFile(zip_path) as zf:
|
||||
names = zf.namelist()
|
||||
LOGGER.debug(f"[diag] zip contents: {names}")
|
||||
|
||||
assert "prechecks.json" in names, \
|
||||
f"Expected prechecks.json in diag zip; got: {names}"
|
||||
|
||||
# Must be valid JSON containing at least the RunID field that
|
||||
# prechecks.Run() always sets.
|
||||
with zf.open("prechecks.json") as fh:
|
||||
data = json.load(fh)
|
||||
|
||||
assert "RunID" in data, \
|
||||
f"Expected RunID key in prechecks.json; got keys: {list(data.keys())}"
|
||||
+62
-8
@@ -2,6 +2,7 @@ import logging
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
from time import sleep
|
||||
import sys
|
||||
@@ -12,7 +13,65 @@ import requests
|
||||
import yaml
|
||||
from retrying import retry
|
||||
|
||||
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
|
||||
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS, GRACEFUL_SHUTDOWN_TIMEOUT, READER_THREAD_JOIN_TIMEOUT
|
||||
|
||||
class CloudflaredProcess:
|
||||
"""
|
||||
Wrapper around a Popen process that continuously drains stdout and stderr
|
||||
in background threads to prevent OS pipe buffers from filling up and
|
||||
blocking the child process. Captured output is logged when the process
|
||||
is cleaned up.
|
||||
"""
|
||||
|
||||
def __init__(self, cmd, allow_input, capture_output):
|
||||
output = subprocess.PIPE if capture_output else subprocess.DEVNULL
|
||||
stdin = subprocess.PIPE if allow_input else None
|
||||
self.process = subprocess.Popen(cmd, stdin=stdin, stdout=output, stderr=subprocess.STDOUT)
|
||||
|
||||
self._capture_output = capture_output
|
||||
self._stdout_lines = []
|
||||
self._threads = []
|
||||
if capture_output:
|
||||
self._threads.append(self._start_reader(self.process.stdout, self._stdout_lines))
|
||||
|
||||
@staticmethod
|
||||
def _start_reader(pipe, sink):
|
||||
def _drain():
|
||||
for line in pipe:
|
||||
sink.append(line)
|
||||
pipe.close()
|
||||
t = threading.Thread(target=_drain, daemon=True)
|
||||
t.start()
|
||||
return t
|
||||
|
||||
def terminate(self):
|
||||
"""Terminate the process if it is still running."""
|
||||
if self.process.poll() is None:
|
||||
self.process.terminate()
|
||||
|
||||
def cleanup(self):
|
||||
"""Terminate, wait for exit, join reader threads, and log output."""
|
||||
self.terminate()
|
||||
try:
|
||||
self.process.wait(timeout=GRACEFUL_SHUTDOWN_TIMEOUT)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
self.process.wait()
|
||||
for t in self._threads:
|
||||
t.join(timeout=READER_THREAD_JOIN_TIMEOUT)
|
||||
if self._capture_output:
|
||||
stdout = b"".join(self._stdout_lines).decode("utf-8", errors="replace")
|
||||
if stdout:
|
||||
LOGGER.info(f"cloudflared stdout:\n{stdout}")
|
||||
|
||||
@property
|
||||
def stdout_lines(self):
|
||||
return self._stdout_lines
|
||||
|
||||
# Proxy common Popen attributes so callers can still use the wrapper
|
||||
# as if it were a Popen (e.g. send_signal, stdin, pid, returncode).
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.process, name)
|
||||
|
||||
def configure_logger():
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -75,20 +134,15 @@ def cloudflared_cmd(config, config_path, cfd_args, cfd_pre_args, root):
|
||||
LOGGER.info(f"Run cmd {cmd} with config {config}")
|
||||
return cmd
|
||||
|
||||
|
||||
@contextmanager
|
||||
def run_cloudflared_background(cmd, allow_input, capture_output):
|
||||
output = subprocess.PIPE if capture_output else subprocess.DEVNULL
|
||||
stdin = subprocess.PIPE if allow_input else None
|
||||
cfd = None
|
||||
try:
|
||||
cfd = subprocess.Popen(cmd, stdin=stdin, stdout=output, stderr=output)
|
||||
cfd = CloudflaredProcess(cmd, allow_input, capture_output)
|
||||
yield cfd
|
||||
finally:
|
||||
if cfd:
|
||||
cfd.terminate()
|
||||
if capture_output:
|
||||
LOGGER.info(f"cloudflared log: {cfd.stderr.read()}")
|
||||
cfd.cleanup()
|
||||
|
||||
|
||||
def get_quicktunnel_url():
|
||||
|
||||
@@ -84,7 +84,7 @@ type TunnelToken struct {
|
||||
}
|
||||
|
||||
func (t TunnelToken) Credentials() Credentials {
|
||||
// nolint: gosimple
|
||||
// nolint: staticcheck
|
||||
return Credentials{
|
||||
AccountTag: t.AccountTag,
|
||||
TunnelSecret: t.TunnelSecret,
|
||||
@@ -122,6 +122,7 @@ const (
|
||||
|
||||
// ShouldFlush returns whether this kind of connection should actively flush data
|
||||
func (t Type) shouldFlush() bool {
|
||||
// nolint: exhaustive
|
||||
switch t {
|
||||
case TypeWebsocket, TypeTCP, TypeControlStream:
|
||||
return true
|
||||
@@ -131,6 +132,7 @@ func (t Type) shouldFlush() bool {
|
||||
}
|
||||
|
||||
func (t Type) String() string {
|
||||
// nolint: exhaustive
|
||||
switch t {
|
||||
case TypeWebsocket:
|
||||
return "websocket"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package dialopts
|
||||
|
||||
// DialOpts holds the configuration for dialing a QUIC connection.
|
||||
type DialOpts struct {
|
||||
// SkipPortReuse skips UDP port reuse. This is useful for probe connections
|
||||
// that should use a random ephemeral port to avoid interfering with the
|
||||
// main connection flow.
|
||||
SkipPortReuse bool
|
||||
}
|
||||
@@ -224,18 +224,10 @@ func NewProtocolSelector(
|
||||
protocolFlag string,
|
||||
accountTag string,
|
||||
tunnelTokenProvided bool,
|
||||
needPQ bool,
|
||||
protocolFetcher edgediscovery.PercentageFetcher,
|
||||
resolveTTL time.Duration,
|
||||
log *zerolog.Logger,
|
||||
) (ProtocolSelector, error) {
|
||||
// With --post-quantum, we force quic
|
||||
if needPQ {
|
||||
return &staticProtocolSelector{
|
||||
current: QUIC,
|
||||
}, nil
|
||||
}
|
||||
|
||||
threshold := switchThreshold(accountTag)
|
||||
fetchedProtocol, err := getProtocol(ProtocolList, protocolFetcher, threshold)
|
||||
log.Debug().Msgf("Fetched protocol: %s", fetchedProtocol)
|
||||
|
||||
@@ -31,7 +31,6 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
name string
|
||||
protocol string
|
||||
tunnelTokenProvided bool
|
||||
needPQ bool
|
||||
expectedProtocol Protocol
|
||||
hasFallback bool
|
||||
expectedFallback Protocol
|
||||
@@ -59,18 +58,6 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
hasFallback: true,
|
||||
expectedFallback: HTTP2,
|
||||
},
|
||||
{
|
||||
name: "named tunnel (post quantum)",
|
||||
protocol: AutoSelectFlag,
|
||||
needPQ: true,
|
||||
expectedProtocol: QUIC,
|
||||
},
|
||||
{
|
||||
name: "named tunnel (post quantum) w/http2",
|
||||
protocol: "http2",
|
||||
needPQ: true,
|
||||
expectedProtocol: QUIC,
|
||||
},
|
||||
}
|
||||
|
||||
fetcher := dynamicMockFetcher{
|
||||
@@ -79,7 +66,7 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
selector, err := NewProtocolSelector(test.protocol, testAccountTag, test.tunnelTokenProvided, test.needPQ, fetcher.fetch(), ResolveTTL, &log)
|
||||
selector, err := NewProtocolSelector(test.protocol, testAccountTag, test.tunnelTokenProvided, fetcher.fetch(), ResolveTTL, &log)
|
||||
if test.wantErr {
|
||||
assert.Error(t, err, "test %s failed", test.name)
|
||||
} else {
|
||||
@@ -97,7 +84,7 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
|
||||
func TestAutoProtocolSelectorRefresh(t *testing.T) {
|
||||
fetcher := dynamicMockFetcher{}
|
||||
selector, err := NewProtocolSelector(AutoSelectFlag, testAccountTag, false, false, fetcher.fetch(), testNoTTL, &log)
|
||||
selector, err := NewProtocolSelector(AutoSelectFlag, testAccountTag, false, fetcher.fetch(), testNoTTL, &log)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, QUIC, selector.Current())
|
||||
|
||||
@@ -127,7 +114,7 @@ func TestAutoProtocolSelectorRefresh(t *testing.T) {
|
||||
func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
|
||||
fetcher := dynamicMockFetcher{}
|
||||
// Since the user chooses http2 on purpose, we always stick to it.
|
||||
selector, err := NewProtocolSelector(HTTP2.String(), testAccountTag, false, false, fetcher.fetch(), testNoTTL, &log)
|
||||
selector, err := NewProtocolSelector(HTTP2.String(), testAccountTag, false, fetcher.fetch(), testNoTTL, &log)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
@@ -156,7 +143,7 @@ func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
|
||||
|
||||
func TestAutoProtocolSelectorNoRefreshWithToken(t *testing.T) {
|
||||
fetcher := dynamicMockFetcher{}
|
||||
selector, err := NewProtocolSelector(AutoSelectFlag, testAccountTag, true, false, fetcher.fetch(), testNoTTL, &log)
|
||||
selector, err := NewProtocolSelector(AutoSelectFlag, testAccountTag, true, fetcher.fetch(), testNoTTL, &log)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, QUIC, selector.Current())
|
||||
|
||||
|
||||
+18
-9
@@ -11,6 +11,8 @@ import (
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection/dialopts"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -26,8 +28,9 @@ func DialQuic(
|
||||
localAddr net.IP,
|
||||
connIndex uint8,
|
||||
logger *zerolog.Logger,
|
||||
opts dialopts.DialOpts,
|
||||
) (quic.Connection, error) {
|
||||
udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, edgeAddr, logger)
|
||||
udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, edgeAddr, opts, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -35,7 +38,7 @@ func DialQuic(
|
||||
conn, err := quic.Dial(ctx, udpConn, net.UDPAddrFromAddrPort(edgeAddr), tlsConfig, quicConfig)
|
||||
if err != nil {
|
||||
// close the udp server socket in case of error connecting to the edge
|
||||
udpConn.Close()
|
||||
_ = udpConn.Close()
|
||||
return nil, &EdgeQuicDialError{Cause: err}
|
||||
}
|
||||
|
||||
@@ -47,10 +50,7 @@ func DialQuic(
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, edgeIP netip.AddrPort, logger *zerolog.Logger) (*net.UDPConn, error) {
|
||||
portMapMutex.Lock()
|
||||
defer portMapMutex.Unlock()
|
||||
|
||||
func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, edgeIP netip.AddrPort, opts dialopts.DialOpts, logger *zerolog.Logger) (*net.UDPConn, error) {
|
||||
listenNetwork := "udp"
|
||||
// https://github.com/quic-go/quic-go/issues/3793 DF bit cannot be set for dual stack listener ("udp") on macOS,
|
||||
// to set the DF bit properly, the network string needs to be specific to the IP family.
|
||||
@@ -62,15 +62,24 @@ func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, edgeIP netip.Add
|
||||
}
|
||||
}
|
||||
|
||||
// Probes skip port reuse entirely to avoid interfering with the main connection flow.
|
||||
// They use a random ephemeral port for each dial.
|
||||
if opts.SkipPortReuse {
|
||||
return net.ListenUDP(listenNetwork, &net.UDPAddr{IP: localIP, Port: 0})
|
||||
}
|
||||
|
||||
portMapMutex.Lock()
|
||||
defer portMapMutex.Unlock()
|
||||
|
||||
// if port was not set yet, it will be zero, so bind will randomly allocate one.
|
||||
if port, ok := portForConnIndex[connIndex]; ok {
|
||||
udpConn, err := net.ListenUDP(listenNetwork, &net.UDPAddr{IP: localIP, Port: port})
|
||||
// if there wasn't an error, or if port was 0 (independently of error or not, just return)
|
||||
if err == nil {
|
||||
return udpConn, nil
|
||||
} else {
|
||||
logger.Debug().Err(err).Msgf("Unable to reuse port %d for connIndex %d. Falling back to random allocation.", port, connIndex)
|
||||
}
|
||||
|
||||
logger.Debug().Err(err).Msgf("Unable to reuse port %d for connIndex %d. Falling back to random allocation.", port, connIndex)
|
||||
}
|
||||
|
||||
// if we reached here, then there was an error or port as not been allocated it.
|
||||
@@ -95,7 +104,7 @@ type wrapCloseableConnQuicConnection struct {
|
||||
|
||||
func (w *wrapCloseableConnQuicConnection) CloseWithError(errorCode quic.ApplicationErrorCode, reason string) error {
|
||||
err := w.Connection.CloseWithError(errorCode, reason)
|
||||
w.udpConn.Close()
|
||||
_ = w.udpConn.Close()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/nettest"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection/dialopts"
|
||||
|
||||
"github.com/cloudflare/cloudflared/client"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
cfdflow "github.com/cloudflare/cloudflared/flow"
|
||||
@@ -156,7 +158,7 @@ func TestQUICServer(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
|
||||
require.NoError(t, err)
|
||||
defer udpListener.Close()
|
||||
defer func() { _ = udpListener.Close() }()
|
||||
quicTransport := &quic.Transport{Conn: udpListener, ConnectionIDLength: 16}
|
||||
quicListener, err := quicTransport.Listen(testTLSServerConfig, testQUICConfig)
|
||||
require.NoError(t, err)
|
||||
@@ -523,7 +525,7 @@ func TestServeUDPSession(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
|
||||
require.NoError(t, err)
|
||||
defer udpListener.Close()
|
||||
defer func() { _ = udpListener.Close() }()
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
@@ -614,7 +616,7 @@ func TestTCPProxy_FlowRateLimited(t *testing.T) {
|
||||
|
||||
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
|
||||
require.NoError(t, err)
|
||||
defer udpListener.Close()
|
||||
defer func() { _ = udpListener.Close() }()
|
||||
|
||||
quicTransport := &quic.Transport{Conn: udpListener, ConnectionIDLength: 16}
|
||||
quicListener, err := quicTransport.Listen(testTLSServerConfig, testQUICConfig)
|
||||
@@ -658,7 +660,7 @@ func TestTCPProxy_FlowRateLimited(t *testing.T) {
|
||||
|
||||
func testCreateUDPConnReuseSourcePortForEdgeIP(t *testing.T, edgeIP netip.AddrPort) {
|
||||
logger := zerolog.Nop()
|
||||
conn, err := createUDPConnForConnIndex(0, nil, edgeIP, &logger)
|
||||
conn, err := createUDPConnForConnIndex(0, nil, edgeIP, dialopts.DialOpts{}, &logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
getPortFunc := func(conn *net.UDPConn) int {
|
||||
@@ -669,24 +671,114 @@ func testCreateUDPConnReuseSourcePortForEdgeIP(t *testing.T, edgeIP netip.AddrPo
|
||||
initialPort := getPortFunc(conn)
|
||||
|
||||
// close conn
|
||||
conn.Close()
|
||||
_ = conn.Close()
|
||||
|
||||
// should get the same port as before.
|
||||
conn, err = createUDPConnForConnIndex(0, nil, edgeIP, &logger)
|
||||
conn, err = createUDPConnForConnIndex(0, nil, edgeIP, dialopts.DialOpts{}, &logger)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, initialPort, getPortFunc(conn))
|
||||
|
||||
// new index, should get a different port
|
||||
conn1, err := createUDPConnForConnIndex(1, nil, edgeIP, &logger)
|
||||
conn1, err := createUDPConnForConnIndex(1, nil, edgeIP, dialopts.DialOpts{}, &logger)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, initialPort, getPortFunc(conn1))
|
||||
|
||||
// not closing the conn and trying to obtain a new conn for same index should give a different random port
|
||||
conn, err = createUDPConnForConnIndex(0, nil, edgeIP, &logger)
|
||||
conn, err = createUDPConnForConnIndex(0, nil, edgeIP, dialopts.DialOpts{}, &logger)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, initialPort, getPortFunc(conn))
|
||||
}
|
||||
|
||||
// TestSkipPortReuse tests that skipPortReuse uses a random ephemeral port for each dial.
|
||||
func TestSkipPortReuse(t *testing.T) {
|
||||
t.Parallel()
|
||||
logger := zerolog.Nop()
|
||||
edgeIP := netip.MustParseAddrPort("127.0.0.1:0")
|
||||
|
||||
// First dial with skipPortReuse should allocate a random port
|
||||
conn1, err := createUDPConnForConnIndex(0, nil, edgeIP, dialopts.DialOpts{SkipPortReuse: true}, &logger)
|
||||
require.NoError(t, err)
|
||||
port1 := conn1.LocalAddr().(*net.UDPAddr).Port
|
||||
|
||||
// Don't close conn1 yet - keep it open to prevent port reuse
|
||||
// Second dial with skipPortReuse should allocate a different random port
|
||||
conn2, err := createUDPConnForConnIndex(0, nil, edgeIP, dialopts.DialOpts{SkipPortReuse: true}, &logger)
|
||||
require.NoError(t, err)
|
||||
port2 := conn2.LocalAddr().(*net.UDPAddr).Port
|
||||
|
||||
// Now close both connections
|
||||
_ = conn1.Close()
|
||||
_ = conn2.Close()
|
||||
// With skipPortReuse, ports should be different (random allocation)
|
||||
require.NotEqual(t, port1, port2, "With skipPortReuse, each dial should use a different random port")
|
||||
}
|
||||
|
||||
// TestDialQuicWithSkipPortReuse tests that DialQuic works correctly with the WithSkipPortReuse option.
|
||||
func TestDialQuicWithSkipPortReuse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
// Start a mock QUIC server (similar to TestQUICServer)
|
||||
udpListener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = udpListener.Close() }()
|
||||
|
||||
serverAddr := netip.MustParseAddrPort(udpListener.LocalAddr().String())
|
||||
|
||||
quicTransport := &quic.Transport{Conn: udpListener, ConnectionIDLength: 16}
|
||||
quicListener, err := quicTransport.Listen(testTLSServerConfig, testQUICConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
serverDone := make(chan struct{})
|
||||
go func() {
|
||||
// Accept one connection
|
||||
session, err := quicListener.Accept(ctx)
|
||||
if err != nil {
|
||||
close(serverDone)
|
||||
return
|
||||
}
|
||||
// Keep session open until context is cancelled
|
||||
<-ctx.Done()
|
||||
_ = session.CloseWithError(0, "test done")
|
||||
close(serverDone)
|
||||
}()
|
||||
|
||||
// Test DialQuic with WithSkipPortReuse option
|
||||
tlsClientConfig := &tls.Config{
|
||||
// nolint: gosec
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"argotunnel"},
|
||||
}
|
||||
|
||||
log := zerolog.New(io.Discard)
|
||||
dialCtx, dialCancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
defer dialCancel()
|
||||
|
||||
// Dial with skipPortReuse option - should use a random ephemeral port
|
||||
conn, err := DialQuic(
|
||||
dialCtx,
|
||||
testQUICConfig,
|
||||
tlsClientConfig,
|
||||
serverAddr,
|
||||
nil, // connect on a random port
|
||||
0,
|
||||
&log,
|
||||
dialopts.DialOpts{SkipPortReuse: true},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, conn)
|
||||
|
||||
// Verify we can get connection state
|
||||
_ = conn.ConnectionState()
|
||||
|
||||
// Clean up
|
||||
_ = conn.CloseWithError(0, "test done")
|
||||
cancel()
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func serveSession(ctx context.Context, datagramConn *datagramV2Connection, edgeQUICSession quic.Connection, closeType closeReason, expectedReason string, t *testing.T) {
|
||||
payload := []byte(t.Name())
|
||||
sessionID := uuid.New()
|
||||
@@ -719,7 +811,7 @@ func serveSession(ctx context.Context, datagramConn *datagramV2Connection, edgeQ
|
||||
// Close connection to terminate session
|
||||
switch closeType {
|
||||
case closedByOrigin:
|
||||
originConn.Close()
|
||||
_ = originConn.Close()
|
||||
case closedByRemote:
|
||||
err = datagramConn.UnregisterUdpSession(ctx, sessionID, expectedReason)
|
||||
require.NoError(t, err)
|
||||
@@ -813,6 +905,7 @@ func testTunnelConnection(t *testing.T, serverAddr netip.AddrPort, index uint8)
|
||||
nil, // connect on a random port
|
||||
index,
|
||||
&log,
|
||||
dialopts.DialOpts{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/cloudflare/cloudflared/features"
|
||||
)
|
||||
|
||||
// errUnknownPostQuantumMode is returned by GetCurvePreferences when the
|
||||
// caller passes a features.PostQuantumMode value that is not one of the
|
||||
// documented constants. It is intentionally unexported: callers should treat
|
||||
// any non-nil error as a programming mistake rather than inspecting it.
|
||||
var errUnknownPostQuantumMode = errors.New("the provided post quantum mode is unknown")
|
||||
|
||||
// P256Kyber768Draft00 is a post-quantum KEM based on Kyber768.
|
||||
const P256Kyber768Draft00 = tls.CurveID(0xfe32) // ID 65074
|
||||
|
||||
// Canonical curve lists returned by GetCurvePreferences. They are kept
|
||||
// package-private so that callers cannot accidentally mutate the shared
|
||||
// slice; GetCurvePreferences always returns a clone.
|
||||
var (
|
||||
// postQuantumStrictCurves is used when the caller requires a
|
||||
// post-quantum handshake. Only PQ curves (X25519MLKEM768 and the
|
||||
// deprecated P256Kyber768Draft00 for backward compatibility) are
|
||||
// advertised; no classical-only curve is included.
|
||||
postQuantumStrictCurves = []tls.CurveID{tls.X25519MLKEM768, P256Kyber768Draft00}
|
||||
// postQuantumPreferCurves is used for the default "prefer" mode: the PQ
|
||||
// curve is advertised first and the classical CurveP256 is listed as a
|
||||
// fallback so peers without PQ support can still negotiate.
|
||||
postQuantumPreferCurves = []tls.CurveID{tls.X25519MLKEM768, P256Kyber768Draft00, tls.CurveP256}
|
||||
)
|
||||
|
||||
// getCurvePreferences returns the TLS curve preferences that should be
|
||||
// applied to edge-facing connections for the given post-quantum mode.
|
||||
//
|
||||
// The returned slice is the canonical, protocol-agnostic curve list and is
|
||||
// suitable for direct assignment to tls.Config.CurvePreferences. A fresh
|
||||
// slice is returned on every call, so callers may mutate it freely without
|
||||
// affecting other callers.
|
||||
//
|
||||
// An error is returned only when profile is not a recognised
|
||||
// features.PostQuantumMode value, which indicates a programming bug in the
|
||||
// caller.
|
||||
func getCurvePreferences(profile features.PostQuantumMode) ([]tls.CurveID, error) {
|
||||
switch profile {
|
||||
case features.PostQuantumPrefer:
|
||||
return slices.Clone(postQuantumPreferCurves), nil
|
||||
case features.PostQuantumStrict:
|
||||
return slices.Clone(postQuantumStrictCurves), nil
|
||||
}
|
||||
|
||||
return nil, errUnknownPostQuantumMode
|
||||
}
|
||||
|
||||
// TLSConfigWithCurvePreferences clones the provided tls.Config and applies
|
||||
// curve preferences based on the given post-quantum mode.
|
||||
//
|
||||
// The original tls.Config is never modified; a clone is returned so that
|
||||
// callers can safely use the same base configuration across multiple
|
||||
// goroutines without racing on CurvePreferences.
|
||||
//
|
||||
// Returns an error only when pqMode is not a recognised
|
||||
// features.PostQuantumMode value.
|
||||
func TLSConfigWithCurvePreferences(tlsConfig *tls.Config, pqMode features.PostQuantumMode) (*tls.Config, error) {
|
||||
// Clone the TLS config before applying per-connection curve
|
||||
// preferences. The TlsConfig may be shared across goroutines;
|
||||
// mutating it directly would race with concurrent connection attempts.
|
||||
config := tlsConfig.Clone()
|
||||
curvePref, err := getCurvePreferences(pqMode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get curve preferences: %w", err)
|
||||
}
|
||||
|
||||
config.CurvePreferences = curvePref
|
||||
return config, nil
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"runtime"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cloudflare/cloudflared/features"
|
||||
)
|
||||
|
||||
// TestCurvePreferences verifies that GetCurvePreferences returns the
|
||||
// documented curve list for each supported PostQuantumMode. The expected
|
||||
// values correspond to the contract described in the package documentation
|
||||
// and must be identical under FIPS and non-FIPS builds (see TUN-10413).
|
||||
func TestCurvePreferences(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
expectedCurves []tls.CurveID
|
||||
pqMode features.PostQuantumMode
|
||||
}{
|
||||
{
|
||||
name: "Prefer PQ",
|
||||
pqMode: features.PostQuantumPrefer,
|
||||
expectedCurves: []tls.CurveID{tls.X25519MLKEM768, P256Kyber768Draft00, tls.CurveP256},
|
||||
},
|
||||
{
|
||||
name: "Strict PQ",
|
||||
pqMode: features.PostQuantumStrict,
|
||||
expectedCurves: []tls.CurveID{tls.X25519MLKEM768, P256Kyber768Draft00},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tcase := range tests {
|
||||
t.Run(tcase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
curves, err := getCurvePreferences(tcase.pqMode)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tcase.expectedCurves, curves)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCurvePreferenceUnknownMode asserts that passing a PostQuantumMode
|
||||
// value outside of the documented constants produces an error instead of
|
||||
// silently returning a nil or default curve list. This protects callers
|
||||
// from accidentally negotiating with an unintended curve set.
|
||||
func TestCurvePreferenceUnknownMode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := getCurvePreferences(features.PostQuantumMode(255))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// TestReturnedSliceIsIndependent ensures GetCurvePreferences returns a
|
||||
// fresh slice on every call, so that callers cannot corrupt the
|
||||
// package-level defaults by mutating the result.
|
||||
func TestReturnedSliceIsIndependent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
first, err := getCurvePreferences(features.PostQuantumPrefer)
|
||||
require.NoError(t, err)
|
||||
// Mutate the returned slice.
|
||||
first[0] = tls.CurveP521
|
||||
|
||||
second, err := getCurvePreferences(features.PostQuantumPrefer)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tls.X25519MLKEM768, second[0], "package defaults must not be affected by caller mutation")
|
||||
}
|
||||
|
||||
// runClientServerHandshake drives a TLS 1.3 handshake with the given curve
|
||||
// preferences set on the client and captures the SupportedCurves list
|
||||
// advertised by the client in its ClientHello. The helper is used by
|
||||
// TestSupportedCurvesNegotiation to exercise the curves end-to-end against
|
||||
// the standard library's TLS stack.
|
||||
func runClientServerHandshake(t *testing.T, curves []tls.CurveID) []tls.CurveID {
|
||||
var advertisedCurves []tls.CurveID
|
||||
ts := httptest.NewUnstartedServer(nil)
|
||||
ts.TLS = &tls.Config{ // nolint: gosec
|
||||
GetConfigForClient: func(chi *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
advertisedCurves = slices.Clone(chi.SupportedCurves)
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
ts.StartTLS()
|
||||
defer ts.Close()
|
||||
clientTLSConfig := ts.Client().Transport.(*http.Transport).TLSClientConfig
|
||||
clientTLSConfig.CurvePreferences = curves
|
||||
resp, err := ts.Client().Head(ts.URL)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return nil
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
return advertisedCurves
|
||||
}
|
||||
|
||||
// TestSupportedCurvesNegotiation verifies that the curves returned by
|
||||
// GetCurvePreferences survive a real TLS handshake unchanged, i.e. the
|
||||
// standard library advertises exactly the curves we expect. Currently only
|
||||
// PostQuantumPrefer is exercised because PostQuantumStrict would cause the
|
||||
// handshake to fail against httptest servers that do not support
|
||||
// X25519MLKEM768 server-side.
|
||||
func TestSupportedCurvesNegotiation(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, tcase := range []features.PostQuantumMode{features.PostQuantumPrefer} {
|
||||
curves, err := getCurvePreferences(tcase)
|
||||
require.NoError(t, err)
|
||||
advertisedCurves := runClientServerHandshake(t, curves)
|
||||
require.True(t, slices.Contains(advertisedCurves, tls.CurveP256))
|
||||
require.True(t, slices.Contains(advertisedCurves, tls.X25519MLKEM768))
|
||||
expectedLength := 2
|
||||
if runtime.GOOS == "linux" {
|
||||
// P256Kyber768Draft00 only exists in linux
|
||||
require.True(t, slices.Contains(advertisedCurves, P256Kyber768Draft00))
|
||||
expectedLength = 3
|
||||
}
|
||||
require.Len(t, advertisedCurves, expectedLength)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Package crypto centralizes the cryptographic primitives and TLS
|
||||
// configuration used by cloudflared when establishing connections to the
|
||||
// Cloudflare edge.
|
||||
//
|
||||
// The primary responsibility of the package is to expose a single, canonical
|
||||
// source of TLS curve preferences so that every edge-facing transport (QUIC
|
||||
// and HTTP/2) negotiates the same key-exchange algorithms regardless of the
|
||||
// code path that sets up the connection.
|
||||
//
|
||||
// # Post-Quantum key exchange
|
||||
//
|
||||
// cloudflared supports the X25519MLKEM768 hybrid post-quantum key exchange.
|
||||
// Two operating modes are exposed via the features.PostQuantumMode flag:
|
||||
//
|
||||
// - PostQuantumPrefer: advertise X25519MLKEM768 and the deprecated
|
||||
// P256Kyber768Draft00 first, then fall back to the classical CurveP256
|
||||
// if the peer does not support either PQ curve. This is the default
|
||||
// used for every outbound edge connection.
|
||||
// - PostQuantumStrict: advertise only the PQ curves (X25519MLKEM768 and
|
||||
// P256Kyber768Draft00). Activated by the user via the --post-quantum
|
||||
// CLI flag. No classical fallback is offered, so a peer that does not
|
||||
// support any PQ curve will fail the handshake.
|
||||
//
|
||||
// The resulting curve lists are identical under FIPS and non-FIPS builds,
|
||||
// which is why GetCurvePreferences does not take a FIPS toggle. If that
|
||||
// property ever changes (for example, if a curve stops being FIPS-approved),
|
||||
// the divergence should be expressed inside this package so callers remain
|
||||
// unchanged.
|
||||
//
|
||||
// # Thread-safety
|
||||
//
|
||||
// GetCurvePreferences returns a fresh slice on every call. Callers are free
|
||||
// to mutate the returned slice without affecting the package-level defaults
|
||||
// or other callers.
|
||||
package crypto
|
||||
@@ -34,4 +34,5 @@ const (
|
||||
cliConfigurationBaseName = "cli-configuration.json"
|
||||
configurationBaseName = "configuration.json"
|
||||
taskResultBaseName = "task-result.json"
|
||||
prechecksBaseName = "prechecks.json"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -126,13 +131,14 @@ func collectLogs(
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error opening log file while collecting logs: %w", err)
|
||||
}
|
||||
defer logHandle.Close()
|
||||
defer func() { _ = logHandle.Close() }()
|
||||
|
||||
// nolint: gosec
|
||||
outputLogHandle, err := os.Create(filepath.Join(os.TempDir(), logFilename))
|
||||
if err != nil {
|
||||
return "", ErrCreatingTemporaryFile
|
||||
}
|
||||
defer outputLogHandle.Close()
|
||||
defer func() { _ = outputLogHandle.Close() }()
|
||||
|
||||
_, err = io.Copy(outputLogHandle, logHandle)
|
||||
if err != nil {
|
||||
@@ -229,12 +235,13 @@ func networkInformationCollectors() (rawNetworkCollector, jsonNetworkCollector c
|
||||
}
|
||||
|
||||
func rawNetworkInformationWriter(resultMap map[string]networkCollectionResult) (string, error) {
|
||||
// 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
|
||||
}
|
||||
|
||||
defer networkDumpHandle.Close()
|
||||
defer func() { _ = networkDumpHandle.Close() }()
|
||||
|
||||
var exitErr error
|
||||
|
||||
@@ -260,12 +267,13 @@ func rawNetworkInformationWriter(resultMap map[string]networkCollectionResult) (
|
||||
}
|
||||
|
||||
func jsonNetworkInformationWriter(resultMap map[string]networkCollectionResult) (string, error) {
|
||||
// nolint: gosec
|
||||
networkDumpHandle, err := os.Create(filepath.Join(os.TempDir(), networkBaseName))
|
||||
if err != nil {
|
||||
return "", ErrCreatingTemporaryFile
|
||||
}
|
||||
|
||||
defer networkDumpHandle.Close()
|
||||
defer func() { _ = networkDumpHandle.Close() }()
|
||||
|
||||
encoder := newFormattedEncoder(networkDumpHandle)
|
||||
|
||||
@@ -290,11 +298,12 @@ func jsonNetworkInformationWriter(resultMap map[string]networkCollectionResult)
|
||||
|
||||
func collectFromEndpointAdapter(collect collectToWriterFunc, fileName string) collectFunc {
|
||||
return func(ctx context.Context) (string, error) {
|
||||
// nolint: gosec
|
||||
dumpHandle, err := os.Create(filepath.Join(os.TempDir(), fileName))
|
||||
if err != nil {
|
||||
return "", ErrCreatingTemporaryFile
|
||||
}
|
||||
defer dumpHandle.Close()
|
||||
defer func() { _ = dumpHandle.Close() }()
|
||||
|
||||
err = collect(ctx, dumpHandle)
|
||||
if err != nil {
|
||||
@@ -349,12 +358,12 @@ func resolveInstanceBaseURL(
|
||||
if !strings.HasPrefix(metricsServerAddress, "http://") {
|
||||
metricsServerAddress = "http://" + metricsServerAddress
|
||||
}
|
||||
url, err := url.Parse(metricsServerAddress)
|
||||
baseUrl, err := url.Parse(metricsServerAddress)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("provided address is not valid: %w", err)
|
||||
}
|
||||
|
||||
return url, nil, nil, nil
|
||||
return baseUrl, nil, nil, nil
|
||||
}
|
||||
|
||||
tunnelState, foundTunnelStates, err := FindMetricsServer(log, client, addresses)
|
||||
@@ -368,6 +377,7 @@ func resolveInstanceBaseURL(
|
||||
func createJobs(
|
||||
client *httpClient,
|
||||
tunnel *TunnelState,
|
||||
region string,
|
||||
diagContainer string,
|
||||
diagPod string,
|
||||
noDiagSystem bool,
|
||||
@@ -430,17 +440,62 @@ 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))
|
||||
if err != nil {
|
||||
return "", ErrCreatingTemporaryFile
|
||||
}
|
||||
defer dumpHandle.Close()
|
||||
defer func() { _ = dumpHandle.Close() }()
|
||||
|
||||
encoder := newFormattedEncoder(dumpHandle)
|
||||
|
||||
@@ -522,6 +577,7 @@ func RunDiagnostic(
|
||||
jobs := createJobs(
|
||||
client,
|
||||
tunnel,
|
||||
options.Region,
|
||||
options.ContainerID,
|
||||
options.PodID,
|
||||
options.Toggles.NoDiagSystem,
|
||||
@@ -545,7 +601,7 @@ func RunDiagnostic(
|
||||
|
||||
defer func() {
|
||||
if !errors.Is(v.Err, ErrCreatingTemporaryFile) {
|
||||
os.Remove(v.path)
|
||||
_ = os.Remove(v.path)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -20,18 +20,18 @@ func NewDockerLogCollector(containerID string) *DockerLogCollector {
|
||||
}
|
||||
|
||||
func (collector *DockerLogCollector) Collect(ctx context.Context) (*LogInformation, error) {
|
||||
tmp := os.TempDir()
|
||||
|
||||
outputHandle, err := os.Create(filepath.Join(tmp, logFilename))
|
||||
// nolint: gosec
|
||||
outputHandle, err := os.Create(filepath.Join(os.TempDir(), logFilename))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening output file: %w", err)
|
||||
}
|
||||
|
||||
defer outputHandle.Close()
|
||||
defer func() { _ = outputHandle.Close() }()
|
||||
|
||||
// Calculate 2 weeks ago
|
||||
since := time.Now().Add(twoWeeksOffset).Format(time.RFC3339)
|
||||
|
||||
// nolint: gosec
|
||||
command := exec.CommandContext(
|
||||
ctx,
|
||||
"docker",
|
||||
|
||||
@@ -13,7 +13,6 @@ const (
|
||||
linuxManagedLogsPath = "/var/log/cloudflared.err"
|
||||
darwinManagedLogsPath = "/Library/Logs/com.cloudflare.cloudflared.err.log"
|
||||
linuxServiceConfigurationPath = "/etc/systemd/system/cloudflared.service"
|
||||
linuxSystemdPath = "/run/systemd/system"
|
||||
)
|
||||
|
||||
type HostLogCollector struct {
|
||||
@@ -27,14 +26,13 @@ func NewHostLogCollector(client HTTPClient) *HostLogCollector {
|
||||
}
|
||||
|
||||
func extractLogsFromJournalCtl(ctx context.Context) (*LogInformation, error) {
|
||||
tmp := os.TempDir()
|
||||
|
||||
outputHandle, err := os.Create(filepath.Join(tmp, logFilename))
|
||||
// nolint: gosec
|
||||
outputHandle, err := os.Create(filepath.Join(os.TempDir(), logFilename))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening output file: %w", err)
|
||||
}
|
||||
|
||||
defer outputHandle.Close()
|
||||
defer func() { _ = outputHandle.Close() }()
|
||||
|
||||
command := exec.CommandContext(
|
||||
ctx,
|
||||
|
||||
@@ -22,18 +22,19 @@ func NewKubernetesLogCollector(containerID, pod string) *KubernetesLogCollector
|
||||
}
|
||||
|
||||
func (collector *KubernetesLogCollector) Collect(ctx context.Context) (*LogInformation, error) {
|
||||
tmp := os.TempDir()
|
||||
outputHandle, err := os.Create(filepath.Join(tmp, logFilename))
|
||||
// nolint: gosec
|
||||
outputHandle, err := os.Create(filepath.Join(os.TempDir(), logFilename))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening output file: %w", err)
|
||||
}
|
||||
|
||||
defer outputHandle.Close()
|
||||
defer func() { _ = outputHandle.Close() }()
|
||||
|
||||
var command *exec.Cmd
|
||||
// Calculate 2 weeks ago
|
||||
since := time.Now().Add(twoWeeksOffset).Format(time.RFC3339)
|
||||
if collector.containerID != "" {
|
||||
// nolint: gosec
|
||||
command = exec.CommandContext(
|
||||
ctx,
|
||||
"kubectl",
|
||||
@@ -47,6 +48,7 @@ func (collector *KubernetesLogCollector) Collect(ctx context.Context) (*LogInfor
|
||||
collector.containerID,
|
||||
)
|
||||
} else {
|
||||
// nolint: gosec
|
||||
command = exec.CommandContext(
|
||||
ctx,
|
||||
"kubectl",
|
||||
|
||||
@@ -67,6 +67,8 @@ func PipeCommandOutputToFile(command *exec.Cmd, outputHandle *os.File) (*LogInfo
|
||||
}
|
||||
|
||||
func CopyFilesFromDirectory(path string) (string, error) {
|
||||
const defaultLogFilename = "cloudflared.log"
|
||||
|
||||
// rolling logs have as suffix the current date thus
|
||||
// when iterating the path files they are already in
|
||||
// chronological order
|
||||
@@ -75,30 +77,32 @@ func CopyFilesFromDirectory(path string) (string, error) {
|
||||
return "", fmt.Errorf("error reading directory %s: %w", path, err)
|
||||
}
|
||||
|
||||
// nolint: gosec
|
||||
outputHandle, err := os.Create(filepath.Join(os.TempDir(), logFilename))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("creating file %s: %w", outputHandle.Name(), err)
|
||||
return "", fmt.Errorf("creating temporary log file %s: %w", logFilename, err)
|
||||
}
|
||||
defer outputHandle.Close()
|
||||
defer func() { _ = outputHandle.Close() }()
|
||||
|
||||
for _, file := range files {
|
||||
// nolint: gosec
|
||||
logHandle, err := os.Open(filepath.Join(path, file.Name()))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error opening file %s:%w", file.Name(), err)
|
||||
return "", fmt.Errorf("error opening file %s: %w", file.Name(), err)
|
||||
}
|
||||
defer logHandle.Close()
|
||||
|
||||
_, err = io.Copy(outputHandle, logHandle)
|
||||
_ = logHandle.Close()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error copying file %s:%w", logHandle.Name(), err)
|
||||
return "", fmt.Errorf("error copying file %s: %w", file.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
logHandle, err := os.Open(filepath.Join(path, "cloudflared.log"))
|
||||
// nolint: gosec
|
||||
logHandle, err := os.Open(filepath.Join(path, defaultLogFilename))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error opening file %s:%w", logHandle.Name(), err)
|
||||
return "", fmt.Errorf("error opening file %s:%w", defaultLogFilename, err)
|
||||
}
|
||||
defer logHandle.Close()
|
||||
defer func() { _ = logHandle.Close() }()
|
||||
|
||||
_, err = io.Copy(outputHandle, logHandle)
|
||||
if err != nil {
|
||||
|
||||
@@ -109,7 +109,7 @@ var friendlyDNSErrorLines = []string{
|
||||
}
|
||||
|
||||
// EdgeDiscovery implements HA service discovery lookup.
|
||||
func edgeDiscovery(log *zerolog.Logger, srvService string) ([][]*EdgeAddr, error) {
|
||||
func EdgeDiscovery(log *zerolog.Logger, srvService string) ([][]*EdgeAddr, error) {
|
||||
logger := log.With().Int(management.EventTypeKey, int(management.Cloudflared)).Logger()
|
||||
logger.Debug().
|
||||
Int(management.EventTypeKey, int(management.Cloudflared)).
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func (ea *EdgeAddr) String() string {
|
||||
@@ -25,8 +26,8 @@ func TestEdgeDiscovery(t *testing.T) {
|
||||
}
|
||||
|
||||
l := zerolog.Nop()
|
||||
addrLists, err := edgeDiscovery(&l, "")
|
||||
assert.NoError(t, err)
|
||||
addrLists, err := EdgeDiscovery(&l, "")
|
||||
require.NoError(t, err)
|
||||
actualAddrSet := map[string]bool{}
|
||||
for _, addrs := range addrLists {
|
||||
for _, addr := range addrs {
|
||||
|
||||
@@ -20,7 +20,7 @@ type Regions struct {
|
||||
|
||||
// ResolveEdge resolves the Cloudflare edge, returning all regions discovered.
|
||||
func ResolveEdge(log *zerolog.Logger, region string, overrideIPVersion ConfigIPVersion) (*Regions, error) {
|
||||
edgeAddrs, err := edgeDiscovery(log, getRegionalServiceName(region))
|
||||
edgeAddrs, err := EdgeDiscovery(log, RegionalServiceName(region))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -91,6 +91,7 @@ func (rs *Regions) GetUnusedAddr(excluding *EdgeAddr, connID int) *EdgeAddr {
|
||||
// evenly across both regions.
|
||||
if rs.region1.AvailableAddrs() == rs.region2.AvailableAddrs() {
|
||||
regions := []Region{rs.region1, rs.region2}
|
||||
//nolint:gosec
|
||||
firstChoice := rand.Intn(2)
|
||||
return getAddrs(excluding, connID, ®ions[firstChoice], ®ions[1-firstChoice])
|
||||
}
|
||||
@@ -131,11 +132,13 @@ func (rs *Regions) GiveBack(addr *EdgeAddr, hasConnectivityError bool) bool {
|
||||
return rs.region2.GiveBack(addr, hasConnectivityError)
|
||||
}
|
||||
|
||||
// Return regionalized service name if `region` isn't empty, otherwise return the global service name for origintunneld
|
||||
func getRegionalServiceName(region string) string {
|
||||
// RegionalServiceName returns the SRV service name for the given region.
|
||||
// When region is empty it returns the global service name ("v2-origintunneld").
|
||||
// Otherwise, it prepends the region, e.g. "us-v2-origintunneld".
|
||||
func RegionalServiceName(region string) string {
|
||||
if region != "" {
|
||||
return region + "-" + srvService // Example: `us-v2-origintunneld`
|
||||
return region + "-" + srvService
|
||||
}
|
||||
|
||||
return srvService // Global service is just `v2-origintunneld`
|
||||
return srvService
|
||||
}
|
||||
|
||||
@@ -237,21 +237,19 @@ func TestNewNoResolveBalancesRegions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRegionalServiceName(t *testing.T) {
|
||||
func TestRegionalServiceName(t *testing.T) {
|
||||
// Empty region should just go to origintunneld
|
||||
globalServiceName := getRegionalServiceName("")
|
||||
assert.Equal(t, srvService, globalServiceName)
|
||||
assert.Equal(t, srvService, RegionalServiceName(""))
|
||||
|
||||
// Non-empty region should go to the regional origintunneld variant
|
||||
for _, region := range []string{"us", "pt", "am"} {
|
||||
regionalServiceName := getRegionalServiceName(region)
|
||||
assert.Equal(t, region+"-"+srvService, regionalServiceName)
|
||||
assert.Equal(t, region+"-"+srvService, RegionalServiceName(region))
|
||||
}
|
||||
}
|
||||
|
||||
func RegionsIsBalanced(t *testing.T, rs *Regions) {
|
||||
delta := rs.region1.AvailableAddrs() - rs.region2.AvailableAddrs()
|
||||
assert.True(t, abs(delta) <= 1)
|
||||
assert.LessOrEqual(t, abs(delta), 1)
|
||||
}
|
||||
|
||||
func abs(x int) int {
|
||||
|
||||
@@ -42,6 +42,10 @@ type FeatureSnapshot struct {
|
||||
// We provide the list of features since we need it to send in the ConnectionOptions during connection
|
||||
// registrations.
|
||||
FeaturesList []string
|
||||
|
||||
// SkipPrechecks indicates when to skip connectivity pre-checks at startup.
|
||||
// Controlled via DNS TXT record to allow remote kill-switch in case of issues.
|
||||
SkipPrechecks bool
|
||||
}
|
||||
|
||||
type PostQuantumMode uint8
|
||||
|
||||
@@ -24,6 +24,7 @@ const (
|
||||
|
||||
type featuresRecord struct {
|
||||
DatagramV3Percentage uint32 `json:"dv3_2"`
|
||||
SkipPrechecks bool `json:"skip_prechecks"`
|
||||
|
||||
// DatagramV3Percentage int32 `json:"dv3"` // Removed in TUN-9291
|
||||
// DatagramV3Percentage uint32 `json:"dv3_1"` // Removed in TUN-9883
|
||||
@@ -89,6 +90,7 @@ func (fs *featureSelector) Snapshot() FeatureSnapshot {
|
||||
PostQuantum: fs.postQuantumMode(),
|
||||
DatagramVersion: fs.datagramVersion(),
|
||||
FeaturesList: fs.clientFeatures(),
|
||||
SkipPrechecks: fs.prechecksSkip(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +123,12 @@ func (fs *featureSelector) datagramVersion() DatagramVersion {
|
||||
return DatagramV2
|
||||
}
|
||||
|
||||
// prechecksSkip returns whether prechecks are enabled via DNS flag.
|
||||
// Defaults to false if not set in the DNS TXT record.
|
||||
func (fs *featureSelector) prechecksSkip() bool {
|
||||
return fs.remoteFeatures.SkipPrechecks
|
||||
}
|
||||
|
||||
// clientFeatures will return the list of currently available features that cloudflared should provide to the edge.
|
||||
func (fs *featureSelector) clientFeatures() []string {
|
||||
// Evaluate any remote features along with static feature list to construct the list of features
|
||||
@@ -186,7 +194,7 @@ func (dr *dnsResolver) lookupRecord(ctx context.Context) ([]byte, error) {
|
||||
}
|
||||
|
||||
if len(records) == 0 {
|
||||
return nil, fmt.Errorf("No TXT record found for %s to determine which features to opt-in", featureSelectorHostname)
|
||||
return nil, fmt.Errorf("no TXT record found for %s to determine which features to opt-in", featureSelectorHostname)
|
||||
}
|
||||
|
||||
return []byte(records[0]), nil
|
||||
|
||||
@@ -3,6 +3,7 @@ module github.com/cloudflare/cloudflared
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/cloudflare/backoff v0.0.0-20240920015135-e46b80a3a7d0
|
||||
github.com/coreos/go-oidc/v3 v3.17.0
|
||||
github.com/coreos/go-systemd/v22 v22.5.0
|
||||
github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434
|
||||
@@ -11,7 +12,7 @@ require (
|
||||
github.com/getsentry/sentry-go v0.43.0
|
||||
github.com/go-chi/chi/v5 v5.2.2
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-jose/go-jose/v4 v4.1.3
|
||||
github.com/go-jose/go-jose/v4 v4.1.4
|
||||
github.com/gobwas/ws v1.2.1
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/google/uuid v1.6.0
|
||||
@@ -24,22 +25,23 @@ require (
|
||||
github.com/prometheus/client_model v0.6.2
|
||||
github.com/quic-go/quic-go v0.52.0
|
||||
github.com/rs/zerolog v1.20.0
|
||||
github.com/shirou/gopsutil/v4 v4.26.3
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/urfave/cli/v2 v2.3.0
|
||||
go.opentelemetry.io/contrib/propagators v0.22.0
|
||||
go.opentelemetry.io/otel v1.40.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0
|
||||
go.opentelemetry.io/otel/sdk v1.40.0
|
||||
go.opentelemetry.io/otel/trace v1.40.0
|
||||
go.opentelemetry.io/proto/otlp v1.2.0
|
||||
go.opentelemetry.io/otel v1.43.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0
|
||||
go.opentelemetry.io/otel/sdk v1.43.0
|
||||
go.opentelemetry.io/otel/trace v1.43.0
|
||||
go.opentelemetry.io/proto/otlp v1.10.0
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
go.uber.org/mock v0.5.1
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/net v0.40.0
|
||||
golang.org/x/sync v0.14.0
|
||||
golang.org/x/sys v0.40.0
|
||||
golang.org/x/term v0.32.0
|
||||
google.golang.org/protobuf v1.36.6
|
||||
golang.org/x/crypto v0.51.0
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/sys v0.45.0
|
||||
golang.org/x/term v0.43.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
nhooyr.io/websocket v1.8.7
|
||||
@@ -53,6 +55,7 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
|
||||
github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 // indirect
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
||||
@@ -60,14 +63,16 @@ require (
|
||||
github.com/gin-gonic/gin v1.9.1 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-playground/validator/v10 v10.15.1 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/google/pprof v0.0.0-20250418163039-24c5476c6587 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
@@ -75,20 +80,24 @@ require (
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/common v0.64.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.3 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
golang.org/x/arch v0.4.0 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/tools v0.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect
|
||||
google.golang.org/grpc v1.72.2 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/oauth2 v0.35.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
||||
google.golang.org/grpc v1.79.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chungthuang/quic-go v0.45.1-0.20250428085412-43229ad201fd h1:VdYI5zFQ2h1/qzoC6rhyPx479bkF8i177Qpg4Q2n1vk=
|
||||
github.com/chungthuang/quic-go v0.45.1-0.20250428085412-43229ad201fd/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
|
||||
github.com/cloudflare/backoff v0.0.0-20240920015135-e46b80a3a7d0 h1:pRcxfaAlK0vR6nOeQs7eAEvjJzdGXl8+KaBlcvpQTyQ=
|
||||
github.com/cloudflare/backoff v0.0.0-20240920015135-e46b80a3a7d0/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
@@ -27,6 +29,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
|
||||
github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 h1:wWke/RUCl7VRjQhwPlR/v0glZXNYzBHdNUzf/Am2Nmg=
|
||||
@@ -56,13 +60,15 @@ github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -105,8 +111,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d h1:PRDnysJ9dF1vUMmEzBu6aHQeUluSQy4eWH3RsSSy/vI=
|
||||
github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@@ -126,6 +132,8 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
@@ -158,6 +166,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
@@ -176,6 +186,8 @@ github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJ
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
|
||||
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
@@ -190,6 +202,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
||||
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
@@ -197,26 +213,28 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/propagators v0.22.0 h1:KGdv58M2//veiYLIhb31mofaI2LgkIPXXAZVeYVyfd8=
|
||||
go.opentelemetry.io/contrib/propagators v0.22.0/go.mod h1:xGOuXr6lLIF9BXipA4pm6UuOSI0M98U6tsI3khbOiwU=
|
||||
go.opentelemetry.io/otel v1.0.0-RC2/go.mod h1:w1thVQ7qbAy8MHb0IFj8a5Q2QU0l2ksf8u/CN8m3NOM=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0/go.mod h1:z46paqbJ9l7c9fIPCXTqTGwhQZ5XoTIsfeFYWboizjs=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.0.0-RC2/go.mod h1:JPQ+z6nNw9mqEGT8o3eoPTdnNI+Aj5JcxEsVGREIAy4=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94=
|
||||
go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -227,53 +245,57 @@ golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
|
||||
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 h1:IkAfh6J/yllPtpYFU0zZN1hUPYdT0ogkBT/9hMxHjvg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
|
||||
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
+6
-1
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
_ "net/http/pprof" //nolint:gosec // G108: the sensitive /debug/pprof/cmdline endpoint is explicitly blocked in newMetricsHandler
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -70,6 +70,11 @@ func newMetricsHandler(
|
||||
log *zerolog.Logger,
|
||||
) *http.ServeMux {
|
||||
router := http.NewServeMux()
|
||||
// Block /debug/pprof/cmdline to prevent leaking secret command-line arguments
|
||||
// (e.g. tunnel tokens) that are exposed via os.Args.
|
||||
router.HandleFunc("/debug/pprof/cmdline", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
})
|
||||
router.Handle("/debug/", http.DefaultServeMux)
|
||||
router.Handle("/metrics", promhttp.Handler())
|
||||
router.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/diagnostic"
|
||||
)
|
||||
|
||||
func testHandler(t *testing.T) *http.ServeMux {
|
||||
t.Helper()
|
||||
|
||||
log := zerolog.Nop()
|
||||
return newMetricsHandler(Config{
|
||||
DiagnosticHandler: diagnostic.NewDiagnosticHandler(
|
||||
&log, 0, nil, uuid.Nil, uuid.Nil, nil, map[string]string{}, nil,
|
||||
),
|
||||
}, &log)
|
||||
}
|
||||
|
||||
func TestPprofCmdlineEndpointIsBlocked(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := testHandler(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/debug/pprof/cmdline", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
}
|
||||
|
||||
func TestOtherPprofEndpointsStillWork(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := testHandler(t)
|
||||
|
||||
// /debug/pprof/ index should still be served by DefaultServeMux
|
||||
req := httptest.NewRequest(http.MethodGet, "/debug/pprof/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ../prechecks/resolvers.go
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -typed -build_flags=-tags=gomock -package mocks -destination mock_resolvers.go -source=../prechecks/resolvers.go
|
||||
//
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
tls "crypto/tls"
|
||||
net "net"
|
||||
netip "net/netip"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
quic "github.com/quic-go/quic-go"
|
||||
zerolog "github.com/rs/zerolog"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
|
||||
dialopts "github.com/cloudflare/cloudflared/connection/dialopts"
|
||||
allregions "github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
)
|
||||
|
||||
// MockDNSResolver is a mock of DNSResolver interface.
|
||||
type MockDNSResolver struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockDNSResolverMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockDNSResolverMockRecorder is the mock recorder for MockDNSResolver.
|
||||
type MockDNSResolverMockRecorder struct {
|
||||
mock *MockDNSResolver
|
||||
}
|
||||
|
||||
// NewMockDNSResolver creates a new mock instance.
|
||||
func NewMockDNSResolver(ctrl *gomock.Controller) *MockDNSResolver {
|
||||
mock := &MockDNSResolver{ctrl: ctrl}
|
||||
mock.recorder = &MockDNSResolverMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockDNSResolver) EXPECT() *MockDNSResolverMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Resolve mocks base method.
|
||||
func (m *MockDNSResolver) Resolve(region string) ([][]*allregions.EdgeAddr, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Resolve", region)
|
||||
ret0, _ := ret[0].([][]*allregions.EdgeAddr)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Resolve indicates an expected call of Resolve.
|
||||
func (mr *MockDNSResolverMockRecorder) Resolve(region any) *MockDNSResolverResolveCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*MockDNSResolver)(nil).Resolve), region)
|
||||
return &MockDNSResolverResolveCall{Call: call}
|
||||
}
|
||||
|
||||
// MockDNSResolverResolveCall wrap *gomock.Call
|
||||
type MockDNSResolverResolveCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockDNSResolverResolveCall) Return(arg0 [][]*allregions.EdgeAddr, arg1 error) *MockDNSResolverResolveCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockDNSResolverResolveCall) Do(f func(string) ([][]*allregions.EdgeAddr, error)) *MockDNSResolverResolveCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockDNSResolverResolveCall) DoAndReturn(f func(string) ([][]*allregions.EdgeAddr, error)) *MockDNSResolverResolveCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// MockTCPDialer is a mock of TCPDialer interface.
|
||||
type MockTCPDialer struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockTCPDialerMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockTCPDialerMockRecorder is the mock recorder for MockTCPDialer.
|
||||
type MockTCPDialerMockRecorder struct {
|
||||
mock *MockTCPDialer
|
||||
}
|
||||
|
||||
// NewMockTCPDialer creates a new mock instance.
|
||||
func NewMockTCPDialer(ctrl *gomock.Controller) *MockTCPDialer {
|
||||
mock := &MockTCPDialer{ctrl: ctrl}
|
||||
mock.recorder = &MockTCPDialerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockTCPDialer) EXPECT() *MockTCPDialerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// DialEdge mocks base method.
|
||||
func (m *MockTCPDialer) DialEdge(ctx context.Context, timeout time.Duration, tlsConfig *tls.Config, addr *net.TCPAddr, localIP net.IP) (net.Conn, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DialEdge", ctx, timeout, tlsConfig, addr, localIP)
|
||||
ret0, _ := ret[0].(net.Conn)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// DialEdge indicates an expected call of DialEdge.
|
||||
func (mr *MockTCPDialerMockRecorder) DialEdge(ctx, timeout, tlsConfig, addr, localIP any) *MockTCPDialerDialEdgeCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DialEdge", reflect.TypeOf((*MockTCPDialer)(nil).DialEdge), ctx, timeout, tlsConfig, addr, localIP)
|
||||
return &MockTCPDialerDialEdgeCall{Call: call}
|
||||
}
|
||||
|
||||
// MockTCPDialerDialEdgeCall wrap *gomock.Call
|
||||
type MockTCPDialerDialEdgeCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockTCPDialerDialEdgeCall) Return(arg0 net.Conn, arg1 error) *MockTCPDialerDialEdgeCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockTCPDialerDialEdgeCall) Do(f func(context.Context, time.Duration, *tls.Config, *net.TCPAddr, net.IP) (net.Conn, error)) *MockTCPDialerDialEdgeCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockTCPDialerDialEdgeCall) DoAndReturn(f func(context.Context, time.Duration, *tls.Config, *net.TCPAddr, net.IP) (net.Conn, error)) *MockTCPDialerDialEdgeCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// MockQUICDialer is a mock of QUICDialer interface.
|
||||
type MockQUICDialer struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockQUICDialerMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockQUICDialerMockRecorder is the mock recorder for MockQUICDialer.
|
||||
type MockQUICDialerMockRecorder struct {
|
||||
mock *MockQUICDialer
|
||||
}
|
||||
|
||||
// NewMockQUICDialer creates a new mock instance.
|
||||
func NewMockQUICDialer(ctrl *gomock.Controller) *MockQUICDialer {
|
||||
mock := &MockQUICDialer{ctrl: ctrl}
|
||||
mock.recorder = &MockQUICDialerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockQUICDialer) EXPECT() *MockQUICDialerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// DialQuic mocks base method.
|
||||
func (m *MockQUICDialer) DialQuic(ctx context.Context, quicConfig *quic.Config, tlsConfig *tls.Config, addr netip.AddrPort, localAddr net.IP, connIndex uint8, logger *zerolog.Logger, opts dialopts.DialOpts) (quic.Connection, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DialQuic", ctx, quicConfig, tlsConfig, addr, localAddr, connIndex, logger, opts)
|
||||
ret0, _ := ret[0].(quic.Connection)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// DialQuic indicates an expected call of DialQuic.
|
||||
func (mr *MockQUICDialerMockRecorder) DialQuic(ctx, quicConfig, tlsConfig, addr, localAddr, connIndex, logger, opts any) *MockQUICDialerDialQuicCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DialQuic", reflect.TypeOf((*MockQUICDialer)(nil).DialQuic), ctx, quicConfig, tlsConfig, addr, localAddr, connIndex, logger, opts)
|
||||
return &MockQUICDialerDialQuicCall{Call: call}
|
||||
}
|
||||
|
||||
// MockQUICDialerDialQuicCall wrap *gomock.Call
|
||||
type MockQUICDialerDialQuicCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockQUICDialerDialQuicCall) Return(arg0 quic.Connection, arg1 error) *MockQUICDialerDialQuicCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockQUICDialerDialQuicCall) Do(f func(context.Context, *quic.Config, *tls.Config, netip.AddrPort, net.IP, uint8, *zerolog.Logger, dialopts.DialOpts) (quic.Connection, error)) *MockQUICDialerDialQuicCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockQUICDialerDialQuicCall) DoAndReturn(f func(context.Context, *quic.Config, *tls.Config, netip.AddrPort, net.IP, uint8, *zerolog.Logger, dialopts.DialOpts) (quic.Connection, error)) *MockQUICDialerDialQuicCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// MockManagementDialer is a mock of ManagementDialer interface.
|
||||
type MockManagementDialer struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockManagementDialerMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockManagementDialerMockRecorder is the mock recorder for MockManagementDialer.
|
||||
type MockManagementDialerMockRecorder struct {
|
||||
mock *MockManagementDialer
|
||||
}
|
||||
|
||||
// NewMockManagementDialer creates a new mock instance.
|
||||
func NewMockManagementDialer(ctrl *gomock.Controller) *MockManagementDialer {
|
||||
mock := &MockManagementDialer{ctrl: ctrl}
|
||||
mock.recorder = &MockManagementDialerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockManagementDialer) EXPECT() *MockManagementDialerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// DialContext mocks base method.
|
||||
func (m *MockManagementDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DialContext", ctx, network, addr)
|
||||
ret0, _ := ret[0].(net.Conn)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// DialContext indicates an expected call of DialContext.
|
||||
func (mr *MockManagementDialerMockRecorder) DialContext(ctx, network, addr any) *MockManagementDialerDialContextCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DialContext", reflect.TypeOf((*MockManagementDialer)(nil).DialContext), ctx, network, addr)
|
||||
return &MockManagementDialerDialContextCall{Call: call}
|
||||
}
|
||||
|
||||
// MockManagementDialerDialContextCall wrap *gomock.Call
|
||||
type MockManagementDialerDialContextCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockManagementDialerDialContextCall) Return(arg0 net.Conn, arg1 error) *MockManagementDialerDialContextCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockManagementDialerDialContextCall) Do(f func(context.Context, string, string) (net.Conn, error)) *MockManagementDialerDialContextCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockManagementDialerDialContextCall) DoAndReturn(f func(context.Context, string, string) (net.Conn, error)) *MockManagementDialerDialContextCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
@@ -3,3 +3,5 @@
|
||||
package mocks
|
||||
|
||||
//go:generate sh -c "go run go.uber.org/mock/mockgen -typed -build_flags=\"-tags=gomock\" -package mocks -destination mock_limiter.go -source=../flow/limiter.go Limiter"
|
||||
|
||||
//go:generate sh -c "go run go.uber.org/mock/mockgen -typed -build_flags=\"-tags=gomock\" -package mocks -destination mock_resolvers.go -source=../prechecks/resolvers.go"
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
package prechecks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/backoff"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 10 * time.Second
|
||||
maxRetries = 2
|
||||
retryBaseDelay = 1 * time.Second
|
||||
maxRetryDelay = 16 * time.Second
|
||||
)
|
||||
|
||||
// RunDialers holds the injectable dependencies for Run(). Production callers build
|
||||
// this with real implementations; tests supply mocks.
|
||||
type RunDialers struct {
|
||||
DNSResolver DNSResolver
|
||||
TCPDialer TCPDialer
|
||||
QUICDialer QUICDialer
|
||||
ManagementDialer ManagementDialer
|
||||
}
|
||||
|
||||
// TransportResults holds the per-target results for each transport probe type.
|
||||
// Each slice has one entry per resolved target group, in the same order as the
|
||||
// target labels slice.
|
||||
type TransportResults struct {
|
||||
QUIC []CheckResult // one per target group
|
||||
HTTP2 []CheckResult // one per target group
|
||||
ManagementAPI CheckResult // single target, no groups
|
||||
}
|
||||
|
||||
// Collect returns all results as a slice in a consistent order for reporting:
|
||||
// all QUIC rows first (one per target), then all HTTP2 rows, then Management API.
|
||||
func (tr TransportResults) Collect() []CheckResult {
|
||||
results := make([]CheckResult, 0, len(tr.QUIC)+len(tr.HTTP2)+1)
|
||||
results = append(results, tr.QUIC...)
|
||||
results = append(results, tr.HTTP2...)
|
||||
results = append(results, tr.ManagementAPI)
|
||||
return results
|
||||
}
|
||||
|
||||
// Run executes the following connectivity pre-checks:
|
||||
//
|
||||
// 1. Edge address resolution — either DNS-based SRV discovery (normal path)
|
||||
// or direct resolution of --edge addresses (static path). The static path
|
||||
// skips DNS probe rows entirely since there are no SRV records to validate.
|
||||
// 2. QUIC, HTTP/2, and Management API probes run concurrently against the
|
||||
// resolved addresses.
|
||||
//
|
||||
// Each failed probe is retried up to maxRetries times with exponential backoff.
|
||||
// The suite is bounded by cfg.Timeout (defaultTimeout if zero).
|
||||
func Run(ctx context.Context, caCert string, cfg Config, log *zerolog.Logger, runDialers RunDialers) Report {
|
||||
runID := uuid.New()
|
||||
|
||||
if cfg.Timeout <= 0 {
|
||||
cfg.Timeout = defaultTimeout
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, cfg.Timeout)
|
||||
defer cancel()
|
||||
|
||||
// Build TLS configs once per protocol.
|
||||
quicTLSConfig, quicTLSErr := probeTLSConfig(caCert, connection.QUIC)
|
||||
http2TLSConfig, http2TLSErr := probeTLSConfig(caCert, connection.HTTP2)
|
||||
|
||||
// 1) Resolve edge addresses. Each ResolvedTarget bundles its addr group
|
||||
// with the DNS CheckResult that labels it, keeping the two in sync.
|
||||
var resolvedTargets []ResolvedTarget
|
||||
if len(cfg.EdgeAddrs) > 0 {
|
||||
// Static path: explicit --edge addresses, one ResolvedTarget per addr.
|
||||
resolvedTargets = resolveStaticEdge(cfg.EdgeAddrs, log)
|
||||
} else {
|
||||
// Normal path: SRV-based discovery; DNS rows carry Pass or Fail status.
|
||||
resolvedTargets = runDNSProbe(ctx, runDialers.DNSResolver, cfg.Region)
|
||||
}
|
||||
|
||||
// Extract parallel slices for the transport probe layer.
|
||||
// nolint:prealloc // False positive. The linter is confused by the append used when producing Report.Results
|
||||
dnsResults := make([]CheckResult, len(resolvedTargets))
|
||||
perGroupAddrs := make([][]*allregions.EdgeAddr, len(resolvedTargets))
|
||||
targetLabels := make([]string, len(resolvedTargets))
|
||||
for i, rt := range resolvedTargets {
|
||||
dnsResults[i] = rt.DNSResult
|
||||
perGroupAddrs[i] = rt.Addrs
|
||||
targetLabels[i] = rt.DNSResult.Target
|
||||
}
|
||||
|
||||
// dnsOK is true when at least one target has addresses to probe.
|
||||
dnsOK := slices.ContainsFunc(resolvedTargets, func(r ResolvedTarget) bool {
|
||||
return len(r.Addrs) > 0
|
||||
})
|
||||
|
||||
// 2) Run transport probes concurrently. Each probe type gets its own
|
||||
// buffered channel — one send, one receive, no routing required.
|
||||
var results TransportResults
|
||||
|
||||
mgmtCh := make(chan CheckResult)
|
||||
go func() {
|
||||
mgmtCh <- probeManagementAPIWithRetry(ctx, runDialers.ManagementDialer)
|
||||
}()
|
||||
|
||||
if !dnsOK {
|
||||
// No addresses available: emit one skip row per target so the table
|
||||
// stays consistent with the DNS rows above.
|
||||
results.QUIC = skipResultsForTargets(dnsResults, ProbeTypeQUIC, componentUDPConnectivity)
|
||||
results.HTTP2 = skipResultsForTargets(dnsResults, ProbeTypeHTTP2, componentTCPConnectivity)
|
||||
} else {
|
||||
filteredAddrs := addrsByGroup(perGroupAddrs, cfg.IPVersion)
|
||||
|
||||
quicCh := make(chan []CheckResult, 1)
|
||||
http2Ch := make(chan []CheckResult, 1)
|
||||
|
||||
go func() {
|
||||
if quicTLSErr != nil {
|
||||
log.Warn().Err(quicTLSErr).Msg("Failed to build QUIC probe TLS config")
|
||||
quicCh <- tlsConfigErrResults(ProbeTypeQUIC, componentUDPConnectivity,
|
||||
targetLabels, fmt.Sprintf("%s: %v", detailsTLSConfigFailed, quicTLSErr), actionQUICBlocked)
|
||||
return
|
||||
}
|
||||
quicCh <- probeAllTargets(ctx, ProbeTypeQUIC, componentUDPConnectivity,
|
||||
filteredAddrs, targetLabels,
|
||||
func(addr *allregions.EdgeAddr) CheckResult {
|
||||
return probeQUIC(ctx, quicTLSConfig, runDialers.QUICDialer, addr, log)
|
||||
})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if http2TLSErr != nil {
|
||||
log.Warn().Err(http2TLSErr).Msg("Failed to build HTTP/2 probe TLS config")
|
||||
http2Ch <- tlsConfigErrResults(ProbeTypeHTTP2, componentTCPConnectivity,
|
||||
targetLabels, fmt.Sprintf("%s: %v", detailsTLSConfigFailed, http2TLSErr), actionHTTP2Blocked)
|
||||
return
|
||||
}
|
||||
http2Ch <- probeAllTargets(ctx, ProbeTypeHTTP2, componentTCPConnectivity,
|
||||
filteredAddrs, targetLabels,
|
||||
func(addr *allregions.EdgeAddr) CheckResult {
|
||||
return probeHTTP2(ctx, http2TLSConfig, runDialers.TCPDialer, addr)
|
||||
})
|
||||
}()
|
||||
|
||||
results.QUIC = <-quicCh
|
||||
results.HTTP2 = <-http2Ch
|
||||
}
|
||||
|
||||
results.ManagementAPI = <-mgmtCh
|
||||
|
||||
return Report{
|
||||
RunID: runID,
|
||||
Results: append(dnsResults, results.Collect()...),
|
||||
SuggestedProtocol: suggestProtocol(results.QUIC, results.HTTP2),
|
||||
}
|
||||
}
|
||||
|
||||
// tlsConfigErrResults returns one Fail CheckResult per target, used when
|
||||
// TLS config construction fails before any dial is attempted.
|
||||
func tlsConfigErrResults(probeType ProbeType, component string, targets []string, details, action string) []CheckResult {
|
||||
results := make([]CheckResult, len(targets))
|
||||
for i, target := range targets {
|
||||
results[i] = CheckResult{
|
||||
Type: probeType,
|
||||
Component: component,
|
||||
Target: target,
|
||||
ProbeStatus: Fail,
|
||||
Details: details,
|
||||
Action: action,
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// probeAllTargets probes each target group sequentially and returns one
|
||||
// CheckResult per group. Within each group, all available addresses (V4 and/or
|
||||
// V6) are tried and the best result is kept.
|
||||
func probeAllTargets(
|
||||
ctx context.Context,
|
||||
probeType ProbeType,
|
||||
component string,
|
||||
perGroupAddrs [][]*allregions.EdgeAddr,
|
||||
targets []string,
|
||||
probeFn func(*allregions.EdgeAddr) CheckResult,
|
||||
) []CheckResult {
|
||||
results := make([]CheckResult, len(perGroupAddrs))
|
||||
for i, addrs := range perGroupAddrs {
|
||||
results[i] = probeTarget(ctx, probeType, component, targets[i], addrs, probeFn)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// probeTarget probes all addresses for a single target group (typically one V4
|
||||
// and/or one V6) and returns the best result. Any address passing means the
|
||||
// target is reachable, so Pass beats Fail within a group.
|
||||
func probeTarget(
|
||||
ctx context.Context,
|
||||
probeType ProbeType,
|
||||
component string,
|
||||
target string,
|
||||
addrs []*allregions.EdgeAddr,
|
||||
probeFn func(*allregions.EdgeAddr) CheckResult,
|
||||
) CheckResult {
|
||||
if len(addrs) == 0 {
|
||||
return CheckResult{
|
||||
Type: probeType,
|
||||
Component: component,
|
||||
Target: target,
|
||||
ProbeStatus: Skip,
|
||||
Details: "No suitable address found for configured IP version",
|
||||
}
|
||||
}
|
||||
|
||||
best := probeWithRetry(ctx, addrs[0], probeFn)
|
||||
for _, addr := range addrs[1:] {
|
||||
if r := probeWithRetry(ctx, addr, probeFn); r.ProbeStatus == Pass {
|
||||
best = r
|
||||
}
|
||||
}
|
||||
best.Target = target
|
||||
return best
|
||||
}
|
||||
|
||||
// probeManagementAPIWithRetry runs the Cloudflare API reachability probe with retry.
|
||||
func probeManagementAPIWithRetry(ctx context.Context, dialer ManagementDialer) CheckResult {
|
||||
var r CheckResult
|
||||
withRetry(ctx, maxRetries, func() bool {
|
||||
r = probeManagementAPI(ctx, dialer)
|
||||
return r.ProbeStatus == Pass
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
// probeWithRetry calls probeFn on addr with exponential-backoff retry up to
|
||||
// maxRetries times, stopping as soon as the probe passes.
|
||||
func probeWithRetry(ctx context.Context, addr *allregions.EdgeAddr, probeFn func(*allregions.EdgeAddr) CheckResult) CheckResult {
|
||||
var r CheckResult
|
||||
withRetry(ctx, maxRetries, func() bool {
|
||||
r = probeFn(addr)
|
||||
return r.ProbeStatus == Pass
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
// addrsByGroup returns the addresses to probe for each resolved target group,
|
||||
// preserving the per-group structure. Each inner slice contains at most one V4
|
||||
// and one V6 address (subject to ipVersion).
|
||||
func addrsByGroup(addrGroups [][]*allregions.EdgeAddr, ipVersion allregions.ConfigIPVersion) [][]*allregions.EdgeAddr {
|
||||
perGroup := make([][]*allregions.EdgeAddr, 0, len(addrGroups))
|
||||
for _, group := range addrGroups {
|
||||
v4, v6 := addrsByFamily(group, ipVersion)
|
||||
var addrs []*allregions.EdgeAddr
|
||||
if v4 != nil {
|
||||
addrs = append(addrs, v4)
|
||||
}
|
||||
if v6 != nil {
|
||||
addrs = append(addrs, v6)
|
||||
}
|
||||
perGroup = append(perGroup, addrs)
|
||||
}
|
||||
return perGroup
|
||||
}
|
||||
|
||||
// skipResultsForTargets returns one skip CheckResult per entry in results,
|
||||
// using each entry's Target label so the transport row aligns with its DNS row.
|
||||
func skipResultsForTargets(targets []CheckResult, probeType ProbeType, component string) []CheckResult {
|
||||
results := make([]CheckResult, len(targets))
|
||||
for i, t := range targets {
|
||||
results[i] = skipResult(probeType, component, t.Target, detailsDNSPrerequisiteFailed)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// worstStatus returns the most severe Status across a slice of CheckResults.
|
||||
// Fail > Pass > Skip. Used to determine whether a transport type as a whole
|
||||
// should be considered failed (any region failing = transport fails).
|
||||
func worstStatus(results []CheckResult) Status {
|
||||
worst := Skip
|
||||
for _, r := range results {
|
||||
if severity(r.ProbeStatus) > severity(worst) {
|
||||
worst = r.ProbeStatus
|
||||
}
|
||||
}
|
||||
return worst
|
||||
}
|
||||
|
||||
// severity maps a Status to a comparable integer so that worse outcomes rank higher.
|
||||
func severity(s Status) int {
|
||||
switch s {
|
||||
case Fail:
|
||||
return 2
|
||||
case Pass:
|
||||
return 1
|
||||
case Skip:
|
||||
return 0
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// suggestProtocol recommends QUIC when all QUIC region probes passed, HTTP/2
|
||||
// when all HTTP/2 probes passed, and nil when neither transport works.
|
||||
// Any region failing means the transport is treated as failed (worst wins).
|
||||
func suggestProtocol(quicResults, http2Results []CheckResult) *connection.Protocol {
|
||||
if len(quicResults) > 0 && worstStatus(quicResults) == Pass {
|
||||
quic := connection.QUIC
|
||||
return &quic
|
||||
}
|
||||
if len(http2Results) > 0 && worstStatus(http2Results) == Pass {
|
||||
http2 := connection.HTTP2
|
||||
return &http2
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// withRetry calls fn up to 1+maxAttempts times, stopping as soon as fn returns
|
||||
// true. Between attempts, it sleeps with exponential backoff bounded by
|
||||
// maxRetryDelay, and stops early if ctx is done.
|
||||
func withRetry(ctx context.Context, maxAttempts int, fn func() bool) {
|
||||
b := backoff.NewWithoutJitter(maxRetryDelay, retryBaseDelay)
|
||||
for attempt := 0; attempt <= maxAttempts; attempt++ {
|
||||
if fn() {
|
||||
return
|
||||
}
|
||||
if attempt == maxAttempts {
|
||||
break
|
||||
}
|
||||
timer := time.NewTimer(b.Duration())
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,581 @@
|
||||
package prechecks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
"github.com/cloudflare/cloudflared/mocks"
|
||||
)
|
||||
|
||||
const (
|
||||
emptyCert = ""
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// twoRegionAddrs returns a two-group [][]*EdgeAddr with one IPv4 address per
|
||||
// region. Used by tests that only need to exercise the V4 path.
|
||||
func twoRegionAddrs() [][]*allregions.EdgeAddr {
|
||||
makeV4 := func(ip string) *allregions.EdgeAddr {
|
||||
parsed := net.ParseIP(ip)
|
||||
return &allregions.EdgeAddr{
|
||||
TCP: &net.TCPAddr{IP: parsed, Port: 7844},
|
||||
UDP: &net.UDPAddr{IP: parsed, Port: 7844},
|
||||
IPVersion: allregions.V4,
|
||||
}
|
||||
}
|
||||
return [][]*allregions.EdgeAddr{
|
||||
{makeV4("1.2.3.4")},
|
||||
{makeV4("5.6.7.8")},
|
||||
}
|
||||
}
|
||||
|
||||
// twoRegionAddrsBothFamilies returns a two-group [][]*EdgeAddr with one IPv4
|
||||
// and one IPv6 address per region, used by per-family probe tests.
|
||||
func twoRegionAddrsBothFamilies() [][]*allregions.EdgeAddr {
|
||||
makeAddr := func(ip string, v allregions.EdgeIPVersion) *allregions.EdgeAddr {
|
||||
parsed := net.ParseIP(ip)
|
||||
return &allregions.EdgeAddr{
|
||||
TCP: &net.TCPAddr{IP: parsed, Port: 7844},
|
||||
UDP: &net.UDPAddr{IP: parsed, Port: 7844},
|
||||
IPVersion: v,
|
||||
}
|
||||
}
|
||||
return [][]*allregions.EdgeAddr{
|
||||
{makeAddr("1.2.3.4", allregions.V4), makeAddr("2001:db8::1", allregions.V6)},
|
||||
{makeAddr("5.6.7.8", allregions.V4), makeAddr("2001:db8::2", allregions.V6)},
|
||||
}
|
||||
}
|
||||
|
||||
// nopConn is a net.Conn whose Close() is a no-op, used as the success value
|
||||
// for TCP and management dial mocks.
|
||||
type nopConn struct{ net.Conn }
|
||||
|
||||
func (nopConn) Close() error { return nil }
|
||||
|
||||
// fakeQUICConn satisfies quic.Connection for tests. Only CloseWithError is
|
||||
// implemented; the pre-check never opens streams so the rest of the interface
|
||||
// is unused via the embedded nil.
|
||||
type fakeQUICConn struct {
|
||||
quic.Connection
|
||||
}
|
||||
|
||||
func (*fakeQUICConn) CloseWithError(_ quic.ApplicationErrorCode, _ string) error { return nil }
|
||||
|
||||
// requireStatuses asserts the probe statuses in report.Results match
|
||||
// expected (in order), failing immediately on length mismatch.
|
||||
func requireStatuses(t *testing.T, report Report, expected ...Status) {
|
||||
t.Helper()
|
||||
require.Len(t, report.Results, len(expected))
|
||||
for i, want := range expected {
|
||||
got := report.Results[i].ProbeStatus
|
||||
assert.Equalf(t, want, got,
|
||||
"result[%d] (%s/%s): got %s, want %s",
|
||||
i, report.Results[i].Component, report.Results[i].Target, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func nopLogger() *zerolog.Logger {
|
||||
l := zerolog.Nop()
|
||||
return &l
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TestRun_AllPass verifies that when all probes succeed the report contains
|
||||
// 7 rows: 2 DNS + 2 QUIC (one per region) + 2 HTTP/2 (one per region) + 1 API.
|
||||
func TestRun_AllPass(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
dns := mocks.NewMockDNSResolver(ctrl)
|
||||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||||
|
||||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
|
||||
// twoRegionAddrs has 2 regions × 1 V4 address each = 2 dials per transport.
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil).Times(2)
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(&fakeQUICConn{}, nil).Times(2)
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
// 2 DNS + 2 QUIC + 2 HTTP2 + 1 API = 7 results.
|
||||
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
|
||||
assert.NotEqual(t, uuid.Nil, report.RunID, "RunID must be set")
|
||||
require.NotNil(t, report.SuggestedProtocol)
|
||||
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
|
||||
assert.False(t, report.hasHardFail())
|
||||
assert.False(t, report.hasWarn())
|
||||
}
|
||||
|
||||
// TestRun_QUICBlocked verifies that when QUIC is blocked on all regions,
|
||||
// the report is degraded (warn) and HTTP/2 is the suggested protocol.
|
||||
func TestRun_QUICBlocked(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
dns := mocks.NewMockDNSResolver(ctrl)
|
||||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||||
|
||||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil).AnyTimes()
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nil, errors.New("connection refused")).AnyTimes()
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
// 2 DNS Pass + 2 QUIC Fail + 2 HTTP2 Pass + 1 API Pass.
|
||||
requireStatuses(t, report, Pass, Pass, Fail, Fail, Pass, Pass, Pass)
|
||||
require.NotNil(t, report.SuggestedProtocol)
|
||||
assert.Equal(t, connection.HTTP2, *report.SuggestedProtocol)
|
||||
assert.False(t, report.hasHardFail())
|
||||
assert.True(t, report.hasWarn())
|
||||
}
|
||||
|
||||
// TestRun_HTTP2Blocked verifies that when HTTP/2 is blocked on all regions,
|
||||
// the report is degraded (warn) and QUIC is the suggested protocol.
|
||||
func TestRun_HTTP2Blocked(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
dns := mocks.NewMockDNSResolver(ctrl)
|
||||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||||
|
||||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nil, errors.New("connection refused")).AnyTimes()
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(&fakeQUICConn{}, nil).AnyTimes()
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
// 2 DNS Pass + 2 QUIC Pass + 2 HTTP2 Fail + 1 API Pass.
|
||||
requireStatuses(t, report, Pass, Pass, Pass, Pass, Fail, Fail, Pass)
|
||||
require.NotNil(t, report.SuggestedProtocol)
|
||||
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
|
||||
assert.False(t, report.hasHardFail())
|
||||
assert.True(t, report.hasWarn())
|
||||
}
|
||||
|
||||
// TestRun_BothTransportsBlocked verifies that when both QUIC and HTTP/2 are
|
||||
// blocked on all regions it is a hard fail with no suggested protocol.
|
||||
func TestRun_BothTransportsBlocked(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
dns := mocks.NewMockDNSResolver(ctrl)
|
||||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||||
|
||||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nil, errors.New("blocked")).AnyTimes()
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nil, errors.New("blocked")).AnyTimes()
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
// 2 DNS Pass + 2 QUIC Fail + 2 HTTP2 Fail + 1 API Pass.
|
||||
requireStatuses(t, report, Pass, Pass, Fail, Fail, Fail, Fail, Pass)
|
||||
assert.Nil(t, report.SuggestedProtocol)
|
||||
assert.True(t, report.hasHardFail())
|
||||
}
|
||||
|
||||
// TestRun_PartialRegionQUICFail verifies "worst wins" semantics: when QUIC
|
||||
// passes for region1 but fails for region2, QUIC is treated as failed and
|
||||
// HTTP/2 becomes the suggested protocol.
|
||||
func TestRun_PartialRegionQUICFail(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
dns := mocks.NewMockDNSResolver(ctrl)
|
||||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||||
|
||||
// Two regions: 1.2.3.4 (region1) and 5.6.7.8 (region2).
|
||||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
|
||||
|
||||
// TCP/HTTP2: both regions pass.
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil).AnyTimes()
|
||||
|
||||
// QUIC: region1 (1.2.3.4) passes, region2 (5.6.7.8) fails.
|
||||
region1Addr := &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 7844}
|
||||
region2Addr := &net.UDPAddr{IP: net.ParseIP("5.6.7.8"), Port: 7844}
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), region1Addr.AddrPort(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(&fakeQUICConn{}, nil).AnyTimes()
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), region2Addr.AddrPort(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nil, errors.New("connection refused")).AnyTimes()
|
||||
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
// 2 DNS Pass + QUIC-region1 Pass + QUIC-region2 Fail + 2 HTTP2 Pass + 1 API Pass.
|
||||
requireStatuses(t, report, Pass, Pass, Pass, Fail, Pass, Pass, Pass)
|
||||
|
||||
// Worst wins: region2 QUIC failed, so QUIC is treated as failed overall.
|
||||
// HTTP/2 passes on all regions → HTTP/2 is the suggested protocol.
|
||||
require.NotNil(t, report.SuggestedProtocol)
|
||||
assert.Equal(t, connection.HTTP2, *report.SuggestedProtocol)
|
||||
assert.False(t, report.hasHardFail())
|
||||
assert.True(t, report.hasWarn())
|
||||
}
|
||||
|
||||
// TestRun_DNSFail_SkipsTransports verifies that when DNS fails, transport rows
|
||||
// are emitted as Skip (one per DNS region) and no transport dials are made.
|
||||
func TestRun_DNSFail_SkipsTransports(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
dns := mocks.NewMockDNSResolver(ctrl)
|
||||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||||
|
||||
dns.EXPECT().Resolve(gomock.Any()).
|
||||
Return(nil, errors.New("no such host")).AnyTimes()
|
||||
// Transport dialers must NOT be called when DNS fails.
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
// DNS failure emits 2 Fail rows (one per default region).
|
||||
// Transport rows: one skip per DNS region for QUIC and HTTP/2 = 2 QUIC skips + 2 HTTP2 skips.
|
||||
// 2 DNS Fail + 2 QUIC Skip + 2 HTTP2 Skip + 1 API Pass = 7 results.
|
||||
require.Len(t, report.Results, 7)
|
||||
assert.Equal(t, Fail, report.Results[0].ProbeStatus, "DNS region1")
|
||||
assert.Equal(t, Fail, report.Results[1].ProbeStatus, "DNS region2")
|
||||
assert.Equal(t, Skip, report.Results[2].ProbeStatus, "QUIC region1 must be skipped")
|
||||
assert.Equal(t, Skip, report.Results[3].ProbeStatus, "QUIC region2 must be skipped")
|
||||
assert.Equal(t, Skip, report.Results[4].ProbeStatus, "HTTP/2 region1 must be skipped")
|
||||
assert.Equal(t, Skip, report.Results[5].ProbeStatus, "HTTP/2 region2 must be skipped")
|
||||
assert.Equal(t, Pass, report.Results[6].ProbeStatus, "API still runs")
|
||||
assert.True(t, report.hasHardFail())
|
||||
}
|
||||
|
||||
// TestRun_ManagementAPIFail verifies that a Management API failure results
|
||||
// in a warning (not a hard fail) and QUIC remains the suggested protocol.
|
||||
func TestRun_ManagementAPIFail(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
dns := mocks.NewMockDNSResolver(ctrl)
|
||||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||||
|
||||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
|
||||
// twoRegionAddrs has 2 regions × 1 V4 address each; each succeeds on first try.
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil).Times(2)
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(&fakeQUICConn{}, nil).Times(2)
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nil, errors.New("connection refused")).AnyTimes()
|
||||
|
||||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
// 2 DNS Pass + 2 QUIC Pass + 2 HTTP2 Pass + 1 API Fail.
|
||||
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Fail)
|
||||
require.NotNil(t, report.SuggestedProtocol)
|
||||
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
|
||||
assert.False(t, report.hasHardFail())
|
||||
assert.True(t, report.hasWarn())
|
||||
}
|
||||
|
||||
// TestRun_RegionFlagForwardedToDNS verifies that the --region flag is passed
|
||||
// verbatim to the DNS resolver and that regional hostnames appear in the results.
|
||||
func TestRun_RegionFlagForwardedToDNS(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
dns := mocks.NewMockDNSResolver(ctrl)
|
||||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||||
|
||||
// The region string must be forwarded verbatim to the DNS resolver.
|
||||
dns.EXPECT().Resolve("us").Return(twoRegionAddrs(), nil)
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil).Times(2)
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(&fakeQUICConn{}, nil).Times(2)
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
report := Run(t.Context(), emptyCert, Config{Region: "us", Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
// DNS rows carry regional hostnames (indices 0 and 1).
|
||||
assert.Equal(t, "us-region1.v2.argotunnel.com", report.Results[0].Target, "DNS region1")
|
||||
assert.Equal(t, "us-region2.v2.argotunnel.com", report.Results[1].Target, "DNS region2")
|
||||
|
||||
// Transport rows reuse the same regional hostnames (QUIC: 2,3 / HTTP2: 4,5).
|
||||
assert.Equal(t, "us-region1.v2.argotunnel.com", report.Results[2].Target, "QUIC region1")
|
||||
assert.Equal(t, "us-region2.v2.argotunnel.com", report.Results[3].Target, "QUIC region2")
|
||||
assert.Equal(t, "us-region1.v2.argotunnel.com", report.Results[4].Target, "HTTP2 region1")
|
||||
assert.Equal(t, "us-region2.v2.argotunnel.com", report.Results[5].Target, "HTTP2 region2")
|
||||
}
|
||||
|
||||
// TestRun_QUICUsesProbeConnIndex verifies that the QUIC probe always uses the
|
||||
// reserved sentinel connIndex (math.MaxUint8 = 255) to bypass port-reuse checks.
|
||||
func TestRun_QUICUsesProbeConnIndex(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
dns := mocks.NewMockDNSResolver(ctrl)
|
||||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||||
|
||||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil).Times(2)
|
||||
// connIndex must be the reserved sentinel (math.MaxUint8 = 255), never 0.
|
||||
// twoRegionAddrs has 2 regions × 1 V4 address each → 2 calls.
|
||||
quicD.EXPECT().DialQuic(
|
||||
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(),
|
||||
gomock.Eq(uint8(math.MaxUint8)),
|
||||
gomock.Any(), gomock.Any(),
|
||||
).Return(&fakeQUICConn{}, nil).Times(2)
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
}
|
||||
|
||||
// TestRun_BothFamiliesProbed verifies that when both V4 and V6 addresses are
|
||||
// present in the DNS response, both are probed (2 regions × 2 families = 4 dials).
|
||||
func TestRun_BothFamiliesProbed(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
dns := mocks.NewMockDNSResolver(ctrl)
|
||||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||||
|
||||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrsBothFamilies(), nil)
|
||||
// 2 regions × 2 families = 4 dial calls each for QUIC and HTTP/2.
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil).Times(4)
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(&fakeQUICConn{}, nil).Times(4)
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
// 2 DNS + 2 QUIC + 2 HTTP2 + 1 API = 7 results, all passing.
|
||||
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
|
||||
require.NotNil(t, report.SuggestedProtocol)
|
||||
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
|
||||
}
|
||||
|
||||
// TestRun_IPVersionRestriction verifies that when a single IP family is
|
||||
// configured, only that family is probed (2 regions × 1 addr = 2 dials per
|
||||
// transport) and the excluded family is never dialled.
|
||||
func TestRun_IPVersionRestriction(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ipVersion allregions.ConfigIPVersion
|
||||
}{
|
||||
{"IPv4Only skips V6", allregions.IPv4Only},
|
||||
{"IPv6Only skips V4", allregions.IPv6Only},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
dns := mocks.NewMockDNSResolver(ctrl)
|
||||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||||
|
||||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrsBothFamilies(), nil)
|
||||
// 2 regions × 1 addr per restricted family = 2 dials each.
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil).Times(2)
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(&fakeQUICConn{}, nil).Times(2)
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: tt.ipVersion},
|
||||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRun_EdgeAddrs_SingleAddr verifies that a single --edge addr bypasses DNS
|
||||
// probing. The report contains one DNS Skip row, transport rows labeled with
|
||||
// the raw addr string, and the Management API row.
|
||||
func TestRun_EdgeAddrs_SingleAddr(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||||
|
||||
// DNS resolver must NOT be called when EdgeAddrs is set.
|
||||
dns := mocks.NewMockDNSResolver(ctrl)
|
||||
dns.EXPECT().Resolve(gomock.Any()).Times(0)
|
||||
|
||||
// One addr resolves to one group → one dial per transport.
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil).Times(1)
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(&fakeQUICConn{}, nil).Times(1)
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
cfg := Config{
|
||||
EdgeAddrs: []string{"127.0.0.1:7844"},
|
||||
Timeout: 2 * time.Second,
|
||||
IPVersion: allregions.Auto,
|
||||
}
|
||||
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
|
||||
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
// 1 DNS Skip + 1 QUIC + 1 HTTP2 + 1 API = 4 results.
|
||||
requireStatuses(t, report, Pass, Pass, Pass, Pass)
|
||||
assert.Equal(t, ProbeTypeDNS, report.Results[0].Type, "first row must be DNS skip")
|
||||
assert.Equal(t, "127.0.0.1:7844", report.Results[1].Target, "QUIC target must be the raw --edge addr")
|
||||
assert.Equal(t, "127.0.0.1:7844", report.Results[2].Target, "HTTP2 target must be the raw --edge addr")
|
||||
require.NotNil(t, report.SuggestedProtocol)
|
||||
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
|
||||
}
|
||||
|
||||
// TestRun_EdgeAddrs_MultipleAddrs verifies that multiple --edge addrs produce
|
||||
// one transport row per addr, each labeled with its original addr string.
|
||||
func TestRun_EdgeAddrs_MultipleAddrs(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||||
|
||||
dns := mocks.NewMockDNSResolver(ctrl)
|
||||
dns.EXPECT().Resolve(gomock.Any()).Times(0)
|
||||
|
||||
// Two addrs → two groups → two dials per transport.
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil).Times(2)
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(&fakeQUICConn{}, nil).Times(2)
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
cfg := Config{
|
||||
EdgeAddrs: []string{"127.0.0.1:7844", "127.0.0.2:7844"},
|
||||
Timeout: 2 * time.Second,
|
||||
IPVersion: allregions.Auto,
|
||||
}
|
||||
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
|
||||
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
// 2 DNS Pass (one per addr) + 2 QUIC + 2 HTTP2 + 1 API = 7 results.
|
||||
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
|
||||
assert.Equal(t, ProbeTypeDNS, report.Results[0].Type, "first row must be DNS skip addr1")
|
||||
assert.Equal(t, "127.0.0.1:7844", report.Results[0].Target, "DNS skip addr1 label")
|
||||
assert.Equal(t, ProbeTypeDNS, report.Results[1].Type, "second row must be DNS skip addr2")
|
||||
assert.Equal(t, "127.0.0.2:7844", report.Results[1].Target, "DNS skip addr2 label")
|
||||
assert.Equal(t, "127.0.0.1:7844", report.Results[2].Target, "QUIC addr1")
|
||||
assert.Equal(t, "127.0.0.2:7844", report.Results[3].Target, "QUIC addr2")
|
||||
assert.Equal(t, "127.0.0.1:7844", report.Results[4].Target, "HTTP2 addr1")
|
||||
assert.Equal(t, "127.0.0.2:7844", report.Results[5].Target, "HTTP2 addr2")
|
||||
}
|
||||
|
||||
// TestRun_EdgeAddrs_UnresolvableAddr verifies that when all --edge addrs fail
|
||||
// to resolve, the DNS resolver is not called and transport rows are skipped,
|
||||
// mirroring the DNS skip row.
|
||||
func TestRun_EdgeAddrs_UnresolvableAddr(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||||
|
||||
dns := mocks.NewMockDNSResolver(ctrl)
|
||||
dns.EXPECT().Resolve(gomock.Any()).Times(0)
|
||||
|
||||
// Unresolvable addr → no groups → no transport dials.
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
cfg := Config{
|
||||
EdgeAddrs: []string{"not-a-valid-addr"},
|
||||
Timeout: 2 * time.Second,
|
||||
IPVersion: allregions.Auto,
|
||||
}
|
||||
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
|
||||
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
// 1 DNS Fail + 1 QUIC Skip + 1 HTTP2 Skip + 1 API = 4 results.
|
||||
requireStatuses(t, report, Fail, Skip, Skip, Pass)
|
||||
assert.Equal(t, ProbeTypeDNS, report.Results[0].Type)
|
||||
assert.Equal(t, "not-a-valid-addr", report.Results[0].Target)
|
||||
assert.Equal(t, ProbeTypeQUIC, report.Results[1].Type)
|
||||
assert.Equal(t, ProbeTypeHTTP2, report.Results[2].Type)
|
||||
assert.Nil(t, report.SuggestedProtocol)
|
||||
assert.True(t, report.hasHardFail())
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package prechecks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
)
|
||||
|
||||
// DNSResolver abstracts edge DNS discovery used by DNS probes.
|
||||
//
|
||||
// The production implementation wraps allregions.EdgeDiscovery
|
||||
// (edgediscovery/allregions/discovery.go), which performs an SRV lookup for
|
||||
// _v2-origintunneld._tcp.argotunnel.com, falls back to DNS-over-TLS when the
|
||||
// system resolver fails, and resolves each discovered hostname via
|
||||
// net.LookupIP. The returned slice already has each address tagged with
|
||||
// .IPVersion = V4 or V6.
|
||||
//
|
||||
// Note: allregions.EdgeDiscovery must be exported (currently unexported as
|
||||
// edgeDiscovery) before a production adapter can be wired up.
|
||||
type DNSResolver interface {
|
||||
// Resolve performs edge discovery for the given region string (empty for
|
||||
// global, "us" / "fed" for regional endpoints) and returns the resolved
|
||||
// addresses grouped by CNAME target, mirroring the structure returned by
|
||||
// allregions.EdgeDiscovery.
|
||||
Resolve(region string) ([][]*allregions.EdgeAddr, error)
|
||||
}
|
||||
|
||||
// TCPDialer abstracts the TCP + TLS handshake used by HTTP/2 connectivity probes.
|
||||
//
|
||||
// The production implementation wraps edgediscovery.DialEdge
|
||||
// (edgediscovery/dial.go), which is the same function supervisor/tunnel.go
|
||||
// uses for production HTTP/2 connections. Reusing it ensures the pre-check
|
||||
// validates the identical dial path the tunnel will take.
|
||||
type TCPDialer interface {
|
||||
// DialEdge dials the given edge TCP address with TLS, respecting the
|
||||
// provided timeout, and returns the established connection. The caller is
|
||||
// responsible for closing the connection.
|
||||
DialEdge(ctx context.Context, timeout time.Duration, tlsConfig *tls.Config, addr *net.TCPAddr, localIP net.IP) (net.Conn, error)
|
||||
}
|
||||
|
||||
// QUICDialer abstracts the UDP + QUIC handshake used by QUIC connectivity probes.
|
||||
//
|
||||
// The production implementation wraps connection.DialQuic
|
||||
// (connection/quic.go), which is the same function supervisor/tunnel.go uses
|
||||
// for production QUIC connections. The pre-check performs a handshake only —
|
||||
// no streams are opened and no RPC frames are sent — to avoid triggering the
|
||||
// OTD registration timeout described in TUN-6732.
|
||||
type QUICDialer interface {
|
||||
// DialQuic performs a QUIC handshake to the given edge address and returns
|
||||
// the established connection. The caller is responsible for closing the
|
||||
// connection. connIndex is used for UDP port reuse bookkeeping consistent
|
||||
// with the production dial path.
|
||||
DialQuic(
|
||||
ctx context.Context,
|
||||
quicConfig *quic.Config,
|
||||
tlsConfig *tls.Config,
|
||||
addr netip.AddrPort,
|
||||
localAddr net.IP,
|
||||
connIndex uint8,
|
||||
logger *zerolog.Logger,
|
||||
) (quic.Connection, error)
|
||||
}
|
||||
|
||||
// ManagementDialer abstracts the TCP dial to api.cloudflare.com:443 used by
|
||||
// the Management API probe.
|
||||
//
|
||||
// A successful TCP connection (no TLS handshake required) is sufficient to
|
||||
// confirm that port 443 is reachable. This probe is always a soft failure:
|
||||
// the tunnel can run without it, but automatic software updates will be
|
||||
// unavailable.
|
||||
type ManagementDialer interface {
|
||||
// DialContext opens a TCP connection to the given network address. The
|
||||
// caller is responsible for closing the connection.
|
||||
DialContext(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
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"
|
||||
"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,
|
||||
) (quic.Connection, 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
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
package prechecks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
"github.com/cloudflare/cloudflared/mocks"
|
||||
)
|
||||
|
||||
// Test constants for repeated string values.
|
||||
const (
|
||||
testRegion1Global = region1Global
|
||||
testRegion2Global = region2Global
|
||||
testRegion1US = region1US
|
||||
testRegion2US = region2US
|
||||
testRegion1Fed = region1Fed
|
||||
testRegion2Fed = region2Fed
|
||||
|
||||
testEdgePort = 7844
|
||||
)
|
||||
|
||||
// testTLSConfig is a minimal *tls.Config for tests. Mock dialers never
|
||||
// perform a real TLS handshake, so an empty config is sufficient.
|
||||
var testTLSConfig = &tls.Config{} //nolint:gosec
|
||||
|
||||
// mockQuicConnection is a minimal test double for quic.Connection.
|
||||
type mockQuicConnection struct {
|
||||
closeErr error
|
||||
}
|
||||
|
||||
func (m *mockQuicConnection) AcceptStream(_ context.Context) (quic.Stream, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockQuicConnection) AcceptUniStream(_ context.Context) (quic.ReceiveStream, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockQuicConnection) OpenStream() (quic.Stream, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockQuicConnection) OpenStreamSync(_ context.Context) (quic.Stream, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockQuicConnection) OpenUniStream() (quic.SendStream, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockQuicConnection) OpenUniStreamSync(_ context.Context) (quic.SendStream, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockQuicConnection) LocalAddr() net.Addr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockQuicConnection) RemoteAddr() net.Addr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockQuicConnection) CloseWithError(_ quic.ApplicationErrorCode, _ string) error {
|
||||
return m.closeErr
|
||||
}
|
||||
|
||||
func (m *mockQuicConnection) Context() context.Context {
|
||||
return context.Background()
|
||||
}
|
||||
|
||||
func (m *mockQuicConnection) ConnectionState() quic.ConnectionState {
|
||||
return quic.ConnectionState{}
|
||||
}
|
||||
|
||||
func (m *mockQuicConnection) SendDatagram(_ []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockQuicConnection) ReceiveDatagram(_ context.Context) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockQuicConnection) AddPath(*quic.Transport) (*quic.Path, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Helper to create test edge addresses.
|
||||
func createTestEdgeAddr(ip string, port int, version allregions.EdgeIPVersion) *allregions.EdgeAddr {
|
||||
parsedIP := net.ParseIP(ip)
|
||||
return &allregions.EdgeAddr{
|
||||
TCP: &net.TCPAddr{IP: parsedIP, Port: port},
|
||||
UDP: &net.UDPAddr{IP: parsedIP, Port: port},
|
||||
IPVersion: version,
|
||||
}
|
||||
}
|
||||
|
||||
// probeDNS tests.
|
||||
|
||||
func TestProbeDNS_Success(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
v4Addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
v6Addr := createTestEdgeAddr("2001:db8::1", testEdgePort, allregions.V6)
|
||||
|
||||
resolver := mocks.NewMockDNSResolver(ctrl)
|
||||
resolver.EXPECT().Resolve("").Return([][]*allregions.EdgeAddr{{v4Addr, v6Addr}}, nil)
|
||||
|
||||
targets := probeDNS(resolver, "")
|
||||
|
||||
require.Len(t, targets, 1)
|
||||
assert.NotEmpty(t, targets[0].Addrs)
|
||||
assert.Equal(t, ProbeTypeDNS, targets[0].DNSResult.Type)
|
||||
assert.Equal(t, testRegion1Global, targets[0].DNSResult.Target)
|
||||
assert.Equal(t, Pass, targets[0].DNSResult.ProbeStatus)
|
||||
assert.Equal(t, dnsResolvedSuccessfully, targets[0].DNSResult.Details)
|
||||
}
|
||||
|
||||
func TestProbeDNS_MultipleRegions(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
v4Addr1 := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
v4Addr2 := createTestEdgeAddr("192.0.2.2", testEdgePort, allregions.V4)
|
||||
|
||||
resolver := mocks.NewMockDNSResolver(ctrl)
|
||||
resolver.EXPECT().Resolve("").Return([][]*allregions.EdgeAddr{{v4Addr1}, {v4Addr2}}, nil)
|
||||
|
||||
targets := probeDNS(resolver, "")
|
||||
|
||||
require.Len(t, targets, 2)
|
||||
|
||||
assert.Equal(t, testRegion1Global, targets[0].DNSResult.Target)
|
||||
assert.Equal(t, Pass, targets[0].DNSResult.ProbeStatus)
|
||||
assert.NotEmpty(t, targets[0].Addrs)
|
||||
|
||||
assert.Equal(t, testRegion2Global, targets[1].DNSResult.Target)
|
||||
assert.Equal(t, Pass, targets[1].DNSResult.ProbeStatus)
|
||||
assert.NotEmpty(t, targets[1].Addrs)
|
||||
}
|
||||
|
||||
func TestProbeDNS_ResolverError(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resolver := mocks.NewMockDNSResolver(ctrl)
|
||||
resolver.EXPECT().Resolve("").Return(nil, errors.New("DNS lookup failed"))
|
||||
|
||||
targets := probeDNS(resolver, "")
|
||||
|
||||
require.Len(t, targets, 2)
|
||||
assert.Empty(t, targets[0].Addrs)
|
||||
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
|
||||
assert.Equal(t, "DNS lookup failed", targets[0].DNSResult.Details)
|
||||
assert.Contains(t, targets[0].DNSResult.Action, testRegion1Global)
|
||||
assert.Empty(t, targets[1].Addrs)
|
||||
assert.Equal(t, Fail, targets[1].DNSResult.ProbeStatus)
|
||||
assert.Contains(t, targets[1].DNSResult.Action, testRegion2Global)
|
||||
}
|
||||
|
||||
func TestProbeDNS_EmptyResults(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resolver := mocks.NewMockDNSResolver(ctrl)
|
||||
resolver.EXPECT().Resolve("").Return([][]*allregions.EdgeAddr{}, nil)
|
||||
|
||||
targets := probeDNS(resolver, "")
|
||||
|
||||
require.Len(t, targets, 2)
|
||||
assert.Empty(t, targets[0].Addrs)
|
||||
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
|
||||
assert.Equal(t, dnsNoAddressesReturned, targets[0].DNSResult.Details)
|
||||
}
|
||||
|
||||
func TestProbeDNS_EmptyGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resolver := mocks.NewMockDNSResolver(ctrl)
|
||||
resolver.EXPECT().Resolve("").Return([][]*allregions.EdgeAddr{{}}, nil)
|
||||
|
||||
targets := probeDNS(resolver, "")
|
||||
|
||||
require.Len(t, targets, 1)
|
||||
assert.Empty(t, targets[0].Addrs)
|
||||
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
|
||||
assert.Equal(t, dnsNoAddressesReturned, targets[0].DNSResult.Details)
|
||||
}
|
||||
|
||||
func TestProbeDNS_RegionFlag(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
v4Addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
resolver := mocks.NewMockDNSResolver(ctrl)
|
||||
resolver.EXPECT().Resolve("us").Return([][]*allregions.EdgeAddr{{v4Addr}}, nil)
|
||||
|
||||
targets := probeDNS(resolver, "us")
|
||||
|
||||
require.Len(t, targets, 1)
|
||||
assert.Equal(t, testRegion1US, targets[0].DNSResult.Target)
|
||||
}
|
||||
|
||||
// probeQUIC tests.
|
||||
|
||||
func TestProbeQUIC_Success(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mockConn := &mockQuicConnection{}
|
||||
dialer := mocks.NewMockQUICDialer(ctrl)
|
||||
dialer.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(mockConn, nil)
|
||||
|
||||
addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
logger := zerolog.New(nil)
|
||||
|
||||
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
|
||||
|
||||
assert.Equal(t, ProbeTypeQUIC, result.Type)
|
||||
assert.Equal(t, Pass, result.ProbeStatus)
|
||||
assert.Equal(t, detailsQUICHandshakeSuccessful, result.Details)
|
||||
}
|
||||
|
||||
func TestProbeQUIC_DialError(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dialer := mocks.NewMockQUICDialer(ctrl)
|
||||
dialer.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("connection refused"))
|
||||
|
||||
addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
logger := zerolog.New(nil)
|
||||
|
||||
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
|
||||
|
||||
assert.Equal(t, ProbeTypeQUIC, result.Type)
|
||||
assert.Equal(t, Fail, result.ProbeStatus)
|
||||
assert.Equal(t, detailsQUICHandshakeFailed, result.Details)
|
||||
assert.Equal(t, actionQUICBlocked, result.Action)
|
||||
}
|
||||
|
||||
func TestProbeQUIC_CloseErrorDoesNotAffectResult(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mockConn := &mockQuicConnection{closeErr: errors.New("close failed")}
|
||||
dialer := mocks.NewMockQUICDialer(ctrl)
|
||||
dialer.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(mockConn, nil)
|
||||
|
||||
addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
logger := zerolog.New(nil)
|
||||
|
||||
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
|
||||
|
||||
assert.Equal(t, ProbeTypeQUIC, result.Type)
|
||||
assert.Equal(t, Pass, result.ProbeStatus)
|
||||
assert.Equal(t, detailsQUICHandshakeSuccessful, result.Details)
|
||||
}
|
||||
|
||||
func TestProbeQUIC_ContextTimeout(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dialer := mocks.NewMockQUICDialer(ctrl)
|
||||
dialer.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, context.DeadlineExceeded)
|
||||
|
||||
addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
logger := zerolog.New(nil)
|
||||
|
||||
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
|
||||
|
||||
assert.Equal(t, Fail, result.ProbeStatus)
|
||||
assert.Equal(t, detailsQUICHandshakeFailed, result.Details)
|
||||
}
|
||||
|
||||
// probeHTTP2 tests.
|
||||
|
||||
func TestProbeHTTP2_Success(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dialer := mocks.NewMockTCPDialer(ctrl)
|
||||
dialer.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&net.TCPConn{}, nil)
|
||||
|
||||
addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
|
||||
result := probeHTTP2(context.Background(), testTLSConfig, dialer, addr)
|
||||
|
||||
assert.Equal(t, ProbeTypeHTTP2, result.Type)
|
||||
assert.Equal(t, Pass, result.ProbeStatus)
|
||||
assert.Equal(t, detailsHTTP2HandshakeSuccessful, result.Details)
|
||||
}
|
||||
|
||||
func TestProbeHTTP2_DialError(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dialer := mocks.NewMockTCPDialer(ctrl)
|
||||
dialer.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("connection refused"))
|
||||
|
||||
addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
|
||||
result := probeHTTP2(context.Background(), testTLSConfig, dialer, addr)
|
||||
|
||||
assert.Equal(t, ProbeTypeHTTP2, result.Type)
|
||||
assert.Equal(t, Fail, result.ProbeStatus)
|
||||
assert.Equal(t, detailsHTTP2BlockedOrUnreachable, result.Details)
|
||||
assert.Equal(t, actionHTTP2Blocked, result.Action)
|
||||
}
|
||||
|
||||
// probeManagementAPI tests.
|
||||
|
||||
func TestProbeManagementAPI_Success(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dialer := mocks.NewMockManagementDialer(ctrl)
|
||||
dialer.EXPECT().DialContext(gomock.Any(), "tcp", "api.cloudflare.com:443").Return(&net.TCPConn{}, nil)
|
||||
|
||||
result := probeManagementAPI(context.Background(), dialer)
|
||||
|
||||
assert.Equal(t, ProbeTypeManagementAPI, result.Type)
|
||||
assert.Equal(t, "Cloudflare API", result.Component)
|
||||
assert.Equal(t, "api.cloudflare.com:443", result.Target)
|
||||
assert.Equal(t, Pass, result.ProbeStatus)
|
||||
assert.Equal(t, detailsApiReachable, result.Details)
|
||||
}
|
||||
|
||||
func TestProbeManagementAPI_DialError(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dialer := mocks.NewMockManagementDialer(ctrl)
|
||||
dialer.EXPECT().DialContext(gomock.Any(), "tcp", "api.cloudflare.com:443").Return(nil, errors.New("connection refused"))
|
||||
|
||||
result := probeManagementAPI(context.Background(), dialer)
|
||||
|
||||
assert.Equal(t, ProbeTypeManagementAPI, result.Type)
|
||||
assert.Equal(t, Fail, result.ProbeStatus)
|
||||
assert.Equal(t, detailsAPIConnectionFailed, result.Details)
|
||||
assert.Equal(t, actionAPIUnreachable, result.Action)
|
||||
}
|
||||
|
||||
// skipResult tests.
|
||||
|
||||
func TestSkipResult(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := skipResult(ProbeTypeQUIC, "UDP Connectivity", "Port 7844 (QUIC)", detailsDNSPrerequisiteFailed)
|
||||
|
||||
assert.Equal(t, ProbeTypeQUIC, result.Type)
|
||||
assert.Equal(t, "UDP Connectivity", result.Component)
|
||||
assert.Equal(t, "Port 7844 (QUIC)", result.Target)
|
||||
assert.Equal(t, Skip, result.ProbeStatus)
|
||||
assert.Equal(t, detailsDNSPrerequisiteFailed, result.Details)
|
||||
}
|
||||
|
||||
// regionTargets tests.
|
||||
|
||||
func TestRegionTargets(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
region string
|
||||
wantRegion1 string
|
||||
wantRegion2 string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "empty region returns global hostnames",
|
||||
region: "",
|
||||
wantRegion1: testRegion1Global,
|
||||
wantRegion2: testRegion2Global,
|
||||
},
|
||||
{
|
||||
name: "us region returns US hostnames",
|
||||
region: "us",
|
||||
wantRegion1: testRegion1US,
|
||||
wantRegion2: testRegion2US,
|
||||
},
|
||||
{
|
||||
name: "fed region returns fed hostnames",
|
||||
region: "fed",
|
||||
wantRegion1: testRegion1Fed,
|
||||
wantRegion2: testRegion2Fed,
|
||||
},
|
||||
{
|
||||
name: "unknown region defaults to global hostnames",
|
||||
region: "eu",
|
||||
wantRegion1: testRegion1Global,
|
||||
wantRegion2: testRegion2Global,
|
||||
description: "Unknown regions should default to global hostnames",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
gotR1, gotR2 := regionTargets(tt.region)
|
||||
assert.Equal(t, tt.wantRegion1, gotR1)
|
||||
assert.Equal(t, tt.wantRegion2, gotR2)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// addrsByFamily tests.
|
||||
|
||||
func TestAddrsByFamily(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
v4Addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
v6Addr := createTestEdgeAddr("2001:db8::1", testEdgePort, allregions.V6)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
group []*allregions.EdgeAddr
|
||||
ipVersion allregions.ConfigIPVersion
|
||||
wantV4 bool
|
||||
wantV6 bool
|
||||
}{
|
||||
{
|
||||
name: "auto returns both v4 and v6",
|
||||
group: []*allregions.EdgeAddr{v4Addr, v6Addr},
|
||||
ipVersion: allregions.Auto,
|
||||
wantV4: true,
|
||||
wantV6: true,
|
||||
},
|
||||
{
|
||||
name: "ipv4 only returns v4 and nil v6",
|
||||
group: []*allregions.EdgeAddr{v4Addr, v6Addr},
|
||||
ipVersion: allregions.IPv4Only,
|
||||
wantV4: true,
|
||||
wantV6: false,
|
||||
},
|
||||
{
|
||||
name: "ipv6 only returns nil v4 and v6",
|
||||
group: []*allregions.EdgeAddr{v4Addr, v6Addr},
|
||||
ipVersion: allregions.IPv6Only,
|
||||
wantV4: false,
|
||||
wantV6: true,
|
||||
},
|
||||
{
|
||||
name: "empty group returns nil for both",
|
||||
group: []*allregions.EdgeAddr{},
|
||||
ipVersion: allregions.Auto,
|
||||
wantV4: false,
|
||||
wantV6: false,
|
||||
},
|
||||
{
|
||||
name: "only v4 available returns v4 and nil v6",
|
||||
group: []*allregions.EdgeAddr{v4Addr},
|
||||
ipVersion: allregions.Auto,
|
||||
wantV4: true,
|
||||
wantV6: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
gotV4, gotV6 := addrsByFamily(tt.group, tt.ipVersion)
|
||||
if tt.wantV4 {
|
||||
assert.NotNil(t, gotV4)
|
||||
} else {
|
||||
assert.Nil(t, gotV4)
|
||||
}
|
||||
if tt.wantV6 {
|
||||
assert.NotNil(t, gotV6)
|
||||
} else {
|
||||
assert.Nil(t, gotV6)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// IPv6 address tests for probeQUIC.
|
||||
|
||||
func TestProbeQUIC_IPv6Address(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mockConn := &mockQuicConnection{}
|
||||
dialer := mocks.NewMockQUICDialer(ctrl)
|
||||
dialer.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(mockConn, nil)
|
||||
|
||||
addr := createTestEdgeAddr("2001:db8::1", testEdgePort, allregions.V6)
|
||||
logger := zerolog.New(nil)
|
||||
|
||||
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
|
||||
|
||||
assert.Equal(t, Pass, result.ProbeStatus)
|
||||
assert.Equal(t, detailsQUICHandshakeSuccessful, result.Details)
|
||||
}
|
||||
|
||||
// IPv6 address tests for probeHTTP2.
|
||||
|
||||
func TestProbeHTTP2_IPv6Address(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dialer := mocks.NewMockTCPDialer(ctrl)
|
||||
dialer.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&net.TCPConn{}, nil)
|
||||
|
||||
addr := createTestEdgeAddr("2001:db8::1", testEdgePort, allregions.V6)
|
||||
|
||||
result := probeHTTP2(context.Background(), testTLSConfig, dialer, addr)
|
||||
|
||||
assert.Equal(t, Pass, result.ProbeStatus)
|
||||
}
|
||||
|
||||
// resolveStaticEdge tests.
|
||||
|
||||
// TestResolveStaticEdge_SingleAddr verifies that a single resolvable --edge
|
||||
// addr produces one group labeled with the original addr string.
|
||||
func TestResolveStaticEdge_SingleAddr(t *testing.T) {
|
||||
t.Parallel()
|
||||
logger := zerolog.Nop()
|
||||
targets := resolveStaticEdge([]string{"127.0.0.1:7844"}, &logger)
|
||||
require.Len(t, targets, 1)
|
||||
assert.Equal(t, "127.0.0.1:7844", targets[0].DNSResult.Target)
|
||||
assert.Equal(t, Pass, targets[0].DNSResult.ProbeStatus)
|
||||
assert.NotEmpty(t, targets[0].Addrs)
|
||||
}
|
||||
|
||||
// TestResolveStaticEdge_MultipleAddrs verifies that multiple --edge addrs each
|
||||
// produce their own ResolvedTarget, preserving per-addr structure and label order.
|
||||
func TestResolveStaticEdge_MultipleAddrs(t *testing.T) {
|
||||
t.Parallel()
|
||||
logger := zerolog.Nop()
|
||||
targets := resolveStaticEdge([]string{"127.0.0.1:7844", "127.0.0.2:7844"}, &logger)
|
||||
require.Len(t, targets, 2)
|
||||
assert.Equal(t, "127.0.0.1:7844", targets[0].DNSResult.Target)
|
||||
assert.Equal(t, "127.0.0.2:7844", targets[1].DNSResult.Target)
|
||||
}
|
||||
|
||||
// TestResolveStaticEdge_InvalidAddr verifies that an unresolvable addr is
|
||||
// silently skipped and does not appear in the output.
|
||||
func TestResolveStaticEdge_InvalidAddr(t *testing.T) {
|
||||
t.Parallel()
|
||||
logger := zerolog.Nop()
|
||||
// "not-a-valid-addr" has no port — ResolveTCPAddr will fail.
|
||||
targets := resolveStaticEdge([]string{"not-a-valid-addr"}, &logger)
|
||||
require.Len(t, targets, 1)
|
||||
assert.Equal(t, "not-a-valid-addr", targets[0].DNSResult.Target)
|
||||
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
|
||||
assert.Equal(t, dnsNoAddressesReturned, targets[0].DNSResult.Details)
|
||||
assert.Empty(t, targets[0].Addrs)
|
||||
}
|
||||
|
||||
// TestResolveStaticEdge_PartiallyValid verifies that a mix of valid and invalid
|
||||
// addrs produces one ResolvedTarget per addr — valid ones with Addrs and a Skip
|
||||
// DNSResult, invalid ones with nil Addrs and a Fail DNSResult.
|
||||
func TestResolveStaticEdge_PartiallyValid(t *testing.T) {
|
||||
t.Parallel()
|
||||
logger := zerolog.Nop()
|
||||
targets := resolveStaticEdge([]string{"127.0.0.1:7844", "not-a-valid-addr", "127.0.0.2:7844"}, &logger)
|
||||
require.Len(t, targets, 3)
|
||||
assert.Equal(t, "127.0.0.1:7844", targets[0].DNSResult.Target)
|
||||
assert.Equal(t, Pass, targets[0].DNSResult.ProbeStatus)
|
||||
assert.NotEmpty(t, targets[0].Addrs)
|
||||
assert.Equal(t, "not-a-valid-addr", targets[1].DNSResult.Target)
|
||||
assert.Equal(t, Fail, targets[1].DNSResult.ProbeStatus)
|
||||
assert.Empty(t, targets[1].Addrs)
|
||||
assert.Equal(t, "127.0.0.2:7844", targets[2].DNSResult.Target)
|
||||
assert.Equal(t, Pass, targets[2].DNSResult.ProbeStatus)
|
||||
assert.NotEmpty(t, targets[2].Addrs)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package prechecks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection/dialopts"
|
||||
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
)
|
||||
|
||||
// DNSResolver abstracts edge DNS discovery used by DNS probes.
|
||||
type DNSResolver interface {
|
||||
// Resolve performs edge discovery for the given region string and returns
|
||||
// the resolved edge regions.
|
||||
Resolve(region string) ([][]*allregions.EdgeAddr, error)
|
||||
}
|
||||
|
||||
// TCPDialer abstracts the TCP + TLS handshake used by HTTP/2 connectivity probes.
|
||||
type TCPDialer interface {
|
||||
// DialEdge dials the given edge TCP address with TLS and returns the
|
||||
// established connection. The caller is responsible for closing the connection.
|
||||
DialEdge(ctx context.Context, timeout time.Duration, tlsConfig *tls.Config, addr *net.TCPAddr, localIP net.IP) (net.Conn, error)
|
||||
}
|
||||
|
||||
// QUICDialer abstracts the UDP + QUIC handshake used by QUIC connectivity probes.
|
||||
type QUICDialer interface {
|
||||
// DialQuic performs a QUIC handshake to the given edge address and returns
|
||||
// the established connection. The caller is responsible for closing the
|
||||
// connection. connIndex is used for UDP port reuse bookkeeping consistent
|
||||
// with the production dial path.
|
||||
DialQuic(
|
||||
ctx context.Context,
|
||||
quicConfig *quic.Config,
|
||||
tlsConfig *tls.Config,
|
||||
addr netip.AddrPort,
|
||||
localAddr net.IP,
|
||||
connIndex uint8,
|
||||
logger *zerolog.Logger,
|
||||
opts dialopts.DialOpts,
|
||||
) (quic.Connection, error)
|
||||
}
|
||||
|
||||
// ManagementDialer abstracts the TCP dial to api.cloudflare.com:443 used by
|
||||
// the Management API probe.
|
||||
type ManagementDialer interface {
|
||||
// DialContext opens a TCP connection to the given network address. The
|
||||
// caller is responsible for closing the connection.
|
||||
DialContext(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
}
|
||||
+38
-58
@@ -10,19 +10,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// tableWidth is the total character width of the separator lines.
|
||||
tableWidth = 80
|
||||
|
||||
// Status names.
|
||||
passStatus = "PASS"
|
||||
failStatus = "FAIL"
|
||||
skipStatus = "SKIP"
|
||||
unknownStatus = "UNKNOWN"
|
||||
|
||||
// Section separators.
|
||||
sectionChar = "-"
|
||||
headerTitle = "CONNECTIVITY PRE-CHECKS"
|
||||
|
||||
// Log message constants.
|
||||
logMsgPrecheck = "precheck"
|
||||
logMsgPrecheckComplete = "precheck complete"
|
||||
@@ -35,8 +28,6 @@ const (
|
||||
logFieldDetails = "details"
|
||||
logFieldHardFail = "hard_fail"
|
||||
logFieldSuggestedProtocol = "suggested_protocol"
|
||||
|
||||
sep = " "
|
||||
)
|
||||
|
||||
// statusLabel returns the display label for a given Status.
|
||||
@@ -58,21 +49,9 @@ func (s Status) logString() string {
|
||||
return strings.ToLower(s.String())
|
||||
}
|
||||
|
||||
// separator returns a full-width horizontal line.
|
||||
func separator() string {
|
||||
return strings.Repeat(sectionChar, tableWidth)
|
||||
}
|
||||
|
||||
// header returns the top section title line.
|
||||
func header() string {
|
||||
leftDashes := strings.Repeat(sectionChar, 3)
|
||||
rightLen := tableWidth - len(leftDashes) - len(headerTitle) - len(sep)*2
|
||||
return leftDashes + sep + headerTitle + sep + strings.Repeat(sectionChar, rightLen)
|
||||
}
|
||||
|
||||
// renderTable uses text/tabwriter to format the results rows with
|
||||
// automatically aligned columns, returning the rendered string.
|
||||
func renderTable(results []CheckResult) string {
|
||||
// automatically aligned columns, returning the rendered lines.
|
||||
func renderTable(results []CheckResult) []string {
|
||||
var buf bytes.Buffer
|
||||
// minwidth=0, tabwidth=8, padding=2, padchar=' ', flags=0
|
||||
w := tabwriter.NewWriter(&buf, 0, 8, 2, ' ', 0)
|
||||
@@ -81,27 +60,27 @@ func renderTable(results []CheckResult) string {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", r.Component, r.Target, r.ProbeStatus.statusLabel(), r.Details)
|
||||
}
|
||||
_ = w.Flush()
|
||||
return buf.String()
|
||||
return strings.Split(strings.TrimSuffix(buf.String(), "\n"), "\n")
|
||||
}
|
||||
|
||||
// renderActions collects all non-empty Action strings from results and returns
|
||||
// the formatted warning/error block that appears between the table and SUMMARY.
|
||||
// A Fail result is rendered as ERROR when the report is a hard fail, and as
|
||||
// WARNING otherwise (degraded but tunnel can still run).
|
||||
func renderActions(r Report) string {
|
||||
func renderActions(r Report) []string {
|
||||
hardFail := r.hasHardFail()
|
||||
var sb strings.Builder
|
||||
actions := make([]string, 0)
|
||||
for _, res := range r.Results {
|
||||
if res.Action == "" || res.ProbeStatus != Fail {
|
||||
continue
|
||||
}
|
||||
if hardFail {
|
||||
_, _ = fmt.Fprintf(&sb, "ERROR: %s\n", res.Action)
|
||||
actions = append(actions, fmt.Sprintf("ERROR: %s", res.Action))
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(&sb, "WARNING: %s\n", res.Action)
|
||||
actions = append(actions, fmt.Sprintf("WARNING: %s", res.Action))
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
return actions
|
||||
}
|
||||
|
||||
// summaryLine builds the SUMMARY: line based on the Report state.
|
||||
@@ -110,9 +89,19 @@ func summaryLine(r Report) string {
|
||||
case r.hasHardFail():
|
||||
return "SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel."
|
||||
case r.hasWarn():
|
||||
return fmt.Sprintf("SUMMARY: Environment ready with degraded transport. cloudflared will proceed using '%s'.", r.SuggestedProtocol)
|
||||
if r.SuggestedProtocol == nil {
|
||||
return "SUMMARY: Environment ready with degraded transport."
|
||||
}
|
||||
|
||||
protocol := r.SuggestedProtocol.String()
|
||||
return fmt.Sprintf("SUMMARY: Environment ready with degraded transport. cloudflared will proceed using '%s'.", protocol)
|
||||
default:
|
||||
return fmt.Sprintf("SUMMARY: Environment is healthy. cloudflared will use '%s' as primary protocol.", r.SuggestedProtocol)
|
||||
if r.SuggestedProtocol == nil {
|
||||
return "SUMMARY: Environment is healthy."
|
||||
}
|
||||
|
||||
protocol := r.SuggestedProtocol.String()
|
||||
return fmt.Sprintf("SUMMARY: Environment is healthy. cloudflared will use '%s' as primary protocol.", protocol)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,28 +160,12 @@ func (r Report) hasWarn() bool {
|
||||
return (quicFail != http2Fail) || apiFail
|
||||
}
|
||||
|
||||
// String renders the Report as a human-readable table suitable for os.Stdout.
|
||||
func (r Report) String() string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(header())
|
||||
sb.WriteString("\n")
|
||||
|
||||
sb.WriteString(renderTable(r.Results))
|
||||
|
||||
actions := renderActions(r)
|
||||
if actions != "" {
|
||||
sb.WriteString(actions)
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(summaryLine(r))
|
||||
sb.WriteString("\n")
|
||||
|
||||
sb.WriteString(separator())
|
||||
sb.WriteString("\n")
|
||||
|
||||
return sb.String()
|
||||
// String renders the Report as human-readable table lines suitable for logging.
|
||||
func (r Report) String() []string {
|
||||
lines := renderTable(r.Results)
|
||||
lines = append(lines, renderActions(r)...)
|
||||
lines = append(lines, "", summaryLine(r))
|
||||
return lines
|
||||
}
|
||||
|
||||
// LogEvent emits each CheckResult as a structured zerolog log line, followed by
|
||||
@@ -210,9 +183,16 @@ func (r Report) LogEvent(logger *zerolog.Logger) {
|
||||
Msg(logMsgPrecheck)
|
||||
}
|
||||
|
||||
logger.Info().
|
||||
Str(logFieldRunID, runID).
|
||||
Bool(logFieldHardFail, r.hasHardFail()).
|
||||
Str(logFieldSuggestedProtocol, r.SuggestedProtocol.String()).
|
||||
Msg(logMsgPrecheckComplete)
|
||||
if r.SuggestedProtocol != nil {
|
||||
logger.Info().
|
||||
Str(logFieldRunID, runID).
|
||||
Bool(logFieldHardFail, r.hasHardFail()).
|
||||
Str(logFieldSuggestedProtocol, r.SuggestedProtocol.String()).
|
||||
Msg(logMsgPrecheckComplete)
|
||||
} else {
|
||||
logger.Info().
|
||||
Str(logFieldRunID, runID).
|
||||
Bool(logFieldHardFail, r.hasHardFail()).
|
||||
Msg(logMsgPrecheckComplete)
|
||||
}
|
||||
}
|
||||
|
||||
+111
-112
@@ -23,13 +23,13 @@ var fixedRunID = uuid.MustParse("00000000-0000-0000-0000-000000000001")
|
||||
func allPassReport() Report {
|
||||
return Report{
|
||||
RunID: fixedRunID,
|
||||
SuggestedProtocol: connection.QUIC,
|
||||
SuggestedProtocol: new(connection.QUIC),
|
||||
Results: []CheckResult{
|
||||
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
|
||||
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
|
||||
{Type: ProbeTypeQUIC, Component: "UDP Connectivity", Target: "Port 7844 (QUIC)", ProbeStatus: Pass, Details: "Handshake successful"},
|
||||
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: "TLS handshake successful"},
|
||||
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: "Reachable"},
|
||||
{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},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -39,20 +39,20 @@ func allPassReport() Report {
|
||||
func quicBlockedReport() Report {
|
||||
return Report{
|
||||
RunID: fixedRunID,
|
||||
SuggestedProtocol: connection.HTTP2,
|
||||
SuggestedProtocol: new(connection.HTTP2),
|
||||
Results: []CheckResult{
|
||||
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
|
||||
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
|
||||
{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: "Handshake failed",
|
||||
Action: "Allow outbound QUIC on port 7844. cloudflared will use http2 in the meantime.",
|
||||
Details: detailsQUICHandshakeFailed,
|
||||
Action: actionQUICBlocked,
|
||||
},
|
||||
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: "TLS handshake successful"},
|
||||
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: "Reachable"},
|
||||
{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},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -62,12 +62,12 @@ func quicBlockedReport() Report {
|
||||
func apiFailReport() Report {
|
||||
return Report{
|
||||
RunID: fixedRunID,
|
||||
SuggestedProtocol: connection.QUIC,
|
||||
SuggestedProtocol: new(connection.QUIC),
|
||||
Results: []CheckResult{
|
||||
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
|
||||
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
|
||||
{Type: ProbeTypeQUIC, Component: "UDP Connectivity", Target: "Port 7844 (QUIC)", ProbeStatus: Pass, Details: "Handshake successful"},
|
||||
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: "TLS handshake successful"},
|
||||
{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",
|
||||
@@ -84,16 +84,16 @@ func apiFailReport() Report {
|
||||
func bothTransportsBlockedReport() Report {
|
||||
return Report{
|
||||
RunID: fixedRunID,
|
||||
SuggestedProtocol: connection.HTTP2,
|
||||
SuggestedProtocol: nil,
|
||||
Results: []CheckResult{
|
||||
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
|
||||
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
|
||||
{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: "Handshake failed",
|
||||
Details: detailsQUICHandshakeFailed,
|
||||
Action: "Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.",
|
||||
},
|
||||
{
|
||||
@@ -101,9 +101,9 @@ func bothTransportsBlockedReport() Report {
|
||||
Component: "TCP Connectivity",
|
||||
Target: "Port 7844 (HTTP/2)",
|
||||
ProbeStatus: Fail,
|
||||
Details: "Blocked or unreachable",
|
||||
Details: detailsHTTP2BlockedOrUnreachable,
|
||||
},
|
||||
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: "Reachable"},
|
||||
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: detailsApiReachable},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ func bothTransportsBlockedReport() Report {
|
||||
func dnsFailReport() Report {
|
||||
return Report{
|
||||
RunID: fixedRunID,
|
||||
SuggestedProtocol: connection.HTTP2,
|
||||
SuggestedProtocol: nil,
|
||||
Results: []CheckResult{
|
||||
{
|
||||
Type: ProbeTypeDNS,
|
||||
@@ -134,111 +134,106 @@ func dnsFailReport() Report {
|
||||
|
||||
func TestString_AllPass(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := "" +
|
||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
||||
"COMPONENT TARGET STATUS DETAILS\n" +
|
||||
"DNS Resolution region1.v2.argotunnel.com PASS Resolved successfully\n" +
|
||||
"DNS Resolution region2.v2.argotunnel.com PASS Resolved successfully\n" +
|
||||
"UDP Connectivity Port 7844 (QUIC) PASS Handshake successful\n" +
|
||||
"TCP Connectivity Port 7844 (HTTP/2) PASS TLS handshake successful\n" +
|
||||
"Cloudflare API api.cloudflare.com:443 PASS Reachable\n" +
|
||||
"\n" +
|
||||
"SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol.\n" +
|
||||
"--------------------------------------------------------------------------------\n"
|
||||
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 := "" +
|
||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
||||
"COMPONENT TARGET STATUS DETAILS\n" +
|
||||
"DNS Resolution region1.v2.argotunnel.com PASS Resolved successfully\n" +
|
||||
"DNS Resolution region2.v2.argotunnel.com PASS Resolved successfully\n" +
|
||||
"UDP Connectivity Port 7844 (QUIC) FAIL Handshake failed\n" +
|
||||
"TCP Connectivity Port 7844 (HTTP/2) PASS TLS handshake successful\n" +
|
||||
"Cloudflare API api.cloudflare.com:443 PASS Reachable\n" +
|
||||
"WARNING: Allow outbound QUIC on port 7844. cloudflared will use http2 in the meantime.\n" +
|
||||
"\n" +
|
||||
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'http2'.\n" +
|
||||
"--------------------------------------------------------------------------------\n"
|
||||
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 := "" +
|
||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
||||
"COMPONENT TARGET STATUS DETAILS\n" +
|
||||
"DNS Resolution region1.v2.argotunnel.com PASS Resolved successfully\n" +
|
||||
"DNS Resolution region2.v2.argotunnel.com PASS Resolved successfully\n" +
|
||||
"UDP Connectivity Port 7844 (QUIC) PASS Handshake successful\n" +
|
||||
"TCP Connectivity Port 7844 (HTTP/2) PASS TLS handshake successful\n" +
|
||||
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused\n" +
|
||||
"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.\n" +
|
||||
"\n" +
|
||||
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'quic'.\n" +
|
||||
"--------------------------------------------------------------------------------\n"
|
||||
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 := "" +
|
||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
||||
"COMPONENT TARGET STATUS DETAILS\n" +
|
||||
"DNS Resolution region1.v2.argotunnel.com PASS Resolved successfully\n" +
|
||||
"DNS Resolution region2.v2.argotunnel.com PASS Resolved successfully\n" +
|
||||
"UDP Connectivity Port 7844 (QUIC) FAIL Handshake failed\n" +
|
||||
"TCP Connectivity Port 7844 (HTTP/2) FAIL Blocked or unreachable\n" +
|
||||
"Cloudflare API api.cloudflare.com:443 PASS Reachable\n" +
|
||||
"ERROR: Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.\n" +
|
||||
"\n" +
|
||||
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.\n" +
|
||||
"--------------------------------------------------------------------------------\n"
|
||||
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 := "" +
|
||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
||||
"COMPONENT TARGET STATUS DETAILS\n" +
|
||||
"DNS Resolution region1.v2.argotunnel.com FAIL No addresses returned\n" +
|
||||
"DNS Resolution region2.v2.argotunnel.com FAIL No addresses returned\n" +
|
||||
"UDP Connectivity Port 7844 (QUIC) SKIP DNS prerequisite failed\n" +
|
||||
"TCP Connectivity Port 7844 (HTTP/2) SKIP DNS prerequisite failed\n" +
|
||||
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused\n" +
|
||||
"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.\n" +
|
||||
"\n" +
|
||||
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.\n" +
|
||||
"--------------------------------------------------------------------------------\n"
|
||||
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: connection.QUIC}
|
||||
r := Report{RunID: fixedRunID, SuggestedProtocol: new(connection.QUIC)}
|
||||
out := r.String()
|
||||
// Must not panic and must still emit a valid skeleton.
|
||||
assert.Contains(t, out, "CONNECTIVITY PRE-CHECKS")
|
||||
assert.Contains(t, out, "SUMMARY:")
|
||||
assert.Contains(t, out, separator())
|
||||
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"`
|
||||
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
|
||||
@@ -276,11 +271,11 @@ func TestLogEvent_AllPass(t *testing.T) {
|
||||
status string
|
||||
details string
|
||||
}{
|
||||
{"DNS Resolution", "region1.v2.argotunnel.com", "pass", "Resolved successfully"},
|
||||
{"DNS Resolution", "region2.v2.argotunnel.com", "pass", "Resolved successfully"},
|
||||
{"UDP Connectivity", "Port 7844 (QUIC)", "pass", "Handshake successful"},
|
||||
{"TCP Connectivity", "Port 7844 (HTTP/2)", "pass", "TLS handshake successful"},
|
||||
{"Cloudflare API", "api.cloudflare.com:443", "pass", "Reachable"},
|
||||
{"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]
|
||||
@@ -299,7 +294,8 @@ func TestLogEvent_AllPass(t *testing.T) {
|
||||
assert.Equal(t, fixedRunID.String(), summary.RunID)
|
||||
require.NotNil(t, summary.HardFail)
|
||||
assert.False(t, *summary.HardFail)
|
||||
assert.Equal(t, "quic", summary.SuggestedProtocol)
|
||||
require.NotNil(t, summary.SuggestedProtocol)
|
||||
assert.Equal(t, "quic", *summary.SuggestedProtocol)
|
||||
}
|
||||
|
||||
func TestLogEvent_QuicBlocked(t *testing.T) {
|
||||
@@ -311,14 +307,15 @@ func TestLogEvent_QuicBlocked(t *testing.T) {
|
||||
assert.Equal(t, "fail", quic.Status)
|
||||
assert.Equal(t, "UDP Connectivity", quic.Component)
|
||||
assert.Equal(t, "Port 7844 (QUIC)", quic.Target)
|
||||
assert.Equal(t, "Handshake failed", quic.Details)
|
||||
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)
|
||||
assert.Equal(t, "http2", summary.SuggestedProtocol)
|
||||
require.NotNil(t, summary.SuggestedProtocol)
|
||||
assert.Equal(t, "http2", *summary.SuggestedProtocol)
|
||||
assert.Equal(t, fixedRunID.String(), summary.RunID)
|
||||
}
|
||||
|
||||
@@ -342,7 +339,8 @@ func TestLogEvent_APIFail(t *testing.T) {
|
||||
summary := entries[len(entries)-1]
|
||||
require.NotNil(t, summary.HardFail)
|
||||
assert.False(t, *summary.HardFail)
|
||||
assert.Equal(t, "quic", summary.SuggestedProtocol)
|
||||
require.NotNil(t, summary.SuggestedProtocol)
|
||||
assert.Equal(t, "quic", *summary.SuggestedProtocol)
|
||||
}
|
||||
|
||||
func TestLogEvent_BothTransportsBlocked(t *testing.T) {
|
||||
@@ -351,14 +349,14 @@ func TestLogEvent_BothTransportsBlocked(t *testing.T) {
|
||||
|
||||
// Both transport rows carry status=fail.
|
||||
assert.Equal(t, "fail", entries[2].Status)
|
||||
assert.Equal(t, "Handshake failed", entries[2].Details)
|
||||
assert.Equal(t, "QUIC connection failed", entries[2].Details)
|
||||
assert.Equal(t, "fail", entries[3].Status)
|
||||
assert.Equal(t, "Blocked or unreachable", entries[3].Details)
|
||||
assert.Equal(t, "HTTP/2 connection is blocked or unreachable", entries[3].Details)
|
||||
|
||||
// Summary: hard fail is true.
|
||||
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) {
|
||||
@@ -377,15 +375,15 @@ func TestLogEvent_DNSFail(t *testing.T) {
|
||||
assert.Equal(t, "skip", entries[3].Status)
|
||||
assert.Equal(t, "DNS prerequisite failed", entries[3].Details)
|
||||
|
||||
// Summary: hard fail is true.
|
||||
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: connection.HTTP2}
|
||||
r := Report{RunID: fixedRunID, SuggestedProtocol: new(connection.HTTP2)}
|
||||
entries := captureLogLines(t, r)
|
||||
|
||||
// No result lines, only the summary.
|
||||
@@ -394,7 +392,8 @@ func TestLogEvent_EmptyReport(t *testing.T) {
|
||||
assert.Equal(t, fixedRunID.String(), entries[0].RunID)
|
||||
require.NotNil(t, entries[0].HardFail)
|
||||
assert.False(t, *entries[0].HardFail)
|
||||
assert.Equal(t, "http2", entries[0].SuggestedProtocol)
|
||||
require.NotNil(t, entries[0].SuggestedProtocol)
|
||||
assert.Equal(t, "http2", *entries[0].SuggestedProtocol)
|
||||
}
|
||||
|
||||
// hasHardFail / hasWarn helper tests
|
||||
|
||||
+26
-8
@@ -57,25 +57,36 @@ type CheckResult struct {
|
||||
Type ProbeType
|
||||
|
||||
// Component is the human-readable probe category shown in the table header
|
||||
// column, e.g. "DNS Resolution", "QUIC Connectivity".
|
||||
// column
|
||||
Component string
|
||||
|
||||
// Target is the address or resource that was probed, e.g.
|
||||
// "region1.v2.argotunnel.com" or "Port 7844 (QUIC)".
|
||||
// Target is the address or resource that was probed
|
||||
Target string
|
||||
|
||||
// ProbeStatus is the outcome of the probe.
|
||||
ProbeStatus Status
|
||||
|
||||
// Details is a short description of the result shown in the table, e.g.
|
||||
// "Resolved successfully" or "Handshake failed".
|
||||
// Details is a short description of the result shown in the table
|
||||
Details string
|
||||
|
||||
// Action is non-empty when ProbeStatus is Fail and contains a human-readable
|
||||
// remediation instruction, e.g. "Allow outbound QUIC on port 7844."
|
||||
// remediation instruction
|
||||
Action string
|
||||
}
|
||||
|
||||
// ResolvedTarget bundles a resolved edge target's addresses with the DNS
|
||||
// CheckResult that describes it. This keeps addr groups and their report rows
|
||||
// together as a single unit, avoiding parallel-slice synchronization.
|
||||
type ResolvedTarget struct {
|
||||
// Addrs holds the resolved edge addresses for this target. May be empty
|
||||
// when DNS resolution succeeded structurally but returned no IPs.
|
||||
Addrs []*allregions.EdgeAddr
|
||||
|
||||
// DNSResult is the CheckResult representing DNS resolution for this target.
|
||||
// Its Target field is the human-readable label used across all probe rows.
|
||||
DNSResult CheckResult
|
||||
}
|
||||
|
||||
// Report aggregates all CheckResults produced by a single Run() invocation.
|
||||
// Pre-checks run in parallel with tunnel initialization and are purely
|
||||
// diagnostic: the Report is displayed to the user but never gates startup.
|
||||
@@ -90,8 +101,9 @@ type Report struct {
|
||||
Results []CheckResult
|
||||
|
||||
// SuggestedProtocol is the connection protocol the pre-checks recommend
|
||||
// based on transport probe results.
|
||||
SuggestedProtocol connection.Protocol
|
||||
// based on transport probe results. Nil when no valid protocol is available
|
||||
// (e.g., when both transports fail or DNS is unresolvable).
|
||||
SuggestedProtocol *connection.Protocol
|
||||
}
|
||||
|
||||
// Config controls the behavior of a pre-check Run().
|
||||
@@ -108,4 +120,10 @@ type Config struct {
|
||||
// checks. It mirrors the --edge-ip-version CLI flag so that the pre-check
|
||||
// exercises the same code paths the tunnel itself will use.
|
||||
IPVersion allregions.ConfigIPVersion
|
||||
|
||||
// EdgeAddrs, when non-empty, contains the --edge flag values (explicit
|
||||
// edge addresses). When set, DNS probing is skipped entirely — there are
|
||||
// no SRV records to validate — and transport probes target each addr
|
||||
// individually, labeled with the original addr string.
|
||||
EdgeAddrs []string
|
||||
}
|
||||
|
||||
+72
-50
@@ -189,16 +189,25 @@ sudo dnf install cloudflared
|
||||
<h2><a name="gokeyless-packages">Gokeyless</a></h2>
|
||||
|
||||
<ul>
|
||||
<li><a href="#gokeyless-debian-any">Any Debian Based Distribution (Recommended)</a></li>
|
||||
<li><a href="#gokeyless-debian-bookworm">Debian Bookworm</a></li>
|
||||
<li><a href="#gokeyless-ubuntu-focal">Ubuntu 20.04 (Focal Fossa)</a></li>
|
||||
<li><a href="#gokeyless-ubuntu-jammy">Ubuntu 22.04 (Jammy Jellyfish)</a></li>
|
||||
<li><a href="#gokeyless-ubuntu-noble">Ubuntu 24.04 (Noble Numbat)</a></li>
|
||||
<li><a href="#gokeyless-amazon-linux">Amazon Linux</a></li>
|
||||
<li><a href="#gokeyless-rhel-generic">RHEL Generic</a></li>
|
||||
<li><a href="#gokeyless-centos-7">Centos 7</a></li>
|
||||
<li><a href="#gokeyless-centos-8">Centos 8</a></li>
|
||||
<li><a href="#gokeyless-centos-stream">Centos Stream</a></li>
|
||||
<li><strong>Debian / Ubuntu (apt)</strong>
|
||||
<ul>
|
||||
<li><a href="#gokeyless-debian-trixie">Debian 13 (Trixie)</a></li>
|
||||
<li><a href="#gokeyless-debian-bookworm">Debian 12 (Bookworm)</a></li>
|
||||
<li><a href="#gokeyless-debian-bullseye">Debian 11 (Bullseye)</a></li>
|
||||
<li><a href="#gokeyless-ubuntu-noble">Ubuntu 24.04 (Noble Numbat)</a></li>
|
||||
<li><a href="#gokeyless-ubuntu-jammy">Ubuntu 22.04 (Jammy Jellyfish)</a></li>
|
||||
<li><a href="#gokeyless-ubuntu-focal">Ubuntu 20.04 (Focal Fossa)</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>RHEL / CentOS / Amazon Linux (yum/dnf)</strong>
|
||||
<ul>
|
||||
<li><a href="#gokeyless-amazon-linux-2023">Amazon Linux 2023</a></li>
|
||||
<li><a href="#gokeyless-amazon-linux-2">Amazon Linux 2</a></li>
|
||||
<li><a href="#gokeyless-rhel-9">RHEL 9 / CentOS Stream 9</a></li>
|
||||
<li><a href="#gokeyless-rhel-8">RHEL 8 / CentOS 8</a></li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 style="color: #d9534f;">Warning: Public Key Rollover (30 October 2025)</h3>
|
||||
@@ -209,20 +218,20 @@ sudo dnf install cloudflared
|
||||
package updates. The previous keys will still work for other distributions for the time being, but it is now DEPRECATED and will be removed on 30 April 2026
|
||||
</p>
|
||||
|
||||
<h3><a name="gokeyless-debian-any">Any Debian Based Distribution (Recommended)</a></h3>
|
||||
<h3><a name="gokeyless-debian-trixie">Debian 13 (Trixie)</a></h3>
|
||||
<pre>
|
||||
# Add cloudflare gpg key
|
||||
sudo mkdir -p --mode=0755 /usr/share/keyrings
|
||||
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
|
||||
|
||||
# Add this repo to your apt repositories
|
||||
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/gokeyless any main' | sudo tee /etc/apt/sources.list.d/cloudflare.list
|
||||
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/gokeyless trixie main' | sudo tee /etc/apt/sources.list.d/cloudflare.list
|
||||
|
||||
# install gokeyless
|
||||
sudo apt-get update && sudo apt-get install gokeyless
|
||||
</pre>
|
||||
|
||||
<h3><a name="gokeyless-debian-bookworm">Debian Bookworm</a></h3>
|
||||
<h3><a name="gokeyless-debian-bookworm">Debian 12 (Bookworm)</a></h3>
|
||||
<pre>
|
||||
# Add cloudflare gpg key
|
||||
sudo mkdir -p --mode=0755 /usr/share/keyrings
|
||||
@@ -235,27 +244,14 @@ echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudf
|
||||
sudo apt-get update && sudo apt-get install gokeyless
|
||||
</pre>
|
||||
|
||||
<h3><a name="gokeyless-ubuntu-focal">Ubuntu 20.04 (Focal Fossa)</a></h3>
|
||||
<h3><a name="gokeyless-debian-bullseye">Debian 11 (Bullseye)</a></h3>
|
||||
<pre>
|
||||
# Add cloudflare gpg key
|
||||
sudo mkdir -p --mode=0755 /usr/share/keyrings
|
||||
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
|
||||
|
||||
# Add this repo to your apt repositories
|
||||
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/gokeyless focal main' | sudo tee /etc/apt/sources.list.d/cloudflare.list
|
||||
|
||||
# install gokeyless
|
||||
sudo apt-get update && sudo apt-get install gokeyless
|
||||
</pre>
|
||||
|
||||
<h3><a name="gokeyless-ubuntu-jammy">Ubuntu 22.04 (Jammy Jellyfish)</a></h3>
|
||||
<pre>
|
||||
# Add cloudflare gpg key
|
||||
sudo mkdir -p --mode=0755 /usr/share/keyrings
|
||||
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
|
||||
|
||||
# Add this repo to your apt repositories
|
||||
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/gokeyless jammy main' | sudo tee /etc/apt/sources.list.d/cloudflare.list
|
||||
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/gokeyless bullseye main' | sudo tee /etc/apt/sources.list.d/cloudflare.list
|
||||
|
||||
# install gokeyless
|
||||
sudo apt-get update && sudo apt-get install gokeyless
|
||||
@@ -274,10 +270,46 @@ echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudf
|
||||
sudo apt-get update && sudo apt-get install gokeyless
|
||||
</pre>
|
||||
|
||||
<h3><a name="gokeyless-amazon-linux">Amazon Linux</a></h3>
|
||||
<h3><a name="gokeyless-ubuntu-jammy">Ubuntu 22.04 (Jammy Jellyfish)</a></h3>
|
||||
<pre>
|
||||
# Add cloudflare gpg key
|
||||
sudo mkdir -p --mode=0755 /usr/share/keyrings
|
||||
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
|
||||
|
||||
# Add this repo to your apt repositories
|
||||
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/gokeyless jammy main' | sudo tee /etc/apt/sources.list.d/cloudflare.list
|
||||
|
||||
# install gokeyless
|
||||
sudo apt-get update && sudo apt-get install gokeyless
|
||||
</pre>
|
||||
|
||||
<h3><a name="gokeyless-ubuntu-focal">Ubuntu 20.04 (Focal Fossa)</a></h3>
|
||||
<pre>
|
||||
# Add cloudflare gpg key
|
||||
sudo mkdir -p --mode=0755 /usr/share/keyrings
|
||||
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
|
||||
|
||||
# Add this repo to your apt repositories
|
||||
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/gokeyless focal main' | sudo tee /etc/apt/sources.list.d/cloudflare.list
|
||||
|
||||
# install gokeyless
|
||||
sudo apt-get update && sudo apt-get install gokeyless
|
||||
</pre>
|
||||
|
||||
<h3 style="color: #17a2b8;">Important: RPM Repository Selection for Gokeyless</h3>
|
||||
<div style="background-color: #d1ecf1; border-left: 4px solid #17a2b8; padding: 10px; margin: 10px 0;">
|
||||
<strong>Gokeyless uses CGO for PKCS#11/HSM support</strong>, which creates glibc dependencies. We provide two RPM repositories:
|
||||
<ul>
|
||||
<li><code>rpm/</code> - For <strong>RHEL 9+, CentOS Stream 9, Amazon Linux 2023</strong> (glibc 2.34+)</li>
|
||||
<li><code>rpm-el8/</code> - For <strong>RHEL 8, CentOS 8, Amazon Linux 2</strong> (glibc 2.28)</li>
|
||||
</ul>
|
||||
Use the appropriate repository for your distribution to avoid glibc version errors.
|
||||
</div>
|
||||
|
||||
<h3><a name="gokeyless-amazon-linux-2023">Amazon Linux 2023</a></h3>
|
||||
<pre>
|
||||
# Add gokeyless.repo to /etc/yum.repos.d/
|
||||
curl -fsSl https://pkg.cloudflare.com/gokeyless.repo | sudo tee /etc/yum.repos.d/gokeyless.repo
|
||||
curl -fsSl https://pkg.cloudflare.com/gokeyless/rpm/gokeyless.repo | sudo tee /etc/yum.repos.d/gokeyless.repo
|
||||
|
||||
#update repo
|
||||
sudo yum update
|
||||
@@ -286,10 +318,10 @@ sudo yum update
|
||||
sudo yum install gokeyless
|
||||
</pre>
|
||||
|
||||
<h3><a name="gokeyless-rhel-generic">RHEL Generic</a></h3>
|
||||
<h3><a name="gokeyless-amazon-linux-2">Amazon Linux 2</a></h3>
|
||||
<pre>
|
||||
# Add gokeyless.repo to /etc/yum.repos.d/
|
||||
curl -fsSl https://pkg.cloudflare.com/gokeyless.repo | sudo tee /etc/yum.repos.d/gokeyless.repo
|
||||
# Add gokeyless.repo to /etc/yum.repos.d/ (EL8 repository for glibc 2.28 compatibility)
|
||||
curl -fsSl https://pkg.cloudflare.com/gokeyless/rpm-el8/gokeyless.repo | sudo tee /etc/yum.repos.d/gokeyless.repo
|
||||
|
||||
#update repo
|
||||
sudo yum update
|
||||
@@ -298,36 +330,26 @@ sudo yum update
|
||||
sudo yum install gokeyless
|
||||
</pre>
|
||||
|
||||
<h3><a name="gokeyless-centos-7">Centos 7</a></h3>
|
||||
<pre>
|
||||
# This requires yum config-manager
|
||||
sudo yum install yum-utils
|
||||
|
||||
# Add gokeyless.repo to config-manager
|
||||
sudo yum-config-manager --add-repo https://pkg.cloudflare.com/gokeyless.repo
|
||||
|
||||
# install gokeyless
|
||||
sudo yum install gokeyless
|
||||
</pre>
|
||||
|
||||
<h3><a name="gokeyless-centos-8">Centos 8</a></h3>
|
||||
<h3><a name="gokeyless-rhel-9">RHEL 9 / CentOS Stream 9</a></h3>
|
||||
<pre>
|
||||
# This requires dnf config-manager
|
||||
# Add gokeyless.repo to config-manager
|
||||
sudo dnf config-manager --add-repo https://pkg.cloudflare.com/gokeyless.repo
|
||||
sudo dnf config-manager --add-repo https://pkg.cloudflare.com/gokeyless/rpm/gokeyless.repo
|
||||
|
||||
# install gokeyless
|
||||
sudo dnf install gokeyless
|
||||
</pre>
|
||||
|
||||
<h3><a name="gokeyless-centos-stream">Centos Stream</a></h3>
|
||||
<h3><a name="gokeyless-rhel-8">RHEL 8 / CentOS 8</a></h3>
|
||||
<pre>
|
||||
# This requires dnf config-manager
|
||||
# Add gokeyless.repo to config-manager
|
||||
sudo dnf config-manager --add-repo https://pkg.cloudflare.com/gokeyless.repo
|
||||
# Add gokeyless.repo to config-manager (EL8 repository for glibc 2.28 compatibility)
|
||||
sudo dnf config-manager --add-repo https://pkg.cloudflare.com/gokeyless/rpm-el8/gokeyless.repo
|
||||
|
||||
# install gokeyless
|
||||
sudo dnf install gokeyless
|
||||
</pre>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudflare/cloudflared/features"
|
||||
)
|
||||
|
||||
const (
|
||||
X25519Kyber768Draft00PQKex = tls.CurveID(0x6399) // X25519Kyber768Draft00
|
||||
X25519Kyber768Draft00PQKexName = "X25519Kyber768Draft00"
|
||||
P256Kyber768Draft00PQKex = tls.CurveID(0xfe32) // P256Kyber768Draft00
|
||||
P256Kyber768Draft00PQKexName = "P256Kyber768Draft00"
|
||||
X25519MLKEM768PQKex = tls.CurveID(0x11ec) // X25519MLKEM768
|
||||
X25519MLKEM768PQKexName = "X25519MLKEM768"
|
||||
)
|
||||
|
||||
var (
|
||||
nonFipsPostQuantumStrictPKex []tls.CurveID = []tls.CurveID{X25519MLKEM768PQKex}
|
||||
nonFipsPostQuantumPreferPKex []tls.CurveID = []tls.CurveID{X25519MLKEM768PQKex}
|
||||
fipsPostQuantumStrictPKex []tls.CurveID = []tls.CurveID{P256Kyber768Draft00PQKex}
|
||||
fipsPostQuantumPreferPKex []tls.CurveID = []tls.CurveID{P256Kyber768Draft00PQKex, tls.CurveP256}
|
||||
)
|
||||
|
||||
func removeDuplicates(curves []tls.CurveID) []tls.CurveID {
|
||||
bucket := make(map[tls.CurveID]bool)
|
||||
var result []tls.CurveID
|
||||
for _, curve := range curves {
|
||||
if _, ok := bucket[curve]; !ok {
|
||||
bucket[curve] = true
|
||||
result = append(result, curve)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func curvePreference(pqMode features.PostQuantumMode, fipsEnabled bool, currentCurve []tls.CurveID) ([]tls.CurveID, error) {
|
||||
switch pqMode {
|
||||
case features.PostQuantumStrict:
|
||||
// If the user passes the -post-quantum flag, we override
|
||||
// CurvePreferences to only support hybrid post-quantum key agreements.
|
||||
if fipsEnabled {
|
||||
return fipsPostQuantumStrictPKex, nil
|
||||
}
|
||||
return nonFipsPostQuantumStrictPKex, nil
|
||||
case features.PostQuantumPrefer:
|
||||
if fipsEnabled {
|
||||
// Ensure that all curves returned are FIPS compliant.
|
||||
// Moreover the first curves are post-quantum and then the
|
||||
// non post-quantum.
|
||||
return fipsPostQuantumPreferPKex, nil
|
||||
}
|
||||
curves := append(nonFipsPostQuantumPreferPKex, currentCurve...)
|
||||
curves = removeDuplicates(curves)
|
||||
return curves, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Unexpected post quantum mode")
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cloudflare/cloudflared/features"
|
||||
"github.com/cloudflare/cloudflared/fips"
|
||||
)
|
||||
|
||||
func TestCurvePreferences(t *testing.T) {
|
||||
// This tests if the correct curves are returned
|
||||
// given a PostQuantumMode and a FIPS enabled bool
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
currentCurves []tls.CurveID
|
||||
expectedCurves []tls.CurveID
|
||||
pqMode features.PostQuantumMode
|
||||
fipsEnabled bool
|
||||
}{
|
||||
{
|
||||
name: "FIPS with Prefer PQ",
|
||||
pqMode: features.PostQuantumPrefer,
|
||||
fipsEnabled: true,
|
||||
currentCurves: []tls.CurveID{tls.CurveP384},
|
||||
expectedCurves: []tls.CurveID{P256Kyber768Draft00PQKex, tls.CurveP256},
|
||||
},
|
||||
{
|
||||
name: "FIPS with Strict PQ",
|
||||
pqMode: features.PostQuantumStrict,
|
||||
fipsEnabled: true,
|
||||
currentCurves: []tls.CurveID{tls.CurveP256, tls.CurveP384},
|
||||
expectedCurves: []tls.CurveID{P256Kyber768Draft00PQKex},
|
||||
},
|
||||
{
|
||||
name: "FIPS with Prefer PQ - no duplicates",
|
||||
pqMode: features.PostQuantumPrefer,
|
||||
fipsEnabled: true,
|
||||
currentCurves: []tls.CurveID{tls.CurveP256},
|
||||
expectedCurves: []tls.CurveID{P256Kyber768Draft00PQKex, tls.CurveP256},
|
||||
},
|
||||
{
|
||||
name: "Non FIPS with Prefer PQ",
|
||||
pqMode: features.PostQuantumPrefer,
|
||||
fipsEnabled: false,
|
||||
currentCurves: []tls.CurveID{tls.CurveP256},
|
||||
expectedCurves: []tls.CurveID{X25519MLKEM768PQKex, tls.CurveP256},
|
||||
},
|
||||
{
|
||||
name: "Non FIPS with Prefer PQ - no duplicates",
|
||||
pqMode: features.PostQuantumPrefer,
|
||||
fipsEnabled: false,
|
||||
currentCurves: []tls.CurveID{X25519Kyber768Draft00PQKex, tls.CurveP256},
|
||||
expectedCurves: []tls.CurveID{X25519MLKEM768PQKex, X25519Kyber768Draft00PQKex, tls.CurveP256},
|
||||
},
|
||||
{
|
||||
name: "Non FIPS with Prefer PQ - correct preference order",
|
||||
pqMode: features.PostQuantumPrefer,
|
||||
fipsEnabled: false,
|
||||
currentCurves: []tls.CurveID{tls.CurveP256, X25519Kyber768Draft00PQKex},
|
||||
expectedCurves: []tls.CurveID{X25519MLKEM768PQKex, tls.CurveP256, X25519Kyber768Draft00PQKex},
|
||||
},
|
||||
{
|
||||
name: "Non FIPS with Strict PQ",
|
||||
pqMode: features.PostQuantumStrict,
|
||||
fipsEnabled: false,
|
||||
currentCurves: []tls.CurveID{tls.CurveP256},
|
||||
expectedCurves: []tls.CurveID{X25519MLKEM768PQKex},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tcase := range tests {
|
||||
t.Run(tcase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
curves, err := curvePreference(tcase.pqMode, tcase.fipsEnabled, tcase.currentCurves)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tcase.expectedCurves, curves)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runClientServerHandshake(t *testing.T, curves []tls.CurveID) []tls.CurveID {
|
||||
var advertisedCurves []tls.CurveID
|
||||
ts := httptest.NewUnstartedServer(nil)
|
||||
ts.TLS = &tls.Config{ // nolint: gosec
|
||||
GetConfigForClient: func(chi *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
advertisedCurves = slices.Clone(chi.SupportedCurves)
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
ts.StartTLS()
|
||||
defer ts.Close()
|
||||
clientTlsConfig := ts.Client().Transport.(*http.Transport).TLSClientConfig
|
||||
clientTlsConfig.CurvePreferences = curves
|
||||
resp, err := ts.Client().Head(ts.URL)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return advertisedCurves
|
||||
}
|
||||
|
||||
func TestSupportedCurvesNegotiation(t *testing.T) {
|
||||
for _, tcase := range []features.PostQuantumMode{features.PostQuantumPrefer} {
|
||||
curves, err := curvePreference(tcase, fips.IsFipsEnabled(), make([]tls.CurveID, 0))
|
||||
require.NoError(t, err)
|
||||
advertisedCurves := runClientServerHandshake(t, curves)
|
||||
assert.Equal(t, curves, advertisedCurves)
|
||||
}
|
||||
}
|
||||
+32
-21
@@ -19,6 +19,8 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/client"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/connection/dialopts"
|
||||
cfdcrypto "github.com/cloudflare/cloudflared/crypto"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
"github.com/cloudflare/cloudflared/features"
|
||||
@@ -61,6 +63,9 @@ type TunnelConfig struct {
|
||||
|
||||
NeedPQ bool
|
||||
|
||||
// NoPrechecks disables connectivity pre-checks at startup.
|
||||
NoPrechecks bool
|
||||
|
||||
NamedTunnel *connection.TunnelProperties
|
||||
ProtocolSelector connection.ProtocolSelector
|
||||
EdgeTLSConfigs map[connection.Protocol]*tls.Config
|
||||
@@ -83,6 +88,10 @@ func (c *TunnelConfig) connectionOptions(originLocalAddr string, previousAttempt
|
||||
return c.ClientConfig.ConnectionOptionsSnapshot(originIP, previousAttempts)
|
||||
}
|
||||
|
||||
func (c *TunnelConfig) connectionFeatures() features.FeatureSnapshot {
|
||||
return c.ClientConfig.ConnectionFeaturesSnapshot()
|
||||
}
|
||||
|
||||
func StartTunnelDaemon(
|
||||
ctx context.Context,
|
||||
config *TunnelConfig,
|
||||
@@ -126,23 +135,23 @@ type EdgeAddrHandler interface {
|
||||
ShouldGetNewAddress(connIndex uint8, err error) (needsNewAddress bool, connectivityError error)
|
||||
}
|
||||
|
||||
func NewIPAddrFallback(maxRetries uint8) *ipAddrFallback {
|
||||
return &ipAddrFallback{
|
||||
func NewIPAddrFallback(maxRetries uint8) *IpAddrFallback {
|
||||
return &IpAddrFallback{
|
||||
retriesByConnIndex: make(map[uint8]uint8),
|
||||
maxRetries: maxRetries,
|
||||
}
|
||||
}
|
||||
|
||||
// ipAddrFallback will have more conditions to fall back to a new address for certain
|
||||
// IpAddrFallback will have more conditions to fall back to a new address for certain
|
||||
// edge connection errors. This means that this handler will return true for isConnectivityError
|
||||
// for more cases like duplicate connection register and edge quic dial errors.
|
||||
type ipAddrFallback struct {
|
||||
type IpAddrFallback struct {
|
||||
m sync.Mutex
|
||||
retriesByConnIndex map[uint8]uint8
|
||||
maxRetries uint8
|
||||
}
|
||||
|
||||
func (f *ipAddrFallback) ShouldGetNewAddress(connIndex uint8, err error) (needsNewAddress bool, connectivityError error) {
|
||||
func (f *IpAddrFallback) ShouldGetNewAddress(connIndex uint8, err error) (needsNewAddress bool, connectivityError error) {
|
||||
f.m.Lock()
|
||||
defer f.m.Unlock()
|
||||
switch err.(type) {
|
||||
@@ -466,12 +475,21 @@ func (e *EdgeTunnelServer) serveConnection(
|
||||
connIndex)
|
||||
|
||||
case connection.HTTP2:
|
||||
edgeConn, err := edgediscovery.DialEdge(ctx, dialTimeout, e.config.EdgeTLSConfigs[protocol], addr.TCP, e.edgeBindAddr)
|
||||
tlsConfig, err := cfdcrypto.TLSConfigWithCurvePreferences(e.config.EdgeTLSConfigs[protocol], e.config.connectionFeatures().PostQuantum)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create TLS configuration: %w", err), true
|
||||
}
|
||||
|
||||
connLog.Logger().Info().Msgf("Tunnel connection curve preferences: %v", tlsConfig.CurvePreferences)
|
||||
|
||||
edgeConn, err := edgediscovery.DialEdge(ctx, dialTimeout, tlsConfig, addr.TCP, e.edgeBindAddr)
|
||||
if err != nil {
|
||||
connLog.ConnAwareLogger().Err(err).Msg("Unable to establish connection with Cloudflare edge")
|
||||
return err, true
|
||||
}
|
||||
|
||||
// Rebuild the connection options with the local address now that the
|
||||
// edge socket is established.
|
||||
// nolint: gosec
|
||||
connOptions := e.config.connectionOptions(edgeConn.LocalAddr().String(), uint8(backoff.Retries()))
|
||||
// nolint: zerologlint
|
||||
@@ -509,11 +527,9 @@ func (e *EdgeTunnelServer) serveHTTP2(
|
||||
controlStreamHandler connection.ControlStreamHandler,
|
||||
connIndex uint8,
|
||||
) error {
|
||||
pqMode := connOptions.FeatureSnapshot.PostQuantum
|
||||
if pqMode == features.PostQuantumStrict {
|
||||
return unrecoverableError{errors.New("HTTP/2 transport does not support post-quantum")}
|
||||
}
|
||||
|
||||
// HTTP/2 supports post-quantum key exchange the same way QUIC does. Curve
|
||||
// preferences are applied by the caller before the TLS handshake in
|
||||
// DialEdge (see TUN-10413).
|
||||
connLog.Logger().Debug().Msgf("Connecting via http2")
|
||||
h2conn := connection.NewHTTP2Connection(
|
||||
tlsServerConn,
|
||||
@@ -551,18 +567,12 @@ func (e *EdgeTunnelServer) serveQUIC(
|
||||
controlStreamHandler connection.ControlStreamHandler,
|
||||
connIndex uint8,
|
||||
) (err error, recoverable bool) {
|
||||
tlsConfig := e.config.EdgeTLSConfigs[connection.QUIC]
|
||||
|
||||
pqMode := connOptions.FeatureSnapshot.PostQuantum
|
||||
curvePref, err := curvePreference(pqMode, fips.IsFipsEnabled(), tlsConfig.CurvePreferences)
|
||||
config, err := cfdcrypto.TLSConfigWithCurvePreferences(e.config.EdgeTLSConfigs[connection.QUIC], connOptions.FeatureSnapshot.PostQuantum)
|
||||
if err != nil {
|
||||
connLogger.ConnAwareLogger().Err(err).Msgf("failed to get curve preferences")
|
||||
return err, true
|
||||
return fmt.Errorf("could not create TLS configuration: %w", err), true
|
||||
}
|
||||
|
||||
connLogger.Logger().Info().Msgf("Tunnel connection curve preferences: %v", curvePref)
|
||||
|
||||
tlsConfig.CurvePreferences = curvePref
|
||||
connLogger.Logger().Info().Msgf("Tunnel connection curve preferences: %v", config.CurvePreferences)
|
||||
|
||||
// quic-go 0.44 increases the initial packet size to 1280 by default. That breaks anyone running tunnel through WARP
|
||||
// because WARP MTU is 1280.
|
||||
@@ -589,11 +599,12 @@ func (e *EdgeTunnelServer) serveQUIC(
|
||||
conn, err := connection.DialQuic(
|
||||
ctx,
|
||||
quicConfig,
|
||||
tlsConfig,
|
||||
config,
|
||||
edgeAddr,
|
||||
e.edgeBindAddr,
|
||||
connIndex,
|
||||
connLogger.Logger(),
|
||||
dialopts.DialOpts{},
|
||||
)
|
||||
if err != nil {
|
||||
connLogger.ConnAwareLogger().Err(err).Msgf("Failed to dial a quic connection")
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
@@ -43,12 +44,11 @@ func TestWaitForBackoffFallback(t *testing.T) {
|
||||
"auto",
|
||||
"",
|
||||
false,
|
||||
false,
|
||||
mockFetcher.fetch(),
|
||||
resolveTTL,
|
||||
&log,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
initProtocol := protocolSelector.Current()
|
||||
assert.Equal(t, connection.QUIC, initProtocol)
|
||||
@@ -106,12 +106,11 @@ func TestWaitForBackoffFallback(t *testing.T) {
|
||||
"quic",
|
||||
"",
|
||||
false,
|
||||
false,
|
||||
mockFetcher.fetch(),
|
||||
resolveTTL,
|
||||
&log,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
protoFallback = &protocolFallback{backoff, protocolSelector.Current(), false}
|
||||
for i := 0; i < int(maxRetries-1); i++ {
|
||||
protoFallback.BackoffTimer() // simulate retry
|
||||
|
||||
@@ -11,12 +11,10 @@ import (
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
OriginCAPoolFlag = "origin-ca-pool"
|
||||
CaCertFlag = "cacert"
|
||||
)
|
||||
|
||||
// CertReloader can load and reload a TLS certificate from a particular filepath.
|
||||
@@ -65,7 +63,7 @@ func (cr *CertReloader) LoadCert() error {
|
||||
|
||||
// Keep the old certificate if there's a problem reading the new one.
|
||||
if err != nil {
|
||||
sentry.CaptureException(fmt.Errorf("Error parsing X509 key pair: %v", err))
|
||||
sentry.CaptureException(fmt.Errorf("error parsing X509 key pair: %v", err))
|
||||
return err
|
||||
}
|
||||
cr.certificate = &cert
|
||||
@@ -77,6 +75,7 @@ func LoadOriginCA(originCAPoolFilename string, log *zerolog.Logger) (*x509.CertP
|
||||
|
||||
if originCAPoolFilename != "" {
|
||||
var err error
|
||||
// nolint:gosec
|
||||
originCustomCAPool, err = os.ReadFile(originCAPoolFilename)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("unable to read the file %s for --%s", originCAPoolFilename, OriginCAPoolFlag))
|
||||
@@ -116,6 +115,7 @@ func LoadCustomOriginCA(originCAFilename string) (*x509.CertPool, error) {
|
||||
return certPool, nil
|
||||
}
|
||||
|
||||
// nolint: gosec
|
||||
customOriginCA, err := os.ReadFile(originCAFilename)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("unable to read the file %s", originCAFilename))
|
||||
@@ -127,10 +127,10 @@ func LoadCustomOriginCA(originCAFilename string) (*x509.CertPool, error) {
|
||||
return certPool, nil
|
||||
}
|
||||
|
||||
func CreateTunnelConfig(c *cli.Context, serverName string) (*tls.Config, error) {
|
||||
func CreateTunnelConfig(caCert string, serverName string) (*tls.Config, error) {
|
||||
var rootCAs []string
|
||||
if c.String(CaCertFlag) != "" {
|
||||
rootCAs = append(rootCAs, c.String(CaCertFlag))
|
||||
if caCert != "" {
|
||||
rootCAs = append(rootCAs, caCert)
|
||||
}
|
||||
|
||||
userConfig := &TLSParameters{RootCAs: rootCAs, ServerName: serverName}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIsLockFileStale_DeadProcess(t *testing.T) {
|
||||
// write a lock file with a PID that cannot exist (e.g., max int32)
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.lock")
|
||||
content := lockContent{PID: 2147483647, StartTime: 1000000000000}
|
||||
data, err := json.Marshal(content)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.WriteFile(path, data, 0600))
|
||||
|
||||
stale, _, err := isLockFileStale(path)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, stale)
|
||||
}
|
||||
|
||||
func TestIsLockFileStale_LiveProcess(t *testing.T) {
|
||||
// write a lock file with our own PID and start time
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.lock")
|
||||
content, err := newSelfLockContent()
|
||||
require.NoError(t, err)
|
||||
data, err := json.Marshal(content)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.WriteFile(path, data, 0600))
|
||||
|
||||
stale, readBack, err := isLockFileStale(path)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, stale)
|
||||
assert.Equal(t, content.PID, readBack.PID)
|
||||
assert.Equal(t, content.StartTime, readBack.StartTime)
|
||||
}
|
||||
|
||||
func TestIsLockFileStale_EmptyFile(t *testing.T) {
|
||||
// backward compat: old lock files are empty
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.lock")
|
||||
require.NoError(t, os.WriteFile(path, []byte{}, 0600))
|
||||
|
||||
stale, _, err := isLockFileStale(path)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, stale)
|
||||
}
|
||||
|
||||
func TestIsLockFileStale_CorruptFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.lock")
|
||||
require.NoError(t, os.WriteFile(path, []byte("not json"), 0600))
|
||||
|
||||
stale, _, err := isLockFileStale(path)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, stale)
|
||||
}
|
||||
|
||||
func TestReadAuthURL_Exists(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
tokenPath := filepath.Join(dir, "token")
|
||||
url := "https://example.com/cdn-cgi/access/cli?token=abc123"
|
||||
require.NoError(t, os.WriteFile(tokenPath+".url", []byte(url), 0600))
|
||||
|
||||
assert.Equal(t, url, readAuthURL(tokenPath))
|
||||
}
|
||||
|
||||
func TestReadAuthURL_NotExists(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
tokenPath := filepath.Join(dir, "token")
|
||||
|
||||
assert.Empty(t, readAuthURL(tokenPath))
|
||||
}
|
||||
|
||||
func TestTryCreateLockFile_Success(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.lock")
|
||||
|
||||
err := tryCreateLockFile(path)
|
||||
require.NoError(t, err)
|
||||
|
||||
// verify the file contains valid JSON with our PID
|
||||
data, err := os.ReadFile(path) // nolint: gosec
|
||||
require.NoError(t, err)
|
||||
var content lockContent
|
||||
require.NoError(t, json.Unmarshal(data, &content))
|
||||
assert.Equal(t, int32(os.Getpid()), content.PID) // nolint: gosec
|
||||
assert.Positive(t, content.StartTime)
|
||||
}
|
||||
|
||||
func TestTryCreateLockFile_AlreadyExists(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.lock")
|
||||
|
||||
require.NoError(t, tryCreateLockFile(path))
|
||||
|
||||
// second create should fail with "already exists"
|
||||
err := tryCreateLockFile(path)
|
||||
require.Error(t, err)
|
||||
assert.True(t, os.IsExist(err))
|
||||
}
|
||||
|
||||
func TestNewSelfLockContent(t *testing.T) {
|
||||
content, err := newSelfLockContent()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int32(os.Getpid()), content.PID) // nolint: gosec
|
||||
assert.Positive(t, content.StartTime)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
//go:build linux || darwin
|
||||
|
||||
package token
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSignalHandler(t *testing.T) {
|
||||
sigHandler := signalHandler{signals: []os.Signal{syscall.SIGUSR1}}
|
||||
handlerRan := false
|
||||
done := make(chan struct{})
|
||||
timer := time.NewTimer(time.Second)
|
||||
sigHandler.register(func() {
|
||||
handlerRan = true
|
||||
done <- struct{}{}
|
||||
})
|
||||
|
||||
p, err := os.FindProcess(os.Getpid())
|
||||
require.Nil(t, err)
|
||||
p.Signal(syscall.SIGUSR1)
|
||||
|
||||
// Blocks for up to one second to make sure the handler callback runs before the assert.
|
||||
select {
|
||||
case <-done:
|
||||
assert.True(t, handlerRan)
|
||||
case <-timer.C:
|
||||
t.Fail()
|
||||
}
|
||||
sigHandler.deregister()
|
||||
}
|
||||
|
||||
func TestSignalHandlerClose(t *testing.T) {
|
||||
sigHandler := signalHandler{signals: []os.Signal{syscall.SIGUSR1}}
|
||||
done := make(chan struct{})
|
||||
timer := time.NewTimer(time.Second)
|
||||
sigHandler.register(func() { done <- struct{}{} })
|
||||
sigHandler.deregister()
|
||||
|
||||
p, err := os.FindProcess(os.Getpid())
|
||||
require.Nil(t, err)
|
||||
p.Signal(syscall.SIGUSR1)
|
||||
select {
|
||||
case <-done:
|
||||
t.Fail()
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
+168
-95
@@ -1,23 +1,18 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/retry"
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -41,15 +36,10 @@ type AppInfo struct {
|
||||
AppDomain string
|
||||
}
|
||||
|
||||
type lock struct {
|
||||
lockFilePath string
|
||||
backoff *retry.BackoffHandler
|
||||
sigHandler *signalHandler
|
||||
}
|
||||
|
||||
type signalHandler struct {
|
||||
sigChannel chan os.Signal
|
||||
signals []os.Signal
|
||||
// lockContent is the JSON structure written into lock files.
|
||||
type lockContent struct {
|
||||
PID int32 `json:"pid"`
|
||||
StartTime int64 `json:"start_time"`
|
||||
}
|
||||
|
||||
type jwtPayload struct {
|
||||
@@ -100,83 +90,174 @@ func (p jwtPayload) isExpired() bool {
|
||||
return int(time.Now().Unix()) > p.Exp
|
||||
}
|
||||
|
||||
func (s *signalHandler) register(handler func()) {
|
||||
s.sigChannel = make(chan os.Signal, 1)
|
||||
signal.Notify(s.sigChannel, s.signals...)
|
||||
go func(s *signalHandler) {
|
||||
for range s.sigChannel {
|
||||
handler()
|
||||
const (
|
||||
lockRetryInterval = 2 * time.Second
|
||||
lockTimeout = 10 * time.Minute
|
||||
startTimeTolerance = int64(1000) // milliseconds
|
||||
)
|
||||
|
||||
// acquireLockFile loops until it successfully creates a lock file for the
|
||||
// given token file path. The lock file is created at tokenPath + ".lock".
|
||||
//
|
||||
// On each iteration:
|
||||
// 1. Try to create the file atomically with O_CREATE|O_EXCL.
|
||||
// If that succeeds, write our PID + start time and return nil.
|
||||
// 2. If the file already exists, read it and check whether the owning
|
||||
// process is still alive (PID exists and start time matches).
|
||||
// 3. If the owner is alive, sleep for lockRetryInterval and retry.
|
||||
// 4. If the owner is dead (stale lock), remove the file and immediately
|
||||
// retry the O_EXCL create. No sleep (the atomic create is the
|
||||
// tiebreaker if multiple processes race to reclaim).
|
||||
func acquireLockFile(tokenPath string, log *zerolog.Logger) error {
|
||||
lockPath := tokenPath + ".lock"
|
||||
deadline := time.Now().Add(lockTimeout)
|
||||
lastURL := ""
|
||||
for {
|
||||
if time.Now().After(deadline) {
|
||||
return fmt.Errorf("timed out waiting for lock file %s", lockPath)
|
||||
}
|
||||
err := tryCreateLockFile(lockPath)
|
||||
if err == nil {
|
||||
log.Debug().Str("path", lockPath).Msg("lock file acquired")
|
||||
return nil
|
||||
}
|
||||
if !os.IsExist(err) {
|
||||
return errors.Wrapf(err, "failed to create lock file %s", lockPath)
|
||||
}
|
||||
}(s)
|
||||
}
|
||||
|
||||
func (s *signalHandler) deregister() {
|
||||
signal.Stop(s.sigChannel)
|
||||
close(s.sigChannel)
|
||||
}
|
||||
|
||||
func errDeleteTokenFailed(lockFilePath string) error {
|
||||
return fmt.Errorf("failed to acquire a new Access token. Please try to delete %s", lockFilePath)
|
||||
}
|
||||
|
||||
// newLock will get a new file lock
|
||||
func newLock(path string) *lock {
|
||||
lockPath := path + ".lock"
|
||||
backoff := retry.NewBackoff(uint(7), retry.DefaultBaseTime, false)
|
||||
return &lock{
|
||||
lockFilePath: lockPath,
|
||||
backoff: &backoff,
|
||||
sigHandler: &signalHandler{
|
||||
signals: []os.Signal{syscall.SIGINT, syscall.SIGTERM},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lock) Acquire() error {
|
||||
// Intercept SIGINT and SIGTERM to release lock before exiting
|
||||
l.sigHandler.register(func() {
|
||||
_ = l.deleteLockFile()
|
||||
os.Exit(0)
|
||||
})
|
||||
|
||||
// Check for a lock file
|
||||
// if the lock file exists; start polling
|
||||
// if not, create the lock file and go through the normal flow.
|
||||
// See AUTH-1736 for the reason why we do all this
|
||||
for isTokenLocked(l.lockFilePath) {
|
||||
if l.backoff.Backoff(context.Background()) {
|
||||
// lock file exists, so check if the owner is still alive
|
||||
stale, content, checkErr := isLockFileStale(lockPath)
|
||||
if checkErr != nil {
|
||||
// file may be mid-write by another racer, or was removed
|
||||
// between our O_EXCL attempt and this read
|
||||
log.Debug().Err(checkErr).Str("path", lockPath).
|
||||
Msg("could not read lock file, retrying")
|
||||
time.Sleep(lockRetryInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := l.deleteLockFile(); err != nil {
|
||||
return err
|
||||
if !stale {
|
||||
// try to display the auth URL so the user can open a browser
|
||||
// manually if the original window is not visible
|
||||
if authURL := readAuthURL(tokenPath); authURL != "" && authURL != lastURL {
|
||||
fmt.Fprintf(os.Stderr, "\nAnother cloudflared process (pid %d) "+
|
||||
"is already waiting for authentication.\n\n"+
|
||||
"If a browser window did not open, please visit "+
|
||||
"the following URL:\n\n%s\n\n", content.PID, authURL)
|
||||
lastURL = authURL
|
||||
}
|
||||
log.Debug().Str("path", lockPath).
|
||||
Msg("lock file is held by another process, retrying")
|
||||
time.Sleep(lockRetryInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
// stale, so remove and immediately retry
|
||||
log.Debug().Str("path", lockPath).Int32("stale_pid", content.PID).
|
||||
Msg("reclaiming stale lock file")
|
||||
if removeErr := os.Remove(lockPath); removeErr != nil && !os.IsNotExist(removeErr) {
|
||||
log.Debug().Err(removeErr).Str("path", lockPath).
|
||||
Msg("could not remove stale lock file, retrying")
|
||||
time.Sleep(lockRetryInterval)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a lock file so other processes won't also try to get the token at
|
||||
// the same time
|
||||
if err := os.WriteFile(l.lockFilePath, []byte{}, 0600); err != nil {
|
||||
// readAuthURL reads the auth URL companion file for the given token path.
|
||||
// Returns the URL string, or empty string if the file doesn't exist or
|
||||
// can't be read.
|
||||
func readAuthURL(tokenPath string) string {
|
||||
data, err := os.ReadFile(tokenPath + ".url") // nolint: gosec
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
// tryCreateLockFile atomically creates the lock file using O_CREATE|O_EXCL
|
||||
// and writes the current process's PID and start time into it as JSON.
|
||||
// The file is created with 0600 permissions (owner read/write only).
|
||||
func tryCreateLockFile(path string) (retErr error) {
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) // nolint: gosec
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(path)
|
||||
return
|
||||
}
|
||||
retErr = f.Close()
|
||||
}()
|
||||
|
||||
func (l *lock) deleteLockFile() error {
|
||||
if err := os.Remove(l.lockFilePath); err != nil && !os.IsNotExist(err) {
|
||||
return errDeleteTokenFailed(l.lockFilePath)
|
||||
content, err := newSelfLockContent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
return json.NewEncoder(f).Encode(content)
|
||||
}
|
||||
|
||||
func (l *lock) Release() error {
|
||||
defer l.sigHandler.deregister()
|
||||
return l.deleteLockFile()
|
||||
// newSelfLockContent returns a lockContent describing the current process.
|
||||
func newSelfLockContent() (lockContent, error) {
|
||||
pid := int32(os.Getpid()) // nolint: gosec
|
||||
p, err := process.NewProcess(pid)
|
||||
if err != nil {
|
||||
return lockContent{}, fmt.Errorf("failed to look up own process: %w", err)
|
||||
}
|
||||
ct, err := p.CreateTime()
|
||||
if err != nil {
|
||||
return lockContent{}, fmt.Errorf("failed to get own start time: %w", err)
|
||||
}
|
||||
return lockContent{PID: pid, StartTime: ct}, nil
|
||||
}
|
||||
|
||||
// isTokenLocked checks to see if there is another process attempting to get the token already
|
||||
func isTokenLocked(lockFilePath string) bool {
|
||||
exists, err := config.FileExists(lockFilePath)
|
||||
return exists && err == nil
|
||||
// isLockFileStale reads the lock file and checks whether the owning process
|
||||
// is dead or has a mismatched start time. Returns (true, content, nil) if
|
||||
// stale, (false, content, nil) if actively held, or an error if the file
|
||||
// cannot be read.
|
||||
func isLockFileStale(path string) (bool, lockContent, error) {
|
||||
data, err := os.ReadFile(path) // nolint: gosec
|
||||
if err != nil {
|
||||
return false, lockContent{}, err
|
||||
}
|
||||
var content lockContent
|
||||
if err := json.Unmarshal(data, &content); err != nil {
|
||||
// corrupt or empty file (treat as stale)
|
||||
return true, lockContent{}, nil
|
||||
}
|
||||
|
||||
p, err := process.NewProcess(content.PID)
|
||||
if err != nil {
|
||||
return true, content, nil // process does not exist
|
||||
}
|
||||
// CreateTime reads /proc/{pid}/stat on Linux (world-readable, always works).
|
||||
// On Windows and macOS it can fail for processes owned by a different user,
|
||||
// but cloudflared instances sharing a lock file are always running as the
|
||||
// same user (the lock directory is derived from ~ via go-homedir).
|
||||
ct, err := p.CreateTime()
|
||||
if err != nil {
|
||||
return true, content, nil // cannot query process (treat as stale)
|
||||
}
|
||||
|
||||
diff := ct - content.StartTime
|
||||
if diff < 0 {
|
||||
diff = -diff
|
||||
}
|
||||
if diff > startTimeTolerance {
|
||||
return true, content, nil // PID was recycled (different process)
|
||||
}
|
||||
|
||||
// If the lock file is older than lockTimeout, the auth flow is
|
||||
// definitely complete and the process is no longer doing auth work.
|
||||
info, err := os.Stat(path)
|
||||
if err == nil && time.Since(info.ModTime()) > lockTimeout {
|
||||
return true, content, nil
|
||||
}
|
||||
|
||||
return false, content, nil // process is alive and actively authenticating
|
||||
}
|
||||
|
||||
func Init(version string) {
|
||||
@@ -206,13 +287,9 @@ func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, autoClose boo
|
||||
return "", errors.Wrap(err, "failed to generate app token file path")
|
||||
}
|
||||
|
||||
fileLockAppToken := newLock(appTokenPath)
|
||||
if err = fileLockAppToken.Acquire(); err != nil {
|
||||
if err = acquireLockFile(appTokenPath, log); err != nil {
|
||||
return "", errors.Wrap(err, "failed to acquire app token lock")
|
||||
}
|
||||
defer func() {
|
||||
_ = fileLockAppToken.Release()
|
||||
}()
|
||||
|
||||
// check to see if another process has gotten a token while we waited for the lock
|
||||
if token, err := GetAppTokenIfExists(appInfo); token != "" && err == nil {
|
||||
@@ -228,13 +305,9 @@ func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, autoClose boo
|
||||
return "", errors.Wrap(err, "failed to generate org token file path")
|
||||
}
|
||||
|
||||
fileLockOrgToken := newLock(orgTokenPath)
|
||||
if err = fileLockOrgToken.Acquire(); err != nil {
|
||||
if err = acquireLockFile(orgTokenPath, log); err != nil {
|
||||
return "", errors.Wrap(err, "failed to acquire org token lock")
|
||||
}
|
||||
defer func() {
|
||||
_ = fileLockOrgToken.Release()
|
||||
}()
|
||||
// check if an org token has been created since the lock was acquired
|
||||
orgToken, err = GetOrgTokenIfExists(appInfo.AuthDomain)
|
||||
}
|
||||
@@ -243,7 +316,7 @@ func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, autoClose boo
|
||||
log.Debug().Msgf("failed to exchange org token for app token: %s", err)
|
||||
} else {
|
||||
// generate app path
|
||||
if err := os.WriteFile(appTokenPath, []byte(appToken), 0600); err != nil {
|
||||
if err := os.WriteFile(appTokenPath, []byte(appToken), 0600); err != nil { // nolint: gosec
|
||||
return "", errors.Wrap(err, "failed to write app token to disk")
|
||||
}
|
||||
return appToken, nil
|
||||
@@ -260,7 +333,7 @@ func getTokensFromEdge(appURL *url.URL, appAUD, appTokenPath, orgTokenPath strin
|
||||
// this weird parameter is the resource name (token) and the key/value
|
||||
// we want to send to the transfer service. the key is token and the value
|
||||
// is blank (basically just the id generated in the transfer service)
|
||||
resourceData, err := RunTransfer(appURL, appAUD, keyName, keyName, "", true, useHostOnly, autoClose, isFedramp, log)
|
||||
resourceData, err := RunTransfer(appURL, appAUD, keyName, keyName, "", true, useHostOnly, autoClose, isFedramp, log, appTokenPath+".url")
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to run transfer service")
|
||||
}
|
||||
@@ -303,11 +376,11 @@ func GetAppInfo(reqURL *url.URL) (*AppInfo, error) {
|
||||
return nil, errors.Wrap(err, "failed to create app info request")
|
||||
}
|
||||
appInfoReq.Header.Add("User-Agent", userAgent)
|
||||
resp, err := client.Do(appInfoReq)
|
||||
resp, err := client.Do(appInfoReq) // nolint: gosec
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get app info")
|
||||
}
|
||||
resp.Body.Close()
|
||||
_ = resp.Body.Close()
|
||||
|
||||
var aud string
|
||||
location := resp.Request.URL
|
||||
@@ -334,7 +407,7 @@ func GetAppInfo(reqURL *url.URL) (*AppInfo, error) {
|
||||
func handleRedirects(req *http.Request, via []*http.Request, orgToken string) error {
|
||||
// attach org token to login request
|
||||
if strings.Contains(req.URL.Path, AccessLoginWorkerPath) {
|
||||
req.AddCookie(&http.Cookie{Name: tokenCookie, Value: orgToken})
|
||||
req.AddCookie(&http.Cookie{Name: tokenCookie, Value: orgToken}) //nolint: gosec
|
||||
}
|
||||
|
||||
// attach app session cookie to authorized request
|
||||
@@ -344,7 +417,7 @@ func handleRedirects(req *http.Request, via []*http.Request, orgToken string) er
|
||||
if prevReq != nil && prevReq.Response != nil {
|
||||
for _, c := range prevReq.Response.Cookies() {
|
||||
if c.Name == appSessionCookie {
|
||||
req.AddCookie(&http.Cookie{Name: appSessionCookie, Value: c.Value})
|
||||
req.AddCookie(&http.Cookie{Name: appSessionCookie, Value: c.Value}) //nolint: gosec
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -374,11 +447,11 @@ func exchangeOrgToken(appURL *url.URL, orgToken string) (string, error) {
|
||||
return "", errors.Wrap(err, "failed to create app token request")
|
||||
}
|
||||
appTokenRequest.Header.Add("User-Agent", userAgent)
|
||||
resp, err := client.Do(appTokenRequest)
|
||||
resp, err := client.Do(appTokenRequest) // nolint: gosec
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to get app token")
|
||||
}
|
||||
resp.Body.Close()
|
||||
_ = resp.Body.Close()
|
||||
var appToken string
|
||||
for _, c := range resp.Cookies() {
|
||||
//if Org token revoked on exchange, getTokensFromEdge instead
|
||||
@@ -441,7 +514,7 @@ func GetAppTokenIfExists(appInfo *AppInfo) (string, error) {
|
||||
|
||||
// GetTokenIfExists will return the token from local storage if it exists and not expired
|
||||
func getTokenIfExists(path string) (*jose.JSONWebSignature, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
content, err := os.ReadFile(path) // nolint: gosec
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+11
-3
@@ -26,7 +26,10 @@ const (
|
||||
// The "dance" we refer to is building a HTTP request, opening that in a browser waiting for
|
||||
// the user to complete an action, while it long polls in the background waiting for an
|
||||
// action to be completed to download the resource.
|
||||
func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string, shouldEncrypt bool, useHostOnly bool, autoClose bool, fedramp bool, log *zerolog.Logger) ([]byte, error) {
|
||||
//
|
||||
// If urlFilePath is non-empty, the generated auth URL is written to that path so
|
||||
// other waiting processes can display it to the user. Pass "" to skip.
|
||||
func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string, shouldEncrypt bool, useHostOnly bool, autoClose bool, fedramp bool, log *zerolog.Logger, urlFilePath string) ([]byte, error) {
|
||||
encrypterClient, err := NewEncrypter("cloudflared_priv.pem", "cloudflared_pub.pem")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -36,6 +39,11 @@ func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// write auth URL to companion file so other waiting processes can display it
|
||||
if urlFilePath != "" {
|
||||
_ = os.WriteFile(urlFilePath, []byte(requestURL), 0600) // nolint: gosec
|
||||
}
|
||||
|
||||
// See AUTH-1423 for why we use stderr (the way git wraps ssh)
|
||||
err = OpenBrowser(requestURL)
|
||||
if err != nil {
|
||||
@@ -129,11 +137,11 @@ func poll(client *http.Client, requestURL string, log *zerolog.Logger) ([]byte,
|
||||
return nil, "", err
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
resp, err := client.Do(req)
|
||||
resp, err := client.Do(req) // nolint: gosec
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
// ignore everything other than server errors as the resource
|
||||
// may not exist until the user does the interaction
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
sudo: false
|
||||
language: go
|
||||
go:
|
||||
- 1.6
|
||||
- 1.7
|
||||
- tip
|
||||
|
||||
before_script:
|
||||
- go get github.com/GeertJohan/fgt
|
||||
- go get github.com/golang/lint/golint
|
||||
- go get golang.org/x/tools/cmd/goimports
|
||||
- go get honnef.co/go/staticcheck/cmd/staticcheck
|
||||
|
||||
script:
|
||||
- find . -name \*.go | xargs fgt goimports -l
|
||||
- fgt go vet ./...
|
||||
- fgt golint ./...
|
||||
- fgt staticcheck ./...
|
||||
- go test ./...
|
||||
|
||||
notifications:
|
||||
email:
|
||||
recipients:
|
||||
- kyle@cloudflare.com
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
Copyright (c) 2016 CloudFlare Inc.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
# backoff
|
||||
## Go implementation of "Exponential Backoff And Jitter"
|
||||
|
||||
This package implements the backoff strategy described in the AWS
|
||||
Architecture Blog article
|
||||
["Exponential Backoff And Jitter"](http://www.awsarchitectureblog.com/2015/03/backoff.html). Essentially,
|
||||
the backoff has an interval `time.Duration`; the *n<sup>th</sup>* call
|
||||
to backoff will return an a `time.Duration` that is *2 <sup>n</sup> *
|
||||
interval*. If jitter is enabled (which is the default behaviour), the
|
||||
duration is a random value between 0 and *2 <sup>n</sup> * interval*.
|
||||
The backoff is configured with a maximum duration that will not be
|
||||
exceeded; e.g., by default, the longest duration returned is
|
||||
`backoff.DefaultMaxDuration`.
|
||||
|
||||
## Usage
|
||||
|
||||
A `Backoff` is initialised with a call to `New`. Using zero values
|
||||
causes it to use `DefaultMaxDuration` and `DefaultInterval` as the
|
||||
maximum duration and interval.
|
||||
|
||||
```
|
||||
package something
|
||||
|
||||
import "github.com/cloudflare/backoff"
|
||||
|
||||
func retryable() {
|
||||
b := backoff.New(0, 0)
|
||||
for {
|
||||
err := someOperation()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
log.Printf("error in someOperation: %v", err)
|
||||
<-time.After(b.Duration())
|
||||
}
|
||||
|
||||
log.Printf("succeeded after %d tries", b.Tries()+1)
|
||||
b.Reset()
|
||||
}
|
||||
```
|
||||
|
||||
It can also be used to rate limit code that should retry infinitely, but which does not
|
||||
use `Backoff` itself.
|
||||
|
||||
```
|
||||
package something
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/backoff"
|
||||
)
|
||||
|
||||
func retryable() {
|
||||
b := backoff.New(0, 0)
|
||||
b.SetDecay(30 * time.Second)
|
||||
|
||||
for {
|
||||
// b will reset if someOperation returns later than
|
||||
// the last call to b.Duration() + 30s.
|
||||
err := someOperation()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
log.Printf("error in someOperation: %v", err)
|
||||
<-time.After(b.Duration())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tunables
|
||||
|
||||
* `NewWithoutJitter` creates a Backoff that doesn't use jitter.
|
||||
|
||||
The default behaviour is controlled by two variables:
|
||||
|
||||
* `DefaultInterval` sets the base interval for backoffs created with
|
||||
the zero `time.Duration` value in the `Interval` field.
|
||||
* `DefaultMaxDuration` sets the maximum duration for backoffs created
|
||||
with the zero `time.Duration` value in the `MaxDuration` field.
|
||||
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
// Package backoff contains an implementation of an intelligent backoff
|
||||
// strategy. It is based on the approach in the AWS architecture blog
|
||||
// article titled "Exponential Backoff And Jitter", which is found at
|
||||
// http://www.awsarchitectureblog.com/2015/03/backoff.html.
|
||||
//
|
||||
// Essentially, the backoff has an interval `time.Duration`; the nth
|
||||
// call to backoff will return a `time.Duration` that is 2^n *
|
||||
// interval. If jitter is enabled (which is the default behaviour),
|
||||
// the duration is a random value between 0 and 2^n * interval. The
|
||||
// backoff is configured with a maximum duration that will not be
|
||||
// exceeded.
|
||||
//
|
||||
// The `New` function will attempt to use the system's cryptographic
|
||||
// random number generator to seed a Go math/rand random number
|
||||
// source. If this fails, the package will panic on startup.
|
||||
package backoff
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math"
|
||||
mrand "math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var prngMu sync.Mutex
|
||||
var prng *mrand.Rand
|
||||
|
||||
// DefaultInterval is used when a Backoff is initialised with a
|
||||
// zero-value Interval.
|
||||
var DefaultInterval = 5 * time.Minute
|
||||
|
||||
// DefaultMaxDuration is maximum amount of time that the backoff will
|
||||
// delay for.
|
||||
var DefaultMaxDuration = 6 * time.Hour
|
||||
|
||||
// A Backoff contains the information needed to intelligently backoff
|
||||
// and retry operations using an exponential backoff algorithm. It should
|
||||
// be initialised with a call to `New`.
|
||||
//
|
||||
// Only use a Backoff from a single goroutine, it is not safe for concurrent
|
||||
// access.
|
||||
type Backoff struct {
|
||||
// maxDuration is the largest possible duration that can be
|
||||
// returned from a call to Duration.
|
||||
maxDuration time.Duration
|
||||
|
||||
// interval controls the time step for backing off.
|
||||
interval time.Duration
|
||||
|
||||
// noJitter controls whether to use the "Full Jitter"
|
||||
// improvement to attempt to smooth out spikes in a high
|
||||
// contention scenario. If noJitter is set to true, no
|
||||
// jitter will be introduced.
|
||||
noJitter bool
|
||||
|
||||
// decay controls the decay of n. If it is non-zero, n is
|
||||
// reset if more than the last backoff + decay has elapsed since
|
||||
// the last try.
|
||||
decay time.Duration
|
||||
|
||||
n uint64
|
||||
lastTry time.Time
|
||||
}
|
||||
|
||||
// New creates a new backoff with the specified max duration and
|
||||
// interval. Zero values may be used to use the default values.
|
||||
//
|
||||
// Panics if either max or interval is negative.
|
||||
func New(max time.Duration, interval time.Duration) *Backoff {
|
||||
if max < 0 || interval < 0 {
|
||||
panic("backoff: max or interval is negative")
|
||||
}
|
||||
|
||||
b := &Backoff{
|
||||
maxDuration: max,
|
||||
interval: interval,
|
||||
}
|
||||
b.setup()
|
||||
return b
|
||||
}
|
||||
|
||||
// NewWithoutJitter works similarly to New, except that the created
|
||||
// Backoff will not use jitter.
|
||||
func NewWithoutJitter(max time.Duration, interval time.Duration) *Backoff {
|
||||
b := New(max, interval)
|
||||
b.noJitter = true
|
||||
return b
|
||||
}
|
||||
|
||||
func init() {
|
||||
var buf [8]byte
|
||||
var n int64
|
||||
|
||||
_, err := io.ReadFull(rand.Reader, buf[:])
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
n = int64(binary.LittleEndian.Uint64(buf[:]))
|
||||
|
||||
src := mrand.NewSource(n)
|
||||
prng = mrand.New(src)
|
||||
}
|
||||
|
||||
func (b *Backoff) setup() {
|
||||
if b.interval == 0 {
|
||||
b.interval = DefaultInterval
|
||||
}
|
||||
|
||||
if b.maxDuration == 0 {
|
||||
b.maxDuration = DefaultMaxDuration
|
||||
}
|
||||
}
|
||||
|
||||
// Duration returns a time.Duration appropriate for the backoff,
|
||||
// incrementing the attempt counter.
|
||||
func (b *Backoff) Duration() time.Duration {
|
||||
b.setup()
|
||||
|
||||
b.decayN()
|
||||
|
||||
t := b.duration(b.n)
|
||||
|
||||
if b.n < math.MaxUint64 {
|
||||
b.n++
|
||||
}
|
||||
|
||||
if !b.noJitter {
|
||||
prngMu.Lock()
|
||||
t = time.Duration(prng.Int63n(int64(t)))
|
||||
prngMu.Unlock()
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// requires b to be locked.
|
||||
func (b *Backoff) duration(n uint64) (t time.Duration) {
|
||||
// Saturate pow
|
||||
pow := time.Duration(math.MaxInt64)
|
||||
if n < 63 {
|
||||
pow = 1 << n
|
||||
}
|
||||
|
||||
t = b.interval * pow
|
||||
if t/pow != b.interval || t > b.maxDuration {
|
||||
t = b.maxDuration
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Reset resets the attempt counter of a backoff.
|
||||
//
|
||||
// It should be called when the rate-limited action succeeds.
|
||||
func (b *Backoff) Reset() {
|
||||
b.lastTry = time.Time{}
|
||||
b.n = 0
|
||||
}
|
||||
|
||||
// SetDecay sets the duration after which the try counter will be reset.
|
||||
// Panics if decay is smaller than 0.
|
||||
//
|
||||
// The decay only kicks in if at least the last backoff + decay has elapsed
|
||||
// since the last try.
|
||||
func (b *Backoff) SetDecay(decay time.Duration) {
|
||||
if decay < 0 {
|
||||
panic("backoff: decay < 0")
|
||||
}
|
||||
|
||||
b.decay = decay
|
||||
}
|
||||
|
||||
// requires b to be locked
|
||||
func (b *Backoff) decayN() {
|
||||
if b.decay == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if b.lastTry.IsZero() {
|
||||
b.lastTry = time.Now()
|
||||
return
|
||||
}
|
||||
|
||||
lastDuration := b.duration(b.n - 1)
|
||||
decayed := time.Since(b.lastTry) > lastDuration+b.decay
|
||||
b.lastTry = time.Now()
|
||||
|
||||
if !decayed {
|
||||
return
|
||||
}
|
||||
|
||||
b.n = 0
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
*~
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
# purego
|
||||
[](https://pkg.go.dev/github.com/ebitengine/purego?GOOS=darwin)
|
||||
|
||||
A library for calling C functions from Go without Cgo.
|
||||
|
||||
> This is beta software so expect bugs and potentially API breaking changes
|
||||
> but each release will be tagged to avoid breaking people's code.
|
||||
> Bug reports are encouraged.
|
||||
|
||||
## Motivation
|
||||
|
||||
The [Ebitengine](https://github.com/hajimehoshi/ebiten) game engine was ported to use only Go on Windows. This enabled
|
||||
cross-compiling to Windows from any other operating system simply by setting `GOOS=windows`. The purego project was
|
||||
born to bring that same vision to the other platforms supported by Ebitengine.
|
||||
|
||||
## Benefits
|
||||
|
||||
- **Simple Cross-Compilation**: No C means you can build for other platforms easily without a C compiler.
|
||||
- **Faster Compilation**: Efficiently cache your entirely Go builds.
|
||||
- **Smaller Binaries**: Using Cgo generates a C wrapper function for each C function called. Purego doesn't!
|
||||
- **Dynamic Linking**: Load symbols at runtime and use it as a plugin system.
|
||||
- **Foreign Function Interface**: Call into other languages that are compiled into shared objects.
|
||||
- **Cgo Fallback**: Works even with CGO_ENABLED=1 so incremental porting is possible.
|
||||
This also means unsupported GOARCHs (freebsd/riscv64, linux/mips, etc.) will still work
|
||||
except for float arguments and return values.
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
### Tier 1
|
||||
|
||||
Tier 1 platforms are the primary targets officially supported by PureGo. When a new version of PureGo is released, any critical bugs found on Tier 1 platforms are treated as release blockers. The release will be postponed until such issues are resolved.
|
||||
|
||||
- **Android**: amd64<sup>1</sup>, arm64<sup>1</sup>
|
||||
- **iOS**: amd64<sup>1</sup>, arm64<sup>1</sup>
|
||||
- **Linux**: amd64, arm64
|
||||
- **macOS**: amd64, arm64
|
||||
- **Windows**: amd64, arm64
|
||||
|
||||
### Tier 2
|
||||
|
||||
Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs on Tier 2 platforms do not block new PureGo releases. However, fixes contributed by external contributors are very welcome and encouraged.
|
||||
|
||||
- **Android**: 386<sup>1</sup>, arm<sup>1</sup>
|
||||
- **FreeBSD**: amd64<sup>2</sup>, arm64<sup>2</sup>
|
||||
- **Linux**: 386, arm, loong64, ppc64le, riscv64, s390x<sup>1</sup>
|
||||
- **Windows**: 386<sup>3</sup>, arm<sup>3,4</sup>
|
||||
|
||||
#### Support Notes
|
||||
|
||||
1. These architectures require CGO_ENABLED=1 to compile
|
||||
2. These architectures require the special flag `-gcflags="github.com/ebitengine/purego/internal/fakecgo=-std"` to compile with CGO_ENABLED=0
|
||||
3. These architectures only support `SyscallN` and `NewCallback`
|
||||
4. These architectures are no longer supported as of Go 1.26
|
||||
|
||||
## Example
|
||||
|
||||
The example below only showcases purego use for macOS and Linux. The other platforms require special handling which can
|
||||
be seen in the complete example at [examples/libc](https://github.com/ebitengine/purego/tree/main/examples/libc) which supports FreeBSD and Windows.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/ebitengine/purego"
|
||||
)
|
||||
|
||||
func getSystemLibrary() string {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return "/usr/lib/libSystem.B.dylib"
|
||||
case "linux":
|
||||
return "libc.so.6"
|
||||
default:
|
||||
panic(fmt.Errorf("GOOS=%s is not supported", runtime.GOOS))
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
libc, err := purego.Dlopen(getSystemLibrary(), purego.RTLD_NOW|purego.RTLD_GLOBAL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var puts func(string)
|
||||
purego.RegisterLibFunc(&puts, libc, "puts")
|
||||
puts("Calling C from Go without Cgo!")
|
||||
}
|
||||
```
|
||||
|
||||
Then to run: `CGO_ENABLED=0 go run main.go`
|
||||
|
||||
## Questions
|
||||
|
||||
If you have questions about how to incorporate purego in your project or want to discuss
|
||||
how it works join the [Discord](https://discord.gg/HzGZVD6BkY)!
|
||||
|
||||
### External Code
|
||||
|
||||
Purego uses code that originates from the Go runtime. These files are under the BSD-3
|
||||
License that can be found [in the Go Source](https://github.com/golang/go/blob/master/LICENSE).
|
||||
This is a list of the copied files:
|
||||
|
||||
* `abi_*.h` from package `runtime/cgo`
|
||||
* `wincallback.go` from package `runtime`
|
||||
* `zcallback_darwin_*.s` from package `runtime`
|
||||
* `internal/fakecgo/abi_*.h` from package `runtime/cgo`
|
||||
* `internal/fakecgo/asm_GOARCH.s` from package `runtime/cgo`
|
||||
* `internal/fakecgo/callbacks.go` from package `runtime/cgo`
|
||||
* `internal/fakecgo/iscgo.go` from package `runtime/cgo`
|
||||
* `internal/fakecgo/setenv.go` from package `runtime/cgo`
|
||||
* `internal/fakecgo/freebsd.go` from package `runtime/cgo`
|
||||
* `internal/fakecgo/netbsd.go` from package `runtime/cgo`
|
||||
|
||||
The `internal/fakecgo/go_GOOS.go` files were modified from `runtime/cgo/gcc_GOOS_GOARCH.go`.
|
||||
|
||||
The files `abi_*.h` and `internal/fakecgo/abi_*.h` are the same because Bazel does not support cross-package use of
|
||||
`#include` so we need each one once per package. (cf. [issue](https://github.com/bazelbuild/rules_go/issues/3636))
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
// Copyright 2021 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Macros for transitioning from the host ABI to Go ABI0.
|
||||
//
|
||||
// These save the frame pointer, so in general, functions that use
|
||||
// these should have zero frame size to suppress the automatic frame
|
||||
// pointer, though it's harmless to not do this.
|
||||
|
||||
#ifdef GOOS_windows
|
||||
|
||||
// REGS_HOST_TO_ABI0_STACK is the stack bytes used by
|
||||
// PUSH_REGS_HOST_TO_ABI0.
|
||||
#define REGS_HOST_TO_ABI0_STACK (28*8 + 8)
|
||||
|
||||
// PUSH_REGS_HOST_TO_ABI0 prepares for transitioning from
|
||||
// the host ABI to Go ABI0 code. It saves all registers that are
|
||||
// callee-save in the host ABI and caller-save in Go ABI0 and prepares
|
||||
// for entry to Go.
|
||||
//
|
||||
// Save DI SI BP BX R12 R13 R14 R15 X6-X15 registers and the DF flag.
|
||||
// Clear the DF flag for the Go ABI.
|
||||
// MXCSR matches the Go ABI, so we don't have to set that,
|
||||
// and Go doesn't modify it, so we don't have to save it.
|
||||
#define PUSH_REGS_HOST_TO_ABI0() \
|
||||
PUSHFQ \
|
||||
CLD \
|
||||
ADJSP $(REGS_HOST_TO_ABI0_STACK - 8) \
|
||||
MOVQ DI, (0*0)(SP) \
|
||||
MOVQ SI, (1*8)(SP) \
|
||||
MOVQ BP, (2*8)(SP) \
|
||||
MOVQ BX, (3*8)(SP) \
|
||||
MOVQ R12, (4*8)(SP) \
|
||||
MOVQ R13, (5*8)(SP) \
|
||||
MOVQ R14, (6*8)(SP) \
|
||||
MOVQ R15, (7*8)(SP) \
|
||||
MOVUPS X6, (8*8)(SP) \
|
||||
MOVUPS X7, (10*8)(SP) \
|
||||
MOVUPS X8, (12*8)(SP) \
|
||||
MOVUPS X9, (14*8)(SP) \
|
||||
MOVUPS X10, (16*8)(SP) \
|
||||
MOVUPS X11, (18*8)(SP) \
|
||||
MOVUPS X12, (20*8)(SP) \
|
||||
MOVUPS X13, (22*8)(SP) \
|
||||
MOVUPS X14, (24*8)(SP) \
|
||||
MOVUPS X15, (26*8)(SP)
|
||||
|
||||
#define POP_REGS_HOST_TO_ABI0() \
|
||||
MOVQ (0*0)(SP), DI \
|
||||
MOVQ (1*8)(SP), SI \
|
||||
MOVQ (2*8)(SP), BP \
|
||||
MOVQ (3*8)(SP), BX \
|
||||
MOVQ (4*8)(SP), R12 \
|
||||
MOVQ (5*8)(SP), R13 \
|
||||
MOVQ (6*8)(SP), R14 \
|
||||
MOVQ (7*8)(SP), R15 \
|
||||
MOVUPS (8*8)(SP), X6 \
|
||||
MOVUPS (10*8)(SP), X7 \
|
||||
MOVUPS (12*8)(SP), X8 \
|
||||
MOVUPS (14*8)(SP), X9 \
|
||||
MOVUPS (16*8)(SP), X10 \
|
||||
MOVUPS (18*8)(SP), X11 \
|
||||
MOVUPS (20*8)(SP), X12 \
|
||||
MOVUPS (22*8)(SP), X13 \
|
||||
MOVUPS (24*8)(SP), X14 \
|
||||
MOVUPS (26*8)(SP), X15 \
|
||||
ADJSP $-(REGS_HOST_TO_ABI0_STACK - 8) \
|
||||
POPFQ
|
||||
|
||||
#else
|
||||
// SysV ABI
|
||||
|
||||
#define REGS_HOST_TO_ABI0_STACK (6*8)
|
||||
|
||||
// SysV MXCSR matches the Go ABI, so we don't have to set that,
|
||||
// and Go doesn't modify it, so we don't have to save it.
|
||||
// Both SysV and Go require DF to be cleared, so that's already clear.
|
||||
// The SysV and Go frame pointer conventions are compatible.
|
||||
#define PUSH_REGS_HOST_TO_ABI0() \
|
||||
ADJSP $(REGS_HOST_TO_ABI0_STACK) \
|
||||
MOVQ BP, (5*8)(SP) \
|
||||
LEAQ (5*8)(SP), BP \
|
||||
MOVQ BX, (0*8)(SP) \
|
||||
MOVQ R12, (1*8)(SP) \
|
||||
MOVQ R13, (2*8)(SP) \
|
||||
MOVQ R14, (3*8)(SP) \
|
||||
MOVQ R15, (4*8)(SP)
|
||||
|
||||
#define POP_REGS_HOST_TO_ABI0() \
|
||||
MOVQ (0*8)(SP), BX \
|
||||
MOVQ (1*8)(SP), R12 \
|
||||
MOVQ (2*8)(SP), R13 \
|
||||
MOVQ (3*8)(SP), R14 \
|
||||
MOVQ (4*8)(SP), R15 \
|
||||
MOVQ (5*8)(SP), BP \
|
||||
ADJSP $-(REGS_HOST_TO_ABI0_STACK)
|
||||
|
||||
#endif
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Copyright 2021 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Macros for transitioning from the host ABI to Go ABI0.
|
||||
//
|
||||
// These macros save and restore the callee-saved registers
|
||||
// from the stack, but they don't adjust stack pointer, so
|
||||
// the user should prepare stack space in advance.
|
||||
// SAVE_R19_TO_R28(offset) saves R19 ~ R28 to the stack space
|
||||
// of ((offset)+0*8)(RSP) ~ ((offset)+9*8)(RSP).
|
||||
//
|
||||
// SAVE_F8_TO_F15(offset) saves F8 ~ F15 to the stack space
|
||||
// of ((offset)+0*8)(RSP) ~ ((offset)+7*8)(RSP).
|
||||
//
|
||||
// R29 is not saved because Go will save and restore it.
|
||||
|
||||
#define SAVE_R19_TO_R28(offset) \
|
||||
STP (R19, R20), ((offset)+0*8)(RSP) \
|
||||
STP (R21, R22), ((offset)+2*8)(RSP) \
|
||||
STP (R23, R24), ((offset)+4*8)(RSP) \
|
||||
STP (R25, R26), ((offset)+6*8)(RSP) \
|
||||
STP (R27, g), ((offset)+8*8)(RSP)
|
||||
#define RESTORE_R19_TO_R28(offset) \
|
||||
LDP ((offset)+0*8)(RSP), (R19, R20) \
|
||||
LDP ((offset)+2*8)(RSP), (R21, R22) \
|
||||
LDP ((offset)+4*8)(RSP), (R23, R24) \
|
||||
LDP ((offset)+6*8)(RSP), (R25, R26) \
|
||||
LDP ((offset)+8*8)(RSP), (R27, g) /* R28 */
|
||||
#define SAVE_F8_TO_F15(offset) \
|
||||
FSTPD (F8, F9), ((offset)+0*8)(RSP) \
|
||||
FSTPD (F10, F11), ((offset)+2*8)(RSP) \
|
||||
FSTPD (F12, F13), ((offset)+4*8)(RSP) \
|
||||
FSTPD (F14, F15), ((offset)+6*8)(RSP)
|
||||
#define RESTORE_F8_TO_F15(offset) \
|
||||
FLDPD ((offset)+0*8)(RSP), (F8, F9) \
|
||||
FLDPD ((offset)+2*8)(RSP), (F10, F11) \
|
||||
FLDPD ((offset)+4*8)(RSP), (F12, F13) \
|
||||
FLDPD ((offset)+6*8)(RSP), (F14, F15)
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Macros for transitioning from the host ABI to Go ABI0.
|
||||
//
|
||||
// These macros save and restore the callee-saved registers
|
||||
// from the stack, but they don't adjust stack pointer, so
|
||||
// the user should prepare stack space in advance.
|
||||
// SAVE_R22_TO_R31(offset) saves R22 ~ R31 to the stack space
|
||||
// of ((offset)+0*8)(R3) ~ ((offset)+9*8)(R3).
|
||||
//
|
||||
// SAVE_F24_TO_F31(offset) saves F24 ~ F31 to the stack space
|
||||
// of ((offset)+0*8)(R3) ~ ((offset)+7*8)(R3).
|
||||
//
|
||||
// Note: g is R22
|
||||
|
||||
#define SAVE_R22_TO_R31(offset) \
|
||||
MOVV g, ((offset)+(0*8))(R3) \
|
||||
MOVV R23, ((offset)+(1*8))(R3) \
|
||||
MOVV R24, ((offset)+(2*8))(R3) \
|
||||
MOVV R25, ((offset)+(3*8))(R3) \
|
||||
MOVV R26, ((offset)+(4*8))(R3) \
|
||||
MOVV R27, ((offset)+(5*8))(R3) \
|
||||
MOVV R28, ((offset)+(6*8))(R3) \
|
||||
MOVV R29, ((offset)+(7*8))(R3) \
|
||||
MOVV R30, ((offset)+(8*8))(R3) \
|
||||
MOVV R31, ((offset)+(9*8))(R3)
|
||||
|
||||
#define SAVE_F24_TO_F31(offset) \
|
||||
MOVD F24, ((offset)+(0*8))(R3) \
|
||||
MOVD F25, ((offset)+(1*8))(R3) \
|
||||
MOVD F26, ((offset)+(2*8))(R3) \
|
||||
MOVD F27, ((offset)+(3*8))(R3) \
|
||||
MOVD F28, ((offset)+(4*8))(R3) \
|
||||
MOVD F29, ((offset)+(5*8))(R3) \
|
||||
MOVD F30, ((offset)+(6*8))(R3) \
|
||||
MOVD F31, ((offset)+(7*8))(R3)
|
||||
|
||||
#define RESTORE_R22_TO_R31(offset) \
|
||||
MOVV ((offset)+(0*8))(R3), g \
|
||||
MOVV ((offset)+(1*8))(R3), R23 \
|
||||
MOVV ((offset)+(2*8))(R3), R24 \
|
||||
MOVV ((offset)+(3*8))(R3), R25 \
|
||||
MOVV ((offset)+(4*8))(R3), R26 \
|
||||
MOVV ((offset)+(5*8))(R3), R27 \
|
||||
MOVV ((offset)+(6*8))(R3), R28 \
|
||||
MOVV ((offset)+(7*8))(R3), R29 \
|
||||
MOVV ((offset)+(8*8))(R3), R30 \
|
||||
MOVV ((offset)+(9*8))(R3), R31
|
||||
|
||||
#define RESTORE_F24_TO_F31(offset) \
|
||||
MOVD ((offset)+(0*8))(R3), F24 \
|
||||
MOVD ((offset)+(1*8))(R3), F25 \
|
||||
MOVD ((offset)+(2*8))(R3), F26 \
|
||||
MOVD ((offset)+(3*8))(R3), F27 \
|
||||
MOVD ((offset)+(4*8))(R3), F28 \
|
||||
MOVD ((offset)+(5*8))(R3), F29 \
|
||||
MOVD ((offset)+(6*8))(R3), F30 \
|
||||
MOVD ((offset)+(7*8))(R3), F31
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
|
||||
|
||||
//go:build cgo && (darwin || freebsd || linux || netbsd)
|
||||
|
||||
package purego
|
||||
|
||||
// if CGO_ENABLED=1 import the Cgo runtime to ensure that it is set up properly.
|
||||
// This is required since some frameworks need TLS setup the C way which Go doesn't do.
|
||||
// We currently don't support ios in fakecgo mode so force Cgo or fail.
|
||||
// Even if CGO_ENABLED=1 the Cgo runtime is not imported unless `import "C"` is used,
|
||||
// which will import this package automatically. Normally this isn't an issue since it
|
||||
// usually isn't possible to call into C without using that import. However, with purego
|
||||
// it is since we don't use `import "C"`!
|
||||
import (
|
||||
_ "runtime/cgo"
|
||||
|
||||
_ "github.com/ebitengine/purego/internal/cgo"
|
||||
)
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 The Ebitengine Authors
|
||||
|
||||
//go:build darwin || freebsd || linux || netbsd
|
||||
|
||||
package purego
|
||||
|
||||
// Dlerror represents an error value returned from Dlopen, Dlsym, or Dlclose.
|
||||
//
|
||||
// This type is not available on Windows as there is no counterpart to it on Windows.
|
||||
type Dlerror struct {
|
||||
s string
|
||||
}
|
||||
|
||||
func (e Dlerror) Error() string {
|
||||
return e.s
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
|
||||
|
||||
//go:build (darwin || freebsd || linux || netbsd) && !android && !faketime
|
||||
|
||||
package purego
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Unix Specification for dlfcn.h: https://pubs.opengroup.org/onlinepubs/7908799/xsh/dlfcn.h.html
|
||||
|
||||
var (
|
||||
fnDlopen func(path string, mode int) uintptr
|
||||
fnDlsym func(handle uintptr, name string) uintptr
|
||||
fnDlerror func() string
|
||||
fnDlclose func(handle uintptr) bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterFunc(&fnDlopen, dlopenABI0)
|
||||
RegisterFunc(&fnDlsym, dlsymABI0)
|
||||
RegisterFunc(&fnDlerror, dlerrorABI0)
|
||||
RegisterFunc(&fnDlclose, dlcloseABI0)
|
||||
}
|
||||
|
||||
// Dlopen examines the dynamic library or bundle file specified by path. If the file is compatible
|
||||
// with the current process and has not already been loaded into the
|
||||
// current process, it is loaded and linked. After being linked, if it contains
|
||||
// any initializer functions, they are called, before Dlopen
|
||||
// returns. It returns a handle that can be used with Dlsym and Dlclose.
|
||||
// A second call to Dlopen with the same path will return the same handle, but the internal
|
||||
// reference count for the handle will be incremented. Therefore, all
|
||||
// Dlopen calls should be balanced with a Dlclose call.
|
||||
//
|
||||
// This function is not available on Windows.
|
||||
// Use [golang.org/x/sys/windows.LoadLibrary], [golang.org/x/sys/windows.LoadLibraryEx],
|
||||
// [golang.org/x/sys/windows.NewLazyDLL], or [golang.org/x/sys/windows.NewLazySystemDLL] for Windows instead.
|
||||
func Dlopen(path string, mode int) (uintptr, error) {
|
||||
u := fnDlopen(path, mode)
|
||||
if u == 0 {
|
||||
return 0, Dlerror{fnDlerror()}
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Dlsym takes a "handle" of a dynamic library returned by Dlopen and the symbol name.
|
||||
// It returns the address where that symbol is loaded into memory. If the symbol is not found,
|
||||
// in the specified library or any of the libraries that were automatically loaded by Dlopen
|
||||
// when that library was loaded, Dlsym returns zero.
|
||||
//
|
||||
// This function is not available on Windows.
|
||||
// Use [golang.org/x/sys/windows.GetProcAddress] for Windows instead.
|
||||
func Dlsym(handle uintptr, name string) (uintptr, error) {
|
||||
u := fnDlsym(handle, name)
|
||||
if u == 0 {
|
||||
return 0, Dlerror{fnDlerror()}
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Dlclose decrements the reference count on the dynamic library handle.
|
||||
// If the reference count drops to zero and no other loaded libraries
|
||||
// use symbols in it, then the dynamic library is unloaded.
|
||||
//
|
||||
// This function is not available on Windows.
|
||||
// Use [golang.org/x/sys/windows.FreeLibrary] for Windows instead.
|
||||
func Dlclose(handle uintptr) error {
|
||||
if fnDlclose(handle) {
|
||||
return Dlerror{fnDlerror()}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadSymbol(handle uintptr, name string) (uintptr, error) {
|
||||
return Dlsym(handle, name)
|
||||
}
|
||||
|
||||
// these functions exist in dlfcn_stubs.s and are calling C functions linked to in dlfcn_GOOS.go
|
||||
// the indirection is necessary because a function is actually a pointer to the pointer to the code.
|
||||
// sadly, I do not know of anyway to remove the assembly stubs entirely because //go:linkname doesn't
|
||||
// appear to work if you link directly to the C function on darwin arm64.
|
||||
|
||||
//go:linkname dlopen dlopen
|
||||
var dlopen uint8
|
||||
var dlopenABI0 = uintptr(unsafe.Pointer(&dlopen))
|
||||
|
||||
//go:linkname dlsym dlsym
|
||||
var dlsym uint8
|
||||
var dlsymABI0 = uintptr(unsafe.Pointer(&dlsym))
|
||||
|
||||
//go:linkname dlclose dlclose
|
||||
var dlclose uint8
|
||||
var dlcloseABI0 = uintptr(unsafe.Pointer(&dlclose))
|
||||
|
||||
//go:linkname dlerror dlerror
|
||||
var dlerror uint8
|
||||
var dlerrorABI0 = uintptr(unsafe.Pointer(&dlerror))
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2024 The Ebitengine Authors
|
||||
|
||||
package purego
|
||||
|
||||
import "github.com/ebitengine/purego/internal/cgo"
|
||||
|
||||
// Source for constants: https://android.googlesource.com/platform/bionic/+/refs/heads/main/libc/include/dlfcn.h
|
||||
|
||||
const (
|
||||
is64bit = 1 << (^uintptr(0) >> 63) / 2
|
||||
is32bit = 1 - is64bit
|
||||
RTLD_DEFAULT = is32bit * 0xffffffff
|
||||
RTLD_LAZY = 0x00000001
|
||||
RTLD_NOW = is64bit * 0x00000002
|
||||
RTLD_LOCAL = 0x00000000
|
||||
RTLD_GLOBAL = is64bit*0x00100 | is32bit*0x00000002
|
||||
)
|
||||
|
||||
func Dlopen(path string, mode int) (uintptr, error) {
|
||||
return cgo.Dlopen(path, mode)
|
||||
}
|
||||
|
||||
func Dlsym(handle uintptr, name string) (uintptr, error) {
|
||||
return cgo.Dlsym(handle, name)
|
||||
}
|
||||
|
||||
func Dlclose(handle uintptr) error {
|
||||
return cgo.Dlclose(handle)
|
||||
}
|
||||
|
||||
func loadSymbol(handle uintptr, name string) (uintptr, error) {
|
||||
return Dlsym(handle, name)
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
|
||||
|
||||
package purego
|
||||
|
||||
// Source for constants: https://opensource.apple.com/source/dyld/dyld-360.14/include/dlfcn.h.auto.html
|
||||
|
||||
const (
|
||||
RTLD_DEFAULT = 1<<64 - 2 // Pseudo-handle for dlsym so search for any loaded symbol
|
||||
RTLD_LAZY = 0x1 // Relocations are performed at an implementation-dependent time.
|
||||
RTLD_NOW = 0x2 // Relocations are performed when the object is loaded.
|
||||
RTLD_LOCAL = 0x4 // All symbols are not made available for relocation processing by other modules.
|
||||
RTLD_GLOBAL = 0x8 // All symbols are available for relocation processing of other modules.
|
||||
)
|
||||
|
||||
//go:cgo_import_dynamic purego_dlopen dlopen "/usr/lib/libSystem.B.dylib"
|
||||
//go:cgo_import_dynamic purego_dlsym dlsym "/usr/lib/libSystem.B.dylib"
|
||||
//go:cgo_import_dynamic purego_dlerror dlerror "/usr/lib/libSystem.B.dylib"
|
||||
//go:cgo_import_dynamic purego_dlclose dlclose "/usr/lib/libSystem.B.dylib"
|
||||
//go:cgo_import_dynamic purego_error __error "/usr/lib/libSystem.B.dylib"
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
|
||||
|
||||
package purego
|
||||
|
||||
// Constants as defined in https://github.com/freebsd/freebsd-src/blob/main/include/dlfcn.h
|
||||
const (
|
||||
intSize = 32 << (^uint(0) >> 63) // 32 or 64
|
||||
RTLD_DEFAULT = 1<<intSize - 2 // Pseudo-handle for dlsym so search for any loaded symbol
|
||||
RTLD_LAZY = 0x00000001 // Relocations are performed at an implementation-dependent time.
|
||||
RTLD_NOW = 0x00000002 // Relocations are performed when the object is loaded.
|
||||
RTLD_LOCAL = 0x00000000 // All symbols are not made available for relocation processing by other modules.
|
||||
RTLD_GLOBAL = 0x00000100 // All symbols are available for relocation processing of other modules.
|
||||
)
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
|
||||
|
||||
//go:build !android
|
||||
|
||||
package purego
|
||||
|
||||
// Source for constants: https://codebrowser.dev/glibc/glibc/bits/dlfcn.h.html
|
||||
|
||||
const (
|
||||
RTLD_DEFAULT = 0x00000 // Pseudo-handle for dlsym so search for any loaded symbol
|
||||
RTLD_LAZY = 0x00001 // Relocations are performed at an implementation-dependent time.
|
||||
RTLD_NOW = 0x00002 // Relocations are performed when the object is loaded.
|
||||
RTLD_LOCAL = 0x00000 // All symbols are not made available for relocation processing by other modules.
|
||||
RTLD_GLOBAL = 0x00100 // All symbols are available for relocation processing of other modules.
|
||||
)
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
|
||||
|
||||
package purego
|
||||
|
||||
// Source for constants: https://github.com/NetBSD/src/blob/trunk/include/dlfcn.h
|
||||
|
||||
const (
|
||||
intSize = 32 << (^uint(0) >> 63) // 32 or 64
|
||||
RTLD_DEFAULT = 1<<intSize - 2 // Pseudo-handle for dlsym so search for any loaded symbol
|
||||
RTLD_LAZY = 0x00000001 // Relocations are performed at an implementation-dependent time.
|
||||
RTLD_NOW = 0x00000002 // Relocations are performed when the object is loaded.
|
||||
RTLD_LOCAL = 0x00000000 // All symbols are not made available for relocation processing by other modules.
|
||||
RTLD_GLOBAL = 0x00000100 // All symbols are available for relocation processing of other modules.
|
||||
)
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
|
||||
|
||||
//go:build !cgo
|
||||
|
||||
package purego
|
||||
|
||||
//go:cgo_import_dynamic purego_dlopen dlopen "libc.so.7"
|
||||
//go:cgo_import_dynamic purego_dlsym dlsym "libc.so.7"
|
||||
//go:cgo_import_dynamic purego_dlerror dlerror "libc.so.7"
|
||||
//go:cgo_import_dynamic purego_dlclose dlclose "libc.so.7"
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
|
||||
|
||||
//go:build !cgo && !faketime
|
||||
|
||||
package purego
|
||||
|
||||
// if there is no Cgo we must link to each of the functions from dlfcn.h
|
||||
// then the functions are called inside dlfcn_stubs.s
|
||||
|
||||
//go:cgo_import_dynamic purego_dlopen dlopen "libdl.so.2"
|
||||
//go:cgo_import_dynamic purego_dlsym dlsym "libdl.so.2"
|
||||
//go:cgo_import_dynamic purego_dlerror dlerror "libdl.so.2"
|
||||
//go:cgo_import_dynamic purego_dlclose dlclose "libdl.so.2"
|
||||
|
||||
// on amd64 we don't need the following line - on 386 we do...
|
||||
// anyway - with those lines the output is better (but doesn't matter) - without it on amd64 we get multiple DT_NEEDED with "libc.so.6" etc
|
||||
|
||||
//go:cgo_import_dynamic _ _ "libdl.so.2"
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
|
||||
|
||||
package purego
|
||||
|
||||
//go:cgo_import_dynamic purego_dlopen dlopen "libc.so"
|
||||
//go:cgo_import_dynamic purego_dlsym dlsym "libc.so"
|
||||
//go:cgo_import_dynamic purego_dlerror dlerror "libc.so"
|
||||
//go:cgo_import_dynamic purego_dlclose dlclose "libc.so"
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2024 The Ebitengine Authors
|
||||
|
||||
//go:build faketime
|
||||
|
||||
package purego
|
||||
|
||||
import "errors"
|
||||
|
||||
func Dlopen(path string, mode int) (uintptr, error) {
|
||||
return 0, errors.New("Dlopen is not supported in the playground")
|
||||
}
|
||||
|
||||
func Dlsym(handle uintptr, name string) (uintptr, error) {
|
||||
return 0, errors.New("Dlsym is not supported in the playground")
|
||||
}
|
||||
|
||||
func Dlclose(handle uintptr) error {
|
||||
return errors.New("Dlclose is not supported in the playground")
|
||||
}
|
||||
|
||||
func loadSymbol(handle uintptr, name string) (uintptr, error) {
|
||||
return Dlsym(handle, name)
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
|
||||
|
||||
//go:build darwin || !cgo && (freebsd || linux || netbsd) && !faketime
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
// func dlopen(path *byte, mode int) (ret uintptr)
|
||||
TEXT dlopen(SB), NOSPLIT|NOFRAME, $0-0
|
||||
JMP purego_dlopen(SB)
|
||||
|
||||
// func dlsym(handle uintptr, symbol *byte) (ret uintptr)
|
||||
TEXT dlsym(SB), NOSPLIT|NOFRAME, $0-0
|
||||
JMP purego_dlsym(SB)
|
||||
|
||||
// func dlerror() (ret *byte)
|
||||
TEXT dlerror(SB), NOSPLIT|NOFRAME, $0-0
|
||||
JMP purego_dlerror(SB)
|
||||
|
||||
// func dlclose(handle uintptr) (ret int)
|
||||
TEXT dlclose(SB), NOSPLIT|NOFRAME, $0-0
|
||||
JMP purego_dlclose(SB)
|
||||
+571
@@ -0,0 +1,571 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
|
||||
|
||||
//go:build darwin || freebsd || linux || netbsd || windows
|
||||
|
||||
package purego
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/ebitengine/purego/internal/strings"
|
||||
"github.com/ebitengine/purego/internal/xreflect"
|
||||
)
|
||||
|
||||
const (
|
||||
align8ByteMask = 7 // Mask for 8-byte alignment: (val + 7) &^ 7
|
||||
align8ByteSize = 8 // 8-byte alignment boundary
|
||||
)
|
||||
|
||||
var thePool = sync.Pool{New: func() any {
|
||||
return new(syscall15Args)
|
||||
}}
|
||||
|
||||
// RegisterLibFunc is a wrapper around RegisterFunc that uses the C function returned from Dlsym(handle, name).
|
||||
// It panics if it can't find the name symbol.
|
||||
func RegisterLibFunc(fptr any, handle uintptr, name string) {
|
||||
sym, err := loadSymbol(handle, name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RegisterFunc(fptr, sym)
|
||||
}
|
||||
|
||||
// RegisterFunc takes a pointer to a Go function representing the calling convention of the C function.
|
||||
// fptr will be set to a function that when called will call the C function given by cfn with the
|
||||
// parameters passed in the correct registers and stack.
|
||||
//
|
||||
// A panic is produced if the type is not a function pointer or if the function returns more than 1 value.
|
||||
//
|
||||
// These conversions describe how a Go type in the fptr will be used to call
|
||||
// the C function. It is important to note that there is no way to verify that fptr
|
||||
// matches the C function. This also holds true for struct types where the padding
|
||||
// needs to be ensured to match that of C; RegisterFunc does not verify this.
|
||||
//
|
||||
// # Type Conversions (Go <=> C)
|
||||
//
|
||||
// string <=> char*
|
||||
// bool <=> _Bool
|
||||
// uintptr <=> uintptr_t
|
||||
// uint <=> uint32_t or uint64_t
|
||||
// uint8 <=> uint8_t
|
||||
// uint16 <=> uint16_t
|
||||
// uint32 <=> uint32_t
|
||||
// uint64 <=> uint64_t
|
||||
// int <=> int32_t or int64_t
|
||||
// int8 <=> int8_t
|
||||
// int16 <=> int16_t
|
||||
// int32 <=> int32_t
|
||||
// int64 <=> int64_t
|
||||
// float32 <=> float
|
||||
// float64 <=> double
|
||||
// struct <=> struct (darwin amd64/arm64, linux amd64/arm64)
|
||||
// func <=> C function
|
||||
// unsafe.Pointer, *T <=> void*
|
||||
// []T => void*
|
||||
//
|
||||
// There is a special case when the last argument of fptr is a variadic interface (or []interface}
|
||||
// it will be expanded into a call to the C function as if it had the arguments in that slice.
|
||||
// This means that using arg ...any is like a cast to the function with the arguments inside arg.
|
||||
// This is not the same as C variadic.
|
||||
//
|
||||
// # Memory
|
||||
//
|
||||
// In general it is not possible for purego to guarantee the lifetimes of objects returned or received from
|
||||
// calling functions using RegisterFunc. For arguments to a C function it is important that the C function doesn't
|
||||
// hold onto a reference to Go memory. This is the same as the [Cgo rules].
|
||||
//
|
||||
// However, there are some special cases. When passing a string as an argument if the string does not end in a null
|
||||
// terminated byte (\x00) then the string will be copied into memory maintained by purego. The memory is only valid for
|
||||
// that specific call. Therefore, if the C code keeps a reference to that string it may become invalid at some
|
||||
// undefined time. However, if the string does already contain a null-terminated byte then no copy is done.
|
||||
// It is then the responsibility of the caller to ensure the string stays alive as long as it's needed in C memory.
|
||||
// This can be done using runtime.KeepAlive or allocating the string in C memory using malloc. When a C function
|
||||
// returns a null-terminated pointer to char a Go string can be used. Purego will allocate a new string in Go memory
|
||||
// and copy the data over. This string will be garbage collected whenever Go decides it's no longer referenced.
|
||||
// This C created string will not be freed by purego. If the pointer to char is not null-terminated or must continue
|
||||
// to point to C memory (because it's a buffer for example) then use a pointer to byte and then convert that to a slice
|
||||
// using unsafe.Slice. Doing this means that it becomes the responsibility of the caller to care about the lifetime
|
||||
// of the pointer
|
||||
//
|
||||
// # Structs
|
||||
//
|
||||
// Purego can handle the most common structs that have fields of builtin types like int8, uint16, float32, etc. However,
|
||||
// it does not support aligning fields properly. It is therefore the responsibility of the caller to ensure
|
||||
// that all padding is added to the Go struct to match the C one. See `BoolStructFn` in struct_test.go for an example.
|
||||
//
|
||||
// On Darwin ARM64, purego handles proper alignment of struct arguments when passing them on the stack,
|
||||
// following the C ABI's byte-level packing rules.
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// All functions below call this C function:
|
||||
//
|
||||
// char *foo(char *str);
|
||||
//
|
||||
// // Let purego convert types
|
||||
// var foo func(s string) string
|
||||
// goString := foo("copied")
|
||||
// // Go will garbage collect this string
|
||||
//
|
||||
// // Manually, handle allocations
|
||||
// var foo2 func(b string) *byte
|
||||
// mustFree := foo2("not copied\x00")
|
||||
// defer free(mustFree)
|
||||
//
|
||||
// [Cgo rules]: https://pkg.go.dev/cmd/cgo#hdr-Go_references_to_C
|
||||
func RegisterFunc(fptr any, cfn uintptr) {
|
||||
const is32bit = unsafe.Sizeof(uintptr(0)) == 4
|
||||
fn := reflect.ValueOf(fptr).Elem()
|
||||
ty := fn.Type()
|
||||
if ty.Kind() != reflect.Func {
|
||||
panic("purego: fptr must be a function pointer")
|
||||
}
|
||||
if ty.NumOut() > 1 {
|
||||
panic("purego: function can only return zero or one values")
|
||||
}
|
||||
if cfn == 0 {
|
||||
panic("purego: cfn is nil")
|
||||
}
|
||||
if ty.NumOut() == 1 && (ty.Out(0).Kind() == reflect.Float32 || ty.Out(0).Kind() == reflect.Float64) &&
|
||||
runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
|
||||
panic("purego: float returns are not supported")
|
||||
}
|
||||
{
|
||||
// this code checks how many registers and stack this function will use
|
||||
// to avoid crashing with too many arguments
|
||||
var ints int
|
||||
var floats int
|
||||
var stack int
|
||||
for i := 0; i < ty.NumIn(); i++ {
|
||||
arg := ty.In(i)
|
||||
switch arg.Kind() {
|
||||
case reflect.Func:
|
||||
// This only does preliminary testing to ensure the CDecl argument
|
||||
// is the first argument. Full testing is done when the callback is actually
|
||||
// created in NewCallback.
|
||||
for j := 0; j < arg.NumIn(); j++ {
|
||||
in := arg.In(j)
|
||||
if !in.AssignableTo(reflect.TypeOf(CDecl{})) {
|
||||
continue
|
||||
}
|
||||
if j != 0 {
|
||||
panic("purego: CDecl must be the first argument")
|
||||
}
|
||||
}
|
||||
case reflect.String, reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Ptr, reflect.UnsafePointer,
|
||||
reflect.Slice, reflect.Bool:
|
||||
if ints < numOfIntegerRegisters() {
|
||||
ints++
|
||||
} else {
|
||||
stack++
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
if floats < numOfFloatRegisters() {
|
||||
floats++
|
||||
} else {
|
||||
stack++
|
||||
}
|
||||
case reflect.Struct:
|
||||
ensureStructSupportedForRegisterFunc()
|
||||
if arg.Size() == 0 {
|
||||
continue
|
||||
}
|
||||
addInt := func(u uintptr) {
|
||||
ints++
|
||||
}
|
||||
addFloat := func(u uintptr) {
|
||||
floats++
|
||||
}
|
||||
addStack := func(u uintptr) {
|
||||
stack++
|
||||
}
|
||||
_ = addStruct(reflect.New(arg).Elem(), &ints, &floats, &stack, addInt, addFloat, addStack, nil)
|
||||
default:
|
||||
panic("purego: unsupported kind " + arg.Kind().String())
|
||||
}
|
||||
}
|
||||
if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {
|
||||
ensureStructSupportedForRegisterFunc()
|
||||
outType := ty.Out(0)
|
||||
checkStructFieldsSupported(outType)
|
||||
if runtime.GOARCH == "amd64" && outType.Size() > maxRegAllocStructSize {
|
||||
// on amd64 if struct is bigger than 16 bytes allocate the return struct
|
||||
// and pass it in as a hidden first argument.
|
||||
ints++
|
||||
}
|
||||
}
|
||||
|
||||
sizeOfStack := maxArgs - numOfIntegerRegisters()
|
||||
// On Darwin ARM64, use byte-based validation since arguments pack efficiently.
|
||||
// See https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms
|
||||
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
|
||||
stackBytes := estimateStackBytes(ty)
|
||||
maxStackBytes := sizeOfStack * 8
|
||||
if stackBytes > maxStackBytes {
|
||||
panic("purego: too many stack arguments")
|
||||
}
|
||||
} else {
|
||||
if stack > sizeOfStack {
|
||||
panic("purego: too many stack arguments")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
|
||||
var sysargs [maxArgs]uintptr
|
||||
// Use maxArgs instead of numOfFloatRegisters() to keep this code path allocation-free,
|
||||
// since numOfFloatRegisters() is a function call, not a constant.
|
||||
// maxArgs is always greater than or equal to numOfFloatRegisters() so this is safe.
|
||||
var floats [maxArgs]uintptr
|
||||
var numInts int
|
||||
var numFloats int
|
||||
var numStack int
|
||||
var addStack, addInt, addFloat func(x uintptr)
|
||||
if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" {
|
||||
// Windows arm64 uses the same calling convention as macOS and Linux
|
||||
addStack = func(x uintptr) {
|
||||
sysargs[numOfIntegerRegisters()+numStack] = x
|
||||
numStack++
|
||||
}
|
||||
addInt = func(x uintptr) {
|
||||
if numInts >= numOfIntegerRegisters() {
|
||||
addStack(x)
|
||||
} else {
|
||||
sysargs[numInts] = x
|
||||
numInts++
|
||||
}
|
||||
}
|
||||
addFloat = func(x uintptr) {
|
||||
if numFloats < numOfFloatRegisters() {
|
||||
floats[numFloats] = x
|
||||
numFloats++
|
||||
} else {
|
||||
addStack(x)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// On Windows amd64 the arguments are passed in the numbered registered.
|
||||
// So the first int is in the first integer register and the first float
|
||||
// is in the second floating register if there is already a first int.
|
||||
// This is in contrast to how macOS and Linux pass arguments which
|
||||
// tries to use as many registers as possible in the calling convention.
|
||||
addStack = func(x uintptr) {
|
||||
sysargs[numStack] = x
|
||||
numStack++
|
||||
}
|
||||
addInt = addStack
|
||||
addFloat = addStack
|
||||
}
|
||||
|
||||
var keepAlive []any
|
||||
defer func() {
|
||||
runtime.KeepAlive(keepAlive)
|
||||
runtime.KeepAlive(args)
|
||||
}()
|
||||
|
||||
var arm64_r8 uintptr
|
||||
if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {
|
||||
outType := ty.Out(0)
|
||||
if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") && outType.Size() > maxRegAllocStructSize {
|
||||
val := reflect.New(outType)
|
||||
keepAlive = append(keepAlive, val)
|
||||
addInt(val.Pointer())
|
||||
} else if runtime.GOARCH == "arm64" && outType.Size() > maxRegAllocStructSize {
|
||||
isAllFloats, numFields := isAllSameFloat(outType)
|
||||
if !isAllFloats || numFields > 4 {
|
||||
val := reflect.New(outType)
|
||||
keepAlive = append(keepAlive, val)
|
||||
arm64_r8 = val.Pointer()
|
||||
}
|
||||
}
|
||||
}
|
||||
for i, v := range args {
|
||||
if variadic, ok := xreflect.TypeAssert[[]any](args[i]); ok {
|
||||
if i != len(args)-1 {
|
||||
panic("purego: can only expand last parameter")
|
||||
}
|
||||
for _, x := range variadic {
|
||||
keepAlive = addValue(reflect.ValueOf(x), keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Check if we need to start Darwin ARM64 C-style stack packing
|
||||
if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && shouldBundleStackArgs(v, numInts, numFloats) {
|
||||
// Collect and separate remaining args into register vs stack
|
||||
stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,
|
||||
keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
|
||||
keepAlive = newKeepAlive
|
||||
|
||||
// Bundle stack arguments with C-style packing
|
||||
bundleStackArgs(stackArgs, addStack)
|
||||
break
|
||||
}
|
||||
keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
|
||||
}
|
||||
|
||||
syscall := thePool.Get().(*syscall15Args)
|
||||
defer thePool.Put(syscall)
|
||||
|
||||
if runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x" {
|
||||
syscall.Set(cfn, sysargs[:], floats[:], 0)
|
||||
runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
|
||||
} else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" {
|
||||
// Use the normal arm64 calling convention even on Windows
|
||||
syscall.Set(cfn, sysargs[:], floats[:], arm64_r8)
|
||||
runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
|
||||
} else {
|
||||
*syscall = syscall15Args{}
|
||||
// This is a fallback for Windows amd64, 386, and arm. Note this may not support floats
|
||||
syscall.a1, syscall.a2, _ = syscall_syscall15X(cfn, sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4],
|
||||
sysargs[5], sysargs[6], sysargs[7], sysargs[8], sysargs[9], sysargs[10], sysargs[11],
|
||||
sysargs[12], sysargs[13], sysargs[14])
|
||||
syscall.f1 = syscall.a2 // on amd64 a2 stores the float return. On 32bit platforms floats aren't support
|
||||
}
|
||||
if ty.NumOut() == 0 {
|
||||
return nil
|
||||
}
|
||||
outType := ty.Out(0)
|
||||
v := reflect.New(outType).Elem()
|
||||
switch outType.Kind() {
|
||||
case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
v.SetUint(uint64(syscall.a1))
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
v.SetInt(int64(syscall.a1))
|
||||
case reflect.Bool:
|
||||
v.SetBool(byte(syscall.a1) != 0)
|
||||
case reflect.UnsafePointer:
|
||||
// We take the address and then dereference it to trick go vet from creating a possible miss-use of unsafe.Pointer
|
||||
v.SetPointer(*(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1)))
|
||||
case reflect.Ptr:
|
||||
v = reflect.NewAt(outType, unsafe.Pointer(&syscall.a1)).Elem()
|
||||
case reflect.Func:
|
||||
// wrap this C function in a nicely typed Go function
|
||||
v = reflect.New(outType)
|
||||
RegisterFunc(v.Interface(), syscall.a1)
|
||||
case reflect.String:
|
||||
v.SetString(strings.GoString(syscall.a1))
|
||||
case reflect.Float32:
|
||||
// NOTE: syscall.r2 is only the floating return value on 64bit platforms.
|
||||
// On 32bit platforms syscall.r2 is the upper part of a 64bit return.
|
||||
// On 386, x87 FPU returns floats as float64 in ST(0), so we read as float64 and convert.
|
||||
// On PPC64LE, C ABI converts float32 to double in FPR, so we read as float64.
|
||||
// On S390X (big-endian), float32 is in upper 32 bits of the 64-bit FP register.
|
||||
switch runtime.GOARCH {
|
||||
case "386":
|
||||
v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
|
||||
case "ppc64le":
|
||||
v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
|
||||
case "s390x":
|
||||
// S390X is big-endian: float32 in upper 32 bits of 64-bit register
|
||||
v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1 >> 32))))
|
||||
default:
|
||||
v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1))))
|
||||
}
|
||||
case reflect.Float64:
|
||||
// NOTE: syscall.r2 is only the floating return value on 64bit platforms.
|
||||
// On 32bit platforms syscall.r2 is the upper part of a 64bit return.
|
||||
if is32bit {
|
||||
v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
|
||||
} else {
|
||||
v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
|
||||
}
|
||||
case reflect.Struct:
|
||||
v = getStruct(outType, *syscall)
|
||||
default:
|
||||
panic("purego: unsupported return kind: " + outType.Kind().String())
|
||||
}
|
||||
if len(args) > 0 {
|
||||
// reuse args slice instead of allocating one when possible
|
||||
args[0] = v
|
||||
return args[:1]
|
||||
} else {
|
||||
return []reflect.Value{v}
|
||||
}
|
||||
})
|
||||
fn.Set(v)
|
||||
}
|
||||
|
||||
func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat func(x uintptr), addStack func(x uintptr), numInts *int, numFloats *int, numStack *int) []any {
|
||||
const is32bit = unsafe.Sizeof(uintptr(0)) == 4
|
||||
switch v.Kind() {
|
||||
case reflect.String:
|
||||
ptr := strings.CString(v.String())
|
||||
keepAlive = append(keepAlive, ptr)
|
||||
addInt(uintptr(unsafe.Pointer(ptr)))
|
||||
case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
addInt(uintptr(v.Uint()))
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
addInt(uintptr(v.Int()))
|
||||
case reflect.Ptr, reflect.UnsafePointer, reflect.Slice:
|
||||
// There is no need to keepAlive this pointer separately because it is kept alive in the args variable
|
||||
addInt(v.Pointer())
|
||||
case reflect.Func:
|
||||
addInt(NewCallback(v.Interface()))
|
||||
case reflect.Bool:
|
||||
if v.Bool() {
|
||||
addInt(1)
|
||||
} else {
|
||||
addInt(0)
|
||||
}
|
||||
case reflect.Float32:
|
||||
// On S390X big-endian, float32 goes in upper 32 bits of 64-bit FP register
|
||||
if runtime.GOARCH == "s390x" {
|
||||
addFloat(uintptr(math.Float32bits(float32(v.Float()))) << 32)
|
||||
} else {
|
||||
addFloat(uintptr(math.Float32bits(float32(v.Float()))))
|
||||
}
|
||||
case reflect.Float64:
|
||||
if is32bit {
|
||||
bits := math.Float64bits(v.Float())
|
||||
addFloat(uintptr(bits))
|
||||
addFloat(uintptr(bits >> 32))
|
||||
} else {
|
||||
addFloat(uintptr(math.Float64bits(v.Float())))
|
||||
}
|
||||
case reflect.Struct:
|
||||
keepAlive = addStruct(v, numInts, numFloats, numStack, addInt, addFloat, addStack, keepAlive)
|
||||
default:
|
||||
panic("purego: unsupported kind: " + v.Kind().String())
|
||||
}
|
||||
return keepAlive
|
||||
}
|
||||
|
||||
// maxRegAllocStructSize is the biggest a struct can be while still fitting in registers.
|
||||
// if it is bigger than this than enough space must be allocated on the heap and then passed into
|
||||
// the function as the first parameter on amd64 or in R8 on arm64.
|
||||
//
|
||||
// If you change this make sure to update it in objc_runtime_darwin.go
|
||||
const maxRegAllocStructSize = 16
|
||||
|
||||
func isAllSameFloat(ty reflect.Type) (allFloats bool, numFields int) {
|
||||
allFloats = true
|
||||
root := ty.Field(0).Type
|
||||
for root.Kind() == reflect.Struct {
|
||||
root = root.Field(0).Type
|
||||
}
|
||||
first := root.Kind()
|
||||
if first != reflect.Float32 && first != reflect.Float64 {
|
||||
allFloats = false
|
||||
}
|
||||
for i := 0; i < ty.NumField(); i++ {
|
||||
f := ty.Field(i).Type
|
||||
if f.Kind() == reflect.Struct {
|
||||
var structNumFields int
|
||||
allFloats, structNumFields = isAllSameFloat(f)
|
||||
numFields += structNumFields
|
||||
continue
|
||||
}
|
||||
numFields++
|
||||
if f.Kind() != first {
|
||||
allFloats = false
|
||||
}
|
||||
}
|
||||
return allFloats, numFields
|
||||
}
|
||||
|
||||
func checkStructFieldsSupported(ty reflect.Type) {
|
||||
for i := 0; i < ty.NumField(); i++ {
|
||||
f := ty.Field(i).Type
|
||||
if f.Kind() == reflect.Array {
|
||||
f = f.Elem()
|
||||
} else if f.Kind() == reflect.Struct {
|
||||
checkStructFieldsSupported(f)
|
||||
continue
|
||||
}
|
||||
switch f.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.Uintptr, reflect.Ptr, reflect.UnsafePointer, reflect.Float64, reflect.Float32,
|
||||
reflect.Bool:
|
||||
default:
|
||||
panic(fmt.Sprintf("purego: struct field type %s is not supported", f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ensureStructSupportedForRegisterFunc() {
|
||||
if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
|
||||
panic("purego: struct arguments are only supported on amd64 and arm64")
|
||||
}
|
||||
if runtime.GOOS != "darwin" && runtime.GOOS != "linux" {
|
||||
panic("purego: struct arguments are only supported on darwin and linux")
|
||||
}
|
||||
}
|
||||
|
||||
func roundUpTo8(val uintptr) uintptr {
|
||||
return (val + align8ByteMask) &^ align8ByteMask
|
||||
}
|
||||
|
||||
func numOfFloatRegisters() int {
|
||||
switch runtime.GOARCH {
|
||||
case "amd64", "arm64", "loong64", "ppc64le", "riscv64":
|
||||
return 8
|
||||
case "s390x":
|
||||
return 4
|
||||
case "arm":
|
||||
return 16
|
||||
case "386":
|
||||
// i386 SysV ABI passes all arguments on the stack, including floats
|
||||
return 0
|
||||
default:
|
||||
// since this platform isn't supported and can therefore only access
|
||||
// integer registers it is safest to return 8
|
||||
return 8
|
||||
}
|
||||
}
|
||||
|
||||
func numOfIntegerRegisters() int {
|
||||
switch runtime.GOARCH {
|
||||
case "arm64", "loong64", "ppc64le", "riscv64":
|
||||
return 8
|
||||
case "amd64":
|
||||
return 6
|
||||
case "s390x":
|
||||
// S390X uses R2-R6 for integer arguments
|
||||
return 5
|
||||
case "arm":
|
||||
return 4
|
||||
case "386":
|
||||
// i386 SysV ABI passes all arguments on the stack
|
||||
return 0
|
||||
default:
|
||||
// since this platform isn't supported and can therefore only access
|
||||
// integer registers it is fine to return the maxArgs
|
||||
return maxArgs
|
||||
}
|
||||
}
|
||||
|
||||
// estimateStackBytes estimates stack bytes needed for Darwin ARM64 validation.
|
||||
// This is a conservative estimate used only for early error detection.
|
||||
func estimateStackBytes(ty reflect.Type) int {
|
||||
var numInts, numFloats int
|
||||
var stackBytes int
|
||||
|
||||
for i := 0; i < ty.NumIn(); i++ {
|
||||
arg := ty.In(i)
|
||||
size := int(arg.Size())
|
||||
|
||||
// Check if this goes to register or stack
|
||||
usesInt := arg.Kind() != reflect.Float32 && arg.Kind() != reflect.Float64
|
||||
if usesInt && numInts < numOfIntegerRegisters() {
|
||||
numInts++
|
||||
} else if !usesInt && numFloats < numOfFloatRegisters() {
|
||||
numFloats++
|
||||
} else {
|
||||
// Goes to stack - accumulate total bytes
|
||||
stackBytes += size
|
||||
}
|
||||
}
|
||||
// Round total to 8-byte boundary
|
||||
if stackBytes > 0 && stackBytes%align8ByteSize != 0 {
|
||||
stackBytes = int(roundUpTo8(uintptr(stackBytes)))
|
||||
}
|
||||
return stackBytes
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
|
||||
|
||||
package purego
|
||||
|
||||
//go:generate go run wincallback.go
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
|
||||
|
||||
//go:build darwin || freebsd || linux || netbsd || windows
|
||||
|
||||
package purego
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
//go:linkname runtime_cgocall runtime.cgocall
|
||||
func runtime_cgocall(fn uintptr, arg unsafe.Pointer) int32 // from runtime/sys_libc.go
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2024 The Ebitengine Authors
|
||||
|
||||
//go:build freebsd || linux || netbsd
|
||||
|
||||
package cgo
|
||||
|
||||
/*
|
||||
#cgo !netbsd LDFLAGS: -ldl
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func Dlopen(filename string, flag int) (uintptr, error) {
|
||||
cfilename := C.CString(filename)
|
||||
defer C.free(unsafe.Pointer(cfilename))
|
||||
handle := C.dlopen(cfilename, C.int(flag))
|
||||
if handle == nil {
|
||||
return 0, errors.New(C.GoString(C.dlerror()))
|
||||
}
|
||||
return uintptr(handle), nil
|
||||
}
|
||||
|
||||
func Dlsym(handle uintptr, symbol string) (uintptr, error) {
|
||||
csymbol := C.CString(symbol)
|
||||
defer C.free(unsafe.Pointer(csymbol))
|
||||
symbolAddr := C.dlsym(*(*unsafe.Pointer)(unsafe.Pointer(&handle)), csymbol)
|
||||
if symbolAddr == nil {
|
||||
return 0, errors.New(C.GoString(C.dlerror()))
|
||||
}
|
||||
return uintptr(symbolAddr), nil
|
||||
}
|
||||
|
||||
func Dlclose(handle uintptr) error {
|
||||
result := C.dlclose(*(*unsafe.Pointer)(unsafe.Pointer(&handle)))
|
||||
if result != 0 {
|
||||
return errors.New(C.GoString(C.dlerror()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// all that is needed is to assign each dl function because then its
|
||||
// symbol will then be made available to the linker and linked to inside dlfcn.go
|
||||
var (
|
||||
_ = C.dlopen
|
||||
_ = C.dlsym
|
||||
_ = C.dlerror
|
||||
_ = C.dlclose
|
||||
)
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2024 The Ebitengine Authors
|
||||
|
||||
package cgo
|
||||
|
||||
// Empty so that importing this package doesn't cause issue for certain platforms.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user