mirror of
https://github.com/cloudflare/cloudflared
synced 2026-06-08 13:33:07 +00:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e10e072599 | |||
| 686347cf91 | |||
| f45b3a1baf | |||
| 1ac6c45dad | |||
| d78e64c8cc | |||
| 7987d01a6e | |||
| e1dacbcea8 | |||
| 1cc15c6ffa | |||
| 51c5ef726c | |||
| 1fb466941a | |||
| fff1fc7390 | |||
| 9551f2a381 | |||
| 71448c1f7f | |||
| 80b1634515 | |||
| 4ac0c1f2d7 | |||
| 4dafc15f22 | |||
| 92ef55650f | |||
| 9e94122d2b | |||
| 173396be90 | |||
| d9e13ab2ab | |||
| 9e6d58aaea | |||
| f9c2bd51ae | |||
| 41dffd7f3c | |||
| 8825ceecb5 | |||
| 50104548cf | |||
| 08efe4c103 | |||
| 6c3df26b3c | |||
| 1cedefa1c2 | |||
| ddf4e6d854 | |||
| 8e7955ae89 | |||
| ae197908be | |||
| 6ec699509d | |||
| 242fccefa4 | |||
| d0a6318334 | |||
| 398da8860f | |||
| 70ed7ffc5f | |||
| 9ca8b41cf7 | |||
| b4a98b13fe |
@@ -0,0 +1,31 @@
|
||||
# Builds a custom CI Image when necessary
|
||||
|
||||
include:
|
||||
#####################################################
|
||||
############## Build and Push CI Image ##############
|
||||
#####################################################
|
||||
- component: $CI_SERVER_FQDN/cloudflare/ci/docker-image/build-push-image@~latest
|
||||
inputs:
|
||||
stage: pre-build
|
||||
jobPrefix: ci-image
|
||||
runOnChangesTo: [".ci/image/**"]
|
||||
runOnMR: true
|
||||
runOnBranches: '^master$'
|
||||
commentImageRefs: false
|
||||
runner: vm-linux-x86-4cpu-8gb
|
||||
EXTRA_DIB_ARGS: "--manifest=.ci/image/.docker-images"
|
||||
|
||||
#####################################################
|
||||
## Resolve the image reference for downstream jobs ##
|
||||
#####################################################
|
||||
- component: $CI_SERVER_FQDN/cloudflare/ci/docker-image/get-image-ref@~latest
|
||||
inputs:
|
||||
stage: pre-build
|
||||
jobPrefix: ci-image
|
||||
runOnMR: true
|
||||
runOnBranches: '^master$'
|
||||
IMAGE_PATH: "$REGISTRY_HOST/stash/tun/cloudflared/ci-image/master"
|
||||
VARIABLE_NAME: BUILD_IMAGE
|
||||
needs:
|
||||
- job: ci-image-build-push-image
|
||||
optional: true
|
||||
@@ -0,0 +1,53 @@
|
||||
## A set of predefined rules to use on the different jobs
|
||||
.default-rules:
|
||||
# Rules to run the job only on the master branch
|
||||
run-on-master:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: always
|
||||
- when: never
|
||||
# Rules to run the job only on merge requests
|
||||
run-on-mr:
|
||||
- if: $CI_COMMIT_TAG
|
||||
when: never
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
- when: never
|
||||
# Rules to run the job on merge_requests and master branch
|
||||
run-always:
|
||||
- if: $CI_COMMIT_TAG
|
||||
when: never
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH != null && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: always
|
||||
- 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
|
||||
|
||||
.component-tests:
|
||||
image: $BUILD_IMAGE
|
||||
rules:
|
||||
- !reference [.default-rules, run-always]
|
||||
variables:
|
||||
COMPONENT_TESTS_CONFIG: component-test-config.yaml
|
||||
COMPONENT_TESTS_CONFIG_CONTENT: Y2xvdWRmbGFyZWRfYmluYXJ5OiBjbG91ZGZsYXJlZC5leGUKY3JlZGVudGlhbHNfZmlsZTogY3JlZC5qc29uCm9yaWdpbmNlcnQ6IGNlcnQucGVtCnpvbmVfZG9tYWluOiBhcmdvdHVubmVsdGVzdC5jb20Kem9uZV90YWc6IDQ4Nzk2ZjFlNzBiYjc2NjljMjliYjUxYmEyODJiZjY1
|
||||
secrets:
|
||||
DNS_API_TOKEN:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_dev/_terraform_atlantis/component_tests_token/data@kv
|
||||
file: false
|
||||
COMPONENT_TESTS_ORIGINCERT:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_dev/component_tests_cert_pem/data@kv
|
||||
file: false
|
||||
cache: {}
|
||||
@@ -0,0 +1,17 @@
|
||||
include:
|
||||
- local: .ci/commons.gitlab-ci.yml
|
||||
|
||||
######################################
|
||||
### Sync master branch with Github ###
|
||||
######################################
|
||||
push-github:
|
||||
stage: sync
|
||||
rules:
|
||||
- !reference [.default-rules, run-on-master]
|
||||
script:
|
||||
- ./.ci/scripts/github-push.sh
|
||||
secrets:
|
||||
CLOUDFLARED_DEPLOY_SSH_KEY:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cloudflared_github_ssh/data@kv
|
||||
file: false
|
||||
cache: {}
|
||||
@@ -0,0 +1,2 @@
|
||||
images:
|
||||
- name: ci-image
|
||||
@@ -0,0 +1,26 @@
|
||||
ARG CLOUDFLARE_DOCKER_REGISTRY_HOST
|
||||
|
||||
FROM ${CLOUDFLARE_DOCKER_REGISTRY_HOST:-registry.cfdata.org}/stash/cf/debian-images/bookworm/main:2025.7.0@sha256:6350da2f7e728dae2c1420f6dafc38e23cacc0b399d3d5b2f40fe48d9c8ff1ca
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
apt-get install --no-install-recommends --allow-downgrades -y \
|
||||
build-essential \
|
||||
git \
|
||||
go-boring=1.24.6-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 && \
|
||||
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
|
||||
|
||||
WORKDIR /opt
|
||||
@@ -0,0 +1,99 @@
|
||||
.golang-inputs: &golang_inputs
|
||||
runOnMR: true
|
||||
runOnBranches: '^master$'
|
||||
outputDir: artifacts
|
||||
runner: linux-x86-8cpu-16gb
|
||||
stage: build
|
||||
golangVersion: "boring-1.24"
|
||||
CGO_ENABLED: 1
|
||||
|
||||
include:
|
||||
###################
|
||||
### Linux Build ###
|
||||
###################
|
||||
- component: $CI_SERVER_FQDN/cloudflare/ci/golang/boring-make@~latest
|
||||
inputs:
|
||||
<<: *golang_inputs
|
||||
jobPrefix: linux-build
|
||||
GOLANG_MAKE_TARGET: ci-build
|
||||
imageVersion: "3308-283bdf9@sha256:fcd83570c91565a72eab132c38e0f589a481e2f3d4f3779f9f9a93eb555fee4a"
|
||||
|
||||
########################
|
||||
### Linux FIPS Build ###
|
||||
########################
|
||||
- component: $CI_SERVER_FQDN/cloudflare/ci/golang/boring-make@~latest
|
||||
inputs:
|
||||
<<: *golang_inputs
|
||||
jobPrefix: linux-fips-build
|
||||
GOLANG_MAKE_TARGET: ci-fips-build
|
||||
imageVersion: "3308-283bdf9@sha256:fcd83570c91565a72eab132c38e0f589a481e2f3d4f3779f9f9a93eb555fee4a"
|
||||
|
||||
|
||||
#################
|
||||
### Unit Tests ##
|
||||
#################
|
||||
- component: $CI_SERVER_FQDN/cloudflare/ci/golang/boring-make@~latest
|
||||
inputs:
|
||||
<<: *golang_inputs
|
||||
stage: test
|
||||
jobPrefix: test
|
||||
GOLANG_MAKE_TARGET: ci-test
|
||||
imageVersion: "3308-283bdf9@sha256:fcd83570c91565a72eab132c38e0f589a481e2f3d4f3779f9f9a93eb555fee4a"
|
||||
|
||||
|
||||
######################
|
||||
### Unit Tests FIPS ##
|
||||
######################
|
||||
- component: $CI_SERVER_FQDN/cloudflare/ci/golang/boring-make@~latest
|
||||
inputs:
|
||||
<<: *golang_inputs
|
||||
stage: test
|
||||
jobPrefix: test-fips
|
||||
GOLANG_MAKE_TARGET: ci-fips-test
|
||||
imageVersion: "3308-283bdf9@sha256:fcd83570c91565a72eab132c38e0f589a481e2f3d4f3779f9f9a93eb555fee4a"
|
||||
|
||||
|
||||
#################
|
||||
### Vuln Check ##
|
||||
#################
|
||||
- component: $CI_SERVER_FQDN/cloudflare/ci/golang/boring-make@~latest
|
||||
inputs:
|
||||
<<: *golang_inputs
|
||||
runOnBranches: '^$'
|
||||
stage: validate
|
||||
jobPrefix: vulncheck
|
||||
GOLANG_MAKE_TARGET: vulncheck
|
||||
imageVersion: "3308-283bdf9@sha256:fcd83570c91565a72eab132c38e0f589a481e2f3d4f3779f9f9a93eb555fee4a"
|
||||
|
||||
|
||||
#################################
|
||||
### Run Linux Component Tests ###
|
||||
#################################
|
||||
component-tests-linux: &component-tests-linux
|
||||
stage: test
|
||||
extends: .component-tests
|
||||
needs:
|
||||
- ci-image-get-image-ref
|
||||
- linux-build-boring-make
|
||||
script:
|
||||
- ./.ci/scripts/component-tests.sh
|
||||
variables: &component-tests-variables
|
||||
CI: 1
|
||||
COMPONENT_TESTS_CONFIG_CONTENT: Y2xvdWRmbGFyZWRfYmluYXJ5OiBjbG91ZGZsYXJlZApjcmVkZW50aWFsc19maWxlOiBjcmVkLmpzb24Kb3JpZ2luY2VydDogY2VydC5wZW0Kem9uZV9kb21haW46IGFyZ290dW5uZWx0ZXN0LmNvbQp6b25lX3RhZzogNDg3OTZmMWU3MGJiNzY2OWMyOWJiNTFiYTI4MmJmNjU=
|
||||
tags:
|
||||
- linux-x86-8cpu-16gb
|
||||
artifacts:
|
||||
reports:
|
||||
junit: report.xml
|
||||
|
||||
######################################
|
||||
### Run Linux FIPS Component Tests ###
|
||||
######################################
|
||||
component-tests-linux-fips:
|
||||
<<: *component-tests-linux
|
||||
needs:
|
||||
- ci-image-get-image-ref
|
||||
- linux-fips-build-boring-make
|
||||
variables:
|
||||
<<: *component-tests-variables
|
||||
COMPONENT_TESTS_FIPS: 1
|
||||
@@ -0,0 +1,66 @@
|
||||
include:
|
||||
- local: .ci/commons.gitlab-ci.yml
|
||||
|
||||
###############################
|
||||
### Defaults for Mac Builds ###
|
||||
###############################
|
||||
.mac-build-defaults: &mac-build-defaults
|
||||
rules:
|
||||
- !reference [.default-rules, run-on-mr]
|
||||
tags:
|
||||
- "macstadium-${RUNNER_ARCH}"
|
||||
parallel:
|
||||
matrix:
|
||||
- RUNNER_ARCH: [arm, intel]
|
||||
cache: {}
|
||||
|
||||
######################################
|
||||
### Build Cloudflared Mac Binaries ###
|
||||
######################################
|
||||
build-cloudflared-macos: &build-mac
|
||||
<<: *mac-build-defaults
|
||||
stage: build
|
||||
artifacts:
|
||||
paths:
|
||||
- artifacts/*
|
||||
script:
|
||||
- '[ "${RUNNER_ARCH}" = "arm" ] && export TARGET_ARCH=arm64'
|
||||
- '[ "${RUNNER_ARCH}" = "intel" ] && export TARGET_ARCH=amd64'
|
||||
- ARCH=$(uname -m)
|
||||
- echo ARCH=$ARCH - TARGET_ARCH=$TARGET_ARCH
|
||||
- ./.ci/scripts/mac/install-go.sh
|
||||
- BUILD_SCRIPT=.ci/scripts/mac/build.sh
|
||||
- if [[ ! -x ${BUILD_SCRIPT} ]] ; then exit ; fi
|
||||
- set -euo pipefail
|
||||
- echo "Executing ${BUILD_SCRIPT}"
|
||||
- exec ${BUILD_SCRIPT}
|
||||
|
||||
###############################################
|
||||
### Build and Sign Cloudflared Mac Binaries ###
|
||||
###############################################
|
||||
build-and-sign-cloudflared-macos:
|
||||
<<: *build-mac
|
||||
rules:
|
||||
- !reference [.default-rules, run-on-master]
|
||||
secrets:
|
||||
APPLE_DEV_CA_CERT:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/apple_dev_ca_cert_v2/data@kv
|
||||
file: false
|
||||
CFD_CODE_SIGN_CERT:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_code_sign_cert_v2/data@kv
|
||||
file: false
|
||||
CFD_CODE_SIGN_KEY:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_code_sign_key_v2/data@kv
|
||||
file: false
|
||||
CFD_CODE_SIGN_PASS:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_code_sign_pass_v2/data@kv
|
||||
file: false
|
||||
CFD_INSTALLER_CERT:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_installer_cert_v2/data@kv
|
||||
file: false
|
||||
CFD_INSTALLER_KEY:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_installer_key_v2/data@kv
|
||||
file: false
|
||||
CFD_INSTALLER_PASS:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_installer_pass_v2/data@kv
|
||||
file: false
|
||||
@@ -0,0 +1,39 @@
|
||||
include:
|
||||
- local: .ci/commons.gitlab-ci.yml
|
||||
|
||||
###########################################
|
||||
### Push Cloudflared Binaries to Github ###
|
||||
###########################################
|
||||
release-cloudflared-to-github:
|
||||
stage: release
|
||||
image: $BUILD_IMAGE
|
||||
extends: .check-tag
|
||||
needs:
|
||||
- ci-image-get-image-ref
|
||||
- package-windows
|
||||
- build-and-sign-cloudflared-macos
|
||||
rules:
|
||||
- !reference [.default-rules, run-on-master]
|
||||
cache:
|
||||
paths:
|
||||
- .cache/pip
|
||||
variables:
|
||||
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
|
||||
KV_NAMESPACE: 380e19aa04314648949b6ad841417ebe
|
||||
KV_ACCOUNT: 5ab4e9dfbd435d24068829fda0077963
|
||||
secrets:
|
||||
KV_API_TOKEN:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_dev/cfd_kv_api_token/data@kv
|
||||
file: false
|
||||
API_KEY:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_dev/cfd_github_api_key/data@kv
|
||||
file: false
|
||||
script:
|
||||
- python3 --version ; pip --version # For debugging
|
||||
- python3 -m venv venv
|
||||
- source venv/bin/activate
|
||||
- pip install pynacl==1.4.0 pygithub==1.55
|
||||
- echo $VERSION
|
||||
- echo $TAG_EXISTS
|
||||
- echo "Running release because tag exists."
|
||||
- make gitlab-release
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -e -o pipefail
|
||||
|
||||
# Fetch cloudflared from the artifacts folder
|
||||
mv ./artifacts/cloudflared ./cloudflared
|
||||
|
||||
python3 -m venv env
|
||||
. env/bin/activate
|
||||
|
||||
pip install --upgrade -r component-tests/requirements.txt
|
||||
|
||||
# Creates and routes a Named Tunnel for this build. Also constructs
|
||||
# config file from env vars.
|
||||
python3 component-tests/setup.py --type create
|
||||
|
||||
# Define the cleanup function
|
||||
cleanup() {
|
||||
# The Named Tunnel is deleted and its route unprovisioned here.
|
||||
python3 component-tests/setup.py --type cleanup
|
||||
}
|
||||
|
||||
# The trap will call the cleanup function on script exit
|
||||
trap cleanup EXIT
|
||||
|
||||
pytest component-tests -o log_cli=true --log-cli-level=INFO --junit-xml=report.xml
|
||||
@@ -1,8 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e -o pipefail
|
||||
|
||||
OUTPUT=$(goimports -l -d -local github.com/cloudflare/cloudflared $(go list -mod=vendor -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc))
|
||||
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))
|
||||
|
||||
if [ -n "$OUTPUT" ] ; then
|
||||
PAGER=$(which colordiff || echo cat)
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
set -e -o pipefail
|
||||
|
||||
BRANCH="master"
|
||||
TMP_PATH="$PWD/tmp"
|
||||
PRIVATE_KEY_PATH="$TMP_PATH/github-deploy-key"
|
||||
PUBLIC_KEY_GITHUB_PATH="$TMP_PATH/github.pub"
|
||||
|
||||
mkdir -p $TMP_PATH
|
||||
|
||||
# Setup Private Key
|
||||
echo "$CLOUDFLARED_DEPLOY_SSH_KEY" > $PRIVATE_KEY_PATH
|
||||
chmod 400 $PRIVATE_KEY_PATH
|
||||
|
||||
# Download GitHub Public Key for KnownHostsFile
|
||||
ssh-keyscan -t ed25519 github.com > $PUBLIC_KEY_GITHUB_PATH
|
||||
|
||||
# Setup git ssh command with the right configurations
|
||||
export GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=$PUBLIC_KEY_GITHUB_PATH -o IdentitiesOnly=yes -i $PRIVATE_KEY_PATH"
|
||||
|
||||
# Add GitHub as a new remote
|
||||
git remote add github git@github.com:cloudflare/cloudflared.git || true
|
||||
|
||||
# GitLab doesn't pull branch references, instead it creates a new one on each pipeline.
|
||||
# Therefore, we need to manually fetch the reference to then push it to GitHub.
|
||||
git fetch origin $BRANCH:$BRANCH
|
||||
git push -u github $BRANCH
|
||||
|
||||
if TAG="$(git describe --tags --exact-match 2>/dev/null)"; then
|
||||
git push -u github "$TAG"
|
||||
fi
|
||||
@@ -1,19 +1,23 @@
|
||||
#!/bin/bash
|
||||
python3 -m venv env
|
||||
. env/bin/activate
|
||||
pip install pynacl==1.4.0 pygithub==1.55
|
||||
|
||||
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
||||
echo $VERSION
|
||||
|
||||
export TARGET_OS=windows
|
||||
# This controls the directory the built artifacts go into
|
||||
export BUILT_ARTIFACT_DIR=built_artifacts/
|
||||
export BUILT_ARTIFACT_DIR=artifacts/
|
||||
export FINAL_ARTIFACT_DIR=artifacts/
|
||||
mkdir -p $BUILT_ARTIFACT_DIR
|
||||
mkdir -p $FINAL_ARTIFACT_DIR
|
||||
windowsArchs=("amd64" "386")
|
||||
for arch in ${windowsArchs[@]}; do
|
||||
export TARGET_ARCH=$arch
|
||||
# Copy exe into final directory
|
||||
# Copy .exe from artifacts directory
|
||||
cp $BUILT_ARTIFACT_DIR/cloudflared-windows-$arch.exe ./cloudflared.exe
|
||||
make cloudflared-msi
|
||||
# Copy msi into final directory
|
||||
mv cloudflared-$VERSION-$arch.msi $FINAL_ARTIFACT_DIR/cloudflared-windows-$arch.msi
|
||||
cp $BUILT_ARTIFACT_DIR/cloudflared-windows-$arch.exe $FINAL_ARTIFACT_DIR/cloudflared-windows-$arch.exe
|
||||
done
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Define the file to store the list of vulnerabilities to ignore.
|
||||
IGNORE_FILE=".vulnignore"
|
||||
|
||||
# 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"
|
||||
fi
|
||||
|
||||
# Run govulncheck and capture its output.
|
||||
VULN_OUTPUT=$(go run -mod=readonly golang.org/x/vuln/cmd/govulncheck@latest ./... || true)
|
||||
|
||||
# Print the govuln output
|
||||
echo "====================================="
|
||||
echo "Full Output of govulncheck:"
|
||||
echo "====================================="
|
||||
echo "$VULN_OUTPUT"
|
||||
echo "====================================="
|
||||
echo "End of govulncheck Output"
|
||||
echo "====================================="
|
||||
|
||||
# Process the ignore file to remove comments and empty lines.
|
||||
# The 'cut' command gets the vulnerability ID and removes anything after the '#'.
|
||||
# The 'grep' command filters out empty lines and lines starting with '#'.
|
||||
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')
|
||||
|
||||
# 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)
|
||||
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
|
||||
else
|
||||
echo "🎉 No new vulnerabilities found. All clear! ✨"
|
||||
exit 0
|
||||
fi
|
||||
@@ -2,27 +2,23 @@ Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
# Relative path to working directory
|
||||
$CloudflaredDirectory = "go\src\github.com\cloudflare\cloudflared"
|
||||
$env:TARGET_OS = "windows"
|
||||
$env:LOCAL_OS = "windows"
|
||||
|
||||
cd $CloudflaredDirectory
|
||||
New-Item -Path ".\artifacts" -ItemType Directory
|
||||
|
||||
Write-Output "Building for amd64"
|
||||
$env:TARGET_OS = "windows"
|
||||
$env:CGO_ENABLED = 1
|
||||
$env:TARGET_ARCH = "amd64"
|
||||
$env:Path = "$Env:Temp\go\bin;$($env:Path)"
|
||||
|
||||
go env
|
||||
go version
|
||||
|
||||
$env:LOCAL_ARCH = "amd64"
|
||||
$env:CGO_ENABLED = 1
|
||||
& make cloudflared
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed to build cloudflared for amd64" }
|
||||
copy .\cloudflared.exe .\cloudflared-windows-amd64.exe
|
||||
copy .\cloudflared.exe .\artifacts\cloudflared-windows-amd64.exe
|
||||
|
||||
Write-Output "Building for 386"
|
||||
$env:CGO_ENABLED = 0
|
||||
$env:TARGET_ARCH = "386"
|
||||
make cloudflared
|
||||
$env:LOCAL_ARCH = "386"
|
||||
$env:CGO_ENABLED = 0
|
||||
& make cloudflared
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed to build cloudflared for 386" }
|
||||
copy .\cloudflared.exe .\cloudflared-windows-386.exe
|
||||
copy .\cloudflared.exe .\artifacts\cloudflared-windows-386.exe
|
||||
@@ -0,0 +1,40 @@
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
$env:TARGET_OS = "windows"
|
||||
$env:LOCAL_OS = "windows"
|
||||
$env:TARGET_ARCH = "amd64"
|
||||
$env:LOCAL_ARCH = "amd64"
|
||||
$env:CGO_ENABLED = 1
|
||||
|
||||
python --version
|
||||
python -m pip --version
|
||||
|
||||
|
||||
Write-Host "Building cloudflared"
|
||||
& make cloudflared
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed to build cloudflared" }
|
||||
|
||||
|
||||
Write-Host "Running unit tests"
|
||||
# Not testing with race detector because of https://github.com/golang/go/issues/61058
|
||||
# We already test it on other platforms
|
||||
go test -failfast -v -mod=vendor ./...
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed unit tests" }
|
||||
|
||||
|
||||
# On Gitlab runners we need to add all of this addresses to the NO_PROXY list in order for the tests to run.
|
||||
$env:NO_PROXY = "pypi.org,files.pythonhosted.org,api.cloudflare.com,argotunneltest.com,argotunnel.com,trycloudflare.com,${env:NO_PROXY}"
|
||||
Write-Host "No Proxy: ${env:NO_PROXY}"
|
||||
Write-Host "Running component tests"
|
||||
try {
|
||||
python -m pip --disable-pip-version-check install --upgrade -r component-tests/requirements.txt --use-pep517
|
||||
python component-tests/setup.py --type create
|
||||
python -m pytest component-tests -o log_cli=true --log-cli-level=INFO --junit-xml=report.xml
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed component tests"
|
||||
}
|
||||
} finally {
|
||||
python component-tests/setup.py --type cleanup
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
Param(
|
||||
[string]$GoVersion,
|
||||
[string]$ScriptToExecute
|
||||
)
|
||||
|
||||
# The script is a wrapper that downloads a specific version
|
||||
# of go, adds it to the PATH and executes a script with that go
|
||||
# version in the path.
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
# Get the path to the system's temporary directory.
|
||||
$tempPath = [System.IO.Path]::GetTempPath()
|
||||
|
||||
# Create a unique name for the new temporary folder.
|
||||
$folderName = "go_" + (Get-Random)
|
||||
|
||||
# Join the temp path and the new folder name to create the full path.
|
||||
$fullPath = Join-Path -Path $tempPath -ChildPath $folderName
|
||||
|
||||
# Store the current value of PATH environment variable.
|
||||
$oldPath = $env:Path
|
||||
|
||||
# Use a try...finally block to ensure the temporrary folder and PATH are cleaned up.
|
||||
try {
|
||||
# Create the temporary folder.
|
||||
Write-Host "Creating temporary folder at: $fullPath"
|
||||
$newTempFolder = New-Item -ItemType Directory -Path $fullPath -Force
|
||||
|
||||
# Download go
|
||||
$url = "https://go.dev/dl/$GoVersion.windows-amd64.zip"
|
||||
$destinationFile = Join-Path -Path $newTempFolder.FullName -ChildPath "go$GoVersion.windows-amd64.zip"
|
||||
Write-Host "Downloading go from: $url"
|
||||
Invoke-WebRequest -Uri $url -OutFile $destinationFile
|
||||
Write-Host "File downloaded to: $destinationFile"
|
||||
|
||||
# Unzip the downloaded file.
|
||||
Write-Host "Unzipping the file..."
|
||||
Expand-Archive -Path $destinationFile -DestinationPath $newTempFolder.FullName -Force
|
||||
Write-Host "File unzipped successfully."
|
||||
|
||||
# Define the go/bin path wich is inside the temporary folder
|
||||
$goBinPath = Join-Path -Path $fullPath -ChildPath "go\bin"
|
||||
|
||||
# Add the go/bin path to the PATH environment variable.
|
||||
$env:Path = "$goBinPath;$($env:Path)"
|
||||
Write-Host "Added $goBinPath to the environment PATH."
|
||||
|
||||
go env
|
||||
go version
|
||||
|
||||
& $ScriptToExecute
|
||||
} finally {
|
||||
# Cleanup: Remove the path from the environment variable and then the temporary folder.
|
||||
Write-Host "Starting cleanup..."
|
||||
|
||||
$env:Path = $oldPath
|
||||
Write-Host "Reverted changes in the environment PATH."
|
||||
|
||||
# Remove the temporary folder and its contents.
|
||||
if (Test-Path -Path $fullPath) {
|
||||
Remove-Item -Path $fullPath -Recurse -Force
|
||||
Write-Host "Temporary folder and its contents have been removed."
|
||||
} else {
|
||||
Write-Host "Temporary folder does not exist, no cleanup needed."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
include:
|
||||
- local: .ci/commons.gitlab-ci.yml
|
||||
|
||||
###################################
|
||||
### Defaults for Windows Builds ###
|
||||
###################################
|
||||
.windows-build-defaults: &windows-build-defaults
|
||||
rules:
|
||||
- !reference [.default-rules, run-always]
|
||||
tags:
|
||||
- windows-x86
|
||||
cache: {}
|
||||
|
||||
##########################################
|
||||
### Build Cloudflared Windows Binaries ###
|
||||
##########################################
|
||||
build-cloudflared-windows:
|
||||
<<: *windows-build-defaults
|
||||
stage: build
|
||||
script:
|
||||
- powershell -ExecutionPolicy Bypass -File ".\.ci\scripts\windows\go-wrapper.ps1" "${GO_VERSION}" ".\.ci\scripts\windows\builds.ps1"
|
||||
artifacts:
|
||||
paths:
|
||||
- artifacts/*
|
||||
|
||||
######################################################
|
||||
### Load Environment Variables for Component Tests ###
|
||||
######################################################
|
||||
load-windows-env-variables:
|
||||
stage: pre-build
|
||||
extends: .component-tests
|
||||
script:
|
||||
- echo "COMPONENT_TESTS_CONFIG=$COMPONENT_TESTS_CONFIG" >> windows.env
|
||||
- echo "COMPONENT_TESTS_CONFIG_CONTENT=$COMPONENT_TESTS_CONFIG_CONTENT" >> windows.env
|
||||
- echo "DNS_API_TOKEN=$DNS_API_TOKEN" >> windows.env
|
||||
# We have to encode the `COMPONENT_TESTS_ORIGINCERT` secret, because it content is a file, otherwise we can't export it using gitlab
|
||||
- echo "COMPONENT_TESTS_ORIGINCERT=$(echo "$COMPONENT_TESTS_ORIGINCERT" | base64 -w0)" >> windows.env
|
||||
variables:
|
||||
COMPONENT_TESTS_CONFIG_CONTENT: Y2xvdWRmbGFyZWRfYmluYXJ5OiBjbG91ZGZsYXJlZC5leGUKY3JlZGVudGlhbHNfZmlsZTogY3JlZC5qc29uCm9yaWdpbmNlcnQ6IGNlcnQucGVtCnpvbmVfZG9tYWluOiBhcmdvdHVubmVsdGVzdC5jb20Kem9uZV90YWc6IDQ4Nzk2ZjFlNzBiYjc2NjljMjliYjUxYmEyODJiZjY1
|
||||
artifacts:
|
||||
access: 'none'
|
||||
reports:
|
||||
dotenv: windows.env
|
||||
|
||||
###################################
|
||||
### Run Windows Component Tests ###
|
||||
###################################
|
||||
component-tests-cloudflared-windows:
|
||||
<<: *windows-build-defaults
|
||||
stage: test
|
||||
needs: ["load-windows-env-variables"]
|
||||
script:
|
||||
# We have to decode the secret we encoded on the `load-windows-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"
|
||||
artifacts:
|
||||
reports:
|
||||
junit: report.xml
|
||||
|
||||
################################
|
||||
### Package Windows Binaries ###
|
||||
################################
|
||||
package-windows:
|
||||
rules:
|
||||
- !reference [.default-rules, run-on-master]
|
||||
stage: package
|
||||
needs:
|
||||
- ci-image-get-image-ref
|
||||
- build-cloudflared-windows
|
||||
image: $BUILD_IMAGE
|
||||
script:
|
||||
- .ci/scripts/package-windows.sh
|
||||
cache: {}
|
||||
artifacts:
|
||||
paths:
|
||||
- artifacts/*
|
||||
+32
-158
@@ -1,172 +1,46 @@
|
||||
variables:
|
||||
# Define GOPATH within the project directory to allow GitLab CI to cache it.
|
||||
# By default, Go places modules in GOMODCACHE, often outside the project.
|
||||
# Explicitly setting GOMODCACHE ensures it's within the cached path.
|
||||
GOPATH: "$CI_PROJECT_DIR/.go"
|
||||
GOMODCACHE: "$GOPATH/pkg/mod"
|
||||
GO_BIN_DIR: "$GOPATH/bin"
|
||||
|
||||
cache:
|
||||
# Cache Go modules and the binaries.
|
||||
# The 'key' ensures a unique cache per branch, or you can use a fixed key
|
||||
# for a shared cache across all branches if that fits your workflow.
|
||||
key: "$CI_COMMIT_REF_SLUG"
|
||||
paths:
|
||||
- ${GOPATH}/pkg/mod/ # For Go modules
|
||||
- ${GO_BIN_DIR}/
|
||||
|
||||
stages: [build, release]
|
||||
GO_VERSION: "go1.24.6"
|
||||
GIT_DEPTH: "0"
|
||||
|
||||
default:
|
||||
id_tokens:
|
||||
VAULT_ID_TOKEN:
|
||||
aud: https://vault.cfdata.org
|
||||
|
||||
# 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
|
||||
stages: [sync, pre-build, build, validate, test, package, release]
|
||||
|
||||
## A set of predefined rules to use on the different jobs
|
||||
.default_rules:
|
||||
# Rules to run the job only on the master branch
|
||||
run_on_master:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: always
|
||||
- when: never
|
||||
# Rules to run the job only on branches that are not master. This is needed because for now
|
||||
# we need to keep a similar behavior due to the integration with teamcity, which requires us
|
||||
# to not trigger pipelines on tags and/or merge requests.
|
||||
run_on_branch:
|
||||
- if: $CI_COMMIT_TAG
|
||||
when: never
|
||||
- if: $CI_PIPELINE_SOURCE != "merge_request_event" && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
|
||||
when: always
|
||||
- when: never
|
||||
include:
|
||||
#####################################################
|
||||
########## Import Commons Configurations ############
|
||||
#####################################################
|
||||
- local: .ci/commons.gitlab-ci.yml
|
||||
|
||||
#####################################################
|
||||
########### Sync Repository with Github #############
|
||||
#####################################################
|
||||
- local: .ci/github.gitlab-ci.yml
|
||||
|
||||
# Template for Go setup, including caching and installation
|
||||
.go_setup:
|
||||
image: docker-registry.cfdata.org/stash/devtools/ci-builders/golang-1.24/master:3090-3e32590@sha256:fc81df4f8322f022d93712ee40bb1e5752fdbe9868d1e5a23fd851ad6fbecb91
|
||||
before_script:
|
||||
- mkdir -p ${GOPATH} ${GOMODCACHE} ${GO_BIN_DIR}
|
||||
- export PATH=$PATH:${GO_BIN_DIR}
|
||||
- go env -w GOMODCACHE=${GOMODCACHE} # Ensure go uses the cached module path
|
||||
#####################################################
|
||||
############# Build or Fetch CI Image ###############
|
||||
#####################################################
|
||||
- local: .ci/ci-image.gitlab-ci.yml
|
||||
|
||||
# Check if govulncheck is already installed and install it if not
|
||||
- if [ ! -f ${GO_BIN_DIR}/govulncheck ]; then
|
||||
echo "govulncheck not found in cache, installing...";
|
||||
go install golang.org/x/vuln/cmd/govulncheck@latest;
|
||||
else
|
||||
echo "govulncheck found in cache, skipping installation.";
|
||||
fi
|
||||
#####################################################
|
||||
################## Linux Builds ###################
|
||||
#####################################################
|
||||
- local: .ci/linux.gitlab-ci.yml
|
||||
|
||||
# -----------------------------------------------
|
||||
# Stage 1: Build on every PR
|
||||
# -----------------------------------------------
|
||||
build_cloudflared_macos: &build
|
||||
stage: build
|
||||
rules:
|
||||
- !reference [.default_rules, run_on_branch]
|
||||
tags:
|
||||
- "macstadium-${RUNNER_ARCH}"
|
||||
parallel:
|
||||
matrix:
|
||||
- RUNNER_ARCH: [arm, intel]
|
||||
artifacts:
|
||||
paths:
|
||||
- artifacts/*
|
||||
script:
|
||||
- '[ "${RUNNER_ARCH}" = "arm" ] && export TARGET_ARCH=arm64'
|
||||
- '[ "${RUNNER_ARCH}" = "intel" ] && export TARGET_ARCH=amd64'
|
||||
- ARCH=$(uname -m)
|
||||
- echo ARCH=$ARCH - TARGET_ARCH=$TARGET_ARCH
|
||||
- ./.teamcity/mac/install-go.sh
|
||||
- BUILD_SCRIPT=.teamcity/mac/build.sh
|
||||
- if [[ ! -x ${BUILD_SCRIPT} ]] ; then exit ; fi
|
||||
- set -euo pipefail
|
||||
- echo "Executing ${BUILD_SCRIPT}"
|
||||
- exec ${BUILD_SCRIPT}
|
||||
#####################################################
|
||||
################## Windows Builds ###################
|
||||
#####################################################
|
||||
- local: .ci/windows.gitlab-ci.yml
|
||||
|
||||
vulncheck:
|
||||
stage: build
|
||||
extends: .go_setup
|
||||
rules:
|
||||
- !reference [.default_rules, run_on_branch]
|
||||
script:
|
||||
- make vulncheck
|
||||
#####################################################
|
||||
################### macOS Builds ####################
|
||||
#####################################################
|
||||
- local: .ci/mac.gitlab-ci.yml
|
||||
|
||||
# -----------------------------------------------
|
||||
# Stage 1: Build and sign only on releases
|
||||
# -----------------------------------------------
|
||||
build_and_sign_cloudflared_macos:
|
||||
<<: *build
|
||||
rules:
|
||||
- !reference [.default_rules, run_on_master]
|
||||
secrets:
|
||||
APPLE_DEV_CA_CERT:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/apple_dev_ca_cert_v2/data@kv
|
||||
file: false
|
||||
CFD_CODE_SIGN_CERT:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_code_sign_cert_v2/data@kv
|
||||
file: false
|
||||
CFD_CODE_SIGN_KEY:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_code_sign_key_v2/data@kv
|
||||
file: false
|
||||
CFD_CODE_SIGN_PASS:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_code_sign_pass_v2/data@kv
|
||||
file: false
|
||||
CFD_INSTALLER_CERT:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_installer_cert_v2/data@kv
|
||||
file: false
|
||||
CFD_INSTALLER_KEY:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_installer_key_v2/data@kv
|
||||
file: false
|
||||
CFD_INSTALLER_PASS:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_installer_pass_v2/data@kv
|
||||
file: false
|
||||
|
||||
# -----------------------------------------------
|
||||
# Stage 2: Release to Github after building and signing
|
||||
# -----------------------------------------------
|
||||
release_cloudflared_macos_to_github:
|
||||
stage: release
|
||||
image: docker-registry.cfdata.org/stash/tun/docker-images/cloudflared-ci/main:6-8616fe631b76-amd64@sha256:96f4fd05e66cec03e0864c1bcf09324c130d4728eef45ee994716da499183614
|
||||
extends: .check_tag
|
||||
dependencies:
|
||||
- build_and_sign_cloudflared_macos
|
||||
rules:
|
||||
- !reference [.default_rules, run_on_master]
|
||||
cache:
|
||||
paths:
|
||||
- .cache/pip
|
||||
variables:
|
||||
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
|
||||
KV_NAMESPACE: 380e19aa04314648949b6ad841417ebe
|
||||
KV_ACCOUNT: 5ab4e9dfbd435d24068829fda0077963
|
||||
secrets:
|
||||
KV_API_TOKEN:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_dev/cfd_kv_api_token/data@kv
|
||||
file: false
|
||||
API_KEY:
|
||||
vault: gitlab/cloudflare/tun/cloudflared/_dev/cfd_github_api_key/data@kv
|
||||
file: false
|
||||
script:
|
||||
- python3 --version ; pip --version # For debugging
|
||||
- python3 -m venv venv
|
||||
- source venv/bin/activate
|
||||
- pip install pynacl==1.4.0 pygithub==1.55
|
||||
- echo $VERSION
|
||||
- echo $TAG_EXISTS
|
||||
- echo "Running release because tag exists."
|
||||
- make macos-release
|
||||
#####################################################
|
||||
################# Release Packages ##################
|
||||
#####################################################
|
||||
- local: .ci/release.gitlab-ci.yml
|
||||
|
||||
Vendored
-47
@@ -1,47 +0,0 @@
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
$WorkingDirectory = Get-Location
|
||||
$CloudflaredDirectory = "$WorkingDirectory\go\src\github.com\cloudflare\cloudflared"
|
||||
|
||||
go env
|
||||
go version
|
||||
|
||||
$env:TARGET_OS = "windows"
|
||||
$env:CGO_ENABLED = 1
|
||||
$env:TARGET_ARCH = "amd64"
|
||||
$env:Path = "$Env:Temp\go\bin;$($env:Path)"
|
||||
|
||||
python --version
|
||||
python -m pip --version
|
||||
|
||||
cd $CloudflaredDirectory
|
||||
|
||||
go env
|
||||
go version
|
||||
|
||||
Write-Output "Building cloudflared"
|
||||
|
||||
& make cloudflared
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed to build cloudflared" }
|
||||
|
||||
echo $LASTEXITCODE
|
||||
|
||||
Write-Output "Running unit tests"
|
||||
|
||||
# Not testing with race detector because of https://github.com/golang/go/issues/61058
|
||||
# We already test it on other platforms
|
||||
go test -failfast -v -mod=vendor ./...
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed unit tests" }
|
||||
|
||||
Write-Output "Running component tests"
|
||||
|
||||
python -m pip --disable-pip-version-check install --upgrade -r component-tests/requirements.txt --use-pep517
|
||||
python component-tests/setup.py --type create
|
||||
python -m pytest component-tests -o log_cli=true --log-cli-level=INFO
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
python component-tests/setup.py --type cleanup
|
||||
throw "Failed component tests"
|
||||
}
|
||||
python component-tests/setup.py --type cleanup
|
||||
@@ -0,0 +1,3 @@
|
||||
# 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
|
||||
@@ -1,3 +1,7 @@
|
||||
## 2025.7.1
|
||||
### Notices
|
||||
- `cloudflared` will no longer officially support Debian and Ubuntu distros that reached end-of-life: `buster`, `bullseye`, `impish`, `trusty`.
|
||||
|
||||
## 2025.1.1
|
||||
### New Features
|
||||
- This release introduces the use of new Post Quantum curves and the ability to use Post Quantum curves when running tunnels with the QUIC protocol this applies to non-FIPS and FIPS builds.
|
||||
|
||||
+5
-2
@@ -27,8 +27,11 @@ LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared
|
||||
# copy our compiled binary
|
||||
COPY --from=builder --chown=nonroot /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
|
||||
|
||||
# run as non-privileged user
|
||||
USER nonroot
|
||||
# run as nonroot user
|
||||
# We need to use numeric user id's because Kubernetes doesn't support strings:
|
||||
# https://github.com/kubernetes/kubernetes/blob/v1.33.2/pkg/kubelet/kuberuntime/security_context_others.go#L49
|
||||
# The `nonroot` user maps to `65532`, from: https://github.com/GoogleContainerTools/distroless/blob/main/common/variables.bzl#L18
|
||||
USER 65532:65532
|
||||
|
||||
# command / entrypoint of container
|
||||
ENTRYPOINT ["cloudflared", "--no-autoupdate"]
|
||||
|
||||
+5
-2
@@ -22,8 +22,11 @@ LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared
|
||||
# copy our compiled binary
|
||||
COPY --from=builder --chown=nonroot /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
|
||||
|
||||
# run as non-privileged user
|
||||
USER nonroot
|
||||
# run as nonroot user
|
||||
# We need to use numeric user id's because Kubernetes doesn't support strings:
|
||||
# https://github.com/kubernetes/kubernetes/blob/v1.33.2/pkg/kubelet/kuberuntime/security_context_others.go#L49
|
||||
# The `nonroot` user maps to `65532`, from: https://github.com/GoogleContainerTools/distroless/blob/main/common/variables.bzl#L18
|
||||
USER 65532:65532
|
||||
|
||||
# command / entrypoint of container
|
||||
ENTRYPOINT ["cloudflared", "--no-autoupdate"]
|
||||
|
||||
+5
-2
@@ -22,8 +22,11 @@ LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared
|
||||
# copy our compiled binary
|
||||
COPY --from=builder --chown=nonroot /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
|
||||
|
||||
# run as non-privileged user
|
||||
USER nonroot
|
||||
# run as nonroot user
|
||||
# We need to use numeric user id's because Kubernetes doesn't support strings:
|
||||
# https://github.com/kubernetes/kubernetes/blob/v1.33.2/pkg/kubelet/kuberuntime/security_context_others.go#L49
|
||||
# The `nonroot` user maps to `65532`, from: https://github.com/GoogleContainerTools/distroless/blob/main/common/variables.bzl#L18
|
||||
USER 65532:65532
|
||||
|
||||
# command / entrypoint of container
|
||||
ENTRYPOINT ["cloudflared", "--no-autoupdate"]
|
||||
|
||||
@@ -24,13 +24,19 @@ else
|
||||
DEB_PACKAGE_NAME := $(BINARY_NAME)
|
||||
endif
|
||||
|
||||
DATE := $(shell date -u -r RELEASE_NOTES '+%Y-%m-%d-%H%M UTC')
|
||||
# Use git in windows since we don't have access to the `date` tool
|
||||
ifeq ($(TARGET_OS), windows)
|
||||
DATE := $(shell git log -1 --format="%ad" --date=format-local:'%Y-%m-%dT%H:%M UTC' -- RELEASE_NOTES)
|
||||
else
|
||||
DATE := $(shell date -u -r RELEASE_NOTES '+%Y-%m-%d-%H:%M UTC')
|
||||
endif
|
||||
|
||||
VERSION_FLAGS := -X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"
|
||||
ifdef PACKAGE_MANAGER
|
||||
VERSION_FLAGS := $(VERSION_FLAGS) -X "github.com/cloudflare/cloudflared/cmd/cloudflared/updater.BuiltForPackageManager=$(PACKAGE_MANAGER)"
|
||||
endif
|
||||
|
||||
ifdef CONTAINER_BUILD
|
||||
ifdef CONTAINER_BUILD
|
||||
VERSION_FLAGS := $(VERSION_FLAGS) -X "github.com/cloudflare/cloudflared/metrics.Runtime=virtual"
|
||||
endif
|
||||
|
||||
@@ -64,6 +70,8 @@ else ifeq ($(LOCAL_ARCH),x86_64)
|
||||
TARGET_ARCH ?= amd64
|
||||
else ifeq ($(LOCAL_ARCH),amd64)
|
||||
TARGET_ARCH ?= amd64
|
||||
else ifeq ($(LOCAL_ARCH),386)
|
||||
TARGET_ARCH ?= 386
|
||||
else ifeq ($(LOCAL_ARCH),i686)
|
||||
TARGET_ARCH ?= amd64
|
||||
else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 5),armv8)
|
||||
@@ -111,7 +119,7 @@ ifneq ($(TARGET_ARM), )
|
||||
ARM_COMMAND := GOARM=$(TARGET_ARM)
|
||||
endif
|
||||
|
||||
ifeq ($(TARGET_ARM), 7)
|
||||
ifeq ($(TARGET_ARM), 7)
|
||||
PACKAGE_ARCH := armhf
|
||||
else
|
||||
PACKAGE_ARCH := $(TARGET_ARCH)
|
||||
@@ -120,6 +128,8 @@ endif
|
||||
#for FIPS compliance, FPM defaults to MD5.
|
||||
RPM_DIGEST := --rpm-digest sha256
|
||||
|
||||
GO_TEST_LOG_OUTPUT = /tmp/gotest.log
|
||||
|
||||
.PHONY: all
|
||||
all: cloudflared test
|
||||
|
||||
@@ -129,7 +139,7 @@ clean:
|
||||
|
||||
.PHONY: vulncheck
|
||||
vulncheck:
|
||||
@govulncheck ./...
|
||||
@./.ci/scripts/vuln-check.sh
|
||||
|
||||
.PHONY: cloudflared
|
||||
cloudflared:
|
||||
@@ -152,11 +162,9 @@ generate-docker-version:
|
||||
|
||||
.PHONY: test
|
||||
test: vet
|
||||
ifndef CI
|
||||
go test -v -mod=vendor -race $(LDFLAGS) ./...
|
||||
else
|
||||
@mkdir -p .cover
|
||||
go test -v -mod=vendor -race $(LDFLAGS) -coverprofile=".cover/c.out" ./...
|
||||
$Q go test -json -v -mod=vendor -race $(LDFLAGS) ./... 2>&1 | tee $(GO_TEST_LOG_OUTPUT)
|
||||
ifneq ($(FIPS), true)
|
||||
@go run -mod=readonly github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest -input $(GO_TEST_LOG_OUTPUT)
|
||||
endif
|
||||
|
||||
.PHONY: cover
|
||||
@@ -174,7 +182,7 @@ fuzz:
|
||||
@go test -fuzz=FuzzIPDecoder -fuzztime=600s ./packet
|
||||
@go test -fuzz=FuzzICMPDecoder -fuzztime=600s ./packet
|
||||
@go test -fuzz=FuzzSessionWrite -fuzztime=600s ./quic/v3
|
||||
@go test -fuzz=FuzzSessionServe -fuzztime=600s ./quic/v3
|
||||
@go test -fuzz=FuzzSessionRead -fuzztime=600s ./quic/v3
|
||||
@go test -fuzz=FuzzRegistrationDatagram -fuzztime=600s ./quic/v3
|
||||
@go test -fuzz=FuzzPayloadDatagram -fuzztime=600s ./quic/v3
|
||||
@go test -fuzz=FuzzRegistrationResponseDatagram -fuzztime=600s ./quic/v3
|
||||
@@ -230,14 +238,19 @@ github-release:
|
||||
python3 github_release.py --path $(PWD)/built_artifacts --release-version $(VERSION)
|
||||
python3 github_message.py --release-version $(VERSION)
|
||||
|
||||
.PHONY: macos-release
|
||||
macos-release:
|
||||
.PHONY: gitlab-release
|
||||
gitlab-release:
|
||||
python3 github_release.py --path $(PWD)/artifacts/ --release-version $(VERSION)
|
||||
|
||||
.PHONY: r2-linux-release
|
||||
r2-linux-release:
|
||||
python3 ./release_pkgs.py
|
||||
|
||||
.PHONY: r2-next-linux-release
|
||||
# Publishes to a separate R2 repository during GPG key rollover, using dual-key signing.
|
||||
r2-next-linux-release:
|
||||
python3 ./release_pkgs.py --upload-repo-file
|
||||
|
||||
.PHONY: capnp
|
||||
capnp:
|
||||
which capnp # https://capnproto.org/install.html
|
||||
@@ -246,7 +259,7 @@ capnp:
|
||||
|
||||
.PHONY: vet
|
||||
vet:
|
||||
go vet -mod=vendor github.com/cloudflare/cloudflared/...
|
||||
$Q go vet -mod=vendor github.com/cloudflare/cloudflared/...
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
@@ -255,7 +268,7 @@ fmt:
|
||||
|
||||
.PHONY: fmt-check
|
||||
fmt-check:
|
||||
@./fmt-check.sh
|
||||
@./.ci/scripts/fmt-check.sh
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@@ -264,3 +277,23 @@ lint:
|
||||
.PHONY: mocks
|
||||
mocks:
|
||||
go generate mocks/mockgen.go
|
||||
|
||||
.PHONY: ci-build
|
||||
ci-build:
|
||||
@GOOS=linux GOARCH=amd64 $(MAKE) cloudflared
|
||||
@mkdir -p artifacts
|
||||
@mv cloudflared artifacts/cloudflared
|
||||
|
||||
.PHONY: ci-fips-build
|
||||
ci-fips-build:
|
||||
@FIPS=true GOOS=linux GOARCH=amd64 $(MAKE) cloudflared
|
||||
@mkdir -p artifacts
|
||||
@mv cloudflared artifacts/cloudflared
|
||||
|
||||
.PHONY: ci-test
|
||||
ci-test: fmt-check lint test
|
||||
@go run -mod=readonly github.com/jstemmer/go-junit-report/v2@latest -in $(GO_TEST_LOG_OUTPUT) -parser gojson -out report.xml -set-exit-code
|
||||
|
||||
.PHONY: ci-fips-test
|
||||
ci-fips-test:
|
||||
@FIPS=true $(MAKE) ci-test
|
||||
|
||||
@@ -1,3 +1,47 @@
|
||||
2025.10.0
|
||||
- 2025-10-14 chore: Fix upload of RPM repo file during double signing
|
||||
- 2025-10-13 TUN-9882: Bump datagram v3 write channel capacity
|
||||
- 2025-10-10 chore: Fix import of GPG keys when two keys are provided
|
||||
- 2025-10-10 chore: Fix parameter order when uploading RPM .repo file to R2
|
||||
- 2025-10-10 TUN-9883: Add new datagram v3 feature flag
|
||||
- 2025-10-09 chore: Force usage of go-boring 1.24
|
||||
- 2025-10-08 TUN-9882: Improve metrics for datagram v3
|
||||
- 2025-10-07 GRC-16749: Add fedramp tags to catalog
|
||||
- 2025-10-07 TUN-9882: Add buffers for UDP and ICMP datagrams in datagram v3
|
||||
- 2025-10-07 TUN-9882: Add write deadline for UDP origin writes
|
||||
- 2025-09-29 TUN-9776: Support signing Debian packages with two keys for rollover
|
||||
- 2025-09-22 TUN-9800: Add pipeline to sync between gitlab and github repos
|
||||
|
||||
2025.9.1
|
||||
- 2025-09-22 TUN-9855: Create script to ignore vulnerabilities from govuln check
|
||||
- 2025-09-19 TUN-9852: Remove fmt.Println from cloudflared access command
|
||||
|
||||
2025.9.0
|
||||
- 2025-09-15 TUN-9820: Add support for FedRAMP in originRequest Access config
|
||||
- 2025-09-11 TUN-9800: Migrate cloudflared-ci pipelines to Gitlab CI
|
||||
- 2025-09-04 TUN-9803: Add windows builds to gitlab-ci
|
||||
- 2025-08-27 TUN-9755: Set endpoint in tunnel credentials when generating locally managed tunnel with a Fed token
|
||||
|
||||
2025.8.1
|
||||
- 2025-08-19 AUTH-7480 update fed callback url for login helper
|
||||
- 2025-08-19 CUSTESC-53681: Correct QUIC connection management for datagram handlers
|
||||
- 2025-08-12 AUTH-7260: Add support for login interstitial auto closure
|
||||
|
||||
2025.8.0
|
||||
- 2025-08-07 vuln: Fix GO-2025-3770 vulnerability
|
||||
- 2025-07-23 TUN-9583: set proper url and hostname for cloudflared tail command
|
||||
- 2025-07-07 TUN-9542: Remove unsupported Debian-based releases
|
||||
|
||||
2025.7.0
|
||||
- 2025-07-03 TUN-9540: Use numeric user id for Dockerfiles
|
||||
- 2025-07-01 TUN-9161: Remove P256Kyber768Draft00PQKex curve from nonFips curve preferences
|
||||
- 2025-07-01 TUN-9531: Bump go-boring from 1.24.2 to 1.24.4
|
||||
- 2025-07-01 TUN-9511: Add metrics for virtual DNS origin
|
||||
- 2025-06-30 TUN-9470: Add OriginDialerService to include TCP
|
||||
- 2025-06-30 TUN-9473: Add --dns-resolver-addrs flag
|
||||
- 2025-06-27 TUN-9472: Add virtual DNS service
|
||||
- 2025-06-23 TUN-9469: Centralize UDP origin proxy dialing as ingress service
|
||||
|
||||
2025.6.1
|
||||
- 2025-06-16 TUN-9467: add vulncheck to cloudflared
|
||||
- 2025-06-16 TUN-9495: Remove references to cloudflare-go
|
||||
|
||||
+8
-7
@@ -26,11 +26,13 @@ const (
|
||||
)
|
||||
|
||||
type StartOptions struct {
|
||||
AppInfo *token.AppInfo
|
||||
OriginURL string
|
||||
Headers http.Header
|
||||
Host string
|
||||
TLSClientConfig *tls.Config
|
||||
AppInfo *token.AppInfo
|
||||
OriginURL string
|
||||
Headers http.Header
|
||||
Host string
|
||||
TLSClientConfig *tls.Config
|
||||
AutoCloseInterstitial bool
|
||||
IsFedramp bool
|
||||
}
|
||||
|
||||
// Connection wraps up all the needed functions to forward over the tunnel
|
||||
@@ -46,7 +48,6 @@ type StdinoutStream struct{}
|
||||
// Read will read from Stdin
|
||||
func (c *StdinoutStream) Read(p []byte) (int, error) {
|
||||
return os.Stdin.Read(p)
|
||||
|
||||
}
|
||||
|
||||
// Write will write to Stdout
|
||||
@@ -139,7 +140,7 @@ func BuildAccessRequest(options *StartOptions, log *zerolog.Logger) (*http.Reque
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := token.FetchTokenWithRedirect(req.URL, options.AppInfo, log)
|
||||
token, err := token.FetchTokenWithRedirect(req.URL, options.AppInfo, options.AutoCloseInterstitial, options.IsFedramp, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -14,4 +14,7 @@ spec:
|
||||
lifecycle: "Active"
|
||||
owner: "teams/tunnel-teams-routing"
|
||||
cf:
|
||||
compliance:
|
||||
fedramp-high: "pending"
|
||||
fedramp-moderate: "yes"
|
||||
FIPS: "required"
|
||||
|
||||
+13
-27
@@ -1,9 +1,9 @@
|
||||
pinned_go: &pinned_go go-boring=1.24.2-1
|
||||
pinned_go: &pinned_go go-boring=1.24.4-1
|
||||
|
||||
build_dir: &build_dir /cfsetup_build
|
||||
default-flavor: bookworm
|
||||
|
||||
bullseye: &bullseye
|
||||
bookworm: &bookworm
|
||||
build-linux:
|
||||
build_dir: *build_dir
|
||||
builddeps: &build_deps
|
||||
@@ -123,28 +123,6 @@ bullseye: &bullseye
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm64
|
||||
- make cloudflared-deb
|
||||
package-windows:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
- wget
|
||||
# libmsi and libgcab are libraries the wixl binary depends on.
|
||||
- libmsi-dev
|
||||
- libgcab-dev
|
||||
- python3-venv
|
||||
pre-cache:
|
||||
- wget https://github.com/sudarshan-reddy/msitools/releases/download/v0.101b/wixl -P /usr/local/bin
|
||||
- chmod a+x /usr/local/bin/wixl
|
||||
post-cache:
|
||||
- python3 -m venv env
|
||||
- . env/bin/activate
|
||||
- pip install pynacl==1.4.0 pygithub==1.55
|
||||
- .teamcity/package-windows.sh
|
||||
test:
|
||||
build_dir: *build_dir
|
||||
builddeps: &build_deps_tests
|
||||
@@ -233,7 +211,7 @@ bullseye: &bullseye
|
||||
- make github-release
|
||||
r2-linux-release:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
builddeps: &r2-linux-release-deps
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
@@ -253,5 +231,13 @@ bullseye: &bullseye
|
||||
- pip install pynacl==1.4.0 pygithub==1.55 boto3==1.22.9 python-gnupg==0.4.9
|
||||
- make r2-linux-release
|
||||
|
||||
bookworm: *bullseye
|
||||
trixie: *bullseye
|
||||
r2-next-linux-release:
|
||||
build_dir: *build_dir
|
||||
builddeps: *r2-linux-release-deps
|
||||
post-cache:
|
||||
- python3 -m venv env
|
||||
- . env/bin/activate
|
||||
- pip install pynacl==1.4.0 pygithub==1.55 boto3==1.22.9 python-gnupg==0.4.9
|
||||
- make r2-next-linux-release
|
||||
|
||||
trixie: *bookworm
|
||||
|
||||
@@ -45,6 +45,6 @@ func (m *mockFeatureSelector) Snapshot() features.FeatureSnapshot {
|
||||
return features.FeatureSnapshot{
|
||||
PostQuantum: features.PostQuantumPrefer,
|
||||
DatagramVersion: features.DatagramV3,
|
||||
FeaturesList: []string{features.FeaturePostQuantum, features.FeatureDatagramV3_1},
|
||||
FeaturesList: []string{features.FeaturePostQuantum, features.FeatureDatagramV3_2},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, log *z
|
||||
options := &carrier.StartOptions{
|
||||
OriginURL: forwarder.URL,
|
||||
Headers: headers, //TODO: TUN-2688 support custom headers from config file
|
||||
IsFedramp: forwarder.IsFedramp,
|
||||
}
|
||||
|
||||
// we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side
|
||||
@@ -92,6 +93,7 @@ func ssh(c *cli.Context) error {
|
||||
OriginURL: url.String(),
|
||||
Headers: headers,
|
||||
Host: url.Host,
|
||||
IsFedramp: c.Bool(fedrampFlag),
|
||||
}
|
||||
|
||||
if connectTo := c.String(sshConnectTo); connectTo != "" {
|
||||
|
||||
@@ -51,6 +51,7 @@ Host {{.Hostname}}
|
||||
ProxyCommand {{.Cloudflared}} access ssh --hostname %h
|
||||
{{end}}
|
||||
`
|
||||
fedrampFlag = "fedramp"
|
||||
)
|
||||
|
||||
const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b@sentry.io/189878"
|
||||
@@ -79,6 +80,10 @@ func Commands() []*cli.Command {
|
||||
Aliases: []string{"forward"},
|
||||
Category: "Access",
|
||||
Usage: "access <subcommand>",
|
||||
Flags: []cli.Flag{&cli.BoolFlag{
|
||||
Name: fedrampFlag,
|
||||
Usage: "use when performing operations in fedramp account",
|
||||
}},
|
||||
Description: `Cloudflare Access protects internal resources by securing, authenticating and monitoring access
|
||||
per-user and by application. With Cloudflare Access, only authenticated users with the required permissions are
|
||||
able to reach sensitive resources. The commands provided here allow you to interact with Access protected
|
||||
@@ -104,6 +109,10 @@ func Commands() []*cli.Command {
|
||||
Name: "no-verbose",
|
||||
Usage: "print only the jwt to stdout",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "auto-close",
|
||||
Usage: "automatically close the auth interstitial after action",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: appURLFlag,
|
||||
},
|
||||
@@ -322,7 +331,7 @@ func curl(c *cli.Context) error {
|
||||
log.Info().Msg("You don't have an Access token set. Please run access token <access application> to fetch one.")
|
||||
return run("curl", cmdArgs...)
|
||||
}
|
||||
tok, err = token.FetchToken(appURL, appInfo, log)
|
||||
tok, err = token.FetchToken(appURL, appInfo, c.Bool(cfdflags.AutoCloseInterstitial), c.Bool(fedrampFlag), log)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to refresh token")
|
||||
return err
|
||||
@@ -442,7 +451,7 @@ func sshGen(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, appInfo, log)
|
||||
cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, appInfo, c.Bool(cfdflags.AutoCloseInterstitial), c.Bool(fedrampFlag), log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -542,7 +551,7 @@ func verifyTokenAtEdge(appUrl *url.URL, appInfo *token.AppInfo, c *cli.Context,
|
||||
if c.IsSet(sshTokenSecretFlag) {
|
||||
headers.Add(cfAccessClientSecretHeader, c.String(sshTokenSecretFlag))
|
||||
}
|
||||
options := &carrier.StartOptions{AppInfo: appInfo, OriginURL: appUrl.String(), Headers: headers}
|
||||
options := &carrier.StartOptions{AppInfo: appInfo, OriginURL: appUrl.String(), Headers: headers, AutoCloseInterstitial: c.Bool(cfdflags.AutoCloseInterstitial), IsFedramp: c.Bool(fedrampFlag)}
|
||||
|
||||
if valid, err := isTokenValid(options, log); err != nil {
|
||||
return err
|
||||
|
||||
@@ -157,4 +157,13 @@ const (
|
||||
|
||||
// ApiURL is the command line flag used to define the base URL of the API
|
||||
ApiURL = "api-url"
|
||||
|
||||
// Virtual DNS resolver service resolver addresses to use instead of dynamically fetching them from the OS.
|
||||
VirtualDNSServiceResolverAddresses = "dns-resolver-addrs"
|
||||
|
||||
// Management hostname to signify incoming management requests
|
||||
ManagementHostname = "management-hostname"
|
||||
|
||||
// Automatically close the login interstitial browser window after the user makes a decision.
|
||||
AutoCloseInterstitial = "auto-close"
|
||||
)
|
||||
|
||||
@@ -51,6 +51,7 @@ func buildTailManagementTokenSubcommand() *cli.Command {
|
||||
|
||||
func managementTokenCommand(c *cli.Context) error {
|
||||
log := createLogger(c)
|
||||
|
||||
token, err := getManagementToken(c, log)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -99,7 +100,7 @@ func buildTailCommand(subcommands []*cli.Command) *cli.Command {
|
||||
EnvVars: []string{"TUNNEL_MANAGEMENT_TOKEN"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "management-hostname",
|
||||
Name: cfdflags.ManagementHostname,
|
||||
Usage: "Management hostname to signify incoming management requests",
|
||||
EnvVars: []string{"TUNNEL_MANAGEMENT_HOSTNAME"},
|
||||
Hidden: true,
|
||||
@@ -236,7 +237,14 @@ func getManagementToken(c *cli.Context, log *zerolog.Logger) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
client, err := userCreds.Client(c.String(cfdflags.ApiURL), buildInfo.UserAgent(), log)
|
||||
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
|
||||
}
|
||||
@@ -261,7 +269,7 @@ func getManagementToken(c *cli.Context, log *zerolog.Logger) (string, error) {
|
||||
// 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) {
|
||||
var err error
|
||||
managementHostname := c.String("management-hostname")
|
||||
|
||||
token := c.String("token")
|
||||
if token == "" {
|
||||
token, err = getManagementToken(c, log)
|
||||
@@ -269,6 +277,19 @@ func buildURL(c *cli.Context, log *zerolog.Logger) (url.URL, error) {
|
||||
return url.URL{}, fmt.Errorf("unable to acquire management token for requested tunnel id: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
claims, err := management.ParseToken(token)
|
||||
if err != nil {
|
||||
return url.URL{}, fmt.Errorf("failed to determine if token is FED: %w", err)
|
||||
}
|
||||
|
||||
var managementHostname string
|
||||
if claims.IsFed() {
|
||||
managementHostname = credentials.FedRampHostname
|
||||
} else {
|
||||
managementHostname = c.String(cfdflags.ManagementHostname)
|
||||
}
|
||||
|
||||
query := url.Values{}
|
||||
query.Add("access_token", token)
|
||||
connector := c.String("connector-id")
|
||||
|
||||
@@ -97,7 +97,7 @@ var (
|
||||
"no-tls-verify",
|
||||
"no-chunked-encoding",
|
||||
"http2-origin",
|
||||
"management-hostname",
|
||||
cfdflags.ManagementHostname,
|
||||
"service-op-ip",
|
||||
"local-ssh-port",
|
||||
"ssh-idle-timeout",
|
||||
@@ -459,8 +459,23 @@ func StartServer(
|
||||
}
|
||||
}
|
||||
|
||||
userCreds, err := credentials.Read(c.String(cfdflags.OriginCert), log)
|
||||
var isFEDEndpoint bool
|
||||
if err != nil {
|
||||
isFEDEndpoint = false
|
||||
} else {
|
||||
isFEDEndpoint = userCreds.IsFEDEndpoint()
|
||||
}
|
||||
|
||||
var managementHostname string
|
||||
if isFEDEndpoint {
|
||||
managementHostname = credentials.FedRampHostname
|
||||
} else {
|
||||
managementHostname = c.String(cfdflags.ManagementHostname)
|
||||
}
|
||||
|
||||
mgmt := management.New(
|
||||
c.String("management-hostname"),
|
||||
managementHostname,
|
||||
c.Bool("management-diagnostics"),
|
||||
serviceIP,
|
||||
connectorID,
|
||||
@@ -1042,7 +1057,7 @@ func configureProxyFlags(shouldHide bool) []cli.Flag {
|
||||
Value: false,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "management-hostname",
|
||||
Name: cfdflags.ManagementHostname,
|
||||
Usage: "Management hostname to signify incoming management requests",
|
||||
EnvVars: []string{"TUNNEL_MANAGEMENT_HOSTNAME"},
|
||||
Hidden: true,
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
"github.com/cloudflare/cloudflared/features"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
"github.com/cloudflare/cloudflared/ingress/origins"
|
||||
"github.com/cloudflare/cloudflared/orchestration"
|
||||
"github.com/cloudflare/cloudflared/supervisor"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
@@ -34,7 +36,6 @@ import (
|
||||
const (
|
||||
secretValue = "*****"
|
||||
icmpFunnelTimeout = time.Second * 10
|
||||
fedRampRegion = "fed" // const string denoting the region used to connect to FEDRamp servers
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -219,6 +220,27 @@ func prepareTunnelConfig(
|
||||
resolvedRegion = endpoint
|
||||
}
|
||||
|
||||
warpRoutingConfig := ingress.NewWarpRoutingConfig(&cfg.WarpRouting)
|
||||
|
||||
// Setup origin dialer service and virtual services
|
||||
originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: ingress.NewDialer(warpRoutingConfig),
|
||||
TCPWriteTimeout: c.Duration(flags.WriteStreamTimeout),
|
||||
}, log)
|
||||
|
||||
// Setup DNS Resolver Service
|
||||
originMetrics := origins.NewMetrics(prometheus.DefaultRegisterer)
|
||||
dnsResolverAddrs := c.StringSlice(flags.VirtualDNSServiceResolverAddresses)
|
||||
dnsService := origins.NewDNSResolverService(origins.NewDNSDialer(), log, originMetrics)
|
||||
if len(dnsResolverAddrs) > 0 {
|
||||
addrs, err := parseResolverAddrPorts(dnsResolverAddrs)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid %s provided: %w", flags.VirtualDNSServiceResolverAddresses, err)
|
||||
}
|
||||
dnsService = origins.NewStaticDNSResolverService(addrs, origins.NewDNSDialer(), log, originMetrics)
|
||||
}
|
||||
originDialerService.AddReservedService(dnsService, []netip.AddrPort{origins.VirtualDNSServiceAddr})
|
||||
|
||||
tunnelConfig := &supervisor.TunnelConfig{
|
||||
ClientConfig: clientConfig,
|
||||
GracePeriod: gracePeriod,
|
||||
@@ -246,6 +268,8 @@ func prepareTunnelConfig(
|
||||
DisableQUICPathMTUDiscovery: c.Bool(flags.QuicDisablePathMTUDiscovery),
|
||||
QUICConnectionLevelFlowControlLimit: c.Uint64(flags.QuicConnLevelFlowControlLimit),
|
||||
QUICStreamLevelFlowControlLimit: c.Uint64(flags.QuicStreamLevelFlowControlLimit),
|
||||
OriginDNSService: dnsService,
|
||||
OriginDialerService: originDialerService,
|
||||
}
|
||||
icmpRouter, err := newICMPRouter(c, log)
|
||||
if err != nil {
|
||||
@@ -254,10 +278,10 @@ func prepareTunnelConfig(
|
||||
tunnelConfig.ICMPRouterServer = icmpRouter
|
||||
}
|
||||
orchestratorConfig := &orchestration.Config{
|
||||
Ingress: &ingressRules,
|
||||
WarpRouting: ingress.NewWarpRoutingConfig(&cfg.WarpRouting),
|
||||
ConfigurationFlags: parseConfigFlags(c),
|
||||
WriteTimeout: tunnelConfig.WriteStreamTimeout,
|
||||
Ingress: &ingressRules,
|
||||
WarpRouting: warpRoutingConfig,
|
||||
OriginDialerService: originDialerService,
|
||||
ConfigurationFlags: parseConfigFlags(c),
|
||||
}
|
||||
return tunnelConfig, orchestratorConfig, nil
|
||||
}
|
||||
@@ -494,3 +518,19 @@ func findLocalAddr(dst net.IP, port int) (netip.Addr, error) {
|
||||
localAddr := localAddrPort.Addr()
|
||||
return localAddr, nil
|
||||
}
|
||||
|
||||
func parseResolverAddrPorts(input []string) ([]netip.AddrPort, error) {
|
||||
// We don't allow more than 10 resolvers to be provided statically for the resolver service.
|
||||
if len(input) > 10 {
|
||||
return nil, errors.New("too many addresses provided, max: 10")
|
||||
}
|
||||
addrs := make([]netip.AddrPort, 0, len(input))
|
||||
for _, val := range input {
|
||||
addr, err := netip.ParseAddrPort(val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/credentials"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
@@ -19,11 +20,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
baseLoginURL = "https://dash.cloudflare.com/argotunnel"
|
||||
callbackURL = "https://login.cloudflareaccess.org/"
|
||||
// For now these are the same but will change in the future once we know which URLs to use (TUN-8872)
|
||||
fedBaseLoginURL = "https://dash.cloudflare.com/argotunnel"
|
||||
fedCallbackStoreURL = "https://login.cloudflareaccess.org/"
|
||||
baseLoginURL = "https://dash.cloudflare.com/argotunnel"
|
||||
callbackURL = "https://login.cloudflareaccess.org/"
|
||||
fedBaseLoginURL = "https://dash.fed.cloudflare.com/argotunnel"
|
||||
fedCallbackStoreURL = "https://login.fed.cloudflareaccess.org/"
|
||||
fedRAMPParamName = "fedramp"
|
||||
loginURLParamName = "loginURL"
|
||||
callbackURLParamName = "callbackURL"
|
||||
@@ -97,6 +97,8 @@ func login(c *cli.Context) error {
|
||||
callbackStoreURL,
|
||||
false,
|
||||
false,
|
||||
c.Bool(cfdflags.AutoCloseInterstitial),
|
||||
isFEDRamp,
|
||||
log,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -155,10 +155,12 @@ func (sc *subcommandContext) create(name string, credentialsFilePath string, sec
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tunnelCredentials := connection.Credentials{
|
||||
AccountTag: credential.AccountID(),
|
||||
TunnelSecret: tunnelSecret,
|
||||
TunnelID: tunnel.ID,
|
||||
Endpoint: credential.Endpoint(),
|
||||
}
|
||||
usedCertPath := false
|
||||
if credentialsFilePath == "" {
|
||||
|
||||
@@ -241,6 +241,11 @@ var (
|
||||
Usage: "Overrides the remote configuration for max active private network flows (TCP/UDP) that this cloudflared instance supports",
|
||||
EnvVars: []string{"TUNNEL_MAX_ACTIVE_FLOWS"},
|
||||
}
|
||||
dnsResolverAddrsFlag = &cli.StringSliceFlag{
|
||||
Name: flags.VirtualDNSServiceResolverAddresses,
|
||||
Usage: "Overrides the dynamic DNS resolver resolution to use these address:port's instead.",
|
||||
EnvVars: []string{"TUNNEL_DNS_RESOLVER_ADDRS"},
|
||||
}
|
||||
)
|
||||
|
||||
func buildCreateCommand() *cli.Command {
|
||||
@@ -718,6 +723,7 @@ func buildRunCommand() *cli.Command {
|
||||
icmpv4SrcFlag,
|
||||
icmpv6SrcFlag,
|
||||
maxActiveFlowsFlag,
|
||||
dnsResolverAddrsFlag,
|
||||
}
|
||||
flags = append(flags, configureProxyFlags(false)...)
|
||||
return &cli.Command{
|
||||
|
||||
@@ -9,7 +9,7 @@ import pytest
|
||||
|
||||
import test_logging
|
||||
from conftest import CfdModes
|
||||
from util import select_platform, start_cloudflared, wait_tunnel_ready, write_config
|
||||
from util import select_platform, skip_on_ci, start_cloudflared, wait_tunnel_ready, write_config
|
||||
|
||||
|
||||
def default_config_dir():
|
||||
@@ -82,6 +82,7 @@ class TestServiceMode:
|
||||
os.remove(default_config_file())
|
||||
self.launchctl_cmd("list", success=False)
|
||||
|
||||
@skip_on_ci("we can't run sudo command on CI")
|
||||
@select_platform("Linux")
|
||||
@pytest.mark.skipif(os.path.exists("/etc/cloudflared/config.yml"),
|
||||
reason=f"There is already a config file in default path")
|
||||
@@ -98,6 +99,7 @@ class TestServiceMode:
|
||||
|
||||
self.sysv_service_scenario(config, tmp_path, assert_log_file)
|
||||
|
||||
@skip_on_ci("we can't run sudo command on CI")
|
||||
@select_platform("Linux")
|
||||
@pytest.mark.skipif(os.path.exists("/etc/cloudflared/config.yml"),
|
||||
reason=f"There is already a config file in default path")
|
||||
@@ -116,6 +118,7 @@ class TestServiceMode:
|
||||
|
||||
self.sysv_service_scenario(config, tmp_path, assert_rotating_log)
|
||||
|
||||
@skip_on_ci("we can't run sudo command on CI")
|
||||
@select_platform("Linux")
|
||||
@pytest.mark.skipif(os.path.exists("/etc/cloudflared/config.yml"),
|
||||
reason=f"There is already a config file in default path")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import base64
|
||||
import json
|
||||
|
||||
from setup import get_config_from_file, persist_origin_cert
|
||||
from setup import get_config_from_file
|
||||
from util import start_cloudflared
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import pytest
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
import json
|
||||
from retrying import retry
|
||||
|
||||
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
|
||||
@@ -35,6 +34,12 @@ def fips_enabled():
|
||||
nofips = pytest.mark.skipif(
|
||||
fips_enabled(), reason=f"Only runs without FIPS (COMPONENT_TESTS_FIPS=0)")
|
||||
|
||||
def skip_on_ci(reason):
|
||||
env_ci = os.getenv("CI")
|
||||
running_in_ci = env_ci is not None and env_ci != "0"
|
||||
return pytest.mark.skipif(
|
||||
running_in_ci, reason=f"This test can't run on CI due to: {reason}")
|
||||
|
||||
def write_config(directory, config):
|
||||
config_path = directory / "config.yml"
|
||||
with open(config_path, 'w') as outfile:
|
||||
@@ -111,6 +116,7 @@ def inner_wait_tunnel_ready(tunnel_url=None, require_min_connections=1):
|
||||
metrics_url = f'http://localhost:{METRICS_PORT}/ready'
|
||||
|
||||
with requests.Session() as s:
|
||||
LOGGER.debug("Waiting for tunnel to be ready...")
|
||||
resp = send_request(s, metrics_url, True)
|
||||
|
||||
ready_connections = resp.json()["readyConnections"]
|
||||
|
||||
@@ -242,6 +242,8 @@ type AccessConfig struct {
|
||||
|
||||
// AudTag is the AudTag to verify access JWT against.
|
||||
AudTag []string `yaml:"audTag" json:"audTag"`
|
||||
|
||||
Environment string `yaml:"environment" json:"environment,omitempty"`
|
||||
}
|
||||
|
||||
type IngressIPRule struct {
|
||||
|
||||
+15
-14
@@ -1,7 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
@@ -16,6 +16,7 @@ type Forwarder struct {
|
||||
TokenClientID string `json:"service_token_id" yaml:"serviceTokenID"`
|
||||
TokenSecret string `json:"secret_token_id" yaml:"serviceTokenSecret"`
|
||||
Destination string `json:"destination"`
|
||||
IsFedramp bool `json:"is_fedramp" yaml:"isFedramp"`
|
||||
}
|
||||
|
||||
// Tunnel represents a tunnel that should be started
|
||||
@@ -46,24 +47,24 @@ type Root struct {
|
||||
|
||||
// Hash returns the computed values to see if the forwarder values change
|
||||
func (f *Forwarder) Hash() string {
|
||||
h := md5.New()
|
||||
io.WriteString(h, f.URL)
|
||||
io.WriteString(h, f.Listener)
|
||||
io.WriteString(h, f.TokenClientID)
|
||||
io.WriteString(h, f.TokenSecret)
|
||||
io.WriteString(h, f.Destination)
|
||||
h := sha256.New()
|
||||
_, _ = io.WriteString(h, f.URL)
|
||||
_, _ = io.WriteString(h, f.Listener)
|
||||
_, _ = io.WriteString(h, f.TokenClientID)
|
||||
_, _ = io.WriteString(h, f.TokenSecret)
|
||||
_, _ = 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 := md5.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))
|
||||
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))
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ func (c *controlStream) ServeControlStream(
|
||||
tunnelConfigGetter TunnelConfigJSONGetter,
|
||||
) error {
|
||||
registrationClient := c.registerClientFunc(ctx, rw, c.registerTimeout)
|
||||
|
||||
c.observer.logConnecting(c.connIndex, c.edgeAddress, c.protocol)
|
||||
registrationDetails, err := registrationClient.RegisterConnection(
|
||||
ctx,
|
||||
c.tunnelProperties.Credentials.Auth(),
|
||||
|
||||
+17
-18
@@ -1,7 +1,6 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
@@ -53,26 +52,26 @@ func serverRegistrationErrorFromRPC(err error) ServerRegisterTunnelError {
|
||||
}
|
||||
}
|
||||
|
||||
type muxerShutdownError struct{}
|
||||
type ControlStreamError struct{}
|
||||
|
||||
func (e muxerShutdownError) Error() string {
|
||||
return "muxer shutdown"
|
||||
var _ error = &ControlStreamError{}
|
||||
|
||||
func (e *ControlStreamError) Error() string {
|
||||
return "control stream encountered a failure while serving"
|
||||
}
|
||||
|
||||
var errMuxerStopped = muxerShutdownError{}
|
||||
type StreamListenerError struct{}
|
||||
|
||||
func isHandshakeErrRecoverable(err error, connIndex uint8, observer *Observer) bool {
|
||||
log := observer.log.With().
|
||||
Uint8(LogFieldConnIndex, connIndex).
|
||||
Err(err).
|
||||
Logger()
|
||||
var _ error = &StreamListenerError{}
|
||||
|
||||
switch err.(type) {
|
||||
case edgediscovery.DialError:
|
||||
log.Error().Msg("Connection unable to dial edge")
|
||||
default:
|
||||
log.Error().Msg("Connection failed")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
func (e *StreamListenerError) Error() string {
|
||||
return "accept stream listener encountered a failure while serving"
|
||||
}
|
||||
|
||||
type DatagramManagerError struct{}
|
||||
|
||||
var _ error = &DatagramManagerError{}
|
||||
|
||||
func (e *DatagramManagerError) Error() string {
|
||||
return "datagram manager encountered a failure while serving"
|
||||
}
|
||||
|
||||
@@ -46,6 +46,15 @@ func (o *Observer) RegisterSink(sink EventSink) {
|
||||
o.addSinkChan <- sink
|
||||
}
|
||||
|
||||
func (o *Observer) logConnecting(connIndex uint8, address net.IP, protocol Protocol) {
|
||||
o.log.Debug().
|
||||
Int(management.EventTypeKey, int(management.Cloudflared)).
|
||||
Uint8(LogFieldConnIndex, connIndex).
|
||||
IPAddr(LogFieldIPAddress, address).
|
||||
Str(LogFieldProtocol, protocol.String()).
|
||||
Msg("Registering tunnel connection")
|
||||
}
|
||||
|
||||
func (o *Observer) logConnected(connectionID uuid.UUID, connIndex uint8, location string, address net.IP, protocol Protocol) {
|
||||
o.log.Info().
|
||||
Int(management.EventTypeKey, int(management.Cloudflared)).
|
||||
|
||||
@@ -3,6 +3,7 @@ package connection
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -12,7 +13,6 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/sync/errgroup"
|
||||
@@ -65,7 +65,7 @@ func NewTunnelConnection(
|
||||
streamWriteTimeout time.Duration,
|
||||
gracePeriod time.Duration,
|
||||
logger *zerolog.Logger,
|
||||
) (TunnelConnection, error) {
|
||||
) TunnelConnection {
|
||||
return &quicConnection{
|
||||
conn: conn,
|
||||
logger: logger,
|
||||
@@ -77,10 +77,11 @@ func NewTunnelConnection(
|
||||
rpcTimeout: rpcTimeout,
|
||||
streamWriteTimeout: streamWriteTimeout,
|
||||
gracePeriod: gracePeriod,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Serve starts a QUIC connection that begins accepting streams.
|
||||
// Returning a nil error means cloudflared will exit for good and will not attempt to reconnect.
|
||||
func (q *quicConnection) Serve(ctx context.Context) error {
|
||||
// The edge assumes the first stream is used for the control plane
|
||||
controlStream, err := q.conn.OpenStream()
|
||||
@@ -88,16 +89,16 @@ func (q *quicConnection) Serve(ctx context.Context) error {
|
||||
return fmt.Errorf("failed to open a registration control stream: %w", err)
|
||||
}
|
||||
|
||||
// If either goroutine returns nil error, we rely on this cancellation to make sure the other goroutine exits
|
||||
// as fast as possible as well. Nil error means we want to exit for good (caller code won't retry serving this
|
||||
// connection).
|
||||
// If either goroutine returns a non nil error, then the error group cancels the context, thus also canceling the
|
||||
// other goroutine as fast as possible.
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
// other goroutines. We enforce returning a not-nil error for each function started in the errgroup by logging
|
||||
// the error returned and returning a custom error type instead.
|
||||
errGroup, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
// In the future, if cloudflared can autonomously push traffic to the edge, we have to make sure the control
|
||||
// stream is already fully registered before the other goroutines can proceed.
|
||||
// Close the quic connection if any of the following routines return from the errgroup (regardless of their error)
|
||||
// because they are no longer processing requests for the connection.
|
||||
defer q.Close()
|
||||
|
||||
// Start the control stream routine
|
||||
errGroup.Go(func() error {
|
||||
// err is equal to nil if we exit due to unregistration. If that happens we want to wait the full
|
||||
// amount of the grace period, allowing requests to finish before we cancel the context, which will
|
||||
@@ -114,16 +115,26 @@ func (q *quicConnection) Serve(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
return err
|
||||
if err != nil {
|
||||
q.logger.Error().Err(err).Msg("failed to serve the control stream")
|
||||
}
|
||||
return &ControlStreamError{}
|
||||
})
|
||||
// Start the accept stream loop routine
|
||||
errGroup.Go(func() error {
|
||||
defer cancel()
|
||||
return q.acceptStream(ctx)
|
||||
err := q.acceptStream(ctx)
|
||||
if err != nil {
|
||||
q.logger.Error().Err(err).Msg("failed to accept incoming stream requests")
|
||||
}
|
||||
return &StreamListenerError{}
|
||||
})
|
||||
// Start the datagram handler routine
|
||||
errGroup.Go(func() error {
|
||||
defer cancel()
|
||||
return q.datagramHandler.Serve(ctx)
|
||||
err := q.datagramHandler.Serve(ctx)
|
||||
if err != nil {
|
||||
q.logger.Error().Err(err).Msg("failed to run the datagram handler")
|
||||
}
|
||||
return &DatagramManagerError{}
|
||||
})
|
||||
|
||||
return errGroup.Wait()
|
||||
@@ -140,7 +151,6 @@ func (q *quicConnection) Close() {
|
||||
}
|
||||
|
||||
func (q *quicConnection) acceptStream(ctx context.Context) error {
|
||||
defer q.Close()
|
||||
for {
|
||||
quicStream, err := q.conn.AcceptStream(ctx)
|
||||
if err != nil {
|
||||
@@ -230,7 +240,7 @@ func (q *quicConnection) dispatchRequest(ctx context.Context, stream *rpcquic.Re
|
||||
ConnIndex: q.connIndex,
|
||||
}), rwa.connectResponseSent
|
||||
default:
|
||||
return errors.Errorf("unsupported error type: %s", request.Type), false
|
||||
return fmt.Errorf("unsupported error type: %s", request.Type), false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"golang.org/x/net/nettest"
|
||||
|
||||
"github.com/cloudflare/cloudflared/client"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
cfdflow "github.com/cloudflare/cloudflared/flow"
|
||||
|
||||
"github.com/cloudflare/cloudflared/datagramsession"
|
||||
@@ -823,6 +824,15 @@ func testTunnelConnection(t *testing.T, serverAddr netip.AddrPort, index uint8)
|
||||
sessionManager := datagramsession.NewManager(&log, datagramMuxer.SendToSession, sessionDemuxChan)
|
||||
var connIndex uint8 = 0
|
||||
packetRouter := ingress.NewPacketRouter(nil, datagramMuxer, connIndex, &log)
|
||||
testDefaultDialer := ingress.NewDialer(ingress.WarpRoutingConfig{
|
||||
ConnectTimeout: config.CustomDuration{Duration: 1 * time.Second},
|
||||
TCPKeepAlive: config.CustomDuration{Duration: 15 * time.Second},
|
||||
MaxActiveFlows: 0,
|
||||
})
|
||||
originDialer := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 1 * time.Second,
|
||||
}, &log)
|
||||
|
||||
datagramConn := &datagramV2Connection{
|
||||
conn,
|
||||
@@ -830,13 +840,14 @@ func testTunnelConnection(t *testing.T, serverAddr netip.AddrPort, index uint8)
|
||||
sessionManager,
|
||||
cfdflow.NewLimiter(0),
|
||||
datagramMuxer,
|
||||
originDialer,
|
||||
packetRouter,
|
||||
15 * time.Second,
|
||||
0 * time.Second,
|
||||
&log,
|
||||
}
|
||||
|
||||
tunnelConn, err := NewTunnelConnection(
|
||||
tunnelConn := NewTunnelConnection(
|
||||
ctx,
|
||||
conn,
|
||||
index,
|
||||
@@ -849,7 +860,6 @@ func testTunnelConnection(t *testing.T, serverAddr netip.AddrPort, index uint8)
|
||||
0*time.Second,
|
||||
&log,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
return tunnelConn, datagramConn
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
pkgerrors "github.com/pkg/errors"
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -32,6 +34,10 @@ const (
|
||||
demuxChanCapacity = 16
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidDestinationIP = errors.New("unable to parse destination IP")
|
||||
)
|
||||
|
||||
// DatagramSessionHandler is a service that can serve datagrams for a connection and handle sessions from incoming
|
||||
// connection streams.
|
||||
type DatagramSessionHandler interface {
|
||||
@@ -51,7 +57,10 @@ type datagramV2Connection struct {
|
||||
|
||||
// datagramMuxer mux/demux datagrams from quic connection
|
||||
datagramMuxer *cfdquic.DatagramMuxerV2
|
||||
packetRouter *ingress.PacketRouter
|
||||
// originDialer is the origin dialer for UDP requests
|
||||
originDialer ingress.OriginUDPDialer
|
||||
// packetRouter acts as the origin router for ICMP requests
|
||||
packetRouter *ingress.PacketRouter
|
||||
|
||||
rpcTimeout time.Duration
|
||||
streamWriteTimeout time.Duration
|
||||
@@ -61,6 +70,7 @@ type datagramV2Connection struct {
|
||||
|
||||
func NewDatagramV2Connection(ctx context.Context,
|
||||
conn quic.Connection,
|
||||
originDialer ingress.OriginUDPDialer,
|
||||
icmpRouter ingress.ICMPRouter,
|
||||
index uint8,
|
||||
rpcTimeout time.Duration,
|
||||
@@ -79,6 +89,7 @@ func NewDatagramV2Connection(ctx context.Context,
|
||||
sessionManager: sessionManager,
|
||||
flowLimiter: flowLimiter,
|
||||
datagramMuxer: datagramMuxer,
|
||||
originDialer: originDialer,
|
||||
packetRouter: packetRouter,
|
||||
rpcTimeout: rpcTimeout,
|
||||
streamWriteTimeout: streamWriteTimeout,
|
||||
@@ -87,24 +98,17 @@ func NewDatagramV2Connection(ctx context.Context,
|
||||
}
|
||||
|
||||
func (d *datagramV2Connection) Serve(ctx context.Context) error {
|
||||
// If either goroutine returns nil error, we rely on this cancellation to make sure the other goroutine exits
|
||||
// as fast as possible as well. Nil error means we want to exit for good (caller code won't retry serving this
|
||||
// connection).
|
||||
// If either goroutine returns a non nil error, then the error group cancels the context, thus also canceling the
|
||||
// other goroutine as fast as possible.
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
// If either goroutine from the errgroup returns at all (error or nil), we rely on its cancellation to make sure
|
||||
// the other goroutines as well.
|
||||
errGroup, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
errGroup.Go(func() error {
|
||||
defer cancel()
|
||||
return d.sessionManager.Serve(ctx)
|
||||
})
|
||||
errGroup.Go(func() error {
|
||||
defer cancel()
|
||||
return d.datagramMuxer.ServeReceive(ctx)
|
||||
})
|
||||
errGroup.Go(func() error {
|
||||
defer cancel()
|
||||
return d.packetRouter.Serve(ctx)
|
||||
})
|
||||
|
||||
@@ -128,12 +132,29 @@ func (q *datagramV2Connection) RegisterUdpSession(ctx context.Context, sessionID
|
||||
tracing.EndWithErrorStatus(registerSpan, err)
|
||||
return nil, err
|
||||
}
|
||||
// We need to force the net.IP to IPv4 (if it's an IPv4 address) otherwise the net.IP conversion from capnp
|
||||
// will be a IPv4-mapped-IPv6 address.
|
||||
// In the case that the address is IPv6 we leave it untouched and parse it as normal.
|
||||
ip := dstIP.To4()
|
||||
if ip == nil {
|
||||
ip = dstIP
|
||||
}
|
||||
// Parse the dstIP and dstPort into a netip.AddrPort
|
||||
// This should never fail because the IP was already parsed as a valid net.IP
|
||||
destAddr, ok := netip.AddrFromSlice(ip)
|
||||
if !ok {
|
||||
log.Err(errInvalidDestinationIP).Msgf("Failed to parse destination proxy IP: %s", ip)
|
||||
tracing.EndWithErrorStatus(registerSpan, errInvalidDestinationIP)
|
||||
q.flowLimiter.Release()
|
||||
return nil, errInvalidDestinationIP
|
||||
}
|
||||
dstAddrPort := netip.AddrPortFrom(destAddr, dstPort)
|
||||
|
||||
// Each session is a series of datagram from an eyeball to a dstIP:dstPort.
|
||||
// (src port, dst IP, dst port) uniquely identifies a session, so it needs a dedicated connected socket.
|
||||
originProxy, err := ingress.DialUDP(dstIP, dstPort)
|
||||
originProxy, err := q.originDialer.DialUDP(dstAddrPort)
|
||||
if err != nil {
|
||||
log.Err(err).Msgf("Failed to create udp proxy to %s:%d", dstIP, dstPort)
|
||||
log.Err(err).Msgf("Failed to create udp proxy to %s", dstAddrPort)
|
||||
tracing.EndWithErrorStatus(registerSpan, err)
|
||||
q.flowLimiter.Release()
|
||||
return nil, err
|
||||
|
||||
@@ -84,6 +84,7 @@ func TestRateLimitOnNewDatagramV2UDPSession(t *testing.T) {
|
||||
t.Context(),
|
||||
conn,
|
||||
nil,
|
||||
nil,
|
||||
0,
|
||||
0*time.Second,
|
||||
0*time.Second,
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
const (
|
||||
logFieldOriginCertPath = "originCertPath"
|
||||
FedEndpoint = "fed"
|
||||
FedRampBaseApiURL = "https://api.fed.cloudflare.com/client/v4"
|
||||
FedRampHostname = "management.fed.argotunnel.com"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
@@ -21,6 +23,10 @@ func (c User) AccountID() string {
|
||||
return c.cert.AccountID
|
||||
}
|
||||
|
||||
func (c User) Endpoint() string {
|
||||
return c.cert.Endpoint
|
||||
}
|
||||
|
||||
func (c User) ZoneID() string {
|
||||
return c.cert.ZoneID
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ const (
|
||||
FeaturePostQuantum = "postquantum"
|
||||
FeatureQUICSupportEOF = "support_quic_eof"
|
||||
FeatureManagementLogs = "management_logs"
|
||||
FeatureDatagramV3_1 = "support_datagram_v3_1"
|
||||
FeatureDatagramV3_2 = "support_datagram_v3_2"
|
||||
|
||||
DeprecatedFeatureDatagramV3 = "support_datagram_v3" // Deprecated: TUN-9291
|
||||
DeprecatedFeatureDatagramV3 = "support_datagram_v3" // Deprecated: TUN-9291
|
||||
DeprecatedFeatureDatagramV3_1 = "support_datagram_v3_1" // Deprecated: TUN-9883
|
||||
)
|
||||
|
||||
var defaultFeatures = []string{
|
||||
@@ -26,6 +27,7 @@ var defaultFeatures = []string{
|
||||
// List of features that are no longer in-use.
|
||||
var deprecatedFeatures = []string{
|
||||
DeprecatedFeatureDatagramV3,
|
||||
DeprecatedFeatureDatagramV3_1,
|
||||
}
|
||||
|
||||
// Features set by user provided flags
|
||||
@@ -58,7 +60,7 @@ const (
|
||||
// DatagramV2 is the currently supported datagram protocol for UDP and ICMP packets
|
||||
DatagramV2 DatagramVersion = FeatureDatagramV2
|
||||
// DatagramV3 is a new datagram protocol for UDP and ICMP packets. It is not backwards compatible with datagram v2.
|
||||
DatagramV3 DatagramVersion = FeatureDatagramV3_1
|
||||
DatagramV3 DatagramVersion = FeatureDatagramV3_2
|
||||
)
|
||||
|
||||
// Remove any duplicate features from the list and remove deprecated features
|
||||
|
||||
@@ -23,9 +23,10 @@ const (
|
||||
// If the TXT record is missing a key, the field will unmarshal to the default Go value
|
||||
|
||||
type featuresRecord struct {
|
||||
DatagramV3Percentage uint32 `json:"dv3_1"`
|
||||
DatagramV3Percentage uint32 `json:"dv3_2"`
|
||||
|
||||
// DatagramV3Percentage int32 `json:"dv3"` // Removed in TUN-9291
|
||||
// DatagramV3Percentage uint32 `json:"dv3_1"` // Removed in TUN-9883
|
||||
// PostQuantumPercentage int32 `json:"pq"` // Removed in TUN-7970
|
||||
}
|
||||
|
||||
@@ -105,7 +106,7 @@ func (fs *featureSelector) postQuantumMode() PostQuantumMode {
|
||||
|
||||
func (fs *featureSelector) datagramVersion() DatagramVersion {
|
||||
// If user provides the feature via the cli, we take it as priority over remote feature evaluation
|
||||
if slices.Contains(fs.cliFeatures, FeatureDatagramV3_1) {
|
||||
if slices.Contains(fs.cliFeatures, FeatureDatagramV3_2) {
|
||||
return DatagramV3
|
||||
}
|
||||
// If the user specifies DatagramV2, we also take that over remote
|
||||
|
||||
@@ -22,15 +22,15 @@ func TestUnmarshalFeaturesRecord(t *testing.T) {
|
||||
expectedPercentage uint32
|
||||
}{
|
||||
{
|
||||
record: []byte(`{"dv3_1":0}`),
|
||||
record: []byte(`{"dv3_2":0}`),
|
||||
expectedPercentage: 0,
|
||||
},
|
||||
{
|
||||
record: []byte(`{"dv3_1":39}`),
|
||||
record: []byte(`{"dv3_2":39}`),
|
||||
expectedPercentage: 39,
|
||||
},
|
||||
{
|
||||
record: []byte(`{"dv3_1":100}`),
|
||||
record: []byte(`{"dv3_2":100}`),
|
||||
expectedPercentage: 100,
|
||||
},
|
||||
{
|
||||
@@ -40,7 +40,7 @@ func TestUnmarshalFeaturesRecord(t *testing.T) {
|
||||
record: []byte(`{"kyber":768}`), // Unmarshal to default struct if key is not present
|
||||
},
|
||||
{
|
||||
record: []byte(`{"pq": 101,"dv3":100}`), // Expired keys don't unmarshal to anything
|
||||
record: []byte(`{"pq": 101,"dv3":100,"dv3_1":100}`), // Expired keys don't unmarshal to anything
|
||||
},
|
||||
}
|
||||
|
||||
@@ -111,10 +111,10 @@ func TestFeaturePrecedenceEvaluationDatagramVersion(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "user_specified_v3",
|
||||
cli: []string{FeatureDatagramV3_1},
|
||||
cli: []string{FeatureDatagramV3_2},
|
||||
remote: featuresRecord{},
|
||||
expectedFeatures: dedupAndRemoveFeatures(append(defaultFeatures, FeatureDatagramV3_1)),
|
||||
expectedVersion: FeatureDatagramV3_1,
|
||||
expectedFeatures: dedupAndRemoveFeatures(append(defaultFeatures, FeatureDatagramV3_2)),
|
||||
expectedVersion: FeatureDatagramV3_2,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -150,6 +150,12 @@ func TestDeprecatedFeaturesRemoved(t *testing.T) {
|
||||
remote: featuresRecord{},
|
||||
expectedFeatures: defaultFeatures,
|
||||
},
|
||||
{
|
||||
name: "support_datagram_v3_1",
|
||||
cli: []string{DeprecatedFeatureDatagramV3_1},
|
||||
remote: featuresRecord{},
|
||||
expectedFeatures: defaultFeatures,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -10,7 +10,7 @@ require (
|
||||
github.com/fortytw2/leaktest v1.3.0
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/getsentry/sentry-go v0.16.0
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/go-chi/chi/v5 v5.2.2
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-jose/go-jose/v4 v4.1.0
|
||||
github.com/gobwas/ws v1.2.1
|
||||
|
||||
@@ -58,8 +58,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
|
||||
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
|
||||
+1
-1
@@ -317,7 +317,7 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq
|
||||
return Ingress{}, err
|
||||
}
|
||||
if access.Required {
|
||||
verifier := middleware.NewJWTValidator(access.TeamName, "", access.AudTag)
|
||||
verifier := middleware.NewJWTValidator(access.TeamName, access.Environment, access.AudTag)
|
||||
handlers = append(handlers, verifier)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
|
||||
"github.com/cloudflare/cloudflared/credentials"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -13,7 +15,8 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
cloudflareAccessCertsURL = "https://%s.cloudflareaccess.com"
|
||||
cloudflareAccessCertsURL = "https://%s.cloudflareaccess.com"
|
||||
cloudflareAccessFedCertsURL = "https://%s.fed.cloudflareaccess.com"
|
||||
)
|
||||
|
||||
// JWTValidator is an implementation of Verifier that validates access based JWT tokens.
|
||||
@@ -22,10 +25,14 @@ type JWTValidator struct {
|
||||
audTags []string
|
||||
}
|
||||
|
||||
func NewJWTValidator(teamName string, certsURL string, audTags []string) *JWTValidator {
|
||||
if certsURL == "" {
|
||||
func NewJWTValidator(teamName string, environment string, audTags []string) *JWTValidator {
|
||||
var certsURL string
|
||||
if environment == credentials.FedEndpoint {
|
||||
certsURL = fmt.Sprintf(cloudflareAccessFedCertsURL, teamName)
|
||||
} else {
|
||||
certsURL = fmt.Sprintf(cloudflareAccessCertsURL, teamName)
|
||||
}
|
||||
|
||||
certsEndpoint := fmt.Sprintf("%s/cdn-cgi/access/certs", certsURL)
|
||||
|
||||
config := &oidc.Config{
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
type OriginConnection interface {
|
||||
// Stream should generally be implemented as a bidirectional io.Copy.
|
||||
Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger)
|
||||
Close()
|
||||
Close() error
|
||||
}
|
||||
|
||||
type streamHandlerFunc func(originConn io.ReadWriter, remoteConn net.Conn, log *zerolog.Logger)
|
||||
@@ -48,16 +48,7 @@ func (tc *tcpConnection) Write(b []byte) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
nBytes, err := tc.Conn.Write(b)
|
||||
if err != nil {
|
||||
tc.logger.Err(err).Msg("Error writing to the TCP connection")
|
||||
}
|
||||
|
||||
return nBytes, err
|
||||
}
|
||||
|
||||
func (tc *tcpConnection) Close() {
|
||||
tc.Conn.Close()
|
||||
return tc.Conn.Write(b)
|
||||
}
|
||||
|
||||
// tcpOverWSConnection is an OriginConnection that streams to TCP over WS.
|
||||
@@ -75,8 +66,8 @@ func (wc *tcpOverWSConnection) Stream(ctx context.Context, tunnelConn io.ReadWri
|
||||
wsConn.Close()
|
||||
}
|
||||
|
||||
func (wc *tcpOverWSConnection) Close() {
|
||||
wc.conn.Close()
|
||||
func (wc *tcpOverWSConnection) Close() error {
|
||||
return wc.conn.Close()
|
||||
}
|
||||
|
||||
// socksProxyOverWSConnection is an OriginConnection that streams SOCKS connections over WS.
|
||||
@@ -95,5 +86,6 @@ func (sp *socksProxyOverWSConnection) Stream(ctx context.Context, tunnelConn io.
|
||||
wsConn.Close()
|
||||
}
|
||||
|
||||
func (sp *socksProxyOverWSConnection) Close() {
|
||||
func (sp *socksProxyOverWSConnection) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package ingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const writeDeadlineUDP = 200 * time.Millisecond
|
||||
|
||||
// OriginTCPDialer provides a TCP dial operation to a requested address.
|
||||
type OriginTCPDialer interface {
|
||||
DialTCP(ctx context.Context, addr netip.AddrPort) (net.Conn, error)
|
||||
}
|
||||
|
||||
// OriginUDPDialer provides a UDP dial operation to a requested address.
|
||||
type OriginUDPDialer interface {
|
||||
DialUDP(addr netip.AddrPort) (net.Conn, error)
|
||||
}
|
||||
|
||||
// OriginDialer provides both TCP and UDP dial operations to an address.
|
||||
type OriginDialer interface {
|
||||
OriginTCPDialer
|
||||
OriginUDPDialer
|
||||
}
|
||||
|
||||
type OriginConfig struct {
|
||||
// The default Dialer used if no reserved services are found for an origin request.
|
||||
DefaultDialer OriginDialer
|
||||
// Timeout on write operations for TCP connections to the origin.
|
||||
TCPWriteTimeout time.Duration
|
||||
}
|
||||
|
||||
// OriginDialerService provides a proxy TCP and UDP dialer to origin services while allowing reserved
|
||||
// services to be provided. These reserved services are assigned to specific [netip.AddrPort]s
|
||||
// and provide their own [OriginDialer]'s to handle origin dialing per protocol.
|
||||
type OriginDialerService struct {
|
||||
// Reserved TCP services for reserved AddrPort values
|
||||
reservedTCPServices map[netip.AddrPort]OriginTCPDialer
|
||||
// Reserved UDP services for reserved AddrPort values
|
||||
reservedUDPServices map[netip.AddrPort]OriginUDPDialer
|
||||
// The default Dialer used if no reserved services are found for an origin request
|
||||
defaultDialer OriginDialer
|
||||
defaultDialerM sync.RWMutex
|
||||
// Write timeout for TCP connections
|
||||
writeTimeout time.Duration
|
||||
|
||||
logger *zerolog.Logger
|
||||
}
|
||||
|
||||
func NewOriginDialer(config OriginConfig, logger *zerolog.Logger) *OriginDialerService {
|
||||
return &OriginDialerService{
|
||||
reservedTCPServices: map[netip.AddrPort]OriginTCPDialer{},
|
||||
reservedUDPServices: map[netip.AddrPort]OriginUDPDialer{},
|
||||
defaultDialer: config.DefaultDialer,
|
||||
writeTimeout: config.TCPWriteTimeout,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// AddReservedService adds a reserved virtual service to dial to.
|
||||
// Not locked and expected to be initialized before calling first dial and not afterwards.
|
||||
func (d *OriginDialerService) AddReservedService(service OriginDialer, addrs []netip.AddrPort) {
|
||||
for _, addr := range addrs {
|
||||
d.reservedTCPServices[addr] = service
|
||||
d.reservedUDPServices[addr] = service
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateDefaultDialer updates the default dialer.
|
||||
func (d *OriginDialerService) UpdateDefaultDialer(dialer *Dialer) {
|
||||
d.defaultDialerM.Lock()
|
||||
defer d.defaultDialerM.Unlock()
|
||||
d.defaultDialer = dialer
|
||||
}
|
||||
|
||||
// DialTCP will perform a dial TCP to the requested addr.
|
||||
func (d *OriginDialerService) DialTCP(ctx context.Context, addr netip.AddrPort) (net.Conn, error) {
|
||||
conn, err := d.dialTCP(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Assign the write timeout for the TCP operations
|
||||
return &tcpConnection{
|
||||
Conn: conn,
|
||||
writeTimeout: d.writeTimeout,
|
||||
logger: d.logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *OriginDialerService) dialTCP(ctx context.Context, addr netip.AddrPort) (net.Conn, error) {
|
||||
// Check to see if any reserved services are available for this addr and call their dialer instead.
|
||||
if dialer, ok := d.reservedTCPServices[addr]; ok {
|
||||
return dialer.DialTCP(ctx, addr)
|
||||
}
|
||||
d.defaultDialerM.RLock()
|
||||
dialer := d.defaultDialer
|
||||
d.defaultDialerM.RUnlock()
|
||||
return dialer.DialTCP(ctx, addr)
|
||||
}
|
||||
|
||||
// DialUDP will perform a dial UDP to the requested addr.
|
||||
func (d *OriginDialerService) DialUDP(addr netip.AddrPort) (net.Conn, error) {
|
||||
// Check to see if any reserved services are available for this addr and call their dialer instead.
|
||||
if dialer, ok := d.reservedUDPServices[addr]; ok {
|
||||
return dialer.DialUDP(addr)
|
||||
}
|
||||
d.defaultDialerM.RLock()
|
||||
dialer := d.defaultDialer
|
||||
d.defaultDialerM.RUnlock()
|
||||
return dialer.DialUDP(addr)
|
||||
}
|
||||
|
||||
type Dialer struct {
|
||||
Dialer net.Dialer
|
||||
}
|
||||
|
||||
func NewDialer(config WarpRoutingConfig) *Dialer {
|
||||
return &Dialer{
|
||||
Dialer: net.Dialer{
|
||||
Timeout: config.ConnectTimeout.Duration,
|
||||
KeepAlive: config.TCPKeepAlive.Duration,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dialer) DialTCP(ctx context.Context, dest netip.AddrPort) (net.Conn, error) {
|
||||
conn, err := d.Dialer.DialContext(ctx, "tcp", dest.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to dial tcp to origin %s: %w", dest, err)
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (d *Dialer) DialUDP(dest netip.AddrPort) (net.Conn, error) {
|
||||
conn, err := d.Dialer.Dial("udp", dest.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to dial udp to origin %s: %w", dest, err)
|
||||
}
|
||||
return &writeDeadlineConn{
|
||||
Conn: conn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// writeDeadlineConn is a wrapper around a net.Conn that sets a write deadline of 200ms.
|
||||
// This is to prevent the socket from blocking on the write operation if it were to occur. However,
|
||||
// we typically never expect this to occur except under high load or kernel issues.
|
||||
type writeDeadlineConn struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (w *writeDeadlineConn) Write(b []byte) (int, error) {
|
||||
if err := w.SetWriteDeadline(time.Now().Add(writeDeadlineUDP)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return w.Conn.Write(b)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package ingress
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type UDPProxy interface {
|
||||
io.ReadWriteCloser
|
||||
LocalAddr() net.Addr
|
||||
}
|
||||
|
||||
type udpProxy struct {
|
||||
*net.UDPConn
|
||||
}
|
||||
|
||||
func DialUDP(dstIP net.IP, dstPort uint16) (UDPProxy, error) {
|
||||
dstAddr := &net.UDPAddr{
|
||||
IP: dstIP,
|
||||
Port: int(dstPort),
|
||||
}
|
||||
|
||||
// We use nil as local addr to force runtime to find the best suitable local address IP given the destination
|
||||
// address as context.
|
||||
udpConn, err := net.DialUDP("udp", nil, dstAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create UDP proxy to origin (%v:%v): %w", dstIP, dstPort, err)
|
||||
}
|
||||
|
||||
return &udpProxy{udpConn}, nil
|
||||
}
|
||||
|
||||
func DialUDPAddrPort(dest netip.AddrPort) (*net.UDPConn, error) {
|
||||
addr := net.UDPAddrFromAddrPort(dest)
|
||||
|
||||
// We use nil as local addr to force runtime to find the best suitable local address IP given the destination
|
||||
// address as context.
|
||||
udpConn, err := net.DialUDP("udp", nil, addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to dial udp to origin %s: %w", dest, err)
|
||||
}
|
||||
|
||||
return udpConn, nil
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package origins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
)
|
||||
|
||||
const (
|
||||
// We need a DNS record:
|
||||
// 1. That will be around for as long as cloudflared is
|
||||
// 2. That Cloudflare controls: to allow us to make changes if needed
|
||||
// 3. That is an external record to a typical customer's network: enforcing that the DNS request go to the
|
||||
// local DNS resolver over any local /etc/host configurations setup.
|
||||
// 4. That cloudflared would normally query: ensuring that users with a positive security model for DNS queries
|
||||
// don't need to adjust anything.
|
||||
//
|
||||
// This hostname is one that used during the edge discovery process and as such satisfies the above constraints.
|
||||
defaultLookupHost = "region1.v2.argotunnel.com"
|
||||
defaultResolverPort uint16 = 53
|
||||
|
||||
// We want the refresh time to be short to accommodate DNS resolver changes locally, but not too frequent as to
|
||||
// shuffle the resolver if multiple are configured.
|
||||
refreshFreq = 5 * time.Minute
|
||||
refreshTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
// Virtual DNS service address
|
||||
VirtualDNSServiceAddr = netip.AddrPortFrom(netip.MustParseAddr("2606:4700:0cf1:2000:0000:0000:0000:0001"), 53)
|
||||
|
||||
defaultResolverAddr = netip.AddrPortFrom(netip.MustParseAddr("127.0.0.1"), defaultResolverPort)
|
||||
)
|
||||
|
||||
type netDial func(network string, address string) (net.Conn, error)
|
||||
|
||||
// DNSResolverService will make DNS requests to the local DNS resolver via the Dial method.
|
||||
type DNSResolverService struct {
|
||||
addresses []netip.AddrPort
|
||||
addressesM sync.RWMutex
|
||||
static bool
|
||||
dialer ingress.OriginDialer
|
||||
resolver peekResolver
|
||||
logger *zerolog.Logger
|
||||
metrics Metrics
|
||||
}
|
||||
|
||||
func NewDNSResolverService(dialer ingress.OriginDialer, logger *zerolog.Logger, metrics Metrics) *DNSResolverService {
|
||||
return &DNSResolverService{
|
||||
addresses: []netip.AddrPort{defaultResolverAddr},
|
||||
dialer: dialer,
|
||||
resolver: &resolver{dialFunc: net.Dial},
|
||||
logger: logger,
|
||||
metrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
func NewStaticDNSResolverService(resolverAddrs []netip.AddrPort, dialer ingress.OriginDialer, logger *zerolog.Logger, metrics Metrics) *DNSResolverService {
|
||||
s := NewDNSResolverService(dialer, logger, metrics)
|
||||
s.addresses = resolverAddrs
|
||||
s.static = true
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *DNSResolverService) DialTCP(ctx context.Context, _ netip.AddrPort) (net.Conn, error) {
|
||||
s.metrics.IncrementDNSTCPRequests()
|
||||
dest := s.getAddress()
|
||||
// The dialer ignores the provided address because the request will instead go to the local DNS resolver.
|
||||
return s.dialer.DialTCP(ctx, dest)
|
||||
}
|
||||
|
||||
func (s *DNSResolverService) DialUDP(_ netip.AddrPort) (net.Conn, error) {
|
||||
s.metrics.IncrementDNSUDPRequests()
|
||||
dest := s.getAddress()
|
||||
// The dialer ignores the provided address because the request will instead go to the local DNS resolver.
|
||||
return s.dialer.DialUDP(dest)
|
||||
}
|
||||
|
||||
// StartRefreshLoop is a routine that is expected to run in the background to update the DNS local resolver if
|
||||
// adjusted while the cloudflared process is running.
|
||||
// Does not run when the resolver was provided with external resolver addresses via CLI.
|
||||
func (s *DNSResolverService) StartRefreshLoop(ctx context.Context) {
|
||||
if s.static {
|
||||
s.logger.Debug().Msgf("Canceled DNS local resolver refresh loop because static resolver addresses were provided: %s", s.addresses)
|
||||
return
|
||||
}
|
||||
// Call update first to load an address before handling traffic
|
||||
err := s.update(ctx)
|
||||
if err != nil {
|
||||
s.logger.Err(err).Msg("Failed to initialize DNS local resolver")
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.Tick(refreshFreq):
|
||||
err := s.update(ctx)
|
||||
if err != nil {
|
||||
s.logger.Err(err).Msg("Failed to refresh DNS local resolver")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DNSResolverService) update(ctx context.Context) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, refreshTimeout)
|
||||
defer cancel()
|
||||
// Make a standard DNS request to a well-known DNS record that will last a long time
|
||||
_, err := s.resolver.lookupNetIP(ctx, defaultLookupHost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate the address before updating internal reference
|
||||
_, address := s.resolver.addr()
|
||||
peekAddrPort, err := netip.ParseAddrPort(address)
|
||||
if err == nil {
|
||||
s.setAddress(peekAddrPort)
|
||||
return nil
|
||||
}
|
||||
// It's possible that the address didn't have an attached port, attempt to parse just the address and use
|
||||
// the default port 53
|
||||
peekAddr, err := netip.ParseAddr(address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.setAddress(netip.AddrPortFrom(peekAddr, defaultResolverPort))
|
||||
return nil
|
||||
}
|
||||
|
||||
// returns the address from the peekResolver or from the static addresses if provided.
|
||||
// If multiple addresses are provided in the static addresses pick one randomly.
|
||||
func (s *DNSResolverService) getAddress() netip.AddrPort {
|
||||
s.addressesM.RLock()
|
||||
defer s.addressesM.RUnlock()
|
||||
l := len(s.addresses)
|
||||
if l <= 0 {
|
||||
return defaultResolverAddr
|
||||
}
|
||||
if l == 1 {
|
||||
return s.addresses[0]
|
||||
}
|
||||
// Only initialize the random selection if there is more than one element in the list.
|
||||
var i int64 = 0
|
||||
r, err := rand.Int(rand.Reader, big.NewInt(int64(l)))
|
||||
// We ignore errors from crypto rand and use index 0; this should be extremely unlikely and the
|
||||
// list index doesn't need to be cryptographically secure, but linters insist.
|
||||
if err == nil {
|
||||
i = r.Int64()
|
||||
}
|
||||
return s.addresses[i]
|
||||
}
|
||||
|
||||
// lock and update the address used for the local DNS resolver
|
||||
func (s *DNSResolverService) setAddress(addr netip.AddrPort) {
|
||||
s.addressesM.Lock()
|
||||
defer s.addressesM.Unlock()
|
||||
if !slices.Contains(s.addresses, addr) {
|
||||
s.logger.Debug().Msgf("Updating DNS local resolver: %s", addr)
|
||||
}
|
||||
// We only store one address when reading the peekResolver, so we just replace the whole list.
|
||||
s.addresses = []netip.AddrPort{addr}
|
||||
}
|
||||
|
||||
type peekResolver interface {
|
||||
addr() (network string, address string)
|
||||
lookupNetIP(ctx context.Context, host string) ([]netip.Addr, error)
|
||||
}
|
||||
|
||||
// resolver is a shim that inspects the go runtime's DNS resolution process to capture the DNS resolver
|
||||
// address used to complete a DNS request.
|
||||
type resolver struct {
|
||||
network string
|
||||
address string
|
||||
dialFunc netDial
|
||||
}
|
||||
|
||||
func (r *resolver) addr() (network string, address string) {
|
||||
return r.network, r.address
|
||||
}
|
||||
|
||||
func (r *resolver) lookupNetIP(ctx context.Context, host string) ([]netip.Addr, error) {
|
||||
resolver := &net.Resolver{
|
||||
PreferGo: true,
|
||||
// Use the peekDial to inspect the results of the DNS resolver used during the LookupIPAddr call.
|
||||
Dial: r.peekDial,
|
||||
}
|
||||
return resolver.LookupNetIP(ctx, "ip", host)
|
||||
}
|
||||
|
||||
func (r *resolver) peekDial(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
r.network = network
|
||||
r.address = address
|
||||
return r.dialFunc(network, address)
|
||||
}
|
||||
|
||||
// NewDNSDialer creates a custom dialer for the DNS resolver service to utilize.
|
||||
func NewDNSDialer() *ingress.Dialer {
|
||||
return &ingress.Dialer{
|
||||
Dialer: net.Dialer{
|
||||
// We want short timeouts for the DNS requests
|
||||
Timeout: 5 * time.Second,
|
||||
// We do not want keep alive since the edge will not reuse TCP connections per request
|
||||
KeepAlive: -1,
|
||||
KeepAliveConfig: net.KeepAliveConfig{
|
||||
Enable: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package origins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func TestDNSResolver_DefaultResolver(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
service := NewDNSResolverService(NewDNSDialer(), &log, &noopMetrics{})
|
||||
mockResolver := &mockPeekResolver{
|
||||
address: "127.0.0.2:53",
|
||||
}
|
||||
service.resolver = mockResolver
|
||||
validateAddrs(t, []netip.AddrPort{defaultResolverAddr}, service.addresses)
|
||||
}
|
||||
|
||||
func TestStaticDNSResolver_DefaultResolver(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
addresses := []netip.AddrPort{netip.MustParseAddrPort("1.1.1.1:53"), netip.MustParseAddrPort("1.0.0.1:53")}
|
||||
service := NewStaticDNSResolverService(addresses, NewDNSDialer(), &log, &noopMetrics{})
|
||||
mockResolver := &mockPeekResolver{
|
||||
address: "127.0.0.2:53",
|
||||
}
|
||||
service.resolver = mockResolver
|
||||
validateAddrs(t, addresses, service.addresses)
|
||||
}
|
||||
|
||||
func TestDNSResolver_UpdateResolverAddress(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
service := NewDNSResolverService(NewDNSDialer(), &log, &noopMetrics{})
|
||||
|
||||
mockResolver := &mockPeekResolver{}
|
||||
service.resolver = mockResolver
|
||||
|
||||
tests := []struct {
|
||||
addr string
|
||||
expected netip.AddrPort
|
||||
}{
|
||||
{"127.0.0.2:53", netip.MustParseAddrPort("127.0.0.2:53")},
|
||||
// missing port should be added (even though this is unlikely to happen)
|
||||
{"127.0.0.3", netip.MustParseAddrPort("127.0.0.3:53")},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
mockResolver.address = test.addr
|
||||
// Update the resolver address
|
||||
err := service.update(t.Context())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
// Validate expected
|
||||
validateAddrs(t, []netip.AddrPort{test.expected}, service.addresses)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticDNSResolver_RefreshLoopExits(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
addresses := []netip.AddrPort{netip.MustParseAddrPort("1.1.1.1:53"), netip.MustParseAddrPort("1.0.0.1:53")}
|
||||
service := NewStaticDNSResolverService(addresses, NewDNSDialer(), &log, &noopMetrics{})
|
||||
|
||||
mockResolver := &mockPeekResolver{
|
||||
address: "127.0.0.2:53",
|
||||
}
|
||||
service.resolver = mockResolver
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
go service.StartRefreshLoop(ctx)
|
||||
|
||||
// Wait for the refresh loop to end _and_ not update the addresses
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// Validate expected
|
||||
validateAddrs(t, addresses, service.addresses)
|
||||
}
|
||||
|
||||
func TestDNSResolver_UpdateResolverAddressInvalid(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
service := NewDNSResolverService(NewDNSDialer(), &log, &noopMetrics{})
|
||||
mockResolver := &mockPeekResolver{}
|
||||
service.resolver = mockResolver
|
||||
|
||||
invalidAddresses := []string{
|
||||
"999.999.999.999",
|
||||
"localhost",
|
||||
"255.255.255",
|
||||
}
|
||||
|
||||
for _, addr := range invalidAddresses {
|
||||
mockResolver.address = addr
|
||||
// Update the resolver address should not update for these invalid addresses
|
||||
err := service.update(t.Context())
|
||||
if err == nil {
|
||||
t.Error("service update should throw an error")
|
||||
}
|
||||
// Validate expected
|
||||
validateAddrs(t, []netip.AddrPort{defaultResolverAddr}, service.addresses)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSResolver_UpdateResolverErrorIgnored(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
service := NewDNSResolverService(NewDNSDialer(), &log, &noopMetrics{})
|
||||
resolverErr := errors.New("test resolver error")
|
||||
mockResolver := &mockPeekResolver{err: resolverErr}
|
||||
service.resolver = mockResolver
|
||||
|
||||
// Update the resolver address should not update when the resolver cannot complete the lookup
|
||||
err := service.update(t.Context())
|
||||
if err != resolverErr {
|
||||
t.Error("service update should throw an error")
|
||||
}
|
||||
// Validate expected
|
||||
validateAddrs(t, []netip.AddrPort{defaultResolverAddr}, service.addresses)
|
||||
}
|
||||
|
||||
func TestDNSResolver_DialUDPUsesResolvedAddress(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
mockDialer := &mockDialer{expected: defaultResolverAddr}
|
||||
service := NewDNSResolverService(mockDialer, &log, &noopMetrics{})
|
||||
mockResolver := &mockPeekResolver{}
|
||||
service.resolver = mockResolver
|
||||
|
||||
// Attempt a dial to 127.0.0.2:53 which should be ignored and instead resolve to 127.0.0.1:53
|
||||
_, err := service.DialUDP(netip.MustParseAddrPort("127.0.0.2:53"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSResolver_DialTCPUsesResolvedAddress(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
mockDialer := &mockDialer{expected: defaultResolverAddr}
|
||||
service := NewDNSResolverService(mockDialer, &log, &noopMetrics{})
|
||||
mockResolver := &mockPeekResolver{}
|
||||
service.resolver = mockResolver
|
||||
|
||||
// Attempt a dial to 127.0.0.2:53 which should be ignored and instead resolve to 127.0.0.1:53
|
||||
_, err := service.DialTCP(t.Context(), netip.MustParseAddrPort("127.0.0.2:53"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
type mockPeekResolver struct {
|
||||
err error
|
||||
address string
|
||||
}
|
||||
|
||||
func (r *mockPeekResolver) addr() (network, address string) {
|
||||
return "udp", r.address
|
||||
}
|
||||
|
||||
func (r *mockPeekResolver) lookupNetIP(ctx context.Context, host string) ([]netip.Addr, error) {
|
||||
// We can return an empty result as it doesn't matter as long as the lookup doesn't fail
|
||||
return []netip.Addr{}, r.err
|
||||
}
|
||||
|
||||
type mockDialer struct {
|
||||
expected netip.AddrPort
|
||||
}
|
||||
|
||||
func (d *mockDialer) DialTCP(ctx context.Context, addr netip.AddrPort) (net.Conn, error) {
|
||||
if d.expected != addr {
|
||||
return nil, errors.New("unexpected address dialed")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (d *mockDialer) DialUDP(addr netip.AddrPort) (net.Conn, error) {
|
||||
if d.expected != addr {
|
||||
return nil, errors.New("unexpected address dialed")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func validateAddrs(t *testing.T, expected []netip.AddrPort, actual []netip.AddrPort) {
|
||||
if len(actual) != len(expected) {
|
||||
t.Errorf("addresses should only contain one element: %s", actual)
|
||||
}
|
||||
for _, e := range expected {
|
||||
if !slices.Contains(actual, e) {
|
||||
t.Errorf("missing address: %s in %s", e, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package origins
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
namespace = "cloudflared"
|
||||
subsystem = "virtual_origins"
|
||||
)
|
||||
|
||||
type Metrics interface {
|
||||
IncrementDNSUDPRequests()
|
||||
IncrementDNSTCPRequests()
|
||||
}
|
||||
|
||||
type metrics struct {
|
||||
dnsResolverRequests *prometheus.CounterVec
|
||||
}
|
||||
|
||||
func (m *metrics) IncrementDNSUDPRequests() {
|
||||
m.dnsResolverRequests.WithLabelValues("udp").Inc()
|
||||
}
|
||||
|
||||
func (m *metrics) IncrementDNSTCPRequests() {
|
||||
m.dnsResolverRequests.WithLabelValues("tcp").Inc()
|
||||
}
|
||||
|
||||
func NewMetrics(registerer prometheus.Registerer) Metrics {
|
||||
m := &metrics{
|
||||
dnsResolverRequests: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "dns_requests_total",
|
||||
Help: "Total count of DNS requests that have been proxied to the virtual DNS resolver origin",
|
||||
}, []string{"protocol"}),
|
||||
}
|
||||
registerer.MustRegister(m.dnsResolverRequests)
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package origins
|
||||
|
||||
type noopMetrics struct{}
|
||||
|
||||
func (noopMetrics) IncrementDNSUDPRequests() {}
|
||||
func (noopMetrics) IncrementDNSTCPRequests() {}
|
||||
@@ -12,14 +12,7 @@ const (
|
||||
accessClaimsCtxKey ctxKey = iota
|
||||
)
|
||||
|
||||
const (
|
||||
connectorIDQuery = "connector_id"
|
||||
accessTokenQuery = "access_token"
|
||||
)
|
||||
|
||||
var (
|
||||
errMissingAccessToken = managementError{Code: 1001, Message: "missing access_token query parameter"}
|
||||
)
|
||||
var errMissingAccessToken = managementError{Code: 1001, Message: "missing access_token query parameter"}
|
||||
|
||||
// HTTP middleware setting the parsed access_token claims in the request context
|
||||
func ValidateAccessTokenQueryMiddleware(next http.Handler) http.Handler {
|
||||
@@ -30,7 +23,7 @@ func ValidateAccessTokenQueryMiddleware(next http.Handler) http.Handler {
|
||||
writeHTTPErrorResponse(w, errMissingAccessToken)
|
||||
return
|
||||
}
|
||||
token, err := parseToken(accessToken)
|
||||
token, err := ParseToken(accessToken)
|
||||
if err != nil {
|
||||
writeHTTPErrorResponse(w, errMissingAccessToken)
|
||||
return
|
||||
|
||||
+8
-1
@@ -7,9 +7,12 @@ import (
|
||||
"github.com/go-jose/go-jose/v4/jwt"
|
||||
)
|
||||
|
||||
const tunnelstoreFEDIssuer = "fed-tunnelstore"
|
||||
|
||||
type managementTokenClaims struct {
|
||||
Tunnel tunnel `json:"tun"`
|
||||
Actor actor `json:"actor"`
|
||||
jwt.Claims
|
||||
}
|
||||
|
||||
// VerifyTunnel compares the tun claim isn't empty
|
||||
@@ -37,7 +40,7 @@ func (t *actor) verify() bool {
|
||||
return t.ID != ""
|
||||
}
|
||||
|
||||
func parseToken(token string) (*managementTokenClaims, error) {
|
||||
func ParseToken(token string) (*managementTokenClaims, error) {
|
||||
jwt, err := jwt.ParseSigned(token, []jose.SignatureAlgorithm{jose.ES256})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed jwt: %v", err)
|
||||
@@ -54,3 +57,7 @@ func parseToken(token string) (*managementTokenClaims, error) {
|
||||
}
|
||||
return &claims, nil
|
||||
}
|
||||
|
||||
func (m *managementTokenClaims) IsFed() bool {
|
||||
return m.Issuer == tunnelstoreFEDIssuer
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
validToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjEifQ.eyJ0dW4iOnsiaWQiOiI3YjA5ODE0OS01MWZlLTRlZTUtYTY4Ny0zZTM3NDQ2NmVmYzciLCJhY2NvdW50X3RhZyI6ImNkMzkxZTljMDYyNmE4Zjc2Y2IxZjY3MGY2NTkxYjA1In0sImFjdG9yIjp7ImlkIjoiZGNhcnJAY2xvdWRmbGFyZS5jb20iLCJzdXBwb3J0IjpmYWxzZX0sInJlcyI6WyJsb2dzIl0sImV4cCI6MTY3NzExNzY5NiwiaWF0IjoxNjc3MTE0MDk2LCJpc3MiOiJ0dW5uZWxzdG9yZSJ9.mKenOdOy3Xi4O-grldFnAAemdlE9WajEpTDC_FwezXQTstWiRTLwU65P5jt4vNsIiZA4OJRq7bH-QYID9wf9NA"
|
||||
validToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjEifQ.eyJ0dW4iOnsiaWQiOiI3YjA5ODE0OS01MWZlLTRlZTUtYTY4Ny0zZTM3NDQ2NmVmYzciLCJhY2NvdW50X3RhZyI6ImNkMzkxZTljMDYyNmE4Zjc2Y2IxZjY3MGY2NTkxYjA1In0sImFjdG9yIjp7ImlkIjoiZGNhcnJAY2xvdWRmbGFyZS5jb20iLCJzdXBwb3J0IjpmYWxzZX0sInJlcyI6WyJsb2dzIl0sImV4cCI6MTY3NzExNzY5NiwiaWF0IjoxNjc3MTE0MDk2LCJpc3MiOiJ0dW5uZWxzdG9yZSJ9.mKenOdOy3Xi4O-grldFnAAemdlE9WajEpTDC_FwezXQTstWiRTLwU65P5jt4vNsIiZA4OJRq7bH-QYID9wf9NA" // nolint: gosec
|
||||
|
||||
accountTag = "cd391e9c0626a8f76cb1f670f6591b05"
|
||||
tunnelID = "7b098149-51fe-4ee5-a687-3e374466efc7"
|
||||
@@ -105,12 +105,12 @@ func TestParseToken(t *testing.T) {
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
jwt := signToken(t, test.claims, key)
|
||||
claims, err := parseToken(jwt)
|
||||
claims, err := ParseToken(jwt)
|
||||
if test.err != nil {
|
||||
require.EqualError(t, err, test.err.Error())
|
||||
return
|
||||
}
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.claims, *claims)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package orchestration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
@@ -20,9 +19,9 @@ type newLocalConfig struct {
|
||||
|
||||
// Config is the original config as read and parsed by cloudflared.
|
||||
type Config struct {
|
||||
Ingress *ingress.Ingress
|
||||
WarpRouting ingress.WarpRoutingConfig
|
||||
WriteTimeout time.Duration
|
||||
Ingress *ingress.Ingress
|
||||
WarpRouting ingress.WarpRoutingConfig
|
||||
OriginDialerService *ingress.OriginDialerService
|
||||
|
||||
// Extra settings used to configure this instance but that are not eligible for remotely management
|
||||
// ie. (--protocol, --loglevel, ...)
|
||||
|
||||
@@ -38,7 +38,9 @@ type Orchestrator struct {
|
||||
tags []pogs.Tag
|
||||
// flowLimiter tracks active sessions across the tunnel and limits new sessions if they are above the limit.
|
||||
flowLimiter cfdflow.Limiter
|
||||
log *zerolog.Logger
|
||||
// Origin dialer service to manage egress socket dialing.
|
||||
originDialerService *ingress.OriginDialerService
|
||||
log *zerolog.Logger
|
||||
|
||||
// orchestrator must not handle any more updates after shutdownC is closed
|
||||
shutdownC <-chan struct{}
|
||||
@@ -50,18 +52,20 @@ func NewOrchestrator(ctx context.Context,
|
||||
config *Config,
|
||||
tags []pogs.Tag,
|
||||
internalRules []ingress.Rule,
|
||||
log *zerolog.Logger) (*Orchestrator, error) {
|
||||
log *zerolog.Logger,
|
||||
) (*Orchestrator, error) {
|
||||
o := &Orchestrator{
|
||||
// Lowest possible version, any remote configuration will have version higher than this
|
||||
// Starting at -1 allows a configuration migration (local to remote) to override the current configuration as it
|
||||
// will start at version 0.
|
||||
currentVersion: -1,
|
||||
internalRules: internalRules,
|
||||
config: config,
|
||||
tags: tags,
|
||||
flowLimiter: cfdflow.NewLimiter(config.WarpRouting.MaxActiveFlows),
|
||||
log: log,
|
||||
shutdownC: ctx.Done(),
|
||||
currentVersion: -1,
|
||||
internalRules: internalRules,
|
||||
config: config,
|
||||
tags: tags,
|
||||
flowLimiter: cfdflow.NewLimiter(config.WarpRouting.MaxActiveFlows),
|
||||
originDialerService: config.OriginDialerService,
|
||||
log: log,
|
||||
shutdownC: ctx.Done(),
|
||||
}
|
||||
if err := o.updateIngress(*config.Ingress, config.WarpRouting); err != nil {
|
||||
return nil, err
|
||||
@@ -175,7 +179,15 @@ func (o *Orchestrator) updateIngress(ingressRules ingress.Ingress, warpRouting i
|
||||
// Update the flow limit since the configuration might have changed
|
||||
o.flowLimiter.SetLimit(warpRouting.MaxActiveFlows)
|
||||
|
||||
proxy := proxy.NewOriginProxy(ingressRules, warpRouting, o.tags, o.flowLimiter, o.config.WriteTimeout, o.log)
|
||||
// Update the origin dialer service with the new dialer settings
|
||||
// We need to update the dialer here instead of creating a new instance of OriginDialerService because it has
|
||||
// its own references and go routines. Specifically, the UDP dialer is a reference to this same service all the
|
||||
// way into the datagram manager. Reconstructing the datagram manager is not something we currently provide during
|
||||
// runtime in response to a configuration push except when starting a tunnel connection.
|
||||
o.originDialerService.UpdateDefaultDialer(ingress.NewDialer(warpRouting))
|
||||
|
||||
// Create and replace the origin proxy with a new instance
|
||||
proxy := proxy.NewOriginProxy(ingressRules, o.originDialerService, o.tags, o.flowLimiter, o.log)
|
||||
o.proxy.Store(proxy)
|
||||
o.config.Ingress = &ingressRules
|
||||
o.config.WarpRouting = warpRouting
|
||||
|
||||
@@ -41,6 +41,11 @@ var (
|
||||
Value: "test",
|
||||
},
|
||||
}
|
||||
testDefaultDialer = ingress.NewDialer(ingress.WarpRoutingConfig{
|
||||
ConnectTimeout: config.CustomDuration{Duration: 1 * time.Second},
|
||||
TCPKeepAlive: config.CustomDuration{Duration: 15 * time.Second},
|
||||
MaxActiveFlows: 0,
|
||||
})
|
||||
)
|
||||
|
||||
// TestUpdateConfiguration tests that
|
||||
@@ -50,8 +55,13 @@ var (
|
||||
// - configurations can be deserialized
|
||||
// - receiving an old version is noop
|
||||
func TestUpdateConfiguration(t *testing.T) {
|
||||
originDialer := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 1 * time.Second,
|
||||
}, &testLogger)
|
||||
initConfig := &Config{
|
||||
Ingress: &ingress.Ingress{},
|
||||
Ingress: &ingress.Ingress{},
|
||||
OriginDialerService: originDialer,
|
||||
}
|
||||
orchestrator, err := NewOrchestrator(t.Context(), initConfig, testTags, []ingress.Rule{ingress.NewManagementRule(management.New("management.argotunnel.com", false, "1.1.1.1:80", uuid.Nil, "", &testLogger, nil))}, &testLogger)
|
||||
require.NoError(t, err)
|
||||
@@ -179,8 +189,13 @@ func TestUpdateConfiguration(t *testing.T) {
|
||||
// Validates that a new version 0 will be applied if the configuration is loaded locally.
|
||||
// This will happen when a locally managed tunnel is migrated to remote configuration and receives its first configuration.
|
||||
func TestUpdateConfiguration_FromMigration(t *testing.T) {
|
||||
originDialer := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 1 * time.Second,
|
||||
}, &testLogger)
|
||||
initConfig := &Config{
|
||||
Ingress: &ingress.Ingress{},
|
||||
Ingress: &ingress.Ingress{},
|
||||
OriginDialerService: originDialer,
|
||||
}
|
||||
orchestrator, err := NewOrchestrator(t.Context(), initConfig, testTags, []ingress.Rule{}, &testLogger)
|
||||
require.NoError(t, err)
|
||||
@@ -205,8 +220,13 @@ func TestUpdateConfiguration_FromMigration(t *testing.T) {
|
||||
|
||||
// Validates that the default ingress rule will be set if there is no rule provided from the remote.
|
||||
func TestUpdateConfiguration_WithoutIngressRule(t *testing.T) {
|
||||
originDialer := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 1 * time.Second,
|
||||
}, &testLogger)
|
||||
initConfig := &Config{
|
||||
Ingress: &ingress.Ingress{},
|
||||
Ingress: &ingress.Ingress{},
|
||||
OriginDialerService: originDialer,
|
||||
}
|
||||
orchestrator, err := NewOrchestrator(t.Context(), initConfig, testTags, []ingress.Rule{}, &testLogger)
|
||||
require.NoError(t, err)
|
||||
@@ -244,6 +264,11 @@ func TestConcurrentUpdateAndRead(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer tcpOrigin.Close()
|
||||
|
||||
originDialer := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 1 * time.Second,
|
||||
}, &testLogger)
|
||||
|
||||
var (
|
||||
configJSONV1 = []byte(fmt.Sprintf(`
|
||||
{
|
||||
@@ -296,7 +321,8 @@ func TestConcurrentUpdateAndRead(t *testing.T) {
|
||||
appliedV2 = make(chan struct{})
|
||||
|
||||
initConfig = &Config{
|
||||
Ingress: &ingress.Ingress{},
|
||||
Ingress: &ingress.Ingress{},
|
||||
OriginDialerService: originDialer,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -313,7 +339,7 @@ func TestConcurrentUpdateAndRead(t *testing.T) {
|
||||
go func() {
|
||||
serveTCPOrigin(t, tcpOrigin, &wg)
|
||||
}()
|
||||
for i := 0; i < concurrentRequests; i++ {
|
||||
for i := range concurrentRequests {
|
||||
originProxy, err := orchestrator.GetOriginProxy()
|
||||
require.NoError(t, err)
|
||||
wg.Add(1)
|
||||
@@ -323,48 +349,37 @@ func TestConcurrentUpdateAndRead(t *testing.T) {
|
||||
assert.NoError(t, err, "proxyHTTP %d failed %v", i, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var warpRoutingDisabled bool
|
||||
// The response can be from initOrigin, http_status:204 or http_status:418
|
||||
switch resp.StatusCode {
|
||||
// v1 proxy, warp enabled
|
||||
// v1 proxy
|
||||
case 200:
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, t.Name(), string(body))
|
||||
warpRoutingDisabled = false
|
||||
// v2 proxy, warp disabled
|
||||
// v2 proxy
|
||||
case 204:
|
||||
assert.Greater(t, i, concurrentRequests/4)
|
||||
warpRoutingDisabled = true
|
||||
// v3 proxy, warp enabled
|
||||
// v3 proxy
|
||||
case 418:
|
||||
assert.Greater(t, i, concurrentRequests/2)
|
||||
warpRoutingDisabled = false
|
||||
}
|
||||
|
||||
// Once we have originProxy, it won't be changed by configuration updates.
|
||||
// We can infer the version by the ProxyHTTP response code
|
||||
pr, pw := io.Pipe()
|
||||
|
||||
w := newRespReadWriteFlusher()
|
||||
|
||||
// Write TCP message and make sure it's echo back. This has to be done in a go routune since ProxyTCP doesn't
|
||||
// return until the stream is closed.
|
||||
if !warpRoutingDisabled {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer pw.Close()
|
||||
tcpEyeball(t, pw, tcpBody, w)
|
||||
}()
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer pw.Close()
|
||||
tcpEyeball(t, pw, tcpBody, w)
|
||||
}()
|
||||
|
||||
err = proxyTCP(ctx, originProxy, tcpOrigin.Addr().String(), w, pr)
|
||||
if warpRoutingDisabled {
|
||||
assert.Error(t, err, "expect proxyTCP %d to return error", i)
|
||||
} else {
|
||||
assert.NoError(t, err, "proxyTCP %d failed %v", i, err)
|
||||
}
|
||||
assert.NoError(t, err, "proxyTCP %d failed %v", i, err)
|
||||
}(i, originProxy)
|
||||
|
||||
if i == concurrentRequests/4 {
|
||||
@@ -406,39 +421,47 @@ func TestOverrideWarpRoutingConfigWithLocalValues(t *testing.T) {
|
||||
require.EqualValues(t, expectedValue, warpRouting["maxActiveFlows"])
|
||||
}
|
||||
|
||||
remoteValue := uint64(100)
|
||||
remoteIngress := ingress.Ingress{}
|
||||
originDialer := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 1 * time.Second,
|
||||
}, &testLogger)
|
||||
|
||||
// All the possible values set for MaxActiveFlows from the various points that can provide it:
|
||||
// 1. Initialized value
|
||||
// 2. Local CLI flag config
|
||||
// 3. Remote configuration value
|
||||
initValue := uint64(0)
|
||||
localValue := uint64(100)
|
||||
remoteValue := uint64(500)
|
||||
|
||||
initConfig := &Config{
|
||||
Ingress: &ingress.Ingress{},
|
||||
WarpRouting: ingress.WarpRoutingConfig{
|
||||
MaxActiveFlows: initValue,
|
||||
},
|
||||
OriginDialerService: originDialer,
|
||||
ConfigurationFlags: map[string]string{
|
||||
flags.MaxActiveFlows: fmt.Sprintf("%d", localValue),
|
||||
},
|
||||
}
|
||||
|
||||
// We expect the local configuration flag to be the starting value
|
||||
orchestrator, err := NewOrchestrator(ctx, initConfig, testTags, []ingress.Rule{}, &testLogger)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertMaxActiveFlows(orchestrator, localValue)
|
||||
|
||||
// Assigning the MaxActiveFlows in the remote config should be ignored over the local config
|
||||
remoteWarpConfig := ingress.WarpRoutingConfig{
|
||||
MaxActiveFlows: remoteValue,
|
||||
}
|
||||
remoteConfig := &Config{
|
||||
Ingress: &remoteIngress,
|
||||
WarpRouting: remoteWarpConfig,
|
||||
ConfigurationFlags: map[string]string{},
|
||||
}
|
||||
orchestrator, err := NewOrchestrator(ctx, remoteConfig, testTags, []ingress.Rule{}, &testLogger)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertMaxActiveFlows(orchestrator, remoteValue)
|
||||
|
||||
// Add a local override for the maxActiveFlows
|
||||
localValue := uint64(500)
|
||||
remoteConfig.ConfigurationFlags[flags.MaxActiveFlows] = fmt.Sprintf("%d", localValue)
|
||||
// Force a configuration refresh
|
||||
err = orchestrator.updateIngress(remoteIngress, remoteWarpConfig)
|
||||
err = orchestrator.updateIngress(ingress.Ingress{}, remoteWarpConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check the value being used is the local one
|
||||
assertMaxActiveFlows(orchestrator, localValue)
|
||||
|
||||
// Remove local override for the maxActiveFlows
|
||||
delete(remoteConfig.ConfigurationFlags, flags.MaxActiveFlows)
|
||||
// Force a configuration refresh
|
||||
err = orchestrator.updateIngress(remoteIngress, remoteWarpConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check the value being used is now the remote again
|
||||
assertMaxActiveFlows(orchestrator, remoteValue)
|
||||
}
|
||||
|
||||
func proxyHTTP(originProxy connection.OriginProxy, hostname string) (*http.Response, error) {
|
||||
@@ -546,6 +569,10 @@ func updateWithValidation(t *testing.T, orchestrator *Orchestrator, version int3
|
||||
|
||||
// TestClosePreviousProxies makes sure proxies started in the previous configuration version are shutdown
|
||||
func TestClosePreviousProxies(t *testing.T) {
|
||||
originDialer := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 1 * time.Second,
|
||||
}, &testLogger)
|
||||
var (
|
||||
hostname = "hello.tunnel1.org"
|
||||
configWithHelloWorld = []byte(fmt.Sprintf(`
|
||||
@@ -576,7 +603,8 @@ func TestClosePreviousProxies(t *testing.T) {
|
||||
}
|
||||
`)
|
||||
initConfig = &Config{
|
||||
Ingress: &ingress.Ingress{},
|
||||
Ingress: &ingress.Ingress{},
|
||||
OriginDialerService: originDialer,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -638,8 +666,13 @@ func TestPersistentConnection(t *testing.T) {
|
||||
hostname = "http://ws.tunnel.org"
|
||||
)
|
||||
msg := t.Name()
|
||||
originDialer := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 1 * time.Second,
|
||||
}, &testLogger)
|
||||
initConfig := &Config{
|
||||
Ingress: &ingress.Ingress{},
|
||||
Ingress: &ingress.Ingress{},
|
||||
OriginDialerService: originDialer,
|
||||
}
|
||||
orchestrator, err := NewOrchestrator(t.Context(), initConfig, testTags, []ingress.Rule{}, &testLogger)
|
||||
require.NoError(t, err)
|
||||
@@ -752,8 +785,9 @@ func TestSerializeLocalConfig(t *testing.T) {
|
||||
ConfigurationFlags: map[string]string{"a": "b"},
|
||||
}
|
||||
|
||||
result, _ := json.Marshal(c)
|
||||
fmt.Println(string(result))
|
||||
result, err := json.Marshal(c)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{"__configuration_flags":{"a":"b"},"ingress":[],"warp-routing":{"connectTimeout":0,"tcpKeepAlive":0}}`, string(result))
|
||||
}
|
||||
|
||||
func wsEcho(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
+55
-17
@@ -5,11 +5,11 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
pkgerrors "github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@@ -35,7 +35,7 @@ const (
|
||||
// Proxy represents a means to Proxy between cloudflared and the origin services.
|
||||
type Proxy struct {
|
||||
ingressRules ingress.Ingress
|
||||
warpRouting *ingress.WarpRoutingService
|
||||
originDialer ingress.OriginTCPDialer
|
||||
tags []pogs.Tag
|
||||
flowLimiter cfdflow.Limiter
|
||||
log *zerolog.Logger
|
||||
@@ -44,21 +44,19 @@ type Proxy struct {
|
||||
// NewOriginProxy returns a new instance of the Proxy struct.
|
||||
func NewOriginProxy(
|
||||
ingressRules ingress.Ingress,
|
||||
warpRouting ingress.WarpRoutingConfig,
|
||||
originDialer ingress.OriginDialer,
|
||||
tags []pogs.Tag,
|
||||
flowLimiter cfdflow.Limiter,
|
||||
writeTimeout time.Duration,
|
||||
log *zerolog.Logger,
|
||||
) *Proxy {
|
||||
proxy := &Proxy{
|
||||
ingressRules: ingressRules,
|
||||
originDialer: originDialer,
|
||||
tags: tags,
|
||||
flowLimiter: flowLimiter,
|
||||
log: log,
|
||||
}
|
||||
|
||||
proxy.warpRouting = ingress.NewWarpRoutingService(warpRouting, writeTimeout)
|
||||
|
||||
return proxy
|
||||
}
|
||||
|
||||
@@ -146,24 +144,18 @@ func (p *Proxy) ProxyHTTP(
|
||||
// ProxyTCP proxies to a TCP connection between the origin service and cloudflared.
|
||||
func (p *Proxy) ProxyTCP(
|
||||
ctx context.Context,
|
||||
rwa connection.ReadWriteAcker,
|
||||
conn connection.ReadWriteAcker,
|
||||
req *connection.TCPRequest,
|
||||
) error {
|
||||
incrementTCPRequests()
|
||||
defer decrementTCPConcurrentRequests()
|
||||
|
||||
if p.warpRouting == nil {
|
||||
err := errors.New(`cloudflared received a request from WARP client, but your configuration has disabled ingress from WARP clients. To enable this, set "warp-routing:\n\t enabled: true" in your config.yaml`)
|
||||
p.log.Error().Msg(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
logger := newTCPLogger(p.log, req)
|
||||
|
||||
// Try to start a new flow
|
||||
if err := p.flowLimiter.Acquire(management.TCP.String()); err != nil {
|
||||
logger.Warn().Msg("Too many concurrent flows being handled, rejecting tcp proxy")
|
||||
return pkgerrors.Wrap(err, "failed to start tcp flow due to rate limiting")
|
||||
return errors.Wrap(err, "failed to start tcp flow due to rate limiting")
|
||||
}
|
||||
defer p.flowLimiter.Release()
|
||||
|
||||
@@ -173,7 +165,14 @@ func (p *Proxy) ProxyTCP(
|
||||
tracedCtx := tracing.NewTracedContext(serveCtx, req.CfTraceID, &logger)
|
||||
logger.Debug().Msg("tcp proxy stream started")
|
||||
|
||||
if err := p.proxyStream(tracedCtx, rwa, req.Dest, p.warpRouting.Proxy, &logger); err != nil {
|
||||
// Parse the destination into a netip.AddrPort
|
||||
dest, err := netip.ParseAddrPort(req.Dest)
|
||||
if err != nil {
|
||||
logRequestError(&logger, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.proxyTCPStream(tracedCtx, conn, dest, p.originDialer, &logger); err != nil {
|
||||
logRequestError(&logger, err)
|
||||
return err
|
||||
}
|
||||
@@ -279,14 +278,14 @@ func (p *Proxy) proxyStream(
|
||||
tr *tracing.TracedContext,
|
||||
rwa connection.ReadWriteAcker,
|
||||
dest string,
|
||||
connectionProxy ingress.StreamBasedOriginProxy,
|
||||
originDialer ingress.StreamBasedOriginProxy,
|
||||
logger *zerolog.Logger,
|
||||
) error {
|
||||
ctx := tr.Context
|
||||
_, connectSpan := tr.Tracer().Start(ctx, "stream-connect")
|
||||
|
||||
start := time.Now()
|
||||
originConn, err := connectionProxy.EstablishConnection(ctx, dest, logger)
|
||||
originConn, err := originDialer.EstablishConnection(ctx, dest, logger)
|
||||
if err != nil {
|
||||
connectStreamErrors.Inc()
|
||||
tracing.EndWithErrorStatus(connectSpan, err)
|
||||
@@ -310,6 +309,45 @@ func (p *Proxy) proxyStream(
|
||||
return nil
|
||||
}
|
||||
|
||||
// proxyTCPStream proxies private network type TCP connections as a stream towards an available origin.
|
||||
//
|
||||
// This is different than proxyStream because it's not leveraged ingress rule services and uses the
|
||||
// originDialer from OriginDialerService.
|
||||
func (p *Proxy) proxyTCPStream(
|
||||
tr *tracing.TracedContext,
|
||||
tunnelConn connection.ReadWriteAcker,
|
||||
dest netip.AddrPort,
|
||||
originDialer ingress.OriginTCPDialer,
|
||||
logger *zerolog.Logger,
|
||||
) error {
|
||||
ctx := tr.Context
|
||||
_, connectSpan := tr.Tracer().Start(ctx, "stream-connect")
|
||||
|
||||
start := time.Now()
|
||||
originConn, err := originDialer.DialTCP(ctx, dest)
|
||||
if err != nil {
|
||||
connectStreamErrors.Inc()
|
||||
tracing.EndWithErrorStatus(connectSpan, err)
|
||||
return err
|
||||
}
|
||||
connectSpan.End()
|
||||
defer originConn.Close()
|
||||
logger.Debug().Msg("origin connection established")
|
||||
|
||||
encodedSpans := tr.GetSpans()
|
||||
|
||||
if err := tunnelConn.AckConnection(encodedSpans); err != nil {
|
||||
connectStreamErrors.Inc()
|
||||
return err
|
||||
}
|
||||
|
||||
connectLatency.Observe(float64(time.Since(start).Milliseconds()))
|
||||
logger.Debug().Msg("proxy stream acknowledged")
|
||||
|
||||
stream.Pipe(tunnelConn, originConn, logger)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Proxy) proxyLocalRequest(proxy ingress.HTTPLocalProxy, w connection.ResponseWriter, req *http.Request, isWebsocket bool) {
|
||||
if isWebsocket {
|
||||
// These headers are added since they are stripped off during an eyeball request to origintunneld, but they
|
||||
|
||||
+34
-38
@@ -33,17 +33,17 @@ import (
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/tracing"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
var (
|
||||
testTags = []pogs.Tag{{Name: "Name", Value: "value"}}
|
||||
noWarpRouting = ingress.WarpRoutingConfig{}
|
||||
testWarpRouting = ingress.WarpRoutingConfig{
|
||||
ConnectTimeout: config.CustomDuration{Duration: time.Second},
|
||||
}
|
||||
testTags = []pogs.Tag{{Name: "Name", Value: "value"}}
|
||||
testDefaultDialer = ingress.NewDialer(ingress.WarpRoutingConfig{
|
||||
ConnectTimeout: config.CustomDuration{Duration: 1 * time.Second},
|
||||
TCPKeepAlive: config.CustomDuration{Duration: 15 * time.Second},
|
||||
MaxActiveFlows: 0,
|
||||
})
|
||||
)
|
||||
|
||||
type mockHTTPRespWriter struct {
|
||||
@@ -162,7 +162,12 @@ func TestProxySingleOrigin(t *testing.T) {
|
||||
|
||||
require.NoError(t, ingressRule.StartOrigins(&log, ctx.Done()))
|
||||
|
||||
proxy := NewOriginProxy(ingressRule, noWarpRouting, testTags, cfdflow.NewLimiter(0), time.Duration(0), &log)
|
||||
originDialer := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 1 * time.Second,
|
||||
}, &log)
|
||||
|
||||
proxy := NewOriginProxy(ingressRule, originDialer, testTags, cfdflow.NewLimiter(0), &log)
|
||||
t.Run("testProxyHTTP", testProxyHTTP(proxy))
|
||||
t.Run("testProxyWebsocket", testProxyWebsocket(proxy))
|
||||
t.Run("testProxySSE", testProxySSE(proxy))
|
||||
@@ -357,7 +362,7 @@ type MultipleIngressTest struct {
|
||||
}
|
||||
|
||||
func runIngressTestScenarios(t *testing.T, unvalidatedIngress []config.UnvalidatedIngressRule, tests []MultipleIngressTest) {
|
||||
ingress, err := ingress.ParseIngress(&config.Configuration{
|
||||
ingressRule, err := ingress.ParseIngress(&config.Configuration{
|
||||
TunnelID: t.Name(),
|
||||
Ingress: unvalidatedIngress,
|
||||
})
|
||||
@@ -366,9 +371,14 @@ func runIngressTestScenarios(t *testing.T, unvalidatedIngress []config.Unvalidat
|
||||
log := zerolog.Nop()
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
require.NoError(t, ingress.StartOrigins(&log, ctx.Done()))
|
||||
require.NoError(t, ingressRule.StartOrigins(&log, ctx.Done()))
|
||||
|
||||
proxy := NewOriginProxy(ingress, noWarpRouting, testTags, cfdflow.NewLimiter(0), time.Duration(0), &log)
|
||||
originDialer := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 1 * time.Second,
|
||||
}, &log)
|
||||
|
||||
proxy := NewOriginProxy(ingressRule, originDialer, testTags, cfdflow.NewLimiter(0), &log)
|
||||
|
||||
for _, test := range tests {
|
||||
responseWriter := newMockHTTPRespWriter()
|
||||
@@ -416,7 +426,12 @@ func TestProxyError(t *testing.T) {
|
||||
|
||||
log := zerolog.Nop()
|
||||
|
||||
proxy := NewOriginProxy(ing, noWarpRouting, testTags, cfdflow.NewLimiter(0), time.Duration(0), &log)
|
||||
originDialer := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 1 * time.Second,
|
||||
}, &log)
|
||||
|
||||
proxy := NewOriginProxy(ing, originDialer, testTags, cfdflow.NewLimiter(0), &log)
|
||||
|
||||
responseWriter := newMockHTTPRespWriter()
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1", nil)
|
||||
@@ -467,7 +482,7 @@ func (r *replayer) Bytes() []byte {
|
||||
// WS - TCP: When a tcp based ingress is configured on the origin and the
|
||||
// eyeball sends tcp packets wrapped in websockets. (E.g: cloudflared access).
|
||||
func TestConnections(t *testing.T) {
|
||||
logger := logger.Create(nil)
|
||||
log := zerolog.Nop()
|
||||
replayer := &replayer{rw: bytes.NewBuffer([]byte{})}
|
||||
type args struct {
|
||||
ingressServiceScheme string
|
||||
@@ -475,9 +490,6 @@ func TestConnections(t *testing.T) {
|
||||
eyeballResponseWriter connection.ResponseWriter
|
||||
eyeballRequestBody io.ReadCloser
|
||||
|
||||
// Can be set to nil to show warp routing is not enabled.
|
||||
warpRoutingService *ingress.WarpRoutingService
|
||||
|
||||
// eyeball connection type.
|
||||
connectionType connection.Type
|
||||
|
||||
@@ -488,6 +500,11 @@ func TestConnections(t *testing.T) {
|
||||
flowLimiterResponse error
|
||||
}
|
||||
|
||||
originDialer := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 0,
|
||||
}, &log)
|
||||
|
||||
type want struct {
|
||||
message []byte
|
||||
headers http.Header
|
||||
@@ -530,7 +547,6 @@ func TestConnections(t *testing.T) {
|
||||
originService: runEchoTCPService,
|
||||
eyeballResponseWriter: newTCPRespWriter(replayer),
|
||||
eyeballRequestBody: newTCPRequestBody([]byte("test2")),
|
||||
warpRoutingService: ingress.NewWarpRoutingService(testWarpRouting, time.Duration(0)),
|
||||
connectionType: connection.TypeTCP,
|
||||
requestHeaders: map[string][]string{
|
||||
"Cf-Cloudflared-Proxy-Src": {"non-blank-value"},
|
||||
@@ -548,7 +564,6 @@ func TestConnections(t *testing.T) {
|
||||
originService: runEchoWSService,
|
||||
// eyeballResponseWriter gets set after roundtrip dial.
|
||||
eyeballRequestBody: newPipedWSRequestBody([]byte("test3")),
|
||||
warpRoutingService: ingress.NewWarpRoutingService(testWarpRouting, time.Duration(0)),
|
||||
requestHeaders: map[string][]string{
|
||||
"Cf-Cloudflared-Proxy-Src": {"non-blank-value"},
|
||||
},
|
||||
@@ -601,23 +616,6 @@ func TestConnections(t *testing.T) {
|
||||
headers: map[string][]string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tcp-tcp proxy without warpRoutingService enabled",
|
||||
args: args{
|
||||
ingressServiceScheme: "tcp://",
|
||||
originService: runEchoTCPService,
|
||||
eyeballResponseWriter: newTCPRespWriter(replayer),
|
||||
eyeballRequestBody: newTCPRequestBody([]byte("test2")),
|
||||
connectionType: connection.TypeTCP,
|
||||
requestHeaders: map[string][]string{
|
||||
"Cf-Cloudflared-Proxy-Src": {"non-blank-value"},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
message: []byte{},
|
||||
err: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ws-ws proxy when origin is different",
|
||||
args: args{
|
||||
@@ -670,7 +668,6 @@ func TestConnections(t *testing.T) {
|
||||
originService: runEchoTCPService,
|
||||
eyeballResponseWriter: newTCPRespWriter(replayer),
|
||||
eyeballRequestBody: newTCPRequestBody([]byte("rate-limited")),
|
||||
warpRoutingService: ingress.NewWarpRoutingService(testWarpRouting, time.Duration(0)),
|
||||
connectionType: connection.TypeTCP,
|
||||
requestHeaders: map[string][]string{
|
||||
"Cf-Cloudflared-Proxy-Src": {"non-blank-value"},
|
||||
@@ -693,7 +690,7 @@ func TestConnections(t *testing.T) {
|
||||
test.args.originService(t, ln)
|
||||
|
||||
ingressRule := createSingleIngressConfig(t, test.args.ingressServiceScheme+ln.Addr().String())
|
||||
_ = ingressRule.StartOrigins(logger, ctx.Done())
|
||||
_ = ingressRule.StartOrigins(&log, ctx.Done())
|
||||
|
||||
// Mock flow limiter
|
||||
ctrl := gomock.NewController(t)
|
||||
@@ -702,8 +699,7 @@ func TestConnections(t *testing.T) {
|
||||
flowLimiter.EXPECT().Acquire("tcp").AnyTimes().Return(test.args.flowLimiterResponse)
|
||||
flowLimiter.EXPECT().Release().AnyTimes()
|
||||
|
||||
proxy := NewOriginProxy(ingressRule, testWarpRouting, testTags, flowLimiter, time.Duration(0), logger)
|
||||
proxy.warpRouting = test.args.warpRoutingService
|
||||
proxy := NewOriginProxy(ingressRule, originDialer, testTags, flowLimiter, &log)
|
||||
|
||||
dest := ln.Addr().String()
|
||||
req, err := http.NewRequest(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package v3_test
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"net/netip"
|
||||
@@ -14,12 +15,18 @@ import (
|
||||
|
||||
func makePayload(size int) []byte {
|
||||
payload := make([]byte, size)
|
||||
for i := range len(payload) {
|
||||
payload[i] = 0xfc
|
||||
}
|
||||
_, _ = rand.Read(payload)
|
||||
return payload
|
||||
}
|
||||
|
||||
func makePayloads(size int, count int) [][]byte {
|
||||
payloads := make([][]byte, count)
|
||||
for i := range payloads {
|
||||
payloads[i] = makePayload(size)
|
||||
}
|
||||
return payloads
|
||||
}
|
||||
|
||||
func TestSessionRegistration_MarshalUnmarshal(t *testing.T) {
|
||||
payload := makePayload(1280)
|
||||
tests := []*v3.UDPSessionRegistrationDatagram{
|
||||
|
||||
+4
-7
@@ -2,12 +2,11 @@ package v3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
"github.com/cloudflare/cloudflared/management"
|
||||
|
||||
cfdflow "github.com/cloudflare/cloudflared/flow"
|
||||
@@ -38,18 +37,16 @@ type SessionManager interface {
|
||||
UnregisterSession(requestID RequestID)
|
||||
}
|
||||
|
||||
type DialUDP func(dest netip.AddrPort) (*net.UDPConn, error)
|
||||
|
||||
type sessionManager struct {
|
||||
sessions map[RequestID]Session
|
||||
mutex sync.RWMutex
|
||||
originDialer DialUDP
|
||||
originDialer ingress.OriginUDPDialer
|
||||
limiter cfdflow.Limiter
|
||||
metrics Metrics
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
func NewSessionManager(metrics Metrics, log *zerolog.Logger, originDialer DialUDP, limiter cfdflow.Limiter) SessionManager {
|
||||
func NewSessionManager(metrics Metrics, log *zerolog.Logger, originDialer ingress.OriginUDPDialer, limiter cfdflow.Limiter) SessionManager {
|
||||
return &sessionManager{
|
||||
sessions: make(map[RequestID]Session),
|
||||
originDialer: originDialer,
|
||||
@@ -76,7 +73,7 @@ func (s *sessionManager) RegisterSession(request *UDPSessionRegistrationDatagram
|
||||
}
|
||||
|
||||
// Attempt to bind the UDP socket for the new session
|
||||
origin, err := s.originDialer(request.Dest)
|
||||
origin, err := s.originDialer.DialUDP(request.Dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+24
-3
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/mocks"
|
||||
|
||||
cfdflow "github.com/cloudflare/cloudflared/flow"
|
||||
@@ -18,9 +19,21 @@ import (
|
||||
v3 "github.com/cloudflare/cloudflared/quic/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
testDefaultDialer = ingress.NewDialer(ingress.WarpRoutingConfig{
|
||||
ConnectTimeout: config.CustomDuration{Duration: 1 * time.Second},
|
||||
TCPKeepAlive: config.CustomDuration{Duration: 15 * time.Second},
|
||||
MaxActiveFlows: 0,
|
||||
})
|
||||
)
|
||||
|
||||
func TestRegisterSession(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
manager := v3.NewSessionManager(&noopMetrics{}, &log, ingress.DialUDPAddrPort, cfdflow.NewLimiter(0))
|
||||
originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 0,
|
||||
}, &log)
|
||||
manager := v3.NewSessionManager(&noopMetrics{}, &log, originDialerService, cfdflow.NewLimiter(0))
|
||||
|
||||
request := v3.UDPSessionRegistrationDatagram{
|
||||
RequestID: testRequestID,
|
||||
@@ -76,7 +89,11 @@ func TestRegisterSession(t *testing.T) {
|
||||
|
||||
func TestGetSession_Empty(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
manager := v3.NewSessionManager(&noopMetrics{}, &log, ingress.DialUDPAddrPort, cfdflow.NewLimiter(0))
|
||||
originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 0,
|
||||
}, &log)
|
||||
manager := v3.NewSessionManager(&noopMetrics{}, &log, originDialerService, cfdflow.NewLimiter(0))
|
||||
|
||||
_, err := manager.GetSession(testRequestID)
|
||||
if !errors.Is(err, v3.ErrSessionNotFound) {
|
||||
@@ -86,6 +103,10 @@ func TestGetSession_Empty(t *testing.T) {
|
||||
|
||||
func TestRegisterSessionRateLimit(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 0,
|
||||
}, &log)
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
flowLimiterMock := mocks.NewMockLimiter(ctrl)
|
||||
@@ -93,7 +114,7 @@ func TestRegisterSessionRateLimit(t *testing.T) {
|
||||
flowLimiterMock.EXPECT().Acquire("udp").Return(cfdflow.ErrTooManyActiveFlows)
|
||||
flowLimiterMock.EXPECT().Release().Times(0)
|
||||
|
||||
manager := v3.NewSessionManager(&noopMetrics{}, &log, ingress.DialUDPAddrPort, flowLimiterMock)
|
||||
manager := v3.NewSessionManager(&noopMetrics{}, &log, originDialerService, flowLimiterMock)
|
||||
|
||||
request := v3.UDPSessionRegistrationDatagram{
|
||||
RequestID: testRequestID,
|
||||
|
||||
+71
-18
@@ -9,28 +9,59 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
namespace = "cloudflared"
|
||||
subsystem = "udp"
|
||||
namespace = "cloudflared"
|
||||
subsystem_udp = "udp"
|
||||
subsystem_icmp = "icmp"
|
||||
|
||||
commandMetricLabel = "command"
|
||||
reasonMetricLabel = "reason"
|
||||
)
|
||||
|
||||
type DroppedReason int
|
||||
|
||||
const (
|
||||
DroppedWriteFailed DroppedReason = iota
|
||||
DroppedWriteDeadlineExceeded
|
||||
DroppedWriteFull
|
||||
DroppedWriteFlowUnknown
|
||||
DroppedReadFailed
|
||||
// Origin payloads that are too large to proxy.
|
||||
DroppedReadTooLarge
|
||||
)
|
||||
|
||||
var droppedReason = map[DroppedReason]string{
|
||||
DroppedWriteFailed: "write_failed",
|
||||
DroppedWriteDeadlineExceeded: "write_deadline_exceeded",
|
||||
DroppedWriteFull: "write_full",
|
||||
DroppedWriteFlowUnknown: "write_flow_unknown",
|
||||
DroppedReadFailed: "read_failed",
|
||||
DroppedReadTooLarge: "read_too_large",
|
||||
}
|
||||
|
||||
func (dr DroppedReason) String() string {
|
||||
return droppedReason[dr]
|
||||
}
|
||||
|
||||
type Metrics interface {
|
||||
IncrementFlows(connIndex uint8)
|
||||
DecrementFlows(connIndex uint8)
|
||||
PayloadTooLarge(connIndex uint8)
|
||||
FailedFlow(connIndex uint8)
|
||||
RetryFlowResponse(connIndex uint8)
|
||||
MigrateFlow(connIndex uint8)
|
||||
UnsupportedRemoteCommand(connIndex uint8, command string)
|
||||
DroppedUDPDatagram(connIndex uint8, reason DroppedReason)
|
||||
DroppedICMPPackets(connIndex uint8, reason DroppedReason)
|
||||
}
|
||||
|
||||
type metrics struct {
|
||||
activeUDPFlows *prometheus.GaugeVec
|
||||
totalUDPFlows *prometheus.CounterVec
|
||||
payloadTooLarge *prometheus.CounterVec
|
||||
retryFlowResponses *prometheus.CounterVec
|
||||
migratedFlows *prometheus.CounterVec
|
||||
unsupportedRemoteCommands *prometheus.CounterVec
|
||||
droppedUDPDatagrams *prometheus.CounterVec
|
||||
droppedICMPPackets *prometheus.CounterVec
|
||||
failedFlows *prometheus.CounterVec
|
||||
}
|
||||
|
||||
func (m *metrics) IncrementFlows(connIndex uint8) {
|
||||
@@ -42,8 +73,8 @@ func (m *metrics) DecrementFlows(connIndex uint8) {
|
||||
m.activeUDPFlows.WithLabelValues(fmt.Sprintf("%d", connIndex)).Dec()
|
||||
}
|
||||
|
||||
func (m *metrics) PayloadTooLarge(connIndex uint8) {
|
||||
m.payloadTooLarge.WithLabelValues(fmt.Sprintf("%d", connIndex)).Inc()
|
||||
func (m *metrics) FailedFlow(connIndex uint8) {
|
||||
m.failedFlows.WithLabelValues(fmt.Sprintf("%d", connIndex)).Inc()
|
||||
}
|
||||
|
||||
func (m *metrics) RetryFlowResponse(connIndex uint8) {
|
||||
@@ -58,52 +89,74 @@ func (m *metrics) UnsupportedRemoteCommand(connIndex uint8, command string) {
|
||||
m.unsupportedRemoteCommands.WithLabelValues(fmt.Sprintf("%d", connIndex), command).Inc()
|
||||
}
|
||||
|
||||
func (m *metrics) DroppedUDPDatagram(connIndex uint8, reason DroppedReason) {
|
||||
m.droppedUDPDatagrams.WithLabelValues(fmt.Sprintf("%d", connIndex), reason.String()).Inc()
|
||||
}
|
||||
|
||||
func (m *metrics) DroppedICMPPackets(connIndex uint8, reason DroppedReason) {
|
||||
m.droppedICMPPackets.WithLabelValues(fmt.Sprintf("%d", connIndex), reason.String()).Inc()
|
||||
}
|
||||
|
||||
func NewMetrics(registerer prometheus.Registerer) Metrics {
|
||||
m := &metrics{
|
||||
activeUDPFlows: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Subsystem: subsystem_udp,
|
||||
Name: "active_flows",
|
||||
Help: "Concurrent count of UDP flows that are being proxied to any origin",
|
||||
}, []string{quic.ConnectionIndexMetricLabel}),
|
||||
totalUDPFlows: prometheus.NewCounterVec(prometheus.CounterOpts{ //nolint:promlinter
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Subsystem: subsystem_udp,
|
||||
Name: "total_flows",
|
||||
Help: "Total count of UDP flows that have been proxied to any origin",
|
||||
}, []string{quic.ConnectionIndexMetricLabel}),
|
||||
payloadTooLarge: prometheus.NewCounterVec(prometheus.CounterOpts{ //nolint:promlinter
|
||||
failedFlows: prometheus.NewCounterVec(prometheus.CounterOpts{ //nolint:promlinter
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "payload_too_large",
|
||||
Help: "Total count of UDP flows that have had origin payloads that are too large to proxy",
|
||||
Subsystem: subsystem_udp,
|
||||
Name: "failed_flows",
|
||||
Help: "Total count of flows that errored and closed",
|
||||
}, []string{quic.ConnectionIndexMetricLabel}),
|
||||
retryFlowResponses: prometheus.NewCounterVec(prometheus.CounterOpts{ //nolint:promlinter
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Subsystem: subsystem_udp,
|
||||
Name: "retry_flow_responses",
|
||||
Help: "Total count of UDP flows that have had to send their registration response more than once",
|
||||
}, []string{quic.ConnectionIndexMetricLabel}),
|
||||
migratedFlows: prometheus.NewCounterVec(prometheus.CounterOpts{ //nolint:promlinter
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Subsystem: subsystem_udp,
|
||||
Name: "migrated_flows",
|
||||
Help: "Total count of UDP flows have been migrated across local connections",
|
||||
}, []string{quic.ConnectionIndexMetricLabel}),
|
||||
unsupportedRemoteCommands: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
unsupportedRemoteCommands: prometheus.NewCounterVec(prometheus.CounterOpts{ //nolint:promlinter
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Subsystem: subsystem_udp,
|
||||
Name: "unsupported_remote_command_total",
|
||||
Help: "Total count of unsupported remote RPC commands for the ",
|
||||
Help: "Total count of unsupported remote RPC commands called",
|
||||
}, []string{quic.ConnectionIndexMetricLabel, commandMetricLabel}),
|
||||
droppedUDPDatagrams: prometheus.NewCounterVec(prometheus.CounterOpts{ //nolint:promlinter
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem_udp,
|
||||
Name: "dropped_datagrams",
|
||||
Help: "Total count of UDP dropped datagrams",
|
||||
}, []string{quic.ConnectionIndexMetricLabel, reasonMetricLabel}),
|
||||
droppedICMPPackets: prometheus.NewCounterVec(prometheus.CounterOpts{ //nolint:promlinter
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem_icmp,
|
||||
Name: "dropped_packets",
|
||||
Help: "Total count of ICMP dropped datagrams",
|
||||
}, []string{quic.ConnectionIndexMetricLabel, reasonMetricLabel}),
|
||||
}
|
||||
registerer.MustRegister(
|
||||
m.activeUDPFlows,
|
||||
m.totalUDPFlows,
|
||||
m.payloadTooLarge,
|
||||
m.failedFlows,
|
||||
m.retryFlowResponses,
|
||||
m.migratedFlows,
|
||||
m.unsupportedRemoteCommands,
|
||||
m.droppedUDPDatagrams,
|
||||
m.droppedICMPPackets,
|
||||
)
|
||||
return m
|
||||
}
|
||||
|
||||
+11
-6
@@ -1,10 +1,15 @@
|
||||
package v3_test
|
||||
|
||||
import v3 "github.com/cloudflare/cloudflared/quic/v3"
|
||||
|
||||
type noopMetrics struct{}
|
||||
|
||||
func (noopMetrics) IncrementFlows(connIndex uint8) {}
|
||||
func (noopMetrics) DecrementFlows(connIndex uint8) {}
|
||||
func (noopMetrics) PayloadTooLarge(connIndex uint8) {}
|
||||
func (noopMetrics) RetryFlowResponse(connIndex uint8) {}
|
||||
func (noopMetrics) MigrateFlow(connIndex uint8) {}
|
||||
func (noopMetrics) UnsupportedRemoteCommand(connIndex uint8, command string) {}
|
||||
func (noopMetrics) IncrementFlows(connIndex uint8) {}
|
||||
func (noopMetrics) DecrementFlows(connIndex uint8) {}
|
||||
func (noopMetrics) FailedFlow(connIndex uint8) {}
|
||||
func (noopMetrics) PayloadTooLarge(connIndex uint8) {}
|
||||
func (noopMetrics) RetryFlowResponse(connIndex uint8) {}
|
||||
func (noopMetrics) MigrateFlow(connIndex uint8) {}
|
||||
func (noopMetrics) UnsupportedRemoteCommand(connIndex uint8, command string) {}
|
||||
func (noopMetrics) DroppedUDPDatagram(connIndex uint8, reason v3.DroppedReason) {}
|
||||
func (noopMetrics) DroppedICMPPackets(connIndex uint8, reason v3.DroppedReason) {}
|
||||
|
||||
+119
-86
@@ -17,6 +17,9 @@ const (
|
||||
// Allocating a 16 channel buffer here allows for the writer to be slightly faster than the reader.
|
||||
// This has worked previously well for datagramv2, so we will start with this as well
|
||||
demuxChanCapacity = 16
|
||||
// This provides a small buffer for the PacketRouter to poll ICMP packets from the QUIC connection
|
||||
// before writing them to the origin.
|
||||
icmpDatagramChanCapacity = 128
|
||||
|
||||
logSrcKey = "src"
|
||||
logDstKey = "dst"
|
||||
@@ -59,14 +62,15 @@ type QuicConnection interface {
|
||||
}
|
||||
|
||||
type datagramConn struct {
|
||||
conn QuicConnection
|
||||
index uint8
|
||||
sessionManager SessionManager
|
||||
icmpRouter ingress.ICMPRouter
|
||||
metrics Metrics
|
||||
logger *zerolog.Logger
|
||||
datagrams chan []byte
|
||||
readErrors chan error
|
||||
conn QuicConnection
|
||||
index uint8
|
||||
sessionManager SessionManager
|
||||
icmpRouter ingress.ICMPRouter
|
||||
metrics Metrics
|
||||
logger *zerolog.Logger
|
||||
datagrams chan []byte
|
||||
icmpDatagramChan chan *ICMPDatagram
|
||||
readErrors chan error
|
||||
|
||||
icmpEncoderPool sync.Pool // a pool of *packet.Encoder
|
||||
icmpDecoderPool sync.Pool
|
||||
@@ -75,14 +79,15 @@ type datagramConn struct {
|
||||
func NewDatagramConn(conn QuicConnection, sessionManager SessionManager, icmpRouter ingress.ICMPRouter, index uint8, metrics Metrics, logger *zerolog.Logger) DatagramConn {
|
||||
log := logger.With().Uint8("datagramVersion", 3).Logger()
|
||||
return &datagramConn{
|
||||
conn: conn,
|
||||
index: index,
|
||||
sessionManager: sessionManager,
|
||||
icmpRouter: icmpRouter,
|
||||
metrics: metrics,
|
||||
logger: &log,
|
||||
datagrams: make(chan []byte, demuxChanCapacity),
|
||||
readErrors: make(chan error, 2),
|
||||
conn: conn,
|
||||
index: index,
|
||||
sessionManager: sessionManager,
|
||||
icmpRouter: icmpRouter,
|
||||
metrics: metrics,
|
||||
logger: &log,
|
||||
datagrams: make(chan []byte, demuxChanCapacity),
|
||||
icmpDatagramChan: make(chan *ICMPDatagram, icmpDatagramChanCapacity),
|
||||
readErrors: make(chan error, 2),
|
||||
icmpEncoderPool: sync.Pool{
|
||||
New: func() any {
|
||||
return packet.NewEncoder()
|
||||
@@ -168,6 +173,9 @@ func (c *datagramConn) Serve(ctx context.Context) error {
|
||||
readCtx, cancel := context.WithCancel(connCtx)
|
||||
defer cancel()
|
||||
go c.pollDatagrams(readCtx)
|
||||
// Processing ICMP datagrams also monitors the reader context since the ICMP datagrams from the reader are the input
|
||||
// for the routine.
|
||||
go c.processICMPDatagrams(readCtx)
|
||||
for {
|
||||
// We make sure to monitor the context of cloudflared and the underlying connection to return if any errors occur.
|
||||
var datagram []byte
|
||||
@@ -175,64 +183,65 @@ func (c *datagramConn) Serve(ctx context.Context) error {
|
||||
// Monitor the context of cloudflared
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
// Monitor the context of the underlying connection
|
||||
// Monitor the context of the underlying quic connection
|
||||
case <-connCtx.Done():
|
||||
return connCtx.Err()
|
||||
// Monitor for any hard errors from reading the connection
|
||||
case err := <-c.readErrors:
|
||||
return err
|
||||
// Otherwise, wait and dequeue datagrams as they come in
|
||||
// Wait and dequeue datagrams as they come in
|
||||
case d := <-c.datagrams:
|
||||
datagram = d
|
||||
}
|
||||
|
||||
// Each incoming datagram will be processed in a new go routine to handle the demuxing and action associated.
|
||||
go func() {
|
||||
typ, err := ParseDatagramType(datagram)
|
||||
typ, err := ParseDatagramType(datagram)
|
||||
if err != nil {
|
||||
c.logger.Err(err).Msgf("unable to parse datagram type: %d", typ)
|
||||
continue
|
||||
}
|
||||
switch typ {
|
||||
case UDPSessionRegistrationType:
|
||||
reg := &UDPSessionRegistrationDatagram{}
|
||||
err := reg.UnmarshalBinary(datagram)
|
||||
if err != nil {
|
||||
c.logger.Err(err).Msgf("unable to parse datagram type: %d", typ)
|
||||
return
|
||||
c.logger.Err(err).Msgf("unable to unmarshal session registration datagram")
|
||||
continue
|
||||
}
|
||||
switch typ {
|
||||
case UDPSessionRegistrationType:
|
||||
reg := &UDPSessionRegistrationDatagram{}
|
||||
err := reg.UnmarshalBinary(datagram)
|
||||
if err != nil {
|
||||
c.logger.Err(err).Msgf("unable to unmarshal session registration datagram")
|
||||
return
|
||||
}
|
||||
logger := c.logger.With().Str(logFlowID, reg.RequestID.String()).Logger()
|
||||
// We bind the new session to the quic connection context instead of cloudflared context to allow for the
|
||||
// quic connection to close and close only the sessions bound to it. Closing of cloudflared will also
|
||||
// initiate the close of the quic connection, so we don't have to worry about the application context
|
||||
// in the scope of a session.
|
||||
c.handleSessionRegistrationDatagram(connCtx, reg, &logger)
|
||||
case UDPSessionPayloadType:
|
||||
payload := &UDPSessionPayloadDatagram{}
|
||||
err := payload.UnmarshalBinary(datagram)
|
||||
if err != nil {
|
||||
c.logger.Err(err).Msgf("unable to unmarshal session payload datagram")
|
||||
return
|
||||
}
|
||||
logger := c.logger.With().Str(logFlowID, payload.RequestID.String()).Logger()
|
||||
c.handleSessionPayloadDatagram(payload, &logger)
|
||||
case ICMPType:
|
||||
packet := &ICMPDatagram{}
|
||||
err := packet.UnmarshalBinary(datagram)
|
||||
if err != nil {
|
||||
c.logger.Err(err).Msgf("unable to unmarshal icmp datagram")
|
||||
return
|
||||
}
|
||||
c.handleICMPPacket(packet)
|
||||
case UDPSessionRegistrationResponseType:
|
||||
// cloudflared should never expect to receive UDP session responses as it will not initiate new
|
||||
// sessions towards the edge.
|
||||
c.logger.Error().Msgf("unexpected datagram type received: %d", UDPSessionRegistrationResponseType)
|
||||
return
|
||||
default:
|
||||
c.logger.Error().Msgf("unknown datagram type received: %d", typ)
|
||||
logger := c.logger.With().Str(logFlowID, reg.RequestID.String()).Logger()
|
||||
// We bind the new session to the quic connection context instead of cloudflared context to allow for the
|
||||
// quic connection to close and close only the sessions bound to it. Closing of cloudflared will also
|
||||
// initiate the close of the quic connection, so we don't have to worry about the application context
|
||||
// in the scope of a session.
|
||||
//
|
||||
// Additionally, we spin out the registration into a separate go routine to handle the Serve'ing of the
|
||||
// session in a separate routine from the demuxer.
|
||||
go c.handleSessionRegistrationDatagram(connCtx, reg, &logger)
|
||||
case UDPSessionPayloadType:
|
||||
payload := &UDPSessionPayloadDatagram{}
|
||||
err := payload.UnmarshalBinary(datagram)
|
||||
if err != nil {
|
||||
c.logger.Err(err).Msgf("unable to unmarshal session payload datagram")
|
||||
continue
|
||||
}
|
||||
}()
|
||||
logger := c.logger.With().Str(logFlowID, payload.RequestID.String()).Logger()
|
||||
c.handleSessionPayloadDatagram(payload, &logger)
|
||||
case ICMPType:
|
||||
packet := &ICMPDatagram{}
|
||||
err := packet.UnmarshalBinary(datagram)
|
||||
if err != nil {
|
||||
c.logger.Err(err).Msgf("unable to unmarshal icmp datagram")
|
||||
continue
|
||||
}
|
||||
c.handleICMPPacket(packet)
|
||||
case UDPSessionRegistrationResponseType:
|
||||
// cloudflared should never expect to receive UDP session responses as it will not initiate new
|
||||
// sessions towards the edge.
|
||||
c.logger.Error().Msgf("unexpected datagram type received: %d", UDPSessionRegistrationResponseType)
|
||||
continue
|
||||
default:
|
||||
c.logger.Error().Msgf("unknown datagram type received: %d", typ)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,24 +252,21 @@ func (c *datagramConn) handleSessionRegistrationDatagram(ctx context.Context, da
|
||||
Str(logDstKey, datagram.Dest.String()).
|
||||
Logger()
|
||||
session, err := c.sessionManager.RegisterSession(datagram, c)
|
||||
switch err {
|
||||
case nil:
|
||||
// Continue as normal
|
||||
case ErrSessionAlreadyRegistered:
|
||||
// Session is already registered and likely the response got lost
|
||||
c.handleSessionAlreadyRegistered(datagram.RequestID, &log)
|
||||
return
|
||||
case ErrSessionBoundToOtherConn:
|
||||
// Session is already registered but to a different connection
|
||||
c.handleSessionMigration(datagram.RequestID, &log)
|
||||
return
|
||||
case ErrSessionRegistrationRateLimited:
|
||||
// There are too many concurrent sessions so we return an error to force a retry later
|
||||
c.handleSessionRegistrationRateLimited(datagram, &log)
|
||||
return
|
||||
default:
|
||||
log.Err(err).Msg("flow registration failure")
|
||||
c.handleSessionRegistrationFailure(datagram.RequestID, &log)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case ErrSessionAlreadyRegistered:
|
||||
// Session is already registered and likely the response got lost
|
||||
c.handleSessionAlreadyRegistered(datagram.RequestID, &log)
|
||||
case ErrSessionBoundToOtherConn:
|
||||
// Session is already registered but to a different connection
|
||||
c.handleSessionMigration(datagram.RequestID, &log)
|
||||
case ErrSessionRegistrationRateLimited:
|
||||
// There are too many concurrent sessions so we return an error to force a retry later
|
||||
c.handleSessionRegistrationRateLimited(datagram, &log)
|
||||
default:
|
||||
log.Err(err).Msg("flow registration failure")
|
||||
c.handleSessionRegistrationFailure(datagram.RequestID, &log)
|
||||
}
|
||||
return
|
||||
}
|
||||
log = log.With().Str(logSrcKey, session.LocalAddr().String()).Logger()
|
||||
@@ -362,30 +368,54 @@ func (c *datagramConn) handleSessionRegistrationRateLimited(datagram *UDPSession
|
||||
func (c *datagramConn) handleSessionPayloadDatagram(datagram *UDPSessionPayloadDatagram, logger *zerolog.Logger) {
|
||||
s, err := c.sessionManager.GetSession(datagram.RequestID)
|
||||
if err != nil {
|
||||
c.metrics.DroppedUDPDatagram(c.index, DroppedWriteFlowUnknown)
|
||||
logger.Err(err).Msgf("unable to find flow")
|
||||
return
|
||||
}
|
||||
// We ignore the bytes written to the socket because any partial write must return an error.
|
||||
_, err = s.Write(datagram.Payload)
|
||||
if err != nil {
|
||||
logger.Err(err).Msgf("unable to write payload for the flow")
|
||||
return
|
||||
}
|
||||
s.Write(datagram.Payload)
|
||||
}
|
||||
|
||||
// Handles incoming ICMP datagrams.
|
||||
// Handles incoming ICMP datagrams into a serialized channel to be handled by a single consumer.
|
||||
func (c *datagramConn) handleICMPPacket(datagram *ICMPDatagram) {
|
||||
if c.icmpRouter == nil {
|
||||
// ICMPRouter is disabled so we drop the current packet and ignore all incoming ICMP packets
|
||||
return
|
||||
}
|
||||
select {
|
||||
case c.icmpDatagramChan <- datagram:
|
||||
default:
|
||||
// If the ICMP datagram channel is full, drop any additional incoming.
|
||||
c.metrics.DroppedICMPPackets(c.index, DroppedWriteFull)
|
||||
c.logger.Warn().Msg("failed to write icmp packet to origin: dropped")
|
||||
}
|
||||
}
|
||||
|
||||
// Consumes from the ICMP datagram channel to write out the ICMP requests to an origin.
|
||||
func (c *datagramConn) processICMPDatagrams(ctx context.Context) {
|
||||
if c.icmpRouter == nil {
|
||||
// ICMPRouter is disabled so we ignore all incoming ICMP packets
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
// If the provided context is closed we want to exit the write loop
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case datagram := <-c.icmpDatagramChan:
|
||||
c.writeICMPPacket(datagram)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *datagramConn) writeICMPPacket(datagram *ICMPDatagram) {
|
||||
// Decode the provided ICMPDatagram as an ICMP packet
|
||||
rawPacket := packet.RawPacket{Data: datagram.Payload}
|
||||
cachedDecoder := c.icmpDecoderPool.Get()
|
||||
defer c.icmpDecoderPool.Put(cachedDecoder)
|
||||
decoder, ok := cachedDecoder.(*packet.ICMPDecoder)
|
||||
if !ok {
|
||||
c.metrics.DroppedICMPPackets(c.index, DroppedWriteFailed)
|
||||
c.logger.Error().Msg("Could not get ICMPDecoder from the pool. Dropping packet")
|
||||
return
|
||||
}
|
||||
@@ -393,6 +423,7 @@ func (c *datagramConn) handleICMPPacket(datagram *ICMPDatagram) {
|
||||
icmp, err := decoder.Decode(rawPacket)
|
||||
|
||||
if err != nil {
|
||||
c.metrics.DroppedICMPPackets(c.index, DroppedWriteFailed)
|
||||
c.logger.Err(err).Msgf("unable to marshal icmp packet")
|
||||
return
|
||||
}
|
||||
@@ -400,6 +431,7 @@ func (c *datagramConn) handleICMPPacket(datagram *ICMPDatagram) {
|
||||
// If the ICMP packet's TTL is expired, we won't send it to the origin and immediately return a TTL Exceeded Message
|
||||
if icmp.TTL <= 1 {
|
||||
if err := c.SendICMPTTLExceed(icmp, rawPacket); err != nil {
|
||||
c.metrics.DroppedICMPPackets(c.index, DroppedWriteFailed)
|
||||
c.logger.Err(err).Msg("failed to return ICMP TTL exceed error")
|
||||
}
|
||||
return
|
||||
@@ -411,6 +443,7 @@ func (c *datagramConn) handleICMPPacket(datagram *ICMPDatagram) {
|
||||
// connection context which will have no tracing information available.
|
||||
err = c.icmpRouter.Request(c.conn.Context(), icmp, newPacketResponder(c, c.index))
|
||||
if err != nil {
|
||||
c.metrics.DroppedICMPPackets(c.index, DroppedWriteFailed)
|
||||
c.logger.Err(err).
|
||||
Str(logSrcKey, icmp.Src.String()).
|
||||
Str(logDstKey, icmp.Dst.String()).
|
||||
|
||||
+130
-42
@@ -13,6 +13,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fortytw2/leaktest"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -88,7 +89,11 @@ func (m *mockEyeball) SendICMPTTLExceed(icmp *packet.ICMP, rawPacket packet.RawP
|
||||
|
||||
func TestDatagramConn_New(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
conn := v3.NewDatagramConn(newMockQuicConn(), v3.NewSessionManager(&noopMetrics{}, &log, ingress.DialUDPAddrPort, cfdflow.NewLimiter(0)), &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 0,
|
||||
}, &log)
|
||||
conn := v3.NewDatagramConn(newMockQuicConn(t.Context()), v3.NewSessionManager(&noopMetrics{}, &log, originDialerService, cfdflow.NewLimiter(0)), &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
if conn == nil {
|
||||
t.Fatal("expected valid connection")
|
||||
}
|
||||
@@ -96,8 +101,14 @@ func TestDatagramConn_New(t *testing.T) {
|
||||
|
||||
func TestDatagramConn_SendUDPSessionDatagram(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
quic := newMockQuicConn()
|
||||
conn := v3.NewDatagramConn(quic, v3.NewSessionManager(&noopMetrics{}, &log, ingress.DialUDPAddrPort, cfdflow.NewLimiter(0)), &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 0,
|
||||
}, &log)
|
||||
connCtx, connCancel := context.WithCancelCause(t.Context())
|
||||
defer connCancel(context.Canceled)
|
||||
quic := newMockQuicConn(connCtx)
|
||||
conn := v3.NewDatagramConn(quic, v3.NewSessionManager(&noopMetrics{}, &log, originDialerService, cfdflow.NewLimiter(0)), &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
|
||||
payload := []byte{0xef, 0xef}
|
||||
err := conn.SendUDPSessionDatagram(payload)
|
||||
@@ -111,8 +122,14 @@ func TestDatagramConn_SendUDPSessionDatagram(t *testing.T) {
|
||||
|
||||
func TestDatagramConn_SendUDPSessionResponse(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
quic := newMockQuicConn()
|
||||
conn := v3.NewDatagramConn(quic, v3.NewSessionManager(&noopMetrics{}, &log, ingress.DialUDPAddrPort, cfdflow.NewLimiter(0)), &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 0,
|
||||
}, &log)
|
||||
connCtx, connCancel := context.WithCancelCause(t.Context())
|
||||
defer connCancel(context.Canceled)
|
||||
quic := newMockQuicConn(connCtx)
|
||||
conn := v3.NewDatagramConn(quic, v3.NewSessionManager(&noopMetrics{}, &log, originDialerService, cfdflow.NewLimiter(0)), &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
|
||||
err := conn.SendUDPSessionResponse(testRequestID, v3.ResponseDestinationUnreachable)
|
||||
require.NoError(t, err)
|
||||
@@ -133,8 +150,14 @@ func TestDatagramConn_SendUDPSessionResponse(t *testing.T) {
|
||||
|
||||
func TestDatagramConnServe_ApplicationClosed(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
quic := newMockQuicConn()
|
||||
conn := v3.NewDatagramConn(quic, v3.NewSessionManager(&noopMetrics{}, &log, ingress.DialUDPAddrPort, cfdflow.NewLimiter(0)), &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 0,
|
||||
}, &log)
|
||||
connCtx, connCancel := context.WithCancelCause(t.Context())
|
||||
defer connCancel(context.Canceled)
|
||||
quic := newMockQuicConn(connCtx)
|
||||
conn := v3.NewDatagramConn(quic, v3.NewSessionManager(&noopMetrics{}, &log, originDialerService, cfdflow.NewLimiter(0)), &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 1*time.Second)
|
||||
defer cancel()
|
||||
@@ -146,11 +169,17 @@ func TestDatagramConnServe_ApplicationClosed(t *testing.T) {
|
||||
|
||||
func TestDatagramConnServe_ConnectionClosed(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
quic := newMockQuicConn()
|
||||
originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 0,
|
||||
}, &log)
|
||||
connCtx, connCancel := context.WithCancelCause(t.Context())
|
||||
defer connCancel(context.Canceled)
|
||||
quic := newMockQuicConn(connCtx)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 1*time.Second)
|
||||
defer cancel()
|
||||
quic.ctx = ctx
|
||||
conn := v3.NewDatagramConn(quic, v3.NewSessionManager(&noopMetrics{}, &log, ingress.DialUDPAddrPort, cfdflow.NewLimiter(0)), &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
conn := v3.NewDatagramConn(quic, v3.NewSessionManager(&noopMetrics{}, &log, originDialerService, cfdflow.NewLimiter(0)), &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
|
||||
err := conn.Serve(t.Context())
|
||||
if !errors.Is(err, context.DeadlineExceeded) {
|
||||
@@ -160,8 +189,12 @@ func TestDatagramConnServe_ConnectionClosed(t *testing.T) {
|
||||
|
||||
func TestDatagramConnServe_ReceiveDatagramError(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
originDialerService := ingress.NewOriginDialer(ingress.OriginConfig{
|
||||
DefaultDialer: testDefaultDialer,
|
||||
TCPWriteTimeout: 0,
|
||||
}, &log)
|
||||
quic := &mockQuicConnReadError{err: net.ErrClosed}
|
||||
conn := v3.NewDatagramConn(quic, v3.NewSessionManager(&noopMetrics{}, &log, ingress.DialUDPAddrPort, cfdflow.NewLimiter(0)), &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
conn := v3.NewDatagramConn(quic, v3.NewSessionManager(&noopMetrics{}, &log, originDialerService, cfdflow.NewLimiter(0)), &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
|
||||
err := conn.Serve(t.Context())
|
||||
if !errors.Is(err, net.ErrClosed) {
|
||||
@@ -171,15 +204,17 @@ func TestDatagramConnServe_ReceiveDatagramError(t *testing.T) {
|
||||
|
||||
func TestDatagramConnServe_SessionRegistrationRateLimit(t *testing.T) {
|
||||
log := zerolog.Nop()
|
||||
quic := newMockQuicConn()
|
||||
connCtx, connCancel := context.WithCancelCause(t.Context())
|
||||
defer connCancel(context.Canceled)
|
||||
quic := newMockQuicConn(connCtx)
|
||||
sessionManager := &mockSessionManager{
|
||||
expectedRegErr: v3.ErrSessionRegistrationRateLimited,
|
||||
}
|
||||
conn := v3.NewDatagramConn(quic, sessionManager, &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
|
||||
// Setup the muxer
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
ctx, cancel := context.WithCancelCause(t.Context())
|
||||
defer cancel(context.Canceled)
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- conn.Serve(ctx)
|
||||
@@ -199,9 +234,12 @@ func TestDatagramConnServe_SessionRegistrationRateLimit(t *testing.T) {
|
||||
|
||||
require.EqualValues(t, testRequestID, resp.RequestID)
|
||||
require.EqualValues(t, v3.ResponseTooManyActiveFlows, resp.ResponseType)
|
||||
|
||||
assertContextClosed(t, ctx, done, cancel)
|
||||
}
|
||||
|
||||
func TestDatagramConnServe_ErrorDatagramTypes(t *testing.T) {
|
||||
defer leaktest.Check(t)()
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
input []byte
|
||||
@@ -226,7 +264,9 @@ func TestDatagramConnServe_ErrorDatagramTypes(t *testing.T) {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
logOutput := new(LockedBuffer)
|
||||
log := zerolog.New(logOutput)
|
||||
quic := newMockQuicConn()
|
||||
connCtx, connCancel := context.WithCancelCause(t.Context())
|
||||
defer connCancel(context.Canceled)
|
||||
quic := newMockQuicConn(connCtx)
|
||||
quic.send <- test.input
|
||||
conn := v3.NewDatagramConn(quic, &mockSessionManager{}, &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
|
||||
@@ -265,8 +305,11 @@ func (b *LockedBuffer) String() string {
|
||||
}
|
||||
|
||||
func TestDatagramConnServe_RegisterSession_SessionManagerError(t *testing.T) {
|
||||
defer leaktest.Check(t)()
|
||||
log := zerolog.Nop()
|
||||
quic := newMockQuicConn()
|
||||
connCtx, connCancel := context.WithCancelCause(t.Context())
|
||||
defer connCancel(context.Canceled)
|
||||
quic := newMockQuicConn(connCtx)
|
||||
expectedErr := errors.New("unable to register session")
|
||||
sessionManager := mockSessionManager{expectedRegErr: expectedErr}
|
||||
conn := v3.NewDatagramConn(quic, &sessionManager, &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
@@ -300,8 +343,11 @@ func TestDatagramConnServe_RegisterSession_SessionManagerError(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDatagramConnServe(t *testing.T) {
|
||||
defer leaktest.Check(t)()
|
||||
log := zerolog.Nop()
|
||||
quic := newMockQuicConn()
|
||||
connCtx, connCancel := context.WithCancelCause(t.Context())
|
||||
defer connCancel(context.Canceled)
|
||||
quic := newMockQuicConn(connCtx)
|
||||
session := newMockSession()
|
||||
sessionManager := mockSessionManager{session: &session}
|
||||
conn := v3.NewDatagramConn(quic, &sessionManager, &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
@@ -348,8 +394,11 @@ func TestDatagramConnServe(t *testing.T) {
|
||||
// instances causes inteference resulting in multiple different raw packets being decoded
|
||||
// as the same decoded packet.
|
||||
func TestDatagramConnServeDecodeMultipleICMPInParallel(t *testing.T) {
|
||||
defer leaktest.Check(t)()
|
||||
log := zerolog.Nop()
|
||||
quic := newMockQuicConn()
|
||||
connCtx, connCancel := context.WithCancelCause(t.Context())
|
||||
defer connCancel(context.Canceled)
|
||||
quic := newMockQuicConn(connCtx)
|
||||
session := newMockSession()
|
||||
sessionManager := mockSessionManager{session: &session}
|
||||
router := newMockICMPRouter()
|
||||
@@ -389,10 +438,14 @@ func TestDatagramConnServeDecodeMultipleICMPInParallel(t *testing.T) {
|
||||
wg := sync.WaitGroup{}
|
||||
var receivedPackets []*packet.ICMP
|
||||
go func() {
|
||||
for ctx.Err() == nil {
|
||||
icmpPacket := <-router.recv
|
||||
receivedPackets = append(receivedPackets, icmpPacket)
|
||||
wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case icmpPacket := <-router.recv:
|
||||
receivedPackets = append(receivedPackets, icmpPacket)
|
||||
wg.Done()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -428,8 +481,11 @@ func TestDatagramConnServeDecodeMultipleICMPInParallel(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDatagramConnServe_RegisterTwice(t *testing.T) {
|
||||
defer leaktest.Check(t)()
|
||||
log := zerolog.Nop()
|
||||
quic := newMockQuicConn()
|
||||
connCtx, connCancel := context.WithCancelCause(t.Context())
|
||||
defer connCancel(context.Canceled)
|
||||
quic := newMockQuicConn(connCtx)
|
||||
session := newMockSession()
|
||||
sessionManager := mockSessionManager{session: &session}
|
||||
conn := v3.NewDatagramConn(quic, &sessionManager, &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
@@ -490,12 +546,17 @@ func TestDatagramConnServe_RegisterTwice(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDatagramConnServe_MigrateConnection(t *testing.T) {
|
||||
defer leaktest.Check(t)()
|
||||
log := zerolog.Nop()
|
||||
quic := newMockQuicConn()
|
||||
connCtx, connCancel := context.WithCancelCause(t.Context())
|
||||
defer connCancel(context.Canceled)
|
||||
quic := newMockQuicConn(connCtx)
|
||||
session := newMockSession()
|
||||
sessionManager := mockSessionManager{session: &session}
|
||||
conn := v3.NewDatagramConn(quic, &sessionManager, &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
quic2 := newMockQuicConn()
|
||||
conn2Ctx, conn2Cancel := context.WithCancelCause(t.Context())
|
||||
defer conn2Cancel(context.Canceled)
|
||||
quic2 := newMockQuicConn(conn2Ctx)
|
||||
conn2 := v3.NewDatagramConn(quic2, &sessionManager, &noopICMPRouter{}, 1, &noopMetrics{}, &log)
|
||||
|
||||
// Setup the muxer
|
||||
@@ -573,8 +634,11 @@ func TestDatagramConnServe_MigrateConnection(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDatagramConnServe_Payload_GetSessionError(t *testing.T) {
|
||||
defer leaktest.Check(t)()
|
||||
log := zerolog.Nop()
|
||||
quic := newMockQuicConn()
|
||||
connCtx, connCancel := context.WithCancelCause(t.Context())
|
||||
defer connCancel(context.Canceled)
|
||||
quic := newMockQuicConn(connCtx)
|
||||
// mockSessionManager will return the ErrSessionNotFound for any session attempting to be queried by the muxer
|
||||
sessionManager := mockSessionManager{session: nil, expectedGetErr: v3.ErrSessionNotFound}
|
||||
conn := v3.NewDatagramConn(quic, &sessionManager, &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
@@ -600,9 +664,12 @@ func TestDatagramConnServe_Payload_GetSessionError(t *testing.T) {
|
||||
assertContextClosed(t, ctx, done, cancel)
|
||||
}
|
||||
|
||||
func TestDatagramConnServe_Payload(t *testing.T) {
|
||||
func TestDatagramConnServe_Payloads(t *testing.T) {
|
||||
defer leaktest.Check(t)()
|
||||
log := zerolog.Nop()
|
||||
quic := newMockQuicConn()
|
||||
connCtx, connCancel := context.WithCancelCause(t.Context())
|
||||
defer connCancel(context.Canceled)
|
||||
quic := newMockQuicConn(connCtx)
|
||||
session := newMockSession()
|
||||
sessionManager := mockSessionManager{session: &session}
|
||||
conn := v3.NewDatagramConn(quic, &sessionManager, &noopICMPRouter{}, 0, &noopMetrics{}, &log)
|
||||
@@ -615,15 +682,26 @@ func TestDatagramConnServe_Payload(t *testing.T) {
|
||||
done <- conn.Serve(ctx)
|
||||
}()
|
||||
|
||||
// Send new session registration
|
||||
expectedPayload := []byte{0xef, 0xef}
|
||||
datagram := newSessionPayloadDatagram(testRequestID, expectedPayload)
|
||||
quic.send <- datagram
|
||||
// Send session payloads
|
||||
expectedPayloads := makePayloads(256, 16)
|
||||
go func() {
|
||||
for _, payload := range expectedPayloads {
|
||||
datagram := newSessionPayloadDatagram(testRequestID, payload)
|
||||
quic.send <- datagram
|
||||
}
|
||||
}()
|
||||
|
||||
// Session should receive the payload
|
||||
payload := <-session.recv
|
||||
if !slices.Equal(expectedPayload, payload) {
|
||||
t.Fatalf("expected session receieve the payload sent via the muxer")
|
||||
// Session should receive the payloads (in-order)
|
||||
for i, payload := range expectedPayloads {
|
||||
select {
|
||||
case recv := <-session.recv:
|
||||
if !slices.Equal(recv, payload) {
|
||||
t.Fatalf("expected session receieve the payload[%d] sent via the muxer: (%x) (%x)", i, recv[:16], payload[:16])
|
||||
}
|
||||
case err := <-ctx.Done():
|
||||
// we expect the payload to return before the context to cancel on the session
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel the muxer Serve context and make sure it closes with the expected error
|
||||
@@ -631,8 +709,11 @@ func TestDatagramConnServe_Payload(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDatagramConnServe_ICMPDatagram_TTLDecremented(t *testing.T) {
|
||||
defer leaktest.Check(t)()
|
||||
log := zerolog.Nop()
|
||||
quic := newMockQuicConn()
|
||||
connCtx, connCancel := context.WithCancelCause(t.Context())
|
||||
defer connCancel(context.Canceled)
|
||||
quic := newMockQuicConn(connCtx)
|
||||
router := newMockICMPRouter()
|
||||
conn := v3.NewDatagramConn(quic, &mockSessionManager{}, router, 0, &noopMetrics{}, &log)
|
||||
|
||||
@@ -677,8 +758,11 @@ func TestDatagramConnServe_ICMPDatagram_TTLDecremented(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDatagramConnServe_ICMPDatagram_TTLExceeded(t *testing.T) {
|
||||
defer leaktest.Check(t)()
|
||||
log := zerolog.Nop()
|
||||
quic := newMockQuicConn()
|
||||
connCtx, connCancel := context.WithCancelCause(t.Context())
|
||||
defer connCancel(context.Canceled)
|
||||
quic := newMockQuicConn(connCtx)
|
||||
router := newMockICMPRouter()
|
||||
conn := v3.NewDatagramConn(quic, &mockSessionManager{}, router, 0, &noopMetrics{}, &log)
|
||||
|
||||
@@ -797,9 +881,9 @@ type mockQuicConn struct {
|
||||
recv chan []byte
|
||||
}
|
||||
|
||||
func newMockQuicConn() *mockQuicConn {
|
||||
func newMockQuicConn(ctx context.Context) *mockQuicConn {
|
||||
return &mockQuicConn{
|
||||
ctx: context.Background(),
|
||||
ctx: ctx,
|
||||
send: make(chan []byte, 1),
|
||||
recv: make(chan []byte, 1),
|
||||
}
|
||||
@@ -817,7 +901,12 @@ func (m *mockQuicConn) SendDatagram(payload []byte) error {
|
||||
}
|
||||
|
||||
func (m *mockQuicConn) ReceiveDatagram(_ context.Context) ([]byte, error) {
|
||||
return <-m.send, nil
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
return nil, m.ctx.Err()
|
||||
case b := <-m.send:
|
||||
return b, nil
|
||||
}
|
||||
}
|
||||
|
||||
type mockQuicConnReadError struct {
|
||||
@@ -881,11 +970,10 @@ func (m *mockSession) Serve(ctx context.Context) error {
|
||||
return v3.SessionCloseErr
|
||||
}
|
||||
|
||||
func (m *mockSession) Write(payload []byte) (n int, err error) {
|
||||
func (m *mockSession) Write(payload []byte) {
|
||||
b := make([]byte, len(payload))
|
||||
copy(b, payload)
|
||||
m.recv <- b
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (m *mockSession) Close() error {
|
||||
|
||||
+140
-67
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -22,6 +23,11 @@ const (
|
||||
// this value (maxDatagramPayloadLen).
|
||||
maxOriginUDPPacketSize = 1500
|
||||
|
||||
// The maximum amount of datagrams a session will queue up before it begins dropping datagrams.
|
||||
// This channel buffer is small because we assume that the dedicated writer to the origin is typically
|
||||
// fast enought to keep the channel empty.
|
||||
writeChanCapacity = 512
|
||||
|
||||
logFlowID = "flowID"
|
||||
logPacketSizeKey = "packetSize"
|
||||
)
|
||||
@@ -49,7 +55,7 @@ func newSessionIdleErr(timeout time.Duration) error {
|
||||
}
|
||||
|
||||
type Session interface {
|
||||
io.WriteCloser
|
||||
io.Closer
|
||||
ID() RequestID
|
||||
ConnectionID() uint8
|
||||
RemoteAddr() net.Addr
|
||||
@@ -58,6 +64,7 @@ type Session interface {
|
||||
Migrate(eyeball DatagramConn, ctx context.Context, logger *zerolog.Logger)
|
||||
// Serve starts the event loop for processing UDP packets
|
||||
Serve(ctx context.Context) error
|
||||
Write(payload []byte)
|
||||
}
|
||||
|
||||
type session struct {
|
||||
@@ -67,12 +74,18 @@ type session struct {
|
||||
originAddr net.Addr
|
||||
localAddr net.Addr
|
||||
eyeball atomic.Pointer[DatagramConn]
|
||||
writeChan chan []byte
|
||||
// activeAtChan is used to communicate the last read/write time
|
||||
activeAtChan chan time.Time
|
||||
closeChan chan error
|
||||
contextChan chan context.Context
|
||||
metrics Metrics
|
||||
log *zerolog.Logger
|
||||
errChan chan error
|
||||
// The close channel signal only exists for the write loop because the read loop is always waiting on a read
|
||||
// from the UDP socket to the origin. To close the read loop we close the socket.
|
||||
// Additionally, we can't close the writeChan to indicate that writes are complete because the producer (edge)
|
||||
// side may still be trying to write to this session.
|
||||
closeWrite chan struct{}
|
||||
contextChan chan context.Context
|
||||
metrics Metrics
|
||||
log *zerolog.Logger
|
||||
|
||||
// A special close function that we wrap with sync.Once to make sure it is only called once
|
||||
closeFn func() error
|
||||
@@ -89,10 +102,12 @@ func NewSession(
|
||||
log *zerolog.Logger,
|
||||
) Session {
|
||||
logger := log.With().Str(logFlowID, id.String()).Logger()
|
||||
// closeChan has two slots to allow for both writers (the closeFn and the Serve routine) to both be able to
|
||||
// write to the channel without blocking since there is only ever one value read from the closeChan by the
|
||||
writeChan := make(chan []byte, writeChanCapacity)
|
||||
// errChan has three slots to allow for all writers (the closeFn, the read loop and the write loop) to
|
||||
// write to the channel without blocking since there is only ever one value read from the errChan by the
|
||||
// waitForCloseCondition.
|
||||
closeChan := make(chan error, 2)
|
||||
errChan := make(chan error, 3)
|
||||
closeWrite := make(chan struct{})
|
||||
session := &session{
|
||||
id: id,
|
||||
closeAfterIdle: closeAfterIdle,
|
||||
@@ -100,10 +115,12 @@ func NewSession(
|
||||
originAddr: originAddr,
|
||||
localAddr: localAddr,
|
||||
eyeball: atomic.Pointer[DatagramConn]{},
|
||||
writeChan: writeChan,
|
||||
// activeAtChan has low capacity. It can be full when there are many concurrent read/write. markActive() will
|
||||
// drop instead of blocking because last active time only needs to be an approximation
|
||||
activeAtChan: make(chan time.Time, 1),
|
||||
closeChan: closeChan,
|
||||
errChan: errChan,
|
||||
closeWrite: closeWrite,
|
||||
// contextChan is an unbounded channel to help enforce one active migration of a session at a time.
|
||||
contextChan: make(chan context.Context),
|
||||
metrics: metrics,
|
||||
@@ -111,9 +128,12 @@ func NewSession(
|
||||
closeFn: sync.OnceValue(func() error {
|
||||
// We don't want to block on sending to the close channel if it is already full
|
||||
select {
|
||||
case closeChan <- SessionCloseErr:
|
||||
case errChan <- SessionCloseErr:
|
||||
default:
|
||||
}
|
||||
// Indicate to the write loop that the session is now closed
|
||||
close(closeWrite)
|
||||
// Close the socket directly to unblock the read loop and cause it to also end
|
||||
return origin.Close()
|
||||
}),
|
||||
}
|
||||
@@ -154,66 +174,115 @@ func (s *session) Migrate(eyeball DatagramConn, ctx context.Context, logger *zer
|
||||
}
|
||||
|
||||
func (s *session) Serve(ctx context.Context) error {
|
||||
go func() {
|
||||
// QUIC implementation copies data to another buffer before returning https://github.com/quic-go/quic-go/blob/v0.24.0/session.go#L1967-L1975
|
||||
// This makes it safe to share readBuffer between iterations
|
||||
readBuffer := [maxOriginUDPPacketSize + DatagramPayloadHeaderLen]byte{}
|
||||
// To perform a zero copy write when passing the datagram to the connection, we prepare the buffer with
|
||||
// the required datagram header information. We can reuse this buffer for this session since the header is the
|
||||
// same for the each read.
|
||||
_ = MarshalPayloadHeaderTo(s.id, readBuffer[:DatagramPayloadHeaderLen])
|
||||
for {
|
||||
// Read from the origin UDP socket
|
||||
n, err := s.origin.Read(readBuffer[DatagramPayloadHeaderLen:])
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) ||
|
||||
errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
s.log.Debug().Msgf("flow (origin) connection closed: %v", err)
|
||||
}
|
||||
s.closeChan <- err
|
||||
return
|
||||
}
|
||||
if n < 0 {
|
||||
s.log.Warn().Int(logPacketSizeKey, n).Msg("flow (origin) packet read was negative and was dropped")
|
||||
continue
|
||||
}
|
||||
if n > maxDatagramPayloadLen {
|
||||
connectionIndex := s.ConnectionID()
|
||||
s.metrics.PayloadTooLarge(connectionIndex)
|
||||
s.log.Error().Int(logPacketSizeKey, n).Msg("flow (origin) packet read was too large and was dropped")
|
||||
continue
|
||||
}
|
||||
// We need to synchronize on the eyeball in-case that the connection was migrated. This should be rarely a point
|
||||
// of lock contention, as a migration can only happen during startup of a session before traffic flow.
|
||||
eyeball := *(s.eyeball.Load())
|
||||
// Sending a packet to the session does block on the [quic.Connection], however, this is okay because it
|
||||
// will cause back-pressure to the kernel buffer if the writes are not fast enough to the edge.
|
||||
err = eyeball.SendUDPSessionDatagram(readBuffer[:DatagramPayloadHeaderLen+n])
|
||||
if err != nil {
|
||||
s.closeChan <- err
|
||||
return
|
||||
}
|
||||
// Mark the session as active since we proxied a valid packet from the origin.
|
||||
s.markActive()
|
||||
}
|
||||
}()
|
||||
go s.writeLoop()
|
||||
go s.readLoop()
|
||||
return s.waitForCloseCondition(ctx, s.closeAfterIdle)
|
||||
}
|
||||
|
||||
func (s *session) Write(payload []byte) (n int, err error) {
|
||||
n, err = s.origin.Write(payload)
|
||||
if err != nil {
|
||||
s.log.Err(err).Msg("failed to write payload to flow (remote)")
|
||||
return n, err
|
||||
// Read datagrams from the origin and write them to the connection.
|
||||
func (s *session) readLoop() {
|
||||
// QUIC implementation copies data to another buffer before returning https://github.com/quic-go/quic-go/blob/v0.24.0/session.go#L1967-L1975
|
||||
// This makes it safe to share readBuffer between iterations
|
||||
readBuffer := [maxOriginUDPPacketSize + DatagramPayloadHeaderLen]byte{}
|
||||
// To perform a zero copy write when passing the datagram to the connection, we prepare the buffer with
|
||||
// the required datagram header information. We can reuse this buffer for this session since the header is the
|
||||
// same for the each read.
|
||||
_ = MarshalPayloadHeaderTo(s.id, readBuffer[:DatagramPayloadHeaderLen])
|
||||
for {
|
||||
// Read from the origin UDP socket
|
||||
n, err := s.origin.Read(readBuffer[DatagramPayloadHeaderLen:])
|
||||
if err != nil {
|
||||
if isConnectionClosed(err) {
|
||||
s.log.Debug().Msgf("flow (read) connection closed: %v", err)
|
||||
}
|
||||
s.closeSession(err)
|
||||
return
|
||||
}
|
||||
if n < 0 {
|
||||
s.metrics.DroppedUDPDatagram(s.ConnectionID(), DroppedReadFailed)
|
||||
s.log.Warn().Int(logPacketSizeKey, n).Msg("flow (origin) packet read was negative and was dropped")
|
||||
continue
|
||||
}
|
||||
if n > maxDatagramPayloadLen {
|
||||
s.metrics.DroppedUDPDatagram(s.ConnectionID(), DroppedReadTooLarge)
|
||||
s.log.Error().Int(logPacketSizeKey, n).Msg("flow (origin) packet read was too large and was dropped")
|
||||
continue
|
||||
}
|
||||
// We need to synchronize on the eyeball in-case that the connection was migrated. This should be rarely a point
|
||||
// of lock contention, as a migration can only happen during startup of a session before traffic flow.
|
||||
eyeball := *(s.eyeball.Load())
|
||||
// Sending a packet to the session does block on the [quic.Connection], however, this is okay because it
|
||||
// will cause back-pressure to the kernel buffer if the writes are not fast enough to the edge.
|
||||
err = eyeball.SendUDPSessionDatagram(readBuffer[:DatagramPayloadHeaderLen+n])
|
||||
if err != nil {
|
||||
s.closeSession(err)
|
||||
return
|
||||
}
|
||||
// Mark the session as active since we proxied a valid packet from the origin.
|
||||
s.markActive()
|
||||
}
|
||||
// Write must return a non-nil error if it returns n < len(p). https://pkg.go.dev/io#Writer
|
||||
if n < len(payload) {
|
||||
s.log.Err(io.ErrShortWrite).Msg("failed to write the full payload to flow (remote)")
|
||||
return n, io.ErrShortWrite
|
||||
}
|
||||
|
||||
func (s *session) Write(payload []byte) {
|
||||
select {
|
||||
case s.writeChan <- payload:
|
||||
default:
|
||||
s.metrics.DroppedUDPDatagram(s.ConnectionID(), DroppedWriteFull)
|
||||
s.log.Error().Msg("failed to write flow payload to origin: dropped")
|
||||
}
|
||||
}
|
||||
|
||||
// Read datagrams from the write channel to the origin.
|
||||
func (s *session) writeLoop() {
|
||||
for {
|
||||
select {
|
||||
case <-s.closeWrite:
|
||||
// When the closeWrite channel is closed, we will no longer write to the origin and end this
|
||||
// goroutine since the session is now closed.
|
||||
return
|
||||
case payload := <-s.writeChan:
|
||||
n, err := s.origin.Write(payload)
|
||||
if err != nil {
|
||||
// Check if this is a write deadline exceeded to the connection
|
||||
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
s.metrics.DroppedUDPDatagram(s.ConnectionID(), DroppedWriteDeadlineExceeded)
|
||||
s.log.Warn().Err(err).Msg("flow (write) deadline exceeded: dropping packet")
|
||||
continue
|
||||
}
|
||||
if isConnectionClosed(err) {
|
||||
s.log.Debug().Msgf("flow (write) connection closed: %v", err)
|
||||
}
|
||||
s.log.Err(err).Msg("failed to write flow payload to origin")
|
||||
s.closeSession(err)
|
||||
// If we fail to write to the origin socket, we need to end the writer and close the session
|
||||
return
|
||||
}
|
||||
// Write must return a non-nil error if it returns n < len(p). https://pkg.go.dev/io#Writer
|
||||
if n < len(payload) {
|
||||
s.metrics.DroppedUDPDatagram(s.ConnectionID(), DroppedWriteFailed)
|
||||
s.log.Err(io.ErrShortWrite).Msg("failed to write the full flow payload to origin")
|
||||
continue
|
||||
}
|
||||
// Mark the session as active since we successfully proxied a packet to the origin.
|
||||
s.markActive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isConnectionClosed(err error) bool {
|
||||
return errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF)
|
||||
}
|
||||
|
||||
// Send an error to the error channel to report that an error has either happened on the tunnel or origin side of the
|
||||
// proxied connection.
|
||||
func (s *session) closeSession(err error) {
|
||||
select {
|
||||
case s.errChan <- err:
|
||||
default:
|
||||
// In the case that the errChan is already full, we will skip over it and return as to not block
|
||||
// the caller because we should start cleaning up the session.
|
||||
s.log.Warn().Msg("error channel was full")
|
||||
}
|
||||
// Mark the session as active since we proxied a packet to the origin.
|
||||
s.markActive()
|
||||
return n, err
|
||||
}
|
||||
|
||||
// ResetIdleTimer will restart the current idle timer.
|
||||
@@ -240,7 +309,8 @@ func (s *session) Close() error {
|
||||
|
||||
func (s *session) waitForCloseCondition(ctx context.Context, closeAfterIdle time.Duration) error {
|
||||
connCtx := ctx
|
||||
// Closing the session at the end cancels read so Serve() can return
|
||||
// Closing the session at the end cancels read so Serve() can return, additionally, it closes the
|
||||
// closeWrite channel which indicates to the write loop to return.
|
||||
defer s.Close()
|
||||
if closeAfterIdle == 0 {
|
||||
// Provided that the default caller doesn't specify one
|
||||
@@ -260,7 +330,10 @@ func (s *session) waitForCloseCondition(ctx context.Context, closeAfterIdle time
|
||||
// still be active on the existing connection.
|
||||
connCtx = newContext
|
||||
continue
|
||||
case reason := <-s.closeChan:
|
||||
case reason := <-s.errChan:
|
||||
// Any error returned here is from the read or write loops indicating that it can no longer process datagrams
|
||||
// and as such the session needs to close.
|
||||
s.metrics.FailedFlow(s.ConnectionID())
|
||||
return reason
|
||||
case <-checkIdleTimer.C:
|
||||
// The check idle timer will only return after an idle period since the last active
|
||||
|
||||
@@ -4,20 +4,24 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// FuzzSessionWrite verifies that we don't run into any panics when writing variable sized payloads to the origin.
|
||||
// FuzzSessionWrite verifies that we don't run into any panics when writing a single variable sized payload to the origin.
|
||||
func FuzzSessionWrite(f *testing.F) {
|
||||
f.Fuzz(func(t *testing.T, b []byte) {
|
||||
testSessionWrite(t, b)
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzSessionServe verifies that we don't run into any panics when reading variable sized payloads from the origin.
|
||||
func FuzzSessionServe(f *testing.F) {
|
||||
f.Fuzz(func(t *testing.T, b []byte) {
|
||||
// The origin transport read is bound to 1280 bytes
|
||||
if len(b) > 1280 {
|
||||
b = b[:1280]
|
||||
}
|
||||
testSessionServe_Origin(t, b)
|
||||
testSessionWrite(t, [][]byte{b})
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzSessionRead verifies that we don't run into any panics when reading a single variable sized payload from the origin.
|
||||
func FuzzSessionRead(f *testing.F) {
|
||||
f.Fuzz(func(t *testing.T, b []byte) {
|
||||
// The origin transport read is bound to 1280 bytes
|
||||
if len(b) > 1280 {
|
||||
b = b[:1280]
|
||||
}
|
||||
testSessionRead(t, [][]byte{b})
|
||||
})
|
||||
}
|
||||
|
||||
+74
-66
@@ -31,60 +31,61 @@ func TestSessionNew(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func testSessionWrite(t *testing.T, payload []byte) {
|
||||
func testSessionWrite(t *testing.T, payloads [][]byte) {
|
||||
log := zerolog.Nop()
|
||||
origin, server := net.Pipe()
|
||||
defer origin.Close()
|
||||
defer server.Close()
|
||||
// Start origin server read
|
||||
serverRead := make(chan []byte, 1)
|
||||
// Start origin server reads
|
||||
serverRead := make(chan []byte, len(payloads))
|
||||
go func() {
|
||||
read := make([]byte, 1500)
|
||||
_, _ = server.Read(read[:])
|
||||
serverRead <- read
|
||||
for range len(payloads) {
|
||||
buf := make([]byte, 1500)
|
||||
_, _ = server.Read(buf[:])
|
||||
serverRead <- buf
|
||||
}
|
||||
close(serverRead)
|
||||
}()
|
||||
// Create session and write to origin
|
||||
|
||||
// Create a session
|
||||
session := v3.NewSession(testRequestID, 5*time.Second, origin, testOriginAddr, testLocalAddr, &noopEyeball{}, &noopMetrics{}, &log)
|
||||
n, err := session.Write(payload)
|
||||
defer session.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != len(payload) {
|
||||
t.Fatal("unable to write the whole payload")
|
||||
// Start the Serve to begin the writeLoop
|
||||
ctx, cancel := context.WithCancelCause(t.Context())
|
||||
defer cancel(context.Canceled)
|
||||
done := make(chan error)
|
||||
go func() {
|
||||
done <- session.Serve(ctx)
|
||||
}()
|
||||
// Write the payloads to the session
|
||||
for _, payload := range payloads {
|
||||
session.Write(payload)
|
||||
}
|
||||
|
||||
read := <-serverRead
|
||||
if !slices.Equal(payload, read[:len(payload)]) {
|
||||
t.Fatal("payload provided from origin and read value are not the same")
|
||||
// Read from the origin to ensure the payloads were received (in-order)
|
||||
for i, payload := range payloads {
|
||||
read := <-serverRead
|
||||
if !slices.Equal(payload, read[:len(payload)]) {
|
||||
t.Fatalf("payload[%d] provided from origin and read value are not the same (%x) and (%x)", i, payload[:16], read[:16])
|
||||
}
|
||||
}
|
||||
_, more := <-serverRead
|
||||
if more {
|
||||
t.Fatalf("expected the session to have all of the origin payloads received: %d", len(serverRead))
|
||||
}
|
||||
|
||||
assertContextClosed(t, ctx, done, cancel)
|
||||
}
|
||||
|
||||
func TestSessionWrite(t *testing.T) {
|
||||
defer leaktest.Check(t)()
|
||||
for i := range 1280 {
|
||||
payloads := makePayloads(i, 16)
|
||||
testSessionWrite(t, payloads)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionWrite_Max(t *testing.T) {
|
||||
defer leaktest.Check(t)()
|
||||
payload := makePayload(1280)
|
||||
testSessionWrite(t, payload)
|
||||
}
|
||||
|
||||
func TestSessionWrite_Min(t *testing.T) {
|
||||
defer leaktest.Check(t)()
|
||||
payload := makePayload(0)
|
||||
testSessionWrite(t, payload)
|
||||
}
|
||||
|
||||
func TestSessionServe_OriginMax(t *testing.T) {
|
||||
defer leaktest.Check(t)()
|
||||
payload := makePayload(1280)
|
||||
testSessionServe_Origin(t, payload)
|
||||
}
|
||||
|
||||
func TestSessionServe_OriginMin(t *testing.T) {
|
||||
defer leaktest.Check(t)()
|
||||
payload := makePayload(0)
|
||||
testSessionServe_Origin(t, payload)
|
||||
}
|
||||
|
||||
func testSessionServe_Origin(t *testing.T, payload []byte) {
|
||||
func testSessionRead(t *testing.T, payloads [][]byte) {
|
||||
log := zerolog.Nop()
|
||||
origin, server := net.Pipe()
|
||||
defer origin.Close()
|
||||
@@ -100,37 +101,42 @@ func testSessionServe_Origin(t *testing.T, payload []byte) {
|
||||
done <- session.Serve(ctx)
|
||||
}()
|
||||
|
||||
// Write from the origin server
|
||||
_, err := server.Write(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
select {
|
||||
case data := <-eyeball.recvData:
|
||||
// check received data matches provided from origin
|
||||
expectedData := makePayload(1500)
|
||||
_ = v3.MarshalPayloadHeaderTo(testRequestID, expectedData[:])
|
||||
copy(expectedData[17:], payload)
|
||||
if !slices.Equal(expectedData[:v3.DatagramPayloadHeaderLen+len(payload)], data) {
|
||||
t.Fatal("expected datagram did not equal expected")
|
||||
// Write from the origin server to the eyeball
|
||||
go func() {
|
||||
for _, payload := range payloads {
|
||||
_, _ = server.Write(payload)
|
||||
}
|
||||
}()
|
||||
|
||||
// Read from the eyeball to ensure the payloads were received (in-order)
|
||||
for i, payload := range payloads {
|
||||
select {
|
||||
case data := <-eyeball.recvData:
|
||||
// check received data matches provided from origin
|
||||
expectedData := makePayload(1500)
|
||||
_ = v3.MarshalPayloadHeaderTo(testRequestID, expectedData[:])
|
||||
copy(expectedData[17:], payload)
|
||||
if !slices.Equal(expectedData[:v3.DatagramPayloadHeaderLen+len(payload)], data) {
|
||||
t.Fatalf("expected datagram[%d] did not equal expected", i)
|
||||
}
|
||||
case err := <-ctx.Done():
|
||||
// we expect the payload to return before the context to cancel on the session
|
||||
t.Fatal(err)
|
||||
}
|
||||
cancel(errExpectedContextCanceled)
|
||||
case err := <-ctx.Done():
|
||||
// we expect the payload to return before the context to cancel on the session
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = <-done
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !errors.Is(context.Cause(ctx), errExpectedContextCanceled) {
|
||||
t.Fatal(err)
|
||||
assertContextClosed(t, ctx, done, cancel)
|
||||
}
|
||||
|
||||
func TestSessionRead(t *testing.T) {
|
||||
defer leaktest.Check(t)()
|
||||
for i := range 1280 {
|
||||
payloads := makePayloads(i, 16)
|
||||
testSessionRead(t, payloads)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionServe_OriginTooLarge(t *testing.T) {
|
||||
func TestSessionRead_OriginTooLarge(t *testing.T) {
|
||||
defer leaktest.Check(t)()
|
||||
log := zerolog.Nop()
|
||||
eyeball := newMockEyeball()
|
||||
@@ -317,6 +323,8 @@ func TestSessionServe_IdleTimeout(t *testing.T) {
|
||||
closeAfterIdle := 2 * time.Second
|
||||
session := v3.NewSession(testRequestID, closeAfterIdle, origin, testOriginAddr, testLocalAddr, &noopEyeball{}, &noopMetrics{}, &log)
|
||||
err := session.Serve(t.Context())
|
||||
|
||||
// Session should idle timeout if no reads or writes occur
|
||||
if !errors.Is(err, v3.SessionIdleErr{}) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
+97
-26
@@ -50,7 +50,7 @@ class PkgUploader:
|
||||
config=config,
|
||||
)
|
||||
|
||||
print(f"uploading asset: {filename} to {upload_file_path} in bucket{self.bucket_name}...")
|
||||
print(f"uploading asset: {filename} to {upload_file_path} in bucket {self.bucket_name}...")
|
||||
try:
|
||||
r2.upload_file(filename, self.bucket_name, upload_file_path)
|
||||
except ClientError as e:
|
||||
@@ -133,16 +133,19 @@ class PkgCreator:
|
||||
"""
|
||||
|
||||
def create_repo_file(self, file_path, binary_name, baseurl, gpgkey_url):
|
||||
with open(os.path.join(file_path, binary_name + '.repo'), "w+") as repo_file:
|
||||
repo_file.write(f"[{binary_name}-stable]")
|
||||
repo_file.write(f"{binary_name}-stable")
|
||||
repo_file.write(f"baseurl={baseurl}/rpm")
|
||||
repo_file.write("enabled=1")
|
||||
repo_file.write("type=rpm")
|
||||
repo_file.write("gpgcheck=1")
|
||||
repo_file.write(f"gpgkey={gpgkey_url}")
|
||||
repo_file_path = os.path.join(file_path, binary_name + '.repo')
|
||||
with open(repo_file_path, "w+") as repo_file:
|
||||
repo_file.write(f"[{binary_name}-stable]\n")
|
||||
repo_file.write(f"name={binary_name}-stable\n")
|
||||
repo_file.write(f"baseurl={baseurl}/rpm\n")
|
||||
repo_file.write("enabled=1\n")
|
||||
repo_file.write("type=rpm\n")
|
||||
repo_file.write("gpgcheck=1\n")
|
||||
repo_file.write(f"gpgkey={gpgkey_url}\n")
|
||||
return repo_file_path
|
||||
|
||||
def _sign_rpms(self, file_path):
|
||||
|
||||
def _sign_rpms(self, file_path, gpg_key_name):
|
||||
p = Popen(["rpm", "--define", f"_gpg_name {gpg_key_name}", "--addsign", file_path], stdout=PIPE, stderr=PIPE)
|
||||
out, err = p.communicate()
|
||||
if p.returncode != 0:
|
||||
@@ -150,7 +153,7 @@ class PkgCreator:
|
||||
raise
|
||||
|
||||
def _sign_repomd(self):
|
||||
p = Popen(["gpg", "--batch", "--detach-sign", "--armor", "./rpm/repodata/repomd.xml"], stdout=PIPE, stderr=PIPE)
|
||||
p = Popen(["gpg", "--batch", "--yes", "--detach-sign", "--armor", "./rpm/repodata/repomd.xml"], stdout=PIPE, stderr=PIPE)
|
||||
out, err = p.communicate()
|
||||
if p.returncode != 0:
|
||||
print(f"sign repomd result => {out}, {err}")
|
||||
@@ -176,7 +179,7 @@ class PkgCreator:
|
||||
old_path = os.path.join(root, file)
|
||||
new_path = os.path.join(new_dir, file)
|
||||
shutil.copyfile(old_path, new_path)
|
||||
self._sign_rpms(new_path)
|
||||
self._sign_rpms(new_path, gpg_key_name)
|
||||
|
||||
"""
|
||||
imports gpg keys into the system so reprepro and createrepo can use it to sign packages.
|
||||
@@ -186,11 +189,34 @@ class PkgCreator:
|
||||
def import_gpg_keys(self, private_key, public_key):
|
||||
gpg = gnupg.GPG()
|
||||
private_key = base64.b64decode(private_key)
|
||||
gpg.import_keys(private_key)
|
||||
import_result = gpg.import_keys(private_key)
|
||||
if not import_result.fingerprints:
|
||||
raise Exception("Failed to import private key")
|
||||
|
||||
public_key = base64.b64decode(public_key)
|
||||
gpg.import_keys(public_key)
|
||||
|
||||
imported_fingerprint = import_result.fingerprints[0]
|
||||
data = gpg.list_keys(secret=True)
|
||||
return (data[0]["fingerprint"], data[0]["uids"][0])
|
||||
|
||||
# Find the specific key we just imported by comparing fingerprints
|
||||
for key in data:
|
||||
if key["fingerprint"] == imported_fingerprint:
|
||||
return (key["fingerprint"], key["uids"][0])
|
||||
|
||||
raise Exception(f"Could not find imported key with fingerprint {imported_fingerprint}")
|
||||
|
||||
def import_multiple_gpg_keys(self, primary_private_key, primary_public_key, secondary_private_key=None, secondary_public_key=None):
|
||||
"""
|
||||
Import one or two GPG keypairs. Returns a list of (fingerprint, uid) with the primary first.
|
||||
"""
|
||||
results = []
|
||||
if primary_private_key and primary_public_key:
|
||||
results.append(self.import_gpg_keys(primary_private_key, primary_public_key))
|
||||
if secondary_private_key and secondary_public_key:
|
||||
# Ensure secondary is imported and appended
|
||||
results.append(self.import_gpg_keys(secondary_private_key, secondary_public_key))
|
||||
return results
|
||||
|
||||
"""
|
||||
basically rpm --import <key_file>
|
||||
@@ -247,11 +273,13 @@ def upload_from_directories(pkg_uploader, directory, release, binary):
|
||||
"""
|
||||
|
||||
|
||||
def create_deb_packaging(pkg_creator, pkg_uploader, releases, gpg_key_id, binary_name, archs, package_component,
|
||||
def create_deb_packaging(pkg_creator, pkg_uploader, releases, primary_gpg_key_id, secondary_gpg_key_id, binary_name, archs, package_component,
|
||||
release_version):
|
||||
# set configuration for package creation.
|
||||
print(f"initialising configuration for {binary_name} , {archs}")
|
||||
Path("./conf").mkdir(parents=True, exist_ok=True)
|
||||
# If in rollover mode (secondary provided), tell reprepro to sign with both keys.
|
||||
sign_with_ids = primary_gpg_key_id if not secondary_gpg_key_id else f"{primary_gpg_key_id} {secondary_gpg_key_id}"
|
||||
pkg_creator.create_distribution_conf(
|
||||
"./conf/distributions",
|
||||
binary_name,
|
||||
@@ -260,7 +288,7 @@ def create_deb_packaging(pkg_creator, pkg_uploader, releases, gpg_key_id, binary
|
||||
archs,
|
||||
package_component,
|
||||
f"apt repository for {binary_name}",
|
||||
gpg_key_id)
|
||||
sign_with_ids)
|
||||
|
||||
# create deb pkgs
|
||||
for release in releases:
|
||||
@@ -287,15 +315,19 @@ def create_rpm_packaging(
|
||||
gpg_key_name,
|
||||
base_url,
|
||||
gpg_key_url,
|
||||
upload_repo_file=False,
|
||||
):
|
||||
print(f"creating rpm pkgs...")
|
||||
pkg_creator.create_rpm_pkgs(artifacts_path, gpg_key_name)
|
||||
pkg_creator.create_repo_file(artifacts_path, binary_name, base_url, gpg_key_url)
|
||||
repo_file = pkg_creator.create_repo_file(artifacts_path, binary_name, base_url, gpg_key_url)
|
||||
|
||||
print("Uploading repo file")
|
||||
pkg_uploader.upload_pkg_to_r2(repo_file, binary_name + ".repo")
|
||||
|
||||
print("uploading latest to r2...")
|
||||
upload_from_directories(pkg_uploader, "rpm", None, binary_name)
|
||||
|
||||
if release_version:
|
||||
if upload_repo_file:
|
||||
print(f"uploading versioned release {release_version} to r2...")
|
||||
upload_from_directories(pkg_uploader, "rpm", release_version, binary_name)
|
||||
|
||||
@@ -336,18 +368,29 @@ def parse_args():
|
||||
signing packages"
|
||||
)
|
||||
|
||||
# Optional secondary keypair for key rollover
|
||||
parser.add_argument(
|
||||
"--gpg-private-key-2", default=os.environ.get("LINUX_SIGNING_PRIVATE_KEY_2"), help="Secondary GPG private key for rollover"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gpg-public-key-2", default=os.environ.get("LINUX_SIGNING_PUBLIC_KEY_2"), help="Secondary GPG public key for rollover"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--gpg-public-key-url", default=os.environ.get("GPG_PUBLIC_KEY_URL"), help="GPG public key url that\
|
||||
downloaders can use to verify signing"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--gpg-public-key-url-2", default=os.environ.get("GPG_PUBLIC_KEY_URL_2"), help="Secondary GPG public key url for rollover"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--pkg-upload-url", default=os.environ.get("PKG_URL"), help="URL to be used by downloaders"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--deb-based-releases", default=["any", "bookworm", "bullseye", "buster", "noble", "jammy", "impish", "focal", "bionic",
|
||||
"xenial", "trusty"],
|
||||
"--deb-based-releases", default=["any", "bookworm", "noble", "jammy", "focal", "bionic", "xenial"],
|
||||
help="list of debian based releases that need to be packaged for"
|
||||
)
|
||||
|
||||
@@ -356,6 +399,10 @@ def parse_args():
|
||||
it is the caller's responsiblity to ensure that these debs are already present in a directory. This script\
|
||||
will not build binaries or create their debs."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--upload-repo-file", action='store_true', help="Upload RPM repo file to R2"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
return args
|
||||
@@ -369,13 +416,36 @@ if __name__ == "__main__":
|
||||
exit(1)
|
||||
|
||||
pkg_creator = PkgCreator()
|
||||
(gpg_key_id, gpg_key_name) = pkg_creator.import_gpg_keys(args.gpg_private_key, args.gpg_public_key)
|
||||
# Import one or two keypairs; primary first
|
||||
key_results = pkg_creator.import_multiple_gpg_keys(
|
||||
args.gpg_private_key,
|
||||
args.gpg_public_key,
|
||||
args.gpg_private_key_2,
|
||||
args.gpg_public_key_2,
|
||||
)
|
||||
if not key_results or len(key_results) == 0:
|
||||
raise SystemExit("No GPG keys were provided for signing")
|
||||
primary_gpg_key_id, primary_gpg_key_name = key_results[0]
|
||||
secondary_gpg_key_id = None
|
||||
secondary_gpg_key_name = None
|
||||
if len(key_results) > 1:
|
||||
secondary_gpg_key_id, secondary_gpg_key_name = key_results[1]
|
||||
# Import RPM public keys (one or two)
|
||||
pkg_creator.import_rpm_key(args.gpg_public_key)
|
||||
|
||||
pkg_uploader = PkgUploader(args.account, args.bucket, args.id, args.secret)
|
||||
print(f"signing with gpg_key: {gpg_key_id}")
|
||||
create_deb_packaging(pkg_creator, pkg_uploader, args.deb_based_releases, gpg_key_id, args.binary, args.archs,
|
||||
"main", args.release_tag)
|
||||
print(f"signing with primary gpg_key: {primary_gpg_key_id} and secondary gpg_key: {secondary_gpg_key_id}")
|
||||
create_deb_packaging(
|
||||
pkg_creator,
|
||||
pkg_uploader,
|
||||
args.deb_based_releases,
|
||||
primary_gpg_key_id,
|
||||
secondary_gpg_key_id,
|
||||
args.binary,
|
||||
args.archs,
|
||||
"main",
|
||||
args.release_tag,
|
||||
)
|
||||
|
||||
create_rpm_packaging(
|
||||
pkg_creator,
|
||||
@@ -383,7 +453,8 @@ if __name__ == "__main__":
|
||||
"./built_artifacts",
|
||||
args.release_tag,
|
||||
args.binary,
|
||||
gpg_key_name,
|
||||
args.gpg_public_key_url,
|
||||
primary_gpg_key_name,
|
||||
args.pkg_upload_url,
|
||||
args.gpg_public_key_url,
|
||||
args.upload_repo_file,
|
||||
)
|
||||
|
||||
@@ -17,8 +17,8 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
nonFipsPostQuantumStrictPKex []tls.CurveID = []tls.CurveID{X25519MLKEM768PQKex, X25519Kyber768Draft00PQKex}
|
||||
nonFipsPostQuantumPreferPKex []tls.CurveID = []tls.CurveID{X25519MLKEM768PQKex, X25519Kyber768Draft00PQKex}
|
||||
nonFipsPostQuantumStrictPKex []tls.CurveID = []tls.CurveID{X25519MLKEM768PQKex}
|
||||
nonFipsPostQuantumPreferPKex []tls.CurveID = []tls.CurveID{X25519MLKEM768PQKex}
|
||||
fipsPostQuantumStrictPKex []tls.CurveID = []tls.CurveID{P256Kyber768Draft00PQKex}
|
||||
fipsPostQuantumPreferPKex []tls.CurveID = []tls.CurveID{P256Kyber768Draft00PQKex, tls.CurveP256}
|
||||
)
|
||||
|
||||
@@ -2,12 +2,16 @@ package supervisor
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cloudflare/cloudflared/features"
|
||||
"github.com/cloudflare/cloudflared/fips"
|
||||
)
|
||||
|
||||
func TestCurvePreferences(t *testing.T) {
|
||||
@@ -48,7 +52,7 @@ func TestCurvePreferences(t *testing.T) {
|
||||
pqMode: features.PostQuantumPrefer,
|
||||
fipsEnabled: false,
|
||||
currentCurves: []tls.CurveID{tls.CurveP256},
|
||||
expectedCurves: []tls.CurveID{X25519MLKEM768PQKex, X25519Kyber768Draft00PQKex, tls.CurveP256},
|
||||
expectedCurves: []tls.CurveID{X25519MLKEM768PQKex, tls.CurveP256},
|
||||
},
|
||||
{
|
||||
name: "Non FIPS with Prefer PQ - no duplicates",
|
||||
@@ -62,14 +66,14 @@ func TestCurvePreferences(t *testing.T) {
|
||||
pqMode: features.PostQuantumPrefer,
|
||||
fipsEnabled: false,
|
||||
currentCurves: []tls.CurveID{tls.CurveP256, X25519Kyber768Draft00PQKex},
|
||||
expectedCurves: []tls.CurveID{X25519MLKEM768PQKex, X25519Kyber768Draft00PQKex, tls.CurveP256},
|
||||
expectedCurves: []tls.CurveID{X25519MLKEM768PQKex, tls.CurveP256, X25519Kyber768Draft00PQKex},
|
||||
},
|
||||
{
|
||||
name: "Non FIPS with Strict PQ",
|
||||
pqMode: features.PostQuantumStrict,
|
||||
fipsEnabled: false,
|
||||
currentCurves: []tls.CurveID{tls.CurveP256},
|
||||
expectedCurves: []tls.CurveID{X25519MLKEM768PQKex, X25519Kyber768Draft00PQKex},
|
||||
expectedCurves: []tls.CurveID{X25519MLKEM768PQKex},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -82,3 +86,34 @@ func TestCurvePreferences(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runClientServerHandshake(t *testing.T, curves []tls.CurveID) []tls.CurveID {
|
||||
var advertisedCurves []tls.CurveID
|
||||
ts := httptest.NewUnstartedServer(nil)
|
||||
ts.TLS = &tls.Config{ // nolint: gosec
|
||||
GetConfigForClient: func(chi *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
advertisedCurves = slices.Clone(chi.SupportedCurves)
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
ts.StartTLS()
|
||||
defer ts.Close()
|
||||
clientTlsConfig := ts.Client().Transport.(*http.Transport).TLSClientConfig
|
||||
clientTlsConfig.CurvePreferences = curves
|
||||
resp, err := ts.Client().Head(ts.URL)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return advertisedCurves
|
||||
}
|
||||
|
||||
func TestSupportedCurvesNegotiation(t *testing.T) {
|
||||
for _, tcase := range []features.PostQuantumMode{features.PostQuantumPrefer} {
|
||||
curves, err := curvePreference(tcase, fips.IsFipsEnabled(), make([]tls.CurveID, 0))
|
||||
require.NoError(t, err)
|
||||
advertisedCurves := runClientServerHandshake(t, curves)
|
||||
assert.Equal(t, curves, advertisedCurves)
|
||||
}
|
||||
}
|
||||
|
||||
+13
-10
@@ -13,7 +13,6 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
"github.com/cloudflare/cloudflared/orchestration"
|
||||
v3 "github.com/cloudflare/cloudflared/quic/v3"
|
||||
"github.com/cloudflare/cloudflared/retry"
|
||||
@@ -78,7 +77,8 @@ func NewSupervisor(config *TunnelConfig, orchestrator *orchestration.Orchestrato
|
||||
edgeBindAddr := config.EdgeBindAddr
|
||||
|
||||
datagramMetrics := v3.NewMetrics(prometheus.DefaultRegisterer)
|
||||
sessionManager := v3.NewSessionManager(datagramMetrics, config.Log, ingress.DialUDPAddrPort, orchestrator.GetFlowLimiter())
|
||||
|
||||
sessionManager := v3.NewSessionManager(datagramMetrics, config.Log, config.OriginDialerService, orchestrator.GetFlowLimiter())
|
||||
|
||||
edgeTunnelServer := EdgeTunnelServer{
|
||||
config: config,
|
||||
@@ -125,10 +125,14 @@ func (s *Supervisor) Run(
|
||||
}()
|
||||
}
|
||||
|
||||
// Setup DNS Resolver refresh
|
||||
go s.config.OriginDNSService.StartRefreshLoop(ctx)
|
||||
|
||||
if err := s.initialize(ctx, connectedSignal); err != nil {
|
||||
if err == errEarlyShutdown {
|
||||
return nil
|
||||
}
|
||||
s.log.Logger().Error().Err(err).Msg("initial tunnel connection failed")
|
||||
return err
|
||||
}
|
||||
var tunnelsWaiting []int
|
||||
@@ -151,6 +155,7 @@ func (s *Supervisor) Run(
|
||||
// (note that this may also be caused by context cancellation)
|
||||
case tunnelError := <-s.tunnelErrors:
|
||||
tunnelsActive--
|
||||
s.log.ConnAwareLogger().Err(tunnelError.err).Int(connection.LogFieldConnIndex, tunnelError.index).Msg("Connection terminated")
|
||||
if tunnelError.err != nil && !shuttingDown {
|
||||
switch tunnelError.err.(type) {
|
||||
case ReconnectSignal:
|
||||
@@ -163,7 +168,6 @@ func (s *Supervisor) Run(
|
||||
if _, retry := s.tunnelsProtocolFallback[tunnelError.index].GetMaxBackoffDuration(ctx); !retry {
|
||||
continue
|
||||
}
|
||||
s.log.ConnAwareLogger().Err(tunnelError.err).Int(connection.LogFieldConnIndex, tunnelError.index).Msg("Connection terminated")
|
||||
tunnelsWaiting = append(tunnelsWaiting, tunnelError.index)
|
||||
s.waitForNextTunnel(tunnelError.index)
|
||||
|
||||
@@ -282,7 +286,10 @@ func (s *Supervisor) startFirstTunnel(
|
||||
*quic.IdleTimeoutError,
|
||||
*quic.ApplicationError,
|
||||
edgediscovery.DialError,
|
||||
*connection.EdgeQuicDialError:
|
||||
*connection.EdgeQuicDialError,
|
||||
*connection.ControlStreamError,
|
||||
*connection.StreamListenerError,
|
||||
*connection.DatagramManagerError:
|
||||
// Try again for these types of errors
|
||||
default:
|
||||
// Uncaught errors should bail startup
|
||||
@@ -298,13 +305,9 @@ func (s *Supervisor) startTunnel(
|
||||
index int,
|
||||
connectedSignal *signal.Signal,
|
||||
) {
|
||||
var err error
|
||||
defer func() {
|
||||
s.tunnelErrors <- tunnelError{index: index, err: err}
|
||||
}()
|
||||
|
||||
// nolint: gosec
|
||||
err = s.edgeTunnelServer.Serve(ctx, uint8(index), s.tunnelsProtocolFallback[index], connectedSignal)
|
||||
err := s.edgeTunnelServer.Serve(ctx, uint8(index), s.tunnelsProtocolFallback[index], connectedSignal)
|
||||
s.tunnelErrors <- tunnelError{index: index, err: err}
|
||||
}
|
||||
|
||||
func (s *Supervisor) newConnectedTunnelSignal(index int) *signal.Signal {
|
||||
|
||||
+11
-10
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/cloudflare/cloudflared/features"
|
||||
"github.com/cloudflare/cloudflared/fips"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
"github.com/cloudflare/cloudflared/ingress/origins"
|
||||
"github.com/cloudflare/cloudflared/management"
|
||||
"github.com/cloudflare/cloudflared/orchestration"
|
||||
quicpogs "github.com/cloudflare/cloudflared/quic"
|
||||
@@ -60,10 +61,12 @@ type TunnelConfig struct {
|
||||
|
||||
NeedPQ bool
|
||||
|
||||
NamedTunnel *connection.TunnelProperties
|
||||
ProtocolSelector connection.ProtocolSelector
|
||||
EdgeTLSConfigs map[connection.Protocol]*tls.Config
|
||||
ICMPRouterServer ingress.ICMPRouterServer
|
||||
NamedTunnel *connection.TunnelProperties
|
||||
ProtocolSelector connection.ProtocolSelector
|
||||
EdgeTLSConfigs map[connection.Protocol]*tls.Config
|
||||
ICMPRouterServer ingress.ICMPRouterServer
|
||||
OriginDNSService *origins.DNSResolverService
|
||||
OriginDialerService *ingress.OriginDialerService
|
||||
|
||||
RPCTimeout time.Duration
|
||||
WriteStreamTimeout time.Duration
|
||||
@@ -553,6 +556,7 @@ func (e *EdgeTunnelServer) serveQUIC(
|
||||
pqMode := connOptions.FeatureSnapshot.PostQuantum
|
||||
curvePref, err := curvePreference(pqMode, fips.IsFipsEnabled(), tlsConfig.CurvePreferences)
|
||||
if err != nil {
|
||||
connLogger.ConnAwareLogger().Err(err).Msgf("failed to get curve preferences")
|
||||
return err, true
|
||||
}
|
||||
|
||||
@@ -613,6 +617,7 @@ func (e *EdgeTunnelServer) serveQUIC(
|
||||
datagramSessionManager = connection.NewDatagramV2Connection(
|
||||
ctx,
|
||||
conn,
|
||||
e.config.OriginDialerService,
|
||||
e.config.ICMPRouterServer,
|
||||
connIndex,
|
||||
e.config.RPCTimeout,
|
||||
@@ -623,7 +628,7 @@ func (e *EdgeTunnelServer) serveQUIC(
|
||||
}
|
||||
|
||||
// Wrap the [quic.Connection] as a TunnelConnection
|
||||
tunnelConn, err := connection.NewTunnelConnection(
|
||||
tunnelConn := connection.NewTunnelConnection(
|
||||
ctx,
|
||||
conn,
|
||||
connIndex,
|
||||
@@ -636,17 +641,13 @@ func (e *EdgeTunnelServer) serveQUIC(
|
||||
e.config.GracePeriod,
|
||||
connLogger.Logger(),
|
||||
)
|
||||
if err != nil {
|
||||
connLogger.ConnAwareLogger().Err(err).Msgf("Failed to create new tunnel connection")
|
||||
return err, true
|
||||
}
|
||||
|
||||
// Serve the TunnelConnection
|
||||
errGroup, serveCtx := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
err := tunnelConn.Serve(serveCtx)
|
||||
if err != nil {
|
||||
connLogger.ConnAwareLogger().Err(err).Msg("Failed to serve tunnel connection")
|
||||
connLogger.ConnAwareLogger().Err(err).Msg("failed to serve tunnel connection")
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
+8
-8
@@ -185,18 +185,18 @@ func Init(version string) {
|
||||
|
||||
// FetchTokenWithRedirect will either load a stored token or generate a new one
|
||||
// it appends the full url as the redirect URL to the access cli request if opening the browser
|
||||
func FetchTokenWithRedirect(appURL *url.URL, appInfo *AppInfo, log *zerolog.Logger) (string, error) {
|
||||
return getToken(appURL, appInfo, false, log)
|
||||
func FetchTokenWithRedirect(appURL *url.URL, appInfo *AppInfo, autoClose bool, isFedramp bool, log *zerolog.Logger) (string, error) {
|
||||
return getToken(appURL, appInfo, false, autoClose, isFedramp, log)
|
||||
}
|
||||
|
||||
// FetchToken will either load a stored token or generate a new one
|
||||
// it appends the host of the appURL as the redirect URL to the access cli request if opening the browser
|
||||
func FetchToken(appURL *url.URL, appInfo *AppInfo, log *zerolog.Logger) (string, error) {
|
||||
return getToken(appURL, appInfo, true, log)
|
||||
func FetchToken(appURL *url.URL, appInfo *AppInfo, autoClose bool, isFedramp bool, log *zerolog.Logger) (string, error) {
|
||||
return getToken(appURL, appInfo, true, autoClose, isFedramp, log)
|
||||
}
|
||||
|
||||
// getToken will either load a stored token or generate a new one
|
||||
func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, log *zerolog.Logger) (string, error) {
|
||||
func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, autoClose bool, isFedramp bool, log *zerolog.Logger) (string, error) {
|
||||
if token, err := GetAppTokenIfExists(appInfo); token != "" && err == nil {
|
||||
return token, nil
|
||||
}
|
||||
@@ -249,18 +249,18 @@ func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, log *zerolog.
|
||||
return appToken, nil
|
||||
}
|
||||
}
|
||||
return getTokensFromEdge(appURL, appInfo.AppAUD, appTokenPath, orgTokenPath, useHostOnly, log)
|
||||
return getTokensFromEdge(appURL, appInfo.AppAUD, appTokenPath, orgTokenPath, useHostOnly, autoClose, isFedramp, log)
|
||||
}
|
||||
|
||||
// getTokensFromEdge will attempt to use the transfer service to retrieve an app and org token, save them to disk,
|
||||
// and return the app token.
|
||||
func getTokensFromEdge(appURL *url.URL, appAUD, appTokenPath, orgTokenPath string, useHostOnly bool, log *zerolog.Logger) (string, error) {
|
||||
func getTokensFromEdge(appURL *url.URL, appAUD, appTokenPath, orgTokenPath string, useHostOnly bool, autoClose bool, isFedramp bool, log *zerolog.Logger) (string, error) {
|
||||
// If no org token exists or if it couldn't be exchanged for an app token, then run the transfer service flow.
|
||||
|
||||
// this weird parameter is the resource name (token) and the key/value
|
||||
// we want to send to the transfer service. the key is token and the value
|
||||
// is blank (basically just the id generated in the transfer service)
|
||||
resourceData, err := RunTransfer(appURL, appAUD, keyName, keyName, "", true, useHostOnly, log)
|
||||
resourceData, err := RunTransfer(appURL, appAUD, keyName, keyName, "", true, useHostOnly, autoClose, isFedramp, log)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to run transfer service")
|
||||
}
|
||||
|
||||
+23
-7
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
const (
|
||||
baseStoreURL = "https://login.cloudflareaccess.org/"
|
||||
fedStoreURL = "https://login.fed.cloudflareaccess.org/"
|
||||
clientTimeout = time.Second * 60
|
||||
)
|
||||
|
||||
@@ -25,12 +26,12 @@ const (
|
||||
// The "dance" we refer to is building a HTTP request, opening that in a browser waiting for
|
||||
// the user to complete an action, while it long polls in the background waiting for an
|
||||
// action to be completed to download the resource.
|
||||
func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string, shouldEncrypt bool, useHostOnly bool, log *zerolog.Logger) ([]byte, error) {
|
||||
func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string, shouldEncrypt bool, useHostOnly bool, autoClose bool, fedramp bool, log *zerolog.Logger) ([]byte, error) {
|
||||
encrypterClient, err := NewEncrypter("cloudflared_priv.pem", "cloudflared_pub.pem")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
requestURL, err := buildRequestURL(transferURL, appAUD, key, value+encrypterClient.PublicKey(), shouldEncrypt, useHostOnly)
|
||||
requestURL, err := buildRequestURL(transferURL, appAUD, key, value+encrypterClient.PublicKey(), shouldEncrypt, useHostOnly, autoClose)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -45,8 +46,14 @@ func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string,
|
||||
|
||||
var resourceData []byte
|
||||
|
||||
storeURL := baseStoreURL
|
||||
|
||||
if fedramp {
|
||||
storeURL = fedStoreURL
|
||||
}
|
||||
|
||||
if shouldEncrypt {
|
||||
buf, key, err := transferRequest(baseStoreURL+"transfer/"+encrypterClient.PublicKey(), log)
|
||||
buf, key, err := transferRequest(storeURL+"transfer/"+encrypterClient.PublicKey(), log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -62,7 +69,7 @@ func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string,
|
||||
|
||||
resourceData = decrypted
|
||||
} else {
|
||||
buf, _, err := transferRequest(baseStoreURL+encrypterClient.PublicKey(), log)
|
||||
buf, _, err := transferRequest(storeURL+encrypterClient.PublicKey(), log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -75,7 +82,7 @@ func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string,
|
||||
// BuildRequestURL creates a request suitable for a resource transfer.
|
||||
// it will return a constructed url based off the base url and query key/value provided.
|
||||
// cli will build a url for cli transfer request.
|
||||
func buildRequestURL(baseURL *url.URL, appAUD string, key, value string, cli, useHostOnly bool) (string, error) {
|
||||
func buildRequestURL(baseURL *url.URL, appAUD string, key, value string, cli, useHostOnly bool, autoClose bool) (string, error) {
|
||||
q := baseURL.Query()
|
||||
q.Set(key, value)
|
||||
q.Set("aud", appAUD)
|
||||
@@ -90,7 +97,11 @@ func buildRequestURL(baseURL *url.URL, appAUD string, key, value string, cli, us
|
||||
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url and the main url
|
||||
q.Set("send_org_token", "true") // indicates that the cli endpoint should return both the org and app token
|
||||
q.Set("edge_token_transfer", "true") // use new LoginHelper service built on workers
|
||||
baseURL.RawQuery = q.Encode() // and this actual baseURL.
|
||||
if autoClose {
|
||||
q.Set("close_interstitial", "true") // Automatically close the success window.
|
||||
}
|
||||
|
||||
baseURL.RawQuery = q.Encode() // and this actual baseURL.
|
||||
baseURL.Path = "cdn-cgi/access/cli"
|
||||
return baseURL.String(), nil
|
||||
}
|
||||
@@ -127,7 +138,12 @@ func poll(client *http.Client, requestURL string, log *zerolog.Logger) ([]byte,
|
||||
// ignore everything other than server errors as the resource
|
||||
// may not exist until the user does the interaction
|
||||
if resp.StatusCode >= 500 {
|
||||
return nil, "", fmt.Errorf("error on request %d", resp.StatusCode)
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := io.Copy(buf, resp.Body); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return nil, "", fmt.Errorf("error on request %d: %s", resp.StatusCode, buf.String())
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
log.Info().Msg("Waiting for login...")
|
||||
|
||||
+12
-2
@@ -1,9 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## v5.0.12 (2024-02-16)
|
||||
|
||||
- History of changes: see https://github.com/go-chi/chi/compare/v5.0.11...v5.0.12
|
||||
|
||||
|
||||
## v5.0.11 (2023-12-19)
|
||||
|
||||
- History of changes: see https://github.com/go-chi/chi/compare/v5.0.10...v5.0.11
|
||||
|
||||
|
||||
## v5.0.10 (2023-07-13)
|
||||
|
||||
- Fixed small edge case in tests of v5.0.9 for older Go versions
|
||||
- History of changes: see https://github.com/go-chi/chi/compare/v5.0.8...v5.0.10
|
||||
- History of changes: see https://github.com/go-chi/chi/compare/v5.0.9...v5.0.10
|
||||
|
||||
|
||||
## v5.0.9 (2023-07-13)
|
||||
@@ -306,7 +316,7 @@ Cheers all, happy coding!
|
||||
request-scoped values. We're very excited about the new context addition and are proud to
|
||||
introduce chi v2, a minimal and powerful routing package for building large HTTP services,
|
||||
with zero external dependencies. Chi focuses on idiomatic design and encourages the use of
|
||||
stdlib HTTP handlers and middlwares.
|
||||
stdlib HTTP handlers and middlewares.
|
||||
- chi v2 deprecates its `chi.Handler` interface and requires `http.Handler` or `http.HandlerFunc`
|
||||
- chi v2 stores URL routing parameters and patterns in the standard request context: `r.Context()`
|
||||
- chi v2 lower-level routing context is accessible by `chi.RouteContext(r.Context()) *chi.Context`,
|
||||
|
||||
+6
-6
@@ -14,7 +14,7 @@
|
||||
|
||||
A typical workflow is:
|
||||
|
||||
1. [Fork the repository.][fork] [This tip maybe also helpful.][go-fork-tip]
|
||||
1. [Fork the repository.][fork]
|
||||
2. [Create a topic branch.][branch]
|
||||
3. Add tests for your change.
|
||||
4. Run `go test`. If your tests pass, return to the step 3.
|
||||
@@ -24,8 +24,8 @@ A typical workflow is:
|
||||
8. [Submit a pull request.][pull-req]
|
||||
|
||||
[go-install]: https://golang.org/doc/install
|
||||
[go-fork-tip]: http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html
|
||||
[fork]: https://help.github.com/articles/fork-a-repo
|
||||
[branch]: http://learn.github.com/p/branching.html
|
||||
[git-help]: https://guides.github.com
|
||||
[pull-req]: https://help.github.com/articles/using-pull-requests
|
||||
[fork]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo
|
||||
[branch]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches
|
||||
[git-help]: https://docs.github.com/en
|
||||
[pull-req]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests
|
||||
|
||||
|
||||
+10
-5
@@ -20,7 +20,9 @@ and [docgen](https://github.com/go-chi/docgen). We hope you enjoy it too!
|
||||
|
||||
## Install
|
||||
|
||||
`go get -u github.com/go-chi/chi/v5`
|
||||
```sh
|
||||
go get -u github.com/go-chi/chi/v5
|
||||
```
|
||||
|
||||
|
||||
## Features
|
||||
@@ -65,7 +67,7 @@ func main() {
|
||||
|
||||
**REST Preview:**
|
||||
|
||||
Here is a little preview of how routing looks like with chi. Also take a look at the generated routing docs
|
||||
Here is a little preview of what routing looks like with chi. Also take a look at the generated routing docs
|
||||
in JSON ([routes.json](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.json)) and in
|
||||
Markdown ([routes.md](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.md)).
|
||||
|
||||
@@ -194,7 +196,7 @@ type Router interface {
|
||||
// path, with a fresh middleware stack for the inline-Router.
|
||||
Group(fn func(r Router)) Router
|
||||
|
||||
// Route mounts a sub-Router along a `pattern`` string.
|
||||
// Route mounts a sub-Router along a `pattern` string.
|
||||
Route(pattern string, fn func(r Router)) Router
|
||||
|
||||
// Mount attaches another http.Handler along ./pattern/*
|
||||
@@ -354,6 +356,7 @@ with `net/http` can be used with chi's mux.
|
||||
| [RouteHeaders] | Route handling for request headers |
|
||||
| [SetHeader] | Short-hand middleware to set a response header key/value |
|
||||
| [StripSlashes] | Strip slashes on routing paths |
|
||||
| [Sunset] | Sunset set Deprecation/Sunset header to response |
|
||||
| [Throttle] | Puts a ceiling on the number of concurrent requests |
|
||||
| [Timeout] | Signals to the request context when the timeout deadline is reached |
|
||||
| [URLFormat] | Parse extension from url and put it on request context |
|
||||
@@ -380,6 +383,7 @@ with `net/http` can be used with chi's mux.
|
||||
[RouteHeaders]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RouteHeaders
|
||||
[SetHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#SetHeader
|
||||
[StripSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#StripSlashes
|
||||
[Sunset]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#Sunset
|
||||
[Throttle]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Throttle
|
||||
[ThrottleBacklog]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleBacklog
|
||||
[ThrottleWithOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleWithOpts
|
||||
@@ -467,7 +471,8 @@ how setting context on a request in Go works.
|
||||
|
||||
* Carl Jackson for https://github.com/zenazn/goji
|
||||
* Parts of chi's thinking comes from goji, and chi's middleware package
|
||||
sources from goji.
|
||||
sources from [goji](https://github.com/zenazn/goji/tree/master/web/middleware).
|
||||
* Please see goji's [LICENSE](https://github.com/zenazn/goji/blob/master/LICENSE) (MIT)
|
||||
* Armon Dadgar for https://github.com/armon/go-radix
|
||||
* Contributions: [@VojtechVitek](https://github.com/VojtechVitek)
|
||||
|
||||
@@ -494,7 +499,7 @@ Copyright (c) 2015-present [Peter Kieltyka](https://github.com/pkieltyka)
|
||||
|
||||
Licensed under [MIT License](./LICENSE)
|
||||
|
||||
[GoDoc]: https://pkg.go.dev/github.com/go-chi/chi?tab=versions
|
||||
[GoDoc]: https://pkg.go.dev/github.com/go-chi/chi/v5
|
||||
[GoDoc Widget]: https://godoc.org/github.com/go-chi/chi?status.svg
|
||||
[Travis]: https://travis-ci.org/go-chi/chi
|
||||
[Travis Widget]: https://travis-ci.org/go-chi/chi.svg?branch=master
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
# Reporting Security Issues
|
||||
|
||||
We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
|
||||
|
||||
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/go-chi/chi/security/advisories/new) tab.
|
||||
+6
-3
@@ -37,8 +37,7 @@
|
||||
//
|
||||
// A placeholder with a name followed by a colon allows a regular
|
||||
// expression match, for example {number:\\d+}. The regular expression
|
||||
// syntax is Go's normal regexp RE2 syntax, except that regular expressions
|
||||
// including { or } are not supported, and / will never be
|
||||
// syntax is Go's normal regexp RE2 syntax, except that / will never be
|
||||
// matched. An anonymous regexp pattern is allowed, using an empty string
|
||||
// before the colon in the placeholder, such as {:\\d+}
|
||||
//
|
||||
@@ -51,7 +50,7 @@
|
||||
// "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/"
|
||||
// "/user/{name}/info" matches "/user/jsmith/info"
|
||||
// "/page/*" matches "/page/intro/latest"
|
||||
// "/page/{other}/index" also matches "/page/intro/latest"
|
||||
// "/page/{other}/latest" also matches "/page/intro/latest"
|
||||
// "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01"
|
||||
package chi
|
||||
|
||||
@@ -127,6 +126,10 @@ type Routes interface {
|
||||
// the method/path - similar to routing a http request, but without
|
||||
// executing the handler thereafter.
|
||||
Match(rctx *Context, method, path string) bool
|
||||
|
||||
// Find searches the routing tree for the pattern that matches
|
||||
// the method/path.
|
||||
Find(rctx *Context, method, path string) string
|
||||
}
|
||||
|
||||
// Middlewares type is a slice of standard middleware handlers with methods
|
||||
|
||||
+18
-13
@@ -60,7 +60,7 @@ type Context struct {
|
||||
URLParams RouteParams
|
||||
|
||||
// Route parameters matched for the current sub-router. It is
|
||||
// intentionally unexported so it cant be tampered.
|
||||
// intentionally unexported so it can't be tampered.
|
||||
routeParams RouteParams
|
||||
|
||||
// The endpoint routing pattern that matched the request URI path
|
||||
@@ -74,9 +74,8 @@ type Context struct {
|
||||
// patterns across a stack of sub-routers.
|
||||
RoutePatterns []string
|
||||
|
||||
// methodNotAllowed hint
|
||||
methodNotAllowed bool
|
||||
methodsAllowed []methodTyp // allowed methods in case of a 405
|
||||
methodNotAllowed bool
|
||||
}
|
||||
|
||||
// Reset a routing context to its initial state.
|
||||
@@ -92,6 +91,7 @@ func (x *Context) Reset() {
|
||||
x.routeParams.Keys = x.routeParams.Keys[:0]
|
||||
x.routeParams.Values = x.routeParams.Values[:0]
|
||||
x.methodNotAllowed = false
|
||||
x.methodsAllowed = x.methodsAllowed[:0]
|
||||
x.parentCtx = nil
|
||||
}
|
||||
|
||||
@@ -109,22 +109,27 @@ func (x *Context) URLParam(key string) string {
|
||||
// RoutePattern builds the routing pattern string for the particular
|
||||
// request, at the particular point during routing. This means, the value
|
||||
// will change throughout the execution of a request in a router. That is
|
||||
// why its advised to only use this value after calling the next handler.
|
||||
// why it's advised to only use this value after calling the next handler.
|
||||
//
|
||||
// For example,
|
||||
//
|
||||
// func Instrument(next http.Handler) http.Handler {
|
||||
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// next.ServeHTTP(w, r)
|
||||
// routePattern := chi.RouteContext(r.Context()).RoutePattern()
|
||||
// measure(w, r, routePattern)
|
||||
// })
|
||||
// }
|
||||
// func Instrument(next http.Handler) http.Handler {
|
||||
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// next.ServeHTTP(w, r)
|
||||
// routePattern := chi.RouteContext(r.Context()).RoutePattern()
|
||||
// measure(w, r, routePattern)
|
||||
// })
|
||||
// }
|
||||
func (x *Context) RoutePattern() string {
|
||||
if x == nil {
|
||||
return ""
|
||||
}
|
||||
routePattern := strings.Join(x.RoutePatterns, "")
|
||||
routePattern = replaceWildcards(routePattern)
|
||||
routePattern = strings.TrimSuffix(routePattern, "//")
|
||||
routePattern = strings.TrimSuffix(routePattern, "/")
|
||||
if routePattern != "/" {
|
||||
routePattern = strings.TrimSuffix(routePattern, "//")
|
||||
routePattern = strings.TrimSuffix(routePattern, "/")
|
||||
}
|
||||
return routePattern
|
||||
}
|
||||
|
||||
|
||||
+44
-10
@@ -107,12 +107,22 @@ func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) {
|
||||
// Handle adds the route `pattern` that matches any http method to
|
||||
// execute the `handler` http.Handler.
|
||||
func (mx *Mux) Handle(pattern string, handler http.Handler) {
|
||||
if method, rest, found := strings.Cut(pattern, " "); found {
|
||||
mx.Method(method, rest, handler)
|
||||
return
|
||||
}
|
||||
|
||||
mx.handle(mALL, pattern, handler)
|
||||
}
|
||||
|
||||
// HandleFunc adds the route `pattern` that matches any http method to
|
||||
// execute the `handlerFn` http.HandlerFunc.
|
||||
func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) {
|
||||
if method, rest, found := strings.Cut(pattern, " "); found {
|
||||
mx.Method(method, rest, handlerFn)
|
||||
return
|
||||
}
|
||||
|
||||
mx.handle(mALL, pattern, handlerFn)
|
||||
}
|
||||
|
||||
@@ -250,20 +260,19 @@ func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router {
|
||||
return im
|
||||
}
|
||||
|
||||
// Group creates a new inline-Mux with a fresh middleware stack. It's useful
|
||||
// Group creates a new inline-Mux with a copy of middleware stack. It's useful
|
||||
// for a group of handlers along the same routing path that use an additional
|
||||
// set of middlewares. See _examples/.
|
||||
func (mx *Mux) Group(fn func(r Router)) Router {
|
||||
im := mx.With().(*Mux)
|
||||
im := mx.With()
|
||||
if fn != nil {
|
||||
fn(im)
|
||||
}
|
||||
return im
|
||||
}
|
||||
|
||||
// Route creates a new Mux with a fresh middleware stack and mounts it
|
||||
// along the `pattern` as a subrouter. Effectively, this is a short-hand
|
||||
// call to Mount. See _examples/.
|
||||
// Route creates a new Mux and mounts it along the `pattern` as a subrouter.
|
||||
// Effectively, this is a short-hand call to Mount. See _examples/.
|
||||
func (mx *Mux) Route(pattern string, fn func(r Router)) Router {
|
||||
if fn == nil {
|
||||
panic(fmt.Sprintf("chi: attempting to Route() a nil subrouter on '%s'", pattern))
|
||||
@@ -352,19 +361,40 @@ func (mx *Mux) Middlewares() Middlewares {
|
||||
// Note: the *Context state is updated during execution, so manage
|
||||
// the state carefully or make a NewRouteContext().
|
||||
func (mx *Mux) Match(rctx *Context, method, path string) bool {
|
||||
return mx.Find(rctx, method, path) != ""
|
||||
}
|
||||
|
||||
// Find searches the routing tree for the pattern that matches
|
||||
// the method/path.
|
||||
//
|
||||
// Note: the *Context state is updated during execution, so manage
|
||||
// the state carefully or make a NewRouteContext().
|
||||
func (mx *Mux) Find(rctx *Context, method, path string) string {
|
||||
m, ok := methodMap[method]
|
||||
if !ok {
|
||||
return false
|
||||
return ""
|
||||
}
|
||||
|
||||
node, _, h := mx.tree.FindRoute(rctx, m, path)
|
||||
node, _, _ := mx.tree.FindRoute(rctx, m, path)
|
||||
pattern := rctx.routePattern
|
||||
|
||||
if node != nil {
|
||||
if node.subroutes == nil {
|
||||
e := node.endpoints[m]
|
||||
return e.pattern
|
||||
}
|
||||
|
||||
if node != nil && node.subroutes != nil {
|
||||
rctx.RoutePath = mx.nextRoutePath(rctx)
|
||||
return node.subroutes.Match(rctx, method, rctx.RoutePath)
|
||||
subPattern := node.subroutes.Find(rctx, method, rctx.RoutePath)
|
||||
if subPattern == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
pattern = strings.TrimSuffix(pattern, "/*")
|
||||
pattern += subPattern
|
||||
}
|
||||
|
||||
return h != nil
|
||||
return pattern
|
||||
}
|
||||
|
||||
// NotFoundHandler returns the default Mux 404 responder whenever a route
|
||||
@@ -441,6 +471,10 @@ func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Find the route
|
||||
if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil {
|
||||
if supportsPathValue {
|
||||
setPathValue(rctx, r)
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user