mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-08-07 15:24:46 +00:00
Compare commits
97 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8679787525 | |||
| 45c0f22e97 | |||
| e8956c10ac | |||
| 541a608dde | |||
| 6fd720a4a4 | |||
| 60a8c3da17 | |||
| 2601f87b57 | |||
| f70adda11c | |||
| ecb88678f1 | |||
| 43bfec0bcd | |||
| 86fccede6d | |||
| a53d9e5d44 | |||
| 9171757c66 | |||
| c06b2d43e9 | |||
| a9d023b7a2 | |||
| f76f75b449 | |||
| 5c66bd68ab | |||
| dba2d33a6b | |||
| 6b0571b598 | |||
| d5e0c2bb89 | |||
| 77e8965e28 | |||
| 8066821462 | |||
| 1e4ebe5471 | |||
| 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 | |||
| 8aebc38b2f | |||
| b4f675c082 | |||
| a29afd842e | |||
| d9cdd49eec | |||
| 8af01d583c | |||
| 3e6d8ed216 | |||
| 1e907e99b5 | |||
| 18eab5879f | |||
| 0d2a7a0385 | |||
| 789a9b110d | |||
| 31f45fb505 | |||
| 17533b124c | |||
| 9ce16c5aac |
@@ -0,0 +1,122 @@
|
||||
.register_inputs: ®ister_inputs
|
||||
stage: release-internal
|
||||
runOnBranches: "^master$"
|
||||
COMPONENT: "common"
|
||||
|
||||
.register_inputs_stable_trixie: ®ister_inputs_stable_trixie
|
||||
<<: *register_inputs
|
||||
runOnChangesTo: ['RELEASE_NOTES']
|
||||
FLAVOR: "trixie"
|
||||
SERIES: "stable"
|
||||
|
||||
.register_inputs_next_trixie: ®ister_inputs_next_trixie
|
||||
<<: *register_inputs
|
||||
FLAVOR: "trixie"
|
||||
SERIES: next
|
||||
|
||||
################################################
|
||||
### Generate Debian Package for Internal APT ###
|
||||
################################################
|
||||
.cloudflared-apt-build: &cloudflared_apt_build
|
||||
stage: package
|
||||
needs:
|
||||
- ci-image-get-image-ref
|
||||
- linux-packaging # For consistency, we only run this job after we knew we could build the packages for external delivery
|
||||
image: $BUILD_IMAGE
|
||||
cache: {}
|
||||
script:
|
||||
- make cloudflared-deb
|
||||
artifacts:
|
||||
paths:
|
||||
- cloudflared*.deb
|
||||
|
||||
##############
|
||||
### Stable ###
|
||||
##############
|
||||
cloudflared-amd64-stable:
|
||||
<<: *cloudflared_apt_build
|
||||
rules:
|
||||
- !reference [.default-rules, run-on-release]
|
||||
variables: &amd64-stable-vars
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
FIPS: true
|
||||
ORIGINAL_NAME: true
|
||||
CGO_ENABLED: 1
|
||||
|
||||
cloudflared-arm64-stable:
|
||||
<<: *cloudflared_apt_build
|
||||
rules:
|
||||
- !reference [.default-rules, run-on-release]
|
||||
variables: &arm64-stable-vars
|
||||
GOOS: linux
|
||||
GOARCH: arm64
|
||||
FIPS: false # TUN-7595
|
||||
ORIGINAL_NAME: true
|
||||
CGO_ENABLED: 1
|
||||
|
||||
# Jobs names
|
||||
.amd64-stable: &amd64-stable ["cloudflared-amd64-stable"]
|
||||
.arm64-stable: &arm64-stable ["cloudflared-arm64-stable"]
|
||||
|
||||
############
|
||||
### Next ###
|
||||
############
|
||||
cloudflared-amd64-next:
|
||||
<<: *cloudflared_apt_build
|
||||
rules:
|
||||
- !reference [.default-rules, run-on-master]
|
||||
variables:
|
||||
<<: *amd64-stable-vars
|
||||
NIGHTLY: true
|
||||
|
||||
cloudflared-arm64-next:
|
||||
<<: *cloudflared_apt_build
|
||||
rules:
|
||||
- !reference [.default-rules, run-on-master]
|
||||
variables:
|
||||
<<: *arm64-stable-vars
|
||||
NIGHTLY: true
|
||||
|
||||
# Jobs names
|
||||
.amd64-next: &amd64-next ["cloudflared-amd64-next"]
|
||||
.arm64-next: &arm64-next ["cloudflared-arm64-next"]
|
||||
|
||||
include:
|
||||
- local: .ci/commons.gitlab-ci.yml
|
||||
|
||||
##########################################
|
||||
### Publish Packages to Internal Repos ###
|
||||
##########################################
|
||||
|
||||
# Trixie AMD64
|
||||
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
|
||||
inputs:
|
||||
<<: *register_inputs_stable_trixie
|
||||
jobPrefix: cloudflared-trixie-amd64
|
||||
needs: *amd64-stable
|
||||
|
||||
# Trixie ARM64
|
||||
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
|
||||
inputs:
|
||||
<<: *register_inputs_stable_trixie
|
||||
jobPrefix: cloudflared-trixie-arm64
|
||||
needs: *arm64-stable
|
||||
|
||||
##################################################
|
||||
### Publish Nightly Packages to Internal Repos ###
|
||||
##################################################
|
||||
|
||||
# Trixie AMD64
|
||||
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
|
||||
inputs:
|
||||
<<: *register_inputs_next_trixie
|
||||
jobPrefix: cloudflared-nightly-trixie-amd64
|
||||
needs: *amd64-next
|
||||
|
||||
# Trixie ARM64
|
||||
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
|
||||
inputs:
|
||||
<<: *register_inputs_next_trixie
|
||||
jobPrefix: cloudflared-nightly-trixie-arm64
|
||||
needs: *arm64-next
|
||||
@@ -20,21 +20,13 @@
|
||||
- if: $CI_COMMIT_BRANCH != null && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: on_success
|
||||
- when: never
|
||||
|
||||
# This before_script is injected into every job that runs on master meaning that if there is no tag the step
|
||||
# will succeed but only write "No tag present - Skipping" to the console.
|
||||
.check-tag:
|
||||
before_script:
|
||||
- |
|
||||
# Check if there is a Git tag pointing to HEAD
|
||||
echo "Tag found: $(git tag --points-at HEAD | grep .)"
|
||||
if git tag --points-at HEAD | grep .; then
|
||||
echo "Tag found: $(git tag --points-at HEAD | grep .)"
|
||||
export "VERSION=$(git tag --points-at HEAD | grep .)"
|
||||
else
|
||||
echo "No tag present — skipping."
|
||||
exit 0
|
||||
fi
|
||||
# Rules to run the job only when a release happens
|
||||
run-on-release:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
changes:
|
||||
- 'RELEASE_NOTES'
|
||||
when: on_success
|
||||
- when: never
|
||||
|
||||
.component-tests:
|
||||
image: $BUILD_IMAGE
|
||||
|
||||
+34
-27
@@ -1,32 +1,39 @@
|
||||
ARG CLOUDFLARE_DOCKER_REGISTRY_HOST
|
||||
|
||||
FROM ${CLOUDFLARE_DOCKER_REGISTRY_HOST:-registry.cfdata.org}/stash/cf/debian-images/bookworm/main:2025.7.0@sha256:6350da2f7e728dae2c1420f6dafc38e23cacc0b399d3d5b2f40fe48d9c8ff1ca
|
||||
|
||||
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.9-1 \
|
||||
libffi-dev \
|
||||
procps \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
python3-setuptools \
|
||||
python3-venv \
|
||||
# libmsi and libgcab are libraries the wixl binary depends on.
|
||||
libmsi-dev \
|
||||
libgcab-dev \
|
||||
# deb and rpm build tools
|
||||
rubygem-fpm \
|
||||
rpm \
|
||||
# create deb and rpm repository files
|
||||
reprepro \
|
||||
createrepo-c && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
# Install wixl
|
||||
curl -o /usr/local/bin/wixl -L https://pkg.cloudflare.com/binaries/wixl && \
|
||||
chmod a+x /usr/local/bin/wixl && \
|
||||
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: "3371-f5539bd6f83d@sha256:a2a68f580070f9411d0d3155959ed63b700ef319b5fcc62db340e92227bbc628"
|
||||
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
|
||||
|
||||
@@ -16,15 +16,18 @@ include:
|
||||
- release-cloudflared-to-r2
|
||||
commentImageRefs: false
|
||||
runner: vm-linux-x86-4cpu-8gb
|
||||
DOCKER_USER_BRANCH: svcgithubdockerhubcloudflar045
|
||||
DOCKER_PASSWORD_BRANCH: gitlab/cloudflare/tun/cloudflared/_dev/dockerhub/svc_password/data
|
||||
# Based on if the CI reference is protected or not the CI component will
|
||||
# either use _BRANCH or _PROD, therefore, to prevent the pipelines from failing
|
||||
# we simply set both to the same value.
|
||||
DOCKER_USER_BRANCH: &docker-hub-user svcgithubdockerhubcloudflar045
|
||||
DOCKER_PASSWORD_BRANCH: &docker-hub-password gitlab/cloudflare/tun/cloudflared/_dev/dockerhub/svc_password/data
|
||||
DOCKER_USER_PROD: *docker-hub-user
|
||||
DOCKER_PASSWORD_PROD: *docker-hub-password
|
||||
EXTRA_DIB_ARGS: --overwrite
|
||||
|
||||
.default-release-job: &release-job-defaults
|
||||
stage: release
|
||||
image: $BUILD_IMAGE
|
||||
rules:
|
||||
- !reference [.default-rules, run-on-master]
|
||||
cache:
|
||||
paths:
|
||||
- .cache/pip
|
||||
@@ -71,7 +74,8 @@ include:
|
||||
###########################################
|
||||
release-cloudflared-to-github:
|
||||
<<: *release-job-defaults
|
||||
extends: .check-tag
|
||||
rules:
|
||||
- !reference [.default-rules, run-on-release]
|
||||
needs:
|
||||
- ci-image-get-image-ref
|
||||
- linux-packaging
|
||||
@@ -86,7 +90,8 @@ release-cloudflared-to-github:
|
||||
#########################################
|
||||
release-cloudflared-to-r2:
|
||||
<<: *release-job-defaults
|
||||
extends: .check-tag
|
||||
rules:
|
||||
- !reference [.default-rules, run-on-release]
|
||||
needs:
|
||||
- ci-image-get-image-ref
|
||||
- linux-packaging # We only release non-FIPS binaries to R2
|
||||
@@ -99,6 +104,8 @@ release-cloudflared-to-r2:
|
||||
#################################################
|
||||
release-cloudflared-nightly-to-r2:
|
||||
<<: *release-job-defaults
|
||||
rules:
|
||||
- !reference [.default-rules, run-on-master]
|
||||
variables:
|
||||
<<: *release-job-variables
|
||||
R2_BUCKET: cloudflared-pkgs-next
|
||||
@@ -115,6 +122,8 @@ release-cloudflared-nightly-to-r2:
|
||||
#############################
|
||||
generate-version-file:
|
||||
<<: *release-job-defaults
|
||||
rules:
|
||||
- !reference [.default-rules, run-on-release]
|
||||
needs:
|
||||
- ci-image-get-image-ref
|
||||
script:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -e -o pipefail
|
||||
set -e -u -o pipefail
|
||||
|
||||
# Fetch cloudflared from the artifacts folder
|
||||
mv ./artifacts/cloudflared ./cloudflared
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -e -o pipefail
|
||||
set -e -u -o pipefail
|
||||
|
||||
OUTPUT=$(go run -mod=readonly golang.org/x/tools/cmd/goimports@v0.30.0 -l -d -local github.com/cloudflare/cloudflared $(go list -mod=vendor -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc))
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -e -o pipefail
|
||||
set -e -u -o pipefail
|
||||
|
||||
BRANCH="master"
|
||||
TMP_PATH="$PWD/tmp"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -e -u -o pipefail
|
||||
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
||||
echo $VERSION
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -e -u -o pipefail
|
||||
|
||||
# Check if architecture argument is provided
|
||||
if [ $# -eq 0 ]; then
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -e -u -o pipefail
|
||||
|
||||
python3 -m venv env
|
||||
. env/bin/activate
|
||||
pip install pynacl==1.4.0 pygithub==1.55
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -e -o pipefail
|
||||
set -e -u -o pipefail
|
||||
|
||||
# Check if a make target is provided as an argument
|
||||
if [ $# -eq 0 ]; then
|
||||
@@ -14,5 +14,5 @@ python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# Our release scripts are written in python, so we should install their dependecies here.
|
||||
pip install pynacl==1.4.0 pygithub==1.55 boto3==1.22.9 python-gnupg==0.4.9
|
||||
pip install pynacl==1.4.0 pygithub==1.55 boto3==1.42.30 python-gnupg==0.4.9
|
||||
make $MAKE_TARGET
|
||||
|
||||
+17
-16
@@ -1,16 +1,17 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
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/*
|
||||
@@ -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
|
||||
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 "========================================"
|
||||
@@ -1,24 +1,30 @@
|
||||
name: Semgrep OSS scan
|
||||
on:
|
||||
pull_request: {}
|
||||
push:
|
||||
branches: [main, master]
|
||||
workflow_dispatch: {}
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
name: Semgrep config
|
||||
- cron: '0 0 25 * *'
|
||||
concurrency:
|
||||
group: semgrep-${{ github.event_name }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
semgrep:
|
||||
name: semgrep/ci
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
|
||||
SEMGREP_URL: https://cloudflare.semgrep.dev
|
||||
SEMGREP_APP_URL: https://cloudflare.semgrep.dev
|
||||
SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version
|
||||
container:
|
||||
image: semgrep/semgrep
|
||||
name: semgrep-oss
|
||||
runs-on: ubuntu-slim
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: semgrep ci
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- id: cache-semgrep
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.local
|
||||
key: semgrep-1.160.0-${{ runner.os }}
|
||||
- if: steps.cache-semgrep.outputs.cache-hit != 'true'
|
||||
run: pip install --user semgrep==1.160.0
|
||||
- run: echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
- run: semgrep scan --config=auto
|
||||
|
||||
@@ -18,3 +18,4 @@ ssh_server_tests/.env
|
||||
/.cover
|
||||
built_artifacts/
|
||||
component-tests/.venv
|
||||
/artifacts
|
||||
|
||||
+25
-4
@@ -1,5 +1,7 @@
|
||||
variables:
|
||||
GO_VERSION: "go1.24.9"
|
||||
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, review]
|
||||
stages:
|
||||
[
|
||||
sync,
|
||||
pre-build,
|
||||
build,
|
||||
validate,
|
||||
test,
|
||||
package,
|
||||
release,
|
||||
release-internal,
|
||||
review,
|
||||
]
|
||||
|
||||
include:
|
||||
#####################################################
|
||||
@@ -45,9 +58,17 @@ include:
|
||||
#####################################################
|
||||
- local: .ci/release.gitlab-ci.yml
|
||||
|
||||
#####################################################
|
||||
########## Release Packages Internally ##############
|
||||
#####################################################
|
||||
- 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,3 +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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
# use a builder image for building cloudflare
|
||||
ARG TARGET_GOOS
|
||||
ARG TARGET_GOARCH
|
||||
FROM golang:1.24.9 AS builder
|
||||
FROM golang:1.26.4 AS builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
TARGET_GOOS=${TARGET_GOOS} \
|
||||
@@ -20,7 +20,7 @@ COPY . .
|
||||
RUN make cloudflared
|
||||
|
||||
# use a distroless base image with glibc
|
||||
FROM gcr.io/distroless/base-debian12:nonroot
|
||||
FROM gcr.io/distroless/base-debian13:nonroot@sha256:b78832f41c8128046807c24840ebee4f1c18ba7870eed423d8750c272c15e147
|
||||
|
||||
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.9 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-debian12:nonroot
|
||||
FROM gcr.io/distroless/base-debian13:nonroot-amd64@sha256:ce2a20e0e277b7d913aa8bcfa098fc2a543dc08028f7393434963fa24b39ea81
|
||||
|
||||
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.9 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-debian12:nonroot-arm64
|
||||
FROM gcr.io/distroless/base-debian13:nonroot-arm64@sha256:7a4876f88e7fe3190972c274b679b3473f61e8d990a4dce3627961b3e22a0eaf
|
||||
|
||||
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.
|
||||
|
||||
+111
@@ -1,3 +1,114 @@
|
||||
2026.7.2
|
||||
- 2026-07-15 VULN-118896: MacOS service: use --token-file instead of --token
|
||||
- 2026-07-14 Update gcr.io/distroless/base-debian13:nonroot Docker digest to b78832f
|
||||
- 2026-07-14 chore: Change internal image name
|
||||
- 2026-07-14 Update gcr.io/distroless/base-debian13:nonroot-arm Docker digest to 7a4876f88
|
||||
- 2026-07-13 VULN-118896: Linux service: Use --token-file instead of --token
|
||||
- 2026-07-13 chore: Remove bookworm internal register publish
|
||||
- 2026-07-13 Update gcr.io/distroless/base-debian13:nonroot-amd64 Docker digest to ce2a20e
|
||||
|
||||
2026.7.1
|
||||
- 2026-07-09 Revert "TUN-10621: Propagate max wait timeout"
|
||||
|
||||
2026.7.0
|
||||
- 2026-07-08 chore: Bump go-chi to version 5.3.1
|
||||
- 2026-07-08 Revert "TUN-10557: Bump quic-go v0.59.1"
|
||||
- 2026-07-01 TUN-10621: Test websocket path
|
||||
- 2026-06-30 ci: add Semgrep OSS scanning workflow
|
||||
- 2026-06-29 TUN-10621: Propagate max wait timeout
|
||||
- 2026-06-29 Migrate config renovate.json
|
||||
- 2026-06-25 chore: Fix renovate
|
||||
- 2026-06-25 Pin dependencies
|
||||
- 2026-06-25 Consolidate init-system detection into shared inits package
|
||||
- 2026-06-24 Fix lint issues
|
||||
- 2026-06-22 Add OpenRC support
|
||||
- 2026-06-22 Fix broken installed-service detection in systemd uninstall
|
||||
- 2026-06-18 TUN-10557: Bump quic-go v0.59.1
|
||||
|
||||
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
|
||||
- 2026-01-19 fix: rpm bundling and rpm key import
|
||||
|
||||
2026.1.0
|
||||
- 2026-01-13 TUN-10162: Update go to 1.24.11 and Debian distroless to debian13
|
||||
- 2025-11-21 Replace jira.cfops.it with jira.cfdata.org in connection/http2_test.go
|
||||
- 2025-11-19 TUN-9863: Update pipelines to use cloudflared EV Certificate
|
||||
- 2025-11-07 TUN-9800: Migrate apt internal builds to Gitlab
|
||||
- 2025-11-04 TUN-9998: Don't need to read origin cert to determine if the endpoint is fedramp
|
||||
- 2025-10-13 TUN-9910: Make the metadata key to carry HTTP status over QUIC transport a constant
|
||||
|
||||
2025.11.1
|
||||
- 2025-11-07 TUN-9800: Fix docker hub push step
|
||||
|
||||
2025.11.0
|
||||
- 2025-11-06 TUN-9863: Introduce Code Signing for Windows Builds
|
||||
- 2025-11-06 TUN-9800: Prefix gitlab steps with operating system
|
||||
|
||||
@@ -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
|
||||
|
||||
+22
-16
@@ -2,10 +2,11 @@ package carrier
|
||||
|
||||
import (
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -23,7 +24,7 @@ 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}
|
||||
@@ -43,8 +44,8 @@ func TestServe(t *testing.T) {
|
||||
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 +57,21 @@ 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)
|
||||
messageSize, err := crand.Int(crand.Reader, big.NewInt(2048))
|
||||
require.NoError(t, err)
|
||||
clientMessage := make([]byte, int(messageSize.Int64())+1)
|
||||
_, err = crand.Read(clientMessage)
|
||||
require.NoError(t, err)
|
||||
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 +100,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)
|
||||
}
|
||||
|
||||
+2
-52
@@ -1,52 +1,2 @@
|
||||
pinned_go: &pinned_go go-boring=1.24.9-1
|
||||
|
||||
build_dir: &build_dir /cfsetup_build
|
||||
default-flavor: bookworm
|
||||
|
||||
bookworm: &bookworm
|
||||
build-fips-internal-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps: &build_fips_deb_deps
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- export ORIGINAL_NAME=true
|
||||
- make cloudflared-deb
|
||||
build-internal-deb-nightly-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_fips_deb_deps
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export NIGHTLY=true
|
||||
- export FIPS=true
|
||||
- export ORIGINAL_NAME=true
|
||||
- make cloudflared-deb
|
||||
build-internal-deb-nightly-arm64:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_fips_deb_deps
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm64
|
||||
- export NIGHTLY=true
|
||||
# - export FIPS=true # TUN-7595
|
||||
- export ORIGINAL_NAME=true
|
||||
- make cloudflared-deb
|
||||
build-deb-arm64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm64
|
||||
- make cloudflared-deb
|
||||
|
||||
trixie: *bookworm
|
||||
# A valid cfsetup.yaml is required but we dont have any real config to specify
|
||||
dummy_key: true
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
@@ -8,6 +13,71 @@ import (
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTokenFile = "token"
|
||||
tokenPerms os.FileMode = 0o600
|
||||
)
|
||||
|
||||
func ensureConfigDirExists(configDir string) error {
|
||||
if err := os.Mkdir(configDir, 0o755); err != nil { //nolint:gosec // config dir must be traversable by non-root user
|
||||
if errors.Is(err, os.ErrExist) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to create config dir at %s: %w", configDir, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeTokenToFile(path string, token string) error {
|
||||
if _, err := tunnel.ParseToken(token); err != nil {
|
||||
return cliutil.UsageError("Provided tunnel token is not valid (%s).", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, []byte(token), tokenPerms); err != nil {
|
||||
return fmt.Errorf("failed to write token to %s: %w", path, err)
|
||||
}
|
||||
|
||||
// If the token file already existed with unrestrictive perms, os.WriteFile
|
||||
// above will not update them
|
||||
if err := os.Chmod(path, tokenPerms); err != nil {
|
||||
return fmt.Errorf("failed to restrict permissions on token file %s: %w", path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeTokenFile(configDir string, log *zerolog.Logger) {
|
||||
tp := tokenPath(configDir)
|
||||
err := os.Remove(tp)
|
||||
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
log.Warn().Msgf("Could not remove service token file at %s: %v", tp, err)
|
||||
}
|
||||
}
|
||||
|
||||
func buildArgsForTokenFile(configDir string) []string {
|
||||
return []string{
|
||||
"tunnel", "run", "--token-file", tokenPath(configDir),
|
||||
}
|
||||
}
|
||||
|
||||
func tokenPath(configDir string) string {
|
||||
return path.Join(configDir, defaultTokenFile)
|
||||
}
|
||||
|
||||
func writeTokenToConfigDir(c *cli.Context, configDir string) error {
|
||||
if err := ensureConfigDirExists(configDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := writeTokenToFile(tokenPath(configDir), c.Args().First()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// nolint:unused // This function is used by the Windows build, the unused warning when building for Linux and MacOS is spurious
|
||||
func buildArgsForToken(c *cli.Context, log *zerolog.Logger) ([]string, error) {
|
||||
token := c.Args().First()
|
||||
if _, err := tunnel.ParseToken(token); err != nil {
|
||||
@@ -19,6 +89,7 @@ func buildArgsForToken(c *cli.Context, log *zerolog.Logger) ([]string, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// nolint:unused // This function is used by the Windows build, the unused warning when building for Linux and MacOS is spurious
|
||||
func getServiceExtraArgsFromCliArgs(c *cli.Context, log *zerolog.Logger) ([]string, error) {
|
||||
if c.NArg() > 0 {
|
||||
// currently, we only support extra args for token
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Package inits provides detection of the init system managing the host
|
||||
// (systemd, OpenRC, or SysV). It is shared by the service installer and the
|
||||
// auto-updater so that init-system detection lives in a single place.
|
||||
//
|
||||
// The functions are safe to call on any GOOS; on non-Linux platforms they
|
||||
// report that no Linux init system is in use.
|
||||
package inits
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// IsSystemd reports whether the host is managed by systemd.
|
||||
func IsSystemd() bool {
|
||||
_, err := os.Stat("/run/systemd/system")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsOpenRC reports whether the host is managed by OpenRC.
|
||||
func IsOpenRC() bool {
|
||||
for _, path := range []string{"/sbin/openrc-run", "/usr/sbin/openrc-run", "/usr/bin/openrc-run"} {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsSysV reports whether the host relies on a SysV-style init system, i.e. a
|
||||
// Linux host that is managed by neither systemd nor OpenRC. systemd and OpenRC
|
||||
// keep the service alive themselves, so only SysV needs the process to restart
|
||||
// itself after an auto-update.
|
||||
func IsSysV() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
}
|
||||
return !IsSystemd() && !IsOpenRC()
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/inits"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
@@ -22,8 +24,21 @@ func runApp(app *cli.App, _ chan struct{}) {
|
||||
Usage: "Manages the cloudflared system service",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "install",
|
||||
Usage: "Install cloudflared as a system service",
|
||||
Name: "install",
|
||||
Usage: "Install cloudflared as a system service",
|
||||
ArgsUsage: "[TOKEN]",
|
||||
Description: `
|
||||
Installs cloudflared as a service using the detected init system (e.g., sysv,
|
||||
systemd, openrc).
|
||||
|
||||
A token may optionally be provided. If a token is provided, it will be written
|
||||
to disk in the service configuration directory and the cloudflared service
|
||||
configured to use it via the --token-file argument.
|
||||
|
||||
If no token is provided, cloudflared will attempt to find a configuration file
|
||||
with tunnel credentials from a predetermined list of configuration directory
|
||||
paths. If found, it will use that configuration file and credentials (or error
|
||||
out if no configuration file with credentials was found).`,
|
||||
Action: cliutil.ConfiguredAction(installLinuxService),
|
||||
Flags: []cli.Flag{
|
||||
noUpdateServiceFlag,
|
||||
@@ -49,13 +64,14 @@ const (
|
||||
cloudflaredService = "cloudflared.service"
|
||||
cloudflaredUpdateService = "cloudflared-update.service"
|
||||
cloudflaredUpdateTimer = "cloudflared-update.timer"
|
||||
cloudflaredOpenRCService = "cloudflared"
|
||||
)
|
||||
|
||||
var systemdAllTemplates = map[string]ServiceTemplate{
|
||||
cloudflaredService: {
|
||||
Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredService),
|
||||
Content: `[Unit]
|
||||
Description=cloudflared
|
||||
Description=Cloudflare Tunnel client
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
@@ -97,12 +113,12 @@ WantedBy=timers.target
|
||||
|
||||
var sysvTemplate = ServiceTemplate{
|
||||
Path: "/etc/init.d/cloudflared",
|
||||
FileMode: 0755,
|
||||
FileMode: 0o755,
|
||||
// nolint: dupword
|
||||
Content: `#!/bin/sh
|
||||
# For RedHat and cousins:
|
||||
# chkconfig: 2345 99 01
|
||||
# description: cloudflared
|
||||
# description: Cloudflare Tunnel client
|
||||
# processname: {{.Path}}
|
||||
### BEGIN INIT INFO
|
||||
# Provides: {{.Path}}
|
||||
@@ -110,8 +126,8 @@ var sysvTemplate = ServiceTemplate{
|
||||
# Required-Stop:
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: cloudflared
|
||||
# Description: cloudflared agent
|
||||
# Short-Description: Cloudflare Tunnel client
|
||||
# Description: Cloudflare Tunnel client
|
||||
### END INIT INFO
|
||||
name=$(basename $(readlink -f $0))
|
||||
cmd="{{.Path}} --pidfile /var/run/$name.pid {{ range .ExtraArgs }} {{ . }}{{ end }}"
|
||||
@@ -186,19 +202,52 @@ exit 0
|
||||
`,
|
||||
}
|
||||
|
||||
var openrcTemplate = ServiceTemplate{
|
||||
Path: "/etc/init.d/" + cloudflaredOpenRCService,
|
||||
FileMode: 0o755,
|
||||
Content: `#!/sbin/openrc-run
|
||||
|
||||
description="Cloudflare Tunnel client"
|
||||
|
||||
: "${cloudflared_user:=root}"
|
||||
|
||||
command="{{.Path}}"
|
||||
command_args="{{ range .ExtraArgs }} {{ . }}{{ end }}"
|
||||
command_user="${cloudflared_user}"
|
||||
|
||||
pidfile="/run/${RC_SVCNAME}.pid"
|
||||
output_log="/var/log/${RC_SVCNAME}.log"
|
||||
error_log="/var/log/${RC_SVCNAME}.err"
|
||||
|
||||
# Use OpenRC's supervisor so the tunnel is restarted on failure.
|
||||
supervisor="supervise-daemon"
|
||||
respawn_delay=5
|
||||
respawn_max=0
|
||||
|
||||
depend() {
|
||||
need net
|
||||
use dns logger
|
||||
after net firewall
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
var openrcConfTemplate = ServiceTemplate{
|
||||
Path: "/etc/conf.d/" + cloudflaredOpenRCService,
|
||||
FileMode: 0o644,
|
||||
Content: `# Configuration for the cloudflared OpenRC service.
|
||||
|
||||
# User the cloudflared daemon runs as. Defaults to root.
|
||||
#cloudflared_user="cloudflared"
|
||||
`,
|
||||
}
|
||||
|
||||
var noUpdateServiceFlag = &cli.BoolFlag{
|
||||
Name: "no-update-service",
|
||||
Usage: "Disable auto-update of the cloudflared linux service, which restarts the server to upgrade for new versions.",
|
||||
Value: false,
|
||||
}
|
||||
|
||||
func isSystemd() bool {
|
||||
if _, err := os.Stat("/run/systemd/system"); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func installLinuxService(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
@@ -210,27 +259,46 @@ func installLinuxService(c *cli.Context) error {
|
||||
Path: etPath,
|
||||
}
|
||||
|
||||
// Check if the "no update flag" is set
|
||||
autoUpdate := !c.IsSet(noUpdateServiceFlag.Name)
|
||||
|
||||
var extraArgsFunc func(c *cli.Context, log *zerolog.Logger) ([]string, error)
|
||||
var extraArgs []string
|
||||
if c.NArg() == 0 {
|
||||
extraArgsFunc = buildArgsForConfig
|
||||
// If passed no arguments e.g., "$ cloudflared service install",
|
||||
// install the service using the detected config file (or error-out if
|
||||
// no config exists).
|
||||
if extraArgs, err = buildArgsForConfig(c, log); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
extraArgsFunc = buildArgsForToken
|
||||
}
|
||||
// If passed one argument e.g., "$ cloudflared service install <token>"
|
||||
// write the token to the config directory and install the service
|
||||
// using --token-file pointing to that file. This is the quick setup
|
||||
// the tunnel UI suggests.
|
||||
|
||||
extraArgs, err := extraArgsFunc(c, log)
|
||||
if err != nil {
|
||||
return err
|
||||
// Ensure token file is removed if install fails
|
||||
defer func() {
|
||||
if err != nil {
|
||||
removeTokenFile(serviceConfigDir, log)
|
||||
}
|
||||
}()
|
||||
|
||||
if err = writeTokenToConfigDir(c, serviceConfigDir); err != nil {
|
||||
return fmt.Errorf("could not write token to configuration directory: %w", err)
|
||||
}
|
||||
|
||||
extraArgs = buildArgsForTokenFile(serviceConfigDir)
|
||||
}
|
||||
|
||||
templateArgs.ExtraArgs = extraArgs
|
||||
|
||||
// Check if the "no update flag" is set
|
||||
autoUpdate := !c.IsSet(noUpdateServiceFlag.Name)
|
||||
|
||||
switch {
|
||||
case isSystemd():
|
||||
case inits.IsSystemd():
|
||||
log.Info().Msgf("Using Systemd")
|
||||
err = installSystemd(&templateArgs, autoUpdate, log)
|
||||
case inits.IsOpenRC():
|
||||
log.Info().Msgf("Using OpenRC")
|
||||
err = installOpenRC(&templateArgs, autoUpdate)
|
||||
default:
|
||||
log.Info().Msgf("Using SysV")
|
||||
err = installSysv(&templateArgs, autoUpdate, log)
|
||||
@@ -258,14 +326,11 @@ func buildArgsForConfig(c *cli.Context, log *zerolog.Logger) ([]string, error) {
|
||||
return err == nil && val != ""
|
||||
}
|
||||
if src.TunnelID == "" || !configPresent(tunnel.CredFileFlag) {
|
||||
return nil, fmt.Errorf(`Configuration file %s must contain entries for the tunnel to run and its associated credentials:
|
||||
tunnel: TUNNEL-UUID
|
||||
credentials-file: CREDENTIALS-FILE
|
||||
`, src.Source())
|
||||
return nil, fmt.Errorf("configuration file %s must contain entries for the tunnel to run and its associated credentials (tunnel: TUNNEL-UUID, credentials-file: CREDENTIALS-FILE)", src.Source())
|
||||
}
|
||||
if src.Source() != serviceConfigPath {
|
||||
if exists, err := config.FileExists(serviceConfigPath); err != nil || exists {
|
||||
return nil, fmt.Errorf("Possible conflicting configuration in %[1]s and %[2]s. Either remove %[2]s or run `cloudflared --config %[2]s service install`", src.Source(), serviceConfigPath)
|
||||
return nil, fmt.Errorf("possible conflicting configuration in %[1]s and %[2]s. Either remove %[2]s or run `cloudflared --config %[2]s service install`", src.Source(), serviceConfigPath)
|
||||
}
|
||||
|
||||
if err := copyFile(src.Source(), serviceConfigPath); err != nil {
|
||||
@@ -348,22 +413,51 @@ func installSysv(templateArgs *ServiceTemplateArgs, autoUpdate bool, log *zerolo
|
||||
return runCommand("service", "cloudflared", "start")
|
||||
}
|
||||
|
||||
func installOpenRC(templateArgs *ServiceTemplateArgs, autoUpdate bool) error {
|
||||
if autoUpdate {
|
||||
templateArgs.ExtraArgs = append([]string{"--autoupdate-freq", "24h0m0s"}, templateArgs.ExtraArgs...)
|
||||
} else {
|
||||
templateArgs.ExtraArgs = append([]string{"--no-autoupdate"}, templateArgs.ExtraArgs...)
|
||||
}
|
||||
|
||||
if err := openrcConfTemplate.Generate(templateArgs); err != nil {
|
||||
return fmt.Errorf("error generating OpenRC conf.d template: %w", err)
|
||||
}
|
||||
if err := openrcTemplate.Generate(templateArgs); err != nil {
|
||||
return fmt.Errorf("error generating OpenRC service template: %w", err)
|
||||
}
|
||||
|
||||
if err := runCommand("rc-update", "add", cloudflaredOpenRCService, "default"); err != nil {
|
||||
return fmt.Errorf("rc-update add %s default: %w", cloudflaredOpenRCService, err)
|
||||
}
|
||||
if err := runCommand("rc-service", cloudflaredOpenRCService, "start"); err != nil {
|
||||
return fmt.Errorf("rc-service %s start: %w", cloudflaredOpenRCService, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func uninstallLinuxService(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
var err error
|
||||
switch {
|
||||
case isSystemd():
|
||||
case inits.IsSystemd():
|
||||
log.Info().Msg("Using Systemd")
|
||||
err = uninstallSystemd(log)
|
||||
case inits.IsOpenRC():
|
||||
log.Info().Msg("Using OpenRC")
|
||||
err = uninstallOpenRC(log)
|
||||
default:
|
||||
log.Info().Msg("Using SysV")
|
||||
err = uninstallSysv(log)
|
||||
}
|
||||
|
||||
removeTokenFile(serviceConfigDir, log)
|
||||
|
||||
if err == nil {
|
||||
log.Info().Msg("Linux service for cloudflared uninstalled successfully")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -371,7 +465,11 @@ func uninstallSystemd(log *zerolog.Logger) error {
|
||||
// Get only the installed services
|
||||
installedServices := make(map[string]ServiceTemplate)
|
||||
for serviceName, serviceTemplate := range systemdAllTemplates {
|
||||
if err := runCommand("systemctl", "list-units", "--all", "|", "grep", serviceName); err == nil {
|
||||
path, err := serviceTemplate.ResolvePath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error resolving path for service %q: %w", serviceName, err)
|
||||
}
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
installedServices[serviceName] = serviceTemplate
|
||||
} else {
|
||||
log.Info().Msgf("Service '%s' not installed, skipping its uninstall", serviceName)
|
||||
@@ -431,28 +529,39 @@ func uninstallSysv(log *zerolog.Logger) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureConfigDirExists(configDir string) error {
|
||||
ok, err := config.FileExists(configDir)
|
||||
if !ok && err == nil {
|
||||
err = os.Mkdir(configDir, 0755)
|
||||
func uninstallOpenRC(log *zerolog.Logger) error {
|
||||
if err := runCommand("rc-service", cloudflaredOpenRCService, "stop"); err != nil {
|
||||
log.Warn().Err(err).Msg("could not stop cloudflared OpenRC service, continuing uninstall")
|
||||
}
|
||||
return err
|
||||
if err := runCommand("rc-update", "del", cloudflaredOpenRCService, "default"); err != nil {
|
||||
log.Warn().Err(err).Msg("could not remove cloudflared from the default runlevel, continuing uninstall")
|
||||
}
|
||||
for _, template := range []ServiceTemplate{openrcTemplate, openrcConfTemplate} {
|
||||
path, err := template.ResolvePath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error resolving OpenRC template path: %w", err)
|
||||
}
|
||||
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("error removing %s: %w", path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(src, dest string) error {
|
||||
srcFile, err := os.Open(src)
|
||||
srcFile, err := os.Open(src) //nolint:gosec // operator-provided service config path
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
defer func() { _ = srcFile.Close() }()
|
||||
|
||||
destFile, err := os.Create(dest)
|
||||
destFile, err := os.Create(dest) //nolint:gosec // operator-provided service config path
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ok := false
|
||||
defer func() {
|
||||
destFile.Close()
|
||||
_ = destFile.Close()
|
||||
if !ok {
|
||||
_ = os.Remove(dest)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
@@ -24,8 +25,19 @@ func runApp(app *cli.App, _ chan struct{}) {
|
||||
Usage: "Manages the cloudflared launch agent",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "install",
|
||||
Usage: "Install cloudflared as an user launch agent",
|
||||
Name: "install",
|
||||
Usage: "Install cloudflared as an user launch agent",
|
||||
ArgsUsage: "[TOKEN]",
|
||||
Description: `
|
||||
Installs cloudflared as a launchd-managed service.
|
||||
|
||||
A token may optionally be provided. If a token is provided, it will be written
|
||||
to disk in the service configuration directory and the cloudflared service
|
||||
configured to use it via the --token-file argument.
|
||||
|
||||
If no token is provided, cloudflared will run without the --token-file argument,
|
||||
causing it to look for credentials in a configuration file upon startup.`,
|
||||
|
||||
Action: cliutil.ConfiguredAction(installLaunchd),
|
||||
},
|
||||
{
|
||||
@@ -76,38 +88,43 @@ func isRootUser() bool {
|
||||
return os.Geteuid() == 0
|
||||
}
|
||||
|
||||
func installPath() (string, error) {
|
||||
// User is root, use /Library/LaunchDaemons instead of home directory
|
||||
func resolveLibraryPath(subPath, fileName string) (string, error) {
|
||||
// We use the system-wide /Library/... instead of ~/Library/... if the user is root
|
||||
if isRootUser() {
|
||||
return fmt.Sprintf("/Library/LaunchDaemons/%s.plist", launchdIdentifier), nil
|
||||
return path.Join("/Library", subPath, fileName), nil
|
||||
}
|
||||
userHomeDir, err := userHomeDir()
|
||||
|
||||
// This returns the home dir of the executing user using OS-specific method
|
||||
// for discovering the home dir. It's not recommended to call this when the
|
||||
// user has root permission as $HOME depends on what options the user uses
|
||||
// with sudo.
|
||||
userHomeDir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", errors.Wrap(err, "Cannot determine home directory for the user")
|
||||
}
|
||||
return fmt.Sprintf("%s/Library/LaunchAgents/%s.plist", userHomeDir, launchdIdentifier), nil
|
||||
return path.Join(userHomeDir, "Library", subPath, fileName), nil
|
||||
}
|
||||
|
||||
// For docs on these subdirectories, see:
|
||||
// https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/MacOSXDirectories/MacOSXDirectories.html
|
||||
func installPath() (string, error) {
|
||||
subpath := "LaunchAgents"
|
||||
if isRootUser() {
|
||||
subpath = "LaunchDaemons"
|
||||
}
|
||||
return resolveLibraryPath(subpath, launchdIdentifier+".plist")
|
||||
}
|
||||
|
||||
func stdoutPath() (string, error) {
|
||||
if isRootUser() {
|
||||
return fmt.Sprintf("/Library/Logs/%s.out.log", launchdIdentifier), nil
|
||||
}
|
||||
userHomeDir, err := userHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s/Library/Logs/%s.out.log", userHomeDir, launchdIdentifier), nil
|
||||
return resolveLibraryPath("Logs", launchdIdentifier+".out.log")
|
||||
}
|
||||
|
||||
func stderrPath() (string, error) {
|
||||
if isRootUser() {
|
||||
return fmt.Sprintf("/Library/Logs/%s.err.log", launchdIdentifier), nil
|
||||
}
|
||||
userHomeDir, err := userHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s/Library/Logs/%s.err.log", userHomeDir, launchdIdentifier), nil
|
||||
return resolveLibraryPath("Logs", launchdIdentifier+".err.log")
|
||||
}
|
||||
|
||||
func configPath() (string, error) {
|
||||
return resolveLibraryPath("Application Support", launchdIdentifier)
|
||||
}
|
||||
|
||||
func installLaunchd(c *cli.Context) error {
|
||||
@@ -125,18 +142,45 @@ func installLaunchd(c *cli.Context) error {
|
||||
etPath, err := os.Executable()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Error determining executable path")
|
||||
return fmt.Errorf("Error determining executable path: %v", err)
|
||||
return fmt.Errorf("Error determining executable path: %w", err)
|
||||
}
|
||||
installPath, err := installPath()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Error determining install path")
|
||||
return errors.Wrap(err, "Error determining install path")
|
||||
}
|
||||
extraArgs, err := getServiceExtraArgsFromCliArgs(c, log)
|
||||
if err != nil {
|
||||
errMsg := "Unable to determine extra arguments for launch daemon"
|
||||
log.Err(err).Msg(errMsg)
|
||||
return errors.Wrap(err, errMsg)
|
||||
|
||||
var extraArgs []string
|
||||
if c.NArg() > 0 {
|
||||
// The service has been installed using a token e.g.,
|
||||
// $ cloudflared service install <token>
|
||||
//
|
||||
// Write the token file to a config directory so we can start the
|
||||
// daemon with --token-file
|
||||
|
||||
// Don't use :=, if we did so we would create a new err variable and
|
||||
// shadow the outer one, causing the defer below to not have access to
|
||||
// the outer err
|
||||
var cp string
|
||||
cp, err = configPath()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Error determining path to config directory")
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure token file is removed if install fails at any point from now
|
||||
// on
|
||||
defer func() {
|
||||
if err != nil {
|
||||
removeTokenFile(cp, log)
|
||||
}
|
||||
}()
|
||||
|
||||
if err = writeTokenToConfigDir(c, cp); err != nil {
|
||||
return fmt.Errorf("could not write token to configuration directory: %w", err)
|
||||
}
|
||||
|
||||
extraArgs = buildArgsForTokenFile(cp)
|
||||
}
|
||||
|
||||
stdoutPath, err := stdoutPath()
|
||||
@@ -206,17 +250,13 @@ func uninstallLaunchd(c *cli.Context) error {
|
||||
if err == nil {
|
||||
log.Info().Msg("Launchd for cloudflared was uninstalled successfully")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func userHomeDir() (string, error) {
|
||||
// This returns the home dir of the executing user using OS-specific method
|
||||
// for discovering the home dir. It's not recommended to call this function
|
||||
// when the user has root permission as $HOME depends on what options the user
|
||||
// use with sudo.
|
||||
homeDir, err := homedir.Dir()
|
||||
cp, err := configPath()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "Cannot determine home directory for the user")
|
||||
log.Err(err).Msg("error determining path to config directory, not removing token file")
|
||||
return err
|
||||
}
|
||||
return homeDir, nil
|
||||
removeTokenFile(cp, log)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+87
-119
@@ -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,11 +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")
|
||||
}
|
||||
|
||||
logTransport := logger.CreateTransportLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
@@ -432,10 +405,7 @@ func StartServer(
|
||||
observer := connection.NewObserver(log, logTransport)
|
||||
|
||||
// Send Quick Tunnel URL to UI if applicable
|
||||
var quickTunnelURL string
|
||||
if namedTunnel != nil {
|
||||
quickTunnelURL = namedTunnel.QuickTunnelUrl
|
||||
}
|
||||
quickTunnelURL := namedTunnel.QuickTunnelUrl
|
||||
if quickTunnelURL != "" {
|
||||
observer.SendURL(quickTunnelURL)
|
||||
}
|
||||
@@ -447,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
|
||||
@@ -459,14 +436,7 @@ func StartServer(
|
||||
}
|
||||
}
|
||||
|
||||
userCreds, err := credentials.Read(c.String(cfdflags.OriginCert), log)
|
||||
var isFEDEndpoint bool
|
||||
if err != nil {
|
||||
isFEDEndpoint = false
|
||||
} else {
|
||||
isFEDEndpoint = userCreds.IsFEDEndpoint()
|
||||
}
|
||||
|
||||
isFEDEndpoint := namedTunnel.Credentials.Endpoint == credentials.FedEndpoint
|
||||
var managementHostname string
|
||||
if isFEDEndpoint {
|
||||
managementHostname = credentials.FedRampHostname
|
||||
@@ -495,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() {
|
||||
@@ -553,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,
|
||||
@@ -609,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 {
|
||||
@@ -647,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{
|
||||
@@ -671,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{
|
||||
@@ -681,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,
|
||||
@@ -921,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),
|
||||
@@ -944,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",
|
||||
@@ -1178,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() {
|
||||
@@ -1238,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 {
|
||||
@@ -1250,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
|
||||
|
||||
@@ -63,12 +63,14 @@ func (s searchByID) Path() (string, error) {
|
||||
Str("originCertPath", originCertPath).
|
||||
Logger()
|
||||
|
||||
// Fallback to look for tunnel credentials in the origin cert directory
|
||||
if originCertPath, err := credentials.FindOriginCert(originCertPath, &originCertLog); err == nil {
|
||||
originCertDir := filepath.Dir(originCertPath)
|
||||
if filePath, err := tunnelFilePath(s.id, originCertDir); err == nil {
|
||||
if s.fs.validFilePath(filePath) {
|
||||
return filePath, nil
|
||||
if originCertPath != "" {
|
||||
// Look for tunnel credentials in the origin cert directory if the flag is provided
|
||||
if originCertPath, err := credentials.FindOriginCert(originCertPath, &originCertLog); err == nil {
|
||||
originCertDir := filepath.Dir(originCertPath)
|
||||
if filePath, err := tunnelFilePath(s.id, originCertDir); err == nil {
|
||||
if s.fs.validFilePath(filePath) {
|
||||
return filePath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/inits"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
@@ -112,10 +113,10 @@ func CheckForUpdate(options updateOptions) (CheckResult, error) {
|
||||
func encodeWindowsPath(path string) string {
|
||||
// We do this because Windows allows spaces in directories such as
|
||||
// Program Files but does not allow these directories to be spaced in batch files.
|
||||
targetPath := strings.Replace(path, "Program Files (x86)", "PROGRA~2", -1)
|
||||
targetPath := strings.ReplaceAll(path, "Program Files (x86)", "PROGRA~2")
|
||||
// This is to do the same in 32 bit systems. We do this second so that the first
|
||||
// replace is for x86 dirs.
|
||||
targetPath = strings.Replace(targetPath, "Program Files", "PROGRA~1", -1)
|
||||
targetPath = strings.ReplaceAll(targetPath, "Program Files", "PROGRA~1")
|
||||
return targetPath
|
||||
}
|
||||
|
||||
@@ -248,7 +249,7 @@ func (a *AutoUpdater) Run(ctx context.Context) error {
|
||||
updateOutcome := loggedUpdate(a.log, updateOptions{updateDisabled: !a.configurable.enabled})
|
||||
if updateOutcome.Updated {
|
||||
buildInfo.CloudflaredVersion = updateOutcome.Version
|
||||
if IsSysV() {
|
||||
if inits.IsSysV() {
|
||||
// SysV doesn't have a mechanism to keep service alive, we have to restart the process
|
||||
a.log.Info().Msg("Restarting service managed by SysV...")
|
||||
pid, err := a.listeners.StartProcess()
|
||||
@@ -298,16 +299,5 @@ func wasInstalledFromPackageManager() bool {
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
return term.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
|
||||
func IsSysV() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, err := os.Stat("/run/systemd/system"); err == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return term.IsTerminal(int(os.Stdout.Fd())) // nolint:gosec
|
||||
}
|
||||
|
||||
+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,17 +29,24 @@ 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)
|
||||
resp = send_request(config.get_url()+"/")
|
||||
assert resp.status_code == 503, "Expected cloudflared to return 503 for all requests with no ingress defined"
|
||||
resp = send_request(config.get_url()+"/test")
|
||||
assert resp.status_code == 503, "Expected cloudflared to return 503 for all requests with no ingress defined"
|
||||
expected_status_code = 503
|
||||
resp = send_request(config.get_url()+"/", expected_status_code)
|
||||
assert resp.status_code == expected_status_code, "Expected cloudflared to return 503 for all requests with no ingress defined"
|
||||
resp = send_request(config.get_url()+"/test", expected_status_code)
|
||||
assert resp.status_code == expected_status_code, "Expected cloudflared to return 503 for all requests with no ingress defined"
|
||||
|
||||
def retry_if_result_none(result):
|
||||
'''
|
||||
Returns True if the result is None, indicating that the function should be retried.
|
||||
'''
|
||||
return result is None
|
||||
|
||||
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
||||
def send_request(url, headers={}):
|
||||
@retry(retry_on_result=retry_if_result_none, stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
||||
def send_request(url, expected_status_code=200):
|
||||
with requests.Session() as s:
|
||||
return s.get(url, timeout=BACKOFF_SECS, headers=headers)
|
||||
resp = s.get(url, timeout=BACKOFF_SECS)
|
||||
return resp if resp.status_code == expected_status_code else None
|
||||
|
||||
+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
|
||||
}
|
||||
@@ -295,7 +295,7 @@ func TestServeWS(t *testing.T) {
|
||||
require.False(t, respWriter.panicked)
|
||||
}
|
||||
|
||||
// TestNoWriteAfterServeHTTPReturns is a regression test of https://jira.cfops.it/browse/TUN-5184
|
||||
// TestNoWriteAfterServeHTTPReturns is a regression test of https://jira.cfdata.org/browse/TUN-5184
|
||||
// to make sure we don't write to the ResponseWriter after the ServeHTTP method returns
|
||||
func TestNoWriteAfterServeHTTPReturns(t *testing.T) {
|
||||
cfdHTTP2Conn, edgeTCPConn := newTestHTTP2Connection()
|
||||
|
||||
+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
|
||||
}
|
||||
|
||||
@@ -33,13 +33,15 @@ const (
|
||||
HTTPMethodKey = "HttpMethod"
|
||||
// HTTPHostKey is used to get or set http host in QUIC ALPN if the underlying proxy connection type is HTTP.
|
||||
HTTPHostKey = "HttpHost"
|
||||
// HTTPStatus is used to return http status code in QUIC ALPN if the underlying proxy connection type is HTTP.
|
||||
HTTPStatus = "HttpStatus"
|
||||
|
||||
QUICMetadataFlowID = "FlowID"
|
||||
)
|
||||
|
||||
// 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
|
||||
@@ -52,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,
|
||||
@@ -167,7 +169,7 @@ func (q *quicConnection) acceptStream(ctx context.Context) error {
|
||||
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.
|
||||
@@ -287,7 +289,7 @@ func (hrw *httpResponseAdapter) AddTrailer(trailerName, trailerValue string) {
|
||||
|
||||
func (hrw *httpResponseAdapter) WriteRespHeaders(status int, header http.Header) error {
|
||||
metadata := make([]pogs.Metadata, 0)
|
||||
metadata = append(metadata, pogs.Metadata{Key: "HttpStatus", Val: strconv.Itoa(status)})
|
||||
metadata = append(metadata, pogs.Metadata{Key: HTTPStatus, Val: strconv.Itoa(status)})
|
||||
for k, vv := range header {
|
||||
for _, v := range vv {
|
||||
httpHeaderKey := fmt.Sprintf("%s:%s", HTTPHeaderKey, k)
|
||||
@@ -327,7 +329,7 @@ func (hrw *httpResponseAdapter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
}
|
||||
|
||||
func (hrw *httpResponseAdapter) WriteErrorResponse(err error) {
|
||||
_ = hrw.WriteConnectResponseData(err, pogs.Metadata{Key: "HttpStatus", Val: strconv.Itoa(http.StatusBadGateway)})
|
||||
_ = hrw.WriteConnectResponseData(err, pogs.Metadata{Key: HTTPStatus, Val: strconv.Itoa(http.StatusBadGateway)})
|
||||
}
|
||||
|
||||
func (hrw *httpResponseAdapter) WriteConnectResponseData(respErr error, metadata ...pogs.Metadata) error {
|
||||
|
||||
@@ -29,6 +29,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/nettest"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection/dialopts"
|
||||
|
||||
"github.com/cloudflare/cloudflared/client"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
cfdflow "github.com/cloudflare/cloudflared/flow"
|
||||
@@ -149,7 +151,6 @@ func TestQUICServer(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
test := test // capture range variable
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
// Start a UDP Listener for QUIC.
|
||||
@@ -157,7 +158,7 @@ func TestQUICServer(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
|
||||
require.NoError(t, err)
|
||||
defer udpListener.Close()
|
||||
defer func() { _ = udpListener.Close() }()
|
||||
quicTransport := &quic.Transport{Conn: udpListener, ConnectionIDLength: 16}
|
||||
quicListener, err := quicTransport.Listen(testTLSServerConfig, testQUICConfig)
|
||||
require.NoError(t, err)
|
||||
@@ -499,7 +500,6 @@ func TestBuildHTTPRequest(t *testing.T) {
|
||||
|
||||
log := zerolog.Nop()
|
||||
for _, test := range tests {
|
||||
test := test // capture range variable
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req, err := buildHTTPRequest(t.Context(), test.connectRequest, test.body, 0, &log)
|
||||
require.NoError(t, err)
|
||||
@@ -525,7 +525,7 @@ func TestServeUDPSession(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
|
||||
require.NoError(t, err)
|
||||
defer udpListener.Close()
|
||||
defer func() { _ = udpListener.Close() }()
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
@@ -616,7 +616,7 @@ func TestTCPProxy_FlowRateLimited(t *testing.T) {
|
||||
|
||||
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
|
||||
require.NoError(t, err)
|
||||
defer udpListener.Close()
|
||||
defer func() { _ = udpListener.Close() }()
|
||||
|
||||
quicTransport := &quic.Transport{Conn: udpListener, ConnectionIDLength: 16}
|
||||
quicListener, err := quicTransport.Listen(testTLSServerConfig, testQUICConfig)
|
||||
@@ -660,7 +660,7 @@ func TestTCPProxy_FlowRateLimited(t *testing.T) {
|
||||
|
||||
func testCreateUDPConnReuseSourcePortForEdgeIP(t *testing.T, edgeIP netip.AddrPort) {
|
||||
logger := zerolog.Nop()
|
||||
conn, err := createUDPConnForConnIndex(0, nil, edgeIP, &logger)
|
||||
conn, err := createUDPConnForConnIndex(0, nil, edgeIP, dialopts.DialOpts{}, &logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
getPortFunc := func(conn *net.UDPConn) int {
|
||||
@@ -671,24 +671,114 @@ func testCreateUDPConnReuseSourcePortForEdgeIP(t *testing.T, edgeIP netip.AddrPo
|
||||
initialPort := getPortFunc(conn)
|
||||
|
||||
// close conn
|
||||
conn.Close()
|
||||
_ = conn.Close()
|
||||
|
||||
// should get the same port as before.
|
||||
conn, err = createUDPConnForConnIndex(0, nil, edgeIP, &logger)
|
||||
conn, err = createUDPConnForConnIndex(0, nil, edgeIP, dialopts.DialOpts{}, &logger)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, initialPort, getPortFunc(conn))
|
||||
|
||||
// new index, should get a different port
|
||||
conn1, err := createUDPConnForConnIndex(1, nil, edgeIP, &logger)
|
||||
conn1, err := createUDPConnForConnIndex(1, nil, edgeIP, dialopts.DialOpts{}, &logger)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, initialPort, getPortFunc(conn1))
|
||||
|
||||
// not closing the conn and trying to obtain a new conn for same index should give a different random port
|
||||
conn, err = createUDPConnForConnIndex(0, nil, edgeIP, &logger)
|
||||
conn, err = createUDPConnForConnIndex(0, nil, edgeIP, dialopts.DialOpts{}, &logger)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, initialPort, getPortFunc(conn))
|
||||
}
|
||||
|
||||
// TestSkipPortReuse tests that skipPortReuse uses a random ephemeral port for each dial.
|
||||
func TestSkipPortReuse(t *testing.T) {
|
||||
t.Parallel()
|
||||
logger := zerolog.Nop()
|
||||
edgeIP := netip.MustParseAddrPort("127.0.0.1:0")
|
||||
|
||||
// First dial with skipPortReuse should allocate a random port
|
||||
conn1, err := createUDPConnForConnIndex(0, nil, edgeIP, dialopts.DialOpts{SkipPortReuse: true}, &logger)
|
||||
require.NoError(t, err)
|
||||
port1 := conn1.LocalAddr().(*net.UDPAddr).Port
|
||||
|
||||
// Don't close conn1 yet - keep it open to prevent port reuse
|
||||
// Second dial with skipPortReuse should allocate a different random port
|
||||
conn2, err := createUDPConnForConnIndex(0, nil, edgeIP, dialopts.DialOpts{SkipPortReuse: true}, &logger)
|
||||
require.NoError(t, err)
|
||||
port2 := conn2.LocalAddr().(*net.UDPAddr).Port
|
||||
|
||||
// Now close both connections
|
||||
_ = conn1.Close()
|
||||
_ = conn2.Close()
|
||||
// With skipPortReuse, ports should be different (random allocation)
|
||||
require.NotEqual(t, port1, port2, "With skipPortReuse, each dial should use a different random port")
|
||||
}
|
||||
|
||||
// TestDialQuicWithSkipPortReuse tests that DialQuic works correctly with the WithSkipPortReuse option.
|
||||
func TestDialQuicWithSkipPortReuse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
// Start a mock QUIC server (similar to TestQUICServer)
|
||||
udpListener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = udpListener.Close() }()
|
||||
|
||||
serverAddr := netip.MustParseAddrPort(udpListener.LocalAddr().String())
|
||||
|
||||
quicTransport := &quic.Transport{Conn: udpListener, ConnectionIDLength: 16}
|
||||
quicListener, err := quicTransport.Listen(testTLSServerConfig, testQUICConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
serverDone := make(chan struct{})
|
||||
go func() {
|
||||
// Accept one connection
|
||||
session, err := quicListener.Accept(ctx)
|
||||
if err != nil {
|
||||
close(serverDone)
|
||||
return
|
||||
}
|
||||
// Keep session open until context is cancelled
|
||||
<-ctx.Done()
|
||||
_ = session.CloseWithError(0, "test done")
|
||||
close(serverDone)
|
||||
}()
|
||||
|
||||
// Test DialQuic with WithSkipPortReuse option
|
||||
tlsClientConfig := &tls.Config{
|
||||
// nolint: gosec
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"argotunnel"},
|
||||
}
|
||||
|
||||
log := zerolog.New(io.Discard)
|
||||
dialCtx, dialCancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
defer dialCancel()
|
||||
|
||||
// Dial with skipPortReuse option - should use a random ephemeral port
|
||||
conn, err := DialQuic(
|
||||
dialCtx,
|
||||
testQUICConfig,
|
||||
tlsClientConfig,
|
||||
serverAddr,
|
||||
nil, // connect on a random port
|
||||
0,
|
||||
&log,
|
||||
dialopts.DialOpts{SkipPortReuse: true},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, conn)
|
||||
|
||||
// Verify we can get connection state
|
||||
_ = conn.ConnectionState()
|
||||
|
||||
// Clean up
|
||||
_ = conn.CloseWithError(0, "test done")
|
||||
cancel()
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func serveSession(ctx context.Context, datagramConn *datagramV2Connection, edgeQUICSession quic.Connection, closeType closeReason, expectedReason string, t *testing.T) {
|
||||
payload := []byte(t.Name())
|
||||
sessionID := uuid.New()
|
||||
@@ -721,7 +811,7 @@ func serveSession(ctx context.Context, datagramConn *datagramV2Connection, edgeQ
|
||||
// Close connection to terminate session
|
||||
switch closeType {
|
||||
case closedByOrigin:
|
||||
originConn.Close()
|
||||
_ = originConn.Close()
|
||||
case closedByRemote:
|
||||
err = datagramConn.UnregisterUdpSession(ctx, sessionID, expectedReason)
|
||||
require.NoError(t, err)
|
||||
@@ -815,6 +905,7 @@ func testTunnelConnection(t *testing.T, serverAddr netip.AddrPort, index uint8)
|
||||
nil, // connect on a random port
|
||||
index,
|
||||
&log,
|
||||
dialopts.DialOpts{},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -8,9 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"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"
|
||||
@@ -24,7 +22,6 @@ import (
|
||||
"github.com/cloudflare/cloudflared/packet"
|
||||
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,20 +31,18 @@ const (
|
||||
demuxChanCapacity = 16
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidDestinationIP = errors.New("unable to parse destination IP")
|
||||
)
|
||||
var errInvalidDestinationIP = pkgerrors.New("unable to parse destination IP")
|
||||
|
||||
// DatagramSessionHandler is a service that can serve datagrams for a connection and handle sessions from incoming
|
||||
// connection streams.
|
||||
type DatagramSessionHandler interface {
|
||||
Serve(context.Context) error
|
||||
|
||||
pogs.SessionManager
|
||||
tunnelpogs.SessionManager
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
cfdquicv3 "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 cfdquicv3.DatagramConn
|
||||
metrics cfdquicv3.Metrics
|
||||
logger *zerolog.Logger
|
||||
}
|
||||
|
||||
func NewDatagramV3Connection(ctx context.Context,
|
||||
conn quic.Connection,
|
||||
sessionManager cfdquic.SessionManager,
|
||||
conn cfdquic.QUICConnection,
|
||||
sessionManager cfdquicv3.SessionManager,
|
||||
icmpRouter ingress.ICMPRouter,
|
||||
index uint8,
|
||||
metrics cfdquic.Metrics,
|
||||
metrics cfdquicv3.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 := cfdquicv3.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)).
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user