mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-06-23 04:10:20 +00:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02eb75b56d | |||
| 81a53555aa | |||
| 2bcaf09734 | |||
| 3315fa6e0f | |||
| ad11e67340 | |||
| 3a60f8ac0f | |||
| 68620efbce | |||
| 4d95ab73f5 | |||
| 57f7d693bb | |||
| ccffef1179 | |||
| 52519f67e8 | |||
| 0e84636de9 | |||
| 4177dd6936 | |||
| f6f60e1059 | |||
| 4494eee13d | |||
| 905d983d14 | |||
| 168f09cb4c | |||
| 0c9014870a | |||
| 31de04f858 | |||
| fbfd76089f | |||
| 21ca2e225e | |||
| f674b82e2a | |||
| ae3799a098 | |||
| 4d8df2b2c0 | |||
| a67c583bf1 | |||
| 22a955f7bb | |||
| a453612e7c | |||
| e8f8b2afb7 | |||
| 7585e38948 | |||
| a9b6f703f0 | |||
| da81fb02ec | |||
| 23b15d0eb6 | |||
| 4a2cbd1870 | |||
| 9978cfd0d5 | |||
| a0401df621 | |||
| cf17ba93b2 | |||
| f827e6216b | |||
| df981b4d89 | |||
| ddd76fa05f | |||
| 9f084e6800 | |||
| df54d27710 | |||
| b0b898c235 | |||
| 5287a9e24b | |||
| e2a71cbecc | |||
| a0e55fc969 | |||
| 1e9deb1002 | |||
| d2a87e9b93 | |||
| c0bc3bdbf0 | |||
| 29b3a7aa7e | |||
| 372a4b7079 | |||
| 649705d291 | |||
| 839b874cad | |||
| 059f4d9898 | |||
| a0bcbf6a44 | |||
| 66587173e2 | |||
| 9388e7f48c | |||
| d6cb78aeb4 | |||
| d7c62aed71 | |||
| 2b95c61044 | |||
| efd0189121 | |||
| 9abcfece66 |
+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.4-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
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
include:
|
||||
- local: .ci/commons.gitlab-ci.yml
|
||||
|
||||
###########################################################################
|
||||
### Build and Push Internal Image (commit SHA on master, version on tag) ###
|
||||
###########################################################################
|
||||
- component: $CI_SERVER_FQDN/cloudflare/ci/docker-image/build-push-image@~latest
|
||||
inputs:
|
||||
stage: release-internal
|
||||
jobPrefix: internal-image
|
||||
runOnMR: false
|
||||
runOnBranches: '^master$'
|
||||
needs:
|
||||
- generate-internal-image-version
|
||||
commentImageRefs: false
|
||||
runner: vm-linux-x86-4cpu-8gb
|
||||
EXTRA_DIB_ARGS: "--manifest=.docker-images-internal"
|
||||
|
||||
###############################################################################
|
||||
### Generate Internal Image Version File ###
|
||||
### Uses `git describe`: version tag on tagged commits, SHA-based on master ###
|
||||
###############################################################################
|
||||
generate-internal-image-version:
|
||||
stage: release-internal
|
||||
image: $BUILD_IMAGE
|
||||
rules:
|
||||
- !reference [.default-rules, run-on-master]
|
||||
needs:
|
||||
- ci-image-get-image-ref
|
||||
script:
|
||||
- make generate-internal-image-version
|
||||
artifacts:
|
||||
paths:
|
||||
- versions-internal
|
||||
@@ -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: "3625-1801d52@sha256:9261597bc2d229c997522848260de758567643d58ae1097196ae368db89a1d0f"
|
||||
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
|
||||
|
||||
@@ -8,7 +8,7 @@ include:
|
||||
rules:
|
||||
- !reference [.default-rules, run-always]
|
||||
tags:
|
||||
- windows-x86
|
||||
- canary-windows-x86
|
||||
cache: {}
|
||||
|
||||
##########################################
|
||||
@@ -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/*
|
||||
@@ -56,7 +56,7 @@ windows-load-env-variables:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_dev/azure_vault/secret/key_vault_secret@kv
|
||||
file: false
|
||||
KEY_VAULT_CERTIFICATE:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_dev/azure_vault/certificate_v2/key_vault_certificate@kv
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_dev/azure_vault/certificate/key_vault_certificate@kv
|
||||
file: false
|
||||
artifacts:
|
||||
access: 'none'
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
images:
|
||||
- name: cloudflared-daemon
|
||||
dockerfile: Dockerfile.$ARCH
|
||||
context: .
|
||||
version_file: versions-internal
|
||||
architectures:
|
||||
- amd64
|
||||
- arm64
|
||||
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 "========================================"
|
||||
+20
-4
@@ -1,5 +1,7 @@
|
||||
variables:
|
||||
GO_VERSION: "go1.24.11"
|
||||
GO_VERSION: "1.26.4"
|
||||
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:
|
||||
#####################################################
|
||||
@@ -50,9 +63,12 @@ include:
|
||||
#####################################################
|
||||
- local: .ci/apt-internal.gitlab-ci.yml
|
||||
|
||||
#####################################################
|
||||
########## Release Internal Docker Image ############
|
||||
#####################################################
|
||||
- local: .ci/internal-image.gitlab-ci.yml
|
||||
|
||||
#####################################################
|
||||
############## 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.4 AS builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
TARGET_GOOS=${TARGET_GOOS} \
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
# use a builder image for building cloudflare
|
||||
FROM golang:1.24.11 AS builder
|
||||
FROM golang:1.26.4 AS builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
# the CONTAINER_BUILD envvar is used set github.com/cloudflare/cloudflared/metrics.Runtime=virtual
|
||||
@@ -15,7 +15,7 @@ COPY . .
|
||||
RUN GOOS=linux GOARCH=amd64 make cloudflared
|
||||
|
||||
# use a distroless base image with glibc
|
||||
FROM gcr.io/distroless/base-debian13:nonroot
|
||||
FROM gcr.io/distroless/base-debian13:nonroot-amd64@sha256:ced0a2b1936b14d5bddc2ee02a807b1586ca6576a967f5b043f4a3301c8a8f6b
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
|
||||
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
# use a builder image for building cloudflare
|
||||
FROM golang:1.24.11 AS builder
|
||||
FROM golang:1.26.4 AS builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
# the CONTAINER_BUILD envvar is used set github.com/cloudflare/cloudflared/metrics.Runtime=virtual
|
||||
@@ -15,7 +15,7 @@ COPY . .
|
||||
RUN GOOS=linux GOARCH=arm64 make cloudflared
|
||||
|
||||
# use a distroless base image with glibc
|
||||
FROM gcr.io/distroless/base-debian13:nonroot-arm64
|
||||
FROM gcr.io/distroless/base-debian13:nonroot-arm64@sha256:9c1ab6a3dbf9e22827b0be4a314d7cfbe008f922b7ca833ed0e5a63318c6169e
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
|
||||
|
||||
|
||||
@@ -159,6 +159,10 @@ container:
|
||||
generate-docker-version:
|
||||
echo latest $(VERSION) > versions
|
||||
|
||||
.PHONY: generate-internal-image-version
|
||||
generate-internal-image-version:
|
||||
echo $(VERSION) > versions-internal
|
||||
|
||||
|
||||
.PHONY: test
|
||||
test: vet
|
||||
@@ -289,3 +293,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,71 @@
|
||||
2026.6.1
|
||||
- 2026-06-18 TUN-10630: Fix precheck protocol override
|
||||
- 2026-06-18 Revert "TUN-10557: Bump quic-go v0.59.1"
|
||||
- 2026-06-16 chore: Fix warnings
|
||||
- 2026-06-15 TUN-10612: Add renovate to cloudflared to update distroless images explicitely
|
||||
- 2026-06-11 TUN-9251: Publish internal image
|
||||
- 2026-05-26 TUN-10557: Bump quic-go v0.59.1
|
||||
|
||||
2026.6.0
|
||||
- 2026-06-08 TUN-10558: Bump go to v1.24.4, x/crypto to v0.52.0 and google.golang.org/grpc to v1.81.1
|
||||
- 2026-06-01 TUN-10563: introduce QUICConnection interface
|
||||
|
||||
2026.5.2
|
||||
- 2026-05-26 TUN-10391: Avoid using fmt.Println
|
||||
|
||||
2026.5.1
|
||||
- 2026-05-22 fix: Bump go to 1.26.3 and go.opentelemetry.io/otel and go-jose/v4 to fix CVE's
|
||||
- 2026-05-22 TUN-10391: Avoid blocking cloudflared due to logging
|
||||
- 2026-05-22 TUN-10391: Add precheck integration tests
|
||||
- 2026-05-14 TUN-10511: Revise --edge support for pre-checks
|
||||
- 2026-05-13 fix: Update golang.org/x/net to v0.54.0
|
||||
- 2026-05-13 TUN-10525: Add prechecks kill switch
|
||||
|
||||
2026.5.0
|
||||
- 2026-05-08 Bump golang.org/x/net from v0.40.0 to v0.53.0
|
||||
- 2026-05-07 TUN-10507: Bump go and go-boring to 1.26.2
|
||||
- 2026-05-07 TUN-10511: Add Static DNS Resolvers
|
||||
- 2026-05-07 TUN-10390: Call prechecks
|
||||
- 2026-05-07 TUN-10513: Disable /debug/pprof/cmdline endpoint
|
||||
- 2026-05-06 TUN-10390: Fix missing TLS settings
|
||||
- 2026-05-05 chore: Fix warnings
|
||||
- 2026-05-04 TUN-10389: Implement main run method
|
||||
- 2026-04-30 TUN-10388: Adding probe check
|
||||
- 2026-04-30 TUN-10388 Implement dialers for connectivity checks
|
||||
- 2026-04-30 TUN-10389: Improve probe functions
|
||||
- 2026-04-29 SECENG-13496 update pkg docs for gokeyless to support multiple builds
|
||||
- 2026-04-29 chore: Add pre-push hooks
|
||||
- 2026-04-29 TUN-10388: Use pointer for suggested protocol
|
||||
- 2026-04-27 TUN-10387: Add no-prechecks flag
|
||||
- 2026-04-23 TUN-10386: Add Table Renderer
|
||||
- 2026-04-21 AUTH-4699, AUTH-8460, TUN-10179: Vendor gopsutil/v4 for cross-platform process identification
|
||||
- 2026-04-21 AUTH-4699, AUTH-8460, TUN-10179: Fix .lock file deletion race condition
|
||||
- 2026-04-20 TUN-10413: Centralize TLS curve configuration in crypto/ and adopt X25519MLKEM768 for QUIC/H2
|
||||
- 2026-04-15 TUN-10385: Add connectivity checks foundation
|
||||
- 2026-04-14 chore: Fix errors in cmd
|
||||
- 2026-04-14 TUN-10384: Probe TLS Helper
|
||||
- 2026-04-14 TUN-10383: Set edge-ip-version to auto
|
||||
- 2026-04-10 SECENG-13056 update gokeyless install instructions on pkg.cloudflare.com/index.html
|
||||
- 2026-04-02 TUN-9952: Bump go to 1.26
|
||||
|
||||
2026.3.0
|
||||
- 2026-03-05 TUN-10292: Add cloudflared management token command
|
||||
- 2026-03-03 chore: Addressing small fixes and typos
|
||||
- 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"
|
||||
- 2025-12-12 TUN-9886 notarize cloudflared
|
||||
|
||||
2026.1.1
|
||||
- 2026-01-19 fix: Update boto3 to run on trixie
|
||||
- 2026-01-19 fix: Fix wixl bundling tool for windows msi packages
|
||||
|
||||
@@ -17,8 +17,7 @@ import (
|
||||
// Websocket is used to carry data via WS binary frames over the tunnel from client to the origin
|
||||
// This implements the functions for glider proxy (sock5) and the carrier interface
|
||||
type Websocket struct {
|
||||
log *zerolog.Logger
|
||||
isSocks bool
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// NewWSConnection returns a new connection object
|
||||
@@ -36,7 +35,7 @@ func (ws *Websocket) ServeStream(options *StartOptions, conn io.ReadWriter) erro
|
||||
ws.log.Err(err).Str(LogFieldOriginURL, options.OriginURL).Msg("failed to connect to origin")
|
||||
return err
|
||||
}
|
||||
defer wsConn.Close()
|
||||
defer func() { _ = wsConn.Close() }()
|
||||
|
||||
stream.Pipe(wsConn, conn, ws.log)
|
||||
return nil
|
||||
|
||||
+27
-26
@@ -2,10 +2,11 @@ package carrier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -23,28 +24,19 @@ import (
|
||||
func websocketClientTLSConfig(t *testing.T) *tls.Config {
|
||||
certPool := x509.NewCertPool()
|
||||
helloCert, err := tlsconfig.GetHelloCertificateX509()
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
certPool.AddCert(helloCert)
|
||||
assert.NotNil(t, certPool)
|
||||
return &tls.Config{RootCAs: certPool}
|
||||
}
|
||||
|
||||
func TestWebsocketHeaders(t *testing.T) {
|
||||
req := testRequest(t, "http://example.com", nil)
|
||||
wsHeaders := websocketHeaders(req)
|
||||
for _, header := range stripWebsocketHeaders {
|
||||
assert.Empty(t, wsHeaders[header])
|
||||
}
|
||||
assert.Equal(t, "curl/7.59.0", wsHeaders.Get("User-Agent"))
|
||||
}
|
||||
|
||||
func TestServe(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
shutdownC := make(chan struct{})
|
||||
errC := make(chan error)
|
||||
listener, err := hello.CreateTLSListener("localhost:1111")
|
||||
assert.NoError(t, err)
|
||||
defer listener.Close()
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = listener.Close() }()
|
||||
|
||||
go func() {
|
||||
errC <- hello.StartHelloWorldServer(&log, listener, shutdownC)
|
||||
@@ -56,19 +48,25 @@ func TestServe(t *testing.T) {
|
||||
assert.NotNil(t, tlsConfig)
|
||||
d := gws.Dialer{TLSClientConfig: tlsConfig}
|
||||
conn, resp, err := clientConnect(req, &d)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
assert.Equal(t, "websocket", resp.Header.Get("Upgrade"))
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
messageSize := rand.Int()%2048 + 1
|
||||
clientMessage := make([]byte, messageSize)
|
||||
// rand.Read always returns len(clientMessage) and a nil error
|
||||
rand.Read(clientMessage)
|
||||
for range 1000 {
|
||||
messageSize, err := rand.Int(rand.Reader, big.NewInt(2048))
|
||||
require.NoError(t, err)
|
||||
clientMessage := make([]byte, messageSize.Int64()+1)
|
||||
for i := range clientMessage {
|
||||
n, err := rand.Int(rand.Reader, big.NewInt(256))
|
||||
n8 := uint8(n.Uint64()) //nolint:gosec // test-only
|
||||
require.NoError(t, err)
|
||||
clientMessage[i] = n8
|
||||
}
|
||||
err = conn.WriteMessage(websocket.BinaryFrame, clientMessage)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
messageType, message, err := conn.ReadMessage()
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, websocket.BinaryFrame, messageType)
|
||||
assert.Equal(t, clientMessage, message)
|
||||
}
|
||||
@@ -97,27 +95,30 @@ func TestWebsocketWrapper(t *testing.T) {
|
||||
req := testRequest(t, testAddr, nil)
|
||||
conn, resp, err := clientConnect(req, &d)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
assert.Equal(t, "websocket", resp.Header.Get("Upgrade"))
|
||||
|
||||
// Websocket now connected to test server so lets check our wrapper
|
||||
wrapper := cfwebsocket.GorillaConn{Conn: conn}
|
||||
buf := make([]byte, 100)
|
||||
wrapper.Write([]byte("abc"))
|
||||
_, err = wrapper.Write([]byte("abc"))
|
||||
require.NoError(t, err)
|
||||
n, err := wrapper.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, n, 3)
|
||||
require.Equal(t, 3, n)
|
||||
require.Equal(t, "abc", string(buf[:n]))
|
||||
|
||||
// Test partial read, read 1 of 3 bytes in one read and the other 2 in another read
|
||||
wrapper.Write([]byte("abc"))
|
||||
_, err = wrapper.Write([]byte("abc"))
|
||||
require.NoError(t, err)
|
||||
buf = buf[:1]
|
||||
n, err = wrapper.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, n, 1)
|
||||
require.Equal(t, 1, n)
|
||||
require.Equal(t, "a", string(buf[:n]))
|
||||
buf = buf[:cap(buf)]
|
||||
n, err = wrapper.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, n, 2)
|
||||
require.Equal(t, 2, n)
|
||||
require.Equal(t, "bc", string(buf[:n]))
|
||||
}
|
||||
|
||||
+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
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
|
||||
@@ -57,3 +60,57 @@ func ConfigureLoggingFlags(shouldHide bool) []cli.Flag {
|
||||
FlagLogOutput,
|
||||
}
|
||||
}
|
||||
|
||||
// LogTable renders lines inside an ASCII table and logs each rendered row.
|
||||
func LogTable(log *zerolog.Logger, lines []string, title ...string) {
|
||||
tableTitle := ""
|
||||
if len(title) > 0 {
|
||||
tableTitle = title[0]
|
||||
}
|
||||
for _, line := range asciiBox(lines, tableTitle, 2) {
|
||||
if line != "" {
|
||||
log.Info().Msg(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// asciiBox wraps lines in a bordered ASCII box with an optional title row.
|
||||
func asciiBox(lines []string, title string, padding int) (box []string) {
|
||||
maxLen := maxLen(lines, title)
|
||||
spacer := strings.Repeat(" ", padding)
|
||||
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
|
||||
box = append(box, border)
|
||||
if title != "" {
|
||||
box = append(box, renderBoxLine(centerLine(title, maxLen), maxLen, spacer))
|
||||
box = append(box, border)
|
||||
}
|
||||
for _, line := range lines {
|
||||
box = append(box, renderBoxLine(line, maxLen, spacer))
|
||||
}
|
||||
box = append(box, border)
|
||||
return
|
||||
}
|
||||
|
||||
// renderBoxLine pads a single line so it fills the box width.
|
||||
func renderBoxLine(line string, maxLen int, spacer string) string {
|
||||
return "|" + spacer + line + strings.Repeat(" ", maxLen-len(line)) + spacer + "|"
|
||||
}
|
||||
|
||||
// centerLine pads line evenly so it is centered within width.
|
||||
func centerLine(line string, width int) string {
|
||||
padding := width - len(line)
|
||||
leftPadding := padding / 2
|
||||
rightPadding := padding - leftPadding
|
||||
return strings.Repeat(" ", leftPadding) + line + strings.Repeat(" ", rightPadding)
|
||||
}
|
||||
|
||||
// maxLen returns the longest visible line length including the title.
|
||||
func maxLen(lines []string, title string) int {
|
||||
max := len(title)
|
||||
for _, line := range lines {
|
||||
if len(line) > max {
|
||||
max = len(line)
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLogTableWithoutTitle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
lines := captureTableLogs(t, []string{"first", "second"})
|
||||
|
||||
assert.Equal(t, []string{
|
||||
"+----------+",
|
||||
"| first |",
|
||||
"| second |",
|
||||
"+----------+",
|
||||
}, lines)
|
||||
}
|
||||
|
||||
func TestLogTableWithTitle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
lines := captureTableLogs(t, []string{"first", "second"}, "TT")
|
||||
|
||||
assert.Equal(t, []string{
|
||||
"+----------+",
|
||||
"| TT |",
|
||||
"+----------+",
|
||||
"| first |",
|
||||
"| second |",
|
||||
"+----------+",
|
||||
}, lines)
|
||||
}
|
||||
|
||||
func captureTableLogs(t *testing.T, lines []string, title ...string) []string {
|
||||
t.Helper()
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
LogTable(&logger, lines, title...)
|
||||
|
||||
// nolint: prealloc
|
||||
var messages []string
|
||||
for _, line := range bytes.Split(bytes.TrimSpace(buf.Bytes()), []byte("\n")) {
|
||||
var entry struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(line, &entry))
|
||||
messages = append(messages, entry.Message)
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
@@ -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,9 @@ const (
|
||||
// NoAutoUpdate is the command line flag to disable cloudflared from checking for updates
|
||||
NoAutoUpdate = "no-autoupdate"
|
||||
|
||||
// NoPrechecks is the command line flag to skip connectivity pre-checks at startup.
|
||||
NoPrechecks = "no-prechecks"
|
||||
|
||||
// LogLevel is the command line flag for the cloudflared logging level
|
||||
LogLevel = "loglevel"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+84
-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,18 @@ 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
|
||||
// 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 +396,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)
|
||||
@@ -448,6 +417,13 @@ func StartServer(
|
||||
}
|
||||
connectorID := tunnelConfig.ClientConfig.ConnectorID
|
||||
|
||||
// Run connectivity pre-checks for cloudflared. This runs in a separate
|
||||
// goroutine, as we want to keep initializing cloudflared while prechecks
|
||||
// are running. Prechecks are controlled via DNS flag for remote kill-switch capability.
|
||||
if !tunnelConfig.ClientConfig.ConnectionFeaturesSnapshot().SkipPrechecks && !c.Bool(cfdflags.NoPrechecks) {
|
||||
go runPrechecks(c, log, tunnelConfig.Region)
|
||||
}
|
||||
|
||||
// Disable ICMP packet routing for quick tunnels
|
||||
if quickTunnelURL != "" {
|
||||
tunnelConfig.ICMPRouterServer = nil
|
||||
@@ -489,7 +465,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 +523,42 @@ func StartServer(
|
||||
return waitToShutdown(&wg, cancel, errC, graceShutdownC, gracePeriod, log)
|
||||
}
|
||||
|
||||
// runPrechecks executes connectivity pre-checks and logs the results.
|
||||
// Pre-checks are diagnostic only and do not gate tunnel startup.
|
||||
func runPrechecks(c *cli.Context, log *zerolog.Logger, region string) {
|
||||
ipVersion := allregions.Auto
|
||||
if ipVersionStr := c.String(cfdflags.EdgeIpVersion); ipVersionStr != "" {
|
||||
parsedVersion, err := parseConfigIPVersion(ipVersionStr)
|
||||
if err == nil {
|
||||
ipVersion = parsedVersion
|
||||
} else {
|
||||
log.Warn().Str("edgeIpVersion", ipVersionStr).Err(err).Msg("Invalid edge-ip-version value, using auto")
|
||||
}
|
||||
}
|
||||
|
||||
cfg := prechecks.Config{
|
||||
Region: region,
|
||||
IPVersion: ipVersion,
|
||||
EdgeAddrs: c.StringSlice(cfdflags.Edge),
|
||||
ProtocolOverride: c.String(cfdflags.Protocol),
|
||||
}
|
||||
|
||||
dialers := prechecks.RunDialers{
|
||||
DNSResolver: &prechecks.EdgeDNSResolver{Log: log},
|
||||
TCPDialer: &prechecks.EdgeTCPDialer{},
|
||||
QUICDialer: &prechecks.EdgeQUICDialer{},
|
||||
ManagementDialer: &prechecks.NetManagementDialer{Dialer: net.Dialer{}},
|
||||
}
|
||||
|
||||
report := prechecks.Run(c.Context, c.String(cfdflags.CACert), cfg, log, dialers)
|
||||
|
||||
// Output the human-readable table
|
||||
cliutil.LogTable(log, report.String(), "CONNECTIVITY PRE-CHECKS")
|
||||
|
||||
// Also log structured results for log aggregation
|
||||
report.LogEvent(log)
|
||||
}
|
||||
|
||||
func waitToShutdown(wg *sync.WaitGroup,
|
||||
cancelServerContext func(),
|
||||
errC <-chan error,
|
||||
@@ -603,13 +615,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 +654,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 +678,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 +688,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 +928,13 @@ func configureCloudflaredFlags(shouldHide bool) []cli.Flag {
|
||||
Value: false,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: cfdflags.NoPrechecks,
|
||||
Usage: "Skip connectivity pre-checks at startup.",
|
||||
EnvVars: []string{"TUNNEL_NO_PRECHECKS"},
|
||||
Value: false,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: cfdflags.Metrics,
|
||||
Value: metrics.GetMetricsDefaultAddress(metrics.Runtime),
|
||||
@@ -938,6 +958,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 +1193,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 +1208,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 +1220,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,7 @@ func prepareTunnelConfig(
|
||||
DisableQUICPathMTUDiscovery: c.Bool(flags.QuicDisablePathMTUDiscovery),
|
||||
QUICConnectionLevelFlowControlLimit: c.Uint64(flags.QuicConnLevelFlowControlLimit),
|
||||
QUICStreamLevelFlowControlLimit: c.Uint64(flags.QuicStreamLevelFlowControlLimit),
|
||||
NoPrechecks: c.Bool(flags.NoPrechecks),
|
||||
OriginDNSService: dnsService,
|
||||
OriginDialerService: originDialerService,
|
||||
}
|
||||
@@ -307,7 +291,7 @@ func gracePeriod(c *cli.Context) (time.Duration, error) {
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
return term.IsTerminal(int(os.Stdout.Fd()))
|
||||
return term.IsTerminal(int(os.Stdout.Fd())) // nolint:gosec
|
||||
}
|
||||
|
||||
// ParseConfigIPVersion returns the IP version from possible expected values from config
|
||||
@@ -348,7 +332,7 @@ func testIPBindable(ip net.IP) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listener.Close()
|
||||
_ = listener.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -510,7 +494,7 @@ func findLocalAddr(dst net.IP, port int) (netip.Addr, error) {
|
||||
if err != nil {
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
defer udpConn.Close()
|
||||
defer func() { _ = udpConn.Close() }()
|
||||
localAddrPort, err := netip.ParseAddrPort(udpConn.LocalAddr().String())
|
||||
if err != nil {
|
||||
return netip.Addr{}, err
|
||||
|
||||
@@ -100,6 +100,7 @@ func login(c *cli.Context) error {
|
||||
c.Bool(cfdflags.AutoCloseInterstitial),
|
||||
isFEDRamp,
|
||||
log,
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Failed to write the certificate.\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", path)
|
||||
@@ -122,7 +123,7 @@ func login(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, resourceData, 0600); err != nil {
|
||||
if err := os.WriteFile(path, resourceData, 0600); err != nil { // nolint: gosec
|
||||
return errors.Wrap(err, fmt.Sprintf("error writing cert to %s", path))
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
)
|
||||
@@ -44,7 +45,7 @@ func RunQuickTunnel(sc *subcommandContext) error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to request quick Tunnel")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
// This will read the entire response into memory so we can print it in case of error
|
||||
rsp_body, err := io.ReadAll(resp.Body)
|
||||
@@ -76,12 +77,10 @@ func RunQuickTunnel(sc *subcommandContext) error {
|
||||
url = "https://" + url
|
||||
}
|
||||
|
||||
for _, line := range AsciiBox([]string{
|
||||
cliutil.LogTable(sc.log, []string{
|
||||
"Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):",
|
||||
url,
|
||||
}, 2) {
|
||||
sc.log.Info().Msg(line)
|
||||
}
|
||||
})
|
||||
|
||||
if !sc.c.IsSet(flags.Protocol) {
|
||||
_ = sc.c.Set(flags.Protocol, "quic")
|
||||
@@ -116,26 +115,3 @@ type QuickTunnel struct {
|
||||
AccountTag string `json:"account_tag"`
|
||||
Secret []byte `json:"secret"`
|
||||
}
|
||||
|
||||
// Print out the given lines in a nice ASCII box.
|
||||
func AsciiBox(lines []string, padding int) (box []string) {
|
||||
maxLen := maxLen(lines)
|
||||
spacer := strings.Repeat(" ", padding)
|
||||
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
|
||||
box = append(box, border)
|
||||
for _, line := range lines {
|
||||
box = append(box, "|"+spacer+line+strings.Repeat(" ", maxLen-len(line))+spacer+"|")
|
||||
}
|
||||
box = append(box, border)
|
||||
return
|
||||
}
|
||||
|
||||
func maxLen(lines []string) int {
|
||||
max := 0
|
||||
for _, line := range lines {
|
||||
if len(line) > max {
|
||||
max = len(line)
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
+27
-37
@@ -1,10 +1,9 @@
|
||||
import json
|
||||
import subprocess
|
||||
from time import sleep
|
||||
|
||||
from constants import MANAGEMENT_HOST_NAME
|
||||
from setup import get_config_from_file
|
||||
from util import get_tunnel_connector_id
|
||||
from util import get_tunnel_connector_id, CloudflaredProcess
|
||||
|
||||
SINGLE_CASE_TIMEOUT = 600
|
||||
|
||||
@@ -30,7 +29,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 +37,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}"
|
||||
|
||||
@@ -66,38 +82,12 @@ class CloudflaredCli:
|
||||
|
||||
def __enter__(self):
|
||||
self.basecmd += ["run"]
|
||||
self.process = subprocess.Popen(self.basecmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
self.logger.info(f"Run cmd {self.basecmd}")
|
||||
return self.process
|
||||
self.cfd = CloudflaredProcess(self.basecmd, allow_input=False, capture_output=True)
|
||||
return self.cfd
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||
terminate_gracefully(self.process, self.logger, self.basecmd)
|
||||
self.logger.debug(f"{self.basecmd} logs: {self.process.stderr.read()}")
|
||||
|
||||
|
||||
def terminate_gracefully(process, logger, cmd):
|
||||
process.terminate()
|
||||
process_terminated = wait_for_terminate(process)
|
||||
if not process_terminated:
|
||||
process.kill()
|
||||
logger.warning(f"{cmd}: cloudflared did not terminate within wait period. Killing process. logs: \
|
||||
stdout: {process.stdout.read()}, stderr: {process.stderr.read()}")
|
||||
|
||||
|
||||
def wait_for_terminate(opened_subprocess, attempts=10, poll_interval=1):
|
||||
"""
|
||||
wait_for_terminate polls the opened_subprocess every x seconds for a given number of attempts.
|
||||
It returns true if the subprocess was terminated and false if it didn't.
|
||||
"""
|
||||
for _ in range(attempts):
|
||||
if _is_process_stopped(opened_subprocess):
|
||||
return True
|
||||
sleep(poll_interval)
|
||||
return False
|
||||
|
||||
|
||||
def _is_process_stopped(process):
|
||||
return process.poll() is not None
|
||||
self.cfd.cleanup()
|
||||
|
||||
|
||||
def cert_path():
|
||||
|
||||
@@ -5,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,9 +3,19 @@ MAX_RETRIES = 5
|
||||
BACKOFF_SECS = 7
|
||||
MAX_LOG_LINES = 50
|
||||
|
||||
PROXY_DNS_PORT = 9053
|
||||
MANAGEMENT_HOST_NAME = "management.argotunnel.com"
|
||||
|
||||
# How long to wait for the cloudflared process to exit after SIGTERM before
|
||||
# sending SIGKILL.
|
||||
GRACEFUL_SHUTDOWN_TIMEOUT = 10
|
||||
# How long to wait for each pipe reader thread to finish after the process
|
||||
# exits.
|
||||
READER_THREAD_JOIN_TIMEOUT = 5
|
||||
# How long to wait for an expected log message to appear before giving up.
|
||||
LOG_POLL_TIMEOUT = 30
|
||||
# How often to re-check the accumulated log lines while polling.
|
||||
LOG_POLL_INTERVAL = 0.5
|
||||
|
||||
|
||||
def protocols():
|
||||
return ["http2", "quic"]
|
||||
|
||||
@@ -17,16 +17,6 @@ class TestEdgeDiscovery:
|
||||
config["edge-ip-version"] = edge_ip_version
|
||||
return config
|
||||
|
||||
@pytest.mark.parametrize("protocol", protocols())
|
||||
def test_default_only(self, tmp_path, component_tests_config, protocol):
|
||||
"""
|
||||
This test runs a tunnel to connect via IPv4-only edge addresses (default is unset "--edge-ip-version 4")
|
||||
"""
|
||||
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)
|
||||
|
||||
@pytest.mark.parametrize("protocol", protocols())
|
||||
def test_ipv4_only(self, tmp_path, component_tests_config, protocol):
|
||||
"""
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from constants import MAX_LOG_LINES
|
||||
from constants import MAX_LOG_LINES, LOG_POLL_INTERVAL, LOG_POLL_TIMEOUT
|
||||
from util import start_cloudflared, wait_tunnel_ready, send_requests
|
||||
|
||||
# Rolling logger rotate log files after 1 MB
|
||||
@@ -12,12 +13,14 @@ expect_message = "Starting Hello"
|
||||
|
||||
|
||||
def assert_log_to_terminal(cloudflared):
|
||||
for _ in range(0, MAX_LOG_LINES):
|
||||
line = cloudflared.stderr.readline()
|
||||
if not line:
|
||||
break
|
||||
if expect_message.encode() in line:
|
||||
return
|
||||
# All logs are drained by a background thread into cloudflared.stdout_lines.
|
||||
# Poll the accumulated lines until the expected message appears.
|
||||
deadline = time.monotonic() + LOG_POLL_TIMEOUT
|
||||
while time.monotonic() < deadline:
|
||||
for line in list(cloudflared.stdout_lines):
|
||||
if expect_message.encode() in line:
|
||||
return
|
||||
time.sleep(LOG_POLL_INTERVAL)
|
||||
raise Exception(f"terminal log doesn't contain {expect_message}")
|
||||
|
||||
|
||||
|
||||
@@ -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]}")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Integration tests for cloudflared connectivity pre-checks (TUN-10391).
|
||||
|
||||
Scope
|
||||
-----
|
||||
These tests verify the end-to-end behavior of cloudflared pre-checks:
|
||||
- that the human-readable table written to the log output has the correct
|
||||
structure and content,
|
||||
- that structured JSON log lines are emitted with the expected fields, and
|
||||
- that running the `diag` subcommand against a live tunnel instance produces a
|
||||
zip archive that contains prechecks.json.
|
||||
|
||||
They do NOT cover every failure mode of the precheck logic — those are owned
|
||||
by the unit tests in prechecks/checker_test.go which use mock dialers.
|
||||
|
||||
At the integration level the only reliable way to induce specific failure modes
|
||||
without real firewall intervention is:
|
||||
|
||||
- --edge <unreachable>: StaticEdgeDNSResolver resolves the literal IP
|
||||
directly (DNS row = PASS), then both QUIC and HTTP/2 probes time out
|
||||
-> hard fail (both transports blocked).
|
||||
This does NOT exercise the DNS-failure -> transport-skip path.
|
||||
|
||||
DNS failure and Management API failure cannot be triggered via CLI flags alone;
|
||||
they require network-level intervention outside the component-test harness.
|
||||
|
||||
stdout/stderr design
|
||||
--------------------
|
||||
The pre-checks table is emitted via cliutil.LogTable, which wraps the content
|
||||
in an ASCII box and logs each line at Info level through zerolog. zerolog
|
||||
writes to stderr, which the test harness merges into stdout (stderr=STDOUT in
|
||||
Popen). We poll a --logfile for the "precheck complete" sentinel before
|
||||
leaving the `with` block, ensuring the goroutine has finished. We then call
|
||||
cfd.terminate(). After the `with` block exits, the process is dead and all
|
||||
output has been captured by CloudflaredProcess's background reader thread. We
|
||||
read the accumulated lines from cfd.stdout_lines.
|
||||
|
||||
Box format (cliutil.asciiBox with padding=2, title="CONNECTIVITY PRE-CHECKS"):
|
||||
+----...----+
|
||||
| CONNECTIVITY PRE-CHECKS | (centered title)
|
||||
+----...----+
|
||||
| COMPONENT TARGET ... | (content rows)
|
||||
...
|
||||
+----...----+
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
import zipfile as zipfilemod
|
||||
|
||||
from constants import METRICS_PORT
|
||||
from util import LOGGER, start_cloudflared, wait_tunnel_ready
|
||||
|
||||
# ASCII box constants (cliutil.asciiBox, padding=2, title="CONNECTIVITY PRE-CHECKS")
|
||||
BOX_TITLE = "CONNECTIVITY PRE-CHECKS"
|
||||
BOX_BORDER_RE = re.compile(r"^\+(-+)\+$", re.MULTILINE) # matches +----...----+
|
||||
COL_HEADER = "COMPONENT" # first word of the column-header row
|
||||
|
||||
# zerolog console format: "2006-01-02T15:04:05Z LVL <message>"
|
||||
_LOG_PREFIX_RE = re.compile(r"^\S+ \w+ ")
|
||||
|
||||
# Component names (probes.go: componentXxx)
|
||||
COMP_DNS = "DNS Resolution"
|
||||
COMP_QUIC = "UDP Connectivity"
|
||||
COMP_H2 = "TCP Connectivity"
|
||||
COMP_API = "Cloudflare API"
|
||||
|
||||
# Target labels used in the rendered table.
|
||||
#
|
||||
# probeRegion() (checker.go:216) always overwrites the Target field of
|
||||
# whatever CheckResult the inner probe function returns with the regionTarget
|
||||
# hostname, so QUIC and HTTP/2 rows carry the same region hostname as the
|
||||
# corresponding DNS row — not the "Port 7844 (QUIC/HTTP2)" strings that
|
||||
# targetPortQUIC/targetPortHTTP2 define. Those port-label constants are only
|
||||
# used in the empty-addrs SKIP branch and inside action message strings.
|
||||
TARGET_API = "api.cloudflare.com:443"
|
||||
TARGET_REGION1 = "region1.v2.argotunnel.com"
|
||||
TARGET_REGION2 = "region2.v2.argotunnel.com"
|
||||
|
||||
# Details strings (probes.go: detailsXxx)
|
||||
DETAILS_DNS_RESOLVED = "DNS Resolved successfully"
|
||||
DETAILS_QUIC_OK = "QUIC connection successful"
|
||||
DETAILS_HTTP2_OK = "HTTP/2 connection successful"
|
||||
DETAILS_API_OK = "API is reachable"
|
||||
DETAILS_QUIC_FAIL = "QUIC connection failed"
|
||||
DETAILS_HTTP2_FAIL = "HTTP/2 connection is blocked or unreachable"
|
||||
|
||||
# Status labels (result.go: xyzStatus)
|
||||
PASS = "PASS"
|
||||
FAIL = "FAIL"
|
||||
SKIP = "SKIP"
|
||||
|
||||
# Action prefixes (result.go: renderActions)
|
||||
PREFIX_ERROR = "ERROR: "
|
||||
PREFIX_WARNING = "WARNING: "
|
||||
|
||||
# Action messages (probes.go: actionXxx)
|
||||
ACTION_QUIC_BLOCKED = "Allow outbound QUIC traffic on port 7844 or use HTTP2."
|
||||
ACTION_HTTP2_BLOCKED = "Allow outbound TCP on port 7844."
|
||||
|
||||
# Exact summary lines (result.go: summaryLine)
|
||||
SUMMARY_HEALTHY = "SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol."
|
||||
SUMMARY_CRITICAL = "SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel."
|
||||
|
||||
# structured log constants (result.go)
|
||||
|
||||
LOG_MSG_PRECHECK = "precheck"
|
||||
LOG_MSG_PRECHECK_COMPLETE = "precheck complete"
|
||||
STATUS_PASS_LOG = "pass"
|
||||
|
||||
UNREACHABLE_EDGE = "192.0.2.1:7844"
|
||||
|
||||
# cloudflared dial timeout per probe: 5 s, up to 2 retries -> ~15 s total.
|
||||
PRECHECK_POLL_TIMEOUT_SECS = 15
|
||||
PRECHECK_POLL_INTERVAL_SECS = 1
|
||||
|
||||
# ---------- helpers ----------
|
||||
|
||||
def _poll_log_file_for_precheck_complete(log_file: str, timeout: float) -> list[dict]:
|
||||
"""
|
||||
Poll a JSON log file until a 'precheck complete' line appears or timeout
|
||||
expires. Returns all precheck-related log lines found.
|
||||
|
||||
cloudflared's --logfile writes one JSON object per line. Polling keeps
|
||||
the test fast on healthy networks and still tolerates slow CI hosts.
|
||||
|
||||
We re-read from the beginning of the file on every poll because the file
|
||||
is append-only, small, and tracking a byte offset would add complexity with
|
||||
no meaningful performance benefit for a ~15 s total window.
|
||||
"""
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
lines = _read_precheck_log_lines_from_file(log_file)
|
||||
if any(l.get("message") == LOG_MSG_PRECHECK_COMPLETE for l in lines):
|
||||
return lines
|
||||
time.sleep(PRECHECK_POLL_INTERVAL_SECS)
|
||||
return _read_precheck_log_lines_from_file(log_file)
|
||||
|
||||
|
||||
def _read_precheck_log_lines_from_file(log_file: str) -> list[dict]:
|
||||
"""Parse all precheck-related JSON log lines from a --logfile path."""
|
||||
result = []
|
||||
try:
|
||||
with open(log_file, "r") as f:
|
||||
for raw_line in f:
|
||||
raw_line = raw_line.strip()
|
||||
if not raw_line:
|
||||
continue
|
||||
try:
|
||||
obj = json.loads(raw_line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
msg = obj.get("message") or obj.get("msg", "")
|
||||
if msg in (LOG_MSG_PRECHECK, LOG_MSG_PRECHECK_COMPLETE):
|
||||
result.append(obj)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
# stdout table parse
|
||||
class TableRow:
|
||||
"""One data row parsed from the rendered precheck table."""
|
||||
def __init__(self, component: str, target: str, status: str, details: str):
|
||||
self.component = component
|
||||
self.target = target
|
||||
self.status = status
|
||||
self.details = details
|
||||
|
||||
def __repr__(self):
|
||||
return f"TableRow({self.component!r}, {self.target!r}, {self.status!r}, {self.details!r})"
|
||||
|
||||
|
||||
def _strip_log_prefix(line: str) -> str:
|
||||
"""Remove the zerolog console prefix ('2006-01-02T15:04:05Z LVL ') if present."""
|
||||
return _LOG_PREFIX_RE.sub("", line, count=1)
|
||||
|
||||
|
||||
def _unbox_line(line: str) -> str:
|
||||
"""Strip the box border padding from a content line: '| text |' -> 'text'.
|
||||
|
||||
Accepts lines that may still carry a zerolog console prefix; the prefix is
|
||||
removed before the box delimiters are stripped.
|
||||
"""
|
||||
msg = _strip_log_prefix(line)
|
||||
if msg.startswith("|") and msg.endswith("|"):
|
||||
return msg[1:-1].strip()
|
||||
return msg.strip()
|
||||
|
||||
|
||||
def _parse_table(stdout: str) -> list[TableRow]:
|
||||
"""
|
||||
Parse the data rows from a precheck table in stdout.
|
||||
|
||||
The table is now wrapped in an ASCII box by cliutil.LogTable. Each
|
||||
content line has the form '| <content> |', optionally preceded by a
|
||||
zerolog console prefix. We strip both the prefix and the box borders
|
||||
before splitting on two-or-more spaces (text/tabwriter padding=2).
|
||||
|
||||
We skip the column-header row and stop at blank lines, SUMMARY, box
|
||||
border lines, ERROR, or WARNING lines.
|
||||
"""
|
||||
rows = []
|
||||
in_data = False
|
||||
for raw_line in stdout.splitlines():
|
||||
msg = _strip_log_prefix(raw_line)
|
||||
line = _unbox_line(raw_line)
|
||||
if line.startswith("COMPONENT"):
|
||||
in_data = True
|
||||
continue
|
||||
if not in_data:
|
||||
continue
|
||||
if (line == "" or line.startswith("SUMMARY") or BOX_BORDER_RE.match(msg)
|
||||
or line.startswith("ERROR") or line.startswith("WARNING")):
|
||||
in_data = False
|
||||
continue
|
||||
parts = re.split(r" +", line.rstrip())
|
||||
if len(parts) >= 3:
|
||||
rows.append(TableRow(
|
||||
component=parts[0],
|
||||
target=parts[1],
|
||||
status=parts[2],
|
||||
details=parts[3] if len(parts) >= 4 else "",
|
||||
))
|
||||
return rows
|
||||
|
||||
|
||||
def _rows_for(rows: list[TableRow], component: str) -> list[TableRow]:
|
||||
return [r for r in rows if r.component == component]
|
||||
|
||||
|
||||
# log assertions
|
||||
|
||||
def _assert_precheck_summary_log(
|
||||
log_lines: list[dict],
|
||||
*,
|
||||
hard_fail: bool,
|
||||
suggested_protocol: str | None = None,
|
||||
):
|
||||
"""Assert the 'precheck complete' summary log line has the expected fields."""
|
||||
summary_lines = [l for l in log_lines if l.get("message") == LOG_MSG_PRECHECK_COMPLETE]
|
||||
assert len(summary_lines) == 1, \
|
||||
f"Expected exactly one '{LOG_MSG_PRECHECK_COMPLETE}' log line; got {summary_lines}"
|
||||
summary = summary_lines[0]
|
||||
|
||||
assert summary.get("hard_fail") is hard_fail, \
|
||||
f"Expected hard_fail={hard_fail} in summary log: {summary}"
|
||||
|
||||
if suggested_protocol is not None:
|
||||
assert summary.get("suggested_protocol") == suggested_protocol, \
|
||||
(f"Expected suggested_protocol={suggested_protocol!r}; "
|
||||
f"got {summary.get('suggested_protocol')!r}")
|
||||
|
||||
|
||||
# ---------- Tests ----------
|
||||
|
||||
class TestPrechecksHappyPath:
|
||||
"""
|
||||
On a healthy connection all probes pass. We assert:
|
||||
- the full table structure (header, column header, separator)
|
||||
- every row's component, target, status, and details
|
||||
- no ERROR/WARNING action lines
|
||||
- the exact summary line
|
||||
- the structured log summary (hard_fail=false, suggested_protocol=quic)
|
||||
"""
|
||||
|
||||
def test_prechecks_pass_on_healthy_connection(self, tmp_path, component_tests_config):
|
||||
log_file = str(tmp_path / "cloudflared.log")
|
||||
config = component_tests_config({"logfile": log_file})
|
||||
|
||||
with start_cloudflared(
|
||||
tmp_path,
|
||||
config,
|
||||
cfd_pre_args=["tunnel", "--ha-connections", "1"],
|
||||
cfd_args=["run"],
|
||||
new_process=True,
|
||||
capture_output=True,
|
||||
) as cfd:
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
||||
# Poll the log file for the sentinel before signalling the process.
|
||||
log_lines = _poll_log_file_for_precheck_complete(
|
||||
log_file, timeout=PRECHECK_POLL_TIMEOUT_SECS
|
||||
)
|
||||
# Signal shutdown.
|
||||
cfd.terminate()
|
||||
|
||||
# The process is now dead. All output was captured by the background
|
||||
# reader thread into cfd.stdout_lines (stderr is merged into stdout).
|
||||
stdout = b"".join(cfd.stdout_lines).decode(errors="replace")
|
||||
|
||||
LOGGER.debug(f"[happy-path] stdout:\n{stdout}")
|
||||
LOGGER.debug(f"[happy-path] log_lines:\n{log_lines}")
|
||||
|
||||
# Strip zerolog console prefixes so pattern matching works on raw messages.
|
||||
messages = "\n".join(_strip_log_prefix(l) for l in stdout.splitlines())
|
||||
|
||||
# ── table structure ──────────────────────────────────────────────────
|
||||
# zerolog writes to stderr which is merged into stdout by the harness.
|
||||
# The table is wrapped in an ASCII box by cliutil.LogTable.
|
||||
assert BOX_TITLE in messages, \
|
||||
f"Expected box title '{BOX_TITLE}' in output;\ngot:\n{stdout}"
|
||||
assert COL_HEADER in messages, \
|
||||
f"Expected column header row in output;\ngot:\n{stdout}"
|
||||
assert BOX_BORDER_RE.search(messages), \
|
||||
f"Expected box border line (+---+) in output;\ngot:\n{stdout}"
|
||||
|
||||
# ── row content ──────────────────────────────────────────────────────
|
||||
rows = _parse_table(stdout)
|
||||
assert len(rows) == 7, \
|
||||
f"Expected 7 rows (2 DNS + 2 QUIC + 2 HTTP/2 + 1 API); got {len(rows)}: {rows}"
|
||||
|
||||
dns_rows = _rows_for(rows, COMP_DNS)
|
||||
assert len(dns_rows) == 2, f"Expected 2 DNS rows; got {dns_rows}"
|
||||
assert dns_rows[0].target == TARGET_REGION1
|
||||
assert dns_rows[1].target == TARGET_REGION2
|
||||
for r in dns_rows:
|
||||
assert r.status == PASS, f"DNS row not PASS: {r}"
|
||||
assert r.details == DETAILS_DNS_RESOLVED, f"DNS row details wrong: {r}"
|
||||
|
||||
quic_rows = _rows_for(rows, COMP_QUIC)
|
||||
assert len(quic_rows) == 2, f"Expected 2 QUIC rows; got {quic_rows}"
|
||||
assert quic_rows[0].target == TARGET_REGION1, f"QUIC row[0] target wrong: {quic_rows[0]}"
|
||||
assert quic_rows[1].target == TARGET_REGION2, f"QUIC row[1] target wrong: {quic_rows[1]}"
|
||||
for r in quic_rows:
|
||||
assert r.status == PASS, f"QUIC row not PASS: {r}"
|
||||
assert r.details == DETAILS_QUIC_OK, f"QUIC row details wrong: {r}"
|
||||
|
||||
h2_rows = _rows_for(rows, COMP_H2)
|
||||
assert len(h2_rows) == 2, f"Expected 2 HTTP/2 rows; got {h2_rows}"
|
||||
assert h2_rows[0].target == TARGET_REGION1, f"HTTP/2 row[0] target wrong: {h2_rows[0]}"
|
||||
assert h2_rows[1].target == TARGET_REGION2, f"HTTP/2 row[1] target wrong: {h2_rows[1]}"
|
||||
for r in h2_rows:
|
||||
assert r.status == PASS, f"HTTP/2 row not PASS: {r}"
|
||||
assert r.details == DETAILS_HTTP2_OK, f"HTTP/2 row details wrong: {r}"
|
||||
|
||||
api_rows = _rows_for(rows, COMP_API)
|
||||
assert len(api_rows) == 1, f"Expected 1 API row; got {api_rows}"
|
||||
assert api_rows[0].target == TARGET_API, f"API row target wrong: {api_rows[0]}"
|
||||
assert api_rows[0].status == PASS, f"API row not PASS: {api_rows[0]}"
|
||||
assert api_rows[0].details == DETAILS_API_OK, f"API row details wrong: {api_rows[0]}"
|
||||
|
||||
# ── no action lines ──────────────────────────────────────────────────
|
||||
assert PREFIX_ERROR not in messages, f"Unexpected ERROR action:\n{stdout}"
|
||||
assert PREFIX_WARNING not in messages, f"Unexpected WARNING action:\n{stdout}"
|
||||
|
||||
# ── summary line ─────────────────────────────────────────────────────
|
||||
assert SUMMARY_HEALTHY in messages, \
|
||||
f"Expected healthy summary;\ngot:\n{stdout}"
|
||||
|
||||
# ── structured log ───────────────────────────────────────────────────
|
||||
assert len(log_lines) > 0, \
|
||||
"Expected at least one structured precheck log line in log file"
|
||||
for line in log_lines:
|
||||
if line.get("message") == LOG_MSG_PRECHECK:
|
||||
assert line.get("status") == STATUS_PASS_LOG, \
|
||||
f"Expected status=pass in precheck log line: {line}"
|
||||
_assert_precheck_summary_log(log_lines, hard_fail=False, suggested_protocol="quic")
|
||||
|
||||
|
||||
class TestPrechecksHardFail:
|
||||
"""
|
||||
When --edge points at an unreachable IP, StaticEdgeDNSResolver resolves
|
||||
the literal address directly (DNS row = PASS), but both transport probes
|
||||
time out -> hard fail. We assert:
|
||||
- the full table structure
|
||||
- DNS row: PASS (the literal IP was resolved)
|
||||
- QUIC row: FAIL with correct details + ERROR action
|
||||
- HTTP/2 row: FAIL with correct details + ERROR action
|
||||
- API row: PASS (api.cloudflare.com:443 is independently reachable)
|
||||
- the exact critical summary line
|
||||
- the structured log summary (hard_fail=true)
|
||||
|
||||
This test does NOT call wait_tunnel_ready because the tunnel will not
|
||||
connect to the unreachable address.
|
||||
"""
|
||||
|
||||
def test_prechecks_hard_fail_when_edge_unreachable(self, tmp_path, component_tests_config):
|
||||
log_file = str(tmp_path / "cloudflared.log")
|
||||
config = component_tests_config({"logfile": log_file})
|
||||
|
||||
with start_cloudflared(
|
||||
tmp_path,
|
||||
config,
|
||||
cfd_pre_args=[
|
||||
"tunnel",
|
||||
"--ha-connections", "1",
|
||||
"--edge", UNREACHABLE_EDGE,
|
||||
],
|
||||
cfd_args=["run"],
|
||||
new_process=True,
|
||||
capture_output=True,
|
||||
) as cfd:
|
||||
log_lines = _poll_log_file_for_precheck_complete(
|
||||
log_file, timeout=PRECHECK_POLL_TIMEOUT_SECS
|
||||
)
|
||||
cfd.terminate()
|
||||
|
||||
stdout = b"".join(cfd.stdout_lines).decode(errors="replace")
|
||||
|
||||
LOGGER.debug(f"[hard-fail] stdout:\n{stdout}")
|
||||
LOGGER.debug(f"[hard-fail] log_lines:\n{log_lines}")
|
||||
|
||||
# Strip zerolog console prefixes so pattern matching works on raw messages.
|
||||
messages = "\n".join(_strip_log_prefix(l) for l in stdout.splitlines())
|
||||
|
||||
# ── table structure ──────────────────────────────────────────────────
|
||||
# zerolog writes to stderr which is merged into stdout by the harness.
|
||||
# The table is wrapped in an ASCII box by cliutil.LogTable.
|
||||
assert BOX_TITLE in messages, \
|
||||
f"Expected box title '{BOX_TITLE}' in output;\ngot:\n{stdout}"
|
||||
assert COL_HEADER in messages, \
|
||||
f"Expected column header row in output;\ngot:\n{stdout}"
|
||||
assert BOX_BORDER_RE.search(messages), \
|
||||
f"Expected box border line (+---+) in output;\ngot:\n{stdout}"
|
||||
|
||||
# ── row content ──────────────────────────────────────────────────────
|
||||
rows = _parse_table(stdout)
|
||||
assert len(rows) == 4, \
|
||||
f"Expected 4 rows (1 DNS + 1 QUIC + 1 HTTP/2 + 1 API); got {len(rows)}: {rows}"
|
||||
|
||||
dns_rows = _rows_for(rows, COMP_DNS)
|
||||
assert len(dns_rows) == 1, f"Expected 1 DNS row; got {dns_rows}"
|
||||
assert dns_rows[0].target == UNREACHABLE_EDGE
|
||||
assert dns_rows[0].status == PASS, f"DNS row not PASS: {dns_rows[0]}"
|
||||
assert dns_rows[0].details == DETAILS_DNS_RESOLVED, f"DNS row details wrong: {dns_rows[0]}"
|
||||
|
||||
quic_rows = _rows_for(rows, COMP_QUIC)
|
||||
assert len(quic_rows) == 1, f"Expected 1 QUIC row; got {quic_rows}"
|
||||
assert quic_rows[0].target == UNREACHABLE_EDGE, f"QUIC row target wrong: {quic_rows[0]}"
|
||||
assert quic_rows[0].status == FAIL, f"QUIC row not FAIL: {quic_rows[0]}"
|
||||
assert quic_rows[0].details == DETAILS_QUIC_FAIL, f"QUIC row details wrong: {quic_rows[0]}"
|
||||
|
||||
h2_rows = _rows_for(rows, COMP_H2)
|
||||
assert len(h2_rows) == 1, f"Expected 1 HTTP/2 row; got {h2_rows}"
|
||||
assert h2_rows[0].target == UNREACHABLE_EDGE, f"HTTP/2 row target wrong: {h2_rows[0]}"
|
||||
assert h2_rows[0].status == FAIL, f"HTTP/2 row not FAIL: {h2_rows[0]}"
|
||||
assert h2_rows[0].details == DETAILS_HTTP2_FAIL, f"HTTP/2 row details wrong: {h2_rows[0]}"
|
||||
|
||||
api_rows = _rows_for(rows, COMP_API)
|
||||
assert len(api_rows) == 1, f"Expected 1 API row; got {api_rows}"
|
||||
assert api_rows[0].target == TARGET_API, f"API row target wrong: {api_rows[0]}"
|
||||
assert api_rows[0].status == PASS, f"API row not PASS: {api_rows[0]}"
|
||||
assert api_rows[0].details == DETAILS_API_OK, f"API row details wrong: {api_rows[0]}"
|
||||
|
||||
assert f"{PREFIX_ERROR}{ACTION_QUIC_BLOCKED}" in messages, \
|
||||
f"Expected QUIC ERROR action;\ngot:\n{stdout}"
|
||||
assert f"{PREFIX_ERROR}{ACTION_HTTP2_BLOCKED}" in messages, \
|
||||
f"Expected HTTP/2 ERROR action;\ngot:\n{stdout}"
|
||||
|
||||
assert SUMMARY_CRITICAL in messages, \
|
||||
f"Expected critical summary;\ngot:\n{stdout}"
|
||||
|
||||
_assert_precheck_summary_log(log_lines, hard_fail=True, suggested_protocol=None)
|
||||
|
||||
|
||||
class TestPreChecksDiag:
|
||||
"""
|
||||
Verify that `cloudflared tunnel diag` includes prechecks.json in the
|
||||
diagnostic zip archive produced against a live tunnel instance.
|
||||
|
||||
The precheck job in diagnostic.go is gated on noDiagNetwork; we do NOT
|
||||
pass --no-diag-network so prechecks.json must be present. We skip the
|
||||
heavier collectors (logs, metrics, system, runtime) to keep the test fast.
|
||||
|
||||
The diag subcommand writes the zip to its current working directory. We
|
||||
run it with cwd=tmp_path so the archive lands there and is cleaned up
|
||||
automatically by pytest. We resolve config.cloudflared_binary to an
|
||||
absolute path before changing cwd, because the binary path may be relative
|
||||
to the original working directory.
|
||||
"""
|
||||
|
||||
def test_diag_contains_prechecks_json(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config()
|
||||
binary = os.path.abspath(config.cloudflared_binary)
|
||||
|
||||
with start_cloudflared(
|
||||
tmp_path,
|
||||
config,
|
||||
cfd_pre_args=["tunnel", "--ha-connections", "1"],
|
||||
cfd_args=["run"],
|
||||
new_process=True,
|
||||
capture_output=True,
|
||||
) as cfd:
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
||||
|
||||
# Run the diag subcommand as a one-shot process against the
|
||||
# already-running instance. We skip log/metrics/system/runtime
|
||||
# collectors; the network collector (which runs prechecks) is left
|
||||
# enabled.
|
||||
diag_result = subprocess.run(
|
||||
[
|
||||
binary,
|
||||
"tunnel",
|
||||
"diag",
|
||||
"--metrics", f"localhost:{METRICS_PORT}",
|
||||
"--no-diag-logs",
|
||||
"--no-diag-metrics",
|
||||
"--no-diag-system",
|
||||
"--no-diag-runtime",
|
||||
],
|
||||
cwd=str(tmp_path),
|
||||
capture_output=True,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
cfd.terminate()
|
||||
|
||||
diag_stdout = diag_result.stdout.decode(errors="replace")
|
||||
diag_stderr = diag_result.stderr.decode(errors="replace")
|
||||
LOGGER.debug(f"[diag] stdout:\n{diag_stdout}")
|
||||
LOGGER.debug(f"[diag] stderr:\n{diag_stderr}")
|
||||
|
||||
assert diag_result.returncode == 0, (
|
||||
f"cloudflared tunnel diag exited with code {diag_result.returncode}\n"
|
||||
f"stdout:\n{diag_stdout}\nstderr:\n{diag_stderr}"
|
||||
)
|
||||
|
||||
# Locate the zip file written to tmp_path by the diag command.
|
||||
zip_files = list(tmp_path.glob("cloudflared-diag-*.zip"))
|
||||
assert len(zip_files) == 1, \
|
||||
f"Expected exactly one cloudflared-diag-*.zip in {tmp_path}; found {zip_files}"
|
||||
|
||||
zip_path = zip_files[0]
|
||||
with zipfilemod.ZipFile(zip_path) as zf:
|
||||
names = zf.namelist()
|
||||
LOGGER.debug(f"[diag] zip contents: {names}")
|
||||
|
||||
assert "prechecks.json" in names, \
|
||||
f"Expected prechecks.json in diag zip; got: {names}"
|
||||
|
||||
# Must be valid JSON containing at least the RunID field that
|
||||
# prechecks.Run() always sets.
|
||||
with zf.open("prechecks.json") as fh:
|
||||
data = json.load(fh)
|
||||
|
||||
assert "RunID" in data, \
|
||||
f"Expected RunID key in prechecks.json; got keys: {list(data.keys())}"
|
||||
@@ -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)
|
||||
|
||||
+108
-8
@@ -2,6 +2,7 @@ import logging
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
from time import sleep
|
||||
import sys
|
||||
@@ -12,7 +13,65 @@ import requests
|
||||
import yaml
|
||||
from retrying import retry
|
||||
|
||||
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
|
||||
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS, GRACEFUL_SHUTDOWN_TIMEOUT, READER_THREAD_JOIN_TIMEOUT
|
||||
|
||||
class CloudflaredProcess:
|
||||
"""
|
||||
Wrapper around a Popen process that continuously drains stdout and stderr
|
||||
in background threads to prevent OS pipe buffers from filling up and
|
||||
blocking the child process. Captured output is logged when the process
|
||||
is cleaned up.
|
||||
"""
|
||||
|
||||
def __init__(self, cmd, allow_input, capture_output):
|
||||
output = subprocess.PIPE if capture_output else subprocess.DEVNULL
|
||||
stdin = subprocess.PIPE if allow_input else None
|
||||
self.process = subprocess.Popen(cmd, stdin=stdin, stdout=output, stderr=subprocess.STDOUT)
|
||||
|
||||
self._capture_output = capture_output
|
||||
self._stdout_lines = []
|
||||
self._threads = []
|
||||
if capture_output:
|
||||
self._threads.append(self._start_reader(self.process.stdout, self._stdout_lines))
|
||||
|
||||
@staticmethod
|
||||
def _start_reader(pipe, sink):
|
||||
def _drain():
|
||||
for line in pipe:
|
||||
sink.append(line)
|
||||
pipe.close()
|
||||
t = threading.Thread(target=_drain, daemon=True)
|
||||
t.start()
|
||||
return t
|
||||
|
||||
def terminate(self):
|
||||
"""Terminate the process if it is still running."""
|
||||
if self.process.poll() is None:
|
||||
self.process.terminate()
|
||||
|
||||
def cleanup(self):
|
||||
"""Terminate, wait for exit, join reader threads, and log output."""
|
||||
self.terminate()
|
||||
try:
|
||||
self.process.wait(timeout=GRACEFUL_SHUTDOWN_TIMEOUT)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
self.process.wait()
|
||||
for t in self._threads:
|
||||
t.join(timeout=READER_THREAD_JOIN_TIMEOUT)
|
||||
if self._capture_output:
|
||||
stdout = b"".join(self._stdout_lines).decode("utf-8", errors="replace")
|
||||
if stdout:
|
||||
LOGGER.info(f"cloudflared stdout:\n{stdout}")
|
||||
|
||||
@property
|
||||
def stdout_lines(self):
|
||||
return self._stdout_lines
|
||||
|
||||
# Proxy common Popen attributes so callers can still use the wrapper
|
||||
# as if it were a Popen (e.g. send_signal, stdin, pid, returncode).
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.process, name)
|
||||
|
||||
def configure_logger():
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -75,20 +134,15 @@ def cloudflared_cmd(config, config_path, cfd_args, cfd_pre_args, root):
|
||||
LOGGER.info(f"Run cmd {cmd} with config {config}")
|
||||
return cmd
|
||||
|
||||
|
||||
@contextmanager
|
||||
def run_cloudflared_background(cmd, allow_input, capture_output):
|
||||
output = subprocess.PIPE if capture_output else subprocess.DEVNULL
|
||||
stdin = subprocess.PIPE if allow_input else None
|
||||
cfd = None
|
||||
try:
|
||||
cfd = subprocess.Popen(cmd, stdin=stdin, stdout=output, stderr=output)
|
||||
cfd = CloudflaredProcess(cmd, allow_input, capture_output)
|
||||
yield cfd
|
||||
finally:
|
||||
if cfd:
|
||||
cfd.terminate()
|
||||
if capture_output:
|
||||
LOGGER.info(f"cloudflared log: {cfd.stderr.read()}")
|
||||
cfd.cleanup()
|
||||
|
||||
|
||||
def get_quicktunnel_url():
|
||||
@@ -185,3 +239,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
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ type TunnelToken struct {
|
||||
}
|
||||
|
||||
func (t TunnelToken) Credentials() Credentials {
|
||||
// nolint: gosimple
|
||||
// nolint: staticcheck
|
||||
return Credentials{
|
||||
AccountTag: t.AccountTag,
|
||||
TunnelSecret: t.TunnelSecret,
|
||||
@@ -122,6 +122,7 @@ const (
|
||||
|
||||
// ShouldFlush returns whether this kind of connection should actively flush data
|
||||
func (t Type) shouldFlush() bool {
|
||||
// nolint: exhaustive
|
||||
switch t {
|
||||
case TypeWebsocket, TypeTCP, TypeControlStream:
|
||||
return true
|
||||
@@ -131,6 +132,7 @@ func (t Type) shouldFlush() bool {
|
||||
}
|
||||
|
||||
func (t Type) String() string {
|
||||
// nolint: exhaustive
|
||||
switch t {
|
||||
case TypeWebsocket:
|
||||
return "websocket"
|
||||
|
||||
@@ -146,8 +146,8 @@ func wsEchoEndpoint(w ResponseWriter, r *http.Request) error {
|
||||
case <-wsCtx.Done():
|
||||
case <-r.Context().Done():
|
||||
}
|
||||
readPipe.Close()
|
||||
writePipe.Close()
|
||||
_ = readPipe.Close()
|
||||
_ = writePipe.Close()
|
||||
}()
|
||||
|
||||
originConn := &echoPipe{reader: readPipe, writer: writePipe}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+20
-26
@@ -11,6 +11,9 @@ import (
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection/dialopts"
|
||||
cfdquic "github.com/cloudflare/cloudflared/quic"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -26,8 +29,9 @@ func DialQuic(
|
||||
localAddr net.IP,
|
||||
connIndex uint8,
|
||||
logger *zerolog.Logger,
|
||||
) (quic.Connection, error) {
|
||||
udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, edgeAddr, logger)
|
||||
opts dialopts.DialOpts,
|
||||
) (cfdquic.QUICConnection, error) {
|
||||
udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, edgeAddr, opts, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -35,22 +39,15 @@ 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}
|
||||
}
|
||||
|
||||
// wrap the session, so that the UDPConn is closed after session is closed.
|
||||
conn = &wrapCloseableConnQuicConnection{
|
||||
conn,
|
||||
udpConn,
|
||||
}
|
||||
return conn, nil
|
||||
return cfdquic.NewQUICConnection(conn, udpConn)
|
||||
}
|
||||
|
||||
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 +59,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.
|
||||
@@ -87,15 +93,3 @@ func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, edgeIP netip.Add
|
||||
|
||||
return udpConn, err
|
||||
}
|
||||
|
||||
type wrapCloseableConnQuicConnection struct {
|
||||
quic.Connection
|
||||
udpConn *net.UDPConn
|
||||
}
|
||||
|
||||
func (w *wrapCloseableConnQuicConnection) CloseWithError(errorCode quic.ApplicationErrorCode, reason string) error {
|
||||
err := w.Connection.CloseWithError(errorCode, reason)
|
||||
w.udpConn.Close()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ const (
|
||||
|
||||
// quicConnection represents the type that facilitates Proxying via QUIC streams.
|
||||
type quicConnection struct {
|
||||
conn quic.Connection
|
||||
conn cfdquic.QUICConnection
|
||||
logger *zerolog.Logger
|
||||
orchestrator Orchestrator
|
||||
datagramHandler DatagramSessionHandler
|
||||
@@ -54,10 +54,10 @@ type quicConnection struct {
|
||||
gracePeriod time.Duration
|
||||
}
|
||||
|
||||
// NewTunnelConnection takes a [quic.Connection] to wrap it for use with cloudflared application logic.
|
||||
// NewTunnelConnection takes a [cfdquic.QUICConnection] to wrap it for use with cloudflared application logic.
|
||||
func NewTunnelConnection(
|
||||
ctx context.Context,
|
||||
conn quic.Connection,
|
||||
conn cfdquic.QUICConnection,
|
||||
connIndex uint8,
|
||||
orchestrator Orchestrator,
|
||||
datagramSessionHandler DatagramSessionHandler,
|
||||
@@ -143,7 +143,7 @@ func (q *quicConnection) Serve(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// serveControlStream will serve the RPC; blocking until the control plane is done.
|
||||
func (q *quicConnection) serveControlStream(ctx context.Context, controlStream quic.Stream) error {
|
||||
func (q *quicConnection) serveControlStream(ctx context.Context, controlStream *quic.Stream) error {
|
||||
return q.controlStreamHandler.ServeControlStream(ctx, controlStream, q.connOptions.ConnectionOptions(), q.orchestrator)
|
||||
}
|
||||
|
||||
@@ -166,10 +166,10 @@ func (q *quicConnection) acceptStream(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (q *quicConnection) runStream(quicStream quic.Stream) {
|
||||
func (q *quicConnection) runStream(quicStream *quic.Stream) {
|
||||
ctx := quicStream.Context()
|
||||
stream := cfdquic.NewSafeStreamCloser(quicStream, q.streamWriteTimeout, q.logger)
|
||||
defer stream.Close()
|
||||
defer func() { _ = stream.Close() }()
|
||||
|
||||
// we are going to fuse readers/writers from stream <- cloudflared -> origin, and we want to guarantee that
|
||||
// code executed in the code path of handleStream don't trigger an earlier close to the downstream write stream.
|
||||
|
||||
@@ -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,12 +525,12 @@ 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())
|
||||
|
||||
// Establish QUIC connection with edge
|
||||
edgeQUICSessionChan := make(chan quic.Connection)
|
||||
edgeQUICSessionChan := make(chan *quic.Conn)
|
||||
go func() {
|
||||
earlyListener, err := quic.Listen(udpListener, testTLSServerConfig, testQUICConfig)
|
||||
assert.NoError(t, err)
|
||||
@@ -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,25 +671,115 @@ 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))
|
||||
}
|
||||
|
||||
func serveSession(ctx context.Context, datagramConn *datagramV2Connection, edgeQUICSession quic.Connection, closeType closeReason, expectedReason string, t *testing.T) {
|
||||
// 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 cfdquic.QUICConnection, closeType closeReason, expectedReason string, t *testing.T) {
|
||||
payload := []byte(t.Name())
|
||||
sessionID := uuid.New()
|
||||
cfdConn, originConn := net.Pipe()
|
||||
@@ -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)
|
||||
@@ -753,7 +843,7 @@ const (
|
||||
closedByTimeout
|
||||
)
|
||||
|
||||
func runRPCServer(ctx context.Context, session quic.Connection, sessionRPCServer pogs.SessionManager, configRPCServer pogs.ConfigurationManager, t *testing.T) {
|
||||
func runRPCServer(ctx context.Context, session cfdquic.QUICConnection, sessionRPCServer pogs.SessionManager, configRPCServer pogs.ConfigurationManager, t *testing.T) {
|
||||
stream, err := session.AcceptStream(ctx)
|
||||
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)
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
pkgerrors "github.com/pkg/errors"
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/rs/zerolog"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@@ -25,7 +23,6 @@ import (
|
||||
cfdquic "github.com/cloudflare/cloudflared/quic"
|
||||
"github.com/cloudflare/cloudflared/tracing"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
rpcquic "github.com/cloudflare/cloudflared/tunnelrpc/quic"
|
||||
)
|
||||
|
||||
@@ -34,9 +31,7 @@ const (
|
||||
demuxChanCapacity = 16
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidDestinationIP = errors.New("unable to parse destination IP")
|
||||
)
|
||||
var errInvalidDestinationIP = errors.New("unable to parse destination IP")
|
||||
|
||||
// DatagramSessionHandler is a service that can serve datagrams for a connection and handle sessions from incoming
|
||||
// connection streams.
|
||||
@@ -47,7 +42,7 @@ type DatagramSessionHandler interface {
|
||||
}
|
||||
|
||||
type datagramV2Connection struct {
|
||||
conn quic.Connection
|
||||
conn cfdquic.QUICConnection
|
||||
index uint8
|
||||
|
||||
// sessionManager tracks active sessions. It receives datagrams from quic connection via datagramMuxer
|
||||
@@ -69,7 +64,7 @@ type datagramV2Connection struct {
|
||||
}
|
||||
|
||||
func NewDatagramV2Connection(ctx context.Context,
|
||||
conn quic.Connection,
|
||||
conn cfdquic.QUICConnection,
|
||||
originDialer ingress.OriginUDPDialer,
|
||||
icmpRouter ingress.ICMPRouter,
|
||||
index uint8,
|
||||
@@ -116,7 +111,7 @@ func (d *datagramV2Connection) Serve(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// RegisterUdpSession is the RPC method invoked by edge to register and run a session
|
||||
func (q *datagramV2Connection) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration, traceContext string) (*tunnelpogs.RegisterUdpSessionResponse, error) {
|
||||
func (q *datagramV2Connection) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration, traceContext string) (*pogs.RegisterUdpSessionResponse, error) {
|
||||
traceCtx := tracing.NewTracedContext(ctx, traceContext, q.logger)
|
||||
ctx, registerSpan := traceCtx.Tracer().Start(traceCtx, "register-session", trace.WithAttributes(
|
||||
attribute.String("session-id", sessionID.String()),
|
||||
@@ -128,7 +123,7 @@ func (q *datagramV2Connection) RegisterUdpSession(ctx context.Context, sessionID
|
||||
if err := q.flowLimiter.Acquire(management.UDP.String()); err != nil {
|
||||
log.Warn().Msgf("Too many concurrent sessions being handled, rejecting udp proxy to %s:%d", dstIP, dstPort)
|
||||
|
||||
err := pkgerrors.Wrap(err, "failed to start udp session due to rate limiting")
|
||||
err := errors.Wrap(err, "failed to start udp session due to rate limiting")
|
||||
tracing.EndWithErrorStatus(registerSpan, err)
|
||||
return nil, err
|
||||
}
|
||||
@@ -166,7 +161,7 @@ func (q *datagramV2Connection) RegisterUdpSession(ctx context.Context, sessionID
|
||||
|
||||
session, err := q.sessionManager.RegisterSession(ctx, sessionID, originProxy)
|
||||
if err != nil {
|
||||
originProxy.Close()
|
||||
_ = originProxy.Close()
|
||||
log.Err(err).Str(datagramsession.LogFieldSessionID, datagramsession.FormatSessionID(sessionID)).Msgf("Failed to register udp session")
|
||||
tracing.EndWithErrorStatus(registerSpan, err)
|
||||
q.flowLimiter.Release()
|
||||
@@ -185,7 +180,7 @@ func (q *datagramV2Connection) RegisterUdpSession(ctx context.Context, sessionID
|
||||
Msgf("Registered session")
|
||||
tracing.End(registerSpan)
|
||||
|
||||
resp := tunnelpogs.RegisterUdpSessionResponse{
|
||||
resp := pogs.RegisterUdpSessionResponse{
|
||||
Spans: traceCtx.GetProtoSpans(),
|
||||
}
|
||||
|
||||
@@ -229,7 +224,7 @@ func (q *datagramV2Connection) closeUDPSession(ctx context.Context, sessionID uu
|
||||
}
|
||||
|
||||
stream := cfdquic.NewSafeStreamCloser(quicStream, q.streamWriteTimeout, q.logger)
|
||||
defer stream.Close()
|
||||
defer func() { _ = stream.Close() }()
|
||||
rpcClientStream, err := rpcquic.NewSessionClient(ctx, stream, q.rpcTimeout)
|
||||
if err != nil {
|
||||
// Log this at debug because this is not an error if session was closed due to lost connection
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
@@ -16,73 +14,15 @@ import (
|
||||
"github.com/cloudflare/cloudflared/mocks"
|
||||
)
|
||||
|
||||
type mockQuicConnection struct{}
|
||||
|
||||
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, s string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockQuicConnection) Context() context.Context {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockQuicConnection) ConnectionState() quic.ConnectionState {
|
||||
panic("not meant to be called")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func TestRateLimitOnNewDatagramV2UDPSession(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
conn := &mockQuicConnection{}
|
||||
ctrl := gomock.NewController(t)
|
||||
flowLimiterMock := mocks.NewMockLimiter(ctrl)
|
||||
connMock := mocks.NewMockQUICConnection(ctrl)
|
||||
|
||||
datagramConn := NewDatagramV2Connection(
|
||||
t.Context(),
|
||||
conn,
|
||||
connMock,
|
||||
nil,
|
||||
nil,
|
||||
0,
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
"github.com/cloudflare/cloudflared/management"
|
||||
cfdquic "github.com/cloudflare/cloudflared/quic/v3"
|
||||
cfdquic "github.com/cloudflare/cloudflared/quic"
|
||||
v3 "github.com/cloudflare/cloudflared/quic/v3"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
@@ -22,20 +22,20 @@ var (
|
||||
)
|
||||
|
||||
type datagramV3Connection struct {
|
||||
conn quic.Connection
|
||||
conn cfdquic.QUICConnection
|
||||
index uint8
|
||||
// datagramMuxer mux/demux datagrams from quic connection
|
||||
datagramMuxer cfdquic.DatagramConn
|
||||
metrics cfdquic.Metrics
|
||||
datagramMuxer v3.DatagramConn
|
||||
metrics v3.Metrics
|
||||
logger *zerolog.Logger
|
||||
}
|
||||
|
||||
func NewDatagramV3Connection(ctx context.Context,
|
||||
conn quic.Connection,
|
||||
sessionManager cfdquic.SessionManager,
|
||||
conn cfdquic.QUICConnection,
|
||||
sessionManager v3.SessionManager,
|
||||
icmpRouter ingress.ICMPRouter,
|
||||
index uint8,
|
||||
metrics cfdquic.Metrics,
|
||||
metrics v3.Metrics,
|
||||
logger *zerolog.Logger,
|
||||
) DatagramSessionHandler {
|
||||
log := logger.
|
||||
@@ -43,7 +43,7 @@ func NewDatagramV3Connection(ctx context.Context,
|
||||
Int(management.EventTypeKey, int(management.UDP)).
|
||||
Uint8(LogFieldConnIndex, index).
|
||||
Logger()
|
||||
datagramMuxer := cfdquic.NewDatagramConn(conn, sessionManager, icmpRouter, index, metrics, &log)
|
||||
datagramMuxer := v3.NewDatagramConn(conn, sessionManager, icmpRouter, index, metrics, &log)
|
||||
|
||||
return &datagramV3Connection{
|
||||
conn,
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/cloudflare/cloudflared/features"
|
||||
)
|
||||
|
||||
// errUnknownPostQuantumMode is returned by GetCurvePreferences when the
|
||||
// caller passes a features.PostQuantumMode value that is not one of the
|
||||
// documented constants. It is intentionally unexported: callers should treat
|
||||
// any non-nil error as a programming mistake rather than inspecting it.
|
||||
var errUnknownPostQuantumMode = errors.New("the provided post quantum mode is unknown")
|
||||
|
||||
// P256Kyber768Draft00 is a post-quantum KEM based on Kyber768.
|
||||
const P256Kyber768Draft00 = tls.CurveID(0xfe32) // ID 65074
|
||||
|
||||
// Canonical curve lists returned by GetCurvePreferences. They are kept
|
||||
// package-private so that callers cannot accidentally mutate the shared
|
||||
// slice; GetCurvePreferences always returns a clone.
|
||||
var (
|
||||
// postQuantumStrictCurves is used when the caller requires a
|
||||
// post-quantum handshake. Only PQ curves (X25519MLKEM768 and the
|
||||
// deprecated P256Kyber768Draft00 for backward compatibility) are
|
||||
// advertised; no classical-only curve is included.
|
||||
postQuantumStrictCurves = []tls.CurveID{tls.X25519MLKEM768, P256Kyber768Draft00}
|
||||
// postQuantumPreferCurves is used for the default "prefer" mode: the PQ
|
||||
// curve is advertised first and the classical CurveP256 is listed as a
|
||||
// fallback so peers without PQ support can still negotiate.
|
||||
postQuantumPreferCurves = []tls.CurveID{tls.X25519MLKEM768, P256Kyber768Draft00, tls.CurveP256}
|
||||
)
|
||||
|
||||
// getCurvePreferences returns the TLS curve preferences that should be
|
||||
// applied to edge-facing connections for the given post-quantum mode.
|
||||
//
|
||||
// The returned slice is the canonical, protocol-agnostic curve list and is
|
||||
// suitable for direct assignment to tls.Config.CurvePreferences. A fresh
|
||||
// slice is returned on every call, so callers may mutate it freely without
|
||||
// affecting other callers.
|
||||
//
|
||||
// An error is returned only when profile is not a recognised
|
||||
// features.PostQuantumMode value, which indicates a programming bug in the
|
||||
// caller.
|
||||
func getCurvePreferences(profile features.PostQuantumMode) ([]tls.CurveID, error) {
|
||||
switch profile {
|
||||
case features.PostQuantumPrefer:
|
||||
return slices.Clone(postQuantumPreferCurves), nil
|
||||
case features.PostQuantumStrict:
|
||||
return slices.Clone(postQuantumStrictCurves), nil
|
||||
}
|
||||
|
||||
return nil, errUnknownPostQuantumMode
|
||||
}
|
||||
|
||||
// TLSConfigWithCurvePreferences clones the provided tls.Config and applies
|
||||
// curve preferences based on the given post-quantum mode.
|
||||
//
|
||||
// The original tls.Config is never modified; a clone is returned so that
|
||||
// callers can safely use the same base configuration across multiple
|
||||
// goroutines without racing on CurvePreferences.
|
||||
//
|
||||
// Returns an error only when pqMode is not a recognised
|
||||
// features.PostQuantumMode value.
|
||||
func TLSConfigWithCurvePreferences(tlsConfig *tls.Config, pqMode features.PostQuantumMode) (*tls.Config, error) {
|
||||
// Clone the TLS config before applying per-connection curve
|
||||
// preferences. The TlsConfig may be shared across goroutines;
|
||||
// mutating it directly would race with concurrent connection attempts.
|
||||
config := tlsConfig.Clone()
|
||||
curvePref, err := getCurvePreferences(pqMode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get curve preferences: %w", err)
|
||||
}
|
||||
|
||||
config.CurvePreferences = curvePref
|
||||
return config, nil
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"runtime"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cloudflare/cloudflared/features"
|
||||
)
|
||||
|
||||
// TestCurvePreferences verifies that GetCurvePreferences returns the
|
||||
// documented curve list for each supported PostQuantumMode. The expected
|
||||
// values correspond to the contract described in the package documentation
|
||||
// and must be identical under FIPS and non-FIPS builds (see TUN-10413).
|
||||
func TestCurvePreferences(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
expectedCurves []tls.CurveID
|
||||
pqMode features.PostQuantumMode
|
||||
}{
|
||||
{
|
||||
name: "Prefer PQ",
|
||||
pqMode: features.PostQuantumPrefer,
|
||||
expectedCurves: []tls.CurveID{tls.X25519MLKEM768, P256Kyber768Draft00, tls.CurveP256},
|
||||
},
|
||||
{
|
||||
name: "Strict PQ",
|
||||
pqMode: features.PostQuantumStrict,
|
||||
expectedCurves: []tls.CurveID{tls.X25519MLKEM768, P256Kyber768Draft00},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tcase := range tests {
|
||||
t.Run(tcase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
curves, err := getCurvePreferences(tcase.pqMode)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tcase.expectedCurves, curves)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCurvePreferenceUnknownMode asserts that passing a PostQuantumMode
|
||||
// value outside of the documented constants produces an error instead of
|
||||
// silently returning a nil or default curve list. This protects callers
|
||||
// from accidentally negotiating with an unintended curve set.
|
||||
func TestCurvePreferenceUnknownMode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := getCurvePreferences(features.PostQuantumMode(255))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// TestReturnedSliceIsIndependent ensures GetCurvePreferences returns a
|
||||
// fresh slice on every call, so that callers cannot corrupt the
|
||||
// package-level defaults by mutating the result.
|
||||
func TestReturnedSliceIsIndependent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
first, err := getCurvePreferences(features.PostQuantumPrefer)
|
||||
require.NoError(t, err)
|
||||
// Mutate the returned slice.
|
||||
first[0] = tls.CurveP521
|
||||
|
||||
second, err := getCurvePreferences(features.PostQuantumPrefer)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tls.X25519MLKEM768, second[0], "package defaults must not be affected by caller mutation")
|
||||
}
|
||||
|
||||
// runClientServerHandshake drives a TLS 1.3 handshake with the given curve
|
||||
// preferences set on the client and captures the SupportedCurves list
|
||||
// advertised by the client in its ClientHello. The helper is used by
|
||||
// TestSupportedCurvesNegotiation to exercise the curves end-to-end against
|
||||
// the standard library's TLS stack.
|
||||
func runClientServerHandshake(t *testing.T, curves []tls.CurveID) []tls.CurveID {
|
||||
var advertisedCurves []tls.CurveID
|
||||
ts := httptest.NewUnstartedServer(nil)
|
||||
ts.TLS = &tls.Config{ // nolint: gosec
|
||||
GetConfigForClient: func(chi *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
advertisedCurves = slices.Clone(chi.SupportedCurves)
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
ts.StartTLS()
|
||||
defer ts.Close()
|
||||
clientTLSConfig := ts.Client().Transport.(*http.Transport).TLSClientConfig
|
||||
clientTLSConfig.CurvePreferences = curves
|
||||
resp, err := ts.Client().Head(ts.URL)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return nil
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
return advertisedCurves
|
||||
}
|
||||
|
||||
// TestSupportedCurvesNegotiation verifies that the curves returned by
|
||||
// GetCurvePreferences survive a real TLS handshake unchanged, i.e. the
|
||||
// standard library advertises exactly the curves we expect. Currently only
|
||||
// PostQuantumPrefer is exercised because PostQuantumStrict would cause the
|
||||
// handshake to fail against httptest servers that do not support
|
||||
// X25519MLKEM768 server-side.
|
||||
func TestSupportedCurvesNegotiation(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, tcase := range []features.PostQuantumMode{features.PostQuantumPrefer} {
|
||||
curves, err := getCurvePreferences(tcase)
|
||||
require.NoError(t, err)
|
||||
advertisedCurves := runClientServerHandshake(t, curves)
|
||||
require.True(t, slices.Contains(advertisedCurves, tls.CurveP256))
|
||||
require.True(t, slices.Contains(advertisedCurves, tls.X25519MLKEM768))
|
||||
expectedLength := 2
|
||||
if runtime.GOOS == "linux" {
|
||||
// P256Kyber768Draft00 only exists in linux
|
||||
require.True(t, slices.Contains(advertisedCurves, P256Kyber768Draft00))
|
||||
expectedLength = 3
|
||||
}
|
||||
require.Len(t, advertisedCurves, expectedLength)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Package crypto centralizes the cryptographic primitives and TLS
|
||||
// configuration used by cloudflared when establishing connections to the
|
||||
// Cloudflare edge.
|
||||
//
|
||||
// The primary responsibility of the package is to expose a single, canonical
|
||||
// source of TLS curve preferences so that every edge-facing transport (QUIC
|
||||
// and HTTP/2) negotiates the same key-exchange algorithms regardless of the
|
||||
// code path that sets up the connection.
|
||||
//
|
||||
// # Post-Quantum key exchange
|
||||
//
|
||||
// cloudflared supports the X25519MLKEM768 hybrid post-quantum key exchange.
|
||||
// Two operating modes are exposed via the features.PostQuantumMode flag:
|
||||
//
|
||||
// - PostQuantumPrefer: advertise X25519MLKEM768 and the deprecated
|
||||
// P256Kyber768Draft00 first, then fall back to the classical CurveP256
|
||||
// if the peer does not support either PQ curve. This is the default
|
||||
// used for every outbound edge connection.
|
||||
// - PostQuantumStrict: advertise only the PQ curves (X25519MLKEM768 and
|
||||
// P256Kyber768Draft00). Activated by the user via the --post-quantum
|
||||
// CLI flag. No classical fallback is offered, so a peer that does not
|
||||
// support any PQ curve will fail the handshake.
|
||||
//
|
||||
// The resulting curve lists are identical under FIPS and non-FIPS builds,
|
||||
// which is why GetCurvePreferences does not take a FIPS toggle. If that
|
||||
// property ever changes (for example, if a curve stops being FIPS-approved),
|
||||
// the divergence should be expressed inside this package so callers remain
|
||||
// unchanged.
|
||||
//
|
||||
// # Thread-safety
|
||||
//
|
||||
// GetCurvePreferences returns a fresh slice on every call. Callers are free
|
||||
// to mutate the returned slice without affecting the package-level defaults
|
||||
// or other callers.
|
||||
package crypto
|
||||
@@ -34,4 +34,5 @@ const (
|
||||
cliConfigurationBaseName = "cli-configuration.json"
|
||||
configurationBaseName = "configuration.json"
|
||||
taskResultBaseName = "task-result.json"
|
||||
prechecksBaseName = "prechecks.json"
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -16,6 +17,8 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
network "github.com/cloudflare/cloudflared/diagnostic/network"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
"github.com/cloudflare/cloudflared/prechecks"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,6 +35,7 @@ const (
|
||||
networkInformationJobName = "network information"
|
||||
cliConfigurationJobName = "cli configuration"
|
||||
configurationJobName = "configuration"
|
||||
prechecksJobName = "connectivity pre-checks"
|
||||
)
|
||||
|
||||
// Struct used to hold the results of different routines executing the network collection.
|
||||
@@ -92,6 +96,7 @@ type Options struct {
|
||||
Address string
|
||||
ContainerID string
|
||||
PodID string
|
||||
Region string
|
||||
Toggles Toggles
|
||||
}
|
||||
|
||||
@@ -126,13 +131,14 @@ func collectLogs(
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error opening log file while collecting logs: %w", err)
|
||||
}
|
||||
defer logHandle.Close()
|
||||
defer func() { _ = logHandle.Close() }()
|
||||
|
||||
// nolint: gosec
|
||||
outputLogHandle, err := os.Create(filepath.Join(os.TempDir(), logFilename))
|
||||
if err != nil {
|
||||
return "", ErrCreatingTemporaryFile
|
||||
}
|
||||
defer outputLogHandle.Close()
|
||||
defer func() { _ = outputLogHandle.Close() }()
|
||||
|
||||
_, err = io.Copy(outputLogHandle, logHandle)
|
||||
if err != nil {
|
||||
@@ -229,12 +235,13 @@ func networkInformationCollectors() (rawNetworkCollector, jsonNetworkCollector c
|
||||
}
|
||||
|
||||
func rawNetworkInformationWriter(resultMap map[string]networkCollectionResult) (string, error) {
|
||||
// nolint: gosec // Intentionally creating a temporary diagnostic file in the OS temp directory.
|
||||
networkDumpHandle, err := os.Create(filepath.Join(os.TempDir(), rawNetworkBaseName))
|
||||
if err != nil {
|
||||
return "", ErrCreatingTemporaryFile
|
||||
}
|
||||
|
||||
defer networkDumpHandle.Close()
|
||||
defer func() { _ = networkDumpHandle.Close() }()
|
||||
|
||||
var exitErr error
|
||||
|
||||
@@ -260,12 +267,13 @@ func rawNetworkInformationWriter(resultMap map[string]networkCollectionResult) (
|
||||
}
|
||||
|
||||
func jsonNetworkInformationWriter(resultMap map[string]networkCollectionResult) (string, error) {
|
||||
// nolint: gosec
|
||||
networkDumpHandle, err := os.Create(filepath.Join(os.TempDir(), networkBaseName))
|
||||
if err != nil {
|
||||
return "", ErrCreatingTemporaryFile
|
||||
}
|
||||
|
||||
defer networkDumpHandle.Close()
|
||||
defer func() { _ = networkDumpHandle.Close() }()
|
||||
|
||||
encoder := newFormattedEncoder(networkDumpHandle)
|
||||
|
||||
@@ -290,11 +298,12 @@ func jsonNetworkInformationWriter(resultMap map[string]networkCollectionResult)
|
||||
|
||||
func collectFromEndpointAdapter(collect collectToWriterFunc, fileName string) collectFunc {
|
||||
return func(ctx context.Context) (string, error) {
|
||||
// nolint: gosec
|
||||
dumpHandle, err := os.Create(filepath.Join(os.TempDir(), fileName))
|
||||
if err != nil {
|
||||
return "", ErrCreatingTemporaryFile
|
||||
}
|
||||
defer dumpHandle.Close()
|
||||
defer func() { _ = dumpHandle.Close() }()
|
||||
|
||||
err = collect(ctx, dumpHandle)
|
||||
if err != nil {
|
||||
@@ -349,12 +358,12 @@ func resolveInstanceBaseURL(
|
||||
if !strings.HasPrefix(metricsServerAddress, "http://") {
|
||||
metricsServerAddress = "http://" + metricsServerAddress
|
||||
}
|
||||
url, err := url.Parse(metricsServerAddress)
|
||||
baseUrl, err := url.Parse(metricsServerAddress)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("provided address is not valid: %w", err)
|
||||
}
|
||||
|
||||
return url, nil, nil, nil
|
||||
return baseUrl, nil, nil, nil
|
||||
}
|
||||
|
||||
tunnelState, foundTunnelStates, err := FindMetricsServer(log, client, addresses)
|
||||
@@ -368,6 +377,7 @@ func resolveInstanceBaseURL(
|
||||
func createJobs(
|
||||
client *httpClient,
|
||||
tunnel *TunnelState,
|
||||
region string,
|
||||
diagContainer string,
|
||||
diagPod string,
|
||||
noDiagSystem bool,
|
||||
@@ -430,17 +440,62 @@ func createJobs(
|
||||
fn: collectFromEndpointAdapter(client.GetTunnelConfiguration, configurationBaseName),
|
||||
bypass: false,
|
||||
},
|
||||
{
|
||||
jobName: prechecksJobName,
|
||||
fn: collectPrechecks(region),
|
||||
bypass: noDiagNetwork,
|
||||
},
|
||||
}
|
||||
|
||||
return jobs
|
||||
}
|
||||
|
||||
// collectPrechecks runs connectivity pre-checks and writes the results to a JSON file.
|
||||
func collectPrechecks(region string) collectFunc {
|
||||
return func(ctx context.Context) (string, error) {
|
||||
cfg := prechecks.Config{
|
||||
Region: region,
|
||||
IPVersion: allregions.Auto,
|
||||
Timeout: defaultTimeout,
|
||||
}
|
||||
|
||||
// Create a no-op logger since we don't want to spam logs during diagnostic collection
|
||||
log := zerolog.New(io.Discard)
|
||||
|
||||
dialers := prechecks.RunDialers{
|
||||
DNSResolver: &prechecks.EdgeDNSResolver{Log: &log},
|
||||
TCPDialer: &prechecks.EdgeTCPDialer{},
|
||||
QUICDialer: &prechecks.EdgeQUICDialer{},
|
||||
ManagementDialer: &prechecks.NetManagementDialer{Dialer: net.Dialer{}},
|
||||
}
|
||||
|
||||
emptyCert := ""
|
||||
report := prechecks.Run(ctx, emptyCert, cfg, &log, dialers)
|
||||
|
||||
// Write the report to a JSON file
|
||||
// nolint: gosec
|
||||
dumpHandle, err := os.Create(filepath.Join(os.TempDir(), prechecksBaseName))
|
||||
if err != nil {
|
||||
return "", ErrCreatingTemporaryFile
|
||||
}
|
||||
defer func() { _ = dumpHandle.Close() }()
|
||||
|
||||
encoder := newFormattedEncoder(dumpHandle)
|
||||
if err := encoder.Encode(report); err != nil {
|
||||
return dumpHandle.Name(), fmt.Errorf("error encoding prechecks report: %w", err)
|
||||
}
|
||||
|
||||
return dumpHandle.Name(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func createTaskReport(taskReport map[string]taskResult) (string, error) {
|
||||
// nolint: gosec
|
||||
dumpHandle, err := os.Create(filepath.Join(os.TempDir(), taskResultBaseName))
|
||||
if err != nil {
|
||||
return "", ErrCreatingTemporaryFile
|
||||
}
|
||||
defer dumpHandle.Close()
|
||||
defer func() { _ = dumpHandle.Close() }()
|
||||
|
||||
encoder := newFormattedEncoder(dumpHandle)
|
||||
|
||||
@@ -522,6 +577,7 @@ func RunDiagnostic(
|
||||
jobs := createJobs(
|
||||
client,
|
||||
tunnel,
|
||||
options.Region,
|
||||
options.ContainerID,
|
||||
options.PodID,
|
||||
options.Toggles.NoDiagSystem,
|
||||
@@ -545,7 +601,7 @@ func RunDiagnostic(
|
||||
|
||||
defer func() {
|
||||
if !errors.Is(v.Err, ErrCreatingTemporaryFile) {
|
||||
os.Remove(v.path)
|
||||
_ = os.Remove(v.path)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -20,18 +20,18 @@ func NewDockerLogCollector(containerID string) *DockerLogCollector {
|
||||
}
|
||||
|
||||
func (collector *DockerLogCollector) Collect(ctx context.Context) (*LogInformation, error) {
|
||||
tmp := os.TempDir()
|
||||
|
||||
outputHandle, err := os.Create(filepath.Join(tmp, logFilename))
|
||||
// nolint: gosec
|
||||
outputHandle, err := os.Create(filepath.Join(os.TempDir(), logFilename))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening output file: %w", err)
|
||||
}
|
||||
|
||||
defer outputHandle.Close()
|
||||
defer func() { _ = outputHandle.Close() }()
|
||||
|
||||
// Calculate 2 weeks ago
|
||||
since := time.Now().Add(twoWeeksOffset).Format(time.RFC3339)
|
||||
|
||||
// nolint: gosec
|
||||
command := exec.CommandContext(
|
||||
ctx,
|
||||
"docker",
|
||||
|
||||
@@ -13,7 +13,6 @@ const (
|
||||
linuxManagedLogsPath = "/var/log/cloudflared.err"
|
||||
darwinManagedLogsPath = "/Library/Logs/com.cloudflare.cloudflared.err.log"
|
||||
linuxServiceConfigurationPath = "/etc/systemd/system/cloudflared.service"
|
||||
linuxSystemdPath = "/run/systemd/system"
|
||||
)
|
||||
|
||||
type HostLogCollector struct {
|
||||
@@ -27,14 +26,13 @@ func NewHostLogCollector(client HTTPClient) *HostLogCollector {
|
||||
}
|
||||
|
||||
func extractLogsFromJournalCtl(ctx context.Context) (*LogInformation, error) {
|
||||
tmp := os.TempDir()
|
||||
|
||||
outputHandle, err := os.Create(filepath.Join(tmp, logFilename))
|
||||
// nolint: gosec
|
||||
outputHandle, err := os.Create(filepath.Join(os.TempDir(), logFilename))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening output file: %w", err)
|
||||
}
|
||||
|
||||
defer outputHandle.Close()
|
||||
defer func() { _ = outputHandle.Close() }()
|
||||
|
||||
command := exec.CommandContext(
|
||||
ctx,
|
||||
|
||||
@@ -22,18 +22,19 @@ func NewKubernetesLogCollector(containerID, pod string) *KubernetesLogCollector
|
||||
}
|
||||
|
||||
func (collector *KubernetesLogCollector) Collect(ctx context.Context) (*LogInformation, error) {
|
||||
tmp := os.TempDir()
|
||||
outputHandle, err := os.Create(filepath.Join(tmp, logFilename))
|
||||
// nolint: gosec
|
||||
outputHandle, err := os.Create(filepath.Join(os.TempDir(), logFilename))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening output file: %w", err)
|
||||
}
|
||||
|
||||
defer outputHandle.Close()
|
||||
defer func() { _ = outputHandle.Close() }()
|
||||
|
||||
var command *exec.Cmd
|
||||
// Calculate 2 weeks ago
|
||||
since := time.Now().Add(twoWeeksOffset).Format(time.RFC3339)
|
||||
if collector.containerID != "" {
|
||||
// nolint: gosec
|
||||
command = exec.CommandContext(
|
||||
ctx,
|
||||
"kubectl",
|
||||
@@ -47,6 +48,7 @@ func (collector *KubernetesLogCollector) Collect(ctx context.Context) (*LogInfor
|
||||
collector.containerID,
|
||||
)
|
||||
} else {
|
||||
// nolint: gosec
|
||||
command = exec.CommandContext(
|
||||
ctx,
|
||||
"kubectl",
|
||||
|
||||
@@ -67,6 +67,8 @@ func PipeCommandOutputToFile(command *exec.Cmd, outputHandle *os.File) (*LogInfo
|
||||
}
|
||||
|
||||
func CopyFilesFromDirectory(path string) (string, error) {
|
||||
const defaultLogFilename = "cloudflared.log"
|
||||
|
||||
// rolling logs have as suffix the current date thus
|
||||
// when iterating the path files they are already in
|
||||
// chronological order
|
||||
@@ -75,30 +77,32 @@ func CopyFilesFromDirectory(path string) (string, error) {
|
||||
return "", fmt.Errorf("error reading directory %s: %w", path, err)
|
||||
}
|
||||
|
||||
// nolint: gosec
|
||||
outputHandle, err := os.Create(filepath.Join(os.TempDir(), logFilename))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("creating file %s: %w", outputHandle.Name(), err)
|
||||
return "", fmt.Errorf("creating temporary log file %s: %w", logFilename, err)
|
||||
}
|
||||
defer outputHandle.Close()
|
||||
defer func() { _ = outputHandle.Close() }()
|
||||
|
||||
for _, file := range files {
|
||||
// nolint: gosec
|
||||
logHandle, err := os.Open(filepath.Join(path, file.Name()))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error opening file %s:%w", file.Name(), err)
|
||||
return "", fmt.Errorf("error opening file %s: %w", file.Name(), err)
|
||||
}
|
||||
defer logHandle.Close()
|
||||
|
||||
_, err = io.Copy(outputHandle, logHandle)
|
||||
_ = logHandle.Close()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error copying file %s:%w", logHandle.Name(), err)
|
||||
return "", fmt.Errorf("error copying file %s: %w", file.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
logHandle, err := os.Open(filepath.Join(path, "cloudflared.log"))
|
||||
// nolint: gosec
|
||||
logHandle, err := os.Open(filepath.Join(path, defaultLogFilename))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error opening file %s:%w", logHandle.Name(), err)
|
||||
return "", fmt.Errorf("error opening file %s:%w", defaultLogFilename, err)
|
||||
}
|
||||
defer logHandle.Close()
|
||||
defer func() { _ = logHandle.Close() }()
|
||||
|
||||
_, err = io.Copy(outputHandle, logHandle)
|
||||
if err != nil {
|
||||
|
||||
@@ -109,7 +109,7 @@ var friendlyDNSErrorLines = []string{
|
||||
}
|
||||
|
||||
// EdgeDiscovery implements HA service discovery lookup.
|
||||
func edgeDiscovery(log *zerolog.Logger, srvService string) ([][]*EdgeAddr, error) {
|
||||
func EdgeDiscovery(log *zerolog.Logger, srvService string) ([][]*EdgeAddr, error) {
|
||||
logger := log.With().Int(management.EventTypeKey, int(management.Cloudflared)).Logger()
|
||||
logger.Debug().
|
||||
Int(management.EventTypeKey, int(management.Cloudflared)).
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func (ea *EdgeAddr) String() string {
|
||||
@@ -25,8 +26,8 @@ func TestEdgeDiscovery(t *testing.T) {
|
||||
}
|
||||
|
||||
l := zerolog.Nop()
|
||||
addrLists, err := edgeDiscovery(&l, "")
|
||||
assert.NoError(t, err)
|
||||
addrLists, err := EdgeDiscovery(&l, "")
|
||||
require.NoError(t, err)
|
||||
actualAddrSet := map[string]bool{}
|
||||
for _, addrs := range addrLists {
|
||||
for _, addr := range addrs {
|
||||
|
||||
@@ -20,7 +20,7 @@ type Regions struct {
|
||||
|
||||
// ResolveEdge resolves the Cloudflare edge, returning all regions discovered.
|
||||
func ResolveEdge(log *zerolog.Logger, region string, overrideIPVersion ConfigIPVersion) (*Regions, error) {
|
||||
edgeAddrs, err := edgeDiscovery(log, getRegionalServiceName(region))
|
||||
edgeAddrs, err := EdgeDiscovery(log, RegionalServiceName(region))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -91,6 +91,7 @@ func (rs *Regions) GetUnusedAddr(excluding *EdgeAddr, connID int) *EdgeAddr {
|
||||
// evenly across both regions.
|
||||
if rs.region1.AvailableAddrs() == rs.region2.AvailableAddrs() {
|
||||
regions := []Region{rs.region1, rs.region2}
|
||||
//nolint:gosec
|
||||
firstChoice := rand.Intn(2)
|
||||
return getAddrs(excluding, connID, ®ions[firstChoice], ®ions[1-firstChoice])
|
||||
}
|
||||
@@ -131,11 +132,13 @@ func (rs *Regions) GiveBack(addr *EdgeAddr, hasConnectivityError bool) bool {
|
||||
return rs.region2.GiveBack(addr, hasConnectivityError)
|
||||
}
|
||||
|
||||
// Return regionalized service name if `region` isn't empty, otherwise return the global service name for origintunneld
|
||||
func getRegionalServiceName(region string) string {
|
||||
// RegionalServiceName returns the SRV service name for the given region.
|
||||
// When region is empty it returns the global service name ("v2-origintunneld").
|
||||
// Otherwise, it prepends the region, e.g. "us-v2-origintunneld".
|
||||
func RegionalServiceName(region string) string {
|
||||
if region != "" {
|
||||
return region + "-" + srvService // Example: `us-v2-origintunneld`
|
||||
return region + "-" + srvService
|
||||
}
|
||||
|
||||
return srvService // Global service is just `v2-origintunneld`
|
||||
return srvService
|
||||
}
|
||||
|
||||
@@ -237,21 +237,19 @@ func TestNewNoResolveBalancesRegions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRegionalServiceName(t *testing.T) {
|
||||
func TestRegionalServiceName(t *testing.T) {
|
||||
// Empty region should just go to origintunneld
|
||||
globalServiceName := getRegionalServiceName("")
|
||||
assert.Equal(t, srvService, globalServiceName)
|
||||
assert.Equal(t, srvService, RegionalServiceName(""))
|
||||
|
||||
// Non-empty region should go to the regional origintunneld variant
|
||||
for _, region := range []string{"us", "pt", "am"} {
|
||||
regionalServiceName := getRegionalServiceName(region)
|
||||
assert.Equal(t, region+"-"+srvService, regionalServiceName)
|
||||
assert.Equal(t, region+"-"+srvService, RegionalServiceName(region))
|
||||
}
|
||||
}
|
||||
|
||||
func RegionsIsBalanced(t *testing.T, rs *Regions) {
|
||||
delta := rs.region1.AvailableAddrs() - rs.region2.AvailableAddrs()
|
||||
assert.True(t, abs(delta) <= 1)
|
||||
assert.LessOrEqual(t, abs(delta), 1)
|
||||
}
|
||||
|
||||
func abs(x int) int {
|
||||
|
||||
@@ -42,6 +42,10 @@ type FeatureSnapshot struct {
|
||||
// We provide the list of features since we need it to send in the ConnectionOptions during connection
|
||||
// registrations.
|
||||
FeaturesList []string
|
||||
|
||||
// SkipPrechecks indicates when to skip connectivity pre-checks at startup.
|
||||
// Controlled via DNS TXT record to allow remote kill-switch in case of issues.
|
||||
SkipPrechecks bool
|
||||
}
|
||||
|
||||
type PostQuantumMode uint8
|
||||
|
||||
@@ -24,6 +24,7 @@ const (
|
||||
|
||||
type featuresRecord struct {
|
||||
DatagramV3Percentage uint32 `json:"dv3_2"`
|
||||
SkipPrechecks bool `json:"skip_prechecks"`
|
||||
|
||||
// DatagramV3Percentage int32 `json:"dv3"` // Removed in TUN-9291
|
||||
// DatagramV3Percentage uint32 `json:"dv3_1"` // Removed in TUN-9883
|
||||
@@ -89,6 +90,7 @@ func (fs *featureSelector) Snapshot() FeatureSnapshot {
|
||||
PostQuantum: fs.postQuantumMode(),
|
||||
DatagramVersion: fs.datagramVersion(),
|
||||
FeaturesList: fs.clientFeatures(),
|
||||
SkipPrechecks: fs.prechecksSkip(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +123,12 @@ func (fs *featureSelector) datagramVersion() DatagramVersion {
|
||||
return DatagramV2
|
||||
}
|
||||
|
||||
// prechecksSkip returns whether prechecks are enabled via DNS flag.
|
||||
// Defaults to false if not set in the DNS TXT record.
|
||||
func (fs *featureSelector) prechecksSkip() bool {
|
||||
return fs.remoteFeatures.SkipPrechecks
|
||||
}
|
||||
|
||||
// clientFeatures will return the list of currently available features that cloudflared should provide to the edge.
|
||||
func (fs *featureSelector) clientFeatures() []string {
|
||||
// Evaluate any remote features along with static feature list to construct the list of features
|
||||
@@ -186,7 +194,7 @@ func (dr *dnsResolver) lookupRecord(ctx context.Context) ([]byte, error) {
|
||||
}
|
||||
|
||||
if len(records) == 0 {
|
||||
return nil, fmt.Errorf("No TXT record found for %s to determine which features to opt-in", featureSelectorHostname)
|
||||
return nil, fmt.Errorf("no TXT record found for %s to determine which features to opt-in", featureSelectorHostname)
|
||||
}
|
||||
|
||||
return []byte(records[0]), nil
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
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.4
|
||||
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/quic-go/quic-go v0.59.1
|
||||
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/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/proto/otlp v1.2.0
|
||||
go.opentelemetry.io/otel v1.43.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0
|
||||
go.opentelemetry.io/otel/sdk v1.43.0
|
||||
go.opentelemetry.io/otel/trace v1.43.0
|
||||
go.opentelemetry.io/proto/otlp v1.10.0
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
go.uber.org/mock v0.5.1
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/net v0.40.0
|
||||
golang.org/x/sync v0.14.0
|
||||
golang.org/x/sys v0.33.0
|
||||
golang.org/x/term v0.32.0
|
||||
google.golang.org/protobuf v1.36.6
|
||||
go.uber.org/mock v0.5.2
|
||||
golang.org/x/crypto v0.52.0
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/sys v0.45.0
|
||||
golang.org/x/term v0.43.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
nhooyr.io/websocket v1.8.7
|
||||
@@ -50,54 +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/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/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.43.0 // indirect
|
||||
golang.org/x/arch v0.4.0 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/tools v0.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect
|
||||
google.golang.org/grpc v1.72.2 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
google.golang.org/grpc v1.81.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
@@ -108,5 +105,4 @@ replace github.com/prometheus/golang_client => github.com/prometheus/golang_clie
|
||||
|
||||
replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1
|
||||
|
||||
// This fork is based on quic-go v0.45
|
||||
replace github.com/quic-go/quic-go => github.com/chungthuang/quic-go v0.45.1-0.20250428085412-43229ad201fd
|
||||
replace github.com/quic-go/quic-go => github.com/chungthuang/quic-go v0.45.1-0.20260529212404-a9fddf436fc4 // This fork is based on quic-go v0.59.1
|
||||
|
||||
@@ -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=
|
||||
@@ -11,18 +9,16 @@ github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP
|
||||
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
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/chungthuang/quic-go v0.45.1-0.20260529212404-a9fddf436fc4 h1:ZaFGQi6lUEnMyl0DvRy2mEp9u7FP+FrUBr7q+c4U68o=
|
||||
github.com/chungthuang/quic-go v0.45.1-0.20260529212404-a9fddf436fc4/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
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.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -81,8 +79,6 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM=
|
||||
github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
@@ -95,7 +91,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=
|
||||
@@ -107,18 +102,13 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
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=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/grpc-ecosystem/grpc-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/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d h1:PRDnysJ9dF1vUMmEzBu6aHQeUluSQy4eWH3RsSSy/vI=
|
||||
github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@@ -138,16 +128,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=
|
||||
@@ -158,16 +146,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
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 +158,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 +170,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 +190,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,84 +205,89 @@ 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/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 v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.0.0-RC2/go.mod h1:JPQ+z6nNw9mqEGT8o3eoPTdnNI+Aj5JcxEsVGREIAy4=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94=
|
||||
go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
|
||||
go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
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.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
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.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 h1:IkAfh6J/yllPtpYFU0zZN1hUPYdT0ogkBT/9hMxHjvg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
|
||||
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
@@ -43,7 +43,7 @@ func (tc *tcpConnection) Stream(_ context.Context, tunnelConn io.ReadWriter, _ *
|
||||
|
||||
func (tc *tcpConnection) Write(b []byte) (int, error) {
|
||||
if tc.writeTimeout > 0 {
|
||||
if err := tc.Conn.SetWriteDeadline(time.Now().Add(tc.writeTimeout)); err != nil {
|
||||
if err := tc.SetWriteDeadline(time.Now().Add(tc.writeTimeout)); err != nil {
|
||||
tc.logger.Err(err).Msg("Error setting write deadline for TCP connection")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
gorillaWS "github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/proxy"
|
||||
@@ -61,7 +60,7 @@ func TestStreamTCPConnection(t *testing.T) {
|
||||
})
|
||||
errGroup.Go(func() error {
|
||||
echoTCPOrigin(t, originConn)
|
||||
originConn.Close()
|
||||
_ = originConn.Close()
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -88,7 +87,7 @@ func TestDefaultStreamWSOverTCPConnection(t *testing.T) {
|
||||
})
|
||||
errGroup.Go(func() error {
|
||||
echoTCPOrigin(t, originConn)
|
||||
originConn.Close()
|
||||
_ = originConn.Close()
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -117,14 +116,14 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
|
||||
for _, status := range statusCodes {
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte(sendMessage), body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte(sendMessage), body)
|
||||
|
||||
require.Equal(t, echoHeaderIncomingValue, r.Header.Get(echoHeaderName))
|
||||
assert.Equal(t, echoHeaderIncomingValue, r.Header.Get(echoHeaderName))
|
||||
w.Header().Set(echoHeaderName, echoHeaderReturnValue)
|
||||
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(echoMessage))
|
||||
_, _ = w.Write([]byte(echoMessage))
|
||||
}
|
||||
origin := httptest.NewServer(http.HandlerFunc(handler))
|
||||
defer origin.Close()
|
||||
@@ -156,7 +155,7 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
|
||||
errGroup.Go(func() error {
|
||||
wsForwarderInConn, err := wsForwarderListener.Accept()
|
||||
require.NoError(t, err)
|
||||
defer wsForwarderInConn.Close()
|
||||
defer func() { _ = wsForwarderInConn.Close() }()
|
||||
|
||||
stream.Pipe(wsForwarderInConn, &wsEyeball{wsForwarderOutConn}, TestLogger)
|
||||
return nil
|
||||
@@ -171,20 +170,22 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
|
||||
|
||||
// Request URL doesn't matter because the transport is using eyeballDialer to connectq
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://test-socks-stream.com", bytes.NewBuffer([]byte(sendMessage)))
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = req.Body.Close() }()
|
||||
req.Header.Set(echoHeaderName, echoHeaderIncomingValue)
|
||||
|
||||
resp, err := transport.RoundTrip(req)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
assert.Equal(t, status, resp.StatusCode)
|
||||
require.Equal(t, echoHeaderReturnValue, resp.Header.Get(echoHeaderName))
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte(echoMessage), body)
|
||||
|
||||
wsForwarderOutConn.Close()
|
||||
edgeConn.Close()
|
||||
tcpOverWSConn.Close()
|
||||
_ = wsForwarderOutConn.Close()
|
||||
_ = edgeConn.Close()
|
||||
_ = tcpOverWSConn.Close()
|
||||
|
||||
require.NoError(t, errGroup.Wait())
|
||||
}
|
||||
@@ -205,7 +206,7 @@ func TestWsConnReturnsBeforeStreamReturns(t *testing.T) {
|
||||
go func() {
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
// Simulate losing connection to origin
|
||||
originConn.Close()
|
||||
_ = originConn.Close()
|
||||
}()
|
||||
ctx := context.WithValue(r.Context(), websocket.PingPeriodContextKey, time.Microsecond)
|
||||
tcpOverWSConn.Stream(ctx, eyeballConn, TestLogger)
|
||||
@@ -221,11 +222,13 @@ func TestWsConnReturnsBeforeStreamReturns(t *testing.T) {
|
||||
for i := 0; i < 50; i++ {
|
||||
eyeballConn, edgeConn := net.Pipe()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodConnect, server.URL, edgeConn)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = req.Body.Close() }()
|
||||
|
||||
resp, err := client.Transport.RoundTrip(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
errGroup.Go(func() error {
|
||||
for {
|
||||
@@ -261,60 +264,18 @@ func echoWSEyeball(t *testing.T, conn net.Conn) {
|
||||
assert.NoError(t, conn.Close())
|
||||
}()
|
||||
|
||||
if !assert.NoError(t, wsutil.WriteClientBinary(conn, testMessage)) {
|
||||
return
|
||||
}
|
||||
require.NoError(t, wsutil.WriteClientBinary(conn, testMessage))
|
||||
|
||||
readMsg, err := wsutil.ReadServerBinary(conn)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, testResponse, readMsg)
|
||||
}
|
||||
|
||||
func echoWSOrigin(t *testing.T, expectMessages bool) *httptest.Server {
|
||||
var upgrader = gorillaWS.Upgrader{
|
||||
ReadBufferSize: 10,
|
||||
WriteBufferSize: 10,
|
||||
}
|
||||
|
||||
ws := func(w http.ResponseWriter, r *http.Request) {
|
||||
header := make(http.Header)
|
||||
for k, vs := range r.Header {
|
||||
if k == "Test-Cloudflared-Echo" {
|
||||
header[k] = vs
|
||||
}
|
||||
}
|
||||
conn, err := upgrader.Upgrade(w, r, header)
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
sawMessage := false
|
||||
for {
|
||||
messageType, p, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if expectMessages && !sawMessage {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
assert.Equal(t, testMessage, p)
|
||||
sawMessage = true
|
||||
if err := conn.WriteMessage(messageType, testResponse); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewTLSServer starts the server in another thread
|
||||
return httptest.NewTLSServer(http.HandlerFunc(ws))
|
||||
}
|
||||
|
||||
func echoTCPOrigin(t *testing.T, conn net.Conn) {
|
||||
readBuffer := make([]byte, len(testMessage))
|
||||
_, err := conn.Read(readBuffer)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, testMessage, readBuffer)
|
||||
|
||||
|
||||
+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,427 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ../quic/quic_connection.go
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -typed -build_flags=-tags=gomock -package mocks -destination mock_quic_connection.go -source=../quic/quic_connection.go
|
||||
//
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
net "net"
|
||||
reflect "reflect"
|
||||
|
||||
quic "github.com/quic-go/quic-go"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockQUICConnection is a mock of QUICConnection interface.
|
||||
type MockQUICConnection struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockQUICConnectionMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockQUICConnectionMockRecorder is the mock recorder for MockQUICConnection.
|
||||
type MockQUICConnectionMockRecorder struct {
|
||||
mock *MockQUICConnection
|
||||
}
|
||||
|
||||
// NewMockQUICConnection creates a new mock instance.
|
||||
func NewMockQUICConnection(ctrl *gomock.Controller) *MockQUICConnection {
|
||||
mock := &MockQUICConnection{ctrl: ctrl}
|
||||
mock.recorder = &MockQUICConnectionMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockQUICConnection) EXPECT() *MockQUICConnectionMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AcceptStream mocks base method.
|
||||
func (m *MockQUICConnection) AcceptStream(ctx context.Context) (*quic.Stream, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AcceptStream", ctx)
|
||||
ret0, _ := ret[0].(*quic.Stream)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// AcceptStream indicates an expected call of AcceptStream.
|
||||
func (mr *MockQUICConnectionMockRecorder) AcceptStream(ctx any) *MockQUICConnectionAcceptStreamCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcceptStream", reflect.TypeOf((*MockQUICConnection)(nil).AcceptStream), ctx)
|
||||
return &MockQUICConnectionAcceptStreamCall{Call: call}
|
||||
}
|
||||
|
||||
// MockQUICConnectionAcceptStreamCall wrap *gomock.Call
|
||||
type MockQUICConnectionAcceptStreamCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockQUICConnectionAcceptStreamCall) Return(arg0 *quic.Stream, arg1 error) *MockQUICConnectionAcceptStreamCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockQUICConnectionAcceptStreamCall) Do(f func(context.Context) (*quic.Stream, error)) *MockQUICConnectionAcceptStreamCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockQUICConnectionAcceptStreamCall) DoAndReturn(f func(context.Context) (*quic.Stream, error)) *MockQUICConnectionAcceptStreamCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// CloseWithError mocks base method.
|
||||
func (m *MockQUICConnection) CloseWithError(code quic.ApplicationErrorCode, reason string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CloseWithError", code, reason)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CloseWithError indicates an expected call of CloseWithError.
|
||||
func (mr *MockQUICConnectionMockRecorder) CloseWithError(code, reason any) *MockQUICConnectionCloseWithErrorCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseWithError", reflect.TypeOf((*MockQUICConnection)(nil).CloseWithError), code, reason)
|
||||
return &MockQUICConnectionCloseWithErrorCall{Call: call}
|
||||
}
|
||||
|
||||
// MockQUICConnectionCloseWithErrorCall wrap *gomock.Call
|
||||
type MockQUICConnectionCloseWithErrorCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockQUICConnectionCloseWithErrorCall) Return(arg0 error) *MockQUICConnectionCloseWithErrorCall {
|
||||
c.Call = c.Call.Return(arg0)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockQUICConnectionCloseWithErrorCall) Do(f func(quic.ApplicationErrorCode, string) error) *MockQUICConnectionCloseWithErrorCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockQUICConnectionCloseWithErrorCall) DoAndReturn(f func(quic.ApplicationErrorCode, string) error) *MockQUICConnectionCloseWithErrorCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// ConnectionState mocks base method.
|
||||
func (m *MockQUICConnection) ConnectionState() quic.ConnectionState {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ConnectionState")
|
||||
ret0, _ := ret[0].(quic.ConnectionState)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ConnectionState indicates an expected call of ConnectionState.
|
||||
func (mr *MockQUICConnectionMockRecorder) ConnectionState() *MockQUICConnectionConnectionStateCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnectionState", reflect.TypeOf((*MockQUICConnection)(nil).ConnectionState))
|
||||
return &MockQUICConnectionConnectionStateCall{Call: call}
|
||||
}
|
||||
|
||||
// MockQUICConnectionConnectionStateCall wrap *gomock.Call
|
||||
type MockQUICConnectionConnectionStateCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockQUICConnectionConnectionStateCall) Return(arg0 quic.ConnectionState) *MockQUICConnectionConnectionStateCall {
|
||||
c.Call = c.Call.Return(arg0)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockQUICConnectionConnectionStateCall) Do(f func() quic.ConnectionState) *MockQUICConnectionConnectionStateCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockQUICConnectionConnectionStateCall) DoAndReturn(f func() quic.ConnectionState) *MockQUICConnectionConnectionStateCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// Context mocks base method.
|
||||
func (m *MockQUICConnection) Context() context.Context {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Context")
|
||||
ret0, _ := ret[0].(context.Context)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Context indicates an expected call of Context.
|
||||
func (mr *MockQUICConnectionMockRecorder) Context() *MockQUICConnectionContextCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Context", reflect.TypeOf((*MockQUICConnection)(nil).Context))
|
||||
return &MockQUICConnectionContextCall{Call: call}
|
||||
}
|
||||
|
||||
// MockQUICConnectionContextCall wrap *gomock.Call
|
||||
type MockQUICConnectionContextCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockQUICConnectionContextCall) Return(arg0 context.Context) *MockQUICConnectionContextCall {
|
||||
c.Call = c.Call.Return(arg0)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockQUICConnectionContextCall) Do(f func() context.Context) *MockQUICConnectionContextCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockQUICConnectionContextCall) DoAndReturn(f func() context.Context) *MockQUICConnectionContextCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// LocalAddr mocks base method.
|
||||
func (m *MockQUICConnection) LocalAddr() net.Addr {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LocalAddr")
|
||||
ret0, _ := ret[0].(net.Addr)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// LocalAddr indicates an expected call of LocalAddr.
|
||||
func (mr *MockQUICConnectionMockRecorder) LocalAddr() *MockQUICConnectionLocalAddrCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LocalAddr", reflect.TypeOf((*MockQUICConnection)(nil).LocalAddr))
|
||||
return &MockQUICConnectionLocalAddrCall{Call: call}
|
||||
}
|
||||
|
||||
// MockQUICConnectionLocalAddrCall wrap *gomock.Call
|
||||
type MockQUICConnectionLocalAddrCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockQUICConnectionLocalAddrCall) Return(arg0 net.Addr) *MockQUICConnectionLocalAddrCall {
|
||||
c.Call = c.Call.Return(arg0)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockQUICConnectionLocalAddrCall) Do(f func() net.Addr) *MockQUICConnectionLocalAddrCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockQUICConnectionLocalAddrCall) DoAndReturn(f func() net.Addr) *MockQUICConnectionLocalAddrCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// OpenStream mocks base method.
|
||||
func (m *MockQUICConnection) OpenStream() (*quic.Stream, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "OpenStream")
|
||||
ret0, _ := ret[0].(*quic.Stream)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// OpenStream indicates an expected call of OpenStream.
|
||||
func (mr *MockQUICConnectionMockRecorder) OpenStream() *MockQUICConnectionOpenStreamCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenStream", reflect.TypeOf((*MockQUICConnection)(nil).OpenStream))
|
||||
return &MockQUICConnectionOpenStreamCall{Call: call}
|
||||
}
|
||||
|
||||
// MockQUICConnectionOpenStreamCall wrap *gomock.Call
|
||||
type MockQUICConnectionOpenStreamCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockQUICConnectionOpenStreamCall) Return(arg0 *quic.Stream, arg1 error) *MockQUICConnectionOpenStreamCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockQUICConnectionOpenStreamCall) Do(f func() (*quic.Stream, error)) *MockQUICConnectionOpenStreamCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockQUICConnectionOpenStreamCall) DoAndReturn(f func() (*quic.Stream, error)) *MockQUICConnectionOpenStreamCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// OpenStreamSync mocks base method.
|
||||
func (m *MockQUICConnection) OpenStreamSync(ctx context.Context) (*quic.Stream, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "OpenStreamSync", ctx)
|
||||
ret0, _ := ret[0].(*quic.Stream)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// OpenStreamSync indicates an expected call of OpenStreamSync.
|
||||
func (mr *MockQUICConnectionMockRecorder) OpenStreamSync(ctx any) *MockQUICConnectionOpenStreamSyncCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenStreamSync", reflect.TypeOf((*MockQUICConnection)(nil).OpenStreamSync), ctx)
|
||||
return &MockQUICConnectionOpenStreamSyncCall{Call: call}
|
||||
}
|
||||
|
||||
// MockQUICConnectionOpenStreamSyncCall wrap *gomock.Call
|
||||
type MockQUICConnectionOpenStreamSyncCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockQUICConnectionOpenStreamSyncCall) Return(arg0 *quic.Stream, arg1 error) *MockQUICConnectionOpenStreamSyncCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockQUICConnectionOpenStreamSyncCall) Do(f func(context.Context) (*quic.Stream, error)) *MockQUICConnectionOpenStreamSyncCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockQUICConnectionOpenStreamSyncCall) DoAndReturn(f func(context.Context) (*quic.Stream, error)) *MockQUICConnectionOpenStreamSyncCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// ReceiveDatagram mocks base method.
|
||||
func (m *MockQUICConnection) ReceiveDatagram(ctx context.Context) ([]byte, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReceiveDatagram", ctx)
|
||||
ret0, _ := ret[0].([]byte)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReceiveDatagram indicates an expected call of ReceiveDatagram.
|
||||
func (mr *MockQUICConnectionMockRecorder) ReceiveDatagram(ctx any) *MockQUICConnectionReceiveDatagramCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReceiveDatagram", reflect.TypeOf((*MockQUICConnection)(nil).ReceiveDatagram), ctx)
|
||||
return &MockQUICConnectionReceiveDatagramCall{Call: call}
|
||||
}
|
||||
|
||||
// MockQUICConnectionReceiveDatagramCall wrap *gomock.Call
|
||||
type MockQUICConnectionReceiveDatagramCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockQUICConnectionReceiveDatagramCall) Return(arg0 []byte, arg1 error) *MockQUICConnectionReceiveDatagramCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockQUICConnectionReceiveDatagramCall) Do(f func(context.Context) ([]byte, error)) *MockQUICConnectionReceiveDatagramCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockQUICConnectionReceiveDatagramCall) DoAndReturn(f func(context.Context) ([]byte, error)) *MockQUICConnectionReceiveDatagramCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// RemoteAddr mocks base method.
|
||||
func (m *MockQUICConnection) RemoteAddr() net.Addr {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RemoteAddr")
|
||||
ret0, _ := ret[0].(net.Addr)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RemoteAddr indicates an expected call of RemoteAddr.
|
||||
func (mr *MockQUICConnectionMockRecorder) RemoteAddr() *MockQUICConnectionRemoteAddrCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoteAddr", reflect.TypeOf((*MockQUICConnection)(nil).RemoteAddr))
|
||||
return &MockQUICConnectionRemoteAddrCall{Call: call}
|
||||
}
|
||||
|
||||
// MockQUICConnectionRemoteAddrCall wrap *gomock.Call
|
||||
type MockQUICConnectionRemoteAddrCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockQUICConnectionRemoteAddrCall) Return(arg0 net.Addr) *MockQUICConnectionRemoteAddrCall {
|
||||
c.Call = c.Call.Return(arg0)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockQUICConnectionRemoteAddrCall) Do(f func() net.Addr) *MockQUICConnectionRemoteAddrCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockQUICConnectionRemoteAddrCall) DoAndReturn(f func() net.Addr) *MockQUICConnectionRemoteAddrCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// SendDatagram mocks base method.
|
||||
func (m *MockQUICConnection) SendDatagram(payload []byte) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SendDatagram", payload)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SendDatagram indicates an expected call of SendDatagram.
|
||||
func (mr *MockQUICConnectionMockRecorder) SendDatagram(payload any) *MockQUICConnectionSendDatagramCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendDatagram", reflect.TypeOf((*MockQUICConnection)(nil).SendDatagram), payload)
|
||||
return &MockQUICConnectionSendDatagramCall{Call: call}
|
||||
}
|
||||
|
||||
// MockQUICConnectionSendDatagramCall wrap *gomock.Call
|
||||
type MockQUICConnectionSendDatagramCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockQUICConnectionSendDatagramCall) Return(arg0 error) *MockQUICConnectionSendDatagramCall {
|
||||
c.Call = c.Call.Return(arg0)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockQUICConnectionSendDatagramCall) Do(f func([]byte) error) *MockQUICConnectionSendDatagramCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockQUICConnectionSendDatagramCall) DoAndReturn(f func([]byte) error) *MockQUICConnectionSendDatagramCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
// 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"
|
||||
|
||||
quic0 "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"
|
||||
quic "github.com/cloudflare/cloudflared/quic"
|
||||
)
|
||||
|
||||
// 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 *quic0.Config, tlsConfig *tls.Config, addr netip.AddrPort, localAddr net.IP, connIndex uint8, logger *zerolog.Logger, opts dialopts.DialOpts) (quic.QUICConnection, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DialQuic", ctx, quicConfig, tlsConfig, addr, localAddr, connIndex, logger, opts)
|
||||
ret0, _ := ret[0].(quic.QUICConnection)
|
||||
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.QUICConnection, 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, *quic0.Config, *tls.Config, netip.AddrPort, net.IP, uint8, *zerolog.Logger, dialopts.DialOpts) (quic.QUICConnection, error)) *MockQUICDialerDialQuicCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockQUICDialerDialQuicCall) DoAndReturn(f func(context.Context, *quic0.Config, *tls.Config, netip.AddrPort, net.IP, uint8, *zerolog.Logger, dialopts.DialOpts) (quic.QUICConnection, 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,7 @@
|
||||
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"
|
||||
|
||||
//go:generate sh -c "go run go.uber.org/mock/mockgen -typed -build_flags=\"-tags=gomock\" -package mocks -destination mock_quic_connection.go -source=../quic/quic_connection.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,383 @@
|
||||
package prechecks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/backoff"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 10 * time.Second
|
||||
maxRetries = 2
|
||||
retryBaseDelay = 1 * time.Second
|
||||
maxRetryDelay = 16 * time.Second
|
||||
)
|
||||
|
||||
// RunDialers holds the injectable dependencies for Run(). Production callers build
|
||||
// this with real implementations; tests supply mocks.
|
||||
type RunDialers struct {
|
||||
DNSResolver DNSResolver
|
||||
TCPDialer TCPDialer
|
||||
QUICDialer QUICDialer
|
||||
ManagementDialer ManagementDialer
|
||||
}
|
||||
|
||||
// TransportResults holds the per-target results for each transport probe type.
|
||||
// Each slice has one entry per resolved target group, in the same order as the
|
||||
// target labels slice.
|
||||
type TransportResults struct {
|
||||
QUIC []CheckResult // one per target group
|
||||
HTTP2 []CheckResult // one per target group
|
||||
ManagementAPI CheckResult // single target, no groups
|
||||
}
|
||||
|
||||
// Collect returns all results as a slice in a consistent order for reporting:
|
||||
// all QUIC rows first (one per target), then all HTTP2 rows, then Management API.
|
||||
func (tr TransportResults) Collect() []CheckResult {
|
||||
results := make([]CheckResult, 0, len(tr.QUIC)+len(tr.HTTP2)+1)
|
||||
results = append(results, tr.QUIC...)
|
||||
results = append(results, tr.HTTP2...)
|
||||
results = append(results, tr.ManagementAPI)
|
||||
return results
|
||||
}
|
||||
|
||||
// Run executes the following connectivity pre-checks:
|
||||
//
|
||||
// 1. Edge address resolution — either DNS-based SRV discovery (normal path)
|
||||
// or direct resolution of --edge addresses (static path). The static path
|
||||
// skips DNS probe rows entirely since there are no SRV records to validate.
|
||||
// 2. QUIC, HTTP/2, and Management API probes run concurrently against the
|
||||
// resolved addresses.
|
||||
//
|
||||
// Each failed probe is retried up to maxRetries times with exponential backoff.
|
||||
// The suite is bounded by cfg.Timeout (defaultTimeout if zero).
|
||||
func Run(ctx context.Context, caCert string, cfg Config, log *zerolog.Logger, runDialers RunDialers) Report {
|
||||
runID := uuid.New()
|
||||
|
||||
if cfg.Timeout <= 0 {
|
||||
cfg.Timeout = defaultTimeout
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, cfg.Timeout)
|
||||
defer cancel()
|
||||
|
||||
// Build TLS configs once per protocol.
|
||||
quicTLSConfig, quicTLSErr := probeTLSConfig(caCert, connection.QUIC)
|
||||
http2TLSConfig, http2TLSErr := probeTLSConfig(caCert, connection.HTTP2)
|
||||
|
||||
// 1) Resolve edge addresses. Each ResolvedTarget bundles its addr group
|
||||
// with the DNS CheckResult that labels it, keeping the two in sync.
|
||||
var resolvedTargets []ResolvedTarget
|
||||
if len(cfg.EdgeAddrs) > 0 {
|
||||
// Static path: explicit --edge addresses, one ResolvedTarget per addr.
|
||||
resolvedTargets = resolveStaticEdge(cfg.EdgeAddrs, log)
|
||||
} else {
|
||||
// Normal path: SRV-based discovery; DNS rows carry Pass or Fail status.
|
||||
resolvedTargets = runDNSProbe(ctx, runDialers.DNSResolver, cfg.Region)
|
||||
}
|
||||
|
||||
// Extract parallel slices for the transport probe layer.
|
||||
// nolint:prealloc // False positive. The linter is confused by the append used when producing Report.Results
|
||||
dnsResults := make([]CheckResult, len(resolvedTargets))
|
||||
perGroupAddrs := make([][]*allregions.EdgeAddr, len(resolvedTargets))
|
||||
targetLabels := make([]string, len(resolvedTargets))
|
||||
for i, rt := range resolvedTargets {
|
||||
dnsResults[i] = rt.DNSResult
|
||||
perGroupAddrs[i] = rt.Addrs
|
||||
targetLabels[i] = rt.DNSResult.Target
|
||||
}
|
||||
|
||||
// dnsOK is true when at least one target has addresses to probe.
|
||||
dnsOK := slices.ContainsFunc(resolvedTargets, func(r ResolvedTarget) bool {
|
||||
return len(r.Addrs) > 0
|
||||
})
|
||||
|
||||
// 2) Run transport probes concurrently. Each probe type gets its own
|
||||
// buffered channel — one send, one receive, no routing required.
|
||||
var results TransportResults
|
||||
|
||||
mgmtCh := make(chan CheckResult)
|
||||
go func() {
|
||||
mgmtCh <- probeManagementAPIWithRetry(ctx, runDialers.ManagementDialer)
|
||||
}()
|
||||
|
||||
if !dnsOK {
|
||||
// No addresses available: emit one skip row per target so the table
|
||||
// stays consistent with the DNS rows above.
|
||||
results.QUIC = skipResultsForTargets(dnsResults, ProbeTypeQUIC, componentUDPConnectivity)
|
||||
results.HTTP2 = skipResultsForTargets(dnsResults, ProbeTypeHTTP2, componentTCPConnectivity)
|
||||
} else {
|
||||
filteredAddrs := addrsByGroup(perGroupAddrs, cfg.IPVersion)
|
||||
|
||||
quicCh := make(chan []CheckResult, 1)
|
||||
http2Ch := make(chan []CheckResult, 1)
|
||||
|
||||
go func() {
|
||||
if quicTLSErr != nil {
|
||||
log.Warn().Err(quicTLSErr).Msg("Failed to build QUIC probe TLS config")
|
||||
quicCh <- tlsConfigErrResults(ProbeTypeQUIC, componentUDPConnectivity,
|
||||
targetLabels, fmt.Sprintf("%s: %v", detailsTLSConfigFailed, quicTLSErr), actionQUICBlocked)
|
||||
return
|
||||
}
|
||||
quicCh <- probeAllTargets(ctx, ProbeTypeQUIC, componentUDPConnectivity,
|
||||
filteredAddrs, targetLabels,
|
||||
func(addr *allregions.EdgeAddr) CheckResult {
|
||||
return probeQUIC(ctx, quicTLSConfig, runDialers.QUICDialer, addr, log)
|
||||
})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if http2TLSErr != nil {
|
||||
log.Warn().Err(http2TLSErr).Msg("Failed to build HTTP/2 probe TLS config")
|
||||
http2Ch <- tlsConfigErrResults(ProbeTypeHTTP2, componentTCPConnectivity,
|
||||
targetLabels, fmt.Sprintf("%s: %v", detailsTLSConfigFailed, http2TLSErr), actionHTTP2Blocked)
|
||||
return
|
||||
}
|
||||
http2Ch <- probeAllTargets(ctx, ProbeTypeHTTP2, componentTCPConnectivity,
|
||||
filteredAddrs, targetLabels,
|
||||
func(addr *allregions.EdgeAddr) CheckResult {
|
||||
return probeHTTP2(ctx, http2TLSConfig, runDialers.TCPDialer, addr)
|
||||
})
|
||||
}()
|
||||
|
||||
results.QUIC = <-quicCh
|
||||
results.HTTP2 = <-http2Ch
|
||||
}
|
||||
|
||||
results.ManagementAPI = <-mgmtCh
|
||||
|
||||
return Report{
|
||||
RunID: runID,
|
||||
Results: append(dnsResults, results.Collect()...),
|
||||
SuggestedProtocol: suggestProtocol(results.QUIC, results.HTTP2, cfg.ProtocolOverride),
|
||||
}
|
||||
}
|
||||
|
||||
// tlsConfigErrResults returns one Fail CheckResult per target, used when
|
||||
// TLS config construction fails before any dial is attempted.
|
||||
func tlsConfigErrResults(probeType ProbeType, component string, targets []string, details, action string) []CheckResult {
|
||||
results := make([]CheckResult, len(targets))
|
||||
for i, target := range targets {
|
||||
results[i] = CheckResult{
|
||||
Type: probeType,
|
||||
Component: component,
|
||||
Target: target,
|
||||
ProbeStatus: Fail,
|
||||
Details: details,
|
||||
Action: action,
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// probeAllTargets probes each target group sequentially and returns one
|
||||
// CheckResult per group. Within each group, all available addresses (V4 and/or
|
||||
// V6) are tried and the best result is kept.
|
||||
func probeAllTargets(
|
||||
ctx context.Context,
|
||||
probeType ProbeType,
|
||||
component string,
|
||||
perGroupAddrs [][]*allregions.EdgeAddr,
|
||||
targets []string,
|
||||
probeFn func(*allregions.EdgeAddr) CheckResult,
|
||||
) []CheckResult {
|
||||
results := make([]CheckResult, len(perGroupAddrs))
|
||||
for i, addrs := range perGroupAddrs {
|
||||
results[i] = probeTarget(ctx, probeType, component, targets[i], addrs, probeFn)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// probeTarget probes all addresses for a single target group (typically one V4
|
||||
// and/or one V6) and returns the best result. Any address passing means the
|
||||
// target is reachable, so Pass beats Fail within a group.
|
||||
func probeTarget(
|
||||
ctx context.Context,
|
||||
probeType ProbeType,
|
||||
component string,
|
||||
target string,
|
||||
addrs []*allregions.EdgeAddr,
|
||||
probeFn func(*allregions.EdgeAddr) CheckResult,
|
||||
) CheckResult {
|
||||
if len(addrs) == 0 {
|
||||
return CheckResult{
|
||||
Type: probeType,
|
||||
Component: component,
|
||||
Target: target,
|
||||
ProbeStatus: Skip,
|
||||
Details: "No suitable address found for configured IP version",
|
||||
}
|
||||
}
|
||||
|
||||
best := probeWithRetry(ctx, addrs[0], probeFn)
|
||||
for _, addr := range addrs[1:] {
|
||||
if r := probeWithRetry(ctx, addr, probeFn); r.ProbeStatus == Pass {
|
||||
best = r
|
||||
}
|
||||
}
|
||||
best.Target = target
|
||||
return best
|
||||
}
|
||||
|
||||
// probeManagementAPIWithRetry runs the Cloudflare API reachability probe with retry.
|
||||
func probeManagementAPIWithRetry(ctx context.Context, dialer ManagementDialer) CheckResult {
|
||||
var r CheckResult
|
||||
withRetry(ctx, maxRetries, func() bool {
|
||||
r = probeManagementAPI(ctx, dialer)
|
||||
return r.ProbeStatus == Pass
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
// probeWithRetry calls probeFn on addr with exponential-backoff retry up to
|
||||
// maxRetries times, stopping as soon as the probe passes.
|
||||
func probeWithRetry(ctx context.Context, addr *allregions.EdgeAddr, probeFn func(*allregions.EdgeAddr) CheckResult) CheckResult {
|
||||
var r CheckResult
|
||||
withRetry(ctx, maxRetries, func() bool {
|
||||
r = probeFn(addr)
|
||||
return r.ProbeStatus == Pass
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
// addrsByGroup returns the addresses to probe for each resolved target group,
|
||||
// preserving the per-group structure. Each inner slice contains at most one V4
|
||||
// and one V6 address (subject to ipVersion).
|
||||
func addrsByGroup(addrGroups [][]*allregions.EdgeAddr, ipVersion allregions.ConfigIPVersion) [][]*allregions.EdgeAddr {
|
||||
perGroup := make([][]*allregions.EdgeAddr, 0, len(addrGroups))
|
||||
for _, group := range addrGroups {
|
||||
v4, v6 := addrsByFamily(group, ipVersion)
|
||||
var addrs []*allregions.EdgeAddr
|
||||
if v4 != nil {
|
||||
addrs = append(addrs, v4)
|
||||
}
|
||||
if v6 != nil {
|
||||
addrs = append(addrs, v6)
|
||||
}
|
||||
perGroup = append(perGroup, addrs)
|
||||
}
|
||||
return perGroup
|
||||
}
|
||||
|
||||
// skipResultsForTargets returns one skip CheckResult per entry in results,
|
||||
// using each entry's Target label so the transport row aligns with its DNS row.
|
||||
func skipResultsForTargets(targets []CheckResult, probeType ProbeType, component string) []CheckResult {
|
||||
results := make([]CheckResult, len(targets))
|
||||
for i, t := range targets {
|
||||
results[i] = skipResult(probeType, component, t.Target, detailsDNSPrerequisiteFailed)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// worstStatus returns the most severe Status across a slice of CheckResults.
|
||||
// Fail > Pass > Skip. Used to determine whether a transport type as a whole
|
||||
// should be considered failed (any region failing = transport fails).
|
||||
func worstStatus(results []CheckResult) Status {
|
||||
worst := Skip
|
||||
for _, r := range results {
|
||||
if severity(r.ProbeStatus) > severity(worst) {
|
||||
worst = r.ProbeStatus
|
||||
}
|
||||
}
|
||||
return worst
|
||||
}
|
||||
|
||||
// severity maps a Status to a comparable integer so that worse outcomes rank higher.
|
||||
func severity(s Status) int {
|
||||
switch s {
|
||||
case Fail:
|
||||
return 2
|
||||
case Pass:
|
||||
return 1
|
||||
case Skip:
|
||||
return 0
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// parseProtocolOverride converts the raw --protocol flag string into a
|
||||
// *connection.Protocol. It returns nil when the string is empty, "auto", or
|
||||
// unrecognised, so the probe heuristic is used in those cases. "h2mux" is
|
||||
// treated as HTTP/2 because both map to the same transport.
|
||||
func parseProtocolOverride(flag string) *connection.Protocol {
|
||||
switch flag {
|
||||
case connection.QUIC.String():
|
||||
p := connection.QUIC
|
||||
return &p
|
||||
case connection.HTTP2.String(), "h2mux":
|
||||
p := connection.HTTP2
|
||||
return &p
|
||||
default:
|
||||
// "auto", empty, or unknown — no override; let the heuristic decide.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// suggestProtocol determines the protocol to report in the pre-check summary.
|
||||
//
|
||||
// When the caller has explicitly overridden the protocol via --protocol, that
|
||||
// choice is honoured when its transport probes produced evidence and did not
|
||||
// fail.
|
||||
//
|
||||
// When there is no override (auto-selection), precedence is QUIC, HTTP/2,
|
||||
// and nil. A protocol is only suggested if all probes pass.
|
||||
//
|
||||
// Any region failing means the transport is treated as failed (worst wins).
|
||||
func suggestProtocol(quicResults, http2Results []CheckResult, overrideFlag string) *connection.Protocol {
|
||||
if override := parseProtocolOverride(overrideFlag); override != nil {
|
||||
switch *override {
|
||||
case connection.QUIC:
|
||||
// Only report QUIC as the suggested protocol if its probes did not
|
||||
// all fail — if they did, fall through to the heuristic so the
|
||||
// summary can report a usable fallback or nil.
|
||||
if len(quicResults) > 0 && worstStatus(quicResults) != Fail {
|
||||
return new(connection.QUIC)
|
||||
}
|
||||
case connection.HTTP2:
|
||||
// Same logic for an explicit HTTP/2 override.
|
||||
if len(http2Results) > 0 && worstStatus(http2Results) != Fail {
|
||||
return new(connection.HTTP2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,730 @@
|
||||
package prechecks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"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 }
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// newFakeQUICConn creates a mock QUIC connection with CloseWithError
|
||||
// expectation pre-configured so gomock does not fail at runtime.
|
||||
func newFakeQUICConn(ctrl *gomock.Controller) *mocks.MockQUICConnection {
|
||||
conn := mocks.NewMockQUICConnection(ctrl)
|
||||
conn.EXPECT().CloseWithError(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
return conn
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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)
|
||||
fakeQUICConn := newFakeQUICConn(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)
|
||||
|
||||
fakeQUICConn := newFakeQUICConn(ctrl)
|
||||
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)
|
||||
fakeQUICConn := newFakeQUICConn(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)
|
||||
fakeQUICConn := newFakeQUICConn(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)
|
||||
fakeQUICConn := newFakeQUICConn(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)
|
||||
fakeQUICConn := newFakeQUICConn(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)
|
||||
fakeQUICConn := newFakeQUICConn(ctrl)
|
||||
|
||||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrsBothFamilies(), nil)
|
||||
// 2 regions × 2 families = 4 dial calls each for QUIC and HTTP/2.
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil).Times(4)
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(fakeQUICConn, nil).Times(4)
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
// 2 DNS + 2 QUIC + 2 HTTP2 + 1 API = 7 results, all passing.
|
||||
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
|
||||
require.NotNil(t, report.SuggestedProtocol)
|
||||
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
|
||||
}
|
||||
|
||||
// TestRun_IPVersionRestriction verifies that when a single IP family is
|
||||
// configured, only that family is probed (2 regions × 1 addr = 2 dials per
|
||||
// transport) and the excluded family is never dialled.
|
||||
func TestRun_IPVersionRestriction(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ipVersion allregions.ConfigIPVersion
|
||||
}{
|
||||
{"IPv4Only skips V6", allregions.IPv4Only},
|
||||
{"IPv6Only skips V4", allregions.IPv6Only},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
dns := mocks.NewMockDNSResolver(ctrl)
|
||||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||||
fakeQUICConn := newFakeQUICConn(ctrl)
|
||||
|
||||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrsBothFamilies(), nil)
|
||||
// 2 regions × 1 addr per restricted family = 2 dials each.
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil).Times(2)
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(fakeQUICConn, nil).Times(2)
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: tt.ipVersion},
|
||||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRun_EdgeAddrs_SingleAddr verifies that a single --edge addr bypasses DNS
|
||||
// probing. The report contains one DNS Skip row, transport rows labeled with
|
||||
// the raw addr string, and the Management API row.
|
||||
func TestRun_EdgeAddrs_SingleAddr(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||||
fakeQUICConn := newFakeQUICConn(ctrl)
|
||||
|
||||
// DNS resolver must NOT be called when EdgeAddrs is set.
|
||||
dns := mocks.NewMockDNSResolver(ctrl)
|
||||
dns.EXPECT().Resolve(gomock.Any()).Times(0)
|
||||
|
||||
// One addr resolves to one group → one dial per transport.
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil).Times(1)
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(fakeQUICConn, nil).Times(1)
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
cfg := Config{
|
||||
EdgeAddrs: []string{"127.0.0.1:7844"},
|
||||
Timeout: 2 * time.Second,
|
||||
IPVersion: allregions.Auto,
|
||||
}
|
||||
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
|
||||
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
// 1 DNS Skip + 1 QUIC + 1 HTTP2 + 1 API = 4 results.
|
||||
requireStatuses(t, report, Pass, Pass, Pass, Pass)
|
||||
assert.Equal(t, ProbeTypeDNS, report.Results[0].Type, "first row must be DNS skip")
|
||||
assert.Equal(t, "127.0.0.1:7844", report.Results[1].Target, "QUIC target must be the raw --edge addr")
|
||||
assert.Equal(t, "127.0.0.1:7844", report.Results[2].Target, "HTTP2 target must be the raw --edge addr")
|
||||
require.NotNil(t, report.SuggestedProtocol)
|
||||
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
|
||||
}
|
||||
|
||||
// TestRun_EdgeAddrs_MultipleAddrs verifies that multiple --edge addrs produce
|
||||
// one transport row per addr, each labeled with its original addr string.
|
||||
func TestRun_EdgeAddrs_MultipleAddrs(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||||
fakeQUICConn := newFakeQUICConn(ctrl)
|
||||
|
||||
dns := mocks.NewMockDNSResolver(ctrl)
|
||||
dns.EXPECT().Resolve(gomock.Any()).Times(0)
|
||||
|
||||
// Two addrs → two groups → two dials per transport.
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil).Times(2)
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(fakeQUICConn, nil).Times(2)
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
cfg := Config{
|
||||
EdgeAddrs: []string{"127.0.0.1:7844", "127.0.0.2:7844"},
|
||||
Timeout: 2 * time.Second,
|
||||
IPVersion: allregions.Auto,
|
||||
}
|
||||
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
|
||||
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
// 2 DNS Pass (one per addr) + 2 QUIC + 2 HTTP2 + 1 API = 7 results.
|
||||
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
|
||||
assert.Equal(t, ProbeTypeDNS, report.Results[0].Type, "first row must be DNS skip addr1")
|
||||
assert.Equal(t, "127.0.0.1:7844", report.Results[0].Target, "DNS skip addr1 label")
|
||||
assert.Equal(t, ProbeTypeDNS, report.Results[1].Type, "second row must be DNS skip addr2")
|
||||
assert.Equal(t, "127.0.0.2:7844", report.Results[1].Target, "DNS skip addr2 label")
|
||||
assert.Equal(t, "127.0.0.1:7844", report.Results[2].Target, "QUIC addr1")
|
||||
assert.Equal(t, "127.0.0.2:7844", report.Results[3].Target, "QUIC addr2")
|
||||
assert.Equal(t, "127.0.0.1:7844", report.Results[4].Target, "HTTP2 addr1")
|
||||
assert.Equal(t, "127.0.0.2:7844", report.Results[5].Target, "HTTP2 addr2")
|
||||
}
|
||||
|
||||
// TestRun_EdgeAddrs_UnresolvableAddr verifies that when all --edge addrs fail
|
||||
// to resolve, the DNS resolver is not called and transport rows are skipped,
|
||||
// mirroring the DNS skip row.
|
||||
func TestRun_EdgeAddrs_UnresolvableAddr(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||||
|
||||
dns := mocks.NewMockDNSResolver(ctrl)
|
||||
dns.EXPECT().Resolve(gomock.Any()).Times(0)
|
||||
|
||||
// Unresolvable addr → no groups → no transport dials.
|
||||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
|
||||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
cfg := Config{
|
||||
EdgeAddrs: []string{"not-a-valid-addr"},
|
||||
Timeout: 2 * time.Second,
|
||||
IPVersion: allregions.Auto,
|
||||
}
|
||||
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
|
||||
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
// 1 DNS Fail + 1 QUIC Skip + 1 HTTP2 Skip + 1 API = 4 results.
|
||||
requireStatuses(t, report, Fail, Skip, Skip, Pass)
|
||||
assert.Equal(t, ProbeTypeDNS, report.Results[0].Type)
|
||||
assert.Equal(t, "not-a-valid-addr", report.Results[0].Target)
|
||||
assert.Equal(t, ProbeTypeQUIC, report.Results[1].Type)
|
||||
assert.Equal(t, ProbeTypeHTTP2, report.Results[2].Type)
|
||||
assert.Nil(t, report.SuggestedProtocol)
|
||||
assert.True(t, report.hasHardFail())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Protocol override tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TestRun_ProtocolOverride_HTTP2_BothPass verifies that when --protocol http2
|
||||
// is set and both transports are reachable, the summary reports HTTP/2 (not
|
||||
// QUIC, which would otherwise win the heuristic).
|
||||
func TestRun_ProtocolOverride_HTTP2_BothPass(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)
|
||||
fakeQUICConn := newFakeQUICConn(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(fakeQUICConn, nil).AnyTimes()
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
cfg := Config{
|
||||
Timeout: 2 * time.Second,
|
||||
IPVersion: allregions.Auto,
|
||||
ProtocolOverride: "http2",
|
||||
}
|
||||
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
|
||||
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
// Both transports pass, but the override must win — HTTP/2 is reported.
|
||||
require.NotNil(t, report.SuggestedProtocol)
|
||||
assert.Equal(t, connection.HTTP2, *report.SuggestedProtocol,
|
||||
"override http2 should be reported even though QUIC probes also passed")
|
||||
assert.False(t, report.hasHardFail())
|
||||
}
|
||||
|
||||
// TestRun_ProtocolOverride_QUIC_BothPass verifies that when --protocol quic is
|
||||
// set and both transports are reachable, the summary reports QUIC (same as the
|
||||
// heuristic would choose, but driven by the override).
|
||||
func TestRun_ProtocolOverride_QUIC_BothPass(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)
|
||||
fakeQUICConn := newFakeQUICConn(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(fakeQUICConn, nil).AnyTimes()
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
cfg := Config{
|
||||
Timeout: 2 * time.Second,
|
||||
IPVersion: allregions.Auto,
|
||||
ProtocolOverride: "quic",
|
||||
}
|
||||
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
|
||||
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
require.NotNil(t, report.SuggestedProtocol)
|
||||
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
|
||||
assert.False(t, report.hasHardFail())
|
||||
}
|
||||
|
||||
// TestRun_ProtocolOverride_HTTP2_QUICBlocked verifies that when --protocol http2
|
||||
// is set and QUIC is blocked, we still report HTTP/2 (not a fallback to the
|
||||
// heuristic, since the overridden transport is healthy).
|
||||
func TestRun_ProtocolOverride_HTTP2_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("blocked")).AnyTimes()
|
||||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nopConn{}, nil)
|
||||
|
||||
cfg := Config{
|
||||
Timeout: 2 * time.Second,
|
||||
IPVersion: allregions.Auto,
|
||||
ProtocolOverride: "http2",
|
||||
}
|
||||
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
|
||||
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
require.NotNil(t, report.SuggestedProtocol)
|
||||
assert.Equal(t, connection.HTTP2, *report.SuggestedProtocol)
|
||||
assert.False(t, report.hasHardFail())
|
||||
}
|
||||
|
||||
// TestRun_ProtocolOverride_HTTP2_BothBlocked verifies that when --protocol http2
|
||||
// is set but the HTTP/2 transport itself also fails (hard fail), the override
|
||||
// falls through to the heuristic which returns nil — there is no usable protocol.
|
||||
func TestRun_ProtocolOverride_HTTP2_BothBlocked(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)
|
||||
|
||||
cfg := Config{
|
||||
Timeout: 2 * time.Second,
|
||||
IPVersion: allregions.Auto,
|
||||
ProtocolOverride: "http2",
|
||||
}
|
||||
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
|
||||
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||||
|
||||
// The overridden transport (HTTP/2) is blocked, so the override cannot be
|
||||
// honoured and the hard-fail path reports no suggested protocol.
|
||||
assert.Nil(t, report.SuggestedProtocol)
|
||||
assert.True(t, report.hasHardFail())
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
package prechecks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection/dialopts"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
edgedial "github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
cfdquic "github.com/cloudflare/cloudflared/quic"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
)
|
||||
|
||||
const (
|
||||
perProbeDialTimeout = 5 * time.Second
|
||||
|
||||
// Action messages for each probe outcome.
|
||||
actionDNSFail = "Ensure your DNS resolver can resolve '%s'. Run: dig A %s @1.1.1.1. If that fails, contact your network administrator."
|
||||
actionQUICBlocked = "Allow outbound QUIC traffic on port 7844 or use HTTP2."
|
||||
actionHTTP2Blocked = "Allow outbound TCP on port 7844."
|
||||
actionAPIUnreachable = "cloudflared will still run, but automatic software updates are unavailable. " +
|
||||
"Ensure port 443 TCP to api.cloudflare.com is open if you want auto-updates."
|
||||
|
||||
// Component names for CheckResult.
|
||||
componentDNSResolution = "DNS Resolution"
|
||||
componentUDPConnectivity = "UDP Connectivity"
|
||||
componentTCPConnectivity = "TCP Connectivity"
|
||||
componentCloudflareAPI = "Cloudflare API"
|
||||
|
||||
// Target identifiers for CheckResult.
|
||||
targetPortQUIC = "Port 7844 (QUIC)"
|
||||
targetPortHTTP2 = "Port 7844 (HTTP/2)"
|
||||
targetAPI = "api.cloudflare.com:443"
|
||||
noDNSTarget = "No DNS target (Using edge flag)"
|
||||
|
||||
// Details messages for CheckResult.
|
||||
dnsNoAddressesReturned = "No addresses returned"
|
||||
dnsResolvedSuccessfully = "DNS Resolved successfully"
|
||||
detailsQUICHandshakeFailed = "QUIC connection failed"
|
||||
detailsQUICHandshakeSuccessful = "QUIC connection successful"
|
||||
detailsHTTP2BlockedOrUnreachable = "HTTP/2 connection is blocked or unreachable"
|
||||
detailsHTTP2HandshakeSuccessful = "HTTP/2 connection successful"
|
||||
detailsAPIConnectionFailed = "API Connection failed"
|
||||
detailsApiReachable = "API is reachable"
|
||||
detailsDNSPrerequisiteFailed = "DNS prerequisite failed"
|
||||
detailsTLSConfigFailed = "TLS configuration failed"
|
||||
|
||||
// Region hostname templates.
|
||||
region1Global = "region1.v2.argotunnel.com"
|
||||
region2Global = "region2.v2.argotunnel.com"
|
||||
region1US = "us-region1.v2.argotunnel.com"
|
||||
region2US = "us-region2.v2.argotunnel.com"
|
||||
region1Fed = "fed-region1.v2.argotunnel.com"
|
||||
region2Fed = "fed-region2.v2.argotunnel.com"
|
||||
)
|
||||
|
||||
// EdgeDNSResolver implements DNSResolver for the standard DNS-based edge
|
||||
// discovery path.
|
||||
type EdgeDNSResolver struct {
|
||||
Log *zerolog.Logger
|
||||
}
|
||||
|
||||
func (r *EdgeDNSResolver) Resolve(region string) ([][]*allregions.EdgeAddr, error) {
|
||||
return allregions.EdgeDiscovery(r.Log, allregions.RegionalServiceName(region))
|
||||
}
|
||||
|
||||
type EdgeTCPDialer struct{}
|
||||
|
||||
func (d *EdgeTCPDialer) DialEdge(
|
||||
ctx context.Context,
|
||||
timeout time.Duration,
|
||||
tlsConfig *tls.Config,
|
||||
addr *net.TCPAddr,
|
||||
localIP net.IP,
|
||||
) (net.Conn, error) {
|
||||
return edgedial.DialEdge(ctx, timeout, tlsConfig, addr, localIP)
|
||||
}
|
||||
|
||||
type EdgeQUICDialer struct{}
|
||||
|
||||
func (d *EdgeQUICDialer) DialQuic(
|
||||
ctx context.Context,
|
||||
quicConfig *quic.Config,
|
||||
tlsConfig *tls.Config,
|
||||
addr netip.AddrPort,
|
||||
localAddr net.IP,
|
||||
connIndex uint8,
|
||||
logger *zerolog.Logger,
|
||||
opts dialopts.DialOpts,
|
||||
) (cfdquic.QUICConnection, error) {
|
||||
return connection.DialQuic(ctx, quicConfig, tlsConfig, addr, localAddr, connIndex, logger, opts)
|
||||
}
|
||||
|
||||
type NetManagementDialer struct {
|
||||
Dialer net.Dialer
|
||||
}
|
||||
|
||||
func (d *NetManagementDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return d.Dialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
|
||||
// probeTLSConfig builds a *tls.Config for a pre-check probe using the same
|
||||
// certificate pool as the production tunnel. The SNI and NextProtos are taken from
|
||||
// p.ProbeTLSSettings() so that the probe SNI is used instead of the production SNI,
|
||||
// which avoids noisy logs in origintunneld.
|
||||
func probeTLSConfig(caCert string, p connection.Protocol) (*tls.Config, error) {
|
||||
settings := p.ProbeTLSSettings()
|
||||
if settings == nil {
|
||||
return nil, fmt.Errorf("no probe TLS settings for protocol %s", p)
|
||||
}
|
||||
cfg, err := tlsconfig.CreateTunnelConfig(caCert, settings.ServerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(settings.NextProtos) > 0 {
|
||||
cfg.NextProtos = settings.NextProtos
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// probeDNS resolves edge addresses for the given region via the supplied
|
||||
// DNSResolver and returns one ResolvedTarget per discovered region. If
|
||||
// resolution fails entirely, every ResolvedTarget will carry a Fail DNSResult
|
||||
// and nil Addrs.
|
||||
func probeDNS(
|
||||
resolver DNSResolver,
|
||||
region string,
|
||||
) []ResolvedTarget {
|
||||
region1Target, region2Target := regionTargets(region)
|
||||
targets := []string{region1Target, region2Target}
|
||||
|
||||
addrGroups, err := resolver.Resolve(region)
|
||||
if err != nil || len(addrGroups) == 0 {
|
||||
detail := dnsNoAddressesReturned
|
||||
if err != nil {
|
||||
detail = err.Error()
|
||||
}
|
||||
return []ResolvedTarget{
|
||||
{DNSResult: newDNSCheckResult(region1Target, Fail, detail, fmt.Sprintf(actionDNSFail, region1Target, region1Target))},
|
||||
{DNSResult: newDNSCheckResult(region2Target, Fail, detail, fmt.Sprintf(actionDNSFail, region2Target, region2Target))},
|
||||
}
|
||||
}
|
||||
|
||||
resolved := make([]ResolvedTarget, 0, len(addrGroups))
|
||||
for i, target := range targets {
|
||||
if i >= len(addrGroups) {
|
||||
break
|
||||
}
|
||||
group := addrGroups[i]
|
||||
if len(group) == 0 {
|
||||
resolved = append(resolved, ResolvedTarget{
|
||||
DNSResult: newDNSCheckResult(target, Fail, dnsNoAddressesReturned, fmt.Sprintf(actionDNSFail, target, target)),
|
||||
})
|
||||
} else {
|
||||
resolved = append(resolved, ResolvedTarget{
|
||||
Addrs: group,
|
||||
DNSResult: newDNSCheckResult(target, Pass, dnsResolvedSuccessfully, ""),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
// probeQUIC performs a QUIC handshake to a single edge address and returns a
|
||||
// CheckResult. The connection is closed immediately after the handshake – no
|
||||
// streams are opened and no RPC frames are sent – to avoid triggering the OTD
|
||||
// registration timeout (TUN-6732). The probe SNI (probe.cftunnel.com) is used
|
||||
// instead of the production quic.cftunnel.com to prevent OTD log noise.
|
||||
//
|
||||
// A per-probe deadline (perProbeDialTimeout) is applied on top of the parent
|
||||
// context so that a single blocked handshake cannot consume the entire suite
|
||||
// budget.
|
||||
func probeQUIC(
|
||||
ctx context.Context,
|
||||
tlsConfig *tls.Config,
|
||||
dialer QUICDialer,
|
||||
addr *allregions.EdgeAddr,
|
||||
logger *zerolog.Logger,
|
||||
) CheckResult {
|
||||
dialCtx, cancel := context.WithTimeout(ctx, perProbeDialTimeout)
|
||||
defer cancel()
|
||||
|
||||
// We call dialer.DialQuic with isProbe = true, which bypasses connIndex check.
|
||||
// Therefore, whatever we add to connIndex will not be relevant.
|
||||
edgeAddrPort := addr.UDP.AddrPort()
|
||||
conn, err := dialer.DialQuic(
|
||||
dialCtx,
|
||||
&quic.Config{},
|
||||
tlsConfig,
|
||||
edgeAddrPort,
|
||||
nil,
|
||||
math.MaxUint8,
|
||||
logger,
|
||||
dialopts.DialOpts{SkipPortReuse: true},
|
||||
)
|
||||
if err != nil {
|
||||
return CheckResult{
|
||||
Type: ProbeTypeQUIC,
|
||||
Component: componentUDPConnectivity,
|
||||
Target: targetPortQUIC,
|
||||
ProbeStatus: Fail,
|
||||
Details: detailsQUICHandshakeFailed,
|
||||
Action: actionQUICBlocked,
|
||||
}
|
||||
}
|
||||
|
||||
if err := conn.CloseWithError(0, "precheck complete"); err != nil {
|
||||
logger.Debug().Err(err).Msg("Failed to close QUIC connection after successful handshake")
|
||||
}
|
||||
|
||||
return CheckResult{
|
||||
Type: ProbeTypeQUIC,
|
||||
Component: componentUDPConnectivity,
|
||||
Target: targetPortQUIC,
|
||||
ProbeStatus: Pass,
|
||||
Details: detailsQUICHandshakeSuccessful,
|
||||
}
|
||||
}
|
||||
|
||||
// probeHTTP2 performs a TCP + TLS handshake to a single edge address and
|
||||
// returns a CheckResult. The connection is closed immediately after the
|
||||
// handshake – no HTTP/2 frames are sent – to keep the probe minimal. The probe
|
||||
// SNI (probe.cftunnel.com) is used instead of the production h2.cftunnel.com
|
||||
// to prevent OTD log noise.
|
||||
//
|
||||
// The dial timeout is capped at perProbeDialTimeout so that a single blocked
|
||||
// dial cannot exhaust the entire suite budget.
|
||||
func probeHTTP2(ctx context.Context, tlsConfig *tls.Config, dialer TCPDialer, addr *allregions.EdgeAddr) CheckResult {
|
||||
conn, err := dialer.DialEdge(ctx, perProbeDialTimeout, tlsConfig, addr.TCP, nil)
|
||||
if err != nil {
|
||||
return CheckResult{
|
||||
Type: ProbeTypeHTTP2,
|
||||
Component: componentTCPConnectivity,
|
||||
Target: targetPortHTTP2,
|
||||
ProbeStatus: Fail,
|
||||
Details: detailsHTTP2BlockedOrUnreachable,
|
||||
Action: actionHTTP2Blocked,
|
||||
}
|
||||
}
|
||||
_ = conn.Close()
|
||||
|
||||
return CheckResult{
|
||||
Type: ProbeTypeHTTP2,
|
||||
Component: componentTCPConnectivity,
|
||||
Target: targetPortHTTP2,
|
||||
ProbeStatus: Pass,
|
||||
Details: detailsHTTP2HandshakeSuccessful,
|
||||
}
|
||||
}
|
||||
|
||||
// probeManagementAPI tests TCP connectivity to api.cloudflare.com:443. A
|
||||
// successful TCP connection (no TLS handshake required) confirms the port is
|
||||
// reachable. This probe is always a soft failure: the tunnel can run without
|
||||
// it, but automatic software updates will be unavailable.
|
||||
func probeManagementAPI(ctx context.Context, dialer ManagementDialer) CheckResult {
|
||||
dialCtx, cancel := context.WithTimeout(ctx, perProbeDialTimeout)
|
||||
defer cancel()
|
||||
|
||||
conn, err := dialer.DialContext(dialCtx, "tcp", targetAPI)
|
||||
if err != nil {
|
||||
return CheckResult{
|
||||
Type: ProbeTypeManagementAPI,
|
||||
Component: componentCloudflareAPI,
|
||||
Target: targetAPI,
|
||||
ProbeStatus: Fail,
|
||||
Details: detailsAPIConnectionFailed,
|
||||
Action: actionAPIUnreachable,
|
||||
}
|
||||
}
|
||||
_ = conn.Close()
|
||||
|
||||
return CheckResult{
|
||||
Type: ProbeTypeManagementAPI,
|
||||
Component: componentCloudflareAPI,
|
||||
Target: targetAPI,
|
||||
ProbeStatus: Pass,
|
||||
Details: detailsApiReachable,
|
||||
}
|
||||
}
|
||||
|
||||
func skipResult(probeType ProbeType, component, target string, details string) CheckResult {
|
||||
return CheckResult{
|
||||
Type: probeType,
|
||||
Component: component,
|
||||
Target: target,
|
||||
ProbeStatus: Skip,
|
||||
Details: details,
|
||||
}
|
||||
}
|
||||
|
||||
// newDNSCheckResult creates a DNS CheckResult with the given fields.
|
||||
// Type and Component are always ProbeTypeDNS and componentDNSResolution.
|
||||
func newDNSCheckResult(target string, status Status, details, action string) CheckResult {
|
||||
return CheckResult{
|
||||
Type: ProbeTypeDNS,
|
||||
Component: componentDNSResolution,
|
||||
Target: target,
|
||||
ProbeStatus: status,
|
||||
Details: details,
|
||||
Action: action,
|
||||
}
|
||||
}
|
||||
|
||||
// regionTargets returns the human-readable hostnames for region1 and region2
|
||||
// based on the optional region flag value.
|
||||
func regionTargets(region string) (string, string) {
|
||||
switch region {
|
||||
case "us":
|
||||
return region1US, region2US
|
||||
case "fed":
|
||||
return region1Fed, region2Fed
|
||||
default:
|
||||
return region1Global, region2Global
|
||||
}
|
||||
}
|
||||
|
||||
// addrsByFamily extracts one V4 and one V6 address from a resolved CNAME group
|
||||
// using allregions.NewRegion so that the IP-version preference logic matches
|
||||
// production exactly. When cfg.IPVersion restricts to a single family the
|
||||
// excluded family's pointer is nil.
|
||||
func addrsByFamily(group []*allregions.EdgeAddr, ipVersion allregions.ConfigIPVersion) (v4, v6 *allregions.EdgeAddr) {
|
||||
if ipVersion != allregions.IPv6Only {
|
||||
v4 = allregions.NewRegion(group, allregions.IPv4Only).GetAnyAddress()
|
||||
}
|
||||
if ipVersion != allregions.IPv4Only {
|
||||
v6 = allregions.NewRegion(group, allregions.IPv6Only).GetAnyAddress()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// runDNSProbe runs probeDNS with retry and returns []ResolvedTarget.
|
||||
func runDNSProbe(ctx context.Context, resolver DNSResolver, region string) []ResolvedTarget {
|
||||
var targets []ResolvedTarget
|
||||
withRetry(ctx, maxRetries, func() bool {
|
||||
targets = probeDNS(resolver, region)
|
||||
for _, t := range targets {
|
||||
if t.DNSResult.ProbeStatus == Fail {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(targets) > 0
|
||||
})
|
||||
return targets
|
||||
}
|
||||
|
||||
// resolveStaticEdge resolves each --edge addr individually, returning one
|
||||
// ResolvedTarget per addr. Unresolvable addrs produce a Fail ResolvedTarget
|
||||
// with nil Addrs so the report shows which addresses could not be reached.
|
||||
func resolveStaticEdge(addrs []string, log *zerolog.Logger) []ResolvedTarget {
|
||||
targets := make([]ResolvedTarget, 0, len(addrs))
|
||||
for _, addr := range addrs {
|
||||
resolved := allregions.ResolveAddrs([]string{addr}, log)
|
||||
if len(resolved) > 0 {
|
||||
targets = append(targets, ResolvedTarget{
|
||||
Addrs: resolved,
|
||||
DNSResult: newDNSCheckResult(addr, Pass, dnsResolvedSuccessfully, ""),
|
||||
})
|
||||
} else {
|
||||
targets = append(targets, ResolvedTarget{
|
||||
DNSResult: newDNSCheckResult(addr, Fail, dnsNoAddressesReturned, fmt.Sprintf(actionDNSFail, addr, addr)),
|
||||
})
|
||||
}
|
||||
}
|
||||
return targets
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
package prechecks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"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
|
||||
|
||||
// Helper to create test edge addresses.
|
||||
func createTestEdgeAddr(ip string, port int, version allregions.EdgeIPVersion) *allregions.EdgeAddr {
|
||||
parsedIP := net.ParseIP(ip)
|
||||
return &allregions.EdgeAddr{
|
||||
TCP: &net.TCPAddr{IP: parsedIP, Port: port},
|
||||
UDP: &net.UDPAddr{IP: parsedIP, Port: port},
|
||||
IPVersion: version,
|
||||
}
|
||||
}
|
||||
|
||||
// probeDNS tests.
|
||||
|
||||
func TestProbeDNS_Success(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
v4Addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
v6Addr := createTestEdgeAddr("2001:db8::1", testEdgePort, allregions.V6)
|
||||
|
||||
resolver := mocks.NewMockDNSResolver(ctrl)
|
||||
resolver.EXPECT().Resolve("").Return([][]*allregions.EdgeAddr{{v4Addr, v6Addr}}, nil)
|
||||
|
||||
targets := probeDNS(resolver, "")
|
||||
|
||||
require.Len(t, targets, 1)
|
||||
assert.NotEmpty(t, targets[0].Addrs)
|
||||
assert.Equal(t, ProbeTypeDNS, targets[0].DNSResult.Type)
|
||||
assert.Equal(t, testRegion1Global, targets[0].DNSResult.Target)
|
||||
assert.Equal(t, Pass, targets[0].DNSResult.ProbeStatus)
|
||||
assert.Equal(t, dnsResolvedSuccessfully, targets[0].DNSResult.Details)
|
||||
}
|
||||
|
||||
func TestProbeDNS_MultipleRegions(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
v4Addr1 := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
v4Addr2 := createTestEdgeAddr("192.0.2.2", testEdgePort, allregions.V4)
|
||||
|
||||
resolver := mocks.NewMockDNSResolver(ctrl)
|
||||
resolver.EXPECT().Resolve("").Return([][]*allregions.EdgeAddr{{v4Addr1}, {v4Addr2}}, nil)
|
||||
|
||||
targets := probeDNS(resolver, "")
|
||||
|
||||
require.Len(t, targets, 2)
|
||||
|
||||
assert.Equal(t, testRegion1Global, targets[0].DNSResult.Target)
|
||||
assert.Equal(t, Pass, targets[0].DNSResult.ProbeStatus)
|
||||
assert.NotEmpty(t, targets[0].Addrs)
|
||||
|
||||
assert.Equal(t, testRegion2Global, targets[1].DNSResult.Target)
|
||||
assert.Equal(t, Pass, targets[1].DNSResult.ProbeStatus)
|
||||
assert.NotEmpty(t, targets[1].Addrs)
|
||||
}
|
||||
|
||||
func TestProbeDNS_ResolverError(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resolver := mocks.NewMockDNSResolver(ctrl)
|
||||
resolver.EXPECT().Resolve("").Return(nil, errors.New("DNS lookup failed"))
|
||||
|
||||
targets := probeDNS(resolver, "")
|
||||
|
||||
require.Len(t, targets, 2)
|
||||
assert.Empty(t, targets[0].Addrs)
|
||||
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
|
||||
assert.Equal(t, "DNS lookup failed", targets[0].DNSResult.Details)
|
||||
assert.Contains(t, targets[0].DNSResult.Action, testRegion1Global)
|
||||
assert.Empty(t, targets[1].Addrs)
|
||||
assert.Equal(t, Fail, targets[1].DNSResult.ProbeStatus)
|
||||
assert.Contains(t, targets[1].DNSResult.Action, testRegion2Global)
|
||||
}
|
||||
|
||||
func TestProbeDNS_EmptyResults(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resolver := mocks.NewMockDNSResolver(ctrl)
|
||||
resolver.EXPECT().Resolve("").Return([][]*allregions.EdgeAddr{}, nil)
|
||||
|
||||
targets := probeDNS(resolver, "")
|
||||
|
||||
require.Len(t, targets, 2)
|
||||
assert.Empty(t, targets[0].Addrs)
|
||||
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
|
||||
assert.Equal(t, dnsNoAddressesReturned, targets[0].DNSResult.Details)
|
||||
}
|
||||
|
||||
func TestProbeDNS_EmptyGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
resolver := mocks.NewMockDNSResolver(ctrl)
|
||||
resolver.EXPECT().Resolve("").Return([][]*allregions.EdgeAddr{{}}, nil)
|
||||
|
||||
targets := probeDNS(resolver, "")
|
||||
|
||||
require.Len(t, targets, 1)
|
||||
assert.Empty(t, targets[0].Addrs)
|
||||
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
|
||||
assert.Equal(t, dnsNoAddressesReturned, targets[0].DNSResult.Details)
|
||||
}
|
||||
|
||||
func TestProbeDNS_RegionFlag(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
v4Addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
resolver := mocks.NewMockDNSResolver(ctrl)
|
||||
resolver.EXPECT().Resolve("us").Return([][]*allregions.EdgeAddr{{v4Addr}}, nil)
|
||||
|
||||
targets := probeDNS(resolver, "us")
|
||||
|
||||
require.Len(t, targets, 1)
|
||||
assert.Equal(t, testRegion1US, targets[0].DNSResult.Target)
|
||||
}
|
||||
|
||||
// probeQUIC tests.
|
||||
|
||||
func TestProbeQUIC_Success(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
successfulQUICConn := mocks.NewMockQUICConnection(ctrl)
|
||||
successfulQUICConn.EXPECT().CloseWithError(gomock.Any(), gomock.Any()).Return(nil)
|
||||
dialer := mocks.NewMockQUICDialer(ctrl)
|
||||
dialer.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(successfulQUICConn, nil)
|
||||
|
||||
addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
logger := zerolog.New(nil)
|
||||
|
||||
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
|
||||
|
||||
assert.Equal(t, ProbeTypeQUIC, result.Type)
|
||||
assert.Equal(t, Pass, result.ProbeStatus)
|
||||
assert.Equal(t, detailsQUICHandshakeSuccessful, result.Details)
|
||||
}
|
||||
|
||||
func TestProbeQUIC_DialError(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dialer := mocks.NewMockQUICDialer(ctrl)
|
||||
dialer.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("connection refused"))
|
||||
|
||||
addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
logger := zerolog.New(nil)
|
||||
|
||||
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
|
||||
|
||||
assert.Equal(t, ProbeTypeQUIC, result.Type)
|
||||
assert.Equal(t, Fail, result.ProbeStatus)
|
||||
assert.Equal(t, detailsQUICHandshakeFailed, result.Details)
|
||||
assert.Equal(t, actionQUICBlocked, result.Action)
|
||||
}
|
||||
|
||||
func TestProbeQUIC_CloseErrorDoesNotAffectResult(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
// Return a mock whose CloseWithError returns an error — probeQUIC must still
|
||||
// report Pass because the handshake itself succeeded.
|
||||
fakeQUICConn := mocks.NewMockQUICConnection(ctrl)
|
||||
fakeQUICConn.EXPECT().CloseWithError(gomock.Any(), gomock.Any()).Return(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(fakeQUICConn, nil)
|
||||
|
||||
addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
logger := zerolog.New(nil)
|
||||
|
||||
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
|
||||
|
||||
assert.Equal(t, ProbeTypeQUIC, result.Type)
|
||||
assert.Equal(t, Pass, result.ProbeStatus)
|
||||
assert.Equal(t, detailsQUICHandshakeSuccessful, result.Details)
|
||||
}
|
||||
|
||||
func TestProbeQUIC_ContextTimeout(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dialer := mocks.NewMockQUICDialer(ctrl)
|
||||
dialer.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, context.DeadlineExceeded)
|
||||
|
||||
addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
logger := zerolog.New(nil)
|
||||
|
||||
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
|
||||
|
||||
assert.Equal(t, Fail, result.ProbeStatus)
|
||||
assert.Equal(t, detailsQUICHandshakeFailed, result.Details)
|
||||
}
|
||||
|
||||
// probeHTTP2 tests.
|
||||
|
||||
func TestProbeHTTP2_Success(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dialer := mocks.NewMockTCPDialer(ctrl)
|
||||
dialer.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&net.TCPConn{}, nil)
|
||||
|
||||
addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
|
||||
result := probeHTTP2(context.Background(), testTLSConfig, dialer, addr)
|
||||
|
||||
assert.Equal(t, ProbeTypeHTTP2, result.Type)
|
||||
assert.Equal(t, Pass, result.ProbeStatus)
|
||||
assert.Equal(t, detailsHTTP2HandshakeSuccessful, result.Details)
|
||||
}
|
||||
|
||||
func TestProbeHTTP2_DialError(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dialer := mocks.NewMockTCPDialer(ctrl)
|
||||
dialer.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("connection refused"))
|
||||
|
||||
addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
|
||||
result := probeHTTP2(context.Background(), testTLSConfig, dialer, addr)
|
||||
|
||||
assert.Equal(t, ProbeTypeHTTP2, result.Type)
|
||||
assert.Equal(t, Fail, result.ProbeStatus)
|
||||
assert.Equal(t, detailsHTTP2BlockedOrUnreachable, result.Details)
|
||||
assert.Equal(t, actionHTTP2Blocked, result.Action)
|
||||
}
|
||||
|
||||
// probeManagementAPI tests.
|
||||
|
||||
func TestProbeManagementAPI_Success(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dialer := mocks.NewMockManagementDialer(ctrl)
|
||||
dialer.EXPECT().DialContext(gomock.Any(), "tcp", "api.cloudflare.com:443").Return(&net.TCPConn{}, nil)
|
||||
|
||||
result := probeManagementAPI(context.Background(), dialer)
|
||||
|
||||
assert.Equal(t, ProbeTypeManagementAPI, result.Type)
|
||||
assert.Equal(t, "Cloudflare API", result.Component)
|
||||
assert.Equal(t, "api.cloudflare.com:443", result.Target)
|
||||
assert.Equal(t, Pass, result.ProbeStatus)
|
||||
assert.Equal(t, detailsApiReachable, result.Details)
|
||||
}
|
||||
|
||||
func TestProbeManagementAPI_DialError(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dialer := mocks.NewMockManagementDialer(ctrl)
|
||||
dialer.EXPECT().DialContext(gomock.Any(), "tcp", "api.cloudflare.com:443").Return(nil, errors.New("connection refused"))
|
||||
|
||||
result := probeManagementAPI(context.Background(), dialer)
|
||||
|
||||
assert.Equal(t, ProbeTypeManagementAPI, result.Type)
|
||||
assert.Equal(t, Fail, result.ProbeStatus)
|
||||
assert.Equal(t, detailsAPIConnectionFailed, result.Details)
|
||||
assert.Equal(t, actionAPIUnreachable, result.Action)
|
||||
}
|
||||
|
||||
// skipResult tests.
|
||||
|
||||
func TestSkipResult(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := skipResult(ProbeTypeQUIC, "UDP Connectivity", "Port 7844 (QUIC)", detailsDNSPrerequisiteFailed)
|
||||
|
||||
assert.Equal(t, ProbeTypeQUIC, result.Type)
|
||||
assert.Equal(t, "UDP Connectivity", result.Component)
|
||||
assert.Equal(t, "Port 7844 (QUIC)", result.Target)
|
||||
assert.Equal(t, Skip, result.ProbeStatus)
|
||||
assert.Equal(t, detailsDNSPrerequisiteFailed, result.Details)
|
||||
}
|
||||
|
||||
// regionTargets tests.
|
||||
|
||||
func TestRegionTargets(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
region string
|
||||
wantRegion1 string
|
||||
wantRegion2 string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "empty region returns global hostnames",
|
||||
region: "",
|
||||
wantRegion1: testRegion1Global,
|
||||
wantRegion2: testRegion2Global,
|
||||
},
|
||||
{
|
||||
name: "us region returns US hostnames",
|
||||
region: "us",
|
||||
wantRegion1: testRegion1US,
|
||||
wantRegion2: testRegion2US,
|
||||
},
|
||||
{
|
||||
name: "fed region returns fed hostnames",
|
||||
region: "fed",
|
||||
wantRegion1: testRegion1Fed,
|
||||
wantRegion2: testRegion2Fed,
|
||||
},
|
||||
{
|
||||
name: "unknown region defaults to global hostnames",
|
||||
region: "eu",
|
||||
wantRegion1: testRegion1Global,
|
||||
wantRegion2: testRegion2Global,
|
||||
description: "Unknown regions should default to global hostnames",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
gotR1, gotR2 := regionTargets(tt.region)
|
||||
assert.Equal(t, tt.wantRegion1, gotR1)
|
||||
assert.Equal(t, tt.wantRegion2, gotR2)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// addrsByFamily tests.
|
||||
|
||||
func TestAddrsByFamily(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
v4Addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
|
||||
v6Addr := createTestEdgeAddr("2001:db8::1", testEdgePort, allregions.V6)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
group []*allregions.EdgeAddr
|
||||
ipVersion allregions.ConfigIPVersion
|
||||
wantV4 bool
|
||||
wantV6 bool
|
||||
}{
|
||||
{
|
||||
name: "auto returns both v4 and v6",
|
||||
group: []*allregions.EdgeAddr{v4Addr, v6Addr},
|
||||
ipVersion: allregions.Auto,
|
||||
wantV4: true,
|
||||
wantV6: true,
|
||||
},
|
||||
{
|
||||
name: "ipv4 only returns v4 and nil v6",
|
||||
group: []*allregions.EdgeAddr{v4Addr, v6Addr},
|
||||
ipVersion: allregions.IPv4Only,
|
||||
wantV4: true,
|
||||
wantV6: false,
|
||||
},
|
||||
{
|
||||
name: "ipv6 only returns nil v4 and v6",
|
||||
group: []*allregions.EdgeAddr{v4Addr, v6Addr},
|
||||
ipVersion: allregions.IPv6Only,
|
||||
wantV4: false,
|
||||
wantV6: true,
|
||||
},
|
||||
{
|
||||
name: "empty group returns nil for both",
|
||||
group: []*allregions.EdgeAddr{},
|
||||
ipVersion: allregions.Auto,
|
||||
wantV4: false,
|
||||
wantV6: false,
|
||||
},
|
||||
{
|
||||
name: "only v4 available returns v4 and nil v6",
|
||||
group: []*allregions.EdgeAddr{v4Addr},
|
||||
ipVersion: allregions.Auto,
|
||||
wantV4: true,
|
||||
wantV6: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
gotV4, gotV6 := addrsByFamily(tt.group, tt.ipVersion)
|
||||
if tt.wantV4 {
|
||||
assert.NotNil(t, gotV4)
|
||||
} else {
|
||||
assert.Nil(t, gotV4)
|
||||
}
|
||||
if tt.wantV6 {
|
||||
assert.NotNil(t, gotV6)
|
||||
} else {
|
||||
assert.Nil(t, gotV6)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// IPv6 address tests for probeQUIC.
|
||||
|
||||
func TestProbeQUIC_IPv6Address(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
successfulQUICConn := mocks.NewMockQUICConnection(ctrl)
|
||||
successfulQUICConn.EXPECT().CloseWithError(gomock.Any(), gomock.Any()).Return(nil)
|
||||
|
||||
dialer := mocks.NewMockQUICDialer(ctrl)
|
||||
dialer.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(successfulQUICConn, nil)
|
||||
|
||||
addr := createTestEdgeAddr("2001:db8::1", testEdgePort, allregions.V6)
|
||||
logger := zerolog.New(nil)
|
||||
|
||||
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
|
||||
|
||||
assert.Equal(t, Pass, result.ProbeStatus)
|
||||
assert.Equal(t, detailsQUICHandshakeSuccessful, result.Details)
|
||||
}
|
||||
|
||||
// IPv6 address tests for probeHTTP2.
|
||||
|
||||
func TestProbeHTTP2_IPv6Address(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
dialer := mocks.NewMockTCPDialer(ctrl)
|
||||
dialer.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&net.TCPConn{}, nil)
|
||||
|
||||
addr := createTestEdgeAddr("2001:db8::1", testEdgePort, allregions.V6)
|
||||
|
||||
result := probeHTTP2(context.Background(), testTLSConfig, dialer, addr)
|
||||
|
||||
assert.Equal(t, Pass, result.ProbeStatus)
|
||||
}
|
||||
|
||||
// resolveStaticEdge tests.
|
||||
|
||||
// TestResolveStaticEdge_SingleAddr verifies that a single resolvable --edge
|
||||
// addr produces one group labeled with the original addr string.
|
||||
func TestResolveStaticEdge_SingleAddr(t *testing.T) {
|
||||
t.Parallel()
|
||||
logger := zerolog.Nop()
|
||||
targets := resolveStaticEdge([]string{"127.0.0.1:7844"}, &logger)
|
||||
require.Len(t, targets, 1)
|
||||
assert.Equal(t, "127.0.0.1:7844", targets[0].DNSResult.Target)
|
||||
assert.Equal(t, Pass, targets[0].DNSResult.ProbeStatus)
|
||||
assert.NotEmpty(t, targets[0].Addrs)
|
||||
}
|
||||
|
||||
// TestResolveStaticEdge_MultipleAddrs verifies that multiple --edge addrs each
|
||||
// produce their own ResolvedTarget, preserving per-addr structure and label order.
|
||||
func TestResolveStaticEdge_MultipleAddrs(t *testing.T) {
|
||||
t.Parallel()
|
||||
logger := zerolog.Nop()
|
||||
targets := resolveStaticEdge([]string{"127.0.0.1:7844", "127.0.0.2:7844"}, &logger)
|
||||
require.Len(t, targets, 2)
|
||||
assert.Equal(t, "127.0.0.1:7844", targets[0].DNSResult.Target)
|
||||
assert.Equal(t, "127.0.0.2:7844", targets[1].DNSResult.Target)
|
||||
}
|
||||
|
||||
// TestResolveStaticEdge_InvalidAddr verifies that an unresolvable addr is
|
||||
// silently skipped and does not appear in the output.
|
||||
func TestResolveStaticEdge_InvalidAddr(t *testing.T) {
|
||||
t.Parallel()
|
||||
logger := zerolog.Nop()
|
||||
// "not-a-valid-addr" has no port — ResolveTCPAddr will fail.
|
||||
targets := resolveStaticEdge([]string{"not-a-valid-addr"}, &logger)
|
||||
require.Len(t, targets, 1)
|
||||
assert.Equal(t, "not-a-valid-addr", targets[0].DNSResult.Target)
|
||||
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
|
||||
assert.Equal(t, dnsNoAddressesReturned, targets[0].DNSResult.Details)
|
||||
assert.Empty(t, targets[0].Addrs)
|
||||
}
|
||||
|
||||
// TestResolveStaticEdge_PartiallyValid verifies that a mix of valid and invalid
|
||||
// addrs produces one ResolvedTarget per addr — valid ones with Addrs and a Skip
|
||||
// DNSResult, invalid ones with nil Addrs and a Fail DNSResult.
|
||||
func TestResolveStaticEdge_PartiallyValid(t *testing.T) {
|
||||
t.Parallel()
|
||||
logger := zerolog.Nop()
|
||||
targets := resolveStaticEdge([]string{"127.0.0.1:7844", "not-a-valid-addr", "127.0.0.2:7844"}, &logger)
|
||||
require.Len(t, targets, 3)
|
||||
assert.Equal(t, "127.0.0.1:7844", targets[0].DNSResult.Target)
|
||||
assert.Equal(t, Pass, targets[0].DNSResult.ProbeStatus)
|
||||
assert.NotEmpty(t, targets[0].Addrs)
|
||||
assert.Equal(t, "not-a-valid-addr", targets[1].DNSResult.Target)
|
||||
assert.Equal(t, Fail, targets[1].DNSResult.ProbeStatus)
|
||||
assert.Empty(t, targets[1].Addrs)
|
||||
assert.Equal(t, "127.0.0.2:7844", targets[2].DNSResult.Target)
|
||||
assert.Equal(t, Pass, targets[2].DNSResult.ProbeStatus)
|
||||
assert.NotEmpty(t, targets[2].Addrs)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user