mirror of
https://github.com/cloudflare/cloudflared
synced 2026-06-08 13:33:07 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21ca2e225e | |||
| f674b82e2a | |||
| ae3799a098 | |||
| 4d8df2b2c0 | |||
| a67c583bf1 | |||
| 22a955f7bb | |||
| a453612e7c | |||
| e8f8b2afb7 | |||
| 7585e38948 | |||
| a9b6f703f0 | |||
| da81fb02ec | |||
| 23b15d0eb6 | |||
| 4a2cbd1870 | |||
| 9978cfd0d5 | |||
| a0401df621 | |||
| cf17ba93b2 | |||
| f827e6216b | |||
| df981b4d89 | |||
| ddd76fa05f | |||
| 9f084e6800 | |||
| df54d27710 | |||
| b0b898c235 | |||
| 5287a9e24b | |||
| e2a71cbecc | |||
| a0e55fc969 | |||
| 1e9deb1002 | |||
| d2a87e9b93 | |||
| c0bc3bdbf0 | |||
| 29b3a7aa7e | |||
| 372a4b7079 | |||
| 649705d291 | |||
| 839b874cad | |||
| 059f4d9898 | |||
| a0bcbf6a44 | |||
| 66587173e2 | |||
| 9388e7f48c | |||
| d6cb78aeb4 |
+33
-33
@@ -2,38 +2,38 @@ ARG CLOUDFLARE_DOCKER_REGISTRY_HOST
|
||||
|
||||
FROM ${CLOUDFLARE_DOCKER_REGISTRY_HOST:-registry.cfdata.org}/stash/cf/debian-images/trixie/main:2026.1.0@sha256:e32092fd01520f5ae7de1fa6bb5a721720900ebeaa48e98f36f6f86168833cd7
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
apt-get install --no-install-recommends --allow-downgrades -y \
|
||||
build-essential \
|
||||
git \
|
||||
go-boring=1.24.11-1 \
|
||||
libffi-dev \
|
||||
procps \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
python3-setuptools \
|
||||
python3-venv \
|
||||
# tool to create msi packages
|
||||
wixl \
|
||||
# install ruby and rpm which are required to install fpm package builder
|
||||
rpm \
|
||||
ruby \
|
||||
ruby-dev \
|
||||
rubygems \
|
||||
# create deb and rpm repository files
|
||||
reprepro \
|
||||
createrepo-c \
|
||||
# gcc for cross architecture compilation in arm
|
||||
gcc-aarch64-linux-gnu \
|
||||
libc6-dev-arm64-cross && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
# Install fpm gem
|
||||
gem install fpm --no-document && \
|
||||
# Initialize rpm repository, SQL Lite DB
|
||||
mkdir -p /var/lib/rpm && \
|
||||
rpm --initdb && \
|
||||
chmod -R 777 /var/lib/rpm && \
|
||||
# Create work directory
|
||||
mkdir -p opt
|
||||
apt-get upgrade -y && \
|
||||
apt-get install --no-install-recommends --allow-downgrades -y \
|
||||
build-essential \
|
||||
git \
|
||||
go-boring=1.26.2-1 \
|
||||
libffi-dev \
|
||||
procps \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
python3-setuptools \
|
||||
python3-venv \
|
||||
# tool to create msi packages
|
||||
wixl \
|
||||
# install ruby and rpm which are required to install fpm package builder
|
||||
rpm \
|
||||
ruby \
|
||||
ruby-dev \
|
||||
rubygems \
|
||||
# create deb and rpm repository files
|
||||
reprepro \
|
||||
createrepo-c \
|
||||
# gcc for cross architecture compilation in arm
|
||||
gcc-aarch64-linux-gnu \
|
||||
libc6-dev-arm64-cross && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
# Install fpm gem
|
||||
gem install fpm --no-document && \
|
||||
# Initialize rpm repository, SQL Lite DB
|
||||
mkdir -p /var/lib/rpm && \
|
||||
rpm --initdb && \
|
||||
chmod -R 777 /var/lib/rpm && \
|
||||
# Create work directory
|
||||
mkdir -p opt
|
||||
|
||||
WORKDIR /opt
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
.golang-inputs: &golang_inputs
|
||||
runOnMR: true
|
||||
runOnBranches: '^master$'
|
||||
runOnBranches: "^master$"
|
||||
outputDir: artifacts
|
||||
runner: linux-x86-8cpu-16gb
|
||||
stage: build
|
||||
golangVersion: "boring-1.24"
|
||||
imageVersion: "3393-947ec7a@sha256:f81acc2c8ecaa84acb290c43c080702ae3aba6464201a20f9d6eff619be7c878"
|
||||
golangVersion: "boring-1.26"
|
||||
imageVersion: "3595-779e088c0ec4@sha256:a9825d640211b76915a60071e9bef3f73ad3572ce770c7c7dd36b3dd3687504c"
|
||||
CGO_ENABLED: 1
|
||||
|
||||
.default-packaging-job: &packaging-job-defaults
|
||||
@@ -65,7 +65,7 @@ include:
|
||||
- component: $CI_SERVER_FQDN/cloudflare/ci/golang/boring-make@~latest
|
||||
inputs:
|
||||
<<: *golang_inputs
|
||||
runOnBranches: '^$'
|
||||
runOnBranches: "^$"
|
||||
stage: validate
|
||||
jobPrefix: vulncheck
|
||||
GOLANG_MAKE_TARGET: vulncheck
|
||||
|
||||
@@ -28,7 +28,7 @@ macos-build-cloudflared: &mac-build
|
||||
- '[ "${RUNNER_ARCH}" = "intel" ] && export TARGET_ARCH=amd64'
|
||||
- ARCH=$(uname -m)
|
||||
- echo ARCH=$ARCH - TARGET_ARCH=$TARGET_ARCH
|
||||
- ./.ci/scripts/mac/install-go.sh
|
||||
- ./.ci/scripts/mac/install-go.sh "$MAC_GO_VERSION"
|
||||
- BUILD_SCRIPT=.ci/scripts/mac/build.sh
|
||||
- if [[ ! -x ${BUILD_SCRIPT} ]] ; then exit ; fi
|
||||
- set -euo pipefail
|
||||
|
||||
@@ -2,9 +2,13 @@ rm -rf /tmp/go
|
||||
export GOCACHE=/tmp/gocache
|
||||
rm -rf $GOCACHE
|
||||
|
||||
brew install go@1.24
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "No go version supplied"
|
||||
fi
|
||||
|
||||
brew install "$1"
|
||||
|
||||
go version
|
||||
which go
|
||||
go env
|
||||
|
||||
|
||||
+16
-15
@@ -4,13 +4,14 @@ set -e -u
|
||||
# Define the file to store the list of vulnerabilities to ignore.
|
||||
IGNORE_FILE=".vulnignore"
|
||||
|
||||
go version
|
||||
# Check if the ignored vulnerabilities file exists. If not, create an empty one.
|
||||
if [ ! -f "$IGNORE_FILE" ]; then
|
||||
touch "$IGNORE_FILE"
|
||||
echo "Created an empty file to store ignored vulnerabilities: $IGNORE_FILE"
|
||||
echo "# Add vulnerability IDs (e.g., GO-2022-0450) to ignore, one per line." >> "$IGNORE_FILE"
|
||||
echo "# You can also add comments on the same line after the ID." >> "$IGNORE_FILE"
|
||||
echo "" >> "$IGNORE_FILE"
|
||||
touch "$IGNORE_FILE"
|
||||
echo "Created an empty file to store ignored vulnerabilities: $IGNORE_FILE"
|
||||
echo "# Add vulnerability IDs (e.g., GO-2022-0450) to ignore, one per line." >>"$IGNORE_FILE"
|
||||
echo "# You can also add comments on the same line after the ID." >>"$IGNORE_FILE"
|
||||
echo "" >>"$IGNORE_FILE"
|
||||
fi
|
||||
|
||||
# Run govulncheck and capture its output.
|
||||
@@ -31,22 +32,22 @@ echo "====================================="
|
||||
CLEAN_IGNORES=$(grep -v '^\s*#' "$IGNORE_FILE" | cut -d'#' -f1 | sed 's/ //g' | sort -u || true)
|
||||
|
||||
# Filter out the ignored vulnerabilities.
|
||||
UNIGNORED_VULNS=$(echo "$VULN_OUTPUT" | grep 'Vulnerability')
|
||||
UNIGNORED_VULNS=$(echo "$VULN_OUTPUT" | grep 'Vulnerability' || true)
|
||||
|
||||
# If the list of ignored vulnerabilities is not empty, filter them out.
|
||||
if [ -n "$CLEAN_IGNORES" ]; then
|
||||
UNIGNORED_VULNS=$(echo "$UNIGNORED_VULNS" | grep -vFf <(echo "$CLEAN_IGNORES") || true)
|
||||
UNIGNORED_VULNS=$(echo "$UNIGNORED_VULNS" | grep -vFf <(echo "$CLEAN_IGNORES") || true)
|
||||
fi
|
||||
|
||||
# If there are any vulnerabilities that were not in our ignore list, print them and exit with an error.
|
||||
if [ -n "$UNIGNORED_VULNS" ]; then
|
||||
echo "🚨 Found new, unignored vulnerabilities:"
|
||||
echo "-------------------------------------"
|
||||
echo "$UNIGNORED_VULNS"
|
||||
echo "-------------------------------------"
|
||||
echo "Exiting with an error. ❌"
|
||||
exit 1
|
||||
echo "🚨 Found new, unignored vulnerabilities:"
|
||||
echo "-------------------------------------"
|
||||
echo "$UNIGNORED_VULNS"
|
||||
echo "-------------------------------------"
|
||||
echo "Exiting with an error. ❌"
|
||||
exit 1
|
||||
else
|
||||
echo "🎉 No new vulnerabilities found. All clear! ✨"
|
||||
exit 0
|
||||
echo "🎉 No new vulnerabilities found. All clear! ✨"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -18,7 +18,7 @@ windows-build-cloudflared:
|
||||
<<: *windows-build-defaults
|
||||
stage: build
|
||||
script:
|
||||
- powershell -ExecutionPolicy Bypass -File ".\.ci\scripts\windows\go-wrapper.ps1" "${GO_VERSION}" ".\.ci\scripts\windows\builds.ps1"
|
||||
- powershell -ExecutionPolicy Bypass -File ".\.ci\scripts\windows\go-wrapper.ps1" "${WIN_GO_VERSION}" ".\.ci\scripts\windows\builds.ps1"
|
||||
artifacts:
|
||||
paths:
|
||||
- artifacts/*
|
||||
@@ -73,7 +73,7 @@ windows-component-tests-cloudflared:
|
||||
script:
|
||||
# We have to decode the secret we encoded on the `windows-load-env-variables` job
|
||||
- $env:COMPONENT_TESTS_ORIGINCERT = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($env:COMPONENT_TESTS_ORIGINCERT))
|
||||
- powershell -ExecutionPolicy Bypass -File ".\.ci\scripts\windows\go-wrapper.ps1" "${GO_VERSION}" ".\.ci\scripts\windows\component-test.ps1"
|
||||
- powershell -ExecutionPolicy Bypass -File ".\.ci\scripts\windows\go-wrapper.ps1" "${WIN_GO_VERSION}" ".\.ci\scripts\windows\component-test.ps1"
|
||||
artifacts:
|
||||
reports:
|
||||
junit: report.xml
|
||||
|
||||
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 "========================================"
|
||||
+15
-4
@@ -1,5 +1,7 @@
|
||||
variables:
|
||||
GO_VERSION: "go1.24.11"
|
||||
GO_VERSION: "1.26.2"
|
||||
MAC_GO_VERSION: "go@$GO_VERSION"
|
||||
WIN_GO_VERSION: "go$GO_VERSION"
|
||||
GIT_DEPTH: "0"
|
||||
|
||||
default:
|
||||
@@ -7,7 +9,18 @@ default:
|
||||
VAULT_ID_TOKEN:
|
||||
aud: https://vault.cfdata.org
|
||||
|
||||
stages: [sync, pre-build, build, validate, test, package, release, release-internal, review]
|
||||
stages:
|
||||
[
|
||||
sync,
|
||||
pre-build,
|
||||
build,
|
||||
validate,
|
||||
test,
|
||||
package,
|
||||
release,
|
||||
release-internal,
|
||||
review,
|
||||
]
|
||||
|
||||
include:
|
||||
#####################################################
|
||||
@@ -54,5 +67,3 @@ include:
|
||||
############## Manual Claude Review #################
|
||||
#####################################################
|
||||
- component: $CI_SERVER_FQDN/cloudflare/ci/ai/review@~latest
|
||||
inputs:
|
||||
whenToRun: "manual"
|
||||
|
||||
+14
-8
@@ -1,3 +1,5 @@
|
||||
version: "2"
|
||||
|
||||
linters:
|
||||
enable:
|
||||
# Some of the linters below are commented out. We should uncomment and start running them, but they return
|
||||
@@ -14,10 +16,7 @@ linters:
|
||||
- errcheck # Errcheck is a program for checking for unchecked errors in Go code. These unchecked errors can be critical bugs in some cases.
|
||||
- errname # Checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error.
|
||||
- exhaustive # Check exhaustiveness of enum switch statements.
|
||||
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification.
|
||||
- goimports # Check import statements are formatted according to the 'goimport' command. Reformat imports in autofix mode.
|
||||
- gosec # Inspects source code for security problems.
|
||||
- gosimple # Linter for Go source code that specializes in simplifying code.
|
||||
- govet # Vet examines Go source code and reports suspicious constructs. It is roughly the same as 'go vet' and uses its passes.
|
||||
- ineffassign # Detects when assignments to existing variables are not used.
|
||||
- importas # Enforces consistent import aliases.
|
||||
@@ -36,7 +35,13 @@ linters:
|
||||
- wastedassign # Finds wasted assignment statements.
|
||||
- whitespace # Whitespace is a linter that checks for unnecessary newlines at the start and end of functions, if, for, etc.
|
||||
- zerologlint # Detects the wrong usage of zerolog that a user forgets to dispatch with Send or Msg.
|
||||
# Other linters are disabled, list of all is here: https://golangci-lint.run/usage/linters/
|
||||
# Other linters are disabled, list of all is here: https://golangci-lint.run/usage/linters/
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt # Formats code according to Go standard formatting
|
||||
- goimports # Formats imports and groups them properly
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
modules-download-mode: vendor
|
||||
@@ -44,9 +49,10 @@ run:
|
||||
# output configuration options
|
||||
output:
|
||||
formats:
|
||||
- format: 'colored-line-number'
|
||||
print-issued-lines: true
|
||||
print-linter-name: true
|
||||
text:
|
||||
colors: true
|
||||
print-linter-name: true
|
||||
print-issued-lines: true
|
||||
|
||||
issues:
|
||||
# Maximum issues count per one linter.
|
||||
@@ -67,7 +73,7 @@ issues:
|
||||
new: true
|
||||
# Show only new issues created after git revision `REV`.
|
||||
# Default: ""
|
||||
new-from-rev: ac34f94d423273c8fa8fdbb5f2ac60e55f2c77d5
|
||||
new-from-rev: d2a87e9b93456ad7f82417400f4209d513668487
|
||||
# Show issues in any part of update files (requires new-from-rev or new-from-patch).
|
||||
# Default: false
|
||||
whole-files: true
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
# Add vulnerability IDs (e.g., GO-2022-0450) to ignore, one per line.
|
||||
# You can also add comments on the same line after the ID.
|
||||
GO-2025-3942 # Ignore core-dns vulnerability since we will be removing the proxy-dns feature in the near future
|
||||
GO-2026-4289 # Ignore core-dns vulnerability since we will be removing the proxy-dns feature in the near future
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
# Cloudflared
|
||||
|
||||
Cloudflare's command-line tool and networking daemon written in Go.
|
||||
Production-grade tunneling and network connectivity services used by millions of
|
||||
developers and organizations worldwide.
|
||||
|
||||
## Essential Commands
|
||||
|
||||
### Build & Test (Always run before commits)
|
||||
|
||||
```bash
|
||||
# Full development check (run before any commit)
|
||||
make test lint
|
||||
|
||||
# Build for current platform
|
||||
make cloudflared
|
||||
|
||||
# Run all unit tests with coverage
|
||||
make test
|
||||
make cover
|
||||
|
||||
# Run specific test
|
||||
go test -run TestFunctionName ./path/to/package
|
||||
|
||||
# Run tests with race detection
|
||||
go test -race ./...
|
||||
```
|
||||
|
||||
### Platform-Specific Builds
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
TARGET_OS=linux TARGET_ARCH=amd64 make cloudflared
|
||||
|
||||
# Windows
|
||||
TARGET_OS=windows TARGET_ARCH=amd64 make cloudflared
|
||||
|
||||
# macOS ARM64
|
||||
TARGET_OS=darwin TARGET_ARCH=arm64 make cloudflared
|
||||
|
||||
# FIPS compliant build
|
||||
FIPS=true make cloudflared
|
||||
```
|
||||
|
||||
### Code Quality & Formatting
|
||||
|
||||
```bash
|
||||
# Run linter (38+ enabled linters)
|
||||
make lint
|
||||
|
||||
# Auto-fix formatting
|
||||
make fmt
|
||||
gofmt -w .
|
||||
goimports -w .
|
||||
|
||||
# Security scanning
|
||||
make vet
|
||||
|
||||
# Component tests (Python integration tests)
|
||||
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
|
||||
|
||||
- Use meaningful package names that reflect functionality
|
||||
- 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
|
||||
// Good: Clear purpose, proper error handling
|
||||
func (c *Connection) HandleRequest(ctx context.Context, req *http.Request) error {
|
||||
if req == nil {
|
||||
return errors.New("request cannot be nil")
|
||||
}
|
||||
// Implementation...
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Always handle errors explicitly, never ignore them
|
||||
- Use `fmt.Errorf` for error wrapping
|
||||
- Create meaningful error messages with context
|
||||
- Use error variables for common errors
|
||||
|
||||
```go
|
||||
// Good error handling patterns
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to process connection: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
### Logging Standards
|
||||
|
||||
- Use `github.com/rs/zerolog` for structured logging
|
||||
- Include relevant context fields
|
||||
- Use appropriate log levels (Debug, Info, Warn, Error)
|
||||
|
||||
```go
|
||||
logger.Info().
|
||||
Str("tunnelID", tunnel.ID).
|
||||
Int("connIndex", connIndex).
|
||||
Msg("Connection established")
|
||||
```
|
||||
|
||||
### Testing Patterns
|
||||
|
||||
- Use `github.com/stretchr/testify` for assertions
|
||||
- Test files end with `_test.go`
|
||||
- Use table-driven tests for multiple scenarios
|
||||
- Always use `t.Parallel()` for parallel-safe tests
|
||||
- Use meaningful test names that describe behavior
|
||||
|
||||
```go
|
||||
func TestMetricsListenerCreation(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Test implementation
|
||||
assert.Equal(t, expected, actual)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
```
|
||||
|
||||
### Constants and Variables
|
||||
|
||||
```go
|
||||
const (
|
||||
MaxGracePeriod = time.Minute * 3
|
||||
MaxConcurrentStreams = math.MaxUint32
|
||||
LogFieldConnIndex = "connIndex"
|
||||
)
|
||||
|
||||
var (
|
||||
// Group related variables
|
||||
switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols))
|
||||
flushableContentTypes = []string{sseContentType, grpcContentType, sseJsonContentType}
|
||||
)
|
||||
```
|
||||
|
||||
### Type Definitions
|
||||
|
||||
- Define interfaces close to their usage
|
||||
- Keep interfaces small and focused
|
||||
- Use descriptive names for complex types
|
||||
|
||||
```go
|
||||
type TunnelConnection interface {
|
||||
Serve(ctx context.Context) error
|
||||
}
|
||||
|
||||
type TunnelProperties struct {
|
||||
Credentials Credentials
|
||||
QuickTunnelUrl string
|
||||
}
|
||||
```
|
||||
|
||||
## Key Architectural Patterns
|
||||
|
||||
### Context Usage
|
||||
|
||||
- Always accept `context.Context` as first parameter for long-running operations
|
||||
- Respect context cancellation in loops and blocking operations
|
||||
- Pass context through call chains
|
||||
|
||||
### Concurrency
|
||||
|
||||
- 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
|
||||
|
||||
- Use structured configuration with validation
|
||||
- Support both file-based and CLI flag configuration
|
||||
- Provide sensible defaults
|
||||
|
||||
### Metrics and Observability
|
||||
|
||||
- Instrument code with Prometheus metrics
|
||||
- Use OpenTelemetry for distributed tracing
|
||||
- Include structured logging with relevant context
|
||||
|
||||
## Boundaries
|
||||
|
||||
### ✅ Always Do
|
||||
|
||||
- Run `make test lint` before any commit
|
||||
- Handle all errors explicitly with proper context
|
||||
- Use `github.com/rs/zerolog` for all logging
|
||||
- Add `t.Parallel()` to all parallel-safe tests
|
||||
- Follow the import grouping conventions
|
||||
- Use meaningful variable and function names
|
||||
- Include context.Context for long-running operations
|
||||
- Close resources in defer statements
|
||||
|
||||
### ⚠️ Ask First Before
|
||||
|
||||
- Adding new dependencies to go.mod
|
||||
- Modifying CI/CD configuration files
|
||||
- Changing build system or Makefile
|
||||
- Modifying component test infrastructure
|
||||
- Adding new linter rules or changing golangci-lint config
|
||||
- Making breaking changes to public APIs
|
||||
- Changing logging levels or structured logging fields
|
||||
|
||||
### 🚫 Never Do
|
||||
|
||||
- Ignore errors without explicit handling (`_ = err`)
|
||||
- Use generic package names (`util`, `helper`, `common`)
|
||||
- Commit code that fails `make test lint`
|
||||
- Use `fmt.Print*` instead of structured logging
|
||||
- Modify vendor dependencies directly
|
||||
- Commit secrets, credentials, or sensitive data
|
||||
- Use deprecated or unsafe Go patterns
|
||||
- Skip testing for new functionality
|
||||
- Remove existing tests unless they're genuinely invalid
|
||||
|
||||
## Dependencies Management
|
||||
|
||||
- Use Go modules (`go.mod`) exclusively
|
||||
- Vendor dependencies for reproducible builds
|
||||
- Keep dependencies up-to-date and secure
|
||||
- Prefer standard library when possible
|
||||
- Cloudflared uses a fork of quic-go always check release notes before bumping
|
||||
this dependency.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- FIPS compliance support available
|
||||
- Vulnerability scanning integrated in CI
|
||||
- Credential handling follows security best practices
|
||||
- Network security with TLS/QUIC protocols
|
||||
- Regular security audits and updates
|
||||
- Post quantum encryption
|
||||
|
||||
## Common Patterns to Follow
|
||||
|
||||
1. **Graceful shutdown**: Always implement proper cleanup
|
||||
2. **Resource management**: Close resources in defer statements
|
||||
3. **Error propagation**: Wrap errors with meaningful context
|
||||
4. **Configuration validation**: Validate inputs early
|
||||
5. **Logging consistency**: Use structured logging throughout
|
||||
6. **Testing coverage**: Aim for comprehensive test coverage
|
||||
7. **Documentation**: Comment exported functions and types
|
||||
|
||||
Remember: This is a mission-critical networking tool used in production by many
|
||||
organizations. Code quality, security, and reliability are paramount.
|
||||
+16
-1
@@ -1,3 +1,18 @@
|
||||
## 2026.4.0
|
||||
### Breaking Change
|
||||
- The default value of `--edge-ip-version` has changed from `4` to `auto`. This means cloudflared will now use whichever address family (IPv4 or IPv6) the system resolver returns first, instead of always preferring IPv4. Users who require IPv4-only connections should explicitly set `--edge-ip-version 4`.
|
||||
|
||||
## 2026.2.0
|
||||
### Breaking Change
|
||||
- Removes the `proxy-dns` feature from cloudflared. This feature allowed running a local DNS over HTTPS (DoH) proxy.
|
||||
Users who relied on this functionality should migrate to alternative solutions.
|
||||
|
||||
Removed commands and flags:
|
||||
- `cloudflared proxy-dns`
|
||||
- `cloudflared tunnel proxy-dns`
|
||||
- `--proxy-dns`, `--proxy-dns-port`, `--proxy-dns-address`, `--proxy-dns-upstream`, `--proxy-dns-max-upstream-conns`, `--proxy-dns-bootstrap`
|
||||
- `resolver` section in configuration file
|
||||
|
||||
## 2025.7.1
|
||||
### Notices
|
||||
- `cloudflared` will no longer officially support Debian and Ubuntu distros that reached end-of-life: `buster`, `bullseye`, `impish`, `trusty`.
|
||||
@@ -281,7 +296,7 @@ of uptime. Previous cloudflared versions will soon be unable to run legacy tempo
|
||||
### Bug Fixes
|
||||
|
||||
- Tunnel create and delete commands no longer use path to credentials from the configuration file.
|
||||
If you need ot place tunnel credentials file at a specific location, you must use `--credentials-file` flag.
|
||||
If you need to place tunnel credentials file at a specific location, you must use `--credentials-file` flag.
|
||||
- Access ssh-gen creates properly named keys for SSH short lived certs.
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# use a builder image for building cloudflare
|
||||
ARG TARGET_GOOS
|
||||
ARG TARGET_GOARCH
|
||||
FROM golang:1.24.11 AS builder
|
||||
FROM golang:1.26.2 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.24.11 AS builder
|
||||
FROM golang:1.26.2 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.24.11 AS builder
|
||||
FROM golang:1.26.2 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"
|
||||
|
||||
@@ -10,7 +10,7 @@ You can also use `cloudflared` to access Tunnel origins (that are protected with
|
||||
at Layer 4 (i.e., not HTTP/websocket), which is relevant for use cases such as SSH, RDP, etc.
|
||||
Such usages are available under `cloudflared access help`.
|
||||
|
||||
You can instead use [WARP client](https://developers.cloudflare.com/cloudflare-one/team-and-resources/devices/warp/)
|
||||
You can instead use [WARP client](https://developers.cloudflare.com/warp-client/)
|
||||
to access private origins behind Tunnels for Layer 4 traffic without requiring `cloudflared access` commands on the client side.
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ User documentation for Cloudflare Tunnel can be found at https://developers.clou
|
||||
|
||||
Once installed, you can authenticate `cloudflared` into your Cloudflare account and begin creating Tunnels to serve traffic to your origins.
|
||||
|
||||
* Create a Tunnel with [these instructions](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/)
|
||||
* Create a Tunnel with [these instructions](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel/)
|
||||
* Route traffic to that Tunnel:
|
||||
* Via public [DNS records in Cloudflare](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/routing-to-tunnel/dns/)
|
||||
* Or via a public hostname guided by a [Cloudflare Load Balancer](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/routing-to-tunnel/public-load-balancers/)
|
||||
@@ -62,7 +62,7 @@ For example, as of January 2023 Cloudflare will support cloudflared version 2023
|
||||
### Requirements
|
||||
- [GNU Make](https://www.gnu.org/software/make/)
|
||||
- [capnp](https://capnproto.org/install.html)
|
||||
- [go >= 1.24](https://go.dev/doc/install)
|
||||
- [go >= 1.26](https://go.dev/doc/install)
|
||||
- Optional tools:
|
||||
- [capnpc-go](https://pkg.go.dev/zombiezen.com/go/capnproto2/capnpc-go)
|
||||
- [goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports)
|
||||
@@ -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,43 @@
|
||||
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
|
||||
- 2026-03-03 fix: Update go-sentry and go-oidc to address CVE's
|
||||
- 2026-02-24 TUN-10258: add agents.md
|
||||
- 2026-02-23 TUN-10267: Update mods to fix CVE GO-2026-4394
|
||||
- 2026-02-20 TUN-10247: Update tail command to use /management/logs endpoint
|
||||
- 2026-02-11 TUN-9858: Add more information to proxy-dns removal message
|
||||
|
||||
2026.2.0
|
||||
- 2026-02-06 TUN-10216: TUN fix cloudflare vulnerabilities GO-2026-4340 and GO-2026-4341
|
||||
- 2026-02-02 TUN-9858: Remove proxy-dns feature from cloudflared
|
||||
|
||||
2026.1.2
|
||||
- 2026-01-23 Revert "TUN-9863: Update pipelines to use cloudflared EV Certificate"
|
||||
- 2026-01-21 Revert "TUN-9886 notarize cloudflared"
|
||||
|
||||
+8
-12
@@ -45,9 +45,7 @@ type baseEndpoints struct {
|
||||
var _ Client = (*RESTClient)(nil)
|
||||
|
||||
func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, log *zerolog.Logger) (*RESTClient, error) {
|
||||
if strings.HasSuffix(baseURL, "/") {
|
||||
baseURL = baseURL[:len(baseURL)-1]
|
||||
}
|
||||
baseURL = strings.TrimSuffix(baseURL, "/")
|
||||
accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/cfd_tunnel", baseURL, accountTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
||||
@@ -68,7 +66,7 @@ func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, lo
|
||||
TLSHandshakeTimeout: defaultTimeout,
|
||||
ResponseHeaderTimeout: defaultTimeout,
|
||||
}
|
||||
http2.ConfigureTransport(&httpTransport)
|
||||
_ = http2.ConfigureTransport(&httpTransport)
|
||||
return &RESTClient{
|
||||
baseEndpoints: &baseEndpoints{
|
||||
accountLevel: *accountLevelEndpoint,
|
||||
@@ -161,7 +159,6 @@ func fetchExhaustively[T any](requestFn func(int) (*http.Response, error)) ([]*T
|
||||
if envelope.Pagination.Count < envelope.Pagination.PerPage || len(fullResponse) >= envelope.Pagination.TotalCount {
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
return fullResponse, nil
|
||||
}
|
||||
@@ -179,14 +176,13 @@ func fetchPage[T any](requestFn func(int) (*http.Response, error), page int) (*r
|
||||
}
|
||||
var parsedRspBody []*T
|
||||
return envelope, parsedRspBody, parseResponseBody(envelope, &parsedRspBody)
|
||||
|
||||
}
|
||||
return nil, nil, errors.New(fmt.Sprintf("Failed to fetch page. Server returned: %d", pageResp.StatusCode))
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Success bool `json:"success,omitempty"`
|
||||
Errors []apiErr `json:"errors,omitempty"`
|
||||
Errors []apiError `json:"errors,omitempty"`
|
||||
Messages []string `json:"messages,omitempty"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
Pagination Pagination `json:"result_info,omitempty"`
|
||||
@@ -206,19 +202,19 @@ func (r *response) checkErrors() error {
|
||||
if len(r.Errors) == 1 {
|
||||
return r.Errors[0]
|
||||
}
|
||||
var messages string
|
||||
var messagesBuilder strings.Builder
|
||||
for _, e := range r.Errors {
|
||||
messages += fmt.Sprintf("%s; ", e)
|
||||
messagesBuilder.WriteString(fmt.Sprintf("%s; ", e))
|
||||
}
|
||||
return fmt.Errorf("API errors: %s", messages)
|
||||
return fmt.Errorf("API errors: %s", messagesBuilder.String())
|
||||
}
|
||||
|
||||
type apiErr struct {
|
||||
type apiError struct {
|
||||
Code json.Number `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (e apiErr) Error() string {
|
||||
func (e apiError) Error() string {
|
||||
return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ type TunnelClient interface {
|
||||
CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error)
|
||||
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
||||
GetTunnelToken(tunnelID uuid.UUID) (string, error)
|
||||
GetManagementToken(tunnelID uuid.UUID) (string, error)
|
||||
GetManagementToken(tunnelID uuid.UUID, resource ManagementResource) (string, error)
|
||||
DeleteTunnel(tunnelID uuid.UUID, cascade bool) error
|
||||
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
|
||||
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
|
||||
|
||||
+29
-11
@@ -15,6 +15,27 @@ import (
|
||||
|
||||
var ErrTunnelNameConflict = errors.New("tunnel with name already exists")
|
||||
|
||||
type ManagementResource int
|
||||
|
||||
const (
|
||||
Logs ManagementResource = iota
|
||||
Admin
|
||||
HostDetails
|
||||
)
|
||||
|
||||
func (r ManagementResource) String() string {
|
||||
switch r {
|
||||
case Logs:
|
||||
return "logs"
|
||||
case Admin:
|
||||
return "admin"
|
||||
case HostDetails:
|
||||
return "host_details"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
type Tunnel struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -50,10 +71,6 @@ type newTunnel struct {
|
||||
TunnelSecret []byte `json:"tunnel_secret"`
|
||||
}
|
||||
|
||||
type managementRequest struct {
|
||||
Resources []string `json:"resources"`
|
||||
}
|
||||
|
||||
type CleanupParams struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
@@ -137,15 +154,16 @@ func (r *RESTClient) GetTunnelToken(tunnelID uuid.UUID) (token string, err error
|
||||
return "", r.statusCodeToError("get tunnel token", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) GetManagementToken(tunnelID uuid.UUID) (token string, err error) {
|
||||
// managementEndpointPath returns the path segment for a management resource endpoint
|
||||
func managementEndpointPath(tunnelID uuid.UUID, res ManagementResource) string {
|
||||
return fmt.Sprintf("%v/management/%s", tunnelID, res.String())
|
||||
}
|
||||
|
||||
func (r *RESTClient) GetManagementToken(tunnelID uuid.UUID, res ManagementResource) (token string, err error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/management", tunnelID))
|
||||
endpoint.Path = path.Join(endpoint.Path, managementEndpointPath(tunnelID, res))
|
||||
|
||||
body := &managementRequest{
|
||||
Resources: []string{"logs"},
|
||||
}
|
||||
|
||||
resp, err := r.sendRequest("POST", endpoint, body)
|
||||
resp, err := r.sendRequest("POST", endpoint, nil)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
|
||||
+71
-7
@@ -2,7 +2,6 @@ package cfapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -11,6 +10,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var loc, _ = time.LoadLocation("UTC")
|
||||
@@ -52,7 +52,6 @@ func Test_unmarshalTunnel(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUnmarshalTunnelOk(t *testing.T) {
|
||||
|
||||
jsonBody := `{"success": true, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}`
|
||||
expected := Tunnel{
|
||||
ID: uuid.Nil,
|
||||
@@ -61,12 +60,11 @@ func TestUnmarshalTunnelOk(t *testing.T) {
|
||||
Connections: []Connection{},
|
||||
}
|
||||
actual, err := unmarshalTunnel(bytes.NewReader([]byte(jsonBody)))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &expected, actual)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &expected, actual)
|
||||
}
|
||||
|
||||
func TestUnmarshalTunnelErr(t *testing.T) {
|
||||
|
||||
tests := []string{
|
||||
`abc`,
|
||||
`{"success": true, "result": abc}`,
|
||||
@@ -76,7 +74,73 @@ func TestUnmarshalTunnelErr(t *testing.T) {
|
||||
|
||||
for i, test := range tests {
|
||||
_, err := unmarshalTunnel(bytes.NewReader([]byte(test)))
|
||||
assert.Error(t, err, fmt.Sprintf("Test #%v failed", i))
|
||||
assert.Error(t, err, "Test #%v failed", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagementResource_String(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
resource ManagementResource
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Logs",
|
||||
resource: Logs,
|
||||
want: "logs",
|
||||
},
|
||||
{
|
||||
name: "Admin",
|
||||
resource: Admin,
|
||||
want: "admin",
|
||||
},
|
||||
{
|
||||
name: "HostDetails",
|
||||
resource: HostDetails,
|
||||
want: "host_details",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, tt.resource.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagementResource_String_Unknown(t *testing.T) {
|
||||
unknown := ManagementResource(999)
|
||||
assert.Equal(t, "", unknown.String())
|
||||
}
|
||||
|
||||
func TestManagementEndpointPath(t *testing.T) {
|
||||
tunnelID := uuid.MustParse("b34cc7ce-925b-46ee-bc23-4cb5c18d8292")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
resource ManagementResource
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Logs resource",
|
||||
resource: Logs,
|
||||
want: "b34cc7ce-925b-46ee-bc23-4cb5c18d8292/management/logs",
|
||||
},
|
||||
{
|
||||
name: "Admin resource",
|
||||
resource: Admin,
|
||||
want: "b34cc7ce-925b-46ee-bc23-4cb5c18d8292/management/admin",
|
||||
},
|
||||
{
|
||||
name: "HostDetails resource",
|
||||
resource: HostDetails,
|
||||
want: "b34cc7ce-925b-46ee-bc23-4cb5c18d8292/management/host_details",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := managementEndpointPath(tunnelID, tt.resource)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +161,6 @@ func TestUnmarshalConnections(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
actual, err := parseConnectionsDetails(bytes.NewReader([]byte(jsonBody)))
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []*ActiveClient{&expected}, actual)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package access
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -23,6 +24,24 @@ func parseRequestHeaders(values []string) http.Header {
|
||||
return headers
|
||||
}
|
||||
|
||||
// bracketBareIPv6 wraps bare IPv6 addresses in a URL with square brackets.
|
||||
// Go 1.26 tightened net/url parsing to strictly require RFC 3986 bracket syntax
|
||||
// for IPv6 addresses in URLs. Before Go 1.26, bare forms like "http://::1" were
|
||||
// accepted; now they are rejected. This function detects bare IPv6 in the host
|
||||
// portion and brackets it so that url.ParseRequestURI can parse it correctly.
|
||||
func bracketBareIPv6(input string) string {
|
||||
prefix := input[:strings.Index(input, "://")+3]
|
||||
rest := input[len(prefix):]
|
||||
host := rest
|
||||
if i := strings.IndexAny(rest, "/?#"); i >= 0 {
|
||||
host = rest[:i]
|
||||
}
|
||||
if net.ParseIP(host) != nil && strings.Contains(host, ":") {
|
||||
return prefix + "[" + host + "]" + rest[len(host):]
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
// parseHostname will attempt to convert a user provided URL string into a string with some light error checking on
|
||||
// certain expectations from the URL.
|
||||
// Will convert all HTTP URLs to HTTPS
|
||||
@@ -33,6 +52,7 @@ func parseURL(input string) (*url.URL, error) {
|
||||
if !strings.HasPrefix(input, "https://") && !strings.HasPrefix(input, "http://") {
|
||||
input = fmt.Sprintf("https://%s", input)
|
||||
}
|
||||
input = bracketBareIPv6(input)
|
||||
url, err := url.ParseRequestURI(input)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse as URL: %w", err)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseRequestHeaders(t *testing.T) {
|
||||
@@ -15,6 +16,30 @@ func TestParseRequestHeaders(t *testing.T) {
|
||||
assert.Equal(t, "000:000:0:1:asd", values.Get("cf-trace-id"))
|
||||
}
|
||||
|
||||
func TestBracketBareIPv6(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"https://::1", "https://[::1]"},
|
||||
{"https://::1/path", "https://[::1]/path"},
|
||||
{"https://::1:8080", "https://[::1:8080]"},
|
||||
{"https://::1:8080/path", "https://[::1:8080]/path"},
|
||||
{"https://::1?query=1", "https://[::1]?query=1"}, // query without path
|
||||
{"https://::1#fragment", "https://[::1]#fragment"}, // fragment without path
|
||||
{"https://[::1]", "https://[::1]"}, // already bracketed
|
||||
{"https://[::1]:8080", "https://[::1]:8080"}, // already bracketed with port
|
||||
{"https://127.0.0.1", "https://127.0.0.1"}, // IPv4 unchanged
|
||||
{"https://example.com", "https://example.com"}, // hostname unchanged
|
||||
{"https://example.com:8080", "https://example.com:8080"}, // hostname:port unchanged
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, bracketBareIPv6(tt.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseURL(t *testing.T) {
|
||||
schemes := []string{
|
||||
"http://",
|
||||
@@ -28,8 +53,8 @@ func TestParseURL(t *testing.T) {
|
||||
{"localhost", "localhost"},
|
||||
{"127.0.0.1", "127.0.0.1"},
|
||||
{"127.0.0.1:9090", "127.0.0.1:9090"},
|
||||
{"::1", "::1"},
|
||||
{"::1:8080", "::1:8080"},
|
||||
{"::1", "[::1]"},
|
||||
{"::1:8080", "[::1:8080]"},
|
||||
{"[::1]", "[::1]"},
|
||||
{"[::1]:8080", "[::1]:8080"},
|
||||
{":8080", ":8080"},
|
||||
@@ -49,7 +74,7 @@ func TestParseURL(t *testing.T) {
|
||||
input := fmt.Sprintf("%s%s%s", scheme, host.input, path)
|
||||
expected := fmt.Sprintf("%s%s%s", "https://", host.expected, path)
|
||||
url, err := parseURL(input)
|
||||
assert.NoError(t, err, "input: %s\texpected: %s", input, expected)
|
||||
require.NoError(t, err, "input: %s\texpected: %s", input, expected)
|
||||
assert.Equal(t, expected, url.String())
|
||||
assert.Equal(t, host.expected, url.Host)
|
||||
assert.Equal(t, "https", url.Scheme)
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
)
|
||||
|
||||
const (
|
||||
// ResolverServiceType is used to identify what kind of overwatch service this is
|
||||
ResolverServiceType = "resolver"
|
||||
|
||||
LogFieldResolverAddress = "resolverAddress"
|
||||
LogFieldResolverPort = "resolverPort"
|
||||
LogFieldResolverMaxUpstreamConns = "resolverMaxUpstreamConns"
|
||||
)
|
||||
|
||||
// ResolverService is used to wrap the tunneldns package's DNS over HTTP
|
||||
// into a service model for the overwatch package.
|
||||
// it also holds a reference to the config object that represents its state
|
||||
type ResolverService struct {
|
||||
resolver config.DNSResolver
|
||||
shutdown chan struct{}
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// NewResolverService creates a new resolver service
|
||||
func NewResolverService(r config.DNSResolver, log *zerolog.Logger) *ResolverService {
|
||||
return &ResolverService{resolver: r,
|
||||
shutdown: make(chan struct{}),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// Name is used to figure out this service is related to the others (normally the addr it binds to)
|
||||
// this is just "resolver" since there can only be one DNS resolver running
|
||||
func (s *ResolverService) Name() string {
|
||||
return ResolverServiceType
|
||||
}
|
||||
|
||||
// Type is used to identify what kind of overwatch service this is
|
||||
func (s *ResolverService) Type() string {
|
||||
return ResolverServiceType
|
||||
}
|
||||
|
||||
// Hash is used to figure out if this forwarder is the unchanged or not from the config file updates
|
||||
func (s *ResolverService) Hash() string {
|
||||
return s.resolver.Hash()
|
||||
}
|
||||
|
||||
// Shutdown stops the tunneldns listener
|
||||
func (s *ResolverService) Shutdown() {
|
||||
s.shutdown <- struct{}{}
|
||||
}
|
||||
|
||||
// Run is the run loop that is started by the overwatch service
|
||||
func (s *ResolverService) Run() error {
|
||||
// create a listener
|
||||
l, err := tunneldns.CreateListener(s.resolver.AddressOrDefault(), s.resolver.PortOrDefault(),
|
||||
s.resolver.UpstreamsOrDefault(), s.resolver.BootstrapsOrDefault(), s.resolver.MaxUpstreamConnectionsOrDefault(), s.log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// start the listener.
|
||||
readySignal := make(chan struct{})
|
||||
err = l.Start(readySignal)
|
||||
if err != nil {
|
||||
_ = l.Stop()
|
||||
return err
|
||||
}
|
||||
<-readySignal
|
||||
|
||||
resolverLog := s.log.With().
|
||||
Str(LogFieldResolverAddress, s.resolver.AddressOrDefault()).
|
||||
Uint16(LogFieldResolverPort, s.resolver.PortOrDefault()).
|
||||
Int(LogFieldResolverMaxUpstreamConns, s.resolver.MaxUpstreamConnectionsOrDefault()).
|
||||
Logger()
|
||||
|
||||
resolverLog.Info().Msg("Starting resolver")
|
||||
|
||||
// wait for shutdown signal
|
||||
<-s.shutdown
|
||||
resolverLog.Info().Msg("Shutting down resolver")
|
||||
return l.Stop()
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// AppService is the main service that runs when no command lines flags are passed to cloudflared
|
||||
// it manages all the running services such as tunnels, forwarders, DNS resolver, etc
|
||||
// it manages all the running services such as tunnels, forwarders, etc
|
||||
type AppService struct {
|
||||
configManager config.Manager
|
||||
serviceManager overwatch.Manager
|
||||
@@ -73,14 +73,6 @@ func (s *AppService) handleConfigUpdate(c config.Root) {
|
||||
activeServices[service.Name()] = struct{}{}
|
||||
}
|
||||
|
||||
// handle resolver changes
|
||||
if c.Resolver.Enabled {
|
||||
service := NewResolverService(c.Resolver, s.log)
|
||||
s.serviceManager.Add(service)
|
||||
activeServices[service.Name()] = struct{}{}
|
||||
|
||||
}
|
||||
|
||||
// TODO: TUN-1451 - tunnels
|
||||
|
||||
// remove any services that are no longer active
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mattn/go-colorable"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
|
||||
"github.com/cloudflare/cloudflared/credentials"
|
||||
)
|
||||
|
||||
// Error definitions for management token operations
|
||||
var (
|
||||
ErrNoTunnelID = errors.New("no tunnel ID provided")
|
||||
ErrInvalidTunnelID = errors.New("unable to parse provided tunnel id as a valid UUID")
|
||||
)
|
||||
|
||||
// GetManagementToken acquires a management token from Cloudflare API for the specified resource
|
||||
func GetManagementToken(c *cli.Context, log *zerolog.Logger, res cfapi.ManagementResource, buildInfo *BuildInfo) (string, error) {
|
||||
userCreds, err := credentials.Read(c.String(cfdflags.OriginCert), log)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var apiURL string
|
||||
if userCreds.IsFEDEndpoint() {
|
||||
apiURL = credentials.FedRampBaseApiURL
|
||||
} else {
|
||||
apiURL = c.String(cfdflags.ApiURL)
|
||||
}
|
||||
|
||||
client, err := userCreds.Client(apiURL, buildInfo.UserAgent(), log)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tunnelIDString := c.Args().First()
|
||||
if tunnelIDString == "" {
|
||||
return "", ErrNoTunnelID
|
||||
}
|
||||
tunnelID, err := uuid.Parse(tunnelIDString)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %v", ErrInvalidTunnelID, err)
|
||||
}
|
||||
|
||||
token, err := client.GetManagementToken(tunnelID, res)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// CreateStderrLogger creates a logger that outputs to stderr to avoid interfering with stdout
|
||||
func CreateStderrLogger(c *cli.Context) *zerolog.Logger {
|
||||
level, levelErr := zerolog.ParseLevel(c.String(cfdflags.LogLevel))
|
||||
if levelErr != nil {
|
||||
level = zerolog.InfoLevel
|
||||
}
|
||||
var writer io.Writer
|
||||
switch c.String(cfdflags.LogFormatOutput) {
|
||||
case cfdflags.LogFormatOutputValueJSON:
|
||||
// zerolog by default outputs as JSON
|
||||
writer = os.Stderr
|
||||
case cfdflags.LogFormatOutputValueDefault:
|
||||
// "default" and unset use the same logger output format
|
||||
fallthrough
|
||||
default:
|
||||
writer = zerolog.ConsoleWriter{
|
||||
Out: colorable.NewColorable(os.Stderr),
|
||||
TimeFormat: time.RFC3339,
|
||||
}
|
||||
}
|
||||
log := zerolog.New(writer).With().Timestamp().Logger().Level(level)
|
||||
return &log
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -111,9 +114,6 @@ const (
|
||||
// ICMPV6Src is the command line flag to set the source address and the interface name to send/receive ICMPv6 messages
|
||||
ICMPV6Src = "icmpv6-src"
|
||||
|
||||
// ProxyDns is the command line flag to run DNS server over HTTPS
|
||||
ProxyDns = "proxy-dns"
|
||||
|
||||
// Name is the command line to set the name of the tunnel
|
||||
Name = "name"
|
||||
|
||||
@@ -123,6 +123,12 @@ 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"
|
||||
|
||||
// Prechecks is the command line flag to run connectivity pre-checks at startup.
|
||||
Prechecks = "prechecks"
|
||||
|
||||
// LogLevel is the command line flag for the cloudflared logging level
|
||||
LogLevel = "loglevel"
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/management"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tail"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
|
||||
@@ -91,6 +92,7 @@ func main() {
|
||||
tracing.Init(Version)
|
||||
token.Init(Version)
|
||||
tail.Init(bInfo)
|
||||
management.Init(bInfo)
|
||||
runApp(app, graceShutdownC)
|
||||
}
|
||||
|
||||
@@ -149,9 +151,10 @@ To determine if an update happened in a script, check for error code 11.`,
|
||||
},
|
||||
}
|
||||
cmds = append(cmds, tunnel.Commands()...)
|
||||
cmds = append(cmds, proxydns.Command(false))
|
||||
cmds = append(cmds, proxydns.Command()) // removed feature, only here for error message
|
||||
cmds = append(cmds, access.Commands()...)
|
||||
cmds = append(cmds, tail.Command())
|
||||
cmds = append(cmds, management.Command())
|
||||
return cmds
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package management
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
|
||||
"github.com/cloudflare/cloudflared/credentials"
|
||||
)
|
||||
|
||||
var buildInfo *cliutil.BuildInfo
|
||||
|
||||
// Init initializes the management package with build info
|
||||
func Init(bi *cliutil.BuildInfo) {
|
||||
buildInfo = bi
|
||||
}
|
||||
|
||||
// Command returns the management command with its subcommands
|
||||
func Command() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "management",
|
||||
Usage: "Monitor cloudflared tunnels via management API",
|
||||
Category: "Management",
|
||||
Hidden: true,
|
||||
Subcommands: []*cli.Command{
|
||||
buildTokenSubcommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// buildTokenSubcommand creates the token subcommand
|
||||
func buildTokenSubcommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "token",
|
||||
Action: cliutil.ConfiguredAction(tokenCommand),
|
||||
Usage: "Get management access jwt for a specific resource",
|
||||
UsageText: "cloudflared management token --resource <resource> TUNNEL_ID",
|
||||
Description: "Get management access jwt for a tunnel with specified resource permissions (logs, admin, host_details)",
|
||||
Hidden: true,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "resource",
|
||||
Usage: "Resource type for token permissions: logs, admin, or host_details",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: cfdflags.OriginCert,
|
||||
Usage: "Path to the certificate generated for your origin when you run cloudflared login.",
|
||||
EnvVars: []string{"TUNNEL_ORIGIN_CERT"},
|
||||
Value: credentials.FindDefaultOriginCertPath(),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: cfdflags.LogLevel,
|
||||
Value: "info",
|
||||
Usage: "Application logging level {debug, info, warn, error, fatal}",
|
||||
EnvVars: []string{"TUNNEL_LOGLEVEL"},
|
||||
},
|
||||
cliutil.FlagLogOutput,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// tokenCommand handles the token subcommand execution
|
||||
func tokenCommand(c *cli.Context) error {
|
||||
log := cliutil.CreateStderrLogger(c)
|
||||
|
||||
// Parse and validate resource flag
|
||||
resourceStr := c.String("resource")
|
||||
resource, err := parseResource(resourceStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid resource '%s': %w", resourceStr, err)
|
||||
}
|
||||
|
||||
// Get management token
|
||||
token, err := cliutil.GetManagementToken(c, log, resource, buildInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Output JSON to stdout
|
||||
tokenResponse := struct {
|
||||
Token string `json:"token"`
|
||||
}{Token: token}
|
||||
|
||||
return json.NewEncoder(os.Stdout).Encode(tokenResponse)
|
||||
}
|
||||
|
||||
// parseResource converts resource string to ManagementResource enum
|
||||
func parseResource(resource string) (cfapi.ManagementResource, error) {
|
||||
switch resource {
|
||||
case "logs":
|
||||
return cfapi.Logs, nil
|
||||
case "admin":
|
||||
return cfapi.Admin, nil
|
||||
case "host_details":
|
||||
return cfapi.HostDetails, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("must be one of: logs, admin, host_details")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package management
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
)
|
||||
|
||||
func TestParseResource_ValidResources(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
expected cfapi.ManagementResource
|
||||
}{
|
||||
{"logs", cfapi.Logs},
|
||||
{"admin", cfapi.Admin},
|
||||
{"host_details", cfapi.HostDetails},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
result, err := parseResource(tt.input)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseResource_InvalidResource(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
invalid := []string{"invalid", "LOGS", "Admin", "", "metrics", "host-details"}
|
||||
|
||||
for _, input := range invalid {
|
||||
t.Run(input, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := parseResource(input)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "must be one of")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandStructure(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cmd := Command()
|
||||
|
||||
assert.Equal(t, "management", cmd.Name)
|
||||
assert.True(t, cmd.Hidden)
|
||||
assert.Len(t, cmd.Subcommands, 1)
|
||||
|
||||
tokenCmd := cmd.Subcommands[0]
|
||||
assert.Equal(t, "token", tokenCmd.Name)
|
||||
assert.True(t, tokenCmd.Hidden)
|
||||
|
||||
// Verify required flags exist
|
||||
var hasResourceFlag bool
|
||||
for _, flag := range tokenCmd.Flags {
|
||||
if flag.Names()[0] == "resource" {
|
||||
hasResourceFlag = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, hasResourceFlag, "token command should have --resource flag")
|
||||
}
|
||||
@@ -1,115 +1,54 @@
|
||||
package proxydns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"errors"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
)
|
||||
|
||||
func Command(hidden bool) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "proxy-dns",
|
||||
Action: cliutil.ConfiguredAction(Run),
|
||||
const removedMessage = "dns-proxy feature is no longer supported"
|
||||
|
||||
Usage: "Run a DNS over HTTPS proxy server.",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "metrics",
|
||||
Value: "localhost:",
|
||||
Usage: "Listen address for metrics reporting.",
|
||||
EnvVars: []string{"TUNNEL_METRICS"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "address",
|
||||
Usage: "Listen address for the DNS over HTTPS proxy server.",
|
||||
Value: "localhost",
|
||||
EnvVars: []string{"TUNNEL_DNS_ADDRESS"},
|
||||
},
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
&cli.IntFlag{
|
||||
Name: "port",
|
||||
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
|
||||
Value: 53,
|
||||
EnvVars: []string{"TUNNEL_DNS_PORT"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "upstream",
|
||||
Usage: "Upstream endpoint URL, you can specify multiple endpoints for redundancy.",
|
||||
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
|
||||
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "bootstrap",
|
||||
Usage: "bootstrap endpoint URL, you can specify multiple endpoints for redundancy.",
|
||||
Value: cli.NewStringSlice("https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"),
|
||||
EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"},
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "max-upstream-conns",
|
||||
Usage: "Maximum concurrent connections to upstream. Setting to 0 means unlimited.",
|
||||
Value: tunneldns.MaxUpstreamConnsDefault,
|
||||
EnvVars: []string{"TUNNEL_DNS_MAX_UPSTREAM_CONNS"},
|
||||
},
|
||||
},
|
||||
ArgsUsage: " ", // can't be the empty string or we get the default output
|
||||
Hidden: hidden,
|
||||
func Command() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "proxy-dns",
|
||||
Action: cliutil.ConfiguredAction(Run),
|
||||
Usage: removedMessage,
|
||||
SkipFlagParsing: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Run implements a foreground runner
|
||||
func Run(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
err := errors.New(removedMessage)
|
||||
log.Error().Msg("DNS Proxy is no longer supported since version 2026.2.0 (https://developers.cloudflare.com/changelog/2025-11-11-cloudflared-proxy-dns/). As an alternative consider using https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/dns-over-https-client/")
|
||||
|
||||
metricsListener, err := net.Listen("tcp", c.String("metrics"))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to open the metrics listener")
|
||||
}
|
||||
|
||||
go metrics.ServeMetrics(metricsListener, context.Background(), metrics.Config{}, log)
|
||||
|
||||
listener, err := tunneldns.CreateListener(
|
||||
c.String("address"),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
uint16(c.Int("port")),
|
||||
c.StringSlice("upstream"),
|
||||
c.StringSlice("bootstrap"),
|
||||
c.Int("max-upstream-conns"),
|
||||
log,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to create the listeners")
|
||||
return err
|
||||
}
|
||||
|
||||
// Try to start the server
|
||||
readySignal := make(chan struct{})
|
||||
err = listener.Start(readySignal)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to start the listeners")
|
||||
return listener.Stop()
|
||||
}
|
||||
<-readySignal
|
||||
|
||||
// Wait for signal
|
||||
signals := make(chan os.Signal, 10)
|
||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Stop(signals)
|
||||
<-signals
|
||||
|
||||
// Shut down server
|
||||
err = listener.Stop()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to stop")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Old flags used by the proxy-dns command, only kept to not break any script that might be setting these flags
|
||||
func ConfigureProxyDNSFlags(shouldHide bool) []cli.Flag {
|
||||
return []cli.Flag{
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "proxy-dns",
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "proxy-dns-port",
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "proxy-dns-address",
|
||||
}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
|
||||
Name: "proxy-dns-upstream",
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "proxy-dns-max-upstream-conns",
|
||||
}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
|
||||
Name: "proxy-dns-bootstrap",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -13,11 +12,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mattn/go-colorable"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"nhooyr.io/websocket"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
|
||||
"github.com/cloudflare/cloudflared/credentials"
|
||||
@@ -50,9 +49,9 @@ func buildTailManagementTokenSubcommand() *cli.Command {
|
||||
}
|
||||
|
||||
func managementTokenCommand(c *cli.Context) error {
|
||||
log := createLogger(c)
|
||||
log := cliutil.CreateStderrLogger(c)
|
||||
|
||||
token, err := getManagementToken(c, log)
|
||||
token, err := cliutil.GetManagementToken(c, log, cfapi.Logs, buildInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -161,31 +160,6 @@ func handleValidationError(resp *http.Response, log *zerolog.Logger) {
|
||||
}
|
||||
}
|
||||
|
||||
// logger will be created to emit only against the os.Stderr as to not obstruct with normal output from
|
||||
// management requests
|
||||
func createLogger(c *cli.Context) *zerolog.Logger {
|
||||
level, levelErr := zerolog.ParseLevel(c.String(cfdflags.LogLevel))
|
||||
if levelErr != nil {
|
||||
level = zerolog.InfoLevel
|
||||
}
|
||||
var writer io.Writer
|
||||
switch c.String(cfdflags.LogFormatOutput) {
|
||||
case cfdflags.LogFormatOutputValueJSON:
|
||||
// zerolog by default outputs as JSON
|
||||
writer = os.Stderr
|
||||
case cfdflags.LogFormatOutputValueDefault:
|
||||
// "default" and unset use the same logger output format
|
||||
fallthrough
|
||||
default:
|
||||
writer = zerolog.ConsoleWriter{
|
||||
Out: colorable.NewColorable(os.Stderr),
|
||||
TimeFormat: time.RFC3339,
|
||||
}
|
||||
}
|
||||
log := zerolog.New(writer).With().Timestamp().Logger().Level(level)
|
||||
return &log
|
||||
}
|
||||
|
||||
// parseFilters will attempt to parse provided filters to send to with the EventStartStreaming
|
||||
func parseFilters(c *cli.Context) (*management.StreamingFilters, error) {
|
||||
var level *management.LogLevel
|
||||
@@ -230,49 +204,13 @@ func parseFilters(c *cli.Context) (*management.StreamingFilters, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getManagementToken will make a call to the Cloudflare API to acquire a management token for the requested tunnel.
|
||||
func getManagementToken(c *cli.Context, log *zerolog.Logger) (string, error) {
|
||||
userCreds, err := credentials.Read(c.String(cfdflags.OriginCert), log)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var apiURL string
|
||||
if userCreds.IsFEDEndpoint() {
|
||||
apiURL = credentials.FedRampBaseApiURL
|
||||
} else {
|
||||
apiURL = c.String(cfdflags.ApiURL)
|
||||
}
|
||||
|
||||
client, err := userCreds.Client(apiURL, buildInfo.UserAgent(), log)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tunnelIDString := c.Args().First()
|
||||
if tunnelIDString == "" {
|
||||
return "", errors.New("no tunnel ID provided")
|
||||
}
|
||||
tunnelID, err := uuid.Parse(tunnelIDString)
|
||||
if err != nil {
|
||||
return "", errors.New("unable to parse provided tunnel id as a valid UUID")
|
||||
}
|
||||
|
||||
token, err := client.GetManagementToken(tunnelID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// buildURL will build the management url to contain the required query parameters to authenticate the request.
|
||||
func buildURL(c *cli.Context, log *zerolog.Logger) (url.URL, error) {
|
||||
func buildURL(c *cli.Context, log *zerolog.Logger, res cfapi.ManagementResource) (url.URL, error) {
|
||||
var err error
|
||||
|
||||
token := c.String("token")
|
||||
if token == "" {
|
||||
token, err = getManagementToken(c, log)
|
||||
token, err = cliutil.GetManagementToken(c, log, res, buildInfo)
|
||||
if err != nil {
|
||||
return url.URL{}, fmt.Errorf("unable to acquire management token for requested tunnel id: %w", err)
|
||||
}
|
||||
@@ -323,7 +261,7 @@ func printJSON(log *management.Log, logger *zerolog.Logger) {
|
||||
|
||||
// Run implements a foreground runner
|
||||
func Run(c *cli.Context) error {
|
||||
log := createLogger(c)
|
||||
log := cliutil.CreateStderrLogger(c)
|
||||
|
||||
signals := make(chan os.Signal, 10)
|
||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||
@@ -345,7 +283,7 @@ func Run(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
u, err := buildURL(c, log)
|
||||
u, err := buildURL(c, log, cfapi.Logs)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("unable to construct management request URL")
|
||||
return nil
|
||||
|
||||
+102
-110
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -31,20 +32,22 @@ 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"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
"github.com/cloudflare/cloudflared/tunnelstate"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
)
|
||||
|
||||
const (
|
||||
//nolint:gosec // This is the Sentry DSN for cloudflared which is safe to be public
|
||||
sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878"
|
||||
|
||||
LogFieldCommand = "command"
|
||||
@@ -77,6 +80,7 @@ var (
|
||||
"config",
|
||||
cfdflags.AutoUpdateFreq,
|
||||
cfdflags.NoAutoUpdate,
|
||||
cfdflags.NoPrechecks,
|
||||
cfdflags.Metrics,
|
||||
"pidfile",
|
||||
"url",
|
||||
@@ -115,12 +119,6 @@ var (
|
||||
cfdflags.LogFile,
|
||||
cfdflags.LogDirectory,
|
||||
cfdflags.TraceOutput,
|
||||
cfdflags.ProxyDns,
|
||||
"proxy-dns-port",
|
||||
"proxy-dns-address",
|
||||
"proxy-dns-upstream",
|
||||
"proxy-dns-max-upstream-conns",
|
||||
"proxy-dns-bootstrap",
|
||||
cfdflags.IsAutoUpdated,
|
||||
cfdflags.Edge,
|
||||
cfdflags.Region,
|
||||
@@ -181,8 +179,7 @@ func Commands() []*cli.Command {
|
||||
buildCleanupCommand(),
|
||||
buildTokenCommand(),
|
||||
buildDiagCommand(),
|
||||
// for compatibility, allow following as tunnel subcommands
|
||||
proxydns.Command(true),
|
||||
proxydns.Command(), // removed feature, only here for error message
|
||||
cliutil.RemovedCommand("db-connect"),
|
||||
}
|
||||
|
||||
@@ -238,7 +235,7 @@ func TunnelCommand(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Run a adhoc named tunnel
|
||||
// Run an adhoc named tunnel
|
||||
// Allows for the creation, routing (optional), and startup of a tunnel in one command
|
||||
// --name required
|
||||
// --url or --hello-world required
|
||||
@@ -248,8 +245,8 @@ func TunnelCommand(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid hostname provided")
|
||||
}
|
||||
url := c.String("url")
|
||||
if url == hostname && url != "" && hostname != "" {
|
||||
tunnelURL := c.String("url")
|
||||
if tunnelURL == hostname && tunnelURL != "" && hostname != "" {
|
||||
return fmt.Errorf("hostname and url shouldn't match. See --help for more information")
|
||||
}
|
||||
|
||||
@@ -258,15 +255,14 @@ func TunnelCommand(c *cli.Context) error {
|
||||
|
||||
// Run a quick tunnel
|
||||
// A unauthenticated named tunnel hosted on <random>.<quick-tunnels-service>.com
|
||||
// We don't support running proxy-dns and a quick tunnel at the same time as the same process
|
||||
shouldRunQuickTunnel := c.IsSet("url") || c.IsSet(ingress.HelloWorldFlag)
|
||||
if !c.IsSet(cfdflags.ProxyDns) && c.String("quick-service") != "" && shouldRunQuickTunnel {
|
||||
if c.String("quick-service") != "" && shouldRunQuickTunnel {
|
||||
return RunQuickTunnel(sc)
|
||||
}
|
||||
|
||||
// If user provides a config, check to see if they meant to use `tunnel run` instead
|
||||
if ref := config.GetConfiguration().TunnelID; ref != "" {
|
||||
return fmt.Errorf("Use `cloudflared tunnel run` to start tunnel %s", ref)
|
||||
return fmt.Errorf("use `cloudflared tunnel run` to start tunnel %s", ref)
|
||||
}
|
||||
|
||||
// Classic tunnel usage is no longer supported
|
||||
@@ -274,16 +270,6 @@ func TunnelCommand(c *cli.Context) error {
|
||||
return errDeprecatedClassicTunnel
|
||||
}
|
||||
|
||||
if c.IsSet(cfdflags.ProxyDns) {
|
||||
if shouldRunQuickTunnel {
|
||||
return fmt.Errorf("running a quick tunnel with `proxy-dns` is not supported")
|
||||
}
|
||||
// NamedTunnelProperties are nil since proxy dns server does not need it.
|
||||
// This is supported for legacy reasons: dns proxy server is not a tunnel and ideally should
|
||||
// not run as part of cloudflared tunnel.
|
||||
return StartServer(sc.c, buildInfo, nil, sc.log)
|
||||
}
|
||||
|
||||
return errors.New(tunnelCmdErrorMessage)
|
||||
}
|
||||
|
||||
@@ -364,12 +350,14 @@ func StartServer(
|
||||
traceLog.Err(err).Msg("Failed to close temporary trace output file")
|
||||
}
|
||||
traceOutputFilepath := c.String(cfdflags.TraceOutput)
|
||||
//nolint:gosec // File path is safe because it is explicitly provided by the user via the --trace-output flag
|
||||
if err := os.Rename(tmpTraceFile.Name(), traceOutputFilepath); err != nil {
|
||||
traceLog.
|
||||
Err(err).
|
||||
Str(LogFieldTraceOutputFilepath, traceOutputFilepath).
|
||||
Msg("Failed to rename temporary trace output file")
|
||||
} else {
|
||||
//nolint:gosec // File path is safe, since it is created by os.CreateTemp
|
||||
err := os.Remove(tmpTraceFile.Name())
|
||||
if err != nil {
|
||||
traceLog.Err(err).Msg("Failed to remove the temporary trace file")
|
||||
@@ -387,30 +375,29 @@ func StartServer(
|
||||
info.Log(log)
|
||||
logClientOptions(c, log)
|
||||
|
||||
// this context drives the server, when it's cancelled tunnel and all other components (origins, dns, etc...) should stop
|
||||
// Run connectivity pre-checks for cloudflared. This runs in a separate
|
||||
// goroutine, as we want to keep initializing cloudflared while prechecks
|
||||
// are running.
|
||||
if c.Bool(cfdflags.Prechecks) && !c.Bool(cfdflags.NoPrechecks) {
|
||||
resolvedRegion := c.String(cfdflags.Region)
|
||||
if resolvedRegion == "" && namedTunnel != nil {
|
||||
resolvedRegion = namedTunnel.Credentials.Endpoint
|
||||
}
|
||||
go runPrechecks(c, log, resolvedRegion)
|
||||
}
|
||||
|
||||
// this context drives the server, when it's canceled tunnel and all other components (origins, dns, etc...) should stop
|
||||
ctx, cancel := context.WithCancel(c.Context)
|
||||
defer cancel()
|
||||
|
||||
go waitForSignal(graceShutdownC, log)
|
||||
|
||||
if c.IsSet(cfdflags.ProxyDns) {
|
||||
dnsReadySignal := make(chan struct{})
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errC <- runDNSProxyServer(c, dnsReadySignal, ctx.Done(), log)
|
||||
}()
|
||||
// Wait for proxy-dns to come up (if used)
|
||||
<-dnsReadySignal
|
||||
}
|
||||
|
||||
connectedSignal := signal.New(make(chan struct{}))
|
||||
go notifySystemd(connectedSignal)
|
||||
if c.IsSet("pidfile") {
|
||||
go writePidFile(connectedSignal, c.String("pidfile"), log)
|
||||
}
|
||||
|
||||
// update needs to be after DNS proxy is up to resolve equinox server address
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
@@ -420,15 +407,8 @@ func StartServer(
|
||||
errC <- autoupdater.Run(ctx)
|
||||
}()
|
||||
|
||||
// Serve DNS proxy stand-alone if no tunnel type (quick, adhoc, named) is going to run
|
||||
if dnsProxyStandAlone(c, namedTunnel) {
|
||||
connectedSignal.Notify()
|
||||
// no grace period, handle SIGINT/SIGTERM immediately
|
||||
return waitToShutdown(&wg, cancel, errC, graceShutdownC, 0, log)
|
||||
}
|
||||
|
||||
if namedTunnel == nil {
|
||||
return fmt.Errorf("namedTunnel is nil outside of DNS proxy stand-alone mode")
|
||||
return fmt.Errorf("namedTunnel is nil")
|
||||
}
|
||||
|
||||
logTransport := logger.CreateTransportLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
@@ -489,7 +469,7 @@ func StartServer(
|
||||
return errors.Wrap(err, "Error opening metrics server listener")
|
||||
}
|
||||
|
||||
defer metricsListener.Close()
|
||||
defer func() { _ = metricsListener.Close() }()
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
@@ -547,6 +527,49 @@ func StartServer(
|
||||
return waitToShutdown(&wg, cancel, errC, graceShutdownC, gracePeriod, log)
|
||||
}
|
||||
|
||||
// runPrechecks executes connectivity pre-checks and logs the results.
|
||||
// Pre-checks are diagnostic only and do not gate tunnel startup.
|
||||
func runPrechecks(c *cli.Context, log *zerolog.Logger, region string) {
|
||||
ipVersion := allregions.Auto
|
||||
if ipVersionStr := c.String(cfdflags.EdgeIpVersion); ipVersionStr != "" {
|
||||
parsedVersion, err := parseConfigIPVersion(ipVersionStr)
|
||||
if err == nil {
|
||||
ipVersion = parsedVersion
|
||||
} else {
|
||||
log.Warn().Str("edgeIpVersion", ipVersionStr).Err(err).Msg("Invalid edge-ip-version value, using auto")
|
||||
}
|
||||
}
|
||||
|
||||
cfg := prechecks.Config{
|
||||
Region: region,
|
||||
IPVersion: ipVersion,
|
||||
}
|
||||
|
||||
// Mirror the static/dynamic edge selection from supervisor/supervisor.go:
|
||||
// when --edge addresses are provided, bypass DNS discovery entirely.
|
||||
var dnsResolver prechecks.DNSResolver
|
||||
if edgeAddrs := c.StringSlice(cfdflags.Edge); len(edgeAddrs) > 0 {
|
||||
dnsResolver = &prechecks.StaticEdgeDNSResolver{Addrs: edgeAddrs, Log: log}
|
||||
} else {
|
||||
dnsResolver = &prechecks.EdgeDNSResolver{Log: log}
|
||||
}
|
||||
|
||||
dialers := prechecks.RunDialers{
|
||||
DNSResolver: dnsResolver,
|
||||
TCPDialer: &prechecks.EdgeTCPDialer{},
|
||||
QUICDialer: &prechecks.EdgeQUICDialer{},
|
||||
ManagementDialer: &prechecks.NetManagementDialer{Dialer: net.Dialer{}},
|
||||
}
|
||||
|
||||
report := prechecks.Run(c.Context, c.String(cfdflags.CACert), cfg, log, dialers)
|
||||
|
||||
// Output the human-readable table to console
|
||||
fmt.Println(report.String())
|
||||
|
||||
// Also log structured results for log aggregation
|
||||
report.LogEvent(log)
|
||||
}
|
||||
|
||||
func waitToShutdown(wg *sync.WaitGroup,
|
||||
cancelServerContext func(),
|
||||
errC <-chan error,
|
||||
@@ -603,13 +626,14 @@ func writePidFile(waitForSignal *signal.Signal, pidPathname string, log *zerolog
|
||||
log.Err(err).Str(LogFieldPIDPathname, pidPathname).Msg("Unable to expand the path, try to use absolute path in --pidfile")
|
||||
return
|
||||
}
|
||||
file, err := os.Create(expandedPath)
|
||||
cleanPath := filepath.Clean(expandedPath)
|
||||
file, err := os.Create(cleanPath)
|
||||
if err != nil {
|
||||
log.Err(err).Str(LogFieldExpandedPath, expandedPath).Msg("Unable to write pid")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
fmt.Fprintf(file, "%d", os.Getpid())
|
||||
defer func() { _ = file.Close() }()
|
||||
_, _ = fmt.Fprintf(file, "%d", os.Getpid())
|
||||
}
|
||||
|
||||
func hostnameFromURI(uri string) string {
|
||||
@@ -641,7 +665,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
flags := configureCloudflaredFlags(shouldHide)
|
||||
flags = append(flags, configureProxyFlags(shouldHide)...)
|
||||
flags = append(flags, cliutil.ConfigureLoggingFlags(shouldHide)...)
|
||||
flags = append(flags, configureProxyDNSFlags(shouldHide)...)
|
||||
flags = append(flags, proxydns.ConfigureProxyDNSFlags(shouldHide)...) // removed feature, only kept to not break any script that might be setting these flags
|
||||
flags = append(flags, []cli.Flag{
|
||||
credentialsFileFlag,
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
@@ -665,7 +689,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
Name: cfdflags.EdgeIpVersion,
|
||||
Usage: "Cloudflare Edge IP address version to connect with. {4, 6, auto}",
|
||||
EnvVars: []string{"TUNNEL_EDGE_IP_VERSION"},
|
||||
Value: "4",
|
||||
Value: "auto",
|
||||
Hidden: false,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
@@ -675,7 +699,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,
|
||||
@@ -915,6 +939,20 @@ 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.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: cfdflags.Prechecks,
|
||||
Usage: "Run connectivity pre-checks at startup.",
|
||||
EnvVars: []string{"TUNNEL_PRECHECKS"},
|
||||
Value: false,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: cfdflags.Metrics,
|
||||
Value: metrics.GetMetricsDefaultAddress(metrics.Runtime),
|
||||
@@ -938,6 +976,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",
|
||||
@@ -1172,58 +1211,13 @@ func sshFlags(shouldHide bool) []cli.Flag {
|
||||
}
|
||||
}
|
||||
|
||||
func configureProxyDNSFlags(shouldHide bool) []cli.Flag {
|
||||
return []cli.Flag{
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: cfdflags.ProxyDns,
|
||||
Usage: "Run a DNS over HTTPS proxy server.",
|
||||
EnvVars: []string{"TUNNEL_DNS"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "proxy-dns-port",
|
||||
Value: 53,
|
||||
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
|
||||
EnvVars: []string{"TUNNEL_DNS_PORT"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "proxy-dns-address",
|
||||
Usage: "Listen address for the DNS over HTTPS proxy server.",
|
||||
Value: "localhost",
|
||||
EnvVars: []string{"TUNNEL_DNS_ADDRESS"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
|
||||
Name: "proxy-dns-upstream",
|
||||
Usage: "Upstream endpoint URL, you can specify multiple endpoints for redundancy.",
|
||||
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
|
||||
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "proxy-dns-max-upstream-conns",
|
||||
Usage: "Maximum concurrent connections to upstream. Setting to 0 means unlimited.",
|
||||
Value: tunneldns.MaxUpstreamConnsDefault,
|
||||
Hidden: shouldHide,
|
||||
EnvVars: []string{"TUNNEL_DNS_MAX_UPSTREAM_CONNS"},
|
||||
}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
|
||||
Name: "proxy-dns-bootstrap",
|
||||
Usage: "bootstrap endpoint URL, you can specify multiple endpoints for redundancy.",
|
||||
Value: cli.NewStringSlice(
|
||||
"https://162.159.36.1/dns-query",
|
||||
"https://162.159.46.1/dns-query",
|
||||
"https://[2606:4700:4700::1111]/dns-query",
|
||||
"https://[2606:4700:4700::1001]/dns-query",
|
||||
),
|
||||
EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func stdinControl(reconnectCh chan supervisor.ReconnectSignal, log *zerolog.Logger) {
|
||||
helpStr := strings.Join([]string{
|
||||
"Supported command:",
|
||||
"reconnect [delay]",
|
||||
"- restarts one randomly chosen connection with optional delay before reconnect\n",
|
||||
}, "\n")
|
||||
|
||||
for {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
@@ -1232,7 +1226,7 @@ func stdinControl(reconnectCh chan supervisor.ReconnectSignal, log *zerolog.Logg
|
||||
|
||||
switch parts[0] {
|
||||
case "":
|
||||
break
|
||||
continue
|
||||
case "reconnect":
|
||||
var reconnect supervisor.ReconnectSignal
|
||||
if len(parts) > 1 {
|
||||
@@ -1244,13 +1238,11 @@ func stdinControl(reconnectCh chan supervisor.ReconnectSignal, log *zerolog.Logg
|
||||
}
|
||||
log.Info().Msgf("Sending %+v", reconnect)
|
||||
reconnectCh <- reconnect
|
||||
case "help":
|
||||
log.Info().Msg(helpStr)
|
||||
default:
|
||||
log.Info().Str(LogFieldCommand, command).Msg("Unknown command")
|
||||
fallthrough
|
||||
case "help":
|
||||
log.Info().Msg(`Supported command:
|
||||
reconnect [delay]
|
||||
- restarts one randomly chosen connection with optional delay before reconnect`)
|
||||
log.Info().Msg(helpStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,13 +111,6 @@ func isSecretEnvVar(key string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func dnsProxyStandAlone(c *cli.Context, namedTunnel *connection.TunnelProperties) bool {
|
||||
return c.IsSet(flags.ProxyDns) &&
|
||||
!(c.IsSet(flags.Name) || // adhoc-named tunnel
|
||||
c.IsSet(ingress.HelloWorldFlag) || // quick or named tunnel
|
||||
namedTunnel != nil) // named tunnel
|
||||
}
|
||||
|
||||
func prepareTunnelConfig(
|
||||
ctx context.Context,
|
||||
c *cli.Context,
|
||||
@@ -147,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
|
||||
}
|
||||
@@ -175,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")
|
||||
}
|
||||
@@ -268,6 +251,8 @@ func prepareTunnelConfig(
|
||||
DisableQUICPathMTUDiscovery: c.Bool(flags.QuicDisablePathMTUDiscovery),
|
||||
QUICConnectionLevelFlowControlLimit: c.Uint64(flags.QuicConnLevelFlowControlLimit),
|
||||
QUICStreamLevelFlowControlLimit: c.Uint64(flags.QuicStreamLevelFlowControlLimit),
|
||||
NoPrechecks: c.Bool(flags.NoPrechecks),
|
||||
Prechecks: c.Bool(flags.Prechecks),
|
||||
OriginDNSService: dnsService,
|
||||
OriginDialerService: originDialerService,
|
||||
}
|
||||
@@ -307,7 +292,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
|
||||
@@ -348,7 +333,7 @@ func testIPBindable(ip net.IP) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listener.Close()
|
||||
_ = listener.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -510,7 +495,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))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func runDNSProxyServer(c *cli.Context, dnsReadySignal chan struct{}, shutdownC <-chan struct{}, log *zerolog.Logger) error {
|
||||
port := c.Int("proxy-dns-port")
|
||||
if port <= 0 || port > 65535 {
|
||||
return errors.New("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.")
|
||||
}
|
||||
maxUpstreamConnections := c.Int("proxy-dns-max-upstream-conns")
|
||||
if maxUpstreamConnections < 0 {
|
||||
return fmt.Errorf("'%s' must be 0 or higher", "proxy-dns-max-upstream-conns")
|
||||
}
|
||||
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"), c.StringSlice("proxy-dns-bootstrap"), maxUpstreamConnections, log)
|
||||
if err != nil {
|
||||
close(dnsReadySignal)
|
||||
listener.Stop()
|
||||
return errors.Wrap(err, "Cannot create the DNS over HTTPS proxy server")
|
||||
}
|
||||
|
||||
err = listener.Start(dnsReadySignal)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot start the DNS over HTTPS proxy server")
|
||||
}
|
||||
<-shutdownC
|
||||
_ = listener.Stop()
|
||||
log.Info().Msg("DNS server stopped")
|
||||
return nil
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
+23
-6
@@ -30,7 +30,7 @@ class CloudflaredCli:
|
||||
listed = self._run_command(cmd_args, "list")
|
||||
return json.loads(listed.stdout)
|
||||
|
||||
def get_management_token(self, config, config_path):
|
||||
def get_management_token(self, config, config_path, resource):
|
||||
basecmd = [config.cloudflared_binary]
|
||||
if config_path is not None:
|
||||
basecmd += ["--config", str(config_path)]
|
||||
@@ -38,18 +38,35 @@ class CloudflaredCli:
|
||||
if origincert:
|
||||
basecmd += ["--origincert", origincert]
|
||||
|
||||
cmd_args = ["tail", "token", config.get_tunnel_id()]
|
||||
cmd_args = ["management", "token", "--resource", resource, config.get_tunnel_id()]
|
||||
cmd = basecmd + cmd_args
|
||||
result = run_subprocess(cmd, "token", self.logger, check=True, capture_output=True, timeout=15)
|
||||
return json.loads(result.stdout.decode("utf-8").strip())["token"]
|
||||
|
||||
def get_management_url(self, path, config, config_path):
|
||||
access_jwt = self.get_management_token(config, config_path)
|
||||
def get_tail_token(self, config, config_path):
|
||||
"""
|
||||
Get management token using the 'tail token' command.
|
||||
Returns a token scoped for 'logs' resource.
|
||||
"""
|
||||
basecmd = [config.cloudflared_binary]
|
||||
if config_path is not None:
|
||||
basecmd += ["--config", str(config_path)]
|
||||
origincert = get_config_from_file()["origincert"]
|
||||
if origincert:
|
||||
basecmd += ["--origincert", origincert]
|
||||
|
||||
cmd_args = ["tail", "token", config.get_tunnel_id()]
|
||||
cmd = basecmd + cmd_args
|
||||
result = run_subprocess(cmd, "tail-token", self.logger, check=True, capture_output=True, timeout=15)
|
||||
return json.loads(result.stdout.decode("utf-8").strip())["token"]
|
||||
|
||||
def get_management_url(self, path, config, config_path, resource):
|
||||
access_jwt = self.get_management_token(config, config_path, resource)
|
||||
connector_id = get_tunnel_connector_id()
|
||||
return f"https://{MANAGEMENT_HOST_NAME}/{path}?connector_id={connector_id}&access_token={access_jwt}"
|
||||
|
||||
def get_management_wsurl(self, path, config, config_path):
|
||||
access_jwt = self.get_management_token(config, config_path)
|
||||
def get_management_wsurl(self, path, config, config_path, resource):
|
||||
access_jwt = self.get_management_token(config, config_path, resource)
|
||||
connector_id = get_tunnel_connector_id()
|
||||
return f"wss://{MANAGEMENT_HOST_NAME}/{path}?connector_id={connector_id}&access_token={access_jwt}"
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import base64
|
||||
|
||||
from dataclasses import dataclass, InitVar
|
||||
|
||||
from constants import METRICS_PORT, PROXY_DNS_PORT
|
||||
from constants import METRICS_PORT
|
||||
|
||||
# frozen=True raises exception when assigning to fields. This emulates immutability
|
||||
|
||||
@@ -99,10 +99,3 @@ class QuickTunnelConfig(BaseConfig):
|
||||
object.__setattr__(self, 'full_config',
|
||||
self.merge_config(additional_config))
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProxyDnsConfig(BaseConfig):
|
||||
full_config = {
|
||||
"port": PROXY_DNS_PORT,
|
||||
"no-autoupdate": True,
|
||||
}
|
||||
|
||||
|
||||
@@ -5,15 +5,14 @@ from time import sleep
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from config import NamedTunnelConfig, ProxyDnsConfig, QuickTunnelConfig
|
||||
from constants import BACKOFF_SECS, PROXY_DNS_PORT
|
||||
from config import NamedTunnelConfig, QuickTunnelConfig
|
||||
from constants import BACKOFF_SECS
|
||||
from util import LOGGER
|
||||
|
||||
|
||||
class CfdModes(Enum):
|
||||
NAMED = auto()
|
||||
QUICK = auto()
|
||||
PROXY_DNS = auto()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -26,16 +25,7 @@ def component_tests_config():
|
||||
config = yaml.safe_load(stream)
|
||||
LOGGER.info(f"component tests base config {config}")
|
||||
|
||||
def _component_tests_config(additional_config={}, cfd_mode=CfdModes.NAMED, run_proxy_dns=True, provide_ingress=True):
|
||||
if run_proxy_dns:
|
||||
# Regression test for TUN-4177, running with proxy-dns should not prevent tunnels from running.
|
||||
# So we run all tests with it.
|
||||
additional_config["proxy-dns"] = True
|
||||
additional_config["proxy-dns-port"] = PROXY_DNS_PORT
|
||||
else:
|
||||
additional_config.pop("proxy-dns", None)
|
||||
additional_config.pop("proxy-dns-port", None)
|
||||
|
||||
def _component_tests_config(additional_config={}, cfd_mode=CfdModes.NAMED, provide_ingress=True):
|
||||
# Allows the ingress rules to be omitted from the provided config
|
||||
ingress = []
|
||||
if provide_ingress:
|
||||
@@ -51,8 +41,6 @@ def component_tests_config():
|
||||
credentials_file=config['credentials_file'],
|
||||
ingress=ingress,
|
||||
hostname=hostname)
|
||||
elif cfd_mode is CfdModes.PROXY_DNS:
|
||||
return ProxyDnsConfig(cloudflared_binary=config['cloudflared_binary'])
|
||||
elif cfd_mode is CfdModes.QUICK:
|
||||
return QuickTunnelConfig(additional_config=additional_config, cloudflared_binary=config['cloudflared_binary'])
|
||||
else:
|
||||
|
||||
@@ -3,7 +3,6 @@ MAX_RETRIES = 5
|
||||
BACKOFF_SECS = 7
|
||||
MAX_LOG_LINES = 50
|
||||
|
||||
PROXY_DNS_PORT = 9053
|
||||
MANAGEMENT_HOST_NAME = "management.argotunnel.com"
|
||||
|
||||
|
||||
|
||||
@@ -20,12 +20,21 @@ class TestEdgeDiscovery:
|
||||
@pytest.mark.parametrize("protocol", protocols())
|
||||
def test_default_only(self, tmp_path, component_tests_config, protocol):
|
||||
"""
|
||||
This test runs a tunnel to connect via IPv4-only edge addresses (default is unset "--edge-ip-version 4")
|
||||
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():
|
||||
pytest.skip("Host has IPv6 only support and current default is IPv4 only")
|
||||
self.expect_address_connections(
|
||||
tmp_path, component_tests_config, protocol, None, self.expect_ipv4_address)
|
||||
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,10 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
import json
|
||||
import requests
|
||||
from conftest import CfdModes
|
||||
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
|
||||
from retrying import retry
|
||||
from cli import CloudflaredCli
|
||||
from util import LOGGER, write_config, start_cloudflared, wait_tunnel_ready, send_requests
|
||||
from util import LOGGER, write_config, start_cloudflared, wait_tunnel_ready, send_requests, decode_jwt_payload
|
||||
import platform
|
||||
|
||||
"""
|
||||
@@ -25,7 +26,7 @@ class TestManagement:
|
||||
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||
if platform.system() == "Windows":
|
||||
return
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
headers = {}
|
||||
headers["Content-Type"] = "application/json"
|
||||
@@ -35,7 +36,7 @@ class TestManagement:
|
||||
require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
connector_id = cfd_cli.get_connector_id(config)[0]
|
||||
url = cfd_cli.get_management_url("host_details", config, config_path)
|
||||
url = cfd_cli.get_management_url("host_details", config, config_path, resource="host_details")
|
||||
resp = send_request(url, headers=headers)
|
||||
|
||||
# Assert response json.
|
||||
@@ -52,13 +53,13 @@ class TestManagement:
|
||||
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||
if platform.system() == "Windows":
|
||||
return
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_url("metrics", config, config_path)
|
||||
url = cfd_cli.get_management_url("metrics", config, config_path, resource="admin")
|
||||
resp = send_request(url)
|
||||
|
||||
# Assert response.
|
||||
@@ -73,13 +74,13 @@ class TestManagement:
|
||||
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||
if platform.system() == "Windows":
|
||||
return
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_url("debug/pprof/heap", config, config_path)
|
||||
url = cfd_cli.get_management_url("debug/pprof/heap", config, config_path, resource="admin")
|
||||
resp = send_request(url)
|
||||
|
||||
# Assert response.
|
||||
@@ -94,18 +95,51 @@ class TestManagement:
|
||||
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||
if platform.system() == "Windows":
|
||||
return
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1", "--management-diagnostics=false"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_url("metrics", config, config_path)
|
||||
url = cfd_cli.get_management_url("metrics", config, config_path, resource="admin")
|
||||
resp = send_request(url)
|
||||
|
||||
# Assert response.
|
||||
assert resp.status_code == 404, "Expected cloudflared to return 404 for /metrics"
|
||||
|
||||
def test_tail_token_command(self, tmp_path, component_tests_config):
|
||||
"""
|
||||
Validates that 'cloudflared tail token' command returns a token
|
||||
scoped for 'logs' and 'ping' resources.
|
||||
"""
|
||||
# TUN-7377: wait_tunnel_ready does not work properly in windows
|
||||
if platform.system() == "Windows":
|
||||
return
|
||||
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
token = cfd_cli.get_tail_token(config, config_path)
|
||||
|
||||
# Verify token was returned
|
||||
assert token, "Expected non-empty token to be returned"
|
||||
|
||||
# Decode JWT payload to verify resource claims
|
||||
claims = decode_jwt_payload(token)
|
||||
|
||||
resource_tag = 'res'
|
||||
# Verify the token has 'logs' and 'ping' in resource array
|
||||
assert resource_tag in claims, f"Expected {resource_tag} claim in token"
|
||||
assert isinstance(claims['res'], list), f"Expected {resource_tag} to be an array"
|
||||
assert 'logs' in claims[resource_tag], \
|
||||
f"Expected 'logs' in resource array, got: {claims[resource_tag]}"
|
||||
assert 'ping' in claims[resource_tag], \
|
||||
f"Expected 'ping' in resource array, got: {claims[resource_tag]}"
|
||||
|
||||
LOGGER.info(f"Tail token successfully verified with resources: {claims[resource_tag]}")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import socket
|
||||
from time import sleep
|
||||
|
||||
import constants
|
||||
from conftest import CfdModes
|
||||
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected
|
||||
|
||||
|
||||
# Sanity checks that test that we only run Proxy DNS and Tunnel when we really expect them to be there.
|
||||
class TestProxyDns:
|
||||
def test_proxy_dns_with_named_tunnel(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.NAMED, run_proxy_dns=True)
|
||||
|
||||
def test_proxy_dns_alone(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.PROXY_DNS, run_proxy_dns=True)
|
||||
|
||||
def test_named_tunnel_alone(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.NAMED, run_proxy_dns=False)
|
||||
|
||||
|
||||
def run_test_scenario(tmp_path, component_tests_config, cfd_mode, run_proxy_dns):
|
||||
expect_proxy_dns = run_proxy_dns
|
||||
expect_tunnel = False
|
||||
|
||||
if cfd_mode == CfdModes.NAMED:
|
||||
expect_tunnel = True
|
||||
pre_args = ["tunnel", "--ha-connections", "1"]
|
||||
args = ["run"]
|
||||
elif cfd_mode == CfdModes.PROXY_DNS:
|
||||
expect_proxy_dns = True
|
||||
pre_args = []
|
||||
args = ["proxy-dns", "--port", str(constants.PROXY_DNS_PORT)]
|
||||
else:
|
||||
assert False, f"Unknown cfd_mode {cfd_mode}"
|
||||
|
||||
config = component_tests_config(cfd_mode=cfd_mode, run_proxy_dns=run_proxy_dns)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=pre_args, cfd_args=args, new_process=True, capture_output=False):
|
||||
if expect_tunnel:
|
||||
wait_tunnel_ready()
|
||||
else:
|
||||
check_tunnel_not_connected()
|
||||
verify_proxy_dns(expect_proxy_dns)
|
||||
|
||||
|
||||
def verify_proxy_dns(should_be_running):
|
||||
# Wait for the Proxy DNS listener to come up.
|
||||
sleep(constants.BACKOFF_SECS)
|
||||
had_failure = False
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.connect(('localhost', constants.PROXY_DNS_PORT))
|
||||
sock.send(b"anything")
|
||||
except:
|
||||
if should_be_running:
|
||||
assert False, "Expected Proxy DNS to be running, but it was not."
|
||||
had_failure = True
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
if not should_be_running and not had_failure:
|
||||
assert False, "Proxy DNS should not have been running, but it was."
|
||||
@@ -6,7 +6,7 @@ from util import LOGGER, start_cloudflared, wait_tunnel_ready, get_quicktunnel_u
|
||||
|
||||
class TestQuickTunnels:
|
||||
def test_quick_tunnel(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.QUICK)
|
||||
LOGGER.debug(config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
@@ -15,22 +15,10 @@ class TestQuickTunnels:
|
||||
send_requests(url, 3, True)
|
||||
|
||||
def test_quick_tunnel_url(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.QUICK)
|
||||
LOGGER.debug(config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--url", f"http://localhost:{METRICS_PORT}/"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
time.sleep(10)
|
||||
url = get_quicktunnel_url()
|
||||
send_requests(url+"/ready", 3, True)
|
||||
|
||||
def test_quick_tunnel_proxy_dns_url(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=True)
|
||||
LOGGER.debug(config)
|
||||
failed_start = start_cloudflared(tmp_path, config, cfd_args=["--url", f"http://localhost:{METRICS_PORT}/"], expect_success=False)
|
||||
assert failed_start.returncode == 1, "Expected cloudflared to fail to run with `proxy-dns` and `hello-world`"
|
||||
|
||||
def test_quick_tunnel_proxy_dns_hello_world(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=True)
|
||||
LOGGER.debug(config)
|
||||
failed_start = start_cloudflared(tmp_path, config, cfd_args=["--hello-world"], expect_success=False)
|
||||
assert failed_start.returncode == 1, "Expected cloudflared to fail to run with `proxy-dns` and `url`"
|
||||
|
||||
@@ -19,13 +19,13 @@ class TestTail:
|
||||
with the access token and start and stop streaming on-demand.
|
||||
"""
|
||||
print("test_start_stop_streaming")
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_wsurl("logs", config, config_path)
|
||||
url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs")
|
||||
async with connect(url, open_timeout=5, close_timeout=3) as websocket:
|
||||
await websocket.send('{"type": "start_streaming"}')
|
||||
await websocket.send('{"type": "stop_streaming"}')
|
||||
@@ -38,13 +38,13 @@ class TestTail:
|
||||
Validates that a streaming logs connection will stream logs
|
||||
"""
|
||||
print("test_streaming_logs")
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_wsurl("logs", config, config_path)
|
||||
url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs")
|
||||
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
|
||||
# send start_streaming
|
||||
await websocket.send(json.dumps({
|
||||
@@ -65,13 +65,13 @@ class TestTail:
|
||||
but not http when filters applied.
|
||||
"""
|
||||
print("test_streaming_logs_filters")
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_wsurl("logs", config, config_path)
|
||||
url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs")
|
||||
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
|
||||
# send start_streaming with tcp logs only
|
||||
await websocket.send(json.dumps({
|
||||
@@ -92,13 +92,13 @@ class TestTail:
|
||||
Validates that a streaming logs connection will stream logs with sampling.
|
||||
"""
|
||||
print("test_streaming_logs_sampling")
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_wsurl("logs", config, config_path)
|
||||
url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs")
|
||||
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
|
||||
# send start_streaming with info logs only
|
||||
await websocket.send(json.dumps({
|
||||
@@ -120,13 +120,13 @@ class TestTail:
|
||||
Validates that a streaming logs session can be overriden by the same actor
|
||||
"""
|
||||
print("test_streaming_logs_actor_override")
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_wsurl("logs", config, config_path)
|
||||
url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs")
|
||||
task = asyncio.ensure_future(start_streaming_to_be_remotely_closed(url))
|
||||
override_task = asyncio.ensure_future(start_streaming_override(url))
|
||||
await asyncio.wait([task, override_task])
|
||||
|
||||
@@ -11,14 +11,14 @@ class TestTunnel:
|
||||
'''Test tunnels with no ingress rules from config.yaml but ingress rules from CLI only'''
|
||||
|
||||
def test_tunnel_hello_world(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(),
|
||||
require_min_connections=1)
|
||||
|
||||
def test_tunnel_url(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--url", f"http://localhost:{METRICS_PORT}/"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
@@ -29,7 +29,7 @@ class TestTunnel:
|
||||
Running a tunnel with no ingress rules provided from either config.yaml or CLI will still work but return 503
|
||||
for all incoming requests.
|
||||
'''
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
|
||||
@@ -185,3 +185,49 @@ def send_request(session, url, require_ok):
|
||||
if require_ok:
|
||||
assert resp.status_code == 200, f"{url} returned {resp}"
|
||||
return resp if resp.status_code == 200 else None
|
||||
|
||||
|
||||
def decode_jwt_payload(token):
|
||||
"""
|
||||
Decode the payload section of a JWT token without signature verification.
|
||||
|
||||
JWT Structure:
|
||||
==============
|
||||
A JWT consists of three Base64URL-encoded parts separated by dots:
|
||||
HEADER.PAYLOAD.SIGNATURE
|
||||
|
||||
The payload contains the JWT claims (the actual data/permissions).
|
||||
|
||||
Args:
|
||||
token (str): The complete JWT token string
|
||||
|
||||
Returns:
|
||||
dict: The decoded payload as a dictionary containing JWT claims
|
||||
|
||||
Raises:
|
||||
ValueError: If the token doesn't have exactly 3 parts
|
||||
|
||||
Note:
|
||||
This function does NOT verify the signature - it only decodes the payload.
|
||||
Use this only when you trust the token source (e.g., tokens you just generated).
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
|
||||
# Split JWT into its three components
|
||||
parts = token.split('.')
|
||||
if len(parts) != 3:
|
||||
raise ValueError(f"Invalid JWT format: expected 3 parts, got {len(parts)}")
|
||||
|
||||
# Extract and decode the payload (middle section)
|
||||
# Base64 requires padding to be a multiple of 4 characters
|
||||
payload_encoded = parts[1]
|
||||
remainder = len(payload_encoded) % 4
|
||||
if remainder != 0:
|
||||
payload_padded = payload_encoded + '=' * (4 - remainder)
|
||||
else:
|
||||
payload_padded = payload_encoded
|
||||
|
||||
# Decode from Base64URL format and parse JSON
|
||||
decoded_payload = base64.urlsafe_b64decode(payload_padded)
|
||||
return json.loads(decoded_payload)
|
||||
|
||||
+2
-72
@@ -4,9 +4,6 @@ import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
)
|
||||
|
||||
// Forwarder represents a client side listener to forward traffic to the edge
|
||||
@@ -26,23 +23,13 @@ type Tunnel struct {
|
||||
ProtocolType string `json:"type"`
|
||||
}
|
||||
|
||||
// DNSResolver represents a client side DNS resolver
|
||||
type DNSResolver struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Address string `json:"address,omitempty"`
|
||||
Port uint16 `json:"port,omitempty"`
|
||||
Upstreams []string `json:"upstreams,omitempty"`
|
||||
Bootstraps []string `json:"bootstraps,omitempty"`
|
||||
MaxUpstreamConnections int `json:"max_upstream_connections,omitempty"`
|
||||
}
|
||||
|
||||
// Root is the base options to configure the service
|
||||
// Root is the base options to configure the service.
|
||||
type Root struct {
|
||||
LogDirectory string `json:"log_directory" yaml:"logDirectory,omitempty"`
|
||||
LogLevel string `json:"log_level" yaml:"logLevel,omitempty"`
|
||||
Forwarders []Forwarder `json:"forwarders,omitempty" yaml:"forwarders,omitempty"`
|
||||
Tunnels []Tunnel `json:"tunnels,omitempty" yaml:"tunnels,omitempty"`
|
||||
Resolver DNSResolver `json:"resolver,omitempty" yaml:"resolver,omitempty"`
|
||||
// `resolver` key is reserved for a removed feature (proxy-dns) and should not be used.
|
||||
}
|
||||
|
||||
// Hash returns the computed values to see if the forwarder values change
|
||||
@@ -55,60 +42,3 @@ func (f *Forwarder) Hash() string {
|
||||
_, _ = io.WriteString(h, f.Destination)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// Hash returns the computed values to see if the forwarder values change
|
||||
func (r *DNSResolver) Hash() string {
|
||||
h := sha256.New()
|
||||
_, _ = io.WriteString(h, r.Address)
|
||||
_, _ = io.WriteString(h, strings.Join(r.Bootstraps, ","))
|
||||
_, _ = io.WriteString(h, strings.Join(r.Upstreams, ","))
|
||||
_, _ = io.WriteString(h, fmt.Sprintf("%d", r.Port))
|
||||
_, _ = io.WriteString(h, fmt.Sprintf("%d", r.MaxUpstreamConnections))
|
||||
_, _ = io.WriteString(h, fmt.Sprintf("%v", r.Enabled))
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// EnabledOrDefault returns the enabled property
|
||||
func (r *DNSResolver) EnabledOrDefault() bool {
|
||||
return r.Enabled
|
||||
}
|
||||
|
||||
// AddressOrDefault returns the address or returns the default if empty
|
||||
func (r *DNSResolver) AddressOrDefault() string {
|
||||
if r.Address != "" {
|
||||
return r.Address
|
||||
}
|
||||
return "localhost"
|
||||
}
|
||||
|
||||
// PortOrDefault return the port or returns the default if 0
|
||||
func (r *DNSResolver) PortOrDefault() uint16 {
|
||||
if r.Port > 0 {
|
||||
return r.Port
|
||||
}
|
||||
return 53
|
||||
}
|
||||
|
||||
// UpstreamsOrDefault returns the upstreams or returns the default if empty
|
||||
func (r *DNSResolver) UpstreamsOrDefault() []string {
|
||||
if len(r.Upstreams) > 0 {
|
||||
return r.Upstreams
|
||||
}
|
||||
return []string{"https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"}
|
||||
}
|
||||
|
||||
// BootstrapsOrDefault returns the bootstraps or returns the default if empty
|
||||
func (r *DNSResolver) BootstrapsOrDefault() []string {
|
||||
if len(r.Bootstraps) > 0 {
|
||||
return r.Bootstraps
|
||||
}
|
||||
return []string{"https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"}
|
||||
}
|
||||
|
||||
// MaxUpstreamConnectionsOrDefault return the max upstream connections or returns the default if negative
|
||||
func (r *DNSResolver) MaxUpstreamConnectionsOrDefault() int {
|
||||
if r.MaxUpstreamConnections >= 0 {
|
||||
return r.MaxUpstreamConnections
|
||||
}
|
||||
return tunneldns.MaxUpstreamConnsDefault
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+21
-9
@@ -19,6 +19,9 @@ const (
|
||||
edgeH2TLSServerName = "h2.cftunnel.com"
|
||||
// edgeQUICServerName is the server name to establish quic connection with edge.
|
||||
edgeQUICServerName = "quic.cftunnel.com"
|
||||
// probeTLSServerName is the server name used for pre-flight connectivity checks.
|
||||
probeTLSServerName = "probe.cftunnel.com"
|
||||
quicProtos = "argotunnel"
|
||||
AutoSelectFlag = "auto"
|
||||
// SRV and TXT record resolution TTL
|
||||
ResolveTTL = time.Hour
|
||||
@@ -69,7 +72,24 @@ func (p Protocol) TLSSettings() *TLSSettings {
|
||||
case QUIC:
|
||||
return &TLSSettings{
|
||||
ServerName: edgeQUICServerName,
|
||||
NextProtos: []string{"argotunnel"},
|
||||
NextProtos: []string{quicProtos},
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ProbeTLSSettings returns TLS settings for pre-flight connectivity checks.
|
||||
func (p Protocol) ProbeTLSSettings() *TLSSettings {
|
||||
switch p {
|
||||
case HTTP2:
|
||||
return &TLSSettings{
|
||||
ServerName: probeTLSServerName,
|
||||
}
|
||||
case QUIC:
|
||||
return &TLSSettings{
|
||||
ServerName: probeTLSServerName,
|
||||
NextProtos: []string{quicProtos},
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
@@ -204,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)
|
||||
|
||||
+54
-34
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
)
|
||||
@@ -14,15 +15,6 @@ const (
|
||||
testAccountTag = "testAccountTag"
|
||||
)
|
||||
|
||||
func mockFetcher(getError bool, protocolPercent ...edgediscovery.ProtocolPercent) edgediscovery.PercentageFetcher {
|
||||
return func() (edgediscovery.ProtocolPercents, error) {
|
||||
if getError {
|
||||
return nil, fmt.Errorf("failed to fetch percentage")
|
||||
}
|
||||
return protocolPercent, nil
|
||||
}
|
||||
}
|
||||
|
||||
type dynamicMockFetcher struct {
|
||||
protocolPercents edgediscovery.ProtocolPercents
|
||||
err error
|
||||
@@ -39,7 +31,6 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
name string
|
||||
protocol string
|
||||
tunnelTokenProvided bool
|
||||
needPQ bool
|
||||
expectedProtocol Protocol
|
||||
hasFallback bool
|
||||
expectedFallback Protocol
|
||||
@@ -67,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{
|
||||
@@ -87,16 +66,16 @@ 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, fmt.Sprintf("test %s failed", test.name))
|
||||
assert.Error(t, err, "test %s failed", test.name)
|
||||
} else {
|
||||
assert.NoError(t, err, fmt.Sprintf("test %s failed", test.name))
|
||||
assert.Equal(t, test.expectedProtocol, selector.Current(), fmt.Sprintf("test %s failed", test.name))
|
||||
require.NoError(t, err, "test %s failed", test.name)
|
||||
assert.Equalf(t, test.expectedProtocol, selector.Current(), "test %s failed", test.name)
|
||||
fallback, ok := selector.Fallback()
|
||||
assert.Equal(t, test.hasFallback, ok, fmt.Sprintf("test %s failed", test.name))
|
||||
assert.Equalf(t, test.hasFallback, ok, "test %s failed", test.name)
|
||||
if test.hasFallback {
|
||||
assert.Equal(t, test.expectedFallback, fallback, fmt.Sprintf("test %s failed", test.name))
|
||||
assert.Equalf(t, test.expectedFallback, fallback, "test %s failed", test.name)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -105,8 +84,8 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
|
||||
func TestAutoProtocolSelectorRefresh(t *testing.T) {
|
||||
fetcher := dynamicMockFetcher{}
|
||||
selector, err := NewProtocolSelector(AutoSelectFlag, testAccountTag, false, false, fetcher.fetch(), testNoTTL, &log)
|
||||
assert.NoError(t, err)
|
||||
selector, err := NewProtocolSelector(AutoSelectFlag, testAccountTag, false, fetcher.fetch(), testNoTTL, &log)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, QUIC, selector.Current())
|
||||
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}}
|
||||
@@ -135,8 +114,8 @@ 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)
|
||||
assert.NoError(t, err)
|
||||
selector, err := NewProtocolSelector(HTTP2.String(), testAccountTag, false, fetcher.fetch(), testNoTTL, &log)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}}
|
||||
@@ -164,10 +143,51 @@ func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
|
||||
|
||||
func TestAutoProtocolSelectorNoRefreshWithToken(t *testing.T) {
|
||||
fetcher := dynamicMockFetcher{}
|
||||
selector, err := NewProtocolSelector(AutoSelectFlag, testAccountTag, true, false, fetcher.fetch(), testNoTTL, &log)
|
||||
assert.NoError(t, err)
|
||||
selector, err := NewProtocolSelector(AutoSelectFlag, testAccountTag, true, fetcher.fetch(), testNoTTL, &log)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, QUIC, selector.Current())
|
||||
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}}
|
||||
assert.Equal(t, QUIC, selector.Current())
|
||||
}
|
||||
|
||||
func TestProbeTLSSettings(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
protocol Protocol
|
||||
expectedServer string
|
||||
expectedProtos []string
|
||||
expectNil bool
|
||||
}{
|
||||
{
|
||||
name: "HTTP2 returns probe SNI",
|
||||
protocol: HTTP2,
|
||||
expectedServer: probeTLSServerName,
|
||||
expectedProtos: nil,
|
||||
},
|
||||
{
|
||||
name: "QUIC returns probe SNI with alpn",
|
||||
protocol: QUIC,
|
||||
expectedServer: probeTLSServerName,
|
||||
expectedProtos: []string{"argotunnel"},
|
||||
},
|
||||
{
|
||||
name: "Unknown protocol returns nil",
|
||||
protocol: Protocol(999),
|
||||
expectNil: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
settings := test.protocol.ProbeTLSSettings()
|
||||
if test.expectNil {
|
||||
assert.Nil(t, settings)
|
||||
} else {
|
||||
assert.NotNil(t, settings)
|
||||
assert.Equal(t, test.expectedServer, settings.ServerName)
|
||||
assert.Equal(t, test.expectedProtos, settings.NextProtos)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+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"
|
||||
@@ -149,7 +151,6 @@ func TestQUICServer(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
test := test // capture range variable
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
// Start a UDP Listener for QUIC.
|
||||
@@ -157,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)
|
||||
@@ -499,7 +500,6 @@ func TestBuildHTTPRequest(t *testing.T) {
|
||||
|
||||
log := zerolog.Nop()
|
||||
for _, test := range tests {
|
||||
test := test // capture range variable
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req, err := buildHTTPRequest(t.Context(), test.connectRequest, test.body, 0, &log)
|
||||
require.NoError(t, err)
|
||||
@@ -525,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())
|
||||
|
||||
@@ -616,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)
|
||||
@@ -660,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 {
|
||||
@@ -671,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()
|
||||
@@ -721,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)
|
||||
@@ -815,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 {
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
module github.com/cloudflare/cloudflared
|
||||
|
||||
go 1.24
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/coredns/coredns v1.12.2
|
||||
github.com/coreos/go-oidc/v3 v3.10.0
|
||||
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
|
||||
github.com/fortytw2/leaktest v1.3.0
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/getsentry/sentry-go v0.16.0
|
||||
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.0
|
||||
github.com/go-jose/go-jose/v4 v4.1.3
|
||||
github.com/gobwas/ws v1.2.1
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/mattn/go-colorable v0.1.13
|
||||
github.com/miekg/dns v1.1.66
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
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/stretchr/testify v1.10.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.35.0
|
||||
go.opentelemetry.io/otel v1.40.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0
|
||||
go.opentelemetry.io/otel/sdk v1.35.0
|
||||
go.opentelemetry.io/otel/trace v1.35.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.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.33.0
|
||||
golang.org/x/term v0.32.0
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/sys v0.43.0
|
||||
golang.org/x/term v0.42.0
|
||||
google.golang.org/protobuf v1.36.6
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -50,51 +50,51 @@ require (
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.0 // indirect
|
||||
github.com/apparentlymart/go-cidr v1.1.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.12.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coredns/caddy v1.1.2-0.20241029205200-8de985351a98 // 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
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
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/golang/protobuf v1.5.4 // 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-opentracing v0.0.0-20180507213350-8e809c8a8645 // 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/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // 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
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.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
|
||||
golang.org/x/arch v0.4.0 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/mod v0.34.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
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
golang.org/x/tools v0.43.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
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
|
||||
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
|
||||
github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bytedance/sonic v1.12.0 h1:YGPgxF9xzaCNvd/ZKdQ28yRovhfMFZQjuk6fKBzZ3ls=
|
||||
@@ -13,16 +11,14 @@ 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=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coredns/caddy v1.1.2-0.20241029205200-8de985351a98 h1:c+Epklw9xk6BZ1OFBPWLA2PcL8QalKvl3if8CP9x8uw=
|
||||
github.com/coredns/caddy v1.1.2-0.20241029205200-8de985351a98/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4=
|
||||
github.com/coredns/coredns v1.12.2 h1:G4oDfi340zlVsriZ8nYiUemiQIew7nqOO+QPvPxIA4Y=
|
||||
github.com/coredns/coredns v1.12.2/go.mod h1:GFz31oVOfCyMArFoypfu1SoaFoNkbdh6lDxtF1B6vfU=
|
||||
github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
|
||||
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
@@ -33,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=
|
||||
@@ -43,16 +41,14 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojt
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/getsentry/sentry-go v0.16.0 h1:owk+S+5XcgJLlGR/3+3s6N4d+uKwqYvh/eS0AIMjPWo=
|
||||
github.com/getsentry/sentry-go v0.16.0/go.mod h1:ZXCloQLj0pG7mja5NK6NPf2V4A88YJ4pNlc2mOHwh6Y=
|
||||
github.com/getsentry/sentry-go v0.43.0 h1:XbXLpFicpo8HmBDaInk7dum18G9KSLcjZiyUKS+hLW4=
|
||||
github.com/getsentry/sentry-go v0.43.0/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
@@ -64,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.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||
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-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=
|
||||
@@ -95,7 +93,6 @@ github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/K
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
@@ -109,7 +106,6 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/pprof v0.0.0-20250418163039-24c5476c6587 h1:b/8HpQhvKLSNzH5oTXN2WkNcMl6YB5K3FRbb+i+Ml34=
|
||||
github.com/google/pprof v0.0.0-20250418163039-24c5476c6587/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
@@ -117,8 +113,6 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
|
||||
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-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
|
||||
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=
|
||||
@@ -138,16 +132,14 @@ 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=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
|
||||
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -162,12 +154,10 @@ github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus
|
||||
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
||||
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
|
||||
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -176,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=
|
||||
@@ -186,14 +178,16 @@ github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQP
|
||||
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs=
|
||||
github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo=
|
||||
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=
|
||||
@@ -204,10 +198,14 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
|
||||
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
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=
|
||||
@@ -215,24 +213,26 @@ 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=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
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.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
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.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
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/trace v1.0.0-RC2/go.mod h1:JPQ+z6nNw9mqEGT8o3eoPTdnNI+Aj5JcxEsVGREIAy4=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
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.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
@@ -245,43 +245,44 @@ 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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
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.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
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/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
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/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
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.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
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.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
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.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
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=
|
||||
|
||||
+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"
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@ func FindProtocol(p []byte) (layers.IPProtocol, error) {
|
||||
// Next header is in the 7th byte of IPv6 header
|
||||
return layers.IPProtocol(p[6]), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknow ip version %d", version)
|
||||
return 0, fmt.Errorf("unknown ip version %d", version)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ func (pd *IPDecoder) decodeByVersion(packet []byte) ([]gopacket.LayerType, error
|
||||
case 6:
|
||||
err = pd.v6parser.DecodeLayers(packet, &decoded)
|
||||
default:
|
||||
err = fmt.Errorf("unknow ip version %d", version)
|
||||
err = fmt.Errorf("unknown ip version %d", version)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
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-region results for each transport probe type.
|
||||
// Each slice has one entry per DNS-resolved region, in the same order as dnsResults.
|
||||
type TransportResults struct {
|
||||
QUIC []CheckResult // one per region
|
||||
HTTP2 []CheckResult // one per region
|
||||
ManagementAPI CheckResult // single target, no regions
|
||||
}
|
||||
|
||||
// Collect returns all results as a slice in a consistent order for reporting:
|
||||
// all QUIC rows first (one per region), 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. DNS resolution (sequential – transport probes depend on its output).
|
||||
// 2. QUIC, HTTP/2, and Management API probes run concurrently.
|
||||
//
|
||||
// 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) DNS – must complete before transport probes know which addresses to dial.
|
||||
addrGroups, dnsResults := runDNSProbe(ctx, runDialers.DNSResolver, cfg.Region)
|
||||
|
||||
dnsOK := !slices.ContainsFunc(dnsResults, func(r CheckResult) bool {
|
||||
return r.ProbeStatus != Pass
|
||||
})
|
||||
|
||||
// 2) Run probes concurrently. Each probe type gets its own buffered channel —
|
||||
// one send, one receive, no routing or name-parsing required.
|
||||
var results TransportResults
|
||||
|
||||
mgmtCh := make(chan CheckResult)
|
||||
go func() {
|
||||
mgmtCh <- probeManagementAPIWithRetry(ctx, runDialers.ManagementDialer)
|
||||
}()
|
||||
|
||||
if !dnsOK {
|
||||
// DNS failed: emit one skip row per region so the table stays consistent.
|
||||
results.QUIC = skipResultsForRegions(dnsResults, ProbeTypeQUIC, componentUDPConnectivity)
|
||||
results.HTTP2 = skipResultsForRegions(dnsResults, ProbeTypeHTTP2, componentTCPConnectivity)
|
||||
} else {
|
||||
perRegionAddrs := addrsByRegion(addrGroups, cfg.IPVersion)
|
||||
regionTargets := dnsTargets(dnsResults)
|
||||
|
||||
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,
|
||||
regionTargets, fmt.Sprintf("%s: %v", detailsTLSConfigFailed, quicTLSErr), actionQUICBlocked)
|
||||
return
|
||||
}
|
||||
quicCh <- probeAllRegions(ctx, ProbeTypeQUIC, componentUDPConnectivity,
|
||||
perRegionAddrs, regionTargets,
|
||||
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,
|
||||
regionTargets, fmt.Sprintf("%s: %v", detailsTLSConfigFailed, http2TLSErr), actionHTTP2Blocked)
|
||||
return
|
||||
}
|
||||
http2Ch <- probeAllRegions(ctx, ProbeTypeHTTP2, componentTCPConnectivity,
|
||||
perRegionAddrs, regionTargets,
|
||||
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 region target, used when
|
||||
// TLS config construction fails before any dial is attempted.
|
||||
func tlsConfigErrResults(probeType ProbeType, component string, regionTargets []string, details, action string) []CheckResult {
|
||||
results := make([]CheckResult, len(regionTargets))
|
||||
for i, target := range regionTargets {
|
||||
results[i] = CheckResult{
|
||||
Type: probeType,
|
||||
Component: component,
|
||||
Target: target,
|
||||
ProbeStatus: Fail,
|
||||
Details: details,
|
||||
Action: action,
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func runDNSProbe(ctx context.Context, resolver DNSResolver, region string) ([][]*allregions.EdgeAddr, []CheckResult) {
|
||||
var addrGroups [][]*allregions.EdgeAddr
|
||||
var dnsResults []CheckResult
|
||||
withRetry(ctx, maxRetries, func() bool {
|
||||
addrGroups, dnsResults = probeDNS(resolver, region)
|
||||
for _, r := range dnsResults {
|
||||
if r.ProbeStatus == Fail {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(dnsResults) > 0
|
||||
})
|
||||
return addrGroups, dnsResults
|
||||
}
|
||||
|
||||
// probeAllRegions probes each region sequentially and returns one CheckResult
|
||||
// per region. Within each region, all available addresses (V4 and/or V6) are
|
||||
// tried and the best result is kept.
|
||||
func probeAllRegions(
|
||||
ctx context.Context,
|
||||
probeType ProbeType,
|
||||
component string,
|
||||
perRegionAddrs [][]*allregions.EdgeAddr,
|
||||
regionTargets []string,
|
||||
probeFn func(*allregions.EdgeAddr) CheckResult,
|
||||
) []CheckResult {
|
||||
results := make([]CheckResult, len(perRegionAddrs))
|
||||
for i, addrs := range perRegionAddrs {
|
||||
results[i] = probeRegion(ctx, probeType, component, regionTargets[i], addrs, probeFn)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// probeRegion probes all addresses for a single region (typically one V4 and/or
|
||||
// one V6) and returns the best result. Any address passing means the region is
|
||||
// reachable, so Pass beats Fail within a region.
|
||||
func probeRegion(
|
||||
ctx context.Context,
|
||||
probeType ProbeType,
|
||||
component string,
|
||||
regionTarget string,
|
||||
addrs []*allregions.EdgeAddr,
|
||||
probeFn func(*allregions.EdgeAddr) CheckResult,
|
||||
) CheckResult {
|
||||
if len(addrs) == 0 {
|
||||
return CheckResult{
|
||||
Type: probeType,
|
||||
Component: component,
|
||||
Target: regionTarget,
|
||||
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 = regionTarget
|
||||
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
|
||||
}
|
||||
|
||||
// addrsByRegion returns the addresses to probe for each DNS-resolved region,
|
||||
// preserving the per-region grouping. Each inner slice contains at most one V4
|
||||
// and one V6 address (subject to ipVersion).
|
||||
func addrsByRegion(addrGroups [][]*allregions.EdgeAddr, ipVersion allregions.ConfigIPVersion) [][]*allregions.EdgeAddr {
|
||||
perRegion := 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)
|
||||
}
|
||||
perRegion = append(perRegion, addrs)
|
||||
}
|
||||
return perRegion
|
||||
}
|
||||
|
||||
// dnsTargets extracts the Target hostname from each DNS CheckResult so that
|
||||
// transport probe rows reuse the same region hostnames.
|
||||
func dnsTargets(dnsResults []CheckResult) []string {
|
||||
targets := make([]string, len(dnsResults))
|
||||
for i, r := range dnsResults {
|
||||
targets[i] = r.Target
|
||||
}
|
||||
return targets
|
||||
}
|
||||
|
||||
// skipResultsForRegions returns one skip CheckResult per DNS region, using each
|
||||
// region's hostname as the Target so the output table row aligns with its DNS row.
|
||||
func skipResultsForRegions(dnsResults []CheckResult, probeType ProbeType, component string) []CheckResult {
|
||||
results := make([]CheckResult, len(dnsResults))
|
||||
for i, dns := range dnsResults {
|
||||
results[i] = skipResult(probeType, component, dns.Target)
|
||||
}
|
||||
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,475 @@
|
||||
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_IPv4OnlySkipsV6 verifies that when IPv4Only is configured only V4
|
||||
// addresses are probed (2 regions × 1 V4 = 2 dials per transport).
|
||||
func TestRun_IPv4OnlySkipsV6(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)
|
||||
// IPv4Only: only V4 addresses are probed → 2 regions × 1 V4 = 2 calls each.
|
||||
// V6 addresses must never be dialed.
|
||||
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.IPv4Only},
|
||||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
|
||||
}
|
||||
|
||||
// TestRun_IPv6OnlySkipsV4 verifies that when IPv6Only is configured only V6
|
||||
// addresses are probed (2 regions × 1 V6 = 2 dials per transport).
|
||||
func TestRun_IPv6OnlySkipsV4(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)
|
||||
// IPv6Only: only V6 addresses are probed → 2 regions × 1 V6 = 2 calls each.
|
||||
// V4 addresses must never be dialled.
|
||||
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.IPv6Only},
|
||||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
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 = "QUIC traffic failed to connect to port 7844."
|
||||
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"
|
||||
|
||||
// Details messages for CheckResult.
|
||||
detailsNoAddressesReturned = "No addresses returned"
|
||||
detailsResolvedSuccessfully = "Resolved successfully"
|
||||
detailsHandshakeFailed = "Handshake failed"
|
||||
detailsHandshakeSuccessful = "Handshake successful"
|
||||
detailsBlockedOrUnreachable = "Blocked or unreachable"
|
||||
detailsTLSHandshakeSuccessful = "TLS handshake successful"
|
||||
detailsConnectionFailed = "Connection failed"
|
||||
detailsTCPPortReachable = "TCP port reachable (TLS not validated)"
|
||||
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))
|
||||
}
|
||||
|
||||
// StaticEdgeDNSResolver implements DNSResolver for the --edge flag path.
|
||||
type StaticEdgeDNSResolver struct {
|
||||
Addrs []string
|
||||
Log *zerolog.Logger
|
||||
}
|
||||
|
||||
func (r *StaticEdgeDNSResolver) Resolve(_ string) ([][]*allregions.EdgeAddr, error) {
|
||||
resolved := allregions.ResolveAddrs(r.Addrs, r.Log)
|
||||
if len(resolved) == 0 {
|
||||
return nil, fmt.Errorf("failed to resolve any edge address")
|
||||
}
|
||||
return [][]*allregions.EdgeAddr{resolved}, nil
|
||||
}
|
||||
|
||||
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 a CheckResult for each region discovered. If
|
||||
// resolution fails for all regions, every result will carry StatusFail.
|
||||
func probeDNS(
|
||||
resolver DNSResolver,
|
||||
region string,
|
||||
) ([][]*allregions.EdgeAddr, []CheckResult) {
|
||||
region1Target, region2Target := regionTargets(region)
|
||||
|
||||
addrGroups, err := resolver.Resolve(region)
|
||||
if err != nil || len(addrGroups) == 0 {
|
||||
detail := detailsNoAddressesReturned
|
||||
if err != nil {
|
||||
detail = err.Error()
|
||||
}
|
||||
return nil, []CheckResult{
|
||||
newDNSCheckResult(region1Target, Fail, detail, fmt.Sprintf(actionDNSFail, region1Target, region1Target)),
|
||||
newDNSCheckResult(region2Target, Fail, detail, fmt.Sprintf(actionDNSFail, region2Target, region2Target)),
|
||||
}
|
||||
}
|
||||
|
||||
targets := []string{region1Target, region2Target}
|
||||
|
||||
results := make([]CheckResult, 0, len(addrGroups))
|
||||
for i, group := range addrGroups {
|
||||
target := fmt.Sprintf("region%d.v2.argotunnel.com", i+1)
|
||||
if i < len(targets) {
|
||||
target = targets[i]
|
||||
}
|
||||
if len(group) == 0 {
|
||||
results = append(results, newDNSCheckResult(target, Fail, detailsNoAddressesReturned, fmt.Sprintf(actionDNSFail, target, target)))
|
||||
} else {
|
||||
results = append(results, newDNSCheckResult(target, Pass, detailsResolvedSuccessfully, ""))
|
||||
}
|
||||
}
|
||||
|
||||
return addrGroups, results
|
||||
}
|
||||
|
||||
// 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: detailsHandshakeFailed,
|
||||
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: detailsHandshakeSuccessful,
|
||||
}
|
||||
}
|
||||
|
||||
// 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: detailsBlockedOrUnreachable,
|
||||
Action: actionHTTP2Blocked,
|
||||
}
|
||||
}
|
||||
_ = conn.Close()
|
||||
|
||||
return CheckResult{
|
||||
Type: ProbeTypeHTTP2,
|
||||
Component: componentTCPConnectivity,
|
||||
Target: targetPortHTTP2,
|
||||
ProbeStatus: Pass,
|
||||
Details: detailsTLSHandshakeSuccessful,
|
||||
}
|
||||
}
|
||||
|
||||
// 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: detailsConnectionFailed,
|
||||
Action: actionAPIUnreachable,
|
||||
}
|
||||
}
|
||||
_ = conn.Close()
|
||||
|
||||
return CheckResult{
|
||||
Type: ProbeTypeManagementAPI,
|
||||
Component: componentCloudflareAPI,
|
||||
Target: targetAPI,
|
||||
ProbeStatus: Pass,
|
||||
Details: detailsTCPPortReachable,
|
||||
}
|
||||
}
|
||||
|
||||
func skipResult(probeType ProbeType, component, target string) CheckResult {
|
||||
return CheckResult{
|
||||
Type: probeType,
|
||||
Component: component,
|
||||
Target: target,
|
||||
ProbeStatus: Skip,
|
||||
Details: detailsDNSPrerequisiteFailed,
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
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)
|
||||
|
||||
addrs, results := probeDNS(resolver, "")
|
||||
|
||||
require.NotNil(t, addrs)
|
||||
require.Len(t, results, 1)
|
||||
assert.Len(t, addrs, 1)
|
||||
assert.Equal(t, ProbeTypeDNS, results[0].Type)
|
||||
assert.Equal(t, testRegion1Global, results[0].Target)
|
||||
assert.Equal(t, Pass, results[0].ProbeStatus)
|
||||
assert.Equal(t, detailsResolvedSuccessfully, results[0].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)
|
||||
|
||||
addrs, results := probeDNS(resolver, "")
|
||||
|
||||
require.NotNil(t, addrs)
|
||||
require.Len(t, results, 2)
|
||||
assert.Len(t, addrs, 2)
|
||||
|
||||
assert.Equal(t, testRegion1Global, results[0].Target)
|
||||
assert.Equal(t, Pass, results[0].ProbeStatus)
|
||||
|
||||
assert.Equal(t, testRegion2Global, results[1].Target)
|
||||
assert.Equal(t, Pass, results[1].ProbeStatus)
|
||||
}
|
||||
|
||||
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"))
|
||||
|
||||
addrs, results := probeDNS(resolver, "")
|
||||
|
||||
assert.Nil(t, addrs)
|
||||
require.Len(t, results, 2)
|
||||
|
||||
assert.Equal(t, Fail, results[0].ProbeStatus)
|
||||
assert.Equal(t, "DNS lookup failed", results[0].Details)
|
||||
assert.Contains(t, results[0].Action, testRegion1Global)
|
||||
assert.Contains(t, results[1].Action, testRegion2Global)
|
||||
|
||||
assert.Equal(t, Fail, results[1].ProbeStatus)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
addrs, results := probeDNS(resolver, "")
|
||||
|
||||
assert.Nil(t, addrs)
|
||||
require.Len(t, results, 2)
|
||||
assert.Equal(t, Fail, results[0].ProbeStatus)
|
||||
assert.Equal(t, "No addresses returned", results[0].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)
|
||||
|
||||
addrs, results := probeDNS(resolver, "")
|
||||
|
||||
require.NotNil(t, addrs)
|
||||
require.Len(t, results, 1)
|
||||
assert.Equal(t, Fail, results[0].ProbeStatus)
|
||||
assert.Equal(t, "No addresses returned", results[0].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)
|
||||
|
||||
_, results := probeDNS(resolver, "us")
|
||||
|
||||
require.Len(t, results, 1)
|
||||
assert.Equal(t, testRegion1US, results[0].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, detailsHandshakeSuccessful, 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, detailsHandshakeFailed, 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, detailsHandshakeSuccessful, 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, detailsHandshakeFailed, 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, detailsTLSHandshakeSuccessful, 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, detailsBlockedOrUnreachable, 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, detailsTCPPortReachable, 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, detailsConnectionFailed, 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)")
|
||||
|
||||
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, detailsHandshakeSuccessful, 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package prechecks
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
// Log field names.
|
||||
logFieldRunID = "run_id"
|
||||
logFieldComponent = "component"
|
||||
logFieldTarget = "target"
|
||||
logFieldStatus = "status"
|
||||
logFieldDetails = "details"
|
||||
logFieldHardFail = "hard_fail"
|
||||
logFieldSuggestedProtocol = "suggested_protocol"
|
||||
|
||||
sep = " "
|
||||
)
|
||||
|
||||
// statusLabel returns the display label for a given Status.
|
||||
func (s Status) statusLabel() string {
|
||||
switch s {
|
||||
case Pass:
|
||||
return passStatus
|
||||
case Fail:
|
||||
return failStatus
|
||||
case Skip:
|
||||
return skipStatus
|
||||
default:
|
||||
return unknownStatus
|
||||
}
|
||||
}
|
||||
|
||||
// logString returns the lowercase string used in structured log fields.
|
||||
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 {
|
||||
var buf bytes.Buffer
|
||||
// minwidth=0, tabwidth=8, padding=2, padchar=' ', flags=0
|
||||
w := tabwriter.NewWriter(&buf, 0, 8, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintln(w, "COMPONENT\tTARGET\tSTATUS\tDETAILS")
|
||||
for _, r := range results {
|
||||
_, _ = 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()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
hardFail := r.hasHardFail()
|
||||
var sb strings.Builder
|
||||
for _, res := range r.Results {
|
||||
if res.Action == "" || res.ProbeStatus != Fail {
|
||||
continue
|
||||
}
|
||||
if hardFail {
|
||||
_, _ = fmt.Fprintf(&sb, "ERROR: %s\n", res.Action)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(&sb, "WARNING: %s\n", res.Action)
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// summaryLine builds the SUMMARY: line based on the Report state.
|
||||
func summaryLine(r Report) string {
|
||||
switch {
|
||||
case r.hasHardFail():
|
||||
return "SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel."
|
||||
case r.hasWarn():
|
||||
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:
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// hasHardFail returns true when the environment cannot establish a tunnel:
|
||||
// - Any DNS probe failed, OR
|
||||
// - Both the QUIC and HTTP/2 transport probes failed.
|
||||
//
|
||||
// A single transport failing is not a hard fail — the other transport can be
|
||||
// used as a fallback (degraded state, reported via hasWarn).
|
||||
func (r Report) hasHardFail() bool {
|
||||
var quicFail, http2Fail bool
|
||||
for _, res := range r.Results {
|
||||
if res.ProbeStatus != Fail {
|
||||
continue
|
||||
}
|
||||
switch res.Type {
|
||||
case ProbeTypeDNS:
|
||||
return true
|
||||
case ProbeTypeQUIC:
|
||||
quicFail = true
|
||||
case ProbeTypeHTTP2:
|
||||
http2Fail = true
|
||||
case ProbeTypeManagementAPI:
|
||||
// Management API failure is not a hard fail
|
||||
}
|
||||
}
|
||||
return quicFail && http2Fail
|
||||
}
|
||||
|
||||
// hasWarn returns true when the environment is degraded but functional:
|
||||
// - Exactly one transport (QUIC or HTTP/2) failed, OR
|
||||
// - The Management API is unreachable (auto-updates unavailable).
|
||||
//
|
||||
// Hard-fail conditions (DNS down, both transports blocked) take precedence
|
||||
// and will cause hasWarn to return false.
|
||||
func (r Report) hasWarn() bool {
|
||||
if r.hasHardFail() {
|
||||
return false
|
||||
}
|
||||
var quicFail, http2Fail, apiFail bool
|
||||
for _, res := range r.Results {
|
||||
if res.ProbeStatus != Fail {
|
||||
continue
|
||||
}
|
||||
switch res.Type {
|
||||
case ProbeTypeQUIC:
|
||||
quicFail = true
|
||||
case ProbeTypeHTTP2:
|
||||
http2Fail = true
|
||||
case ProbeTypeManagementAPI:
|
||||
apiFail = true
|
||||
case ProbeTypeDNS:
|
||||
// DNS failures are only relevant for hard failures
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
// LogEvent emits each CheckResult as a structured zerolog log line, followed by
|
||||
// a final summary event. This is the JSON-logging equivalent of String().
|
||||
// Every line carries run_id so all results from a single invocation can be correlated.
|
||||
func (r Report) LogEvent(logger *zerolog.Logger) {
|
||||
runID := r.RunID.String()
|
||||
for _, res := range r.Results {
|
||||
logger.Info().
|
||||
Str(logFieldRunID, runID).
|
||||
Str(logFieldComponent, res.Component).
|
||||
Str(logFieldTarget, res.Target).
|
||||
Str(logFieldStatus, res.ProbeStatus.logString()).
|
||||
Str(logFieldDetails, res.Details).
|
||||
Msg(logMsgPrecheck)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
package prechecks
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
)
|
||||
|
||||
// fixedRunID is used in all fixtures so golden strings are deterministic.
|
||||
var fixedRunID = uuid.MustParse("00000000-0000-0000-0000-000000000001")
|
||||
|
||||
// Fixtures
|
||||
|
||||
// allPassReport is the all-checks-pass scenario: QUIC is the suggested protocol.
|
||||
func allPassReport() Report {
|
||||
return Report{
|
||||
RunID: fixedRunID,
|
||||
SuggestedProtocol: new(connection.QUIC),
|
||||
Results: []CheckResult{
|
||||
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: "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"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// quicBlockedReport is the degraded scenario: QUIC is blocked, HTTP/2 is the fallback.
|
||||
// Only one transport failed so this is a warning, not a hard fail.
|
||||
func quicBlockedReport() Report {
|
||||
return Report{
|
||||
RunID: fixedRunID,
|
||||
SuggestedProtocol: new(connection.HTTP2),
|
||||
Results: []CheckResult{
|
||||
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: "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: Fail,
|
||||
Details: "Handshake failed",
|
||||
Action: "Allow outbound QUIC on port 7844. cloudflared will use http2 in the meantime.",
|
||||
},
|
||||
{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"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// apiFailReport is the degraded scenario: all connectivity passes but the Cloudflare
|
||||
// API is unreachable. The tunnel can still run; only automatic updates are unavailable.
|
||||
func apiFailReport() Report {
|
||||
return Report{
|
||||
RunID: fixedRunID,
|
||||
SuggestedProtocol: new(connection.QUIC),
|
||||
Results: []CheckResult{
|
||||
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: "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: Fail,
|
||||
Details: "Connection refused",
|
||||
Action: "cloudflared will still run, but automatic software updates are unavailable. Ensure port 443 TCP to api.cloudflare.com is open if you want auto-updates.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// bothTransportsBlockedReport is the hard-fail scenario: both QUIC and HTTP/2 are blocked.
|
||||
func bothTransportsBlockedReport() Report {
|
||||
return Report{
|
||||
RunID: fixedRunID,
|
||||
SuggestedProtocol: nil,
|
||||
Results: []CheckResult{
|
||||
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: "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: Fail,
|
||||
Details: "Handshake failed",
|
||||
Action: "Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.",
|
||||
},
|
||||
{
|
||||
Type: ProbeTypeHTTP2,
|
||||
Component: "TCP Connectivity",
|
||||
Target: "Port 7844 (HTTP/2)",
|
||||
ProbeStatus: Fail,
|
||||
Details: "Blocked or unreachable",
|
||||
},
|
||||
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: "Reachable"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// dnsFailReport is the hard-fail scenario: DNS is unresolvable, transports are skipped.
|
||||
func dnsFailReport() Report {
|
||||
return Report{
|
||||
RunID: fixedRunID,
|
||||
SuggestedProtocol: nil,
|
||||
Results: []CheckResult{
|
||||
{
|
||||
Type: ProbeTypeDNS,
|
||||
Component: "DNS Resolution",
|
||||
Target: "region1.v2.argotunnel.com",
|
||||
ProbeStatus: Fail,
|
||||
Details: "No addresses returned",
|
||||
Action: "Ensure your DNS resolver can resolve 'region1.v2.argotunnel.com'. Run: dig A region1.v2.argotunnel.com @1.1.1.1. If that fails, contact your network administrator.",
|
||||
},
|
||||
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Fail, Details: "No addresses returned"},
|
||||
{Type: ProbeTypeQUIC, Component: "UDP Connectivity", Target: "Port 7844 (QUIC)", ProbeStatus: Skip, Details: "DNS prerequisite failed"},
|
||||
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Skip, Details: "DNS prerequisite failed"},
|
||||
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Fail, Details: "Connection refused"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// String() / table renderer tests
|
||||
|
||||
func TestString_AllPass(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := "" +
|
||||
"--- 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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
assert.Equal(t, want, dnsFailReport().String())
|
||||
}
|
||||
|
||||
func TestString_EmptyResults(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := Report{RunID: fixedRunID, SuggestedProtocol: new(connection.QUIC)}
|
||||
out := r.String()
|
||||
// Must not panic and must still emit a valid skeleton.
|
||||
assert.Contains(t, out, "CONNECTIVITY PRE-CHECKS")
|
||||
assert.Contains(t, out, "SUMMARY:")
|
||||
assert.Contains(t, out, separator())
|
||||
}
|
||||
|
||||
// LogEvent() / structured log renderer tests
|
||||
|
||||
// logEntry is a helper struct to unmarshal a single JSON log line emitted by LogEvent.
|
||||
type logEntry struct {
|
||||
Level string `json:"level"`
|
||||
RunID string `json:"run_id"`
|
||||
Component string `json:"component"`
|
||||
Target string `json:"target"`
|
||||
Status string `json:"status"`
|
||||
Details string `json:"details"`
|
||||
Message string `json:"message"`
|
||||
HardFail *bool `json:"hard_fail"`
|
||||
SuggestedProtocol *string `json:"suggested_protocol"`
|
||||
}
|
||||
|
||||
// captureLogLines runs LogEvent against a buffer-backed zerolog logger and
|
||||
// returns the parsed JSON entries, one per emitted line.
|
||||
func captureLogLines(t *testing.T, r Report) []logEntry {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
r.LogEvent(&logger)
|
||||
|
||||
var entries []logEntry
|
||||
for _, line := range strings.Split(strings.TrimRight(buf.String(), "\n"), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
var e logEntry
|
||||
require.NoError(t, json.Unmarshal([]byte(line), &e), "failed to parse log line: %s", line)
|
||||
entries = append(entries, e)
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func TestLogEvent_AllPass(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := allPassReport()
|
||||
entries := captureLogLines(t, r)
|
||||
|
||||
// One line per result plus one summary line.
|
||||
require.Len(t, entries, len(r.Results)+1)
|
||||
|
||||
// Each result line carries the right fields.
|
||||
expected := []struct {
|
||||
component string
|
||||
target string
|
||||
status string
|
||||
details string
|
||||
}{
|
||||
{"DNS Resolution", "region1.v2.argotunnel.com", "pass", "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"},
|
||||
}
|
||||
for i, exp := range expected {
|
||||
e := entries[i]
|
||||
assert.Equal(t, "info", e.Level, "entry %d: level", i)
|
||||
assert.Equal(t, fixedRunID.String(), e.RunID, "entry %d: run_id", i)
|
||||
assert.Equal(t, exp.component, e.Component, "entry %d: component", i)
|
||||
assert.Equal(t, exp.target, e.Target, "entry %d: target", i)
|
||||
assert.Equal(t, exp.status, e.Status, "entry %d: status", i)
|
||||
assert.Equal(t, exp.details, e.Details, "entry %d: details", i)
|
||||
assert.Equal(t, logMsgPrecheck, e.Message, "entry %d: message", i)
|
||||
}
|
||||
|
||||
// Summary line.
|
||||
summary := entries[len(entries)-1]
|
||||
assert.Equal(t, logMsgPrecheckComplete, summary.Message)
|
||||
assert.Equal(t, fixedRunID.String(), summary.RunID)
|
||||
require.NotNil(t, summary.HardFail)
|
||||
assert.False(t, *summary.HardFail)
|
||||
require.NotNil(t, summary.SuggestedProtocol)
|
||||
assert.Equal(t, "quic", *summary.SuggestedProtocol)
|
||||
}
|
||||
|
||||
func TestLogEvent_QuicBlocked(t *testing.T) {
|
||||
t.Parallel()
|
||||
entries := captureLogLines(t, quicBlockedReport())
|
||||
|
||||
// QUIC row (index 2) must carry status=fail and the right details.
|
||||
quic := entries[2]
|
||||
assert.Equal(t, "fail", quic.Status)
|
||||
assert.Equal(t, "UDP Connectivity", quic.Component)
|
||||
assert.Equal(t, "Port 7844 (QUIC)", quic.Target)
|
||||
assert.Equal(t, "Handshake failed", quic.Details)
|
||||
assert.Equal(t, fixedRunID.String(), quic.RunID)
|
||||
|
||||
// Summary: not a hard fail (HTTP/2 still works), protocol falls back to http2.
|
||||
summary := entries[len(entries)-1]
|
||||
require.NotNil(t, summary.HardFail)
|
||||
assert.False(t, *summary.HardFail)
|
||||
require.NotNil(t, summary.SuggestedProtocol)
|
||||
assert.Equal(t, "http2", *summary.SuggestedProtocol)
|
||||
assert.Equal(t, fixedRunID.String(), summary.RunID)
|
||||
}
|
||||
|
||||
func TestLogEvent_APIFail(t *testing.T) {
|
||||
t.Parallel()
|
||||
entries := captureLogLines(t, apiFailReport())
|
||||
|
||||
// API row (index 4) carries status=fail and the expected details.
|
||||
api := entries[4]
|
||||
assert.Equal(t, "fail", api.Status)
|
||||
assert.Equal(t, "Cloudflare API", api.Component)
|
||||
assert.Equal(t, "api.cloudflare.com:443", api.Target)
|
||||
assert.Equal(t, "Connection refused", api.Details)
|
||||
assert.Equal(t, fixedRunID.String(), api.RunID)
|
||||
|
||||
// All transport rows pass.
|
||||
assert.Equal(t, "pass", entries[2].Status)
|
||||
assert.Equal(t, "pass", entries[3].Status)
|
||||
|
||||
// Summary: not a hard fail, QUIC is still the suggested protocol.
|
||||
summary := entries[len(entries)-1]
|
||||
require.NotNil(t, summary.HardFail)
|
||||
assert.False(t, *summary.HardFail)
|
||||
require.NotNil(t, summary.SuggestedProtocol)
|
||||
assert.Equal(t, "quic", *summary.SuggestedProtocol)
|
||||
}
|
||||
|
||||
func TestLogEvent_BothTransportsBlocked(t *testing.T) {
|
||||
t.Parallel()
|
||||
entries := captureLogLines(t, bothTransportsBlockedReport())
|
||||
|
||||
// Both transport rows carry status=fail.
|
||||
assert.Equal(t, "fail", entries[2].Status)
|
||||
assert.Equal(t, "Handshake failed", entries[2].Details)
|
||||
assert.Equal(t, "fail", entries[3].Status)
|
||||
assert.Equal(t, "Blocked or unreachable", entries[3].Details)
|
||||
|
||||
summary := entries[len(entries)-1]
|
||||
require.NotNil(t, summary.HardFail)
|
||||
assert.True(t, *summary.HardFail)
|
||||
assert.Nil(t, summary.SuggestedProtocol)
|
||||
}
|
||||
|
||||
func TestLogEvent_DNSFail(t *testing.T) {
|
||||
t.Parallel()
|
||||
entries := captureLogLines(t, dnsFailReport())
|
||||
|
||||
// Both DNS rows carry status=fail and the expected details.
|
||||
assert.Equal(t, "fail", entries[0].Status)
|
||||
assert.Equal(t, "No addresses returned", entries[0].Details)
|
||||
assert.Equal(t, "fail", entries[1].Status)
|
||||
assert.Equal(t, "No addresses returned", entries[1].Details)
|
||||
|
||||
// Transport rows are skipped.
|
||||
assert.Equal(t, "skip", entries[2].Status)
|
||||
assert.Equal(t, "DNS prerequisite failed", entries[2].Details)
|
||||
assert.Equal(t, "skip", entries[3].Status)
|
||||
assert.Equal(t, "DNS prerequisite failed", entries[3].Details)
|
||||
|
||||
summary := entries[len(entries)-1]
|
||||
require.NotNil(t, summary.HardFail)
|
||||
assert.True(t, *summary.HardFail)
|
||||
assert.Nil(t, summary.SuggestedProtocol)
|
||||
}
|
||||
|
||||
func TestLogEvent_EmptyReport(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := Report{RunID: fixedRunID, SuggestedProtocol: new(connection.HTTP2)}
|
||||
entries := captureLogLines(t, r)
|
||||
|
||||
// No result lines, only the summary.
|
||||
require.Len(t, entries, 1)
|
||||
assert.Equal(t, logMsgPrecheckComplete, entries[0].Message)
|
||||
assert.Equal(t, fixedRunID.String(), entries[0].RunID)
|
||||
require.NotNil(t, entries[0].HardFail)
|
||||
assert.False(t, *entries[0].HardFail)
|
||||
require.NotNil(t, entries[0].SuggestedProtocol)
|
||||
assert.Equal(t, "http2", *entries[0].SuggestedProtocol)
|
||||
}
|
||||
|
||||
// hasHardFail / hasWarn helper tests
|
||||
|
||||
func TestHasHardFail(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.False(t, allPassReport().hasHardFail())
|
||||
assert.False(t, quicBlockedReport().hasHardFail())
|
||||
assert.False(t, apiFailReport().hasHardFail())
|
||||
assert.True(t, bothTransportsBlockedReport().hasHardFail())
|
||||
assert.True(t, dnsFailReport().hasHardFail())
|
||||
}
|
||||
|
||||
func TestHasWarn(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.False(t, allPassReport().hasWarn())
|
||||
assert.True(t, quicBlockedReport().hasWarn())
|
||||
assert.True(t, apiFailReport().hasWarn())
|
||||
assert.False(t, bothTransportsBlockedReport().hasWarn())
|
||||
assert.False(t, dnsFailReport().hasWarn())
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package prechecks
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
)
|
||||
|
||||
// Status represents the outcome of a single connectivity pre-check.
|
||||
type Status int
|
||||
|
||||
const (
|
||||
// Pass indicates the check completed successfully.
|
||||
Pass Status = iota
|
||||
// Fail indicates the check did not succeed. Whether this is a hard failure
|
||||
// or a degraded-but-functional state depends on which probe(s) failed — see
|
||||
// Report.hasHardFail and Report.hasWarn.
|
||||
Fail
|
||||
// Skip indicates the check was not executed because a prerequisite
|
||||
// check (typically DNS) failed first.
|
||||
Skip
|
||||
)
|
||||
|
||||
// String returns the canonical display name for a Status value.
|
||||
func (s Status) String() string {
|
||||
switch s {
|
||||
case Pass:
|
||||
return "PASS"
|
||||
case Fail:
|
||||
return "FAIL"
|
||||
case Skip:
|
||||
return "SKIP"
|
||||
default:
|
||||
return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
|
||||
// ProbeType identifies which connectivity probe produced a CheckResult.
|
||||
// It is used by hasHardFail and hasWarn to evaluate severity without
|
||||
// matching against human-readable strings.
|
||||
type ProbeType int
|
||||
|
||||
const (
|
||||
ProbeTypeDNS ProbeType = iota // DNS resolution
|
||||
ProbeTypeQUIC // UDP/QUIC transport
|
||||
ProbeTypeHTTP2 // TCP/HTTP2 transport
|
||||
ProbeTypeManagementAPI // Cloudflare management API
|
||||
)
|
||||
|
||||
// CheckResult holds the outcome of one individual connectivity probe.
|
||||
type CheckResult struct {
|
||||
// Type identifies which probe produced this result. Used for severity
|
||||
// classification in hasHardFail and hasWarn.
|
||||
Type ProbeType
|
||||
|
||||
// Component is the human-readable probe category shown in the table header
|
||||
// column
|
||||
Component string
|
||||
|
||||
// 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
|
||||
Details string
|
||||
|
||||
// Action is non-empty when ProbeStatus is Fail and contains a human-readable
|
||||
// remediation instruction
|
||||
Action string
|
||||
}
|
||||
|
||||
// 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.
|
||||
type Report struct {
|
||||
// RunID is a unique identifier for this pre-check run. It is included in
|
||||
// every structured log line so that all results from a single invocation
|
||||
// can be correlated across log aggregation systems.
|
||||
RunID uuid.UUID
|
||||
|
||||
// Results contains one entry per executed probe, in the order they were
|
||||
// collected.
|
||||
Results []CheckResult
|
||||
|
||||
// SuggestedProtocol is the connection protocol the pre-checks recommend
|
||||
// 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().
|
||||
type Config struct {
|
||||
// Region is the optional cloudflared --region flag value. When non-empty
|
||||
// the pre-check probes the regional edge hostnames instead of the global ones.
|
||||
Region string
|
||||
|
||||
// Timeout is the maximum wall-clock duration allowed for the entire
|
||||
// pre-check suite to complete.
|
||||
Timeout time.Duration
|
||||
|
||||
// IPVersion controls which address families are probed for transport
|
||||
// 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
|
||||
}
|
||||
+148
-5
@@ -186,27 +186,170 @@ sudo dnf install cloudflared
|
||||
</pre>
|
||||
|
||||
|
||||
<h2><a name="gokeyless-packages"></a>Gokeyless</a></h2>
|
||||
<h3><a name="go-keyless-debian">Debian</a></h3>
|
||||
<h2><a name="gokeyless-packages">Gokeyless</a></h2>
|
||||
|
||||
<ul>
|
||||
<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>
|
||||
<p style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 10px; margin: 10px 0;">
|
||||
<strong>We have rolled our public key for package signing.</strong> If you are using RPM-based distributions (RHEL,
|
||||
CentOS, Amazon Linux, etc.) or Debian Trixie and have the old key installed, RPM/Deb packages will no longer work with the old key.
|
||||
Please update your repository configuration using the instructions below to ensure you can continue receiving
|
||||
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-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 buster 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="go-keyless-centos">Centos 8</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
|
||||
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 bookworm 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-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 bullseye 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-noble">Ubuntu 24.04 (Noble Numbat)</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 noble 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
|
||||
|
||||
# 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/rpm/gokeyless.repo | sudo tee /etc/yum.repos.d/gokeyless.repo
|
||||
|
||||
#update repo
|
||||
sudo yum update
|
||||
|
||||
# install gokeyless
|
||||
sudo yum install gokeyless
|
||||
</pre>
|
||||
|
||||
<h3><a name="gokeyless-amazon-linux-2">Amazon Linux 2</a></h3>
|
||||
<pre>
|
||||
# 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
|
||||
|
||||
# install gokeyless
|
||||
sudo yum install gokeyless
|
||||
</pre>
|
||||
|
||||
<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-rhel-8">RHEL 8 / CentOS 8</a></h3>
|
||||
<pre>
|
||||
# This requires dnf config-manager
|
||||
# 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)
|
||||
}
|
||||
}
|
||||
+35
-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,12 @@ type TunnelConfig struct {
|
||||
|
||||
NeedPQ bool
|
||||
|
||||
// NoPrechecks disables connectivity pre-checks at startup.
|
||||
NoPrechecks bool
|
||||
|
||||
// Prechecks enables connectivity pre-checks at startup.
|
||||
Prechecks bool
|
||||
|
||||
NamedTunnel *connection.TunnelProperties
|
||||
ProtocolSelector connection.ProtocolSelector
|
||||
EdgeTLSConfigs map[connection.Protocol]*tls.Config
|
||||
@@ -83,6 +91,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 +138,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 +478,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 +530,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 +570,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 +602,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:
|
||||
}
|
||||
}
|
||||
+166
-93
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package tunneldns
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Upstream is a simplified interface for proxy destination
|
||||
type Upstream interface {
|
||||
Exchange(ctx context.Context, query *dns.Msg) (*dns.Msg, error)
|
||||
}
|
||||
|
||||
// ProxyPlugin is a simplified DNS proxy using a generic upstream interface
|
||||
type ProxyPlugin struct {
|
||||
Upstreams []Upstream
|
||||
Next plugin.Handler
|
||||
}
|
||||
|
||||
// ServeDNS implements interface for CoreDNS plugin
|
||||
func (p ProxyPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
var reply *dns.Msg
|
||||
var backendErr error
|
||||
|
||||
for _, upstream := range p.Upstreams {
|
||||
reply, backendErr = upstream.Exchange(ctx, r)
|
||||
if backendErr == nil {
|
||||
w.WriteMsg(reply)
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
return dns.RcodeServerFailure, errors.Wrap(backendErr, "failed to contact any of the upstreams")
|
||||
}
|
||||
|
||||
// Name implements interface for CoreDNS plugin
|
||||
func (p ProxyPlugin) Name() string { return "proxy" }
|
||||
@@ -1,145 +0,0 @@
|
||||
package tunneldns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// UpstreamHTTPS is the upstream implementation for DNS over HTTPS service
|
||||
type UpstreamHTTPS struct {
|
||||
client *http.Client
|
||||
endpoint *url.URL
|
||||
bootstraps []string
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// NewUpstreamHTTPS creates a new DNS over HTTPS upstream from endpoint
|
||||
func NewUpstreamHTTPS(endpoint string, bootstraps []string, maxConnections int, log *zerolog.Logger) (Upstream, error) {
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &UpstreamHTTPS{client: configureClient(u.Hostname(), maxConnections), endpoint: u, bootstraps: bootstraps, log: log}, nil
|
||||
}
|
||||
|
||||
// Exchange provides an implementation for the Upstream interface
|
||||
func (u *UpstreamHTTPS) Exchange(ctx context.Context, query *dns.Msg) (*dns.Msg, error) {
|
||||
queryBuf, err := query.Pack()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to pack DNS query")
|
||||
}
|
||||
|
||||
if len(query.Question) > 0 && query.Question[0].Name == fmt.Sprintf("%s.", u.endpoint.Hostname()) {
|
||||
for _, bootstrap := range u.bootstraps {
|
||||
endpoint, client, err := configureBootstrap(bootstrap)
|
||||
if err != nil {
|
||||
u.log.Err(err).Msgf("failed to configure bootstrap upstream %s", bootstrap)
|
||||
continue
|
||||
}
|
||||
msg, err := exchange(queryBuf, query.Id, endpoint, client, u.log)
|
||||
if err != nil {
|
||||
u.log.Err(err).Msgf("failed to connect to a bootstrap upstream %s", bootstrap)
|
||||
continue
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to reach any bootstrap upstream: %v", u.bootstraps)
|
||||
}
|
||||
|
||||
return exchange(queryBuf, query.Id, u.endpoint, u.client, u.log)
|
||||
}
|
||||
|
||||
func exchange(msg []byte, queryID uint16, endpoint *url.URL, client *http.Client, log *zerolog.Logger) (*dns.Msg, error) {
|
||||
// No content negotiation for now, use DNS wire format
|
||||
buf, backendErr := exchangeWireformat(msg, endpoint, client)
|
||||
if backendErr == nil {
|
||||
response := &dns.Msg{}
|
||||
if err := response.Unpack(buf); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unpack DNS response from body")
|
||||
}
|
||||
|
||||
response.Id = queryID
|
||||
return response, nil
|
||||
}
|
||||
|
||||
log.Err(backendErr).Msgf("failed to connect to an HTTPS backend %q", endpoint)
|
||||
return nil, backendErr
|
||||
}
|
||||
|
||||
// Perform message exchange with the default UDP wireformat defined in current draft
|
||||
// https://datatracker.ietf.org/doc/draft-ietf-doh-dns-over-https
|
||||
func exchangeWireformat(msg []byte, endpoint *url.URL, client *http.Client) ([]byte, error) {
|
||||
req, err := http.NewRequest("POST", endpoint.String(), bytes.NewBuffer(msg))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create an HTTPS request")
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/dns-message")
|
||||
req.Host = endpoint.Host
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to perform an HTTPS request")
|
||||
}
|
||||
|
||||
// Check response status code
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("returned status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Read wireformat response from the body
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read the response body")
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func configureBootstrap(bootstrap string) (*url.URL, *http.Client, error) {
|
||||
b, err := url.Parse(bootstrap)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if ip := net.ParseIP(b.Hostname()); ip == nil {
|
||||
return nil, nil, fmt.Errorf("bootstrap address of %s must be an IP address", b.Hostname())
|
||||
}
|
||||
|
||||
return b, configureClient(b.Hostname(), MaxUpstreamConnsDefault), nil
|
||||
}
|
||||
|
||||
// configureClient will configure a HTTPS client for upstream DoH requests
|
||||
func configureClient(hostname string, maxUpstreamConnections int) *http.Client {
|
||||
// Update TLS and HTTP client configuration
|
||||
tlsConfig := &tls.Config{ServerName: hostname}
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
DisableCompression: true,
|
||||
MaxIdleConns: 1,
|
||||
MaxConnsPerHost: maxUpstreamConnections,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
_ = http2.ConfigureTransport(transport)
|
||||
|
||||
return &http.Client{
|
||||
Timeout: defaultTimeout,
|
||||
Transport: transport,
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package tunneldns
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/metrics"
|
||||
"github.com/coredns/coredns/plugin/metrics/vars"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
"github.com/coredns/coredns/plugin/pkg/rcode"
|
||||
"github.com/coredns/coredns/request"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const (
|
||||
pluginName = "cloudflared"
|
||||
)
|
||||
|
||||
// MetricsPlugin is an adapter for CoreDNS and built-in metrics
|
||||
type MetricsPlugin struct {
|
||||
Next plugin.Handler
|
||||
}
|
||||
|
||||
// NewMetricsPlugin creates a plugin with configured metrics
|
||||
func NewMetricsPlugin(next plugin.Handler) *MetricsPlugin {
|
||||
return &MetricsPlugin{Next: next}
|
||||
}
|
||||
|
||||
// ServeDNS implements the CoreDNS plugin interface
|
||||
func (p MetricsPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
state := request.Request{W: w, Req: r}
|
||||
|
||||
rw := dnstest.NewRecorder(w)
|
||||
status, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, rw, r)
|
||||
|
||||
// Update built-in metrics
|
||||
server := metrics.WithServer(ctx)
|
||||
vars.Report(server, state, ".", "", rcode.ToString(rw.Rcode), pluginName, rw.Len, rw.Start)
|
||||
|
||||
return status, err
|
||||
}
|
||||
|
||||
// Name implements the CoreDNS plugin interface
|
||||
func (p MetricsPlugin) Name() string { return "metrics" }
|
||||
@@ -1,109 +0,0 @@
|
||||
package tunneldns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/cache"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const (
|
||||
LogFieldAddress = "address"
|
||||
LogFieldURL = "url"
|
||||
MaxUpstreamConnsDefault = 5
|
||||
)
|
||||
|
||||
// Listener is an adapter between CoreDNS server and Warp runnable
|
||||
type Listener struct {
|
||||
server *dnsserver.Server
|
||||
wg sync.WaitGroup
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// Create a CoreDNS server plugin from configuration
|
||||
func createConfig(address string, port uint16, p plugin.Handler) *dnsserver.Config {
|
||||
c := &dnsserver.Config{
|
||||
Zone: ".",
|
||||
Transport: "dns",
|
||||
ListenHosts: []string{address},
|
||||
Port: strconv.FormatUint(uint64(port), 10),
|
||||
}
|
||||
|
||||
c.AddPlugin(func(next plugin.Handler) plugin.Handler { return p })
|
||||
return c
|
||||
}
|
||||
|
||||
// Start blocks for serving requests
|
||||
func (l *Listener) Start(readySignal chan struct{}) error {
|
||||
defer close(readySignal)
|
||||
l.log.Info().Str(LogFieldAddress, l.server.Address()).Msg("Starting DNS over HTTPS proxy server")
|
||||
|
||||
// Start UDP listener
|
||||
if udp, err := l.server.ListenPacket(); err == nil {
|
||||
l.wg.Add(1)
|
||||
go func() {
|
||||
_ = l.server.ServePacket(udp)
|
||||
l.wg.Done()
|
||||
}()
|
||||
} else {
|
||||
return errors.Wrap(err, "failed to create a UDP listener")
|
||||
}
|
||||
|
||||
// Start TCP listener
|
||||
tcp, err := l.server.Listen()
|
||||
if err == nil {
|
||||
l.wg.Add(1)
|
||||
go func() {
|
||||
_ = l.server.Serve(tcp)
|
||||
l.wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
return errors.Wrap(err, "failed to create a TCP listener")
|
||||
}
|
||||
|
||||
// Stop signals server shutdown and blocks until completed
|
||||
func (l *Listener) Stop() error {
|
||||
if err := l.server.Stop(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateListener configures the server and bound sockets
|
||||
func CreateListener(address string, port uint16, upstreams []string, bootstraps []string, maxUpstreamConnections int, log *zerolog.Logger) (*Listener, error) {
|
||||
// Build the list of upstreams
|
||||
upstreamList := make([]Upstream, 0)
|
||||
for _, url := range upstreams {
|
||||
log.Info().Str(LogFieldURL, url).Msg("Adding DNS upstream")
|
||||
upstream, err := NewUpstreamHTTPS(url, bootstraps, maxUpstreamConnections, log)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create HTTPS upstream")
|
||||
}
|
||||
upstreamList = append(upstreamList, upstream)
|
||||
}
|
||||
|
||||
// Create a local cache with HTTPS proxy plugin
|
||||
chain := cache.New()
|
||||
chain.Next = ProxyPlugin{
|
||||
Upstreams: upstreamList,
|
||||
}
|
||||
|
||||
// Format an endpoint
|
||||
endpoint := "dns://" + net.JoinHostPort(address, strconv.FormatUint(uint64(port), 10))
|
||||
|
||||
// Create the actual middleware server
|
||||
server, err := dnsserver.NewServer(endpoint, []*dnsserver.Config{createConfig(address, port, NewMetricsPlugin(chain))})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Listener{server: server, log: log}, nil
|
||||
}
|
||||
-236
@@ -1,236 +0,0 @@
|
||||
// Package cidr is a collection of assorted utilities for computing
|
||||
// network and host addresses within network ranges.
|
||||
//
|
||||
// It expects a CIDR-type address structure where addresses are divided into
|
||||
// some number of prefix bits representing the network and then the remaining
|
||||
// suffix bits represent the host.
|
||||
//
|
||||
// For example, it can help to calculate addresses for sub-networks of a
|
||||
// parent network, or to calculate host addresses within a particular prefix.
|
||||
//
|
||||
// At present this package is prioritizing simplicity of implementation and
|
||||
// de-prioritizing speed and memory usage. Thus caution is advised before
|
||||
// using this package in performance-critical applications or hot codepaths.
|
||||
// Patches to improve the speed and memory usage may be accepted as long as
|
||||
// they do not result in a significant increase in code complexity.
|
||||
package cidr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
)
|
||||
|
||||
// Subnet takes a parent CIDR range and creates a subnet within it
|
||||
// with the given number of additional prefix bits and the given
|
||||
// network number.
|
||||
//
|
||||
// For example, 10.3.0.0/16, extended by 8 bits, with a network number
|
||||
// of 5, becomes 10.3.5.0/24 .
|
||||
func Subnet(base *net.IPNet, newBits int, num int) (*net.IPNet, error) {
|
||||
return SubnetBig(base, newBits, big.NewInt(int64(num)))
|
||||
}
|
||||
|
||||
// SubnetBig takes a parent CIDR range and creates a subnet within it with the
|
||||
// given number of additional prefix bits and the given network number. It
|
||||
// differs from Subnet in that it takes a *big.Int for the num, instead of an int.
|
||||
//
|
||||
// For example, 10.3.0.0/16, extended by 8 bits, with a network number of 5,
|
||||
// becomes 10.3.5.0/24 .
|
||||
func SubnetBig(base *net.IPNet, newBits int, num *big.Int) (*net.IPNet, error) {
|
||||
ip := base.IP
|
||||
mask := base.Mask
|
||||
|
||||
parentLen, addrLen := mask.Size()
|
||||
newPrefixLen := parentLen + newBits
|
||||
|
||||
if newPrefixLen > addrLen {
|
||||
return nil, fmt.Errorf("insufficient address space to extend prefix of %d by %d", parentLen, newBits)
|
||||
}
|
||||
|
||||
maxNetNum := uint64(1<<uint64(newBits)) - 1
|
||||
if num.Uint64() > maxNetNum {
|
||||
return nil, fmt.Errorf("prefix extension of %d does not accommodate a subnet numbered %d", newBits, num)
|
||||
}
|
||||
|
||||
return &net.IPNet{
|
||||
IP: insertNumIntoIP(ip, num, newPrefixLen),
|
||||
Mask: net.CIDRMask(newPrefixLen, addrLen),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Host takes a parent CIDR range and turns it into a host IP address with the
|
||||
// given host number.
|
||||
//
|
||||
// For example, 10.3.0.0/16 with a host number of 2 gives 10.3.0.2.
|
||||
func Host(base *net.IPNet, num int) (net.IP, error) {
|
||||
return HostBig(base, big.NewInt(int64(num)))
|
||||
}
|
||||
|
||||
// HostBig takes a parent CIDR range and turns it into a host IP address with
|
||||
// the given host number. It differs from Host in that it takes a *big.Int for
|
||||
// the num, instead of an int.
|
||||
//
|
||||
// For example, 10.3.0.0/16 with a host number of 2 gives 10.3.0.2.
|
||||
func HostBig(base *net.IPNet, num *big.Int) (net.IP, error) {
|
||||
ip := base.IP
|
||||
mask := base.Mask
|
||||
|
||||
parentLen, addrLen := mask.Size()
|
||||
hostLen := addrLen - parentLen
|
||||
|
||||
maxHostNum := big.NewInt(int64(1))
|
||||
maxHostNum.Lsh(maxHostNum, uint(hostLen))
|
||||
maxHostNum.Sub(maxHostNum, big.NewInt(1))
|
||||
|
||||
numUint64 := big.NewInt(int64(num.Uint64()))
|
||||
if num.Cmp(big.NewInt(0)) == -1 {
|
||||
numUint64.Neg(num)
|
||||
numUint64.Sub(numUint64, big.NewInt(int64(1)))
|
||||
num.Sub(maxHostNum, numUint64)
|
||||
}
|
||||
|
||||
if numUint64.Cmp(maxHostNum) == 1 {
|
||||
return nil, fmt.Errorf("prefix of %d does not accommodate a host numbered %d", parentLen, num)
|
||||
}
|
||||
var bitlength int
|
||||
if ip.To4() != nil {
|
||||
bitlength = 32
|
||||
} else {
|
||||
bitlength = 128
|
||||
}
|
||||
return insertNumIntoIP(ip, num, bitlength), nil
|
||||
}
|
||||
|
||||
// AddressRange returns the first and last addresses in the given CIDR range.
|
||||
func AddressRange(network *net.IPNet) (net.IP, net.IP) {
|
||||
// the first IP is easy
|
||||
firstIP := network.IP
|
||||
|
||||
// the last IP is the network address OR NOT the mask address
|
||||
prefixLen, bits := network.Mask.Size()
|
||||
if prefixLen == bits {
|
||||
// Easy!
|
||||
// But make sure that our two slices are distinct, since they
|
||||
// would be in all other cases.
|
||||
lastIP := make([]byte, len(firstIP))
|
||||
copy(lastIP, firstIP)
|
||||
return firstIP, lastIP
|
||||
}
|
||||
|
||||
firstIPInt, bits := ipToInt(firstIP)
|
||||
hostLen := uint(bits) - uint(prefixLen)
|
||||
lastIPInt := big.NewInt(1)
|
||||
lastIPInt.Lsh(lastIPInt, hostLen)
|
||||
lastIPInt.Sub(lastIPInt, big.NewInt(1))
|
||||
lastIPInt.Or(lastIPInt, firstIPInt)
|
||||
|
||||
return firstIP, intToIP(lastIPInt, bits)
|
||||
}
|
||||
|
||||
// AddressCount returns the number of distinct host addresses within the given
|
||||
// CIDR range.
|
||||
//
|
||||
// Since the result is a uint64, this function returns meaningful information
|
||||
// only for IPv4 ranges and IPv6 ranges with a prefix size of at least 65.
|
||||
func AddressCount(network *net.IPNet) uint64 {
|
||||
prefixLen, bits := network.Mask.Size()
|
||||
return 1 << (uint64(bits) - uint64(prefixLen))
|
||||
}
|
||||
|
||||
//VerifyNoOverlap takes a list subnets and supernet (CIDRBlock) and verifies
|
||||
//none of the subnets overlap and all subnets are in the supernet
|
||||
//it returns an error if any of those conditions are not satisfied
|
||||
func VerifyNoOverlap(subnets []*net.IPNet, CIDRBlock *net.IPNet) error {
|
||||
firstLastIP := make([][]net.IP, len(subnets))
|
||||
for i, s := range subnets {
|
||||
first, last := AddressRange(s)
|
||||
firstLastIP[i] = []net.IP{first, last}
|
||||
}
|
||||
for i, s := range subnets {
|
||||
if !CIDRBlock.Contains(firstLastIP[i][0]) || !CIDRBlock.Contains(firstLastIP[i][1]) {
|
||||
return fmt.Errorf("%s does not fully contain %s", CIDRBlock.String(), s.String())
|
||||
}
|
||||
for j := 0; j < len(subnets); j++ {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
|
||||
first := firstLastIP[j][0]
|
||||
last := firstLastIP[j][1]
|
||||
if s.Contains(first) || s.Contains(last) {
|
||||
return fmt.Errorf("%s overlaps with %s", subnets[j].String(), s.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PreviousSubnet returns the subnet of the desired mask in the IP space
|
||||
// just lower than the start of IPNet provided. If the IP space rolls over
|
||||
// then the second return value is true
|
||||
func PreviousSubnet(network *net.IPNet, prefixLen int) (*net.IPNet, bool) {
|
||||
startIP := checkIPv4(network.IP)
|
||||
previousIP := make(net.IP, len(startIP))
|
||||
copy(previousIP, startIP)
|
||||
cMask := net.CIDRMask(prefixLen, 8*len(previousIP))
|
||||
previousIP = Dec(previousIP)
|
||||
previous := &net.IPNet{IP: previousIP.Mask(cMask), Mask: cMask}
|
||||
if startIP.Equal(net.IPv4zero) || startIP.Equal(net.IPv6zero) {
|
||||
return previous, true
|
||||
}
|
||||
return previous, false
|
||||
}
|
||||
|
||||
// NextSubnet returns the next available subnet of the desired mask size
|
||||
// starting for the maximum IP of the offset subnet
|
||||
// If the IP exceeds the maxium IP then the second return value is true
|
||||
func NextSubnet(network *net.IPNet, prefixLen int) (*net.IPNet, bool) {
|
||||
_, currentLast := AddressRange(network)
|
||||
mask := net.CIDRMask(prefixLen, 8*len(currentLast))
|
||||
currentSubnet := &net.IPNet{IP: currentLast.Mask(mask), Mask: mask}
|
||||
_, last := AddressRange(currentSubnet)
|
||||
last = Inc(last)
|
||||
next := &net.IPNet{IP: last.Mask(mask), Mask: mask}
|
||||
if last.Equal(net.IPv4zero) || last.Equal(net.IPv6zero) {
|
||||
return next, true
|
||||
}
|
||||
return next, false
|
||||
}
|
||||
|
||||
//Inc increases the IP by one this returns a new []byte for the IP
|
||||
func Inc(IP net.IP) net.IP {
|
||||
IP = checkIPv4(IP)
|
||||
incIP := make([]byte, len(IP))
|
||||
copy(incIP, IP)
|
||||
for j := len(incIP) - 1; j >= 0; j-- {
|
||||
incIP[j]++
|
||||
if incIP[j] > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return incIP
|
||||
}
|
||||
|
||||
//Dec decreases the IP by one this returns a new []byte for the IP
|
||||
func Dec(IP net.IP) net.IP {
|
||||
IP = checkIPv4(IP)
|
||||
decIP := make([]byte, len(IP))
|
||||
copy(decIP, IP)
|
||||
decIP = checkIPv4(decIP)
|
||||
for j := len(decIP) - 1; j >= 0; j-- {
|
||||
decIP[j]--
|
||||
if decIP[j] < 255 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return decIP
|
||||
}
|
||||
|
||||
func checkIPv4(ip net.IP) net.IP {
|
||||
// Go for some reason allocs IPv6len for IPv4 so we have to correct it
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
return v4
|
||||
}
|
||||
return ip
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
package cidr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
)
|
||||
|
||||
func ipToInt(ip net.IP) (*big.Int, int) {
|
||||
val := &big.Int{}
|
||||
val.SetBytes([]byte(ip))
|
||||
if len(ip) == net.IPv4len {
|
||||
return val, 32
|
||||
} else if len(ip) == net.IPv6len {
|
||||
return val, 128
|
||||
} else {
|
||||
panic(fmt.Errorf("Unsupported address length %d", len(ip)))
|
||||
}
|
||||
}
|
||||
|
||||
func intToIP(ipInt *big.Int, bits int) net.IP {
|
||||
ipBytes := ipInt.Bytes()
|
||||
ret := make([]byte, bits/8)
|
||||
// Pack our IP bytes into the end of the return array,
|
||||
// since big.Int.Bytes() removes front zero padding.
|
||||
for i := 1; i <= len(ipBytes); i++ {
|
||||
ret[len(ret)-i] = ipBytes[len(ipBytes)-i]
|
||||
}
|
||||
return net.IP(ret)
|
||||
}
|
||||
|
||||
func insertNumIntoIP(ip net.IP, bigNum *big.Int, prefixLen int) net.IP {
|
||||
ipInt, totalBits := ipToInt(ip)
|
||||
bigNum.Lsh(bigNum, uint(totalBits-prefixLen))
|
||||
ipInt.Or(ipInt, bigNum)
|
||||
return intToIP(ipInt, totalBits)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user